Files
aurora-admin/tools/lib/brand-mark.mjs
T
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

125 lines
4.8 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
export const BRAND_SPEC_REL = '.design_library/kole-ui/brand/brand-mark.json';
export const BRAND_DATA_REL = 'site/brand-mark.generated.json';
export const BRAND_ASSET_REL = 'site/assets/kole-mark.svg';
export const BRAND_MONO_REL = 'site/assets/kole-mark-mono.svg';
export const BRAND_SCHEMA_VERSION = 2;
export function readBrandSpec(root) {
const file = path.join(root, BRAND_SPEC_REL);
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
}
export function validateBrandSpec(spec) {
const errors = [];
if (!spec || spec.schemaVersion !== BRAND_SCHEMA_VERSION) {
errors.push(`schemaVersion 必须为 ${BRAND_SCHEMA_VERSION}`);
}
if (!spec || spec.viewBox !== '0 0 24 24') errors.push('viewBox 必须为 0 0 24 24');
const g = spec?.geometry || {};
const paths = g.paths;
if (!Array.isArray(paths) || !paths.length || paths.some((d) => typeof d !== 'string' || !d.trim())) {
errors.push('geometry.paths 必须包含至少一条非空路径');
}
if (g.mode !== 'fill' && g.mode !== 'stroke') errors.push('geometry.mode 必须为 fill 或 stroke');
if (g.mode === 'stroke') {
if (g.strokeWidth !== 1.5) errors.push('stroke 模式的 geometry.strokeWidth 必须为 1.5');
if (g.linecap !== 'round') errors.push('stroke 模式的 geometry.linecap 必须为 round');
if (g.linejoin !== 'round') errors.push('stroke 模式的 geometry.linejoin 必须为 round');
}
if (g.opacities !== undefined) {
if (!Array.isArray(g.opacities) || g.opacities.length !== (paths || []).length) {
errors.push('geometry.opacities 必须与 paths 等长');
} else if (g.opacities.some((v) => typeof v !== 'number' || !(v > 0) || v > 1)) {
errors.push('geometry.opacities 取值必须在 (0, 1] 区间');
}
}
if (!/^#[0-9A-Fa-f]{6}$/.test(spec?.favicon?.background || '')) errors.push('favicon.background 必须为六位十六进制颜色');
if (!/^#[0-9A-Fa-f]{6}$/.test(spec?.favicon?.foreground || '')) errors.push('favicon.foreground 必须为六位十六进制颜色');
if (spec?.favicon?.radius !== 6) errors.push('favicon.radius 必须为 6');
for (const [key, value] of Object.entries(spec?.sizes || {})) {
if (!Number.isFinite(value) || value <= 0) errors.push(`sizes.${key} 必须为正数`);
}
return errors;
}
/** 逐路径生成 <path>:实心模式按 opacities 给 fill-opacity;描边模式交给外层 <g> 统一给描边属性。 */
function pathsMarkup(spec) {
const { paths, mode, opacities } = spec.geometry;
return paths
.map((d, i) => {
const opacity = mode === 'fill' && opacities && opacities[i] != null ? ` fill-opacity="${opacities[i]}"` : '';
return `<path d="${d}"${opacity}/>`;
})
.join('');
}
/** 图形的公共外层:实心走 fill,描边走 stroke;颜色由调用方决定。 */
function groupMarkup(spec, color, { mono }) {
const { mode } = spec.geometry;
const strokeAttrs =
`fill="none" stroke="${mono ? '#000000' : color}" stroke-width="${spec.geometry.strokeWidth}"` +
` stroke-linecap="${spec.geometry.linecap}" stroke-linejoin="${spec.geometry.linejoin}"`;
return mode === 'stroke'
? `<g ${strokeAttrs}>${pathsMarkup(spec)}</g>`
: `<g fill="${mono ? '#000000' : color}">${pathsMarkup(spec)}</g>`;
}
export function renderFaviconSvg(spec) {
return [
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${spec.viewBox}">`,
`<rect width="24" height="24" rx="${spec.favicon.radius}" fill="${spec.favicon.background}"/>`,
groupMarkup(spec, spec.favicon.foreground, { mono: false }),
'</svg>',
].join('');
}
export function renderMonoSvg(spec) {
return [
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${spec.viewBox}">`,
groupMarkup(spec, '#000000', { mono: true }),
'</svg>',
].join('');
}
export function generatedData(spec) {
return {
schemaVersion: spec.schemaVersion,
name: spec.name,
viewBox: spec.viewBox,
geometry: {
mode: spec.geometry.mode,
paths: spec.geometry.paths,
...(spec.geometry.opacities ? { opacities: spec.geometry.opacities } : {}),
...(spec.geometry.mode === 'stroke'
? {
strokeWidth: spec.geometry.strokeWidth,
linecap: spec.geometry.linecap,
linejoin: spec.geometry.linejoin,
}
: {}),
},
favicon: {
background: spec.favicon.background,
foreground: spec.favicon.foreground,
radius: spec.favicon.radius,
},
sizes: spec.sizes,
assets: {
favicon: 'assets/kole-mark.svg',
mono: 'assets/kole-mark-mono.svg',
},
};
}
export function atomicWrite(file, content) {
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = `${file}.tmp-${process.pid}`;
fs.writeFileSync(tmp, content, 'utf8');
fs.renameSync(tmp, file);
}