Files
aurora-admin/tools/verify-api-docs.mjs
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

111 lines
6.4 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.
#!/usr/bin/env node
/**
* verify-api-docs.mjs — 组件页 API 表「说明」的出处可溯性验收(零依赖)。
*
* 背景:API 表的说明列在 S6-P37 之前 235 条里 230 条是 `—`。补说明时立了两条硬规矩,
* 这个脚本就是把它们变成可复跑的判据 —— 说明可以来自规格原文、API 命名释义或源码注释,
* 但**每一条都必须能被指认出来源**,且「规格」类必须是规格原文的逐字引用:
*
* 1) 每条 prop 都有说明,且 descSrc ∈ {spec, name, code, contract}(none 即失败)
* 2) descSrc=spec 的 cite 必须逐字出现在该组件的规格原文分条里(防「看起来像规格」的编造)
* 3) 事件说明不得回落到模板句(「组件交互触发事件」),update:* 必须标注 v-model
* 4) 说明与事件文案必须都在 site/i18n.js 的英文字典里(否则英文模式回退中文)
* 5) 规格取值副行只在「分条标签 === 该 prop 的说明」时出现,且片段可回查规格原文
* (反例:card.title 曾被「结构:标题区、内容区、操作区」误配、tag.color 曾切出 `绿#F6FFED`)
*
* 前置:site/data.json 已是当前构建产物(node tools/precompute.mjs 产出)。
*/
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
const ROOT = join(import.meta.dirname, '..');
const failures = [];
const checks = [];
function check(name, ok, detail = '') {
checks.push({ name, ok, detail });
if (!ok) failures.push(`${name}${detail ? `: ${detail}` : ''}`);
}
const dataPath = join(ROOT, 'site', 'data.json');
if (!existsSync(dataPath)) {
console.error('[api-docs] 缺少 site/data.json —— 先跑 node tools/precompute.mjs');
process.exit(2);
}
const data = JSON.parse(readFileSync(dataPath, 'utf8'));
const i18nSrc = readFileSync(join(ROOT, 'site', 'i18n.js'), 'utf8');
const unesc = (s) => s.replace(/\\(['"])/g, '$1');
const dict = new Set();
for (const m of i18nSrc.matchAll(/^\s*(['"])((?:\\.|(?!\1)[\s\S])*?)\1\s*:/gm)) dict.add(unesc(m[2]));
const props = [];
const emits = [];
for (const c of data.components) {
for (const p of (c.api && c.api.props) || []) props.push({ slug: c.slug, c, p });
for (const e of (c.api && c.api.emits) || []) emits.push({ slug: c.slug, e });
}
const specLines = (c) => {
const raw = Array.isArray(c.specLines) ? c.specLines : String(c.specLines || '').split(/\r?\n/);
return raw.map((l) => String(l).replace(/^\s*[-•*]\s*/, '').trim()).filter(Boolean);
};
/* 1) 说明覆盖与出处 */
const srcCount = {};
for (const { p } of props) srcCount[p.descSrc || 'none'] = (srcCount[p.descSrc || 'none'] || 0) + 1;
const emptyDesc = props.filter(({ p }) => !p.desc || p.desc === '—');
check('prop 说明非空', emptyDesc.length === 0,
`${props.length - emptyDesc.length}/${props.length} 有说明(出处 ${JSON.stringify(srcCount)})`);
check('无 none 出处', !srcCount.none, srcCount.none ? `${srcCount.none} 条没有出处` : '全部可溯源');
/* 2) spec 类必须是规格原文逐字引用 */
const specProps = props.filter(({ p }) => p.descSrc === 'spec');
const badCite = specProps.filter(({ c, p }) => !specLines(c).some((l) => l.startsWith(p.cite) || p.cite.startsWith(l)));
check('spec 出处 cite 逐字命中规格原文', badCite.length === 0,
`${specProps.length} 条 spec${badCite.length ? `,异常 ${badCite.length}:${badCite.slice(0, 3).map((x) => `${x.slug}.${x.p.name}`).join(', ')}` : ''}`);
/* 3) 事件说明 */
const TEMPLATES = ['组件交互触发事件', '交互回调触发事件'];
const genericEmits = emits.filter(({ e }) => !e.descZh || TEMPLATES.includes(e.descZh));
check('事件说明无模板句', genericEmits.length === 0,
`${emits.length - genericEmits.length}/${emits.length} 具体化${genericEmits.length ? `,异常:${genericEmits.slice(0, 3).map((x) => `${x.slug}.${x.e.name}`).join(', ')}` : ''}`);
const vmodel = emits.filter(({ e }) => String(e.name).startsWith('update:'));
check('update:* 标注 v-model', vmodel.every(({ e }) => e.descZh.includes('v-model')), `${vmodel.length} 条`);
/* 4) 英文词条覆盖(否则英文模式回退中文) */
const descVals = [...new Set(props.map((x) => x.p.desc))].filter((d) => d && d !== '—');
const missDesc = descVals.filter((d) => !dict.has(d));
check('属性说明词条齐全', missDesc.length === 0,
`${descVals.length} 个唯一值${missDesc.length ? `,缺失 ${missDesc.slice(0, 6).join(' / ')}` : ''}`);
const emitVals = [...new Set(emits.map((x) => x.e.descZh))].filter(Boolean);
const missEmit = emitVals.filter((d) => !dict.has(d));
check('事件说明词条齐全', missEmit.length === 0,
`${emitVals.length} 个唯一值${missEmit.length ? `,缺失 ${missEmit.slice(0, 6).join(' / ')}` : ''}`);
/* 5) 规格取值副行:标签一致 + 可回查 */
const withOpts = props.filter((x) => x.p.opts && x.p.opts.length);
const badOpts = withOpts.filter(({ c, p }) => {
const label = String(p.cite || '').split(/[::]/)[0].trim();
if (label !== p.desc && label.indexOf(p.desc) !== 0) return true;
return !specLines(c).some((l) => l.includes(p.opts[0]));
});
check('规格取值副行可回查且标签一致', badOpts.length === 0,
`${withOpts.length} 条${withOpts.length ? `(${withOpts.map((x) => `${x.slug}.${x.p.name}×${x.p.opts.length}`).join(', ')})` : ''}${badOpts.length ? `,异常 ${badOpts.slice(0, 3).map((x) => `${x.slug}.${x.p.name}`).join(', ')}` : ''}`);
/* 6) 覆盖面(有 API 的组件数不应无故缩水) */
const withApi = data.components.filter((c) => {
const a = c.api || {};
return (a.props || []).length || (a.emits || []).length || (a.slots || []).length;
});
const allowedWithoutApi = new Set(['enhancedtabnav', 'dragupload', 'signaturepad']);
const withoutApi = data.components.filter((c) => !withApi.includes(c));
check('有 API 表的组件数', withoutApi.every((c) => allowedWithoutApi.has(c.slug)), `${withApi.length}/${data.components.length};无 API:${withoutApi.map((c) => c.slug).join(', ') || '无'}`);
check('无 API 组件为已知例外', withoutApi.length === [...allowedWithoutApi].filter((slug) => data.components.some((c) => c.slug === slug)).length, `${withoutApi.length} 个`);
for (const r of checks) console.log(`[api-docs] ${r.ok ? 'OK' : 'FAIL'} ${r.name}${r.detail ? ` — ${r.detail}` : ''}`);
if (failures.length) {
console.error(`\n[api-docs] ${failures.length} check(s) failed`);
process.exit(1);
}
console.log(`\n[api-docs] OK — ${checks.length} checks passed`);