# 023-accessibility.mdc (Deep Reference) > 该文件为原始详细规范归档,供 Tier 3 按需读取。 --- # ♿ Accessibility (A11y) — WCAG AA Standards > **⚠️ 双前端区分**:本文件中的 `el-*` 组件示例**仅适用于管理端** (`Case-Database-Frontend-admin/`)。 > 用户端 (`Case-Database-Frontend-user/`) 使用 Headless UI + Tailwind CSS 实现同等无障碍标准,**禁止引入 Element Plus**。 > Headless UI 组件天然内置 ARIA 属性,用户端可直接使用。 ## WCAG AA 基线要求 | 原则 | 要求 | 检测方法 | |------|------|---------| | **可感知** | 图片有 alt;颜色对比度 ≥ 4.5:1 (文本) / 3:1 (大文本) | Axe DevTools / Lighthouse | | **可操作** | 所有功能可键盘访问;无键盘陷阱;跳转链接 | Tab 键手动测试 | | **可理解** | 语言声明;错误提示清晰;一致导航 | 屏幕阅读器测试 | | **健壮性** | 语义化 HTML;ARIA 正确使用 | W3C Validator | --- ## 语义化 HTML(Vue 3) ```vue 跳转到主要内容 首页 订单 ``` --- ## 键盘导航规范 ### 焦点管理 ```typescript // src/composables/useFocusTrap.ts // 对话框焦点捕获:阻止 Tab 跳出对话框 export function useFocusTrap(containerRef: Ref) { function trapFocus(event: KeyboardEvent): void { if (!containerRef.value || event.key !== 'Tab') return const focusable = containerRef.value.querySelectorAll( 'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])' ) const first = focusable[0] const last = focusable[focusable.length - 1] if (event.shiftKey) { if (document.activeElement === first) { last.focus() event.preventDefault() } } else { if (document.activeElement === last) { first.focus() event.preventDefault() } } } onMounted(() => document.addEventListener('keydown', trapFocus)) onUnmounted(() => document.removeEventListener('keydown', trapFocus)) } ``` ```typescript // src/composables/useFocusReturn.ts // 对话框关闭后,焦点回到触发元素 export function useFocusReturn() { const triggerEl = ref(null) function saveTrigger(): void { triggerEl.value = document.activeElement as HTMLElement } function restoreFocus(): void { nextTick(() => triggerEl.value?.focus()) } return { saveTrigger, restoreFocus } } ``` ### 键盘快捷键 ```typescript // src/composables/useKeyboard.ts export function useKeyboard(handlers: Record void>) { function handleKeydown(event: KeyboardEvent): void { const key = [ event.ctrlKey && 'Ctrl', event.altKey && 'Alt', event.shiftKey && 'Shift', event.key, ] .filter(Boolean) .join('+') handlers[key]?.() } onMounted(() => document.addEventListener('keydown', handleKeydown)) onUnmounted(() => document.removeEventListener('keydown', handleKeydown)) } // 使用示例 useKeyboard({ 'Escape': () => closeDialog(), 'Ctrl+s': () => saveForm(), 'Ctrl+/': () => toggleHelp(), }) ``` --- ## ARIA 模式库(Vue 3 组件) ### 模态框 ```vue {{ title }} {{ description }} ``` ### 加载状态 ```vue {{ isLoading ? '提交中...' : '提交' }} {{ isLoading ? '正在加载数据,请稍候' : '' }} ``` ### 表单无障碍 ```vue 3-20 位字母、数字或下划线 {{ formErrors.username }} ``` ### 数据表格 ```vue 名称 编辑 删除 ``` ### 图标按钮(必须有 aria-label) ```vue ``` --- ## 颜色对比度 ```scss // 确保颜色对比度 ≥ 4.5:1 // 工具:https://webaim.org/resources/contrastchecker/ // ✅ 正文文字 .text-primary { color: #1d4ed8; } // 在白底: 7.5:1 ✓ .text-secondary { color: #374151; } // 在白底: 9.4:1 ✓ // ⚠️ 灰色文字需谨慎 .text-muted { color: #6b7280; } // 在白底: 4.6:1 ✓(刚好过 AA) // ❌ 禁止使用 .text-too-light { color: #9ca3af; } // 在白底: 2.8:1 ✗(不过 AA) // 状态颜色需同时用颜色 + 图标/文字(不只靠颜色区分) .status-success { color: #15803d; // ✅ 同时用图标辅助: } ``` --- ## 动效规范(减少动效) ```css /* 尊重用户减少动效偏好 */ @media (prefers-reduced-motion: reduce) { *, ::before, ::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } ``` ```typescript // src/composables/useReducedMotion.ts export function useReducedMotion() { const preferReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)') return computed(() => preferReducedMotion.matches) } ``` --- ## 自动化测试 ```typescript // tests/a11y.spec.ts — 使用 axe-playwright import { checkA11y, injectAxe } from 'axe-playwright' test('dashboard page has no accessibility violations', async ({ page }) => { await page.goto('/dashboard') await injectAxe(page) await checkA11y(page, undefined, { axeOptions: { runOnly: ['wcag2a', 'wcag2aa'] }, }) }) ``` ```bash # 安装 axe 测试依赖 npm install -D axe-playwright @axe-core/playwright ``` --- ## 规则 - 所有图片必须有 `alt`(装饰性图片用 `alt=""`) - 所有图标按钮必须有 `aria-label`,图标本身加 `aria-hidden="true"` - 所有表单字段必须有关联的 `` 或 `aria-label` - 错误提示必须使用 `role="alert"` 或 `aria-live="polite"` - 对话框必须有焦点陷阱(focus trap)和 ESC 关闭支持 - 不能只用颜色区分状态(必须加文字或图标) - 自动化测试须包含 axe-playwright a11y 扫描 - 产品上线前须通过 Lighthouse Accessibility 评分 ≥ 90
{{ description }}
3-20 位字母、数字或下划线
{{ formErrors.username }}