/** * 画个标 · 技能库:图标类(icon) * 注册:HGB_Skills.register([...]) */ (function () { 'use strict'; const U = HGB_Skills.utils; HGB_Skills.register([ { id: 'icon-match', name: '图标语义检索', category: 'icon', emoji: '🪄', scene: '从内置 32 万+ 图标库中按语义关键词检索图标', summary: '在内置多源图标库(Feather/Lucide/Tabler 等 197 源)中按名称关键词模糊搜索,返回匹配图标(源:名称),供 AI 与用户选择。', params: [ { name: 'keyword', type: 'string', required: true, desc: '语义关键词(如 graduation / rocket / heart)' } ], source: 'GitHub: 内置图标库 197 源(Iconify + GitHub 聚合)', run: function (args) { return { keyword: args.keyword, hits: U.searchIcon(args.keyword) }; }, test: function () { return { rocket: U.searchIcon('rocket').slice(0, 3), graduation: U.searchIcon('graduation').slice(0, 3) }; } }, { id: 'icon-consistency', name: '图标风格一致性', category: 'icon', emoji: '🎯', scene: '同一方案多图标风格一致性检查', summary: '检查方案中多个图标是否同源同风(线条/填充/圆角一致):混用不同来源图标会破坏品牌统一感,给出统一建议。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(多源图标库统一性原则)', run: function (args) { const els = (args.plan || {}).elements || []; const icons = els.filter(el => el.type === 'icon' && el.iconId && !['ring', 'deco-bar'].includes(el.iconId)); if (icons.length < 2) { return { pass: true, note: '仅 1 个图标或无需一致性检查', iconCount: icons.length }; } const sources = new Map(); for (const ic of icons) { const src = String(ic.source || (ic.id || '').split(':')[0] || 'unknown'); sources.set(src, (sources.get(src) || 0) + 1); } const uniq = [...sources.entries()].map(([s, n]) => s + '×' + n); return { pass: sources.size === 1, iconCount: icons.length, sources: uniq, verdict: sources.size === 1 ? '图标同源统一,风格一致' : `混用 ${sources.size} 个来源(${uniq.join('、')}),建议统一为同一来源图标` }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '一致性测试' }); return this.run({ plan }); } }, { id: 'icon-tone', name: '图标明暗适配', category: 'icon', emoji: '🌗', scene: '图标在深/浅背景下的可读性检查', summary: '根据方案背景色明暗,检查图标/主色是否清晰可见:深底配深色图标、浅底配浅色图标都会降低可读性。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements, bg}' } ], source: '自研(背景明暗 × 前景对比)', run: function (args) { const plan = args.plan || {}; const bg = plan.bg || '#FFFFFF'; const bgDark = U.lum(bg) < 0.5; const els = plan.elements || []; const icon = els.find(el => el.type === 'icon' && el.iconId && !['ring', 'deco-bar'].includes(el.iconId)); const text = els.find(el => el.type === 'text'); const fg = (icon && icon.stroke) || (text && text.fill) || '#333333'; const fgDark = U.lum(fg) < 0.5; const mismatch = bgDark === fgDark; return { pass: !mismatch, bg: bg, bgTone: bgDark ? '深色' : '浅色', fg: fg, fgTone: fgDark ? '深色' : '浅色', verdict: mismatch ? '背景与主体同向明暗,可读性偏低,建议调整主色或背景' : '背景与主体明暗互补,可读性良好' }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '明暗测试' }); return this.run({ plan }); } }, { id: 'icon-count', name: '图标数量检查', category: 'icon', emoji: '🔢', scene: 'LOGO 图标元素数量与复杂度检查', summary: '检查方案中的图标数量:专业 LOGO 通常 1 个主图标(装饰环除外),图标过多会稀释辨识度,建议精简。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(极简设计原则量化)', run: function (args) { const els = (args.plan || {}).elements || []; const icons = els.filter(el => el.type === 'icon' && el.iconId && !['ring', 'deco-bar'].includes(el.iconId)); const n = icons.length; const pass = n <= 2; return { pass, count: n, icons: icons.map(ic => ic.iconId), verdict: n === 0 ? '无图标(纯文字标)' : (pass ? '图标数量适中,辨识度高' : '图标多达 ' + n + ' 个,易稀释品牌辨识度,建议精简为 1 个主图标'), suggest: n > 2 ? '合并/删除次要图标,保留最具辨识度的一个' : '无需调整' }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '图标数测试' }); return this.run({ plan }); } }, { id: 'icon-text-ratio', name: '图标文字比例', category: 'icon', emoji: '🔍', scene: '图标与品牌文字的大小比例检查', summary: '检查主图标与主文字的宽度比例:图标过小失去存在感、过大挤压文字,理想区间约 0.6~1.5。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(图标/文字比例规范)', run: function (args) { const els = (args.plan || {}).elements || []; const icon = els.find(el => el.type === 'icon' && el.iconId && !['ring', 'deco-bar'].includes(el.iconId)); const text = els.find(el => el.type === 'text' && el.visible !== false); if (!icon || !text) return { pass: true, note: '缺少图标或文字,跳过比例检查' }; const bi = U.elBox(icon), bt = U.elBox(text); const ratio = Math.round((bi.w / bt.w) * 100) / 100; const good = ratio >= 0.6 && ratio <= 1.5; return { pass: good, ratio, iconW: Math.round(bi.w), textW: Math.round(bt.w), verdict: good ? '图标/文字宽度比 ' + ratio + ',主次协调' : (ratio > 1.5 ? '图标偏大(' + ratio + '),建议缩小图标或加大文字' : '图标偏小(' + ratio + '),建议放大图标增强存在感'), suggest: good ? '无需调整' : (ratio > 1.5 ? '缩小图标或加大文字' : '放大图标或缩短文字') }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '比例测试' }); return this.run({ plan }); } }, { id: 'icon-search-cn', name: '中文语义图标检索', category: 'icon', emoji: '🔍', scene: '用中文关键词(分类/语义)检索内置图标', summary: '在内置图标索引(含中文分类)中按中文关键词检索:支持「教育」「餐饮」「医疗」「科技」等中文分类词,返回图标 id/名称/分类,方便中文用户选图。', params: [ { name: 'keyword', type: 'string', required: true, desc: '中文关键词(如 教育 / 餐饮 / 医疗 / 科技)' } ], source: 'GitHub: 内置图标索引中文分类(categoryCn)', run: function (args) { const kw = String(args.keyword || '').toLowerCase(); if (!kw) return { keyword: args.keyword, hits: [], note: '请输入中文关键词' }; const idx = (typeof HGB_BuiltinIcons !== 'undefined' && HGB_BuiltinIcons.iconIndex) || []; const hits = []; for (const ic of idx) { const cat = String(ic.category || '').toLowerCase(); const name = String(ic.name || '').toLowerCase(); if (cat.includes(kw) || name.includes(kw)) { hits.push({ id: ic.id, name: ic.name, category: ic.category }); if (hits.length >= 20) break; } } return { keyword: args.keyword, hits }; }, test: function () { return { edu: this.run({ keyword: '教育' }).hits.slice(0, 3), food: this.run({ keyword: '餐饮' }).hits.slice(0, 3) }; } }, { id: 'icon-padding', name: '图标安全边距检查', category: 'icon', emoji: '🖼️', scene: '检查图标图形是否贴边(缺乏安全边距)', summary: '解析图标 path 坐标,检测图形是否触及 24 网格边缘:贴边图标在叠加底色/圆形裁切时易被切边,输出最小边距与建议。', params: [ { name: 'svg', type: 'string', required: true, desc: '图标 SVG body(path d 片段)' } ], source: '自研(图标网格安全边距规范)', run: function (args) { const body = String(args.svg || ''); const nums = []; const re = /\bd="([^"]+)"/g; let m; while ((m = re.exec(body))) { for (const n of m[1].match(/-?\d*\.?\d+(?:e[+-]?\d+)?/gi) || []) { const v = parseFloat(n); if (v >= -1000 && v <= 1000) nums.push(v); } } if (!nums.length) return { pass: true, note: '未解析到 path 坐标(可能为图形类图标)' }; const min = Math.min(...nums), max = Math.max(...nums); const padL = Math.round(min * 100) / 100, padR = Math.round((24 - max) * 100) / 100; const pass = min >= 0.8 && max <= 23.2; return { pass, min, max, paddingLeft: Math.max(0, padL), paddingRight: Math.max(0, padR), verdict: pass ? '图形在 24 网格内留有安全边距(约 ' + Math.round(Math.min(padL, padR) * 10) / 10 + 'px)' : '图形贴近/超出网格边缘(左 ' + padL + ' 右 ' + padR + '),叠加底色或圆形裁切时易被切边', suggest: pass ? '无需调整' : '优先选择边距更足的图标,或为图标叠加留白容器(badge/圆环)' }; }, test: function () { const tight = ''; const safe = ''; return { tight: this.run({ svg: tight }).pass, safe: this.run({ svg: safe }).pass }; } }, { id: 'stroke-consistency', name: '图标描边一致性', category: 'icon', emoji: '✏️', scene: '同一方案多个图标的描边粗细一致性检查', summary: '检查方案中多个图标的描边粗细(stroke-width)是否统一:混用不同粗细体系(2px / 1.5px)的图标群会显得杂乱,输出各图标描边清单与统一建议。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(图标描边体系规范)', run: function (args) { const els = (args.plan || {}).elements || []; const icons = els.filter(el => el.type === 'icon' && el.iconId && !['ring', 'deco-bar'].includes(el.iconId)); if (icons.length < 2) return { pass: true, note: '仅 1 个图标或无需检查', count: icons.length }; const DEF_STROKE = { lucide: 2, feather: 2, tabler: 1.5, 'heroicons-outline': 1.5, phosphor: 2 }; const rows = icons.map(ic => { const src = String(ic.source || (ic.id || '').split(':')[0] || '').toLowerCase(); const sw = ic.strokeWidth != null ? Number(ic.strokeWidth) : (DEF_STROKE[src] != null ? DEF_STROKE[src] : null); return { iconId: ic.iconId, source: src, strokeWidth: sw }; }); const known = rows.filter(r => r.strokeWidth != null); if (!known.length) return { pass: true, note: '未解析到描边数据(可能为填充图标),跳过', count: icons.length }; const widths = [...new Set(known.map(r => r.strokeWidth))]; const pass = widths.length === 1; return { pass, rows, strokeWidths: widths, verdict: pass ? '图标描边统一(' + widths[0] + 'px),线条体系一致' : '图标描边不统一(' + widths.join('px / ') + 'px),混用不同粗细体系易显杂乱', suggest: pass ? '无需调整' : '统一为同一描边体系(推荐 2px:小尺寸清晰、与多数图标集一致),或整体换用填充图标' }; }, test: function () { const same = { plan: { elements: [{ type: 'icon', iconId: 'x', source: 'lucide' }, { type: 'icon', iconId: 'y', source: 'feather' }] } }; const diff = { plan: { elements: [{ type: 'icon', iconId: 'x', source: 'tabler' }, { type: 'icon', iconId: 'y', source: 'lucide' }] } }; return { same: this.run(same).pass, diffBad: this.run(diff).pass === false, widths: this.run(diff).strokeWidths.length === 2 }; } } ]); })();