/** * 画个标 · 核心:状态管理(Store) * 职责:维护设计数据、元素 CRUD、撤销/重做、事件订阅、版本快照 */ const HGB_Store = (function () { 'use strict'; const CANVAS_W = 600; const CANVAS_H = 600; const MAX_HISTORY = 60; const LS_KEY = 'hgb_design'; const LS_VERSION_KEY = 'hgb_versions'; /* ---------- 默认项目 ---------- */ function defaultProject() { return { name: '我的品牌', bg: '#FFFFFF', elements: [ { id: uid(), type: 'icon', name: '学士帽', iconId: 'graduation-cap', svg: iconSvg('graduation-cap'), x: 210, y: 150, w: 180, h: 180, rotation: 0, fill: 'none', stroke: '#1B4F72', strokeWidth: 2, opacity: 1, visible: true, locked: false }, { id: uid(), type: 'text', name: '品牌中文名', text: '远渡教育', x: 130, y: 360, w: 340, h: 64, rotation: 0, fontSize: 48, fontWeight: 700, fontFamily: 'SimHei', letterSpacing: 8, fill: '#1B4F72', stroke: 'none', strokeWidth: 0, opacity: 1, visible: true, locked: false }, { id: uid(), type: 'text', name: '品牌英文名', text: 'FAR CROSS EDUCATION', x: 130, y: 430, w: 340, h: 32, rotation: 0, fontSize: 18, fontWeight: 400, fontFamily: 'Georgia', letterSpacing: 4, fill: '#2980B9', stroke: 'none', strokeWidth: 0, opacity: 1, visible: true, locked: false } ], selectedId: null, selectedIds: [], currentStyle: null }; } /* ---------- 工具 ---------- */ function uid() { return 'el_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); } function iconSvg(iconId) { // 行业模块图标 + 基础图形 for (const ind of HGB_Industries) { const ic = (ind.icons || []).find(i => i.id === iconId); if (ic) return ic.svg; } const ic = HGB_Icons.shapes.find(i => i.id === iconId); return ic ? ic.svg : ''; } /* ---------- 状态 ---------- */ let state = defaultProject(); let history = []; let historyIdx = -1; let listeners = {}; /* ---------- 历史记录 ---------- */ function snapshot() { return JSON.parse(JSON.stringify(state)); } function pushHistory() { // 移除 redo 分支 history = history.slice(0, historyIdx + 1); history.push(snapshot()); if (history.length > MAX_HISTORY) history.shift(); historyIdx = history.length - 1; emit('history'); } function undo() { if (historyIdx <= 0) return false; historyIdx--; state = JSON.parse(JSON.stringify(history[historyIdx])); emit('change'); return true; } function redo() { if (historyIdx >= history.length - 1) return false; historyIdx++; state = JSON.parse(JSON.stringify(history[historyIdx])); emit('change'); return true; } function canUndo() { return historyIdx > 0; } function canRedo() { return historyIdx < history.length - 1; } /* ---------- 元素操作 ---------- */ function addElement(el) { state.elements.push(el); state.selectedId = el.id; pushHistory(); emit('change'); return el; } function updateElement(id, patch, record = true) { const el = state.elements.find(e => e.id === id); if (!el) return; Object.assign(el, patch); if (record) pushHistory(); emit('change'); } function removeElement(id) { state.elements = state.elements.filter(e => e.id !== id); if (state.selectedId === id) state.selectedId = null; pushHistory(); emit('change'); } /** 载入智能方案(替换全部元素与背景) */ function loadPlan(plan) { state.elements = (plan.elements || []).map(e => ({ ...e })); state.bg = plan.bg || '#FFFFFF'; state.selectedId = null; pushHistory(); emit('change'); } function duplicateElement(id) { const src = state.elements.find(e => e.id === id); if (!src) return null; const copy = JSON.parse(JSON.stringify(src)); copy.id = uid(); copy.name = src.name + ' 副本'; copy.x += 24; copy.y += 24; // 副本放置到顶层 state.elements.push(copy); state.selectedId = copy.id; pushHistory(); emit('change'); return copy; } function select(id, additive) { if (additive && id != null) { // Shift 多选:切换选中 if (state.selectedIds.includes(id)) { state.selectedIds = state.selectedIds.filter(x => x !== id); state.selectedId = state.selectedIds.length ? state.selectedIds[state.selectedIds.length - 1] : null; } else { state.selectedIds.push(id); state.selectedId = id; } } else { state.selectedIds = id != null ? [id] : []; state.selectedId = id; } emit('select'); emit('change'); } /** 当前所有选中元素 */ function selectedElements() { return state.selectedIds.map(id => state.elements.find(e => e.id === id)).filter(Boolean); } /** 移动全部选中元素 */ function moveSelection(dx, dy) { for (const el of selectedElements()) { el.x = Math.round(el.x + dx); el.y = Math.round(el.y + dy); } emit('change'); } /** 对齐全部选中元素 */ function alignSelection(mode) { const els = selectedElements(); if (els.length < 2) return; const xs = els.map(e => e.x), xes = els.map(e => e.x + e.w); const ys = els.map(e => e.y), yes = els.map(e => e.y + e.h); const minX = Math.min(...xs), maxX = Math.max(...xes); const minY = Math.min(...ys), maxY = Math.max(...yes); const boxCx = (minX + maxX) / 2, boxCy = (minY + maxY) / 2; for (const el of els) { if (mode === 'left') el.x = minX; else if (mode === 'center') el.x = Math.round(boxCx - el.w / 2); else if (mode === 'right') el.x = Math.round(maxX - el.w); else if (mode === 'top') el.y = minY; else if (mode === 'middle') el.y = Math.round(boxCy - el.h / 2); else if (mode === 'bottom') el.y = Math.round(maxY - el.h); } pushHistory(); emit('change'); } /** 复制全部选中元素 */ function duplicateSelection() { const srcs = selectedElements(); if (!srcs.length) return 0; for (const src of srcs) { const copy = JSON.parse(JSON.stringify(src)); copy.id = uid(); copy.name = src.name + ' 副本'; copy.x += 24; copy.y += 24; state.elements.push(copy); state.selectedIds.push(copy.id); state.selectedId = copy.id; } pushHistory(); emit('change'); return srcs.length; } /** 粘贴剪贴板元素(生成新副本并选中) */ function pasteElements(list) { if (!list || !list.length) return []; const newEls = list.map(src => { const copy = JSON.parse(JSON.stringify(src)); copy.id = uid(); copy.name = src.name + ' 副本'; copy.x += 24; copy.y += 24; state.elements.push(copy); state.selectedIds.push(copy.id); state.selectedId = copy.id; return copy; }); pushHistory(); emit('change'); return newEls; } /** 删除全部选中元素 */ function removeSelection() { if (!state.selectedIds.length) return 0; const ids = state.selectedIds.slice(); state.elements = state.elements.filter(e => !ids.includes(e.id)); state.selectedIds = []; state.selectedId = null; pushHistory(); emit('change'); return ids.length; } /* ---------- 图层排序 ---------- */ // elements 数组顺序即显示顺序(后画在上层) function layerUp(id) { const i = state.elements.findIndex(e => e.id === id); if (i < 0 || i >= state.elements.length - 1) return; [state.elements[i], state.elements[i + 1]] = [state.elements[i + 1], state.elements[i]]; pushHistory(); emit('change'); } function layerDown(id) { const i = state.elements.findIndex(e => e.id === id); if (i <= 0) return; [state.elements[i], state.elements[i - 1]] = [state.elements[i - 1], state.elements[i]]; pushHistory(); emit('change'); } function layerFront(id) { const el = state.elements.find(e => e.id === id); if (!el) return; state.elements = state.elements.filter(e => e.id !== id); state.elements.push(el); pushHistory(); emit('change'); } function layerBack(id) { const el = state.elements.find(e => e.id === id); if (!el) return; state.elements = state.elements.filter(e => e.id !== id); state.elements.unshift(el); pushHistory(); emit('change'); } /** 拖拽图层:将 fromIndex 元素移动到 toIndex 位置(占据目标位置,其余元素顺移) */ function moveLayer(fromIndex, toIndex) { const len = state.elements.length; if (fromIndex < 0 || fromIndex >= len || toIndex < 0 || toIndex >= len) return; if (fromIndex === toIndex) return; const [el] = state.elements.splice(fromIndex, 1); state.elements.splice(toIndex, 0, el); pushHistory(); emit('change'); } /* ---------- 风格应用 ---------- */ function applyStyle(style) { state.currentStyle = style.id; state.bg = style.bg; for (const el of state.elements) { if (el.type === 'icon') { el.stroke = style.iconStroke; el.strokeWidth = style.strokeWidth; } else if (el.type === 'text') { el.fill = style.colors.primary; if (el.fontFamily === 'SimHei' || el.fontFamily === 'Microsoft YaHei' || el.fontFamily === '黑体') { el.fontFamily = style.fontCn; } } } pushHistory(); emit('change'); emit('style'); } /* ---------- 配色应用 ---------- */ function applyPalette(pal) { let idx = 0; for (const el of state.elements) { if (el.type === 'icon') { el.stroke = pal.colors[Math.min(idx, 1)]; idx++; } else if (el.type === 'text') { el.fill = pal.colors[0]; } } pushHistory(); emit('change'); emit('style'); } /* ---------- 字体应用 ---------- */ function applyFont(family, web) { const el = selected(); if (!el || el.type !== 'text') return; if (web) HGB_Fonts.loadWebFont(family); updateElement(el.id, { fontFamily: family }); } /* ---------- 版本快照 ---------- */ function saveVersion() { const versions = loadVersions(); const v = { id: uid(), time: Date.now(), name: state.name || '未命名设计', snapshot: snapshot() }; versions.unshift(v); saveVersions(versions); emit('versions'); return v; } function loadVersions() { try { return JSON.parse(localStorage.getItem(LS_VERSION_KEY)) || []; } catch (e) { return []; } } function saveVersions(list) { localStorage.setItem(LS_VERSION_KEY, JSON.stringify(list.slice(0, 50))); } function restoreVersion(id) { const v = loadVersions().find(x => x.id === id); if (!v) return false; state = JSON.parse(JSON.stringify(v.snapshot)); pushHistory(); emit('change'); return true; } function deleteVersion(id) { saveVersions(loadVersions().filter(x => x.id !== id)); emit('versions'); } function clearVersions() { saveVersions([]); emit('versions'); } /* ---------- 事件订阅 ---------- */ function on(evt, fn) { (listeners[evt] = listeners[evt] || []).push(fn); } function emit(evt) { (listeners[evt] || []).forEach(fn => fn()); } /* ---------- 选择器 ---------- */ function get() { return state; } function selected() { return state.elements.find(e => e.id === state.selectedId) || null; } function selectedIndex() { return state.elements.findIndex(e => e.id === state.selectedId); } /* ---------- 本地持久化 ---------- */ function persistToLocal() { try { const data = { ...snapshot(), savedAt: Date.now() }; localStorage.setItem(LS_KEY, JSON.stringify(data)); return true; } catch (e) { return false; } } function loadFromLocal() { try { const raw = localStorage.getItem(LS_KEY); if (!raw) return false; const data = JSON.parse(raw); if (data && Array.isArray(data.elements)) { state = data; // 迁移遗留演示默认名:历史版本默认项目名「我的留学教育品牌」会污染各行业生成时的品牌回退 if (state.name === '我的留学教育品牌') state.name = '我的品牌'; history = [snapshot()]; historyIdx = 0; return true; } } catch (e) { /* ignore */ } return false; } /* ---------- 初始化 ---------- */ function init(restoreLocal = true) { if (restoreLocal && loadFromLocal()) return; state = defaultProject(); history = [snapshot()]; historyIdx = 0; } return { CANVAS_W, CANVAS_H, get, selected, selectedIndex, selectedElements, moveSelection, alignSelection, duplicateSelection, removeSelection, pasteElements, addElement, updateElement, removeElement, duplicateElement, loadPlan, select, layerUp, layerDown, layerFront, layerBack, moveLayer, applyStyle, applyPalette, applyFont, undo, redo, canUndo, canRedo, pushHistory, saveVersion, loadVersions, restoreVersion, deleteVersion, clearVersions, persistToLocal, loadFromLocal, init, on, emit, uid, iconSvg }; })();