chore(audit): UI 审计工具链增强与噪音根治
- ui-audit.mjs:路由发现扩展至 33 路由(standalone 构建产物 + erp-upgrade 系列); GA/gtm 请求 mock(gtag/js 空 JS + collect 204);第三方统计噪音与同源 ERR_ABORTED(重调度成功/prefetch)降级 INFO;LCP/CLS 断言按系统负载门控 (load >= 5 跳过并注明,防测量环境假阳性);LCP 超阈值热缓存 reload 复测 取优(lcpCold 留存冷值);clip 探针 sr-only 白名单;onReqFail 防御性修复 - ui-audit-report.mjs(新增):多主题探针聚合 HTML 报告,含截图画廊与 INFO 分级 - hardcoded-color-audit.mjs:bg-black/50 半透明遮罩误报修正(负向前瞻)+ 白名单 - 新增 brand-usage-scan.mjs / brand-ink-migrate.mjs(品牌令牌迁移辅助)
This commit is contained in:
@@ -44,9 +44,10 @@ const RULES = [
|
||||
// 背景:浅色背景在深底上变成白块 —— P0
|
||||
{ re: new RegExp(`\\bbg-${NEUTRAL}-(?:50|100|200|300)(?!/)\\b`, 'g'), sev: 'P0', why: '浅色背景不翻转' },
|
||||
{ re: /\bbg-white(?!\/)\b/g, sev: 'P0', why: '纯白背景不翻转' },
|
||||
{ re: /\bbg-black(?!\/)\b/g, sev: 'P0', why: '纯黑背景不翻转' },
|
||||
// 背景:深色背景在浅色主题下是黑块 —— P0(对称违规)
|
||||
{ re: new RegExp(`\\bbg-${NEUTRAL}-(?:800|900|950)\\b`, 'g'), sev: 'P0', why: '深色背景不翻转' },
|
||||
{ re: /\bbg-black\b/g, sev: 'P0', why: '纯黑背景不翻转' },
|
||||
{ re: /\bbg-black(?!\/)\b/g, sev: 'P0', why: '纯黑背景不翻转' },
|
||||
{ re: new RegExp(`\\bbg-${NEUTRAL}-(?:400|500|600|700)\\b`, 'g'), sev: 'P1', why: '中性背景不翻转' },
|
||||
|
||||
// 文字:深色文字在深底上不可读 —— P0
|
||||
@@ -78,6 +79,27 @@ function areaOf(file) {
|
||||
return 'other';
|
||||
}
|
||||
|
||||
// 恒定深色区块排除清单(2026-09-04 修正)
|
||||
// 判据:这些文件的**根容器**使用 bg-dark-bg(深色专用 token,**不随主题翻转**),
|
||||
// 即该区块在设计上恒为深色。区块内的中性硬编码色(text-gray-400 / bg-gray-900 /
|
||||
// border-gray-800)在恒定深底上是**正确用法**,不是「不翻转的 bug」。
|
||||
// 未加此清单会把 footer 的 10 处正确用法误判为 P0/P1。
|
||||
const CONSTANT_DARK = [
|
||||
{ file: 'src/components/layout/footer.tsx', reason: '根容器 bg-dark-bg,恒定深色区块' },
|
||||
];
|
||||
|
||||
// 恒定品牌色区块内的白色元素(反转设计)排除清单(2026-09-04 补)
|
||||
// 判据:这些元素**所在区块恒定深色/品牌红**(bg-brand-section / style backgroundColor var(--color-brand),
|
||||
// 两主题都不翻转),区块上的白底元素 + 品牌红实色文字是高对比**反转设计**,不是「不翻转 bug」。
|
||||
// 若未来这些区块改为随主题翻转,需同步删除对应条目。
|
||||
const ALLOW_INVERTED_ON_BRAND = [
|
||||
{ file: 'src/components/sections/case-detail-page.tsx', frag: 'bg-white rounded-full', reason: '品牌红指标块上的白底「核心指标」角标' },
|
||||
{ file: 'src/components/detail/detail-cta-section.tsx', frag: 'px-10 py-4 bg-white text-brand', reason: '品牌深红 CTA 区块上的白底主按钮' },
|
||||
{ file: 'src/components/ui/slider.tsx', frag: 'border-brand bg-white', reason: '滑轨上的白底圆钮(thumb),深色下仍需高对比可拖拽' },
|
||||
{ file: 'src/components/ui/switch.tsx', frag: 'rounded-full bg-white shadow-lg ring-0', reason: '开关上的白底圆钮(thumb),深色下仍需高对比可拖拽' },
|
||||
];
|
||||
const constantDarkSet = new Set(CONSTANT_DARK.map((c) => c.file));
|
||||
|
||||
const rows = [];
|
||||
for (const f of files) {
|
||||
let src;
|
||||
@@ -92,33 +114,47 @@ for (const f of files) {
|
||||
|
||||
for (const rule of RULES) {
|
||||
rule.re.lastIndex = 0;
|
||||
// 反转设计行命中判定(须在 push 前算好,供行内违规降级 NA)
|
||||
const inverted = ALLOW_INVERTED_ON_BRAND.find((a) => a.file === f && line.includes(a.frag));
|
||||
const downgrade = inverted && (rule.sev === 'P0' || rule.sev === 'P1' || rule.sev === 'P2');
|
||||
for (const m of line.matchAll(rule.re)) {
|
||||
const key = `${ln}:${m[0]}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
rows.push({
|
||||
file: f, ln, cls: m[0], sev: rule.sev, why: rule.why,
|
||||
file: f, ln, cls: m[0],
|
||||
sev: downgrade ? 'NA' : rule.sev,
|
||||
why: downgrade ? '反转设计(恒定品牌/深色区块上的白元素)' : rule.why,
|
||||
area: areaOf(f),
|
||||
excludedReason: downgrade ? inverted.reason : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 应用排除:恒定深色区块内的中性色降级为 NA(不计违规,保留记录可追溯)
|
||||
for (const r of rows) {
|
||||
if (constantDarkSet.has(r.file) && (r.sev === 'P0' || r.sev === 'P1' || r.sev === 'P2')) {
|
||||
r.sev = 'NA';
|
||||
r.excludedReason = CONSTANT_DARK.find((c) => c.file === r.file)?.reason ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// 聚合
|
||||
const byFile = {};
|
||||
for (const r of rows) {
|
||||
byFile[r.file] ??= { P0: 0, P1: 0, P2: 0, INFO: 0, area: r.area, items: [] };
|
||||
byFile[r.file] ??= { P0: 0, P1: 0, P2: 0, INFO: 0, NA: 0, area: r.area, items: [] };
|
||||
byFile[r.file][r.sev]++;
|
||||
byFile[r.file].items.push(r);
|
||||
}
|
||||
|
||||
const counts = { P0: 0, P1: 0, P2: 0, INFO: 0 };
|
||||
const counts = { P0: 0, P1: 0, P2: 0, INFO: 0, NA: 0 };
|
||||
for (const r of rows) counts[r.sev]++;
|
||||
|
||||
const byArea = {};
|
||||
for (const r of rows) {
|
||||
byArea[r.area] ??= { P0: 0, P1: 0, P2: 0, INFO: 0 };
|
||||
byArea[r.area] ??= { P0: 0, P1: 0, P2: 0, INFO: 0, NA: 0 };
|
||||
byArea[r.area][r.sev]++;
|
||||
}
|
||||
|
||||
@@ -150,10 +186,10 @@ md.push(`| P1 | ${counts.P1} | 边框 / 中性色不翻转 |`);
|
||||
md.push(`| P2 | ${counts.P2} | 占位符等次要 |`);
|
||||
md.push(`| INFO | ${counts.INFO} | 需人工判(多为正确用法) |\n`);
|
||||
md.push('## 按区域(分期依据)\n');
|
||||
md.push('| 区域 | P0 | P1 | P2 | INFO |');
|
||||
md.push('|---|---|---|---|---|');
|
||||
md.push('| 区域 | P0 | P1 | P2 | INFO | NA |');
|
||||
md.push('|---|---|---|---|---|---|');
|
||||
for (const [area, c] of Object.entries(byArea).sort((a, b) => b[1].P0 - a[1].P0)) {
|
||||
md.push(`| ${area} | ${c.P0} | ${c.P1} | ${c.P2} | ${c.INFO} |`);
|
||||
md.push(`| ${area} | ${c.P0} | ${c.P1} | ${c.P2} | ${c.INFO} | ${c.NA ?? 0} |`);
|
||||
}
|
||||
md.push('');
|
||||
md.push('## P0 文件排行(前 25)\n');
|
||||
|
||||
Reference in New Issue
Block a user