/** * 画个标 · 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, '"'); } /** 提取完整 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 => ` ${ind.name} `).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 `
${st.emoji} ${st.step}. ${st.name} ${list.length} 技能 ✓ ${checked} ${allOn ? '已全选' : (checked ? '部分' : '未选')}
${list.map(s => { const on = designState.skills.includes(s.id); return ` `; }).join('')}
`; }).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 = [ ``, ...HGB_Styles.map(s => ` `) ].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 = '

未选图标。可点下方「📦 内置库浏览选择」挑选,或直接让 AI 自动选用。

'; 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 `
${body} ${esc(ic.name)}
`; }).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 = '

⏳ 正在加载素材源…

'; try { await HGB_IconLib.loadSource(builtinState.source); } catch (e) { console.warn('[builtin] 素材源加载失败:', e); if (grid) grid.innerHTML = '

❌ 素材源加载失败:' + esc(e.message) + '

'; } 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 ``; }).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 ``; }).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 = `` + cats.map(c => ``).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 = '

⏳ 正在加载素材源…

'; 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 `
${esc(ic.name)} ${body}
`; }).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 => ` `).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 = '
🧸 正在组合风格部件…
'; try { await HGB_Dicebear.loadStyle(dicebearStyle); } catch (e) { grid.innerHTML = '
⚠️ 风格定义加载失败:' + esc(e.message) + '
'; 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 '
' + '
' + svgInline(c.svg) + '
' + '
' + (HGB_Dicebear.CN[dicebearStyle] || dicebearStyle) + ' · ' + esc(c.seed.slice(-4)) + '
' + '
'; }).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 = '✅ 已保存'; toast('🤖 AI 设置已保存'); }); document.getElementById('btn-ai-clear').addEventListener('click', () => { HGB_AI.clearKey(); keyInput.value = HGB_AI.getKey(); // 清除本机 Key 后为空 testResult.innerHTML = '🧹 已清除本机 API Key(未配置时 AI 降级为内置引擎)'; 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 = '请先填写 API Key'; return; } testResult.innerHTML = '⏳ 正在测试连接…'; try { const r = await HGB_AI.testConnection(); testResult.innerHTML = '✅ 连接成功:' + esc(r) + ''; } catch (err) { const map = { NO_KEY: '未填写 Key', AUTH: 'API Key 无效(401)', BALANCE: '余额不足(402)', RATE_LIMIT: '请求过于频繁(429)', NETWORK: '网络错误,请检查网络/代理', EMPTY: 'AI 返回为空' }; testResult.innerHTML = '❌ ' + (map[err.code] || esc(err.message)) + ''; } }); } /** 技能检查小结(方案卡片:评分 + 通过/失败统计 + 失败项提示;完整明细见预览弹窗) */ 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 ? `🧩 ${score}` : ''; const summary = `${items.length} 项 ✓ ${ok.length}${failed.length ? ' ⚠ ' + failed.length : ''}`; const badges = failed.slice(0, 3).map(it => `${esc(it.name)}⚠`).join(''); return `
${scoreHtml}${summary}${badges}
`; } /** 渲染方案库卡片(卡片内同时展示 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 ? `
${svgInline(markSvg)}
` : `
${svgInline(markSvg)}
${svgInline(HGB_Exporter.buildSvgFrom(p.elements || [], p.bg, null, { trim: true }))}
`; return `
${previewsHtml}
${esc(p.name)}
${p.concept ? `
💡 ${esc(p.concept)}
` : ''} ${p.warnings && p.warnings.length ? `
⚠ ${esc(p.warnings.join('、'))}
` : ''} ${renderAuditBadge(p.skillAudit)}
🏷 ${esc(p.layoutName)} 🎨 ${esc(p.paletteName)} ${p.aiIcon ? `🎯 ${esc(p.aiIcon.split(':')[1] || p.aiIcon)}` : ''} ${p.ai ? '🤖 AI' : ''}
`; }).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) => `` ).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 ? `
💡 设计理念:${esc(p.concept)}
` : ''; 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 ? `🧩 ${a.score}` : ''; el.innerHTML = `
🔬 技能检查报告 (方案评审 · 共 ${a.items.length} 项 · ✓ ${ok.length} / ⚠ ${failed.length})
${scoreHtml}${a.score != null ? (a.score >= 90 ? '质量优秀' : a.score >= 70 ? '基本达标' : '需重点优化') : ''}
${a.items.map(it => `
${it.ok ? '✓' : '⚠'} ${esc(it.name)} ${esc(it.detail)}
`).join('')}
`; } /** 换配色变体:保持当前布局,切换风格色系(配色/背景/字重随风格整体切换) */ 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 => `` ).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 = '

暂无版本。预览方案后点击「💾 保存版本」记录设计

'; 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 `
${esc(v.name)}
🕐 ${timeStr}
`; }).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 }; })();