Files
aurora-admin/tools/gen-families.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

196 lines
7.6 KiB
JavaScript
Raw 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.
#!/usr/bin/env node
/**
* gen-families.mjs - 族层数据生成器(契约层)
*
* 产物:
* 1. .design_library/kole-ui/families.json 族模型快照(build-site.ps1 消费)
* 2. .design_library/kole-ui/components/<slug>.json 注入 family / familyRole / familyParams
*
* 注入策略:**定点插入**,不重排既有 JSON —— 只在 "slug" 行之后插入三个字段,
* 其余字节保持不变,保证 git diff 最小且不覆盖他人未提交改动。
*
* 用法:
* node tools/gen-families.mjs # 生成(原子写)
* node tools/gen-families.mjs --dry-run # 只报将要发生的改动
* node tools/gen-families.mjs --check # 校验磁盘状态与模型一致,不一致则退出码 1
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { FAMILIES, IMPLEMENTED_FAMILIES, memberIndex } from './lib/family-model.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const LIB = path.join(ROOT, '.design_library', 'kole-ui');
const COMPONENTS = path.join(LIB, 'components');
const INDEX_FILE = path.join(COMPONENTS, 'index.json');
const FAMILIES_FILE = path.join(LIB, 'families.json');
const MODE = process.argv.includes('--dry-run') ? 'dry' : process.argv.includes('--check') ? 'check' : 'write';
const problems = [];
let changed = 0;
let unchanged = 0;
function writeAtomic(file, content) {
const tmp = file + '.tmp';
fs.writeFileSync(tmp, content, 'utf8');
fs.renameSync(tmp, file);
}
/** 渲染要插入的三行族字段(2 空格缩进,与既有契约格式一致) */
function familyLines(entry) {
const params = JSON.stringify(entry.params || {});
return (
` "family": ${JSON.stringify(entry.family)},\n` +
` "familyRole": ${JSON.stringify(entry.role)},\n` +
` "familyParams": ${params},\n`
);
}
/** 移除既有族字段(幂等重跑),返回 { text, had } */
function stripFamily(text) {
const re = /^ {2}"family(?:Role|Params)?":.*\n?/gm;
const had = re.test(text);
return { text: text.replace(/^ {2}"family(?:Role|Params)?":.*\n?/gm, ''), had };
}
function main() {
if (!fs.existsSync(INDEX_FILE)) {
console.error('FAIL: 找不到 ' + path.relative(ROOT, INDEX_FILE));
process.exit(2);
}
const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf8'));
const bySlug = new Map(index.components.map((c) => [c.slug, c]));
/* ---- 校验模型自身 ---- */
const entries = memberIndex(); // 同一 slug 属于多族会在此抛错
const baseSlugs = new Set(FAMILIES.map((f) => f.base));
for (const f of FAMILIES) {
if (!bySlug.has(f.base)) problems.push(`族 ${f.id} 的基座 ${f.base} 不在 index.json 中`);
if (!f.members.some((m) => m.slug === f.base)) problems.push(`族 ${f.id} 的基座 ${f.base} 未登记为成员`);
const bases = f.members.filter((m) => m.role === 'base');
if (bases.length !== 1) problems.push(`族 ${f.id} 应恰好 1 个 base,实际 ${bases.length}`);
for (const m of f.members) {
if (!bySlug.has(m.slug)) problems.push(`族 ${f.id} 成员 ${m.slug} 不在 index.json 中`);
const dims = (f.paramSurface || []).map((p) => p.name);
for (const k of Object.keys(m.params || {})) {
if (!dims.includes(k)) problems.push(`族 ${f.id} 成员 ${m.slug} 的参数 ${k} 未在 paramSurface 声明`);
}
}
for (const p of f.paramSurface || []) {
if (p.type === 'enum') {
if (!Array.isArray(p.values) || p.values.length < 2) problems.push(`族 ${f.id} 参数 ${p.name} 为 enum 但取值少于 2`);
for (const m of f.members) {
const v = (m.params || {})[p.name];
if (v !== undefined && !p.values.map(String).includes(String(v))) {
problems.push(`族 ${f.id} 成员 ${m.slug} 的 ${p.name}=${v} 不在 enum 取值内`);
}
}
}
}
}
for (const id of IMPLEMENTED_FAMILIES) {
if (!FAMILIES.some((f) => f.id === id)) problems.push(`IMPLEMENTED_FAMILIES 含未定义族 ${id}`);
}
if (problems.length) {
console.error('FAIL: 族模型自检未通过');
for (const p of problems) console.error(' - ' + p);
process.exit(2);
}
/* ---- 产物 1:families.json ---- */
const payload = {
schemaVersion: 1,
sourceKind: 'family-model',
generatedBy: 'tools/gen-families.mjs',
note: '由 tools/lib/family-model.mjs 生成,勿手改。族把「同源组件」表达为「1 个基座 + N 个参数取值」;组件身份(slug)保持不变,data.json 仍完整输出 79 个组件。',
totalFamilies: FAMILIES.length,
totalMembers: Object.keys(entries).length,
implementedFamilies: IMPLEMENTED_FAMILIES,
families: FAMILIES.map((f) => ({
...f,
basePrefix: bySlug.get(f.base) ? bySlug.get(f.base).frameworksPrefix : null,
memberCount: f.members.length,
members: f.members.map((m) => ({
...m,
frameworksPrefix: bySlug.get(m.slug) ? bySlug.get(m.slug).frameworksPrefix : null,
})),
})),
};
const json = JSON.stringify(payload, null, 2) + '\n';
const prev = fs.existsSync(FAMILIES_FILE) ? fs.readFileSync(FAMILIES_FILE, 'utf8') : null;
if (prev === json) {
console.log('= families.json 无变化');
} else if (MODE === 'check') {
problems.push('families.json 与模型不一致(需重跑 gen-families.mjs)');
} else if (MODE === 'dry') {
console.log('~ 将写入 families.json(' + json.length + ' bytes)');
} else {
writeAtomic(FAMILIES_FILE, json);
console.log('+ families.json 写入 ' + json.length + ' bytes / ' + FAMILIES.length + ' 族 / ' + payload.totalMembers + ' 成员');
}
/* ---- 产物 2:79 份契约注入 ---- */
const touched = [];
for (const slug of Object.keys(entries).sort()) {
const file = path.join(COMPONENTS, slug + '.json');
if (!fs.existsSync(file)) {
problems.push('找不到契约 ' + slug + '.json');
continue;
}
const raw = fs.readFileSync(file, 'utf8');
const { text: stripped, had } = stripFamily(raw);
const anchor = ` "slug": ${JSON.stringify(slug)},\n`;
if (!stripped.includes(anchor)) {
problems.push(slug + '.json 缺少锚点行 ' + JSON.stringify(anchor.trim()) + ',跳过注入');
continue;
}
const next = stripped.replace(anchor, anchor + familyLines(entries[slug]));
let parsed;
try {
parsed = JSON.parse(next);
} catch (e) {
problems.push(slug + '.json 注入后 JSON 解析失败: ' + e.message);
continue;
}
if (parsed.family !== entries[slug].family) {
problems.push(slug + '.json 注入结果字段不符');
continue;
}
if (next === raw && had) {
unchanged++;
continue;
}
if (MODE === 'check') {
problems.push(slug + '.json 族字段缺失或过期');
continue;
}
if (MODE === 'dry') {
touched.push(slug);
continue;
}
writeAtomic(file, next);
touched.push(slug);
}
if (touched.length) changed = touched.length;
console.log(
`\n${MODE === 'check' ? '[check]' : MODE === 'dry' ? '[dry-run]' : '[write]'} ` +
`族 ${FAMILIES.length} / 登记成员 ${payload.totalMembers} / 契约改动 ${changed} / 已最新 ${unchanged}`
);
if (touched.length) console.log(' 改动契约:' + touched.join(' '));
const rest = index.components.filter((c) => !entries[c.slug]);
console.log(` 独立组件(不属任何族):${rest.length} 个 — ${rest.map((c) => c.slug).slice(0, 12).join(' ')}${rest.length > 12 ? ' …' : ''}`);
if (problems.length) {
console.error('\nFAIL:');
for (const p of problems) console.error(' - ' + p);
process.exit(1);
}
console.log('\nOK: 族层数据一致');
}
main();