/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
AI 辅助看板配色与对比度审查:基于 WCAG 标准的自动化前端质检流水线

在企业级数据可视化平台引入大语言模型(LLM)自动生成看板和图表配置后,前端团队迎来了一个全新的挑战——AI 生成样式的合规性与视觉可用性审查(Visual Linting & Contrast Auditing)。
通用大模型在生成 ECharts 或 CSS 样式代码时,通常缺乏严格的物理显示环境感知:
- 在深蓝底色的看板上,偶尔会生成浅灰色(#6b7280)的副标题和坐标轴文字,导致在阳光直射或会议室投影仪下,文字对比度严重不足,肉眼几乎无法阅读;
- 在饼图扇区中,随机分配了几个明度极其接近的颜色,导致相邻色块在色觉障碍人群(如红绿色盲、弱视)眼中完全糊成一团。
根据国际万维网联盟(W3C)发布的 Web 内容无障碍指南(WCAG 2.1),所有面向生产环境的文本和核心图形,必须严格满足特定的**对比度阈值(Contrast Ratio)**要求。
如何在大模型生成图表配置之后、渲染上屏之前,通过一段轻量级的前端质检流水线,自动检测出所有不合规的配色并无损智能修正?
今天我们系统拆解基于 WCAG 2.1 AA 级标准 的自动化色彩质检与对比度动态修复流水线。
WCAG 2.1 对比度核心数学定义
色彩的视觉反差不是简单的 RGB 数值相减,而是基于人类视网膜感知的**相对明度(Relative Luminance, $L$)**计算的比率:
$$L = 0.2126 \\times R_{\\text{lin}} + 0.7152 \\times G_{\\text{lin}} + 0.0722 \\times B_{\\text{lin}}$$
两颜色之间的对比度比率(Contrast Ratio, $CR$)计算公式为:
$$CR = \\frac{L_1 + 0.05}{L_2 + 0.05} \\quad (L_1 \\ge L_2)$$
+———————————————————————————————–+
| 【WCAG 2.1 对比度标准合规红线】 |
| |
| 1. 普通正文文本 (字号 < 18pt / 24px) : 对比度必须 >= 4.5 : 1 (AA 级标准) |
| 2. 大号标题文本 (字号 >= 18pt 或 粗体 14pt) : 对比度必须 >= 3.0 : 1 (AA 级标准) |
| 3. 图表核心图形元素 (如折线、柱子边框、图例标记) : 对比度必须 >= 3.0 : 1 |
| 4. 极端不合规警报 : 对比度 < 2.0 : 1 属于绝对无法辨识的视觉废料,必须强制熔断拦截! |
+———————————————————————————————–+
自动化质检与修复流水线架构
[ 大模型生成的原始 ECharts Option 配置 JSON ]
│
▼
+———————————————————————————————–+
| 第一步:DOM 背景色与文本节点遍历扫描 (Theme & Node Extractor) |
| – 提取全局背景色 (如 `#0f172a`),递归抓取所有 `title.color`、`axisLabel.color`、`legend.color` |
+———————————————————————————————–+
│
▼
+———————————————————————————————–+
| 第二步:WCAG 相对明度与对比度矩阵计算 (Contrast Auditor) |
| – 计算每个文本/图元与所属背景的真实对比度值 CR |
+———————————————————————————————–+
│
▼ (若 CR < 4.5:1,触发自动智能调优)
+———————————————————————————————–+
| 第三步:色相不变性明度自动爬升/压暗算法 (HSL Luminance Auto-Adjustment) |
| – 保持原有色相 (Hue) 和饱和度 (Saturation) 绝对不变,仅单向提升或降低 Lightness (L), |
| 直到对比度刚好达到 4.5:1 安全线! |
+———————————————————————————————–+
│
▼
[ 输出 100% 符合无障碍标准的高清锐利看板配置 ]
核心实现代码:TypeScript / JavaScript 自动化质检模块
// WCAGColorAuditor.js – 生产级质检核心库
export class WCAGColorAuditor {
// 1. 将 HEX 颜色转换为 sRGB 线性相对明度 (0.0 ~ 1.0)
static getRelativeLuminance(hex) {
const cleanHex = hex.replace('#', '');
const r = parseInt(cleanHex.substring(0, 2), 16) / 255;
const g = parseInt(cleanHex.substring(2, 4), 16) / 255;
const b = parseInt(cleanHex.substring(4, 6), 16) / 255;
const toLinear = (c) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}
// 2. 计算两颜色之间的精确对比度
static getContrastRatio(hex1, hex2) {
const l1 = this.getRelativeLuminance(hex1);
const l2 = this.getRelativeLuminance(hex2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// 3. HEX 转 HSL
static hexToHsl(hex) {
const cleanHex = hex.replace('#', '');
let r = parseInt(cleanHex.substring(0, 2), 16) / 255;
let g = parseInt(cleanHex.substring(2, 4), 16) / 255;
let b = parseInt(cleanHex.substring(4, 6), 16) / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max – min;
s = l > 0.5 ? d / (2 – max – min) : d / (max + min);
switch (max) {
case r: h = (g – b) / d + (g < b ? 6 : 0); break;
case g: h = (b – r) / d + 2; break;
case b: h = (r – g) / d + 4; break;
}
h /= 6;
}
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
}
// 4. HSL 转 HEX
static hslToHex(h, s, l) {
s /= 100; l /= 100;
const a = s * Math.min(l, 1 – l);
const f = n => {
const k = (n + h / 30) % 12;
const color = l – a * Math.max(Math.min(k – 3, 9 – k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}`;
}
// 5. 核心:智能修复对比度(微调明度直到满足目标对比度)
static autoFixContrast(textColor, bgColor = '#0f172a', targetRatio = 4.5) {
let currentRatio = this.getContrastRatio(textColor, bgColor);
if (currentRatio >= targetRatio) return textColor; // 已经达标,原样返回
const [h, s, l] = this.hexToHsl(textColor);
const bgLuminance = this.getRelativeLuminance(bgColor);
const isDarkBg = bgLuminance < 0.5;
let adjustedL = l;
let step = isDarkBg ? 2 : -2; // 暗底向上提亮,亮底向下压暗
while (adjustedL >= 0 && adjustedL <= 100) {
adjustedL += step;
const candidateHex = this.hslToHex(h, s, adjustedL);
if (this.getContrastRatio(candidateHex, bgColor) >= targetRatio) {
return candidateHex; // 成功找到最小改动的高清替代色!
}
}
return isDarkBg ? '#ffffff' : '#000000'; // 极限兜底
}
// 6. 全图表配置自动巡检与清洗
static auditAndSanitizeEChartsOption(option, bgColor = '#0f172a') {
const sanitized = JSON.parse(JSON.stringify(option));
// 检查并修复标题颜色
if (sanitized.title && sanitized.title.textStyle && sanitized.title.textStyle.color) {
sanitized.title.textStyle.color = this.autoFixContrast(sanitized.title.textStyle.color, bgColor, 4.5);
}
// 检查并修复 X 轴与 Y 轴刻度标签颜色
const sanitizeAxis = (axis) => {
if (!axis) return;
const axes = Array.isArray(axis) ? axis : [axis];
axes.forEach(a => {
if (a.axisLabel && a.axisLabel.color) {
a.axisLabel.color = this.autoFixContrast(a.axisLabel.color, bgColor, 4.5);
}
});
};
sanitizeAxis(sanitized.xAxis);
sanitizeAxis(sanitized.yAxis);
return sanitized;
}
}
落地效果与三大质检红线
在引入该质检流水线后,AI 自动生成的 200 多个企业大屏看板在不同硬件终端上的展示效果得到了质的飞跃:



