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 断言其互斥),未删。
125 lines
4.8 KiB
JavaScript
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);
|
|
}
|