Files

467 lines
13 KiB
JavaScript

/**
* 画个标 · 核心:状态管理(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
};
})();