feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
/**
|
||||
* 画个标 · 核心:状态管理(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
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user