feat(theme): 全局装饰遮罩 + 尺寸统一;全局审阅修复对比度/暗色/平板布局
用户反馈的两点: 1. 遮罩看不出效果 → 人物与单车统一全局半透明 settings.decoOpacity(默认 .78) (此前 0.92/0.80 不一致且几乎无感),各自保留边缘渐隐 2. 切视图时人物比例变化 → 删掉卡片视图专用的 300px 高度,两种视图尺寸一致 3. 保留原始素材 → 原图归档 assets-src/,处理流程固化为 scripts/prepare-artwork.py 全局审阅发现并修复: - 无障碍对比度:两套主题的状态色当文字用时远低于 AA(最低 2.56:1)→ 双色体系 (填充色 ≥3:1 保持鲜亮 / 文字色 ≥4.5:1 同色系加深),暗色模式同步 - 暗色模式主题失效:顶栏/卡片/表头/分组行是硬编码白色 → 改为随模式切换的变量 - 平板竖屏 768px 表格需内部横滚 242px → 自动切卡片阈值 700 → 1024 - Cloudflare Worker 快照补齐 dataUpdatedAt/updateIntervalSec/dataFilter - 清理失效 CSS(.logo .halo) 测试脚本修掉两个自身 bug(均为误报):color(srgb/…) 与半透明底的解析、 渐变背景被误判为透明。verify-views 新增对比度断言 8 条,四套合计 89 项全通过。
This commit is contained in:
@@ -71,7 +71,13 @@ async function main() {
|
||||
const hits = [...document.querySelectorAll('table.monitor-table tbody tr, .card')]
|
||||
.map((el) => ({ r: el.getBoundingClientRect(), el }))
|
||||
.filter(({ r }) => r.right > a.left + 8 && r.bottom > a.top && r.top < a.bottom)
|
||||
.map(({ el }) => opaque(getComputedStyle(el).backgroundColor) || opaque(getComputedStyle(el.closest('.table-wrap, .grid') ?? el).backgroundColor));
|
||||
.map(({ el }) => {
|
||||
// 不透明判定要同时看背景色与背景图(卡片用的是渐变背景,backgroundColor 仍是透明)
|
||||
const cs = getComputedStyle(el);
|
||||
if (cs.backgroundImage !== 'none') return true;
|
||||
const chain = [cs, getComputedStyle(el.closest('.table-wrap, .grid') ?? el)];
|
||||
return chain.some((s) => s.backgroundImage !== 'none' || opaque(s.backgroundColor));
|
||||
});
|
||||
return { count: hits.length, allOpaque: hits.every(Boolean) };
|
||||
});
|
||||
if (artOverlap) check(`${view} 视图立绘不影响阅读(不重叠或位于面板之下)`, artOverlap.count === 0 || artOverlap.allOpaque, `重叠 ${artOverlap.count} 个,均在面板之下=${artOverlap.allOpaque}`);
|
||||
@@ -175,6 +181,69 @@ async function main() {
|
||||
await mp.screenshot({ path: path.join(__dirname, '..', 'docs', 'screenshot-mobile.png'), fullPage: true });
|
||||
await mCtx.close();
|
||||
|
||||
// ---------- I. 无障碍:文字与图形对比度(WCAG AA) ----------
|
||||
console.log('\n[I] 对比度(WCAG AA:正文 ≥4.5,图形 ≥3.0)');
|
||||
// 对比度需在表格视图下采样(在线/离线状态文字在表格行里)
|
||||
await page.goto(`${BASE}/?source=api&view=table`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('table.monitor-table tbody tr[data-status]', { timeout: 20000 });
|
||||
await page.waitForTimeout(1000);
|
||||
const contrast = await page.evaluate(() => {
|
||||
// 颜色可能是 rgb()/rgba() 或 color(srgb r g b / a)(color-mix 的结果),统一解析后逐层合成,
|
||||
// 否则半透明底色会被当成不透明、0~1 分量会被当成 0~255,算出的比值是错的
|
||||
const parseColor = (c) => {
|
||||
const nums = (c.match(/[\d.]+/g) ?? []).map(Number);
|
||||
const srgb = c.startsWith('color(srgb');
|
||||
const [r, g, b] = srgb ? nums.slice(0, 3).map((v) => v * 255) : nums.slice(0, 3);
|
||||
const a = srgb ? (nums.length > 3 ? nums[3] : 1) : (nums.length > 3 ? nums[3] : 1);
|
||||
return { r, g, b, a };
|
||||
};
|
||||
const effectiveBg = (el) => {
|
||||
const chain = [];
|
||||
for (let n = el; n; n = n.parentElement) chain.push(n);
|
||||
let bg = { r: 255, g: 255, b: 255 };
|
||||
for (const n of chain.reverse()) {
|
||||
const c = parseColor(getComputedStyle(n).backgroundColor);
|
||||
if (!c.a) continue;
|
||||
bg = { r: bg.r * (1 - c.a) + c.r * c.a, g: bg.g * (1 - c.a) + c.g * c.a, b: bg.b * (1 - c.a) + c.b * c.a };
|
||||
}
|
||||
return `rgb(${Math.round(bg.r)}, ${Math.round(bg.g)}, ${Math.round(bg.b)})`;
|
||||
};
|
||||
const luminance = ([r, g, b]) => {
|
||||
const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
|
||||
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
||||
};
|
||||
const ratio = (fg, bg) => {
|
||||
const a = luminance([parseColor(fg).r, parseColor(fg).g, parseColor(fg).b]);
|
||||
const b = luminance([parseColor(bg).r, parseColor(bg).g, parseColor(bg).b]);
|
||||
const [hi, lo] = a > b ? [a, b] : [b, a];
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
};
|
||||
const sample = (sel, label) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return null;
|
||||
return { label, text: el.textContent.trim().slice(0, 8), r: +ratio(getComputedStyle(el).color, effectiveBg(el)).toFixed(2) };
|
||||
};
|
||||
const out = [
|
||||
sample('tr[data-status="up"] .t-state', '在线状态文字'),
|
||||
sample('tr[data-status="down"] .t-state', '离线状态文字'),
|
||||
sample('tr[data-status="pending"] .t-state', '等待状态文字'),
|
||||
sample('#stat-up', '在线统计数字'),
|
||||
sample('#stat-down', '离线统计数字'),
|
||||
sample('#overall-pill', '顶部状态胶囊'),
|
||||
sample('.t-target', '目标地址'),
|
||||
sample('.footer-item', '页脚文字'),
|
||||
].filter(Boolean);
|
||||
// 图形:状态点边框色 vs 其所在卡片底色(图形标准 3:1)
|
||||
const dot = document.querySelector('.dot');
|
||||
if (dot) out.push({ label: '状态点图形', r: +ratio(getComputedStyle(dot).borderTopColor, effectiveBg(dot)).toFixed(2), graphic: true });
|
||||
return out;
|
||||
});
|
||||
for (const c of contrast) {
|
||||
if (c.r == null) continue;
|
||||
const need = c.graphic ? 3 : 4.5;
|
||||
check(`${c.label}对比度 ≥${need}`, c.r >= need, `${c.r}:1`);
|
||||
}
|
||||
|
||||
// 429 是刷新限流保护在起作用(快速连续导航各触发一次强制探测),面板已优雅降级为只读数据;
|
||||
// 这里只把 429 以外的错误视为失败
|
||||
const hardErrors = errors.filter((e) => !/429/.test(e));
|
||||
|
||||
Reference in New Issue
Block a user