1525 lines
68 KiB
JavaScript
1525 lines
68 KiB
JavaScript
/**
|
||
* 画个标 · UI:智能设计工作台
|
||
* 职责:行业/图标配置、方案批量生成、方案库网格、方案预览与变体微调、
|
||
* GitHub 资源导入、导出对话框、方案版本管理、Toast
|
||
*/
|
||
const HGB_UI = (function () {
|
||
'use strict';
|
||
|
||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||
let toastTimer = null;
|
||
|
||
/* ================= 工具 ================= */
|
||
/** HTML 转义(防止资源名称中的特殊字符破坏 DOM) */
|
||
function esc(s) {
|
||
return String(s).replace(/&/g, '&').replace(/</g, '<')
|
||
.replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
/** 提取完整 SVG 文档字符串中的 <svg> 标签(用于内联预览) */
|
||
function svgInline(svgStr) {
|
||
const div = document.createElement('div');
|
||
div.innerHTML = svgStr;
|
||
const svg = div.querySelector('svg');
|
||
return svg ? svg.outerHTML : '';
|
||
}
|
||
|
||
function toast(msg) {
|
||
const el = document.getElementById('toast');
|
||
el.textContent = msg;
|
||
el.classList.remove('hidden');
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => el.classList.add('hidden'), 2600);
|
||
}
|
||
|
||
/* ================= 状态 ================= */
|
||
let designState = {
|
||
industry: null, // 当前行业
|
||
icons: [], // 已选图标 [{id,name,svg}]
|
||
styles: [], // 已选风格方向 id(空 = 智能搭配全部)
|
||
plans: [], // 方案库
|
||
activePlan: null, // 当前预览/导出方案
|
||
activeLockup: 'below', // 预览弹窗中品牌组合的当前摆放位置
|
||
genParam: null, // 最近一次生成参数(供变体使用)
|
||
skills: [], // 启用的技能 id(生成时注入 AI 并执行后处理审计)
|
||
avatarMode: false // 头像生成模式(纯图形、圆形预览、平台规格导出)
|
||
};
|
||
const KIND_LABEL = { gen: '🎨 建议', check: '🔬 检查', deliver: '📦 交付' };
|
||
// 默认按设计流程自动启用前 3 步(洞察→生成→评审),迭代与交付导出按需手动开启
|
||
const DEFAULT_SKILLS = HGB_Skills.all().filter(s => HGB_Skills.stepOf(s.id) <= 3).map(s => s.id);
|
||
let exportFormat = 'png';
|
||
|
||
/* ================= 初始化 ================= */
|
||
function init() {
|
||
designState.industry = HGB_Industries.current();
|
||
designState.skills = DEFAULT_SKILLS.slice();
|
||
applyWorkspace();
|
||
// 默认启用技能按工作区过滤(头像纯图形 → 排除文字类技能)
|
||
designState.skills = designState.skills.filter(id => HGB_Skills.workspacesOf(id).includes(currentWorkspace()));
|
||
bindTopbar();
|
||
bindDesignWorkspace();
|
||
bindBuiltinModal();
|
||
bindDicebearModal();
|
||
bindAISettings();
|
||
bindPlanModal();
|
||
bindExportModal();
|
||
bindVersionActions();
|
||
renderDesignIndustries();
|
||
renderDesignIcons();
|
||
renderDesignStyles();
|
||
renderSkillChecks();
|
||
renderPlans();
|
||
renderVersions();
|
||
}
|
||
|
||
/* ================= 顶栏 ================= */
|
||
function bindTopbar() {
|
||
// 项目名(影响导出文件名与版本名)
|
||
const nameInput = document.getElementById('project-name');
|
||
nameInput.addEventListener('change', () => {
|
||
HGB_Store.get().name = nameInput.value || '我的品牌';
|
||
HGB_Store.persistToLocal();
|
||
});
|
||
|
||
// 导出(针对当前活动方案)
|
||
document.getElementById('btn-export-svg').addEventListener('click', () => {
|
||
if (!requireActivePlan()) return;
|
||
openExportModal('svg');
|
||
});
|
||
document.getElementById('btn-export-png').addEventListener('click', () => {
|
||
if (!requireActivePlan()) return;
|
||
openExportModal('png');
|
||
});
|
||
}
|
||
|
||
/* ================= 智能设计工作台 ================= */
|
||
function bindDesignWorkspace() {
|
||
// 生成方案
|
||
document.getElementById('btn-generate').addEventListener('click', generatePlans);
|
||
}
|
||
|
||
/** 渲染工作台内行业切换 */
|
||
function renderDesignIndustries() {
|
||
const wrap = document.getElementById('design-industry');
|
||
wrap.innerHTML = HGB_Industries.map(ind => `
|
||
<button class="industry-tab ${ind.id === designState.industry.id ? 'active' : ''}"
|
||
data-industry="${ind.id}" title="${ind.desc}">
|
||
${ind.emoji}<span>${ind.name}</span>
|
||
</button>`).join('');
|
||
wrap.querySelectorAll('.industry-tab').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
designState.industry = HGB_Industries.find(btn.dataset.industry);
|
||
designState.icons = [];
|
||
designState.plans = [];
|
||
designState.activePlan = null;
|
||
renderDesignIndustries();
|
||
renderDesignIcons();
|
||
renderPlans();
|
||
toast('🏭 已切换到「' + designState.industry.name + '」行业');
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 当前工作区(LOGO / 头像由页面级 workspace 决定) */
|
||
function currentWorkspace() {
|
||
return designState.avatarMode ? 'avatar' : 'logo';
|
||
}
|
||
|
||
/** 渲染技能勾选区(按设计流程 5 步组织;按工作区过滤适用技能) */
|
||
function renderSkillChecks() {
|
||
const wrap = document.getElementById('design-skills');
|
||
if (!wrap) return;
|
||
const ws = currentWorkspace();
|
||
wrap.innerHTML = HGB_Skills.steps().map(st => {
|
||
const list = HGB_Skills.byStep(st.step, ws);
|
||
if (!list.length) return '';
|
||
const checked = list.filter(s => designState.skills.includes(s.id)).length;
|
||
const allOn = checked === list.length;
|
||
return `
|
||
<details class="skill-step" data-step="${st.step}" ${st.step <= 3 ? 'open' : ''}>
|
||
<summary class="skill-step-head" title="${esc(st.desc)}">
|
||
<span class="step-emoji">${st.emoji}</span>
|
||
<span class="step-name">${st.step}. ${st.name}</span>
|
||
<span class="step-count">${list.length} 技能</span>
|
||
<span class="step-checked">✓ ${checked}</span>
|
||
<span class="step-toggle ${allOn ? 'all' : ''}">${allOn ? '已全选' : (checked ? '部分' : '未选')}</span>
|
||
</summary>
|
||
<div class="skill-step-body">
|
||
${list.map(s => {
|
||
const on = designState.skills.includes(s.id);
|
||
return `
|
||
<label class="skill-check ${on ? 'on' : ''}" title="${esc(s.scene)}">
|
||
<input type="checkbox" value="${esc(s.id)}" ${on ? 'checked' : ''}>
|
||
<span class="skill-check-emoji">${s.emoji}</span>
|
||
<span class="skill-check-name">${esc(s.name)}</span>
|
||
<span class="skill-kind kind-${HGB_Skills.kind(s.id)}">${KIND_LABEL[HGB_Skills.kind(s.id)]}</span>
|
||
</label>`;
|
||
}).join('')}
|
||
<div class="step-ops">
|
||
<button class="tb-btn sm" data-stepall="${st.step}">☑ 全选本步</button>
|
||
<button class="tb-btn sm" data-stepnone="${st.step}">☐ 清空本步</button>
|
||
</div>
|
||
</div>
|
||
</details>`;
|
||
}).join('');
|
||
|
||
wrap.querySelectorAll('input[type="checkbox"]').forEach(cb => {
|
||
cb.addEventListener('change', () => {
|
||
const id = cb.value;
|
||
if (cb.checked) {
|
||
if (!designState.skills.includes(id)) designState.skills.push(id);
|
||
} else {
|
||
designState.skills = designState.skills.filter(x => x !== id);
|
||
}
|
||
cb.closest('.skill-check').classList.toggle('on', cb.checked);
|
||
updateStepState(cb.closest('.skill-step'));
|
||
});
|
||
});
|
||
wrap.querySelectorAll('[data-stepall]').forEach(btn =>
|
||
btn.addEventListener('click', () => {
|
||
const st = parseInt(btn.dataset.stepall, 10);
|
||
HGB_Skills.byStep(st, currentWorkspace()).forEach(s => { if (!designState.skills.includes(s.id)) designState.skills.push(s.id); });
|
||
renderSkillChecks();
|
||
}));
|
||
wrap.querySelectorAll('[data-stepnone]').forEach(btn =>
|
||
btn.addEventListener('click', () => {
|
||
const st = parseInt(btn.dataset.stepnone, 10);
|
||
const ids = HGB_Skills.byStep(st, currentWorkspace()).map(s => s.id);
|
||
designState.skills = designState.skills.filter(x => !ids.includes(x));
|
||
renderSkillChecks();
|
||
}));
|
||
}
|
||
|
||
/** 更新某步骤区块的勾选计数状态 */
|
||
function updateStepState(details) {
|
||
if (!details) return;
|
||
const list = HGB_Skills.byStep(parseInt(details.dataset.step, 10), currentWorkspace());
|
||
const checked = list.filter(s => designState.skills.includes(s.id)).length;
|
||
const allOn = checked === list.length;
|
||
const el = details.querySelector('.step-checked');
|
||
if (el) el.textContent = '✓ ' + checked;
|
||
const t = details.querySelector('.step-toggle');
|
||
if (t) { t.textContent = allOn ? '已全选' : (checked ? '部分' : '未选'); t.classList.toggle('all', allOn); }
|
||
}
|
||
|
||
/** 从方案提取前景色/背景色(供对比度类检查) */
|
||
function planColors(plan) {
|
||
const els = (plan || {}).elements || [];
|
||
const text = els.find(el => el.type === 'text' && el.visible !== false);
|
||
const icon = els.find(el => el.type === 'icon' && el.iconId && !['ring', 'deco-bar'].includes(el.iconId));
|
||
const fg = (text && text.fill) || (icon && icon.stroke) || '#333333';
|
||
return { fg, bg: plan.bg || '#FFFFFF' };
|
||
}
|
||
/** 检查类技能 → 从方案对象构建调用参数的适配器(设计流程第 3 步「方案评审」) */
|
||
const REVIEW_RUNNERS = {
|
||
'composition': p => ({ plan: p }),
|
||
'align-check': p => ({ plan: p }),
|
||
'negative-space': p => ({ plan: p }),
|
||
'spacing-check': p => ({ plan: p }),
|
||
'proportion-check': p => ({ plan: p }),
|
||
'visual-balance': p => ({ plan: p }),
|
||
'focal-point': p => ({ plan: p }),
|
||
'rule-of-thirds': p => ({ plan: p }),
|
||
'circle-crop-safety': p => ({ plan: p }),
|
||
'contrast-check': p => planColors(p),
|
||
'wcag-contrast': p => { const c = planColors(p); return { fg: c.fg, bg: c.bg }; },
|
||
'color-blind-check': p => ({ plan: p }),
|
||
'monochrome-check': p => ({ plan: p }),
|
||
'font-hierarchy': p => ({ plan: p }),
|
||
'text-fit': p => ({ plan: p }),
|
||
'tiny-readable': p => ({ plan: p }),
|
||
'letter-spacing-check': p => ({ plan: p }),
|
||
'line-break': p => ({ plan: p }),
|
||
'cjk-mix-check': p => ({ plan: p }),
|
||
'icon-consistency': p => ({ plan: p }),
|
||
'icon-tone': p => ({ plan: p }),
|
||
'icon-count': p => ({ plan: p }),
|
||
'icon-text-ratio': p => ({ plan: p }),
|
||
'stroke-consistency': p => ({ plan: p }),
|
||
'icon-padding': p => {
|
||
const els = (p || {}).elements || [];
|
||
const ic = els.find(el => el.type === 'icon' && el.iconId && !['ring', 'deco-bar'].includes(el.iconId));
|
||
return { svg: (ic && (ic.svg || ic.path || '')) || '' };
|
||
},
|
||
'print-safety': p => ({ plan: p })
|
||
};
|
||
const REVIEW_NAMES = {
|
||
'composition': '构图', 'align-check': '对齐', 'negative-space': '负空间', 'spacing-check': '间距',
|
||
'proportion-check': '比例', 'visual-balance': '视觉平衡', 'focal-point': '视觉焦点', 'rule-of-thirds': '构图位置',
|
||
'circle-crop-safety': '圆裁安全',
|
||
'contrast-check': '对比度', 'wcag-contrast': '无障碍', 'color-blind-check': '色盲友好', 'monochrome-check': '单色适配',
|
||
'font-hierarchy': '字号层级', 'text-fit': '文字适配', 'tiny-readable': '小尺寸可读', 'letter-spacing-check': '字距',
|
||
'line-break': '断行', 'cjk-mix-check': '中英混排',
|
||
'icon-consistency': '图标一致', 'icon-tone': '图标明暗', 'icon-count': '图标数量',
|
||
'icon-text-ratio': '图文比例', 'icon-padding': '图标边距', 'stroke-consistency': '图标描边',
|
||
'print-safety': '印刷安全区'
|
||
};
|
||
/** 归一化检查技能输出 → {pass, detail} */
|
||
function normalizeReview(r) {
|
||
if (!r) return { pass: true, detail: '无结果' };
|
||
if (r.error) return { pass: false, detail: r.error };
|
||
const pass = r.pass !== false;
|
||
let detail = r.verdict || r.note || '';
|
||
if (!detail && r.issues && r.issues.length) detail = r.issues.join(';');
|
||
if (!detail) detail = pass ? '通过' : '存在风险';
|
||
return { pass, detail };
|
||
}
|
||
|
||
/** 对方案运行启用的质量类技能(设计流程第 3 步「方案评审」),返回审计结果 */
|
||
function auditPlan(plan, skills) {
|
||
if (!skills || !skills.length || !plan) return null;
|
||
const audit = { score: null, items: [], passed: 0, failed: 0 };
|
||
for (const id of Object.keys(REVIEW_RUNNERS)) {
|
||
if (!skills.includes(id)) continue;
|
||
try {
|
||
const r = HGB_Skills.run(id, REVIEW_RUNNERS[id](plan));
|
||
const n = normalizeReview(r);
|
||
audit.items.push({ id, name: REVIEW_NAMES[id] || id, ok: n.pass, detail: n.detail });
|
||
if (n.pass) audit.passed++; else audit.failed++;
|
||
} catch (e) {
|
||
audit.items.push({ id, name: REVIEW_NAMES[id] || id, ok: false, detail: '执行异常: ' + e.message });
|
||
audit.failed++;
|
||
}
|
||
}
|
||
if (audit.items.length) {
|
||
const comp = audit.items.find(it => it.id === 'composition');
|
||
audit.score = Math.min(100, Math.max(40, 100 - audit.failed * 8 + (comp && comp.ok ? 4 : 0)));
|
||
}
|
||
return audit;
|
||
}
|
||
|
||
/** 渲染风格方向多选胶囊(空 = 智能搭配全部风格) */
|
||
function renderDesignStyles() {
|
||
const wrap = document.getElementById('design-styles');
|
||
if (!wrap) return;
|
||
const sel = designState.styles;
|
||
const chips = [
|
||
`<button class="industry-tab ${sel.length === 0 ? 'active' : ''}" data-style="__all__"
|
||
title="不限定风格,AI/引擎从全部 ${HGB_Styles.length} 种风格中智能搭配">✨ 全部风格</button>`,
|
||
...HGB_Styles.map(s => `
|
||
<button class="industry-tab ${sel.includes(s.id) ? 'active' : ''}" data-style="${esc(s.id)}"
|
||
title="${esc(s.desc)}|设计运动:${esc(s.movement)};气质:${s.moods.join(' / ')}">${s.emoji}${esc(s.name)}</button>`)
|
||
].join('');
|
||
wrap.innerHTML = chips;
|
||
wrap.querySelectorAll('[data-style]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const id = btn.dataset.style;
|
||
if (id === '__all__') {
|
||
designState.styles = [];
|
||
} else {
|
||
const i = designState.styles.indexOf(id);
|
||
if (i >= 0) designState.styles.splice(i, 1);
|
||
else designState.styles.push(id);
|
||
}
|
||
renderDesignStyles();
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 线条风来源(Feather/Lucide/Tabler):构建时 body 的 stroke 属性被剥离只剩 path d,
|
||
* 渲染时必须强制描边(fill=none stroke=灰),否则会被误判为填充型、线条填成实心 */
|
||
const STROKE_SOURCES = new Set(['feather', 'lucide', 'tabler']);
|
||
|
||
/** 判断图标渲染类型:描边型(返回 true)或填充型(false)。
|
||
* 判据:线条风来源强制描边;body 带 stroke 或 stroke-linecap/linejoin(Heroicons outline 的
|
||
* stroke= 在 svg 根标签,body 只剩 stroke-linecap/linejoin)也算描边型,否则开放线条会被 fill 填充成空 */
|
||
function isStrokeIcon(ic, source) {
|
||
const src = source || (ic && ic.id ? String(ic.id).split(':')[0] : '');
|
||
// 多色素材(emoji/品牌)保留原色,不走描边/填充二选一
|
||
if (ic && ic.type && ic.type !== 'icon') return false;
|
||
if (STROKE_SOURCES.has(src)) return true;
|
||
const svg = (ic && ic.svg) || '';
|
||
if (/stroke\s*=|\bstroke-linecap\s*=|\bstroke-linejoin\s*=/.test(svg)) return true;
|
||
// 显式 stroke-width="0"(Carbon 等填充型源自带,防默认描边):实为填充型,
|
||
// 若按描边型渲染(fill=none + stroke),path 级 stroke-width=0 会覆盖根描边宽度 → 整图空白
|
||
if (/stroke-width="0"/.test(svg)) return false;
|
||
// 全 body 几何元素都 fill="none"(或未显式 fill)且无任何 stroke(如 oui 源):
|
||
// 实为描边型,否则按填充型渲染时根 fill 对形状无效 → 空白
|
||
const geoms = svg.match(/<(?:path|circle|rect|ellipse|line|polyline|polygon)\b[^>]*>/g);
|
||
if (geoms && geoms.length && !/fill\s*=\s*"(?!none")[^"]*"/.test(svg)) return true;
|
||
return false;
|
||
}
|
||
|
||
/** 图标 body 预处理:把 currentColor 替换为显式色值。
|
||
* 不能替换成 "inherit":body 内渐变 stop 的 currentColor 会沿 currentColor→inherit→
|
||
* 根 svg fill="none" 的继承链变透明(mingcute 空白即此陷阱);显式色值彻底规避。
|
||
* fill="currentColor" 一律替换为实色 #374151:即便描边型图标(line-md filled 变体用
|
||
* currentColor 填充背景 + mask 挖空)也不能换成 none,否则背景透明整图空白。
|
||
* stroke="currentColor" 也一律替换为实色 #374151(不能换 none):部分描边型素材
|
||
* 用 currentColor 画线(streamline-logos 品牌描边 Logo、high-contrast 表情的线条),
|
||
* 换成 none 会整图透明;显式实色保证可见。
|
||
* 同时替换 stop-color,防止渐变引用 currentColor 时失效。 */
|
||
function prepareIconBody(body, isStroke) {
|
||
return body
|
||
.replace(/fill="currentColor"/g, 'fill="#374151"')
|
||
.replace(/stroke="currentColor"/g, 'stroke="#374151"')
|
||
.replace(/stop-color="currentColor"/g, 'stop-color="#374151"');
|
||
}
|
||
|
||
/** 渲染图标选择网格(当前行业图标 + 已选内置图标,最多选 5 个) */
|
||
function renderDesignIcons() {
|
||
const wrap = document.getElementById('design-icons');
|
||
const indIds = (designState.industry.icons || []).map(i => i.id);
|
||
const list = [
|
||
...(designState.industry.icons || []),
|
||
// 已选的内置库图标(不重复显示行业图标)
|
||
...designState.icons.filter(ic => !indIds.includes(ic.id))
|
||
];
|
||
if (!list.length) {
|
||
wrap.innerHTML = '<p class="pane-tip">未选图标。可点下方「📦 内置库浏览选择」挑选,或直接让 AI 自动选用。</p>';
|
||
return;
|
||
}
|
||
wrap.innerHTML = list.map(ic => {
|
||
const sel = isIconSelected(ic);
|
||
const srcTag = ic.source ? ic.source : '行业';
|
||
// 多色素材(emoji/品牌)保留原色;其余按描边/填充型渲染
|
||
const preserve = ic.type && ic.type !== 'icon';
|
||
const isStroke = preserve ? false : isStrokeIcon(ic);
|
||
const svgAttrs = isStroke
|
||
? 'fill="none" stroke="#374151" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"'
|
||
: 'fill="#374151" stroke="none"';
|
||
const body = preserve ? ic.svg : prepareIconBody(ic.svg, isStroke);
|
||
return `
|
||
<div class="design-icon-item ${sel ? 'selected' : ''}"
|
||
data-icid="${esc(ic.id)}" title="${esc(ic.name)}(${esc(srcTag)})">
|
||
<svg viewBox="0 0 24 24" ${svgAttrs}>${body}</svg>
|
||
<span class="design-icon-name">${esc(ic.name)}</span>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
wrap.querySelectorAll('.design-icon-item').forEach(item => {
|
||
item.addEventListener('click', () => toggleDesignIcon(item.dataset.icid));
|
||
});
|
||
}
|
||
|
||
function isIconSelected(ic) {
|
||
return designState.icons.some(p => p.id === ic.id);
|
||
}
|
||
|
||
function toggleDesignIcon(icid) {
|
||
// 从行业图标 + 已选图标中查找
|
||
const pool = [
|
||
...(designState.industry.icons || []),
|
||
...designState.icons
|
||
];
|
||
const ic = pool.find(x => x.id === icid);
|
||
if (!ic) return;
|
||
const idx = designState.icons.findIndex(p => p.id === ic.id);
|
||
if (idx >= 0) {
|
||
designState.icons.splice(idx, 1);
|
||
} else {
|
||
if (designState.icons.length >= 5) {
|
||
toast('⚠️ 最多选择 5 个图标(每个图标将各出「每图标套数」套)');
|
||
return;
|
||
}
|
||
designState.icons.push({ id: ic.id, name: ic.name, svg: ic.svg, source: ic.source || '' });
|
||
}
|
||
renderDesignIcons();
|
||
}
|
||
|
||
/* ================= 内置图标库浏览 ================= */
|
||
const BUILTIN_PER_PAGE = 150;
|
||
let builtinState = { source: 'lucide', category: '__all__', search: '', page: 0, type: '__all__', loading: false };
|
||
|
||
/** 图标分类展示顺序(中文分类胶囊顺序) */
|
||
const CAT_ORDER = ['education', 'food', 'tech', 'health', 'travel', 'business', 'people',
|
||
'animals', 'nature', 'building', 'tools', 'communication', 'media', 'arrows',
|
||
'time', 'money', 'symbol', 'sport', 'others'];
|
||
|
||
/** 来源库中文名(来源按钮主名)——与 icon-sources.js 的 nameCn 一致 */
|
||
const SOURCE_CN = {
|
||
feather: '羽毛图标', lucide: 'Lucide 图标', tabler: 'Tabler 图标',
|
||
bootstrap: 'Bootstrap 图标', remix: 'Remix 图标', ionicons: 'Ionicons 图标',
|
||
heroicons: 'Heroicons 图标', iconoir: 'Iconoir 图标', boxicons: 'Boxicons 图标',
|
||
mdi: 'MDI 图标', fontawesome: 'Font Awesome 图标', antd: 'Ant Design 图标',
|
||
/* 第三轮 iconify 扩容 */
|
||
fluent: '微软 Fluent', msymbols: 'Google 符号', solar: 'Solar 图标',
|
||
phosphor: 'Phosphor 图标', iconpark: '字节 IconPark', hugeicons: 'Huge 图标',
|
||
mingcute: 'MingCute 图标', carbon: 'IBM Carbon', tdesign: '腾讯 TDesign',
|
||
radix: 'Radix 图标', majesticons: 'Majesticons', pixelarticons: '像素图标',
|
||
cssgg: 'CSS.gg 图标',
|
||
/* 第四轮 iconify 扩容 */
|
||
octicon: 'GitHub 官方', devicon: 'Devicon 技术栈', logos: 'SVG Logos 品牌',
|
||
'circle-flags': '圆形国旗', 'flat-color-icons': '多彩平面', 'line-md': 'Material 动效',
|
||
uil: 'Unicons 图标', teenyicons: 'Teenyicons', healthicons: '医疗健康',
|
||
meteocons: '天气图标', ic: 'Google 经典', 'material-symbols-light': '符号 Light',
|
||
cib: 'CoreUI 品牌', streamline: 'Streamline', basil: 'Basil 图标',
|
||
charm: 'Charm 图标', prime: 'Prime 图标', mynaui: 'Mynaui 图标',
|
||
pajamas: 'GitLab Pajamas', flowbite: 'Flowbite', 'si-glyph': 'Simple 字形',
|
||
'devicon-plain': 'Devicon 纯色', dashicons: 'Dashicons', 'eos-icons': 'EOS 图标',
|
||
'akar-icons': 'Akar 图标',
|
||
/* emoji 多色 */
|
||
twemoji: '推特表情', noto: '谷歌 Noto', fluentemoji: '微软扁平表情',
|
||
openmoji: 'OpenMoji 表情', gameicons: '游戏图标', fxemoji: '火狐表情',
|
||
/* 品牌 */
|
||
'simple-icons': 'Simple 品牌'
|
||
};
|
||
|
||
/** 素材类型 Tab 定义(内置库弹窗顶部:按素材类型过滤来源列表) */
|
||
const BUILTIN_TYPES = [
|
||
{ id: '__all__', cn: '全部素材', emoji: '🗂' },
|
||
{ id: 'icon', cn: '单色图标', emoji: '🎯' },
|
||
{ id: 'emoji', cn: '多色表情', emoji: '😀' },
|
||
{ id: 'brand', cn: '品牌 Logo', emoji: '🏷' }
|
||
];
|
||
|
||
function bindBuiltinModal() {
|
||
const modal = document.getElementById('builtin-modal');
|
||
const searchInput = document.getElementById('builtin-search');
|
||
|
||
document.getElementById('btn-builtin-icons').addEventListener('click', async () => {
|
||
builtinState.search = '';
|
||
builtinState.page = 0;
|
||
searchInput.value = '';
|
||
renderBuiltinSources();
|
||
// 确保当前源已加载,再渲染网格(懒加载:默认源 lucide 首次打开需异步加载)
|
||
await ensureBuiltinSource();
|
||
modal.classList.remove('hidden');
|
||
renderBuiltinGrid();
|
||
});
|
||
|
||
modal.querySelectorAll('[data-builtin-close]').forEach(b =>
|
||
b.addEventListener('click', () => modal.classList.add('hidden')));
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) modal.classList.add('hidden');
|
||
});
|
||
|
||
// 搜索(防抖)
|
||
let debounce = null;
|
||
searchInput.addEventListener('input', () => {
|
||
clearTimeout(debounce);
|
||
debounce = setTimeout(() => {
|
||
builtinState.search = searchInput.value.trim().toLowerCase();
|
||
builtinState.page = 0;
|
||
renderBuiltinGrid();
|
||
}, 150);
|
||
});
|
||
|
||
document.getElementById('builtin-load-more').addEventListener('click', () => {
|
||
builtinState.page++;
|
||
renderBuiltinGrid(true);
|
||
});
|
||
}
|
||
|
||
/** 确保当前来源已加载(未加载则异步拉取数据文件) */
|
||
async function ensureBuiltinSource() {
|
||
if (HGB_IconLib.loaded(builtinState.source)) return;
|
||
builtinState.loading = true;
|
||
const grid = document.getElementById('builtin-grid');
|
||
if (grid) grid.innerHTML = '<p class="gh-status">⏳ 正在加载素材源…</p>';
|
||
try {
|
||
await HGB_IconLib.loadSource(builtinState.source);
|
||
} catch (e) {
|
||
console.warn('[builtin] 素材源加载失败:', e);
|
||
if (grid) grid.innerHTML = '<p class="gh-status">❌ 素材源加载失败:' + esc(e.message) + '</p>';
|
||
} finally {
|
||
builtinState.loading = false;
|
||
}
|
||
}
|
||
|
||
function renderBuiltinSources() {
|
||
const meta = HGB_BuiltinIcons.meta || {};
|
||
const total = meta.total || 0;
|
||
|
||
// 类型 Tab(全部 / 图标 / 表情 / 品牌)
|
||
const typeWrap = document.getElementById('builtin-types');
|
||
typeWrap.innerHTML = BUILTIN_TYPES.map(t => {
|
||
const cnt = t.id === '__all__'
|
||
? total
|
||
: Object.values(HGB_BuiltinIcons.sources || {}).reduce((n, s) => s.type === t.id ? n + s.count : n, 0);
|
||
return `<button class="builtin-source btype-btn ${builtinState.type === t.id ? 'active' : ''}"
|
||
data-btype="${t.id}">${t.emoji} ${t.cn}<span class="bsrc-count">${cnt.toLocaleString()}</span></button>`;
|
||
}).join('');
|
||
typeWrap.querySelectorAll('.btype-btn').forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
builtinState.type = btn.dataset.btype;
|
||
builtinState.source = firstSourceOfType(builtinState.type);
|
||
builtinState.category = '__all__';
|
||
builtinState.page = 0;
|
||
renderBuiltinSources();
|
||
await ensureBuiltinSource(); // 切类型默认源可能未加载(懒加载),需先加载再渲染网格
|
||
renderBuiltinGrid();
|
||
});
|
||
});
|
||
|
||
// 来源按钮(按当前类型过滤)
|
||
const wrap = document.getElementById('builtin-sources');
|
||
wrap.innerHTML = Object.keys(HGB_BuiltinIcons.sources || {})
|
||
.filter(sid => builtinState.type === '__all__' || (HGB_BuiltinIcons.sources[sid].type || 'icon') === builtinState.type)
|
||
.map(sid => {
|
||
const src = HGB_BuiltinIcons.sources[sid];
|
||
const count = src ? src.count : 0;
|
||
const cn = src.nameCn || SOURCE_CN[sid] || src.name; // 优先索引中的中文名(源自 icon-sources.js 权威清单)
|
||
return `<button class="builtin-source bsrc-btn ${builtinState.source === sid ? 'active' : ''}"
|
||
data-bsrc="${sid}" title="${cn}(${src.name})">
|
||
<span class="bsrc-cn">${cn}</span>
|
||
<span class="bsrc-count">${count.toLocaleString()}</span></button>`;
|
||
}).join('');
|
||
wrap.querySelectorAll('.builtin-source').forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
builtinState.source = btn.dataset.bsrc;
|
||
builtinState.category = '__all__'; // 切换来源后重置分类
|
||
builtinState.page = 0;
|
||
renderBuiltinSources();
|
||
await ensureBuiltinSource();
|
||
renderBuiltinGrid();
|
||
});
|
||
});
|
||
|
||
// 分类列表(中文,可选):按当前来源统计分类分布(来源需已加载)
|
||
const catWrap = document.getElementById('builtin-cats');
|
||
const srcData = HGB_IconLib.getSource(builtinState.source);
|
||
const catCount = new Map();
|
||
const catCn = {};
|
||
for (const ic of (srcData ? srcData.icons : [])) {
|
||
const key = ic.category || 'others';
|
||
catCount.set(key, (catCount.get(key) || 0) + 1);
|
||
catCn[key] = ic.categoryCn || '其他';
|
||
}
|
||
const cats = CAT_ORDER.filter(c => catCount.has(c));
|
||
catWrap.innerHTML =
|
||
`<button class="builtin-source ${builtinState.category === '__all__' ? 'active' : ''}" data-bcat="__all__"
|
||
title="显示当前来源的全部图标">📂 全部<span class="bsrc-count">${srcData ? srcData.icons.length.toLocaleString() : '—'}</span></button>` +
|
||
cats.map(c => `<button class="builtin-source ${builtinState.category === c ? 'active' : ''}" data-bcat="${c}"
|
||
title="仅显示「${catCn[c]}」分类图标">${catCn[c]}<span class="bsrc-count">${catCount.get(c)}</span></button>`).join('');
|
||
catWrap.querySelectorAll('.builtin-source').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
builtinState.category = btn.dataset.bcat;
|
||
builtinState.page = 0;
|
||
renderBuiltinSources();
|
||
renderBuiltinGrid();
|
||
});
|
||
});
|
||
|
||
document.getElementById('builtin-head-info').textContent =
|
||
`共 ${total.toLocaleString()} 个素材 · 离线可用 · 点击选择(最多 2 个)`;
|
||
}
|
||
|
||
/** 当前类型下的第一个来源(切类型后默认选中) */
|
||
function firstSourceOfType(type) {
|
||
for (const sid of Object.keys(HGB_BuiltinIcons.sources || {})) {
|
||
if (type === '__all__' || (HGB_BuiltinIcons.sources[sid].type || 'icon') === type) return sid;
|
||
}
|
||
return builtinState.source;
|
||
}
|
||
|
||
/** 当前过滤后的图标列表(来源 + 分类 + 搜索)——来源需已加载 */
|
||
function filteredBuiltinIcons() {
|
||
const src = HGB_IconLib.getSource(builtinState.source);
|
||
if (!src || !src.icons) return [];
|
||
let list = src.icons;
|
||
if (builtinState.category && builtinState.category !== '__all__') {
|
||
list = list.filter(i => i.category === builtinState.category);
|
||
}
|
||
const q = builtinState.search;
|
||
if (!q) return list;
|
||
// 同时匹配名称与 id(id 可能带库前缀)
|
||
return list.filter(i =>
|
||
i.name.toLowerCase().includes(q) || i.id.toLowerCase().includes(q));
|
||
}
|
||
|
||
function renderBuiltinGrid(append) {
|
||
const grid = document.getElementById('builtin-grid');
|
||
const moreWrap = document.getElementById('builtin-more-wrap');
|
||
if (builtinState.loading || !HGB_IconLib.loaded(builtinState.source)) {
|
||
grid.innerHTML = '<p class="gh-status">⏳ 正在加载素材源…</p>';
|
||
moreWrap.style.display = 'none';
|
||
return;
|
||
}
|
||
const filtered = filteredBuiltinIcons();
|
||
document.getElementById('builtin-count').textContent =
|
||
builtinState.search ? `匹配 ${filtered.length.toLocaleString()} 个` : `共 ${filtered.length.toLocaleString()} 个`;
|
||
|
||
const start = builtinState.page * BUILTIN_PER_PAGE;
|
||
const slice = filtered.slice(start, start + BUILTIN_PER_PAGE);
|
||
|
||
const html = slice.map(ic => {
|
||
const fullId = builtinState.source + ':' + ic.name;
|
||
const sel = designState.icons.some(p => p.id === fullId);
|
||
// 多色素材(emoji/品牌):保留原色渲染,不覆盖 fill/stroke
|
||
const preserve = ic.type && ic.type !== 'icon';
|
||
const isStroke = preserve ? false : isStrokeIcon(ic, builtinState.source);
|
||
const svgAttrs = isStroke
|
||
? 'fill="none" stroke="#374151" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"'
|
||
: 'fill="#374151" stroke="none"';
|
||
// currentColor 会覆盖 svg 上的颜色值 → 统一替换为与渲染类型匹配的显式色值
|
||
const body = preserve ? ic.svg : prepareIconBody(ic.svg, isStroke);
|
||
return `
|
||
<div class="gh-icon-item ${sel ? 'imported' : ''}" data-bid="${esc(ic.id)}">
|
||
<span class="gh-tip">${esc(ic.name)}</span>
|
||
<svg viewBox="0 0 24 24" ${svgAttrs}>${body}</svg>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
if (append) {
|
||
grid.insertAdjacentHTML('beforeend', html);
|
||
} else {
|
||
grid.innerHTML = html;
|
||
}
|
||
grid.querySelectorAll('.gh-icon-item').forEach(item => {
|
||
item.addEventListener('click', () => toggleBuiltinIcon(item.dataset.bid));
|
||
});
|
||
|
||
moreWrap.style.display = start + slice.length < filtered.length ? 'block' : 'none';
|
||
}
|
||
|
||
/** 选中/取消内置图标 */
|
||
function toggleBuiltinIcon(icid) {
|
||
const src = HGB_IconLib.getSource(builtinState.source);
|
||
const ic = src ? src.icons.find(i => i.id === icid) : null;
|
||
if (!ic) return;
|
||
const fullId = builtinState.source + ':' + ic.name;
|
||
const existing = designState.icons.find(p => p.id === fullId);
|
||
if (existing) {
|
||
designState.icons = designState.icons.filter(p => p.id !== fullId);
|
||
toast('↩️ 已取消「' + ic.name + '」');
|
||
} else {
|
||
if (designState.icons.length >= 5) {
|
||
toast('⚠️ 最多选择 5 个图标(每个图标将各出「每图标套数」套)');
|
||
return;
|
||
}
|
||
designState.icons.push({
|
||
id: fullId,
|
||
name: ic.name,
|
||
svg: ic.svg,
|
||
type: ic.type || 'icon',
|
||
source: ((HGB_BuiltinIcons.sources[builtinState.source] || {}).nameCn) || (HGB_BuiltinIcons.sources[builtinState.source] || {}).name || builtinState.source
|
||
});
|
||
toast('✅ 已选择「' + ic.name + '」,AI 将优先使用该素材');
|
||
}
|
||
renderDesignIcons();
|
||
renderBuiltinGrid();
|
||
}
|
||
|
||
/* ================= 工作区初始化(LOGO/头像由页面级 workspace 决定,不再运行时切换) ================= */
|
||
function applyWorkspace() {
|
||
designState.avatarMode = (window.HGB_WORKSPACE === 'avatar');
|
||
document.body.classList.toggle('avatar-mode', designState.avatarMode);
|
||
const genBtn = document.getElementById('btn-generate');
|
||
if (genBtn) genBtn.textContent = designState.avatarMode ? '🚀 生成头像' : '🤖 AI 生成方案';
|
||
const dbBtn = document.getElementById('btn-dicebear');
|
||
if (dbBtn) dbBtn.classList.toggle('hidden', !designState.avatarMode);
|
||
}
|
||
|
||
/* ================= DiceBear 风格头像(M4) ================= */
|
||
let dicebearStyle = 'adventurer';
|
||
let dicebearSeed = '';
|
||
let activeDicebear = null; // {svg, styleId, seed, cid}
|
||
|
||
function randomSeed() {
|
||
return Math.random().toString(36).slice(2, 10);
|
||
}
|
||
|
||
function bindDicebearModal() {
|
||
const modal = document.getElementById('dicebear-modal');
|
||
document.getElementById('btn-dicebear').addEventListener('click', () => {
|
||
if (!dicebearSeed) {
|
||
dicebearSeed = randomSeed();
|
||
document.getElementById('dicebear-seed').value = dicebearSeed;
|
||
}
|
||
renderDicebearStyles();
|
||
renderDicebear();
|
||
modal.classList.remove('hidden');
|
||
});
|
||
modal.querySelectorAll('[data-dicebear-close]').forEach(b =>
|
||
b.addEventListener('click', () => modal.classList.add('hidden')));
|
||
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.add('hidden'); });
|
||
// 风格切换
|
||
document.getElementById('dicebear-styles').addEventListener('click', e => {
|
||
const btn = e.target.closest('[data-dbs]');
|
||
if (!btn) return;
|
||
dicebearStyle = btn.dataset.dbs;
|
||
renderDicebearStyles();
|
||
renderDicebear();
|
||
});
|
||
// 随机 seed → 换一批
|
||
document.getElementById('dicebear-random').addEventListener('click', () => {
|
||
dicebearSeed = randomSeed();
|
||
document.getElementById('dicebear-seed').value = dicebearSeed;
|
||
renderDicebear();
|
||
});
|
||
// 生成
|
||
document.getElementById('dicebear-gen').addEventListener('click', () => {
|
||
dicebearSeed = (document.getElementById('dicebear-seed').value || '').trim() || randomSeed();
|
||
document.getElementById('dicebear-seed').value = dicebearSeed;
|
||
renderDicebear();
|
||
});
|
||
// 导出选中的 PNG(平台规格 + 圆形裁切)
|
||
document.getElementById('dicebear-export').addEventListener('click', () => {
|
||
if (!activeDicebear) { toast('请先点击选中一张头像'); return; }
|
||
const sizes = HGB_AvatarGen.AVATAR_SIZES.map(s => s.size);
|
||
HGB_Exporter.exportAvatarPngs(activeDicebear.svg, sizes, {
|
||
bg: '#FFFFFF', round: true, base: 'dicebear_' + activeDicebear.styleId
|
||
}).then(() => toast('📤 已导出 ' + sizes.length + ' 个平台规格 PNG(圆形)'));
|
||
});
|
||
}
|
||
|
||
function renderDicebearStyles() {
|
||
const wrap = document.getElementById('dicebear-styles');
|
||
if (!wrap) return;
|
||
wrap.innerHTML = HGB_Dicebear.STYLES.map(s => `
|
||
<button class="industry-tab ${s === dicebearStyle ? 'active' : ''}" data-dbs="${s}" title="DiceBear 官方风格 ${s}">${HGB_Dicebear.CN[s] || s}</button>`).join('');
|
||
}
|
||
|
||
async function renderDicebear() {
|
||
const seed = (document.getElementById('dicebear-seed').value || '').trim() || randomSeed();
|
||
document.getElementById('dicebear-seed').value = seed;
|
||
dicebearSeed = seed;
|
||
const grid = document.getElementById('dicebear-grid');
|
||
grid.innerHTML = '<div class="plans-empty">🧸 正在组合风格部件…</div>';
|
||
try {
|
||
await HGB_Dicebear.loadStyle(dicebearStyle);
|
||
} catch (e) {
|
||
grid.innerHTML = '<div class="plans-empty">⚠️ 风格定义加载失败:' + esc(e.message) + '</div>';
|
||
return;
|
||
}
|
||
const count = 8;
|
||
const cards = [];
|
||
for (let i = 0; i < count; i++) {
|
||
const s = seed + '-' + i;
|
||
cards.push({ svg: HGB_Dicebear.renderAvatar(dicebearStyle, s).svg, styleId: dicebearStyle, seed: s, cid: 'db_' + i });
|
||
}
|
||
grid.innerHTML = cards.map(c => {
|
||
const sel = activeDicebear && activeDicebear.cid === c.cid;
|
||
return '<div class="dicebear-card' + (sel ? ' active' : '') + '" data-cid="' + c.cid + '">' +
|
||
'<div class="dicebear-svg">' + svgInline(c.svg) + '</div>' +
|
||
'<div class="dicebear-name">' + (HGB_Dicebear.CN[dicebearStyle] || dicebearStyle) + ' · ' + esc(c.seed.slice(-4)) + '</div>' +
|
||
'</div>';
|
||
}).join('');
|
||
if (!activeDicebear) activeDicebear = cards[0];
|
||
grid.querySelectorAll('.dicebear-card').forEach(card => {
|
||
card.addEventListener('click', () => {
|
||
activeDicebear = cards.find(x => x.cid === card.dataset.cid) || activeDicebear;
|
||
grid.querySelectorAll('.dicebear-card').forEach(c2 =>
|
||
c2.classList.toggle('active', c2 === card));
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 生成方案:AI 优先(DeepSeek 自主设计),失败自动降级内置引擎 */
|
||
async function generatePlans() {
|
||
// 品牌名:优先「品牌名」输入框,其次顶栏「项目名」,最后回退行业默认
|
||
const brand = document.getElementById('design-brand').value
|
||
|| document.getElementById('project-name').value;
|
||
const sub = document.getElementById('design-sub').value;
|
||
const desc = document.getElementById('design-desc').value;
|
||
const count = parseInt(document.getElementById('design-count').value, 10) || 5;
|
||
const iconCount = designState.icons.length;
|
||
// 图标驱动:LOGO 模式下选中 1-5 个图标 → 每个图标固定作为核心图形,各产出 count 套
|
||
const iconDriven = !designState.avatarMode && iconCount > 0;
|
||
const genBtn = document.getElementById('btn-generate');
|
||
genBtn.disabled = true;
|
||
genBtn.textContent = iconDriven ? '🎨 按图标设计中…' : '⏳ AI 构思中…';
|
||
try {
|
||
// 让浏览器先渲染"生成中"状态
|
||
await new Promise(r => setTimeout(r, 30));
|
||
designState.genParam = {
|
||
industry: designState.industry,
|
||
brand, sub, desc,
|
||
icons: designState.icons.slice(),
|
||
styleIds: designState.styles.slice(), // 风格方向(空 = 全部风格)
|
||
skills: designState.skills.slice(),
|
||
iconLocked: iconDriven, // 图标驱动模式标记(变体/更多变体沿用)
|
||
avatar: designState.avatarMode // 头像模式标记(变体/导出使用)
|
||
};
|
||
let plans;
|
||
if (iconDriven) {
|
||
// 图标驱动:确定性出图(每图标 × 套数),不走 AI 自主选图标
|
||
plans = HGB_Generator.generatePlans({ ...designState.genParam, count });
|
||
toast('📦 已按所选 ' + iconCount + ' 个图标逐套设计:' + iconCount + ' 个 × ' + count + ' 套 = ' + plans.length + ' 套');
|
||
} else if (designState.avatarMode) {
|
||
// 头像模式:AI 优先(DeepSeek 自主设计),失败/无 Key 降级内置引擎即时出图
|
||
if (HGB_AI.hasKey()) {
|
||
try {
|
||
const result = await HGB_AIDesigner.generatePlansAI({
|
||
...designState.genParam,
|
||
avatar: true, // 头像约束:纯图形、无文字、布局限定头像白名单
|
||
count
|
||
});
|
||
plans = result.plans;
|
||
designState.overallConcept = result.overallConcept;
|
||
toast('🤖 AI 已完成头像设计,共 ' + plans.length + ' 套(含设计理念)');
|
||
} catch (aiErr) {
|
||
console.warn('[AI] 头像生成失败,降级内置引擎:', aiErr);
|
||
toast('⚠️ AI 生成失败(' + aiErr.message + '),已用内置引擎即时兜底');
|
||
plans = await HGB_AvatarGen.generateAvatars({ ...designState.genParam, count });
|
||
}
|
||
} else {
|
||
toast('ℹ️ 未配置 AI Key,先用内置引擎即时生成头像;顶栏「🤖 AI 设置」可配置 DeepSeek');
|
||
plans = HGB_AvatarGen.generateAvatars({ ...designState.genParam, count });
|
||
}
|
||
} else if (HGB_AI.hasKey()) {
|
||
try {
|
||
const result = await HGB_AIDesigner.generatePlansAI({
|
||
...designState.genParam,
|
||
count
|
||
});
|
||
plans = result.plans;
|
||
designState.overallConcept = result.overallConcept;
|
||
toast('🤖 AI 已完成设计,共 ' + plans.length + ' 套方案(含设计理念)');
|
||
} catch (aiErr) {
|
||
console.warn('[AI] 生成失败,降级内置引擎:', aiErr);
|
||
toast('⚠️ AI 生成失败(' + aiErr.message + '),已用内置引擎兜底');
|
||
plans = generatePlansLocal(count);
|
||
}
|
||
} else {
|
||
toast('ℹ️ 未配置 AI Key,先用内置引擎生成;顶栏「🤖 AI 设置」可配置 DeepSeek');
|
||
plans = generatePlansLocal(count);
|
||
}
|
||
// 对每套方案运行启用的质量类技能,生成审计结果
|
||
for (const p of plans) {
|
||
p.skillAudit = auditPlan(p, designState.genParam.skills);
|
||
}
|
||
designState.plans = plans;
|
||
designState.activePlan = null;
|
||
renderPlans();
|
||
} catch (err) {
|
||
toast('❌ 生成失败:' + err.message);
|
||
} finally {
|
||
genBtn.disabled = false;
|
||
genBtn.textContent = designState.avatarMode ? '🚀 生成头像' : '🤖 AI 生成方案';
|
||
}
|
||
}
|
||
|
||
/** 内置引擎兜底生成(模板组合) */
|
||
function generatePlansLocal(count) {
|
||
return HGB_Generator.generatePlans({
|
||
...designState.genParam,
|
||
count: count || 5
|
||
});
|
||
}
|
||
|
||
/* ================= AI 设置 ================= */
|
||
function bindAISettings() {
|
||
const modal = document.getElementById('ai-modal');
|
||
const keyInput = document.getElementById('ai-key');
|
||
const modelSelect = document.getElementById('ai-model');
|
||
const testResult = document.getElementById('ai-test-result');
|
||
|
||
HGB_AI.MODELS.forEach(m => {
|
||
const op = document.createElement('option');
|
||
op.value = m;
|
||
op.textContent = m;
|
||
modelSelect.appendChild(op);
|
||
});
|
||
|
||
document.getElementById('btn-ai-settings').addEventListener('click', () => {
|
||
keyInput.value = HGB_AI.getKey();
|
||
modelSelect.value = HGB_AI.getModel();
|
||
testResult.innerHTML = '';
|
||
modal.classList.remove('hidden');
|
||
});
|
||
modal.querySelectorAll('[data-ai-close]').forEach(b =>
|
||
b.addEventListener('click', () => modal.classList.add('hidden')));
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) modal.classList.add('hidden');
|
||
});
|
||
|
||
document.getElementById('btn-ai-save').addEventListener('click', () => {
|
||
HGB_AI.setKey(keyInput.value.trim());
|
||
HGB_AI.setModel(modelSelect.value);
|
||
testResult.innerHTML = '<span class="ai-ok">✅ 已保存</span>';
|
||
toast('🤖 AI 设置已保存');
|
||
});
|
||
|
||
document.getElementById('btn-ai-clear').addEventListener('click', () => {
|
||
HGB_AI.clearKey();
|
||
keyInput.value = HGB_AI.getKey(); // 清除本机 Key 后为空
|
||
testResult.innerHTML = '<span class="ai-ok">🧹 已清除本机 API Key(未配置时 AI 降级为内置引擎)</span>';
|
||
toast('🤖 已清除本机 API Key');
|
||
});
|
||
|
||
document.getElementById('btn-ai-test').addEventListener('click', async () => {
|
||
HGB_AI.setKey(keyInput.value.trim());
|
||
HGB_AI.setModel(modelSelect.value);
|
||
if (!HGB_AI.hasKey()) {
|
||
testResult.innerHTML = '<span class="ai-err">请先填写 API Key</span>';
|
||
return;
|
||
}
|
||
testResult.innerHTML = '<span class="ai-pending">⏳ 正在测试连接…</span>';
|
||
try {
|
||
const r = await HGB_AI.testConnection();
|
||
testResult.innerHTML = '<span class="ai-ok">✅ 连接成功:' + esc(r) + '</span>';
|
||
} catch (err) {
|
||
const map = {
|
||
NO_KEY: '未填写 Key',
|
||
AUTH: 'API Key 无效(401)',
|
||
BALANCE: '余额不足(402)',
|
||
RATE_LIMIT: '请求过于频繁(429)',
|
||
NETWORK: '网络错误,请检查网络/代理',
|
||
EMPTY: 'AI 返回为空'
|
||
};
|
||
testResult.innerHTML = '<span class="ai-err">❌ ' + (map[err.code] || esc(err.message)) + '</span>';
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 技能检查小结(方案卡片:评分 + 通过/失败统计 + 失败项提示;完整明细见预览弹窗) */
|
||
function renderAuditBadge(audit) {
|
||
if (!audit || !audit.items || !audit.items.length) return '';
|
||
const items = audit.items;
|
||
const failed = items.filter(it => !it.ok);
|
||
const ok = items.filter(it => it.ok);
|
||
const score = audit.score;
|
||
const scoreHtml = score != null
|
||
? `<span class="audit-score ${score >= 90 ? 'a' : score >= 70 ? 'b' : 'c'}" title="技能质量评分:满分 100,每项未通过扣 8 分">🧩 ${score}</span>`
|
||
: '';
|
||
const summary = `<span class="audit-item ${failed.length ? 'warn' : 'ok'}" title="${items.length} 项检查 · ${ok.length} 项通过${failed.length ? ' · ' + failed.length + ' 项需关注' : ''}">${items.length} 项 ✓ ${ok.length}${failed.length ? ' ⚠ ' + failed.length : ''}</span>`;
|
||
const badges = failed.slice(0, 3).map(it =>
|
||
`<span class="audit-item warn" title="${esc(it.detail)}">${esc(it.name)}⚠</span>`).join('');
|
||
return `<div class="audit-row">${scoreHtml}${summary}${badges}</div>`;
|
||
}
|
||
|
||
/** 渲染方案库卡片(卡片内同时展示 Mark 图形与默认 Lockup 组合;头像模式只展示圆形 Mark) */
|
||
function renderPlans() {
|
||
const grid = document.getElementById('plans-grid');
|
||
const empty = document.getElementById('plans-empty');
|
||
const avatar = designState.avatarMode;
|
||
document.getElementById('plans-title').textContent =
|
||
`${avatar ? '👤 头像库' : '📚 方案库'}(共 ${designState.plans.length} 套)`;
|
||
empty.classList.add('hidden');
|
||
if (!designState.plans.length) {
|
||
empty.classList.remove('hidden');
|
||
grid.innerHTML = '';
|
||
return;
|
||
}
|
||
grid.innerHTML = designState.plans.map(p => {
|
||
const markSvg = HGB_Exporter.buildSvgFrom(p.markElements || [], p.bg, null, { trim: true });
|
||
const active = designState.activePlan && designState.activePlan.id === p.id;
|
||
const previewsHtml = avatar
|
||
? `<div class="plan-preview plan-preview-avatar" title="头像图形标识(圆形预览)">${svgInline(markSvg)}</div>`
|
||
: `<div class="plan-previews">
|
||
<div class="plan-preview" title="图形标识(Mark)">${svgInline(markSvg)}</div>
|
||
<div class="plan-preview plan-preview-lp" title="品牌组合(Lockup)">${svgInline(HGB_Exporter.buildSvgFrom(p.elements || [], p.bg, null, { trim: true }))}</div>
|
||
</div>`;
|
||
return `
|
||
<div class="plan-card ${active ? 'active' : ''} ${avatar ? 'plan-card-avatar' : ''}" data-plan="${p.id}">
|
||
${previewsHtml}
|
||
<div class="plan-meta">
|
||
<div class="plan-name">${esc(p.name)}</div>
|
||
${p.concept ? `<div class="plan-concept">💡 ${esc(p.concept)}</div>` : ''}
|
||
${p.warnings && p.warnings.length ? `<div class="plan-warn">⚠ ${esc(p.warnings.join('、'))}</div>` : ''}
|
||
${renderAuditBadge(p.skillAudit)}
|
||
<div class="plan-tags">
|
||
<span>🏷 ${esc(p.layoutName)}</span>
|
||
<span>🎨 ${esc(p.paletteName)}</span>
|
||
${p.aiIcon ? `<span title="AI 从内置库选用的图标">🎯 ${esc(p.aiIcon.split(':')[1] || p.aiIcon)}</span>` : ''}
|
||
${p.ai ? '<span class="ai-badge">🤖 AI</span>' : ''}
|
||
</div>
|
||
<div class="plan-actions">
|
||
<button class="tb-btn apply" data-pact="preview">👁 预览</button>
|
||
<button class="tb-btn" data-pact="png">${avatar ? '导出头像' : '导出 PNG'}</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
grid.querySelectorAll('.plan-card').forEach(card => {
|
||
const plan = designState.plans.find(p => p.id === card.dataset.plan);
|
||
if (!plan) return;
|
||
card.querySelectorAll('button').forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
const act = btn.dataset.pact;
|
||
if (act === 'preview') {
|
||
openPlanPreview(plan);
|
||
} else if (act === 'png') {
|
||
designState.activePlan = plan;
|
||
if (designState.avatarMode) {
|
||
openExportModal('png'); // 头像模式:平台规格导出
|
||
} else {
|
||
try {
|
||
const svgStr = HGB_Exporter.buildSvgFrom(plan.elements, plan.bg, null, { trim: true });
|
||
await HGB_Exporter.exportPng(1024, plan.bg, svgStr);
|
||
toast('✅ 已导出方案 PNG(1024px)');
|
||
} catch (err) {
|
||
toast('❌ 导出失败:' + err.message);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ================= 方案预览与变体 ================= */
|
||
function bindPlanModal() {
|
||
const modal = document.getElementById('plan-modal');
|
||
modal.querySelectorAll('[data-plan-close]').forEach(btn => {
|
||
btn.addEventListener('click', closePlanPreview);
|
||
});
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) closePlanPreview();
|
||
});
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && !modal.classList.contains('hidden')) closePlanPreview();
|
||
});
|
||
|
||
document.getElementById('btn-var-color').addEventListener('click', variantColor);
|
||
document.getElementById('btn-var-layout').addEventListener('click', variantLayout);
|
||
document.getElementById('btn-var-more').addEventListener('click', variantMore);
|
||
document.getElementById('btn-plan-png').addEventListener('click', () => exportActive('png'));
|
||
document.getElementById('btn-plan-svg').addEventListener('click', () => exportActive('svg'));
|
||
document.getElementById('btn-plan-pdf').addEventListener('click', () => exportActive('pdf'));
|
||
}
|
||
|
||
function openPlanPreview(plan) {
|
||
designState.activePlan = plan;
|
||
// 重置为默认位置(保留用户当前选择的位置如果在 lockups 中)
|
||
const keep = designState.activeLockup;
|
||
if (!(plan.lockups || []).some(lp => lp.pos === keep)) {
|
||
designState.activeLockup = 'below';
|
||
}
|
||
renderPlans(); // 更新卡片高亮
|
||
renderPlanPreview();
|
||
document.getElementById('plan-modal').classList.remove('hidden');
|
||
}
|
||
|
||
function closePlanPreview() {
|
||
document.getElementById('plan-modal').classList.add('hidden');
|
||
}
|
||
|
||
function renderPlanPreview() {
|
||
const p = designState.activePlan;
|
||
if (!p) return;
|
||
// 区域一:图形标识(Mark)—— 纯图形
|
||
const markSvg = HGB_Exporter.buildSvgFrom(p.markElements || [], p.bg, null, { trim: true });
|
||
document.getElementById('plan-mark').innerHTML = svgInline(markSvg);
|
||
// 区域二:品牌组合(Lockup)—— 各位置
|
||
const lockups = p.lockups || [];
|
||
const activeIdx = Math.max(0, lockups.findIndex(lp => lp.pos === designState.activeLockup));
|
||
const activeLp = lockups[activeIdx] || { pos: '', name: '', elements: p.markElements || [] };
|
||
const lpSvg = HGB_Exporter.buildSvgFrom(activeLp.elements || [], p.bg, null, { trim: true });
|
||
document.getElementById('plan-lockup').innerHTML = svgInline(lpSvg);
|
||
// 位置切换标签
|
||
const tabs = document.getElementById('plan-lockup-tabs');
|
||
if (lockups.length) {
|
||
tabs.innerHTML = lockups.map((lp, i) =>
|
||
`<button class="plan-lockup-tab ${i === activeIdx ? 'active' : ''}" data-lppos="${lp.pos}">${esc(lp.name)}</button>`
|
||
).join('');
|
||
tabs.querySelectorAll('.plan-lockup-tab').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
designState.activeLockup = btn.dataset.lppos;
|
||
renderPlanPreview();
|
||
});
|
||
});
|
||
} else {
|
||
tabs.innerHTML = '';
|
||
}
|
||
document.getElementById('plan-modal-sub').textContent =
|
||
`${p.layoutName} · ${p.paletteName}`;
|
||
const concept = document.getElementById('plan-concept');
|
||
concept.innerHTML = p.concept
|
||
? `<div class="plan-concept-inner">💡 设计理念:${esc(p.concept)}</div>`
|
||
: '';
|
||
renderPlanAudit(p);
|
||
}
|
||
|
||
/** 预览弹窗:技能检查报告明细(方案评审步骤的完整结果) */
|
||
function renderPlanAudit(p) {
|
||
const el = document.getElementById('plan-audit');
|
||
if (!el) return;
|
||
const a = p.skillAudit;
|
||
if (!a || !a.items || !a.items.length) { el.innerHTML = ''; return; }
|
||
const failed = a.items.filter(it => !it.ok);
|
||
const ok = a.items.filter(it => it.ok);
|
||
const scoreHtml = a.score != null
|
||
? `<span class="audit-score ${a.score >= 90 ? 'a' : a.score >= 70 ? 'b' : 'c'}">🧩 ${a.score}</span>`
|
||
: '';
|
||
el.innerHTML = `
|
||
<div class="plan-audit-head">🔬 技能检查报告 <span class="plan-audit-sub">(方案评审 · 共 ${a.items.length} 项 · ✓ ${ok.length} / ⚠ ${failed.length})</span></div>
|
||
<div class="plan-audit-body">
|
||
<div class="plan-audit-sum">${scoreHtml}<span class="plan-audit-note">${a.score != null ? (a.score >= 90 ? '质量优秀' : a.score >= 70 ? '基本达标' : '需重点优化') : ''}</span></div>
|
||
<div class="plan-audit-items">
|
||
${a.items.map(it => `
|
||
<div class="plan-audit-item ${it.ok ? 'ok' : 'warn'}">
|
||
<span class="pa-mark">${it.ok ? '✓' : '⚠'}</span>
|
||
<span class="pa-name">${esc(it.name)}</span>
|
||
<span class="pa-detail">${esc(it.detail)}</span>
|
||
</div>`).join('')}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
/** 换配色变体:保持当前布局,切换风格色系(配色/背景/字重随风格整体切换) */
|
||
function variantColor() {
|
||
const p = designState.activePlan;
|
||
if (!p || !designState.genParam) return;
|
||
// 风格轮换池:已指定风格方向时仅在所选风格内循环,否则全部 20 种
|
||
const styleIds = designState.genParam.styleIds || [];
|
||
let styles = styleIds.length
|
||
? HGB_Styles.filter(s => styleIds.includes(s.id))
|
||
: HGB_Styles.slice();
|
||
if (!styles.length) styles = HGB_Styles.slice();
|
||
const idx = styles.findIndex(s => s.name === p.paletteName);
|
||
const next = styles[(idx < 0 ? 0 : idx + 1) % styles.length];
|
||
const gen = designState.avatarMode ? HGB_AvatarGen : HGB_Generator;
|
||
// 图标驱动:变体保持当前方案的锁定图标(不换其他图标)
|
||
const locked = !designState.avatarMode && p.lockedIcon;
|
||
const v = gen.generateVariant({
|
||
...designState.genParam,
|
||
...(locked ? { icons: [locked] } : {}),
|
||
layoutId: p.layoutId,
|
||
styleId: next.id,
|
||
lockup: designState.activeLockup
|
||
});
|
||
if (locked) v.lockedIcon = locked; // 变体延续锁定图标(供再次切换时保持同一图标)
|
||
v.skillAudit = auditPlan(v, designState.genParam.skills);
|
||
designState.activePlan = v;
|
||
renderPlanPreview();
|
||
renderPlans();
|
||
toast('🎨 已切换风格配色:' + v.paletteName + '(' + next.movement + ')');
|
||
}
|
||
|
||
/** 换布局变体:保持当前配色,切换到下一个布局模板 */
|
||
function variantLayout() {
|
||
const p = designState.activePlan;
|
||
if (!p || !designState.genParam) return;
|
||
// 头像模式仅在头像布局白名单内轮换;LOGO 模式排除头像专有布局
|
||
const layouts = designState.avatarMode
|
||
? HGB_Generator.LAYOUTS.filter(l => HGB_AvatarGen.isAvatarLayout(l.id))
|
||
: HGB_Generator.LAYOUTS.filter(l => !l.avatarOnly);
|
||
const idx = layouts.findIndex(x => x.id === p.layoutId);
|
||
const next = layouts[(idx + 1) % layouts.length];
|
||
const gen = designState.avatarMode ? HGB_AvatarGen : HGB_Generator;
|
||
// 图标驱动:变体保持当前方案的锁定图标(不换其他图标)
|
||
const locked = !designState.avatarMode && p.lockedIcon;
|
||
const v = gen.generateVariant({
|
||
...designState.genParam,
|
||
...(locked ? { icons: [locked] } : {}),
|
||
layoutId: next.id,
|
||
styleId: p.styleId, // 保持当前风格(配色自动跟随)
|
||
lockup: designState.activeLockup
|
||
});
|
||
if (locked) v.lockedIcon = locked; // 变体延续锁定图标(供再次切换时保持同一图标)
|
||
v.skillAudit = auditPlan(v, designState.genParam.skills);
|
||
designState.activePlan = v;
|
||
renderPlanPreview();
|
||
renderPlans();
|
||
toast('🅰 已切换布局:' + v.layoutName);
|
||
}
|
||
|
||
/** 基于当前参数再生成一批方案追加到方案库 */
|
||
async function variantMore() {
|
||
if (!designState.genParam) return;
|
||
let more;
|
||
if (designState.avatarMode) {
|
||
// 头像模式走 avatarGen(布局白名单约束)
|
||
more = await HGB_AvatarGen.generateAvatars({ ...designState.genParam, count: 6 });
|
||
} else if (designState.genParam.iconLocked && (designState.genParam.icons || []).length) {
|
||
// 图标驱动:保持「每图标一套数」结构,按总 6 套折算每图标追加套数
|
||
const n = designState.genParam.icons.length;
|
||
more = HGB_Generator.generatePlans({ ...designState.genParam, count: Math.max(1, Math.floor(6 / n)) });
|
||
} else {
|
||
// LOGO 模式走内置引擎
|
||
more = HGB_Generator.generatePlans({ ...designState.genParam, count: 6 });
|
||
}
|
||
for (const m of more) m.skillAudit = auditPlan(m, designState.genParam.skills);
|
||
designState.plans.push(...more);
|
||
renderPlans();
|
||
toast('➕ 已追加 ' + more.length + ' 套变体到方案库');
|
||
}
|
||
|
||
/** 当前预览弹窗中激活 Lockup 位置的元素数组 */
|
||
function activeLockupElements(p) {
|
||
const lp = (p.lockups || []).find(l => l.pos === designState.activeLockup);
|
||
return lp ? lp.elements : (p.elements || []);
|
||
}
|
||
|
||
/** 预览弹窗内直接导出当前方案(按当前选择的品牌组合位置;头像模式走平台规格弹窗) */
|
||
async function exportActive(fmt) {
|
||
const p = designState.activePlan;
|
||
if (!p) return;
|
||
if (designState.avatarMode && fmt === 'png') {
|
||
openExportModal('png');
|
||
return;
|
||
}
|
||
try {
|
||
const svgStr = HGB_Exporter.buildSvgFrom(
|
||
designState.avatarMode ? (p.markElements || []) : activeLockupElements(p),
|
||
p.bg, null, { trim: true });
|
||
if (fmt === 'svg') {
|
||
HGB_Exporter.exportSvg(svgStr);
|
||
toast('✅ 已导出 SVG 矢量图');
|
||
} else if (fmt === 'png') {
|
||
await HGB_Exporter.exportPng(1024, p.bg, svgStr);
|
||
toast('✅ 已导出 PNG(1024px)');
|
||
} else if (fmt === 'pdf') {
|
||
await HGB_Exporter.exportPdf(1024, p.bg, svgStr);
|
||
toast('✅ 已导出 PDF(1024px)');
|
||
}
|
||
} catch (err) {
|
||
toast('❌ 导出失败:' + err.message);
|
||
}
|
||
}
|
||
|
||
/* ================= 导出对话框 ================= */
|
||
function requireActivePlan() {
|
||
if (!designState.activePlan) {
|
||
toast('ℹ️ 请先「生成方案」并预览一个方案,再导出');
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function bindExportModal() {
|
||
const modal = document.getElementById('export-modal');
|
||
const formatBtns = modal.querySelectorAll('.format-btn');
|
||
|
||
// 头像平台规格(常驻渲染,头像模式显示)
|
||
document.getElementById('avatar-size-list').innerHTML =
|
||
HGB_AvatarGen.AVATAR_SIZES.map(s =>
|
||
`<label class="size-opt"><input type="checkbox" value="${s.size}" checked> ${esc(s.name)}<span class="size-note">${esc(s.note)}</span></label>`
|
||
).join('');
|
||
|
||
// 格式切换
|
||
formatBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
formatBtns.forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
exportFormat = btn.dataset.format;
|
||
updateExportOptions();
|
||
});
|
||
});
|
||
|
||
// 关闭
|
||
modal.querySelectorAll('[data-close]').forEach(btn => {
|
||
btn.addEventListener('click', closeExportModal);
|
||
});
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) closeExportModal();
|
||
});
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && !modal.classList.contains('hidden')) closeExportModal();
|
||
});
|
||
|
||
// 执行导出
|
||
document.getElementById('btn-export-go').addEventListener('click', runExport);
|
||
}
|
||
|
||
/** 打开导出对话框,preset 为默认选中的格式 */
|
||
function openExportModal(preset = 'png') {
|
||
exportFormat = preset;
|
||
const modal = document.getElementById('export-modal');
|
||
modal.querySelector('.modal-title').textContent =
|
||
designState.avatarMode ? '📤 导出头像' : '📤 导出 LOGO';
|
||
modal.querySelectorAll('.format-btn').forEach(b => {
|
||
b.classList.toggle('active', b.dataset.format === preset);
|
||
});
|
||
updateExportOptions();
|
||
modal.classList.remove('hidden');
|
||
}
|
||
|
||
function closeExportModal() {
|
||
document.getElementById('export-modal').classList.add('hidden');
|
||
}
|
||
|
||
/** 根据所选格式启用/禁用尺寸与背景选项;头像模式强制 PNG + 平台规格区 */
|
||
function updateExportOptions() {
|
||
const sizeGroup = document.getElementById('size-group');
|
||
const bgGroup = document.getElementById('bg-group');
|
||
const transparentOpt = document.getElementById('bg-transparent-opt');
|
||
const avatarGroup = document.getElementById('avatar-size-group');
|
||
const formatBtns = document.querySelectorAll('.format-btn');
|
||
|
||
if (designState.avatarMode) {
|
||
// 头像模式:只导出 PNG,显示平台规格区,隐藏通用尺寸与其它格式
|
||
exportFormat = 'png';
|
||
formatBtns.forEach(b => {
|
||
b.classList.toggle('active', b.dataset.format === 'png');
|
||
b.style.display = b.dataset.format === 'png' ? '' : 'none';
|
||
});
|
||
sizeGroup.classList.add('is-disabled');
|
||
avatarGroup.classList.remove('hidden');
|
||
bgGroup.classList.remove('is-disabled');
|
||
transparentOpt.classList.remove('hidden');
|
||
return;
|
||
}
|
||
// 恢复正常 LOGO 导出
|
||
formatBtns.forEach(b => { b.style.display = ''; });
|
||
avatarGroup.classList.add('hidden');
|
||
|
||
if (exportFormat === 'svg') {
|
||
// SVG:无尺寸/背景选项(矢量,自动包含画布背景)
|
||
sizeGroup.classList.add('is-disabled');
|
||
bgGroup.classList.add('is-disabled');
|
||
} else {
|
||
sizeGroup.classList.remove('is-disabled');
|
||
// PDF/JPG 不支持透明背景
|
||
if (exportFormat === 'png') {
|
||
bgGroup.classList.remove('is-disabled');
|
||
transparentOpt.classList.remove('hidden');
|
||
} else {
|
||
bgGroup.classList.remove('is-disabled');
|
||
transparentOpt.classList.add('hidden');
|
||
const r = document.querySelector('input[name="export-bg"][value="transparent"]');
|
||
if (r && r.checked) {
|
||
document.querySelector('input[name="export-bg"][value="design"]').checked = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 读取用户选择的背景色(null = 透明) */
|
||
function resolveBgColor() {
|
||
const r = document.querySelector('input[name="export-bg"]:checked');
|
||
const val = r ? r.value : 'design';
|
||
if (val === 'transparent') return null;
|
||
if (val === 'white') return '#FFFFFF';
|
||
return designState.activePlan ? (designState.activePlan.bg || '#FFFFFF') : '#FFFFFF';
|
||
}
|
||
|
||
/** 读取勾选的尺寸 */
|
||
function selectedSizes() {
|
||
const boxes = document.querySelectorAll('#size-group input[type="checkbox"]:checked');
|
||
return Array.from(boxes).map(b => parseInt(b.value, 10));
|
||
}
|
||
|
||
/** 执行导出(导出当前活动方案) */
|
||
async function runExport() {
|
||
if (!requireActivePlan()) return;
|
||
const goBtn = document.getElementById('btn-export-go');
|
||
goBtn.disabled = true;
|
||
|
||
try {
|
||
// 头像模式:多规格 PNG(平台 + 自定义),可选圆形裁切
|
||
if (designState.avatarMode) {
|
||
const mark = designState.activePlan.markElements || [];
|
||
const sizes = [];
|
||
document.querySelectorAll('#avatar-size-list input:checked').forEach(b => sizes.push(parseInt(b.value, 10)));
|
||
const custom = parseInt(document.getElementById('avatar-custom-size').value, 10);
|
||
if (custom >= 32 && custom <= 2048 && !sizes.includes(custom)) sizes.push(custom);
|
||
if (!sizes.length) {
|
||
toast('⚠️ 请至少选择一种平台规格');
|
||
return;
|
||
}
|
||
const round = document.getElementById('avatar-round').checked;
|
||
const bg = resolveBgColor();
|
||
// 透明背景时 SVG 内背景 rect 用 transparent(头像透明素材);否则用选定背景
|
||
const svgBg = bg === null ? 'transparent' : bg;
|
||
const svgStr = HGB_Exporter.buildSvgFrom(mark, svgBg, null, { trim: true });
|
||
const base = (document.getElementById('project-name').value || '我的品牌') + '_头像';
|
||
const results = await HGB_Exporter.exportAvatarPngs(svgStr, sizes, { bg, round, base });
|
||
toast(`✅ 已导出 ${results.length} 个头像(PNG${round ? ' · 圆形' : ''})`);
|
||
closeExportModal();
|
||
return;
|
||
}
|
||
|
||
const svgStr = HGB_Exporter.buildSvgFrom(
|
||
designState.activePlan.elements, designState.activePlan.bg, null, { trim: true });
|
||
if (exportFormat === 'svg') {
|
||
HGB_Exporter.exportSvg(svgStr);
|
||
toast('✅ 已导出 SVG 矢量图');
|
||
closeExportModal();
|
||
return;
|
||
}
|
||
|
||
const sizes = selectedSizes();
|
||
if (!sizes.length) {
|
||
toast('⚠️ 请至少选择一种尺寸规格');
|
||
return;
|
||
}
|
||
const bg = resolveBgColor();
|
||
|
||
let count = 0;
|
||
for (const size of sizes) {
|
||
if (exportFormat === 'png') await HGB_Exporter.exportPng(size, bg, svgStr);
|
||
else if (exportFormat === 'jpg') await HGB_Exporter.exportJpg(size, bg, svgStr);
|
||
else if (exportFormat === 'pdf') await HGB_Exporter.exportPdf(size, bg, svgStr);
|
||
count++;
|
||
}
|
||
toast(`✅ 已导出 ${count} 个文件(${exportFormat.toUpperCase()})`);
|
||
closeExportModal();
|
||
} catch (err) {
|
||
toast('❌ 导出失败:' + err.message);
|
||
} finally {
|
||
goBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
|
||
/* ================= 方案版本管理 ================= */
|
||
const LS_PLAN_VERSION_KEY = 'hgb_plan_versions';
|
||
|
||
function loadPlanVersions() {
|
||
try {
|
||
return JSON.parse(localStorage.getItem(LS_PLAN_VERSION_KEY)) || [];
|
||
} catch (e) { return []; }
|
||
}
|
||
|
||
function savePlanVersions(list) {
|
||
localStorage.setItem(LS_PLAN_VERSION_KEY, JSON.stringify(list.slice(0, 50)));
|
||
}
|
||
|
||
function bindVersionActions() {
|
||
document.getElementById('btn-save-version').addEventListener('click', () => {
|
||
if (!requireActivePlan()) return;
|
||
const versions = loadPlanVersions();
|
||
const brandName = (document.getElementById('design-brand').value || '').trim();
|
||
versions.unshift({
|
||
id: 'v_' + Date.now().toString(36),
|
||
time: Date.now(),
|
||
name: brandName || designState.activePlan.name,
|
||
plan: JSON.parse(JSON.stringify(designState.activePlan))
|
||
});
|
||
savePlanVersions(versions);
|
||
renderVersions();
|
||
toast('💾 已保存当前方案版本');
|
||
});
|
||
document.getElementById('btn-clear-versions').addEventListener('click', () => {
|
||
if (!loadPlanVersions().length) { toast('ℹ️ 暂无版本记录'); return; }
|
||
if (confirm('确定清空所有版本记录?')) {
|
||
savePlanVersions([]);
|
||
renderVersions();
|
||
toast('🧹 已清空版本记录');
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderVersions() {
|
||
const wrap = document.getElementById('version-list');
|
||
const list = loadPlanVersions();
|
||
if (!list.length) {
|
||
wrap.innerHTML = '<p class="pane-tip">暂无版本。预览方案后点击「💾 保存版本」记录设计</p>';
|
||
return;
|
||
}
|
||
wrap.innerHTML = list.map(v => {
|
||
const t = new Date(v.time);
|
||
const timeStr =
|
||
`${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')} ` +
|
||
`${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`;
|
||
return `
|
||
<div class="version-item" data-version="${v.id}">
|
||
<div class="version-name">${esc(v.name)}</div>
|
||
<div class="version-time">🕐 ${timeStr}</div>
|
||
<div class="version-ops">
|
||
<button class="tb-btn sm" data-vact="restore" title="恢复此方案到预览">↩ 恢复</button>
|
||
<button class="tb-btn sm" data-vact="del" title="删除">🗑</button>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
wrap.querySelectorAll('.version-item').forEach(item => {
|
||
const v = loadPlanVersions().find(x => x.id === item.dataset.version);
|
||
if (!v) return;
|
||
item.querySelectorAll('button').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const act = btn.dataset.vact;
|
||
if (act === 'restore') {
|
||
designState.activePlan = JSON.parse(JSON.stringify(v.plan));
|
||
renderPlans();
|
||
renderPlanPreview();
|
||
openPlanPreview(designState.activePlan);
|
||
toast('↩ 已恢复方案版本');
|
||
} else if (act === 'del') {
|
||
savePlanVersions(loadPlanVersions().filter(x => x.id !== v.id));
|
||
renderVersions();
|
||
toast('🗑 已删除该版本');
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
return { init, toast };
|
||
})();
|