diff --git a/docs/color-heading-optimization-report.md b/docs/color-heading-optimization-report.md new file mode 100644 index 0000000..3076d27 --- /dev/null +++ b/docs/color-heading-optimization-report.md @@ -0,0 +1,3 @@ +# 颜色对比度和标题层级优化报告 +## 项目概述 +本报告记录了Novalon网站的颜色对比度和标题层级结构优化工作,旨在提升网站的可访问性(WCAG 2.1 AA标准)和SEO性能。 diff --git a/docs/heading-hierarchy-guidelines.md b/docs/heading-hierarchy-guidelines.md new file mode 100644 index 0000000..7dca8ad --- /dev/null +++ b/docs/heading-hierarchy-guidelines.md @@ -0,0 +1 @@ +# 标题层级结构规范 diff --git a/package.json b/package.json index 83d2d9a..228a7af 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "test": "playwright test", "test:smoke": "playwright test --grep @smoke", "test:report": "allure generate test-results/allure-results && allure open", + "check:contrast": "tsx scripts/check-color-contrast.ts", + "check:headings": "tsx scripts/check-heading-hierarchy.ts", "audit:performance": "node scripts/performance-audit.js", "audit:seo": "node scripts/seo-check.js", "audit:accessibility": "node scripts/accessibility-test.js", @@ -19,6 +21,7 @@ }, "dependencies": { "@antv/g2": "^5.4.8", + "@playwright/test": "^1.58.2", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@types/three": "^0.183.1", @@ -48,6 +51,7 @@ "eslint-config-next": "^0.2.4", "lighthouse": "^13.0.3", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } } diff --git a/scripts/check-color-contrast.ts b/scripts/check-color-contrast.ts new file mode 100644 index 0000000..9158310 --- /dev/null +++ b/scripts/check-color-contrast.ts @@ -0,0 +1,62 @@ +import { calculateContrastRatio, meetsWCAGStandard } from '../src/lib/color-contrast.ts'; + +interface ColorPair { + name: string; + foreground: string; + background: string; + textSize?: 'normal' | 'large'; +} + +const criticalColorPairs: ColorPair[] = [ + { name: 'Primary text on primary background', foreground: '#1C1C1C', background: '#FFFFFF' }, + { name: 'Secondary text on primary background', foreground: '#3D3D3D', background: '#FFFFFF' }, + { name: 'Tertiary text on primary background', foreground: '#4A4A4A', background: '#FFFFFF' }, + { name: 'Muted text on primary background', foreground: '#6B6B6B', background: '#FFFFFF' }, + { name: 'Brand primary on white', foreground: '#C41E3A', background: '#FFFFFF' }, + { name: 'Brand primary on brand bg', foreground: '#C41E3A', background: '#FEF2F4' }, + { name: 'Link on hover', foreground: '#C41E3A', background: '#FFFFFF' }, +]; + +function checkContrast() { + console.log('🎨 Color Contrast Check\n'); + + let failures = 0; + let passes = 0; + + criticalColorPairs.forEach(pair => { + const result = meetsWCAGStandard( + pair.foreground, + pair.background, + 'AA', + pair.textSize || 'normal' + ); + + const status = result.passes ? '✅ PASS' : '❌ FAIL'; + const ratio = result.ratio.toFixed(2); + + console.log(`${status} ${pair.name}`); + console.log(` Ratio: ${ratio}:1 (Required: ${result.requiredRatio}:1)`); + console.log(` FG: ${pair.foreground} | BG: ${pair.background}\n`); + + if (result.passes) { + passes++; + } else { + failures++; + } + }); + + console.log(`\n📊 Summary:`); + console.log(` Total: ${criticalColorPairs.length}`); + console.log(` ✅ Passes: ${passes}`); + console.log(` ❌ Failures: ${failures}`); + + if (failures > 0) { + console.log('\n⚠️ Some color pairs do not meet WCAG AA standards!'); + process.exit(1); + } else { + console.log('\n✅ All color pairs meet WCAG AA standards!'); + process.exit(0); + } +} + +checkContrast(); diff --git a/scripts/check-heading-hierarchy.ts b/scripts/check-heading-hierarchy.ts new file mode 100644 index 0000000..17fe327 --- /dev/null +++ b/scripts/check-heading-hierarchy.ts @@ -0,0 +1 @@ +import { chromium } from "playwright"; diff --git a/scripts/test-css-contrast.js b/scripts/test-css-contrast.js new file mode 100644 index 0000000..a303dcf --- /dev/null +++ b/scripts/test-css-contrast.js @@ -0,0 +1,25 @@ +const { calculateContrastRatio, meetsWCAGStandard } = require('../src/lib/color-contrast.ts'); + +console.log('Testing CSS color contrast...'); + +const primaryResult = meetsWCAGStandard('#1C1C1C', '#FFFFFF', 'AA', 'normal'); +console.log('Primary text (#1C1C1C) on background (#FFFFFF):', primaryResult); + +const tertiaryResult = meetsWCAGStandard('#4A4A4A', '#FFFFFF', 'AA', 'normal'); +console.log('Tertiary text (#4A4A4A) on background (#FFFFFF):', tertiaryResult); + +const mutedResult = meetsWCAGStandard('#6B6B6B', '#FFFFFF', 'AA', 'normal'); +console.log('Muted text (#6B6B6B) on background (#FFFFFF):', mutedResult); + +console.log('\nExpected: All should pass (passes: true)'); +console.log('Actual results:'); +console.log('- Primary:', primaryResult.passes ? '✓ PASS' : '✗ FAIL', `(ratio: ${primaryResult.ratio.toFixed(2)}:1)`); +console.log('- Tertiary:', tertiaryResult.passes ? '✓ PASS' : '✗ FAIL', `(ratio: ${tertiaryResult.ratio.toFixed(2)}:1)`); +console.log('- Muted:', mutedResult.passes ? '✓ PASS' : '✗ FAIL', `(ratio: ${mutedResult.ratio.toFixed(2)}:1)`); + +if (!primaryResult.passes || !tertiaryResult.passes || !mutedResult.passes) { + console.log('\n⚠️ Some tests failed - need to optimize CSS variables'); + process.exit(1); +} + +console.log('\n✅ All tests passed!'); diff --git a/scripts/verify-color-contrast.js b/scripts/verify-color-contrast.js new file mode 100644 index 0000000..29d34bc --- /dev/null +++ b/scripts/verify-color-contrast.js @@ -0,0 +1,15 @@ +const { calculateContrastRatio, meetsWCAGStandard } = require('../src/lib/color-contrast.ts'); + +console.log('Testing color contrast functions...'); + +const ratio = calculateContrastRatio('#000000', '#FFFFFF'); +console.log('Black on white ratio:', ratio); +console.log('Expected: ~21, Actual:', ratio); + +const result = meetsWCAGStandard('#000000', '#FFFFFF', 'AA', 'normal'); +console.log('WCAG AA compliance:', result); + +const lowContrastResult = meetsWCAGStandard('#808080', '#FFFFFF', 'AA', 'normal'); +console.log('Low contrast test:', lowContrastResult); + +console.log('All tests completed!'); diff --git a/src/app/(marketing)/about/client.tsx b/src/app/(marketing)/about/client.tsx index 6e7b97e..b6dd273 100644 --- a/src/app/(marketing)/about/client.tsx +++ b/src/app/(marketing)/about/client.tsx @@ -223,7 +223,7 @@ export function AboutClient() { {milestone.date}
-

{milestone.title}

+

{milestone.title}

{milestone.description}

diff --git a/src/app/(marketing)/contact/page.tsx b/src/app/(marketing)/contact/page.tsx index 93854d8..8dc6fdd 100644 --- a/src/app/(marketing)/contact/page.tsx +++ b/src/app/(marketing)/contact/page.tsx @@ -183,7 +183,7 @@ export default function ContactPage() { `} >
-

联系方式

+

联系方式

@@ -222,7 +222,7 @@ export default function ContactPage() {
-

工作时间

+

工作时间

@@ -235,7 +235,7 @@ export default function ContactPage() {
-

我们的承诺

+

我们的承诺

@@ -262,7 +262,7 @@ export default function ContactPage() { `} >
-

发送消息

+

发送消息

{isSubmitted ? (
diff --git a/src/app/globals.css b/src/app/globals.css index 1b80da7..aadb46d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -31,7 +31,7 @@ --color-brand-primary-bg: #FEF2F4; /* 背景色系 - 宣纸白 */ - --color-bg-primary: #FAFAFA; + --color-bg-primary: #FFFFFF; --color-bg-secondary: #FFFBF5; --color-bg-tertiary: #F5F5F5; --color-bg-hover: #EFEFEF; @@ -39,8 +39,8 @@ /* 文字色系 - 墨色层次 */ --color-text-primary: #1C1C1C; --color-text-secondary: #3D3D3D; - --color-text-tertiary: #5C5C5C; - --color-text-muted: #8C8C8C; + --color-text-tertiary: #4A4A4A; + --color-text-muted: #6B6B6B; /* 边框色系 */ --color-border-primary: #E5E5E5; diff --git a/src/lib/color-contrast.ts b/src/lib/color-contrast.ts new file mode 100644 index 0000000..bd27419 --- /dev/null +++ b/src/lib/color-contrast.ts @@ -0,0 +1,63 @@ +interface ColorRGB { + r: number; + g: number; + b: number; +} + +interface ContrastResult { + passes: boolean; + ratio: number; + requiredRatio: number; +} + +function hexToRgb(hex: string): ColorRGB { + const cleanHex = hex.replace('#', ''); + const r = parseInt(cleanHex.substring(0, 2), 16); + const g = parseInt(cleanHex.substring(2, 4), 16); + const b = parseInt(cleanHex.substring(4, 6), 16); + return { r, g, b }; +} + +function getLuminance(rgb: ColorRGB): number { + const { r, g, b } = rgb; + const a = [r, g, b].map(v => { + v /= 255; + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }); + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; +} + +export function calculateContrastRatio(foreground: string, background: string): number { + const fgRgb = hexToRgb(foreground); + const bgRgb = hexToRgb(background); + + const fgLuminance = getLuminance(fgRgb); + const bgLuminance = getLuminance(bgRgb); + + const lighter = Math.max(fgLuminance, bgLuminance); + const darker = Math.min(fgLuminance, bgLuminance); + + return (lighter + 0.05) / (darker + 0.05); +} + +export function meetsWCAGStandard( + foreground: string, + background: string, + level: 'AA' | 'AAA', + textSize: 'normal' | 'large' +): ContrastResult { + const ratio = calculateContrastRatio(foreground, background); + + let requiredRatio: number; + if (level === 'AA') { + requiredRatio = textSize === 'normal' ? 4.5 : 3; + } else { + requiredRatio = textSize === 'normal' ? 7 : 4.5; + } + + return { + passes: ratio >= requiredRatio, + ratio, + requiredRatio + }; +} diff --git a/test-results/.gitkeep b/test-results/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test-results/accessibility/.gitkeep b/test-results/accessibility/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test-results/performance/.gitkeep b/test-results/performance/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/lib/color-contrast.spec.ts b/tests/lib/color-contrast.spec.ts new file mode 100644 index 0000000..738a5f3 --- /dev/null +++ b/tests/lib/color-contrast.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@playwright/test'; +import { calculateContrastRatio, meetsWCAGStandard } from '@/lib/color-contrast'; + +test('should calculate correct contrast ratio for black on white', () => { + const ratio = calculateContrastRatio('#000000', '#FFFFFF'); + expect(ratio).toBeCloseTo(21, 1); +}); + +test('should identify WCAG AA compliance for normal text', () => { + const result = meetsWCAGStandard('#000000', '#FFFFFF', 'AA', 'normal'); + expect(result.passes).toBe(true); + expect(result.ratio).toBeGreaterThan(4.5); +}); + +test('should fail WCAG AA for low contrast colors', () => { + const result = meetsWCAGStandard('#808080', '#FFFFFF', 'AA', 'normal'); + expect(result.passes).toBe(false); + expect(result.ratio).toBeLessThan(4.5); +}); diff --git a/tests/styles/color-contrast.spec.ts b/tests/styles/color-contrast.spec.ts new file mode 100644 index 0000000..79c7dca --- /dev/null +++ b/tests/styles/color-contrast.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from '@playwright/test'; +import { calculateContrastRatio, meetsWCAGStandard } from '@/lib/color-contrast'; + +test('primary text on primary background should meet WCAG AA', () => { + const result = meetsWCAGStandard('#1C1C1C', '#FAFAFA', 'AA', 'normal'); + expect(result.passes).toBe(true); +}); + +test('tertiary text on primary background should meet WCAG AA', () => { + const result = meetsWCAGStandard('#5C5C5C', '#FAFAFA', 'AA', 'normal'); + expect(result.passes).toBe(true); +}); + +test('muted text on primary background should meet WCAG AA', () => { + const result = meetsWCAGStandard('#8C8C8C', '#FAFAFA', 'AA', 'normal'); + expect(result.passes).toBe(true); +});