Files
test-repo/docs/.vitepress/theme/components/appendix/browser-frontend/A11yScreenReaderDemo.vue
T
sanbuphy 260d17ee8b feat: 添加多个附录交互式组件和文档更新
- 添加浏览器前端组件:无障碍访问、国际化、实时通信
- 添加 Transformer 注意力机制系列组件
- 更新 Canvas、数据追踪等现有组件
- 修复 ESLint 变量名冲突问题
- 完善相关附录文档
2026-02-24 08:34:53 +08:00

247 lines
5.5 KiB
Vue
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.
<template>
<div class="demo-wrapper">
<div class="demo-header">Accessibility (a11y) / 读屏机眼里的你</div>
<div class="split-pane">
<!-- 糟糕的做法 -->
<div class="pane bad-pane">
<h4 class="pane-title label-bad"> 野路子开发全是 DIV</h4>
<div class="component-card">
<!-- 这里全是用 div 伪造的组件 -->
<div
class="fake-btn"
@mouseenter="speakBad('提交')"
@mouseleave="stopSpeak"
@keydown.enter="showError"
>
提交
</div>
<div
class="fake-icon"
@mouseenter="speakBad('叉叉图')"
@mouseleave="stopSpeak"
>
</div>
</div>
<div class="reader-box">
<div class="reader-header">🎧 读屏机播报内容</div>
<div class="reader-text" :class="{ empty: !currentBadSpeech }">
{{ currentBadSpeech || '(只有字面,不知用途,键盘 Enter 无效)' }}
</div>
</div>
</div>
<!-- 优秀做法 -->
<div class="pane good-pane">
<h4 class="pane-title label-good"> 专业前端语义化 + ARIA</h4>
<div class="component-card">
<!-- 使用真正的按钮和 ARIA -->
<button
class="real-btn"
@mouseenter="speakGood('提交按钮。按下以发送表单。')"
@mouseleave="stopSpeak"
@click="triggerAction"
>
提交
</button>
<button
class="real-icon-btn"
aria-label="关闭窗口"
@mouseenter="speakGood('关闭窗口,按钮。')"
@mouseleave="stopSpeak"
>
<span aria-hidden="true"></span>
</button>
</div>
<div class="reader-box">
<div class="reader-header">🎧 读屏机播报内容</div>
<div class="reader-text active">
{{ currentGoodSpeech || '(悬停查看播报,支持 Tab 和 Enter 交互)' }}
</div>
</div>
</div>
</div>
<div class="status-msg">
💡 提示将鼠标悬停在上方按钮上模拟视障用户读屏机听到的内容<br/>
可以尝试用键盘 Tab 键选中并按 Enter只有右侧的按钮会响应
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentBadSpeech = ref('')
const currentGoodSpeech = ref('')
const speakBad = (text) => {
currentBadSpeech.value = `文本:"${text}"`
}
const speakGood = (text) => {
currentGoodSpeech.value = `🗣️ ${text}`
}
const stopSpeak = () => {
currentBadSpeech.value = ''
currentGoodSpeech.value = ''
}
const showError = () => {
alert('假按钮的 @keydown.enter 事件不会天生自带!')
}
const triggerAction = () => {
alert('真按钮成功触发!')
}
</script>
<style scoped>
.demo-wrapper {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
.demo-header {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
.split-pane {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
.pane {
flex: 1;
min-width: 250px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
}
.pane-title {
font-size: 0.9rem;
font-weight: bold;
margin-bottom: 1rem;
text-align: center;
}
.label-bad { color: var(--vp-c-danger, #e74c3c); }
.label-good { color: var(--vp-c-brand-1, #10b981); }
.component-card {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
flex-grow: 1;
padding: 2rem 0;
}
/* 假按钮完全不响应 tab 且无默认高亮样式 */
.fake-btn, .real-btn {
padding: 0.6rem 1.2rem;
border-radius: 4px;
font-weight: bold;
text-align: center;
user-select: none;
}
.fake-btn {
background: #e2e8f0;
color: #475569;
cursor: pointer;
/* 缺少 focus 可见轮廓 */
outline: none;
}
.real-btn {
background: var(--vp-c-brand);
color: white;
border: none;
cursor: pointer;
}
.real-btn:focus-visible {
outline: 3px solid var(--vp-c-brand-soft);
outline-offset: 2px;
}
.fake-icon, .real-icon-btn {
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 1.2rem;
}
.fake-icon {
background: #f1f5f9;
cursor: pointer;
}
.real-icon-btn {
background: #f1f5f9;
border: none;
cursor: pointer;
color: var(--vp-c-text-1);
}
.real-icon-btn:focus-visible {
outline: 3px solid var(--vp-c-brand-soft);
}
.reader-box {
background: #1e293b;
border-radius: 6px;
padding: 0.8rem;
margin-top: auto;
min-height: 80px;
display: flex;
flex-direction: column;
}
.reader-header {
font-size: 0.75rem;
color: #94a3b8;
margin-bottom: 0.4rem;
}
.reader-text {
color: white;
font-family: monospace;
font-size: 0.85rem;
font-weight: bold;
line-height: 1.4;
}
.reader-text.empty {
color: #64748b;
font-weight: normal;
}
.reader-text.active {
color: #34d399; /* emerald-400 */
}
.status-msg {
margin-top: 1rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
background: var(--vp-c-bg);
padding: 0.8rem;
border-radius: 6px;
border-left: 4px solid var(--vp-c-brand);
}
</style>