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
+20 -33
View File
@@ -31,6 +31,7 @@ import {
import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { dirname, join, relative, sep } from 'node:path';
import { generatedData, readBrandSpec, validateBrandSpec } from './lib/brand-mark.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
@@ -44,6 +45,18 @@ const TESTS = join(ROOT, 'tests', 'mobile');
const WEBSITE = join(ROOT, 'site', 'm');
const DIST = join(ROOT, 'dist', 'mobile');
const PC_INDEX = join(ROOT, '.design_library', 'kole-ui', 'components', 'index.json');
const BRAND_SPEC = readBrandSpec(ROOT);
const BRAND_ERRORS = validateBrandSpec(BRAND_SPEC);
if (BRAND_ERRORS.length) {
throw new Error(`[build-mobile] 品牌图标规格无效:${BRAND_ERRORS.join(';')}`);
}
if (!existsSync(join(ROOT, 'site', 'assets', 'kole-mark.svg')) || !existsSync(join(ROOT, 'site', 'assets', 'kole-mark-mono.svg'))) {
throw new Error('[build-mobile] 缺品牌 SVG 产物,请先运行 npm run build:brand');
}
const brandDataFile = join(ROOT, 'site', 'brand-mark.generated.json');
if (!existsSync(brandDataFile) || JSON.stringify(JSON.parse(readFileSync(brandDataFile, 'utf8')), null, 2) + '\n' !== JSON.stringify(generatedData(BRAND_SPEC), null, 2) + '\n') {
throw new Error('[build-mobile] brand-mark.generated.json 与品牌规格不同步,请先运行 npm run build:brand');
}
/** 允许写入的前缀(相对仓库根,POSIX 风格) */
const ALLOWED_PREFIXES = ['site/m/', 'tests/mobile/', 'dist/mobile/'];
@@ -327,37 +340,10 @@ function tokensOf(slug) {
平台切换**不在顶栏**:PC 顶栏也没有它 —— 两端的唯一入口都是左栏「平台」组。
2026-09-20 去重:此前顶栏另有一个 [PC 端][移动端] 胶囊,与左栏指向同一跳转
(实测两条链接 href 均为 ../index.html),是重复入口,已删。 */
/* ---------- 品牌标识(与 PC 站同源几何)----------
几何真源:24 网格三个互不接触的笔画组成的 K。同一份路径同时用于:
① favicon(BRAND_FAVICON,内联 data-URI)
② 两端顶栏标记(BRAND_MARK_*,内联 SVG,走 currentColor)
描边 2.25 @24 网格 → 16px 下正好 1.5px,等于规范原文「线性图标,描边1.5px」
(.design_library/kole-ui/specs/组件1.txt)。
favicon 里硬编码品牌蓝/白而不用令牌:它渲染在**浏览器标签栏**,不继承 html.kole-dark;
顶栏标记必须走 currentColor 才会随主题令牌反色 —— 两者分工不同,不要混。 */
const BRAND_PATHS = [
'M7 5.5V18.5',
'M17 5.5l-6.25 4.06',
'M10.75 14.44L17 18.5',
]
.map((d) => `<path d="${d}"/>`)
.join('');
const BRAND_SW = '2.25';
/* 移动端顶栏盒 26×26(PC 是 32×32 / 20px 标记),标记取 17px 保持同视觉重量 */
const BRAND_MARK = `<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${BRAND_SW}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${BRAND_PATHS}</svg>`;
const BRAND_FAVICON = (() => {
const svg =
`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'>` +
`<rect width='24' height='24' rx='6' fill='#2F54EB'/>` +
`<g fill='none' stroke='#FFFFFF' stroke-width='${BRAND_SW}' stroke-linecap='round' stroke-linejoin='round'>` +
BRAND_PATHS.split('"').join("'") +
`</g></svg>`;
/* 与 site/index.html 的 favicon 同编码规则:只转义 < > 空格 #,属性单引号原样 */
return (
'data:image/svg+xml,' +
svg.replace(/</g, '%3C').replace(/>/g, '%3E').replace(/ /g, '%20').replace(/#/g, '%23')
);
})();
/* ---------- 品牌标识(brand-mark.json 唯一真源)----------
移动端静态页使用相对站点根的独立 SVG favicon;顶栏标记由 site/m/style.css
以同一规格生成的单色 SVG mask 渲染。这样构建器不再复制或拼接一套几何路径。 */
const BRAND_FAVICON_HREF = '../assets/kole-mark.svg';
function headerHtml(activeKey, isComp) {
const toRoot = isComp ? '../' : '';
@@ -370,7 +356,7 @@ function headerHtml(activeKey, isComp) {
return ` <header class="m-top">
<div class="m-top-inner">
<a class="m-logo" href="${toRoot}index.html">
<span class="m-logo-mark" aria-hidden="true">${BRAND_MARK}</span>
<span class="m-logo-mark" data-brand-mark="mono" aria-hidden="true"></span>
<span class="m-logo-text">Kole UI<em>移动端</em></span>
</a>
<div class="m-ver-picker" id="m-ver-picker">
@@ -856,11 +842,12 @@ function renderSitePage(templateName, repl) {
__BRAND_HEAD__ 由模板放在 <head> 里(本站此前完全没有 favicon,浏览器标签页是
默认地球图标);theme-color 取 --kole-color-card-bg —— .m-top 的 background 就是它。 */
function shellRepl(activeKey, isComp) {
const faviconHref = isComp ? '../../assets/kole-mark.svg' : BRAND_FAVICON_HREF;
return {
__HEADER__: headerHtml(activeKey, isComp),
__THEME_BOOT__: THEME_BOOT,
__BRAND_HEAD__: [
`<link rel="icon" href="${BRAND_FAVICON}">`,
`<link rel="icon" href="${faviconHref}">`,
'<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">',
'<meta name="theme-color" content="#1C1F26" media="(prefers-color-scheme: dark)">',
].join('\n'),