Files
jiu/tools/ds-compare.mjs
T
wangjia c314fb381d feat(tools): Phase 0.3 ds-compare 注册表驱动(每屏+外壳都跟原型比)
- 抽出 tools/screens.mjs 屏注册表单一真源,fidelity 与 ds-compare 共用
- ds-compare 改为遍历注册表(含 shell 外壳项),默认全屏×三主题产并排目检图;
  保留 --html/--prefix 临时单屏;统一注入 NotoSansSC 与 golden 同字体
- 满足规则2:每一屏 + 外壳都需跟原型对比

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
2026-07-01 20:46:52 +08:00

102 lines
4.9 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.
#!/usr/bin/env node
/**
* ds-compare.mjs — 还原保真人工目检编排(原型基准 ↕ Flutter golden 并排)
*
* design-distill 阶段4「保真闸」的人工目检侧:对屏按 A/B/C 三主题截原型基准图(注入
* 统一字体),再与已生成的 Flutter golden 用 montage 上下并排,产出对比图供人工逐屏目检。
* 跨渲染器(Chromium↔Skia)不跑严格 pixelmatch(那是 fidelity.mjs 的活);这里靠肉眼兜底像素级。
*
* 「每一屏 + 外壳都要跟原型比」(规则 2):默认遍历 tools/screens.mjs 注册表(含 shell)。
*
* 前置:先生成对应 Flutter golden
* cd client && flutter test --update-goldens test/golden/<screen>_golden_test.dart
*
* 用法(仓库根运行):
* node tools/ds-compare.mjs # 注册表所有屏(含外壳) × 三主题
* node tools/ds-compare.mjs inventory shell # 指定屏
* node tools/ds-compare.mjs --themes a # 指定主题
* node tools/ds-compare.mjs --html <x.html> --prefix <p> [--width --height --wait-for] # 临时单屏(不在册时)
*
* 输出:design/_compare/<prefix>_<theme>.pnggitignore,可重生,上=原型 下=Flutter)。
* 依赖:design-distill skillshoot-prototype.mjs / montage.mjs),默认 ~/.claude/skills/design-distillDD_SKILL 可覆盖。
*/
import { parseArgs } from 'node:util';
import { execFileSync } from 'node:child_process';
import { mkdirSync, existsSync, writeFileSync } from 'node:fs';
import { homedir, tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { SCREENS } from './screens.mjs';
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
themes: { type: 'string', default: 'a,b,c' },
// 临时单屏(不在注册表时):
html: { type: 'string' },
prefix: { type: 'string' },
width: { type: 'string', default: '1280' },
height: { type: 'string', default: '900' },
'wait-for': { type: 'string', default: '.app' },
'goldens-dir': { type: 'string', default: 'client/test/golden/goldens' },
help: { type: 'boolean', default: false },
},
});
if (values.help) {
console.log(`用法: node tools/ds-compare.mjs [screen...] [--themes a,b,c]\n 临时单屏: --html <x.html> --prefix <p> [--width --height --wait-for]\n已注册屏: ${Object.keys(SCREENS).join(', ')}`);
process.exit(0);
}
const SKILL = process.env.DD_SKILL || join(homedir(), '.claude/skills/design-distill');
const shoot = join(SKILL, 'tools/shoot-prototype.mjs');
const montage = join(SKILL, 'tools/montage.mjs');
for (const t of [shoot, montage]) {
if (!existsSync(t)) { console.error(`[ds-compare] 找不到 skill 工具: ${t}(设 DD_SKILL 覆盖)`); process.exit(2); }
}
const outDir = 'design/_compare';
mkdirSync(outDir, { recursive: true });
const themes = values.themes.split(',').map((s) => s.trim()).filter(Boolean);
// 统一字体注入(与 fidelity.mjs 一致):原型 --font/--font-mono → NotoSansSC,与 golden 同字体。
const injectCss = join(tmpdir(), 'jiu-dscompare-font.css');
writeFileSync(injectCss, [
`@font-face{font-family:'NotoSansSC';src:url('/client/assets/fonts/NotoSansSC.ttf');font-weight:100 900;font-display:block;}`,
`:root{--font:'NotoSansSC',sans-serif!important;--font-mono:'NotoSansSC',monospace!important;}`,
`*{font-family:'NotoSansSC',sans-serif!important;}`,
].join('\n'));
// 构造待对比清单:临时单屏 优先;否则用注册表(指定屏名或全部)。
let jobs;
if (values.html && values.prefix) {
jobs = [{ html: values.html, prefix: values.prefix, width: +values.width, height: +values.height, dpr: 2, waitFor: values['wait-for'] }];
} else {
const names = positionals.length ? positionals : Object.keys(SCREENS);
jobs = [];
for (const n of names) {
if (!SCREENS[n]) { console.error(`[ds-compare] 未注册屏: ${n}(有: ${Object.keys(SCREENS).join(', ')}`); process.exit(1); }
jobs.push(SCREENS[n]);
}
}
for (const s of jobs) {
for (const theme of themes) {
const base = join(outDir, `${s.prefix}_proto_${theme}.png`);
const golden = resolve(values['goldens-dir'], `${s.prefix}_${theme}.png`);
const out = join(outDir, `${s.prefix}_${theme}.png`);
execFileSync('node', [shoot, s.html, base,
'--width', String(s.width), '--height', String(s.height), '--dpr', String(s.dpr || 2),
'--theme', theme, '--lang', 'zh', '--wait-for', s.waitFor,
'--inject-css', injectCss, '--wait', '1200'],
{ stdio: 'inherit' });
if (!existsSync(golden)) {
console.error(`[ds-compare] ⚠ 缺 Flutter golden: ${golden} —— 先跑 flutter test --update-goldens。跳过 ${s.prefix}·${theme} 并排。`);
continue;
}
execFileSync('node', [montage, out, base, golden, '--dir', 'v'], { stdio: 'inherit' });
console.log(`[ds-compare] ✅ ${s.prefix}·${theme}: ${out}(上=原型 下=Flutter,人工目检)`);
}
}