""" 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)