refactor(详情页+S8): 详情页减重(导航互斥/入口去重/i18n去重) + 品牌标识入库
Regression / regression (push) Canceled after 0s

本提交含两条并行工作线,因互相咬合(package.json scripts、regression.yml、
npm run build 链)无法按文件干净拆分,故合并为一次自洽提交。

## 详情页减重(本轮主任务:修复「AI 干太重」)

- 导航收敛:宽屏只用右侧目录、≤1200px 只用页内 sticky 导航,纯 CSS 媒体查询
  实现(不引入 JS 宽度监听)。此前两套导航同时可见,active 状态互相打架。
- 入口去重:标题区由「查看示例/在线测试/契约 JSON」三个减为「在线测试」一个;
  契约 JSON 归入实现资源;删除示例底部重复的「在线测试」。
- 示例工具栏:删除与目录锚点重复的示例下拉选择器;「全部展开代码」只在
  示例数 >1 时出现(103 个组件里 49 个仅 1 个示例,此前恒显示)。
- 重复文案:示例区两句同义导语合并为一句。
- 首页 CTA 由 5 个减为 2 个(浏览组件/快速开始),测试总览入口挂到已有的
  通过率统计卡上,不再另占 Hero 按钮。
- 统一详情取数:抽出 fetchDetail/loadDetail 作为 details/*.json 的唯一路径,
  FAQ 不再自行 fetch 一遍,与组件页共用缓存与失败兜底。

## i18n

- 删除 12 组重复键(含整段 FAQ 说明),字典 604 → 585 唯一键。
- 删除本次改动产生的 6 个死键。
- verify-i18n.mjs 新增 `unique dictionary keys` 断言:重复键在对象字面量里
  是静默的后值覆盖,此前无从发现;现由门禁拦住。

## 文档事实修正(实测为准)

- TESTING/CONTRIBUTING:79 → 103 组件;旧断言数改为回指 tests/report.json。
- PLATFORMS:移动端 108 文件/18 端 → 282 文件/47 端;契约 5 → 47;
  令牌 17 → 15;uni-app SFC 21 → 50。
- AGENTS:断言 1405/18 页 → 1464/103 页(PC)、807/47 页(移动端)。
- package.json:YOUR-ACCOUNT 占位 → gitea 实址与 kole-ui.mymoyu.top。

## 品牌标识(并行会话成果,一并入库)

- brand-mark.json 收归真源,build:brand 生成 favicon 与单色 SVG;
  PC 与移动端共用资产,verify:brand 24 条断言。
- regression.yml 增加 verify:brand 步骤。

## 门禁与验收

新增工具:verify-component-page.mjs(86 条真实浏览器断言,随详情页改造同步
更新为「只允许一套导航可见」)、verify-brand-mark.mjs、lib/i18n-dead-keys.mjs
(只读诊断)。

回归:PC 100%(1464/1464,103 页,N/A 50)· 移动端 100%(807/807,47 页)。
门禁:component-page 86 · i18n 17 · brand 24 · routes all · smoke all ·
theme OK · isolation 31 · nav all · examples 9 · api-docs 10 · mobile-docs 12。

已知未做:i18n 另有约 44 条历史死键(非本次产生),已记为 ROADMAP S8-P6;
顶栏与悬浮区的两个主题入口为刻意设计(verify-theme 断言其互斥),未删。
This commit is contained in:
aurora-admin
2026-09-22 23:56:05 +08:00
parent f1fbfc2ddb
commit 5ee5d829a4
92 changed files with 1320 additions and 463 deletions
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env node
/**
* verify-brand-mark.mjs — 项目品牌图标静态门禁。
* 组件 Icon registry 是另一套系统,本脚本只检查 favicon 与文档站品牌标记。
*/
import fs from 'node:fs';
import path from 'node:path';
import { collectPublishSet } from './lib/publish-set.mjs';
import {
generatedData,
readBrandSpec,
renderFaviconSvg,
renderMonoSvg,
validateBrandSpec,
BRAND_ASSET_REL,
BRAND_DATA_REL,
BRAND_MONO_REL,
BRAND_SPEC_REL,
} from './lib/brand-mark.mjs';
const ROOT = process.cwd();
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/^\uFEFF/, '');
const exists = (rel) => fs.existsSync(path.join(ROOT, rel));
let pass = 0;
const failures = [];
function check(ok, label, detail = '') {
if (ok) {
pass++;
console.log(` PASS ${label}${detail ? ` — ${detail}` : ''}`);
} else {
failures.push(`${label}${detail ? ` — ${detail}` : ''}`);
console.log(` FAIL ${label}${detail ? ` — ${detail}` : ''}`);
}
}
console.log('Kole UI 项目品牌图标门禁');
check(exists(BRAND_SPEC_REL), 'B1 品牌规格存在', BRAND_SPEC_REL);
check(exists(BRAND_ASSET_REL), 'B2 favicon SVG 存在', BRAND_ASSET_REL);
check(exists(BRAND_MONO_REL), 'B3 单色 SVG 存在', BRAND_MONO_REL);
check(exists(BRAND_DATA_REL), 'B4 生成数据存在', BRAND_DATA_REL);
const spec = readBrandSpec(ROOT);
const specErrors = validateBrandSpec(spec);
check(specErrors.length === 0, 'B5 品牌规格结构有效', specErrors.join(';'));
const expectedFavicon = renderFaviconSvg(spec) + '\n';
const expectedMono = renderMonoSvg(spec) + '\n';
const expectedData = JSON.stringify(generatedData(spec), null, 2) + '\n';
const favicon = exists(BRAND_ASSET_REL) ? read(BRAND_ASSET_REL) : '';
const mono = exists(BRAND_MONO_REL) ? read(BRAND_MONO_REL) : '';
const data = exists(BRAND_DATA_REL) ? read(BRAND_DATA_REL) : '';
check(favicon === expectedFavicon, 'B6 favicon 是规格生成物');
check(mono === expectedMono, 'B7 单色标记是规格生成物');
check(data === expectedData, 'B8 generated JSON 是规格生成物');
check(!/<image\b|(?:href|src)=["']https?:\/\//i.test(favicon + mono), 'B9 品牌 SVG 不含外部资源');
check((favicon.match(/<path\b/g) || []).length === spec.geometry.paths.length, 'B10 favicon 完整包含全部几何路径', `${spec.geometry.paths.length} 条`);
check((mono.match(/<path\b/g) || []).length === spec.geometry.paths.length, 'B11 单色标记完整包含全部几何路径', `${spec.geometry.paths.length} 条`);
const faviconPaint = spec.geometry.mode === 'fill' ? `fill="${spec.favicon.foreground}"` : `stroke="${spec.favicon.foreground}"`;
check(favicon.includes(`fill="${spec.favicon.background}"`) && favicon.includes(faviconPaint), 'B12 favicon 颜色固定');
const monoPaint = spec.geometry.mode === 'fill' ? 'fill="#000000"' : 'stroke="#000000"';
check(mono.includes(monoPaint) && !mono.includes(`fill="${spec.favicon.background}"`), 'B13 单色标记透明背景');
const pc = read('site/index.html');
const mobileBuilder = read('tools/build-mobile.mjs');
const mobileStyle = read('site/m/style.css');
check(pc.includes('href="assets/kole-mark.svg"'), 'B14 PC 使用 favicon 资产');
check(pc.includes('class="logo-mark"') && pc.includes('data-brand-mark="mono"'), 'B15 PC 顶栏使用单色品牌标记');
check(!pc.includes('M7 5.5V18.5'), 'B16 PC 不再内联旧几何路径');
check(mobileBuilder.includes("readBrandSpec(ROOT)"), 'B17 移动端构建读取品牌规格');
check(!mobileBuilder.includes('const BRAND_PATHS ='), 'B18 移动端构建不重复维护几何路径');
check(mobileStyle.includes('data-brand-mark="mono"') || mobileStyle.includes('kole-mark-mono.svg'), 'B19 移动端样式声明单色品牌资产');
check((pc.match(/theme-color/g) || []).length >= 2, 'B20 PC theme-color 保留亮暗两态');
const mobileIndex = JSON.parse(read('.design_library/kole-ui-mobile/components/index.json'));
const mobilePages = [
...['index', 'guide', 'design', 'faq', 'changelog', 'platform'].map((name) => `site/m/${name}.html`),
...mobileIndex.components.map((component) => `site/m/component/${component.slug}.html`),
];
const pageProblems = [];
for (const rel of mobilePages) {
if (!exists(rel)) { pageProblems.push(`${rel}: 文件缺失`); continue; }
const html = read(rel);
const head = (html.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i) || [])[1] || '';
const icons = [...head.matchAll(/<link\b[^>]*rel="icon"[^>]*>/g)];
const href = icons.length === 1 ? (icons[0][0].match(/\bhref="([^"]+)"/) || [])[1] : '';
if (!href || path.posix.normalize(path.posix.join(path.posix.dirname(rel), href)) !== BRAND_ASSET_REL) {
pageProblems.push(`${rel}: favicon 未指向统一资产`);
}
const header = (html.match(/<header class="m-top">([\s\S]*?)<\/header>/) || [])[1] || '';
if (!header.includes('<span class="m-logo-mark" data-brand-mark="mono" aria-hidden="true"></span>')) {
pageProblems.push(`${rel}: 顶栏标记或装饰性语义缺失`);
}
if (header.includes('M7 5.5V18.5') || (head.match(/name="theme-color"/g) || []).length !== 2) {
pageProblems.push(`${rel}: 旧几何残留或 theme-color 缺失`);
}
}
check(pageProblems.length === 0, 'B21 移动端全部生成页使用统一品牌资产', pageProblems.slice(0, 5).join(';') || `${mobilePages.length} 页`);
const pcStyle = read('site/style.css');
const pcMask = (pcStyle.match(/\.logo-mark\[data-brand-mark="mono"\]::before\s*\{([^}]+)\}/) || [])[1] || '';
const mobileMask = (mobileStyle.match(/\.m-logo-mark\[data-brand-mark="mono"\]::before\s*\{([^}]+)\}/) || [])[1] || '';
check(pcMask.includes('background: currentColor') && pcMask.includes("url('assets/kole-mark-mono.svg')")
&& mobileMask.includes('background: currentColor') && mobileMask.includes("url('../assets/kole-mark-mono.svg')"),
'B22 两端 mask 路径同源且继承 currentColor');
const published = new Set(collectPublishSet(ROOT).kept);
check([BRAND_ASSET_REL, BRAND_MONO_REL, BRAND_DATA_REL].every((rel) => published.has(rel)), 'B23 发布集包含全部品牌产物');
const scripts = JSON.parse(read('package.json')).scripts;
check(scripts['build:brand'] === 'node tools/build-brand-mark.mjs'
&& scripts['verify:brand'] === 'node tools/verify-brand-mark.mjs'
&& scripts.build.startsWith('node tools/build-brand-mark.mjs && '), 'B24 品牌构建与门禁入口完整');
if (failures.length) {
console.error(`\n[FAIL] ${failures.length} 条品牌图标断言失败(通过 ${pass} 条)`);
failures.forEach((failure) => console.error(' - ' + failure));
process.exit(1);
}
console.log(`\n[OK] 品牌图标门禁全部通过(${pass} 条断言)`);