Files
aurora-admin 5ee5d829a4
Regression / regression (push) Canceled after 0s
refactor(详情页+S8): 详情页减重(导航互斥/入口去重/i18n去重) + 品牌标识入库
本提交含两条并行工作线,因互相咬合(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 断言其互斥),未删。
2026-09-22 23:56:05 +08:00

164 lines
9.5 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.
/* Kole UI —— 部署打包器(.dockerignore 是唯一真源)
*
* 背景(实测事故):
* 远端 /opt/kole-ui 之前靠手工拷贝维护,没有 git、也没有 .dockerignore,
* 于是 ① .design_library/kole-ui/specs 等目录随镜像发布并对外可下载;
* ② 曾经修好的 nginx.conf(absolute_redirect off)被下一次拷贝覆盖丢失。
* 本脚本把「该发什么、不该发什么」固化为可复现的一步:
* - 文件集只包含 docker build 真正需要的顶层条目;
* - 排除规则**解析 .dockerignore**(含 Docker 的匹配语义),不再维护第二份清单;
* - 硬断言:规范原文/agent-reports/preview/ui_kits 绝不出现,站点运行必需文件必须出现。
*
* 用法:
* node tools/pack-deploy.mjs # 生成 dist-deploy/kole-ui/ + manifest
* node tools/pack-deploy.mjs --tar # 额外打出 dist-deploy/kole-ui-deploy.tar.gz
*/
'use strict';
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { collectPublishSet, TOP_FILES } from './lib/publish-set.mjs';
const ROOT = process.cwd();
const OUT_DIR = path.join(ROOT, 'dist-deploy');
const STAGE = path.join(OUT_DIR, 'kole-ui');
/* 发布集(含 .dockerignore 的解析与匹配语义、内容目录清单、历史版本快照目录)统一在
tools/lib/publish-set.mjs 里,与 tools/verify-deploy-consistency.mjs 同源 ——
「打包器发了什么」与「线上核对看到什么」必须是同一份清单。 */
const { kept, candidates, excludedCount, versionDirs } = collectPublishSet(ROOT);
const pcIndex = JSON.parse(fs.readFileSync(path.join(ROOT, '.design_library', 'kole-ui', 'components', 'index.json'), 'utf8'));
const pcCount = pcIndex.components.length;
const pcFrameworkCount = pcCount * 5;
/* ---------- 硬断言:不该发的绝不允许出现 ---------- */
const MUST_ABSENT = [
'.design_library/kole-ui/specs/组件1.txt',
'.design_library/kole-ui/agent-reports/phase3-component-modal.json',
'.design_library/kole-ui/preview',
'.design_library/kole-ui/ui_kits',
'.design_library/kole-ui/SKILL.md',
/* 移动端规格原文与 PC 规格同级:仓库内部真源,不进镜像 */
'.design_library/kole-ui-mobile/spec',
'AGENTS.md', 'CHANGELOG.md', 'ROADMAP.md', 'TESTING.md', 'PLATFORMS.md',
'docker-compose.yml.bak-20260916', 'build-site.ps1',
];
const MUST_PRESENT = [
'site/index.html', 'site/app.js', 'site/logger.js', 'site/data.json', 'site/data.js',
'site/tokens/tokens.css', 'site/style.css', 'site/i18n.js',
'site/assets/kole-mark.svg', 'site/assets/kole-mark-mono.svg', 'site/brand-mark.generated.json',
/* 逐示例用法片段(P21):目录留在 data.js,正文按组件懒加载;缺了页面就没有「一个使用场景一块代码」 */
'site/examples/button.json',
'frameworks/Button.css', 'frameworks/Button.html', 'frameworks/Button.vue3.vue',
'tests/index.html',
'.design_library/kole-ui/colors_and_type.css', '.design_library/kole-ui/components.css',
'.design_library/kole-ui/css.json', '.design_library/kole-ui/components/button.json',
/* 移动端平台:文档站六页(与 PC 同名同序的指南页 + 平台页)+ 自包含数据 + 索引 + 令牌层 + 实现 + 测试页 */
'site/m/index.html', 'site/m/guide.html', 'site/m/design.html', 'site/m/faq.html',
'site/m/changelog.html', 'site/m/platform.html',
'site/m/style.css', 'site/m/data.mobile.json',
'frameworks-mobile/NavBar.html', 'frameworks-mobile/NavBar.css', 'frameworks-mobile/NavBar.uniapp.vue',
'tests/mobile/index.html', 'tests/mobile/_collect.html',
'.design_library/kole-ui-mobile/colors_and_type.css', '.design_library/kole-ui-mobile/components/index.json',
/* PC × uni-app 端(试点集) */
'frameworks-uniapp-pc/Button.uniapp.vue', '.design_library/kole-ui-uniapp/index.json',
'Dockerfile', '.dockerignore', 'nginx.conf', 'docker-compose.yml', 'sitemap.xml', 'versions.json',
];
const problems = [];
for (const f of MUST_ABSENT) {
const bad = kept.some((k) => k === f || k.startsWith(f + '/'));
if (bad) problems.push('不该出现:' + f);
}
for (const f of MUST_PRESENT) {
if (!kept.includes(f)) problems.push('缺失必需文件:' + f);
}
const shells = kept.filter((f) => /^site\/components\/[^/]+\.html$/.test(f)).length;
if (shells !== pcCount) problems.push(`site/components 薄壳数应为 ${pcCount},实际 ${shells}`);
/* 历史版本快照必须整份随包(缺壳或缺依赖 → 版本菜单点进去白屏) */
for (const dir of versionDirs) {
for (const rel of ['site/index.html', 'site/data.js',
'frameworks/Button.css', '.design_library/kole-ui/colors_and_type.css', 'VERSION.json']) {
if (!kept.includes(`${dir}/${rel}`)) problems.push(`版本快照 ${dir} 缺少 ${rel}`);
}
/* 快照不该带自己的 versions.json:那是归档当时的快照,之后新归档的版本不会进去,
实测会让快照页误判成单版本、角标不可点(回不到最新版)。运行时读部署根那份。 */
if (kept.includes(`${dir}/site/versions.json`)) problems.push(`版本快照 ${dir} 带了过期的 versions.json`);
const snapShells = kept.filter((f) => new RegExp(`^${dir}/site/components/[^/]+\\.html$`).test(f)).length;
if (snapShells !== 79) problems.push(`版本快照 ${dir} 的组件薄壳数应为 79,实际 ${snapShells}`);
}
const fw = kept.filter((f) => f.startsWith('frameworks/')).length;
if (fw !== pcFrameworkCount) problems.push(`frameworks 实现文件应为 ${pcFrameworkCount},实际 ${fw}`);
/* 移动端:组件数从索引读(不写死),实现文件 = 组件数 × 6 端;文档页与测试页各 = 组件数 */
const mobileIndex = JSON.parse(
fs.readFileSync(path.join(ROOT, '.design_library', 'kole-ui-mobile', 'components', 'index.json'), 'utf8')
);
const mbCount = mobileIndex.components.length;
const mbEnds = mobileIndex.ends.length;
const fwMobile = kept.filter((f) => f.startsWith('frameworks-mobile/')).length;
if (fwMobile !== mbCount * mbEnds) {
problems.push(`frameworks-mobile 实现文件应为 ${mbCount * mbEnds}(${mbCount} 组件 × ${mbEnds} 端),实际 ${fwMobile}`);
}
const mDocs = kept.filter((f) => /^site\/m\/component\/[^/]+\.html$/.test(f)).length;
if (mDocs !== mbCount) problems.push(`site/m/component 文档页应为 ${mbCount},实际 ` + mDocs);
/* 移动端文档站根页:与 PC 同名同序的指南页(组件总览/快速开始/设计规范/常见问题/更新日志)+ 平台与端 */
const mRootPages = kept.filter((f) => /^site\/m\/(index|guide|design|faq|changelog|platform)\.html$/.test(f)).length;
if (mRootPages !== 6) problems.push('site/m 根页应为 6(index/guide/design/faq/changelog/platform),实际 ' + mRootPages);
const mTests = kept.filter((f) => /^tests\/mobile\/[^/]+\.html$/.test(f)).length;
if (mTests !== mbCount + 4) {
problems.push(`tests/mobile 页面应为 ${mbCount + 4}(组件页 + index + _collect + _template + _index_template),实际 ${mTests}`);
}
const mTestSlugs = kept.filter(
(f) => /^tests\/mobile\/[^/]+\.html$/.test(f) && !/^tests\/mobile\/(index|_collect|_template|_index_template)\.html$/.test(f)
).length;
if (mTestSlugs !== mbCount) problems.push(`tests/mobile 组件测试页应为 ${mbCount},实际 ` + mTestSlugs);
const fwUniappPc = kept.filter((f) => f.startsWith('frameworks-uniapp-pc/')).length;
if (fwUniappPc !== 3) problems.push('frameworks-uniapp-pc 试点文件应为 3,实际 ' + fwUniappPc);
if (problems.length) {
console.error('[pack-deploy] FAIL');
for (const p of problems) console.error(' - ' + p);
process.exit(1);
}
/* ---------- 落盘 ---------- */
fs.rmSync(STAGE, { recursive: true, force: true });
let bytes = 0;
for (const rel of kept) {
const dst = path.join(STAGE, rel);
fs.mkdirSync(path.dirname(dst), { recursive: true });
const buf = fs.readFileSync(path.join(ROOT, rel));
fs.writeFileSync(dst, buf);
bytes += buf.length;
}
const manifest = {
generatedAt: new Date().toISOString(),
files: kept.length,
bytes,
components: shells,
frameworks: fw,
mobile: { impl: fwMobile, docs: mDocs, testPages: mTestSlugs },
uniappPc: fwUniappPc,
excludedByDockerignore: excludedCount,
list: kept,
};
fs.writeFileSync(path.join(OUT_DIR, 'deploy-manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
console.log('[pack-deploy] OK');
console.log(' staging : ' + STAGE);
console.log(' files : ' + kept.length + '(内容文件 ' + candidates.length + ',按 .dockerignore 排除 ' + excludedCount + ';部署配置文件 ' + TOP_FILES.length + ' 个始终保留)');
console.log(' bytes : ' + (bytes / 1024).toFixed(1) + ' KB');
console.log(' components : ' + shells + ' 个薄壳 / frameworks ' + fw + ' 个实现文件');
console.log(' mobile : ' + fwMobile + ' 个实现文件(' + mbCount + ' 组件 × 6 端)/ 文档页 ' + mDocs + ' / 测试页 ' + mTestSlugs);
console.log(' uniapp-pc : ' + fwUniappPc + ' 个试点 SFC(PC × uni-app)');
console.log(' 断言 : 排除项 ' + MUST_ABSENT.length + ' 条全部未出现;必需项 ' + MUST_PRESENT.length + ' 条全部存在');
if (process.argv.includes('--tar')) {
/* 用相对路径调用 tar:bsdtar(Windows 自带)对含中文的绝对路径 + -C 组合会报 status 2 */
const tgzRel = path.join('dist-deploy', 'kole-ui-deploy.tar.gz');
const tgz = path.join(ROOT, tgzRel);
fs.rmSync(tgz, { force: true });
execFileSync('tar', ['-czf', tgzRel, '-C', 'dist-deploy', 'kole-ui'], { cwd: ROOT, stdio: 'inherit' });
console.log(' tarball : ' + tgz + '(' + (fs.statSync(tgz).size / 1024).toFixed(1) + ' KB)');
}