Files
WoMenQuNaJu/MeetSpot/tools/validate_colors.py
2026-02-04 16:11:55 +08:00

217 lines
6.1 KiB
Python
Raw 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.
"""
WCAG 2.1色彩对比度验证工具
验证所有设计token中的颜色组合是否符合WCAG标准:
- AA级 (正文): 对比度 ≥ 4.5:1
- AA级 (大文字): 对比度 ≥ 3.0:1
- AAA级 (正文): 对比度 ≥ 7.0:1
使用方法:
python tools/validate_colors.py
或集成到CI/CD:
pytest tests/test_accessibility.py::test_color_contrast
"""
import math
import sys
from pathlib import Path
from typing import Dict, Tuple
# 添加项目根目录到Python路径
sys.path.insert(0, str(Path(__file__).parent.parent))
from app.design_tokens import DesignTokens
def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
"""将十六进制颜色转换为RGB"""
hex_color = hex_color.lstrip("#")
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
def relative_luminance(rgb: Tuple[int, int, int]) -> float:
"""
计算相对亮度 (WCAG公式)
https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
"""
r, g, b = [x / 255.0 for x in rgb]
def adjust(channel):
if channel <= 0.03928:
return channel / 12.92
return math.pow((channel + 0.055) / 1.055, 2.4)
r, g, b = adjust(r), adjust(g), adjust(b)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def contrast_ratio(color1: str, color2: str) -> float:
"""
计算两个颜色之间的对比度
https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
Returns:
对比度比值 (1:1 到 21:1)
"""
lum1 = relative_luminance(hex_to_rgb(color1))
lum2 = relative_luminance(hex_to_rgb(color2))
lighter = max(lum1, lum2)
darker = min(lum1, lum2)
return (lighter + 0.05) / (darker + 0.05)
def check_wcag_compliance(
foreground: str, background: str, level: str = "AA", text_size: str = "normal"
) -> Dict[str, any]:
"""
检查颜色组合是否符合WCAG标准
Args:
foreground: 前景色 (文字)
background: 背景色
level: WCAG级别 ("AA""AAA")
text_size: 文字大小 ("normal""large")
Returns:
{
"ratio": 对比度,
"passes": 是否通过,
"level": 合规级别,
"recommendation": 建议
}
"""
ratio = contrast_ratio(foreground, background)
# WCAG标准阈值
thresholds = {
"AA": {"normal": 4.5, "large": 3.0},
"AAA": {"normal": 7.0, "large": 4.5},
}
required_ratio = thresholds[level][text_size]
passes = ratio >= required_ratio
result = {
"ratio": round(ratio, 2),
"passes": passes,
"level": level,
"required": required_ratio,
"foreground": foreground,
"background": background,
}
# 生成建议
if not passes:
if ratio < required_ratio * 0.8:
result["recommendation"] = "对比度严重不足,需要更换颜色"
else:
result["recommendation"] = "对比度略低,建议微调颜色深浅"
else:
if ratio >= thresholds["AAA"][text_size]:
result["recommendation"] = "优秀符合WCAG AAA级标准"
else:
result["recommendation"] = "符合WCAG AA级标准"
return result
def validate_design_tokens():
"""验证所有设计token的色彩对比度"""
results = []
print("=" * 80)
print("MeetSpot Design Tokens - WCAG 2.1色彩对比度验证报告")
print("=" * 80)
print()
# 1. 验证品牌色在白色背景上
print("📊 品牌色 vs 白色背景")
print("-" * 80)
white_bg = DesignTokens.BACKGROUND["primary"]
for color_name, color_value in DesignTokens.BRAND.items():
if color_name == "gradient":
continue # 跳过渐变
result = check_wcag_compliance(color_value, white_bg, "AA", "normal")
results.append(result)
status = "✅ PASS" if result["passes"] else "❌ FAIL"
print(
f"{status} | {color_name:20s} | {color_value:10s} | {result['ratio']:5.2f}:1 | {result['recommendation']}"
)
print()
# 2. 验证文字色在白色背景上
print("📊 文字色 vs 白色背景")
print("-" * 80)
for color_name, color_value in DesignTokens.TEXT.items():
if color_name == "inverse":
continue # 跳过反转色
result = check_wcag_compliance(color_value, white_bg, "AA", "normal")
results.append(result)
status = "✅ PASS" if result["passes"] else "❌ FAIL"
print(
f"{status} | {color_name:20s} | {color_value:10s} | {result['ratio']:5.2f}:1 | {result['recommendation']}"
)
print()
# 3. 验证场所主题色
print("📊 场所主题色验证 (主色 vs 白色背景)")
print("-" * 80)
for venue_name, theme in DesignTokens.VENUE_THEMES.items():
if venue_name == "default":
continue
# 主色 vs 白色背景 (用于大文字/按钮)
result = check_wcag_compliance(
theme["theme_primary"], white_bg, "AA", "large" # 大文字标准 (3.0:1)
)
results.append(result)
status = "✅ PASS" if result["passes"] else "❌ FAIL"
print(
f"{status} | {venue_name:12s} | {theme['theme_primary']:10s} | {result['ratio']:5.2f}:1 | {result['recommendation']}"
)
# 深色 vs 浅色背景 (用于卡片内文字)
result_card = check_wcag_compliance(
theme["theme_dark"], theme["theme_light"], "AA", "normal"
)
results.append(result_card)
status_card = "✅ PASS" if result_card["passes"] else "❌ FAIL"
print(
f" └─ {status_card} | 卡片文字 | {theme['theme_dark']:10s} on {theme['theme_light']:10s} | {result_card['ratio']:5.2f}:1"
)
print()
print("=" * 80)
# 统计结果
total = len(results)
passed = sum(1 for r in results if r["passes"])
failed = total - passed
print(f"验证总数: {total}")
print(f"✅ 通过: {passed} ({passed/total*100:.1f}%)")
print(f"❌ 失败: {failed} ({failed/total*100:.1f}%)")
print("=" * 80)
# 返回是否全部通过
return failed == 0
if __name__ == "__main__":
all_passed = validate_design_tokens()
sys.exit(0 if all_passed else 1)