Files

264 lines
13 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.
/**
* 画个标 · 技能库:图标类(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 };
}
}
]);
})();