264 lines
13 KiB
JavaScript
264 lines
13 KiB
JavaScript
/**
|
||
* 画个标 · 技能库:图标类(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 = '<path d="M0 0h24v24H0z"/>';
|
||
const safe = '<path d="M2 2h20v20H2z"/>';
|
||
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 };
|
||
}
|
||
}
|
||
]);
|
||
})();
|