Files
zhangxiang d959fa9a81 refactor(theme): 营销页 bg-white token 化(暗黑模式 Phase 2 · marketing 批次)
问题:Tailwind 原生 bg-white 是硬编码值,深色主题下不翻转,
导致整页/整 section 恒为纯白,与转深色的页头页脚割裂。
(globals.css:--color-bg-primary-rgb 浅色 255 255 255 → 深色 10 14 20)

改动:10 文件 67 处 bg-white → 项目 token,按语义二分类
- bg-bg-primary  (33 处):页面/大块骨架(min-h-screen 容器、<section>、<main>)
- bg-bg-elevated (34 处):浮起层(带 border 的卡片、表单输入、图标盒、grid 单元格)
  --color-bg-elevated 浅色 #FFFFFF → 深色 #151B23,同样翻转。

浅色主题下 bg-white / bg-bg-primary / bg-bg-elevated 三者同值(纯白),
故本次改动在浅色主题下零像素差异,只修复深色主题。

新增审计工具:
- scripts/audit/hardcoded-color-audit.mjs:硬编码颜色分级盘点
  (含 alpha 误报修正:bg-white/NN 半透明叠加两主题均成立,判 INFO 非 P0)
- scripts/audit/darkmode-bg-verify.mjs:直接验证背景随主题翻转
  (浅色应纯白、深色应 rgb(10,14,20) 且无残留纯白块)

审计结果:marketing P0 67 → 0;全站 P0 97 → 30
(剩余 components 16 / other 8 / layout 3 / ui-kit 3 为第二批;admin 本就为 0)

门禁:
- tsc 0 error
- eslint 0 error(50 warnings 全为既有,改动文件未新增)
- jest 129/130 套件通过,1548 通过 / 2 跳过 / **0 断言失败**
  唯一失败 src/lib/admin-api.test.ts 为**沙箱环境限制非代码问题**:
  CODEBUDDY_BROKER_DENY 拦截 readFileSync,单独复验同样失败,
  与本次改动零交集(改动仅限 src/app/(marketing)/ 下 10 个页面组件)
- 暗黑模式背景验证 18/18 PASS(9 路由 × 浅深双版)
2026-09-20 11:50:58 +08:00

103 lines
4.2 KiB
JavaScript

// 暗黑模式背景翻转验证(Phase 2 token 化专用)
//
// 用途:直接验证「背景随主题翻转」这一改动目标,而不只是验证「页面没报错」。
//
// 机理(globals.css):
// --color-bg-primary-rgb 浅色 255 255 255 (L70) → 深色 10 14 20 (L409)
// --color-bg-elevated 浅色 #FFFFFF (L69) → 深色 #151B23 (L415)
// 因此浅色主题下 bg-bg-primary / bg-bg-elevated 都渲染为纯白 —— 与改动前的
// bg-white **像素一致**(零回归);深色主题下才显现差异。
//
// 断言:
// light : 根节点背景 == rgb(255, 255, 255)(与改动前一致)
// dark : 根节点背景 != rgb(255, 255, 255) 且亮度低(已翻转为深色)
// dark : 页面内不得残留大面积纯白块(抽样主要 section / 卡片)
//
// 环境约束(2026-09-02 踩坑固化):
// - 必须 localhost(非 127.0.0.1):Next dev allowedDevOrigins 白名单不含 127.0.0.1
// - 必须 --no-proxy-server:本机系统代理会拦 403
//
// CLI: node scripts/audit/darkmode-bg-verify.mjs [--routes /,/contact] [--out <dir>]
import { chromium } from 'playwright';
import { writeFileSync, mkdirSync } from 'node:fs';
const args = process.argv.slice(2);
const getList = (flag, def) => {
const i = args.indexOf(flag);
return i >= 0 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1].split(',') : def;
};
const ROUTES = getList('--routes', ['/', '/contact', '/products', '/solutions', '/team', '/cases', '/news', '/methodology', '/products/erp-upgrade']);
const BASE = 'http://localhost:3000';
const OUT = 'dogfood-color-audit';
mkdirSync(OUT, { recursive: true });
const browser = await chromium.launch({ args: ['--no-proxy-server'] });
/** 亮度:0(黑) ~ 255(白) */
function luma(rgb) {
const m = rgb.match(/\d+/g);
if (!m) return null;
const [r, g, b] = m.map(Number);
return Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);
}
async function probe(path, theme) {
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.addInitScript((t) => {
try { localStorage.setItem('novalon-theme', t); } catch {}
}, theme);
const errors = [];
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', (e) => errors.push('PAGEERROR: ' + e.message));
await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 45000 });
const info = await page.evaluate(() => {
const pick = (el) => (el ? getComputedStyle(el).backgroundColor : null);
const body = pick(document.body);
// 抽样:主内容区的前若干 section / 大块容器
const blocks = Array.from(document.querySelectorAll('section, main, [class*="min-h-screen"]'))
.slice(0, 12)
.map((el) => getComputedStyle(el).backgroundColor);
return { body, blocks, html: getComputedStyle(document.documentElement).backgroundColor };
});
await ctx.close();
const bodyLuma = luma(info.body);
// 页面里仍然纯白的块(排除透明)
const whiteBlocks = info.blocks.filter((c) => luma(c) !== null && luma(c) >= 250);
let ok;
let note;
if (theme === 'light') {
ok = bodyLuma !== null && bodyLuma >= 250;
note = `body=${info.body} (luma=${bodyLuma}),浅色应为纯白(与改动前一致)`;
} else {
ok = bodyLuma !== null && bodyLuma < 60 && whiteBlocks.length === 0;
note = `body=${info.body} (luma=${bodyLuma}),残留纯白块 ${whiteBlocks.length} 个`;
}
return { path, theme, ok, bodyLuma, whiteBlocks: whiteBlocks.length, errors, note };
}
const results = [];
for (const r of ROUTES) {
results.push(await probe(r, 'light'));
results.push(await probe(r, 'dark'));
}
await browser.close();
const failed = results.filter((r) => !r.ok || r.errors.length > 0);
writeFileSync(`${OUT}/darkmode-bg-verify.json`, JSON.stringify({ generatedAt: new Date().toISOString(), routes: ROUTES, results, failedCount: failed.length }, null, 2));
for (const r of results) {
console.log(`${r.ok && r.errors.length === 0 ? 'PASS' : 'FAIL'} ${r.theme.padEnd(5)} ${r.path.padEnd(24)} ${r.note}`);
r.errors.slice(0, 2).forEach((e) => console.log(` └ ${e.slice(0, 140)}`));
}
console.log(`\n${results.length - failed.length}/${results.length} PASS`);
process.exit(failed.length > 0 ? 1 : 0);