Files
aurora-admin/tools/inject-icon-table.mjs
T
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 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 未做部分)
2026-09-21 10:05:48 +08:00

145 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
#!/usr/bin/env node
/**
* 图标表注入:把生成好的 core 图标表写进 11 个端实现的标记块里。
*
* 为什么要有这一步(而不是让 11 个端各自抄一份表):
* 11 份手抄的表必然漂移 —— 今天给 core.js 加一个图标,明天有个端忘了同步,
* 表现是"同一个 name 在 React 能渲染、在 Vue3 是空白",而且不会报错。
* 所以数据只有一份真源(core.js),各端只留**标记块**,内容由本脚本填。
* `tools/verify-icons.mjs` 会逐端比对标记块与 core.js,漂移当场拦下。
*
* 标记格式(各端按自己的注释语法):
* JS/CSS: /* === KOLE_ICON_TABLE:BEGIN === *​/ ... /* === KOLE_ICON_TABLE:END === *​/
* HTML: <!-- === KOLE_ICON_TABLE:BEGIN === --> ... <!-- === KOLE_ICON_TABLE:END === -->
*
* 注入后第 4 行是各自的分隔符写法差异 —— 见 FILES 表。
*
* 用法:
* node tools/inject-icon-table.mjs # 注入
* node tools/inject-icon-table.mjs --check # 只校验不写(CI 用)
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } 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 CORE_JS = join(ICON_DIR, 'core.js');
const REGISTRY = join(ICON_DIR, 'registry.json');
const ALIASES = join(ICON_DIR, 'aliases.json');
const CHECK = process.argv.includes('--check');
/** 需要注入的端文件(相对仓库根)。html 用 HTML 注释,其余用块注释。 */
const FILES = [
['frameworks/Icon.html', 'html'],
['frameworks/Icon.jsx', 'js'],
['frameworks/Icon.vue2.vue', 'js'],
['frameworks/Icon.vue3.vue', 'js'],
['frameworks-mobile/Icon.html', 'html'],
['frameworks-mobile/Icon.jsx', 'js'],
['frameworks-mobile/Icon.vue2.vue', 'js'],
['frameworks-mobile/Icon.vue3.vue', 'js'],
['frameworks-mobile/Icon.uniapp.vue', 'js'],
];
const BEGIN = '=== KOLE_ICON_TABLE:BEGIN ===';
const END = '=== KOLE_ICON_TABLE:END ===';
function markers(kind) {
return kind === 'html'
? [`<!-- ${BEGIN} -->`, `<!-- ${END} -->`]
: [`/* ${BEGIN} */`, `/* ${END} */`];
}
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(CORE_JS)) die(`缺 ${CORE_JS} —— 先跑 node tools/build-icons.mjs`);
/* 从 core.js 里取 CORE 字面量:它是 UMD 文件,require 一次最稳(不猜正则) */
const { createRequire } = await import('node:module');
const require2 = createRequire(import.meta.url);
const runtime = require2(CORE_JS);
const CORE = runtime.core;
const coreNames = Object.keys(CORE).sort();
if (!coreNames.length) die('core.js 里没有图标');
/* 别名表:各端运行时要用它把历史名映射到 canonical 名 */
const aliasMap = JSON.parse(readFileSync(ALIASES, 'utf8')).aliases;
/* 生成注入块。
*
* 关键设计:**别名在这里就折叠掉**,注入的是「任何名字 → 路径」的终态表。
* 这样 11 个端只需要「查表 + 拼 SVG」两行逻辑,不需要各自实现别名解析 ——
* 别名规则只有一处(build-icon-aliases.mjs),端上不可能漂移。
*
* 体积取舍:compact 单行 JSON。实测 core 151 个约 33KB/文件,9 个文件共约 300KB。
* 换来的是每个端文件**自包含**(拷走单个文件就能用,dist 无需额外依赖)。
* 仓库既有 4 个移动端端实现已经是「各自内联一份 9 名字形表」的写法,
* 这里是同一模式的规模化,不是新引入的坏味道。门禁逐端比对,漂移无法静默。
*/
function buildBlock(kind) {
const merged = {};
/* 先放 canonical,再放别名(别名不覆盖 canonical —— 别名表已保证不遮蔽真图标) */
for (const [name, paths] of Object.entries(CORE)) merged[name] = paths;
for (const [alias, target] of Object.entries(aliasMap)) {
if (!(alias in merged) && merged[target]) merged[alias] = merged[target];
}
const payload = {
sw: runtime.strokeWidth,
vb: runtime.viewBox,
i: merged,
};
/* 让 `</script` 不能提前闭合 HTML 里的数据块 */
const json = JSON.stringify(payload).replace(/</g, '\\u003c');
const count = Object.keys(merged).length;
const head = kind === 'html'
? `<!-- 生成物:tools/inject-icon-table.mjs 注入(勿手改)。数据真源 .design_library/kole-ui/icons/。\n ${count} 个名字(${Object.keys(CORE).length} canonical + 别名折叠)· 描边 ${runtime.strokeWidth} · 别名规则见 build-icon-aliases.mjs -->`
: `/* 生成物:tools/inject-icon-table.mjs 注入(勿手改)。数据真源 .design_library/kole-ui/icons/。\n ${count} 个名字(${Object.keys(CORE).length} canonical + 别名折叠)· 描边 ${runtime.strokeWidth} · 别名规则见 build-icon-aliases.mjs */`;
if (kind === 'html') {
return `${head}\n<script type="application/json" id="kole-icon-data">${json}</script>`;
}
return `${head}\nconst KOLE_ICON_DATA = ${json};`;
}
/* 注入 */
let changed = 0;
const missing = [];
for (const [rel, kind] of FILES) {
const abs = join(ROOT, rel);
if (!existsSync(abs)) { missing.push(rel); continue; }
const text = readFileSync(abs, 'utf8');
const [b, e] = markers(kind);
const bi = text.indexOf(b);
const ei = text.indexOf(e);
if (bi < 0 || ei < 0 || ei < bi) {
missing.push(`${rel}(缺标记对)`);
continue;
}
const block = buildBlock(kind);
const next = text.slice(0, bi + b.length) + '\n' + block + '\n' + text.slice(ei);
if (next !== text) {
changed++;
if (!CHECK) atomicWrite(abs, next);
}
}
if (missing.length) {
console.log(` 跳过 ${missing.length} 个文件(未就绪或缺标记):`);
missing.forEach((m) => console.log(' - ' + m));
}
console.log('图标表注入');
console.log(` core 图标 ${coreNames.length} 个 | 别名 ${Object.keys(aliasMap).length} 条`);
console.log(` ${CHECK ? '需更新' : '已更新'} ${changed} 个端文件 / 共 ${FILES.length} 个`);
if (CHECK && changed) {
die(`有 ${changed} 个端文件的图标表与 core.js 不一致 —— 跑 npm run build:icons 重新注入`);
}
console.log('OK');