Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」, 那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」, 移动端站已是「K」;移动端文档站则完全没有 favicon)。 - 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边; 描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」 - 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark); 顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28)) - 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg) - 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留) - 移动端 7 个模板补 favicon(此前计数 0) 验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/ theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致; 两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。 ## 并行会话成果(本次一并入库) - 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具 + IconPreview 预览页 + ICON-SPEC.md 冻结规格 - 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁 - PC 组件:103 个大后台组件 / 组件11 批次 - uni-app:PC 端试点 + 移动端端实现 + 真实编译验证 ## 工程 - .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库) - CHANGELOG 补品牌标识条目 - ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
331 lines
16 KiB
JavaScript
331 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 别名构建:把上游各库的历史图标名映射到本仓库的 canonical 名。
|
||
*
|
||
* 为什么需要别名层(而不是把名字改成上游的样子):
|
||
* Element UI 的 283 个图标名是**图标字体**时代的名字,画的是「线」不是「形」——
|
||
* 它们不携带形状数据,只携带一个码点。我们要的是可渲染的 SVG,
|
||
* 所以必须为每个历史名字**指定**它对应哪个真实图标。
|
||
* 这一步是显式映射,不做模糊匹配:猜错了用户看到的就是错的图标,
|
||
* 而"看起来差不多"在静默处最危险(历史教训:6 次"文档声称完成但代码缺失")。
|
||
*
|
||
* 三类映射:
|
||
* identity 名字本身就在 registry 里(同名直通)
|
||
* stripped 去掉 -solid/-outline/-round/-<n> 后缀后命中
|
||
* semantic 需要人工指定(历史名与新名不同词)——见 SEMANTIC 表
|
||
*
|
||
* 产物:.design_library/kole-ui/icons/aliases.json
|
||
* 用法:node tools/build-icon-aliases.mjs
|
||
*/
|
||
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join } from 'node:path';
|
||
|
||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||
const ICON_DIR = join(ROOT, '.design_library', 'kole-ui', 'icons');
|
||
const REGISTRY = join(ICON_DIR, 'registry.json');
|
||
const OUT = join(ICON_DIR, 'aliases.json');
|
||
/* Element UI 历史名清单:由 tools/fetch-icon-sources.mjs 的取证阶段产出,
|
||
这里作为输入读入(离线:文件已在仓库里)。 */
|
||
const EL_NAMES = join(ICON_DIR, 'element-ui-names.txt');
|
||
|
||
function die(msg) { console.error('FAIL: ' + msg); process.exit(1); }
|
||
function atomicWrite(file, content) {
|
||
mkdirSync(dirname(file), { recursive: true });
|
||
const tmp = `${file}.tmp-${process.pid}`;
|
||
writeFileSync(tmp, content.replace(/\r\n/g, '\n'), 'utf8');
|
||
renameSync(tmp, file);
|
||
}
|
||
|
||
if (!existsSync(REGISTRY)) die(`缺 ${REGISTRY} —— 先跑 node tools/build-icons.mjs`);
|
||
const registry = JSON.parse(readFileSync(REGISTRY, 'utf8')).icons;
|
||
const names = new Set(Object.keys(registry));
|
||
const strip = (n) => n.replace(/-(solid|outline|round|filled)$/, '').replace(/-[0-9]+$/, '');
|
||
|
||
/**
|
||
* 语义映射表:历史名 → canonical 名。逐条要能说清"为什么是它"。
|
||
*
|
||
* 未列入的名字由下面的自动规则处理(同名 / 去后缀)。
|
||
* 规则覆盖不到的会进 `unresolved` 并被 verify 门禁拦下 —— 不允许静默丢弃。
|
||
*/
|
||
const SEMANTIC = {
|
||
/* Element UI 的后台菜单系列(s-*):每个都是"某类管理台入口",
|
||
TDesign 用 view-* 与 setting-* 系列表达同一批语义。 */
|
||
's-check': 'check-circle',
|
||
's-claim': 'file-copy',
|
||
's-comment': 'chat',
|
||
's-cooperation': 'usergroup',
|
||
's-custom': 'setting',
|
||
's-data': 'chart-bar',
|
||
's-finance': 'money',
|
||
's-flag': 'flag',
|
||
's-fold': 'folder',
|
||
's-goods': 'goods',
|
||
's-grid': 'grid',
|
||
's-help': 'help-circle',
|
||
's-home': 'home',
|
||
's-management': 'view-organization',
|
||
's-marketing': 'present',
|
||
's-open': 'unlock',
|
||
's-operation': 'control-platform',
|
||
's-opportunity': 'chart-bubble',
|
||
's-order': 'order',
|
||
's-platform': 'app',
|
||
's-promotion': 'loudspeaker',
|
||
's-release': 'send',
|
||
's-shop': 'shop',
|
||
's-ticket': 'ticket',
|
||
's-tools': 'tools',
|
||
's-unfold': 'menu-unfold',
|
||
|
||
/* 单点语义 */
|
||
'attract': 'star',
|
||
'bangzhu': 'help-circle',
|
||
'bank-card': 'creditcard',
|
||
'c-scale-to-original': 'fullscreen',
|
||
'close-notification': 'mute-notification',
|
||
'cloudy-and-sunny': 'cloudy-day',
|
||
'date': 'calendar',
|
||
'discover': 'compass',
|
||
'heavy-rain': 'rain-heavy',
|
||
'light-rain': 'rain-light',
|
||
'mobile-phone': 'mobile',
|
||
'news': 'article',
|
||
'picture-outline-round': 'image',
|
||
'platform-eleme': 'app',
|
||
'potato-strips': 'food',
|
||
'receiving': 'download',
|
||
'scissors': 'cut',
|
||
'star-off': 'star',
|
||
'star-on': 'star-filled',
|
||
'success': 'check-circle',
|
||
'table-lamp': 'lightbulb',
|
||
'tableware': 'fork-spoon',
|
||
'thumb': 'thumb-up',
|
||
'truck': 'undertake-delivery',
|
||
'turn-off-microphone': 'sound-mute',
|
||
'upload2': 'upload',
|
||
'water-cup': 'coffee',
|
||
/* Element UI 的 -left/-right 是被解析器截下的碎片(原为 d-arrow-left 等),
|
||
不是真实图标名,显式标注为"忽略"而不是映射到某个猜的图标。 */
|
||
'-left': null,
|
||
'-right': null,
|
||
};
|
||
|
||
/* 移动端既有 9 名字形表 → 新图标集(保持既有 4 端行为不破,见 ICON-SPEC §六)。
|
||
注意:check / close / star / plus / minus / more **本身已是 canonical 名且语义一致**,
|
||
不需要别名条目 —— 直接命中真实图标即可(更精确,也不会遮蔽真图标)。
|
||
只有旧名与新集用词不同的三个才需要映射。 */
|
||
const MOBILE_LEGACY = {
|
||
warn: 'warning', /* 旧名 warn,新集是 warning */
|
||
info: 'info-circle', /* 旧名 info 画的是 ⓘ(圆圈 i),对应 info-circle */
|
||
arrow: 'chevron-right', /* 旧名 arrow 画的是 ›(右箭头) */
|
||
};
|
||
|
||
const aliases = {};
|
||
const ignored = [];
|
||
const problems = [];
|
||
|
||
/* ① Element UI 历史名 */
|
||
if (!existsSync(EL_NAMES)) die(`缺 ${EL_NAMES}`);
|
||
const elNames = readFileSync(EL_NAMES, 'utf8')
|
||
.split(/\r?\n/)
|
||
.map((s) => s.trim())
|
||
/* 该文件带 `#` 头注释(来源/许可/提取命令),跳过 */
|
||
.filter((s) => s && s.length > 1 && !s.startsWith('#'));
|
||
for (const n of elNames) {
|
||
if (Object.prototype.hasOwnProperty.call(SEMANTIC, n)) {
|
||
const target = SEMANTIC[n];
|
||
if (target === null) { ignored.push(n); continue; }
|
||
if (!names.has(target)) { problems.push(`SEMANTIC['${n}'] → '${target}' 不存在`); continue; }
|
||
if (target !== n) aliases[n] = target;
|
||
continue;
|
||
}
|
||
if (names.has(n)) continue; /* 同名直通,不需要别名条目 */
|
||
const s = strip(n);
|
||
if (names.has(s)) { aliases[n] = s; continue; }
|
||
const cands = [...names].filter((x) => strip(x) === s).sort((a, b) => a.length - b.length);
|
||
if (cands.length) { aliases[n] = cands[0]; continue; }
|
||
problems.push(`无法解析 ${n}`);
|
||
}
|
||
|
||
/* ② 移动端历史名。
|
||
同一条铁律:如果某个旧名本身就是真实图标名,就**不许**再给它挂别名 ——
|
||
否则 `plus` 这种真图标会被旧语义改写(门禁会当场拦下,见文件末尾自检)。 */
|
||
for (const [from, to] of Object.entries(MOBILE_LEGACY)) {
|
||
if (!names.has(to)) { problems.push(`MOBILE_LEGACY['${from}'] → '${to}' 不存在`); continue; }
|
||
if (from === to) continue;
|
||
if (names.has(from)) { problems.push(`MOBILE_LEGACY['${from}'] 与真实图标同名,不许遮蔽`); continue; }
|
||
aliases[from] = to;
|
||
}
|
||
|
||
/* ③ 常见同义词(开发者直觉命名 → canonical) */
|
||
const SYNONYMS = {
|
||
'chevron-left': 'chevron-left', 'angle-left': 'chevron-left', 'caret-left': 'caret-left',
|
||
'x': 'close', 'cross': 'close', 'cancel': 'close',
|
||
'trash': 'delete', 'bin': 'delete', 'garbage': 'delete',
|
||
'pencil': 'edit', 'write': 'edit',
|
||
'magnifier': 'search', 'magnify': 'search', 'find': 'search',
|
||
'cog': 'setting', 'gear': 'setting', 'settings': 'setting',
|
||
'warning': 'warning', 'alert': 'warning', 'caution': 'warning', 'danger': 'warning',
|
||
'info': 'info-circle', 'information': 'info-circle',
|
||
'ok': 'check', 'tick': 'check', 'done': 'check',
|
||
'plus-circle': 'add-circle', 'minus-circle': 'minus-circle',
|
||
'person': 'user', 'account': 'user', 'profile': 'user',
|
||
'picture': 'image', 'photo': 'image',
|
||
'email': 'mail', 'envelope': 'mail',
|
||
'home': 'home', 'house': 'home',
|
||
'refresh': 'refresh', 'reload': 'refresh', 'sync': 'refresh',
|
||
'eye': 'view', 'eye-off': 'hide',
|
||
'lock': 'lock', 'unlock': 'unlock',
|
||
'clock': 'time', 'time': 'time', 'schedule': 'calendar',
|
||
'map-pin': 'location', 'pin-location': 'location',
|
||
'loading': 'loading', 'spinner': 'loading',
|
||
'star-o': 'star', 'star-solid': 'star-filled',
|
||
'heart-o': 'heart', 'heart-solid': 'heart-filled',
|
||
'thumb-up': 'thumb-up', 'like': 'thumb-up',
|
||
'cart': 'cart', 'shopping-cart': 'cart',
|
||
'money': 'money', 'currency': 'money', 'yuan': 'money',
|
||
'list': 'list', 'menu': 'menu', 'hamburger': 'menu',
|
||
'grid': 'grid', 'apps': 'grid', 'dashboard': 'dashboard',
|
||
'chart': 'chart', 'graph': 'chart', 'statistics': 'chart',
|
||
'folder-open': 'folder-open', 'directory': 'folder',
|
||
'file-text': 'file', 'document': 'file',
|
||
'download': 'download', 'upload': 'upload',
|
||
'share': 'share', 'print': 'print',
|
||
'copy': 'copy', 'duplicate': 'copy',
|
||
'filter': 'filter', 'funnel': 'filter',
|
||
'sort': 'sort', 'order': 'sort',
|
||
'more': 'more', 'more-horizontal': 'more', 'ellipsis': 'more',
|
||
'external-link': 'link', 'link': 'link', 'chain': 'link',
|
||
'save': 'save', 'floppy': 'save',
|
||
'print': 'print', 'printer': 'printer',
|
||
'calendar': 'calendar', 'date': 'calendar',
|
||
'bell': 'bell', 'notification': 'notification',
|
||
'message': 'message', 'chat': 'chat', 'comment': 'chat',
|
||
'user-group': 'usergroup', 'users': 'usergroup', 'team': 'usergroup',
|
||
'shield': 'shield-error', 'security': 'shield-error',
|
||
'key': 'key', 'password': 'lock',
|
||
'play': 'play', 'pause': 'pause', 'stop': 'stop',
|
||
'video': 'video', 'music': 'music', 'audio': 'sound',
|
||
'camera': 'camera', 'image-plus': 'image-add',
|
||
'cloud': 'cloud', 'server': 'server', 'database': 'server',
|
||
'code': 'code', 'terminal': 'terminal', 'command': 'terminal',
|
||
'bug': 'bug', 'debug': 'bug',
|
||
'terminal': 'terminal', 'console': 'terminal',
|
||
'wifi': 'wifi', 'signal': 'signal',
|
||
'battery': 'battery', 'power': 'poweroff',
|
||
'mobile': 'mobile', 'phone': 'phone', 'tablet': 'tablet',
|
||
'desktop': 'desktop', 'monitor': 'monitor', 'laptop': 'laptop',
|
||
'keyboard': 'keyboard', 'mouse': 'mouse', 'printer': 'printer',
|
||
'folder-plus': 'folder-add', 'file-plus': 'file-add',
|
||
'cart-plus': 'cart-add', 'user-plus': 'user-add',
|
||
'arrow-up-circle': 'arrow-up-circle', 'arrow-down-circle': 'arrow-down-circle',
|
||
'chevron-up': 'chevron-up', 'chevron-down': 'chevron-down',
|
||
'chevron-right': 'chevron-right', 'chevron-left': 'chevron-left',
|
||
'caret-up': 'caret-up', 'caret-down': 'caret-down',
|
||
'expand-more': 'chevron-down', 'expand-less': 'chevron-up',
|
||
'arrow-back': 'back', 'arrow-forward': 'arrow-right',
|
||
'reply': 'back', 'undo': 'back', 'redo': 'forward',
|
||
'fullscreen': 'fullscreen', 'full-screen': 'fullscreen', 'maximize': 'fullscreen',
|
||
'minimize': 'fullscreen', 'zoom-in': 'zoom-in', 'zoom-out': 'zoom-out',
|
||
'help': 'help-circle', 'question-circle': 'help-circle', 'support': 'help-circle',
|
||
'exclamation': 'warning', 'exclamation-circle': 'error-circle',
|
||
'check-circle-o': 'check-circle', 'check-circle-filled': 'check-circle-filled',
|
||
'close-circle-o': 'close-circle',
|
||
'plus-square': 'add-rectangle', 'minus-square': 'minus-rectangle',
|
||
'check-square': 'check-rectangle',
|
||
'square': 'rectangle', 'circle': 'circle',
|
||
'triangle-up': 'arrow-up', 'triangle-down': 'arrow-down',
|
||
'sort-asc': 'arrow-up', 'sort-desc': 'arrow-down', 'sort-ascending': 'arrow-up',
|
||
'unsorted': 'sort', 'sortable': 'sort',
|
||
'loading-spinner': 'loading', 'spinner-loading': 'loading',
|
||
'drag': 'move', 'move': 'move', 'hand': 'move',
|
||
'crop': 'crop', 'scissors': 'cut', 'cut': 'cut',
|
||
'paste': 'file-copy', 'clipboard': 'file-copy',
|
||
'select-all': 'check-rectangle', 'select': 'check',
|
||
'indeterminate': 'minus-rectangle',
|
||
'star-half': 'star', 'rate': 'star',
|
||
'gift': 'present', 'present': 'present', 'coupon': 'coupon',
|
||
'ticket': 'ticket', 'price': 'price-tag', 'tag': 'tag',
|
||
'flag': 'flag', 'bookmark': 'bookmark', 'collection': 'collection',
|
||
'wallet': 'wallet', 'bank': 'bank', 'credit-card': 'creditcard',
|
||
'order': 'order', 'goods': 'goods', 'shop': 'shop', 'store': 'store',
|
||
'delivery': 'undertake-delivery', 'truck': 'undertake-delivery', 'shipping': 'undertake-delivery',
|
||
'box': 'box', 'package': 'box', 'archive': 'folder',
|
||
'receive': 'download', 'send': 'send', 'publish': 'send',
|
||
'announcement': 'loudspeaker', 'speaker': 'loudspeaker', 'broadcast': 'loudspeaker',
|
||
'megaphone': 'loudspeaker', 'promotion': 'loudspeaker',
|
||
'news': 'article', 'article': 'article', 'blog': 'article',
|
||
'book': 'book', 'notebook': 'notebook', 'note': 'file',
|
||
'reading': 'book-open', 'book-open': 'book-open',
|
||
'graduation': 'education', 'school': 'education', 'education': 'education',
|
||
'workspace': 'app', 'application': 'app', 'platform': 'app',
|
||
'component': 'component', 'widget': 'widget', 'module': 'widget',
|
||
'plugin': 'extension', 'extension': 'extension', 'puzzle': 'extension',
|
||
'integration': 'api', 'api': 'api', 'interface': 'api',
|
||
'webhook': 'link', 'endpoint': 'api',
|
||
'log': 'history', 'history': 'history', 'audit': 'history',
|
||
'version': 'history', 'release': 'send', 'deploy': 'cloud-upload',
|
||
'backup': 'cloud-download', 'restore': 'refresh',
|
||
'migration': 'transform', 'transfer': 'transform',
|
||
'import': 'upload', 'export': 'download',
|
||
'template': 'file-copy', 'snippet': 'code',
|
||
'form': 'file', 'survey': 'file',
|
||
'workflow': 'flowchart', 'flow': 'flowchart', 'process': 'flowchart',
|
||
'pipeline': 'flowchart', 'automation': 'control-platform',
|
||
'report': 'chart-bar', 'analytics': 'chart-bar', 'insight': 'chart-bar',
|
||
'metric': 'chart-line', 'kpi': 'chart-line', 'trend': 'chart-line',
|
||
'pie': 'chart-pie', 'donut': 'chart-ring',
|
||
'funnel-chart': 'chart-bubble', 'scatter': 'chart-scatter',
|
||
'radar-chart': 'chart-radar', 'gauge-chart': 'chart-radar',
|
||
'heatmap': 'chart-bar',
|
||
'table-chart': 'chart-bar', 'pivot': 'table',
|
||
'model': 'widget', 'dataset': 'data-base', 'etl': 'flowchart',
|
||
'ai': 'ai', 'robot': 'robot', 'assistant': 'chat-bubble',
|
||
'prompt': 'ai-edit', 'generate': 'ai', 'magic': 'magic',
|
||
};
|
||
|
||
for (const [from, to] of Object.entries(SYNONYMS)) {
|
||
if (from === to) continue; /* 同名不需要条目 */
|
||
if (!names.has(to)) continue; /* 目标不存在则跳过(由下面的统计报出) */
|
||
if (!names.has(from)) aliases[from] = to; /* 只有当它不是真实图标名时才做别名 */
|
||
}
|
||
|
||
const missingSyn = Object.entries(SYNONYMS).filter(([f, t]) => f !== t && !names.has(t)).map(([f, t]) => `${f}→${t}`);
|
||
if (missingSyn.length) console.log(` 注意:${missingSyn.length} 条同义词的目标不存在(已跳过):${missingSyn.slice(0, 8).join(', ')}`);
|
||
/* 被真图标挡下的同义词:记下来,不是错误但要知道有这么回事 */
|
||
const shadowedSyn = Object.entries(SYNONYMS).filter(([f, t]) => f !== t && names.has(f)).map(([f, t]) => `${f}(真图标)→${t}(未采用)`);
|
||
if (shadowedSyn.length) {
|
||
console.log(` 注意:${shadowedSyn.length} 条同义词因「键本身已是真实图标」而未采用(真图标优先):`);
|
||
console.log(` ${shadowedSyn.slice(0, 10).map((x) => x.split('(')[0]).join(', ')}${shadowedSyn.length > 10 ? ' …' : ''}`);
|
||
}
|
||
|
||
/* 自检 */
|
||
if (problems.length) {
|
||
console.error('无法解析的名字:');
|
||
problems.forEach((p) => console.error(' ' + p));
|
||
die(`${problems.length} 个名字没有落点 —— 别名层不许静默丢弃`);
|
||
}
|
||
for (const [a, t] of Object.entries(aliases)) {
|
||
if (!names.has(t)) die(`别名 ${a} → ${t} 指向不存在的图标`);
|
||
if (names.has(a) && a !== t) die(`别名 ${a} 与真实图标同名,会遮蔽真实图标`);
|
||
}
|
||
|
||
const payload = {
|
||
schemaVersion: 1,
|
||
generatedBy: 'tools/build-icon-aliases.mjs',
|
||
note: '别名只改查找路径,不产生新图标。identity(同名)不在此表中。',
|
||
ignored: ignored.sort(),
|
||
stats: { total: Object.keys(aliases).length, elementUI: elNames.length, mobileLegacy: Object.keys(MOBILE_LEGACY).length },
|
||
aliases,
|
||
};
|
||
atomicWrite(OUT, JSON.stringify(payload, null, 2));
|
||
|
||
console.log('图标别名构建');
|
||
console.log(` 别名总数 ${Object.keys(aliases).length}`);
|
||
console.log(` Element UI 历史名 ${elNames.length}(其中 ${ignored.length} 个标记为忽略:${ignored.join(', ')})`);
|
||
console.log(` 移动端历史名 ${Object.keys(MOBILE_LEGACY).length}`);
|
||
console.log(` 写入 ${OUT}(${(JSON.stringify(payload).length / 1024).toFixed(0)}KB)`);
|
||
console.log('OK');
|