Files

1525 lines
68 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 画个标 · 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, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/** 提取完整 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 };
})();