feat: Enhance SearchLoadingAnimation with pause functionality and progress indicators; update Badge component to support icons with customizable positions

This commit is contained in:
kuekhaoyang
2025-11-18 16:02:50 +08:00
parent e007899f9e
commit 73eeea7161
3 changed files with 89 additions and 927 deletions
-912
View File
@@ -1,912 +0,0 @@
# KVideo UI 审计报告 (UI Audit Report)
## 基于 Liquid Glass 设计系统的全面评估
**审计日期**: 2025-11-18
**项目**: KVideo - 视频聚合平台
**设计系统**: Liquid Glass Design System
---
## 📊 执行摘要 (Executive Summary)
KVideo 项目展现了**优秀的 Liquid Glass 设计系统实现**,核心视觉语言高度一致,组件架构清晰模块化。项目在玻璃态射效果、圆角规范、动画流畅度方面表现出色,已达到 **85% 的设计系统合规度**
**优点**:
- ✅ 完整的 CSS 变量系统,主题切换流畅
- ✅ 核心组件严格遵循 `rounded-2xl` / `rounded-full` 规范
- ✅ 毛玻璃效果 (`backdrop-filter`) 实现精准
- ✅ 响应式设计细致,移动端适配优秀
- ✅ 流体动画系统完整,物理感强
**待改进**:
- ⚠️ 部分组件缺少 ARIA 属性和键盘导航
- ⚠️ 部分圆角使用不一致(混用 Tailwind 原生类)
- ⚠️ 色彩对比度需验证 WCAG 2.2 AA 标准
- ⚠️ 缺少 focus-visible 状态样式
- ⚠️ 部分组件超过 150 行限制
---
## 🎨 设计系统合规性分析
### 1. **玻璃态射效果 (Glass Effect) - 95% 合规**
#### ✅ 优秀实践
```css
/* globals.css - 完美的玻璃态射基础 */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(25px) saturate(180%);
-webkit-backdrop-filter: blur(25px) saturate(180%);
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-md);
border: 1px solid var(--glass-border);
}
```
**分析**:
- 完美实现了毛玻璃效果的三大核心:`backdrop-filter``saturate`、半透明背景
- 提供了 `-webkit-` 前缀以支持 Safari
- 正确使用 CSS 变量确保主题一致性
#### ⚠️ 需改进的地方
**位置**: `components/ui/Card.tsx` - Line 17-18
```tsx
// 当前实现
[-webkit-backdrop-filter:blur(25px)_saturate(180%)]
// 问题: Tailwind 4.0 语法需要验证,建议使用 CSS 类
```
**建议**: 在 `globals.css` 中定义专用类,避免内联样式的可维护性问题
---
### 2. **圆角规范 (Border Radius) - 80% 合规**
#### ✅ 完全符合规范的组件
1. **Button** (`components/ui/Button.tsx`): `rounded-[var(--radius-2xl)]`
2. **Badge** (`components/ui/Badge.tsx`): `rounded-[var(--radius-full)]`
3. **Card** (`components/ui/Card.tsx`): `rounded-[var(--radius-2xl)]`
4. **ThemeSwitcher**: 外层 `rounded-full`,按钮 `rounded-full`
5. **Input**: `rounded-[var(--radius-2xl)]`
#### ⚠️ 不一致的使用
**位置**: `components/search/VideoGrid.tsx` - Line 73
```tsx
// 混用 Tailwind 原生类和 CSS 变量
style={{ borderRadius: 'var(--radius-2xl)' }}
// vs
className="rounded-[var(--radius-2xl)]"
```
**问题**: 同一组件内同时使用 `style``className` 设置圆角,不一致
**建议**: 统一使用 `className` 方式或全部使用 `style`
---
### 3. **色彩系统与对比度 (Color System & Contrast) - 75% 合规**
#### ✅ 优秀实践
```css
/* globals.css - 完整的亮/暗色变量系统 */
:root {
--text-color-light: #1d1d1f; /* 深色文字 */
--text-color-dark: #f5f5f7; /* 浅色文字 */
--accent-color-light: #007aff; /* iOS 蓝 */
--accent-color-dark: #0a84ff; /* 更亮的蓝 */
}
```
#### ⚠️ 对比度验证缺失
**问题**: 未找到明确的 WCAG 2.2 对比度测试文档或注释
**必须验证的组件**:
1. `Badge` - `text-white` on `--accent-color` (需达到 4.5:1)
2. `Button.primary` - `text-white` on `--accent-color`
3. `SearchHistoryDropdown` - `text-[var(--text-color-secondary)]` on `--glass-bg`
4. `TypeBadges` - 选中态文字与背景对比度
**建议**:
```bash
# 使用工具验证
npm install --save-dev @a11y/color-contrast-checker
```
---
### 4. **动画系统 (Animation System) - 90% 合规**
#### ✅ 优秀实践
```css
/* globals.css - 完整的物理感动画库 */
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
--transition-fluid: 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
```
**分析**:
- 使用 `cubic-bezier(0.2, 0.8, 0.2, 1)` 实现自然加速/减速
- 动画命名清晰(`fade-in`, `slide-up`, `spin-slow`
- 提供了 `.animate-*` 工具类
#### ⚠️ 性能优化建议
**位置**: `components/search/VideoGrid.tsx` - Line 79-80
```tsx
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
```
**问题**: 图片缩放动画未使用 GPU 加速
**建议**:
```tsx
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500 will-change-transform"
```
---
## 🧩 组件审计详情
### A. 核心 UI 组件 (`components/ui/`)
#### 1. **Button Component** ✅ 优秀
**文件**: `components/ui/Button.tsx`
**行数**: 48 行 (符合 <150 行规范)
**优点**:
- 严格使用 `rounded-[var(--radius-2xl)]`
- 完整的 hover/active 状态
- 提供 `primary``secondary` 变体
**待改进**:
```tsx
// 缺少 disabled 状态的 aria-disabled 属性
<button
disabled={props.disabled}
aria-disabled={props.disabled} // ❌ 缺失
```
#### 2. **Card Component** ✅ 优秀
**文件**: `components/ui/Card.tsx`
**行数**: 39 行
**优点**:
- 完整的玻璃态射效果
- 可选的 hover 状态
- 支持 onClick 交互
**待改进**:
```tsx
// 当 onClick 存在时,应该是语义化的 <button>
// 当前使用 <div> + onClick 不符合可访问性标准
return (
<div onClick={onClick}> {/* ❌ 应该是 <button> */}
```
#### 3. **Input Component** ✅ 优秀
**文件**: `components/ui/Input.tsx`
**行数**: 49 行
**优点**:
- 使用 `forwardRef` 支持 ref 传递
- 完整的 error 状态处理
- label 绑定规范
**待改进**:
```tsx
// 缺少 focus-visible 样式
focus:outline-none
focus:border-[var(--accent-color)]
// 应该添加
focus-visible:ring-2
focus-visible:ring-[var(--accent-color)]
focus-visible:ring-offset-2
```
#### 4. **Badge Component** ✅ 完美
**文件**: `components/ui/Badge.tsx`
**行数**: 27 行
**优点**:
- 严格使用 `rounded-[var(--radius-full)]`
- 完整的 primary/secondary 变体
- 响应式字体大小
**无需改进**
---
### B. 搜索组件 (`components/search/`)
#### 1. **SearchForm** ⚠️ 需优化
**文件**: `components/search/SearchForm.tsx`
**行数**: 122 行
**问题 1**: 清除按钮缺少键盘访问
```tsx
// Line 89-96
<button
type="button"
onClick={handleClear}
// ❌ 缺少键盘事件处理
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClear();
}
}}
```
**问题 2**: 搜索历史下拉框需要 ARIA 属性
```tsx
// 当前的 Input 组件
<Input
ref={inputRef}
// ❌ 缺失
aria-autocomplete="list"
aria-expanded={showHistory}
aria-controls="search-history-listbox"
role="combobox"
/>
```
#### 2. **VideoGrid** ⚠️ 需优化
**文件**: `components/search/VideoGrid.tsx`
**行数**: 146 行
**问题 1**: 移动端交互逻辑复杂,可访问性差
```tsx
// Line 40-52 - 双击逻辑对键盘用户不友好
const handleCardClick = (e: React.MouseEvent, videoId: string, videoUrl: string) => {
const isMobile = window.innerWidth < 1024;
if (isMobile) {
if (activeCardId === videoId) {
window.location.href = videoUrl;
} else {
e.preventDefault();
setActiveCardId(videoId);
}
}
};
```
**建议**: 使用 `<button>` 触发详情展开,`<Link>` 用于导航,分离关注点
**问题 2**: 图片缺少加载失败处理
```tsx
// Line 73-79
<img
src={video.vod_pic}
alt={video.vod_name}
// ❌ 应添加
onError={(e) => {
e.currentTarget.src = '/placeholder-image.png';
}}
/>
```
#### 3. **TypeBadges** ⚠️ 需优化
**文件**: `components/search/TypeBadges.tsx`
**行数**: 162 行 (超过 150 行限制)
**问题**: 文件过长,违反单一职责原则
**建议**: 拆分为两个文件
```
TypeBadges.tsx (主组件,60 行)
TypeBadgeItem.tsx (单个徽章,40 行)
TypeBadgeList.tsx (徽章列表容器,50 行)
```
---
### C. 播放器组件 (`components/player/`)
#### 1. **VideoPlayer** ⚠️ 需优化
**文件**: `components/player/VideoPlayer.tsx`
**行数**: 117 行
**问题 1**: 错误状态缺少 ARIA live region
```tsx
// Line 75-91 - 错误提示
<div className="text-center text-white max-w-md px-4">
{/* ❌ 应添加 */}
<div role="alert" aria-live="assertive">
<Icons.AlertTriangle size={48} />
<p className="text-lg font-semibold mb-2"></p>
<p className="text-sm text-gray-300 mb-4">{videoError}</p>
</div>
</div>
```
**问题 2**: 返回按钮应该使用 `<Link>` 而不是 `onClick`
```tsx
<Button
variant="secondary"
onClick={onBack} // ❌ 非 SPA 友好
// 应该
as={Link}
href={previousUrl}
/>
```
#### 2. **EpisodeList** ✅ 优秀
**文件**: `components/player/EpisodeList.tsx`
**行数**: 65 行
**优点**:
- 当前播放集数高亮明确
- 使用语义化 `<button>` 元素
- 空状态处理完善
**待改进**: 添加键盘导航
```tsx
// 添加 arrow key 支持
<div
role="list"
onKeyDown={(e) => {
if (e.key === 'ArrowUp') {
// 聚焦上一集
} else if (e.key === 'ArrowDown') {
// 聚焦下一集
}
}}
>
```
---
### D. 历史记录组件
#### **WatchHistorySidebar** ⚠️ 需重大优化
**文件**: `components/history/WatchHistorySidebar.tsx`
**行数**: 215 行 (违反 150 行规范)
**问题 1**: 文件过长,需拆分
```
WatchHistorySidebar.tsx (主组件 + 布局,80 行)
HistoryItem.tsx (单个历史条目,60 行)
HistoryEmptyState.tsx (空状态,30 行)
```
**问题 2**: 侧边栏缺少焦点管理
```tsx
// Line 84-91 - Sidebar
<aside
className={...}
// ❌ 缺失
role="complementary"
aria-label="观看历史侧边栏"
aria-hidden={!isOpen}
tabIndex={isOpen ? 0 : -1}
>
```
**问题 3**: 删除按钮需要确认对话框
```tsx
// Line 188-196 - Delete button
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
// ❌ 直接删除,应该先确认
if (window.confirm('确定要删除这条历史记录吗?')) {
removeFromHistory(item.videoId, item.source);
}
}}
```
---
## ♿ 可访问性 (Accessibility) 审计
### 严重问题 (Critical Issues)
#### 1. **键盘导航缺失** 🔴 高优先级
**影响组件**:
- `VideoGrid` (卡片点击)
- `TypeBadges` (类型选择)
- `SearchHistoryDropdown` (历史选择)
**问题**: 用户无法仅使用键盘操作这些交互元素
**解决方案**:
```tsx
// VideoGrid.tsx - 添加键盘支持
<Link
href={videoUrl}
onKeyDown={(e) => {
if (e.key === 'Enter') {
// 导航到视频页
}
}}
role="button"
tabIndex={0}
>
```
#### 2. **ARIA 属性不完整** 🔴 高优先级
**影响组件**:
- `WatchHistorySidebar` (侧边栏角色)
- `SearchForm` (combobox 属性)
- `VideoPlayer` (错误提示)
**问题**: 屏幕阅读器无法正确理解组件功能
**解决方案**:
```tsx
// WatchHistorySidebar.tsx
<aside
role="complementary"
aria-labelledby="history-sidebar-title"
aria-modal="false" // 非模态侧边栏
>
<h2 id="history-sidebar-title"></h2>
```
#### 3. **对比度未验证** 🟡 中优先级
**需测试的元素**:
```
1. Badge (primary): #007aff 背景 + white 文字
2. Button (disabled): opacity-50 状态
3. SearchLoadingAnimation: 进度条色彩
4. TypeBadges (selected): 选中态对比度
```
**工具**: 使用 [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
#### 4. **focus-visible 样式缺失** 🟡 中优先级
**问题**: 所有交互元素仅使用 `focus:outline-none`,键盘用户无法看到焦点
**解决方案** (全局添加):
```css
/* globals.css */
*:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
border-radius: var(--radius-2xl);
}
button:focus-visible,
a:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}
```
---
## 📱 响应式设计审计
### 优点 ✅
1. **完整的断点系统**: `sm:` / `md:` / `lg:` / `xl:` / `2xl:` 使用规范
2. **移动端适配优秀**: 触摸区域最小 44x44px (`min-h-[44px]` in Button)
3. **字体大小响应式**: `text-sm md:text-base` 模式一致
### 待改进 ⚠️
#### 1. **VideoGrid 列数在超大屏幕过多**
```tsx
// 当前: 2xl:grid-cols-7 (7 列)
// 建议: 2xl:grid-cols-6 max-w-[1920px] mx-auto
```
#### 2. **SearchForm 在小屏幕输入框被挤压**
```tsx
// Line 83-85
className="text-lg pr-24 md:pr-32 truncate"
// 建议: text-base sm:text-lg
```
#### 3. **WatchHistorySidebar 宽度在小屏过宽**
```tsx
// Line 84
w-[90%] max-w-[420px]
// 在 iPhone SE (375px) 上 = 337.5px,接近全屏
// 建议: w-[85%] sm:w-[90%]
```
---
## 🏗️ 架构建议
### 1. **文件行数超标** 🔴 必须修复
**超过 150 行的文件**:
- `TypeBadges.tsx` (162 行) → 拆分为 3 个文件
- `WatchHistorySidebar.tsx` (215 行) → 拆分为 3 个文件
- `SearchForm.tsx` (122 行) → 接近限制,可考虑提取 hooks
- `VideoGrid.tsx` (146 行) → 接近限制,可提取 `VideoCard.tsx`
- `PopularFeatures.tsx` (321 行) → 严重超标,拆分为 5 个文件
### 2. **创建 `hooks/` 目录** 🟡 建议
**提取自定义逻辑**:
```
lib/hooks/
useKeyboardNavigation.ts (从 VideoGrid 提取)
useFocusTrap.ts (从 WatchHistorySidebar 提取)
useMediaQuery.ts (从 TypeBadges 提取)
useClickOutside.ts (从 SearchHistoryDropdown 提取)
```
### 3. **创建 `accessibility/` 工具库** 🟡 建议
```tsx
// lib/accessibility/focus-management.ts
export const trapFocus = (container: HTMLElement) => { ... }
export const restoreFocus = (element: HTMLElement) => { ... }
// lib/accessibility/aria-announcer.ts
export const announceToScreenReader = (message: string) => {
const announcer = document.getElementById('aria-live-announcer');
if (announcer) announcer.textContent = message;
}
```
---
## 🎯 TODO 清单 (优先级排序)
### 🔴 Critical (必须立即修复)
#### 1. **修复文件行数超标** (预计 4 小时)
- [✅] 拆分 `PopularFeatures.tsx` (321 行 → 拆分为 5 个文件)
- [✅] `PopularFeatures.tsx` (主组件,80 行)
- [✅] `MovieGrid.tsx` (电影网格,60 行)
- [✅] `TagManager.tsx` (标签管理,50 行)
- [✅] `MovieCard.tsx` (单个卡片,40 行)
- [✅] `InfiniteScroll.tsx` (滚动加载,40 行)
- [✅] 拆分 `WatchHistorySidebar.tsx` (215 行 → 拆分为 3 个文件)
- [✅] `WatchHistorySidebar.tsx` (主组件 + 布局,80 行)
- [✅] `HistoryItem.tsx` (单个历史条目,60 行)
- [✅] `HistoryEmptyState.tsx` (空状态,30 行)
- [✅] 拆分 `TypeBadges.tsx` (162 行 → 拆分为 3 个文件)
- [✅] `TypeBadges.tsx` (主组件,60 行)
- [✅] `TypeBadgeItem.tsx` (单个徽章,40 行)
- [✅] `TypeBadgeList.tsx` (徽章列表容器,50 行)
#### 2. **添加全局 focus-visible 样式** (预计 30 分钟)
- [✅] 在 `globals.css` 中添加全局焦点样式
```css
*:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}
```
- [✅] 移除所有组件的 `focus:outline-none`(仅保留必要的)
- [✅] 测试所有交互元素的键盘可见性
#### 3. **修复 Card 组件的语义化问题** (预计 1 小时)
- [✅] 当 `onClick` 存在时,将 `<div>` 改为 `<button>`
- [✅] 为 `button` 类型的 Card 添加 `type="button"`
- [✅] 更新所有使用 Card 的地方,确保样式一致
#### 4. **WCAG 对比度验证** (预计 2 小时)
- [✅] 安装 `@a11y/color-contrast-checker`
- [✅] 测试所有文字与背景的对比度
- [✅] Badge (primary & secondary)
- [✅] Button (primary, secondary, disabled)
- [✅] SearchLoadingAnimation 进度条
- [✅] TypeBadges 选中态
- [✅] 创建 `CONTRAST_TEST_RESULTS.md` 文档
- [✅] 调整不符合标准的色彩值
---
### 🟡 High Priority (高优先级,2 周内完成)
#### 5. **完善 ARIA 属性** (预计 3 小时)
- [✅] **SearchForm.tsx**
- [✅] 添加 `role="combobox"`
- [✅] 添加 `aria-expanded={showHistory}`
- [✅] 添加 `aria-controls="search-history-listbox"`
- [✅] 添加 `aria-autocomplete="list"`
- [✅] **WatchHistorySidebar.tsx**
- [✅] 添加 `role="complementary"`
- [✅] 添加 `aria-labelledby="history-sidebar-title"`
- [✅] 添加 `aria-hidden={!isOpen}`
- [✅] 实现焦点陷阱 (focus trap)
- [✅] **VideoPlayer.tsx**
- [✅] 错误提示添加 `role="alert"`
- [✅] 添加 `aria-live="assertive"`
- [✅] **VideoGrid.tsx**
- [✅] 添加 `role="list"` 到网格容器
- [✅] 添加 `role="listitem"` 到每个卡片
#### 6. **添加键盘导航支持** (预计 4 小时) ✅ **已完成**
- [x] **VideoGrid.tsx**
- [x] 添加 `onKeyDown` 处理 Enter/Space 键
- [x] 实现方向键导航(上下左右)
- [x] 添加 `tabIndex={0}` 到每个卡片
- [x] 添加 `aria-label` 描述性标签
- [x] 添加视觉焦点指示器 (ring)
- [x] **TypeBadges.tsx**
- [x] 添加 `onKeyDown` 处理 Enter/Space 键
- [x] 实现方向键在徽章间切换
- [x] 添加 `role="group"` 和 `aria-label="类型筛选"`
- [x] 添加 `aria-pressed` 状态
- [x] 添加视觉焦点指示器
- [x] 支持移动端滚动到视图
- [x] **SearchHistoryDropdown.tsx**
- [x] 添加方向键上下选择
- [x] 添加 Escape 键关闭下拉框
- [x] 添加 Home/End 键跳转首尾
- [x] 添加 `aria-selected` 状态
- [x] 添加视觉焦点指示器
- [x] 支持 Enter/Space 键选择
- [x] **EpisodeList.tsx**
- [x] 添加方向键上下切换集数
- [x] 添加 `role="radiogroup"`
- [x] 当前集数添加 `aria-current="true"`
- [x] 添加 `role="radio"` 到每个按钮
- [x] 添加 `aria-checked` 状态
- [x] 添加 `focus-visible` 样式
- [x] 支持自动滚动到焦点项
**实现细节**:
- ✅ 创建了 `useKeyboardNavigation` 自定义 Hook,统一管理键盘导航逻辑
- ✅ 支持三种导航模式:`horizontal`(水平)、`vertical`(垂直)、`grid`(网格)
- ✅ 网格导航自动检测列数,支持响应式布局
- ✅ 所有交互元素均添加 `tabIndex={0}` 支持键盘聚焦
- ✅ 使用 `focus-visible:ring-2` 提供清晰的视觉焦点反馈
- ✅ 完整的 ARIA 属性支持,符合 WAI-ARIA 标准
- ✅ 焦点项自动滚动到视图内,优化用户体验
#### 7. **图片加载优化** (预计 2 小时)
- [x] 创建 `public/placeholder-poster.svg` 占位图
- [x] **VideoGrid.tsx** - 添加 `onError` 处理
```tsx
<img
src={video.vod_pic}
onError={(e) => {
e.currentTarget.src = '/placeholder-poster.svg';
}}
/>
```
- [x] **WatchHistorySidebar.tsx** - 同样添加 `onError`
- [x] **PopularFeatures.tsx** - 添加 `onError`
#### 8. **性能优化 - GPU 加速** (预计 1 小时)
- [x] 给所有 hover scale 动画添加 `will-change-transform`
- [x] 给 fixed/sticky 元素添加 `transform: translateZ(0)`
- [x] 优化 `SearchLoadingAnimation` 的 shimmer 动画
---
### 🟢 Medium Priority (中优先级,1 个月内完成)
#### 9. **响应式优化** (预计 2 小时)
- [x] **VideoGrid.tsx**
- [x] 修改 `2xl:grid-cols-7` → `2xl:grid-cols-6`
- [x] 添加 `max-w-[1920px] mx-auto` 限制最大宽度
- [x] **SearchForm.tsx**
- [x] 修改 `text-lg` → `text-base sm:text-lg`
- [x] 调整移动端按钮内边距
- [x] **WatchHistorySidebar.tsx**
- [x] 修改 `w-[90%]` → `w-[85%] sm:w-[90%]`
#### 10. **创建辅助 Hooks** (预计 3 小时)
- [x] 创建 `lib/hooks/useKeyboardNavigation.ts`
```tsx
export function useKeyboardNavigation(items: any[], onSelect: (item: any) => void) {
// 实现方向键导航逻辑
}
```
- [x] 创建 `lib/hooks/useFocusTrap.ts`
```tsx
export function useFocusTrap(containerRef: RefObject<HTMLElement>) {
// 实现焦点陷阱
}
```
- [x] 创建 `lib/hooks/useMediaQuery.ts`
```tsx
export function useMediaQuery(query: string) {
// 实现媒体查询 hook
}
```
- [x] 创建 `lib/hooks/useClickOutside.ts`
```tsx
export function useClickOutside(ref: RefObject<HTMLElement>, handler: () => void) {
// 点击外部关闭
}
```
#### 11. **添加确认对话框组件** (预计 2 小时) ✅ **已完成**
- [x] 创建 `components/ui/ConfirmDialog.tsx`
```tsx
interface ConfirmDialogProps {
isOpen: boolean;
title: string;
message: string;
onConfirm: () => void;
onCancel: () => void;
}
```
- [x] 在 `WatchHistorySidebar` 中使用
- [x] 在删除历史时显示确认对话框
**实现细节**:
- ✅ 创建了完整的 `ConfirmDialog` 组件,遵循 Liquid Glass 设计系统
- ✅ 支持 danger/warning/info 三种变体
- ✅ 完整的 ARIA 属性支持 (`alertdialog`, `aria-modal`, `aria-labelledby`, `aria-describedby`)
- ✅ 键盘支持:Escape 键关闭,焦点管理
- ✅ 在 `WatchHistorySidebar` 中集成,用于删除单个历史和清空全部历史
- ✅ 更新 `Button` 组件支持 `forwardRef`,增强可访问性
#### 12. **创建可访问性工具库** (预计 3 小时) ✅ **已完成**
- [x] 创建 `lib/accessibility/focus-management.ts`
- [x] `trapFocus(container: HTMLElement)`
- [x] `restoreFocus(element: HTMLElement)`
- [x] `getFocusableElements(container: HTMLElement)`
- [x] `saveFocus()` - 额外添加的实用函数
- [x] 创建 `lib/accessibility/aria-announcer.ts`
- [x] `announceToScreenReader(message: string)`
- [x] `announceError(message: string)` - 额外添加
- [x] `announceSuccess(message: string)` - 额外添加
- [x] `announceLoading(message: string)` - 额外添加
- [x] `clearAnnouncer()` - 额外添加
- [x] 在 `layout.tsx` 中添加 live region
```tsx
<div
id="aria-live-announcer"
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
/>
```
- [x] 创建 `lib/accessibility/keyboard-utils.ts`
- [x] `isActivationKey(event: KeyboardEvent)`
- [x] `handleEscape(callback: () => void)`
- [x] `hasModifierKey(event: KeyboardEvent)` - 额外添加
- [x] `getArrowKeyDirection(event: KeyboardEvent)` - 额外添加
- [x] `preventDefaultForKeys(event: KeyboardEvent, keys: string[])` - 额外添加
- [x] `createKeyboardHandler(handlers: Record<string, Function>)` - 额外添加
- [x] 创建 `lib/accessibility/index.ts` - 统一导出所有工具
**实现细节**:
- ✅ **focus-management.ts**: 完整的焦点管理工具,包括焦点陷阱、焦点恢复、获取可聚焦元素
- ✅ **aria-announcer.ts**: 屏幕阅读器播报工具,支持不同优先级(polite/assertive
- ✅ **keyboard-utils.ts**: 键盘交互工具,涵盖激活键、Escape 键、方向键等
- ✅ 在 `app/layout.tsx` 中添加了 ARIA live region
- ✅ 在 `globals.css` 中添加了 `.sr-only` 工具类
- ✅ 所有工具都有完整的 TypeScript 类型定义和中英文注释
- ✅ 创建了统一的导出文件 `lib/accessibility/index.ts`,简化导入
---
### 🔵 Low Priority (低优先级,时间允许时完成)
#### 13. **统一圆角使用方式** (预计 1 小时)
- [✅] 全局搜索 `style={{ borderRadius: 'var(--radius-`
- [✅] 统一改为 `className="rounded-[var(--radius-2xl)]"`
- [✅] 或者相反,全部统一使用 `style`
#### 14. **添加暗色模式动画过渡** (预计 1 小时)
- [✅] 在 `ThemeProvider.tsx` 中添加 View Transition API
```tsx
if (document.startViewTransition) {
document.startViewTransition(() => {
applyTheme(newTheme);
});
} else {
applyTheme(newTheme);
}
```
#### 15. **优化 SearchLoadingAnimation** (预计 1 小时)
- [ ] 添加暂停/恢复功能
- [ ] 添加动画完成回调
- [ ] 优化 shimmer 效果性能
#### 16. **为 Badge 添加 icon 支持** (预计 30 分钟)
- [ ] 在 `Badge.tsx` 中添加 `icon` prop
```tsx
interface BadgeProps {
icon?: ReactNode;
children: ReactNode;
}
```
- [ ] 更新文档
#### 17. **创建 Storybook** (预计 8 小时)
- [ ] 安装 Storybook 7.x
- [ ] 为所有 UI 组件创建 stories
- [ ] 添加 A11y addon
- [ ] 配置主题切换
---
## 📊 合规性评分总结
| 类别 | 得分 | 状态 |
|------|------|------|
| **玻璃态射效果** | 95/100 | ✅ 优秀 |
| **圆角规范** | 80/100 | ⚠️ 需改进 |
| **色彩系统** | 75/100 | ⚠️ 需验证 |
| **动画系统** | 90/100 | ✅ 优秀 |
| **响应式设计** | 85/100 | ✅ 良好 |
| **可访问性** | 60/100 | 🔴 需重大改进 |
| **架构规范** | 70/100 | ⚠️ 部分超标 |
**总体评分**: **79/100** (良好)
---
## 🎓 最佳实践建议
### 1. **建立组件审查清单**
每个新组件提交前检查:
```markdown
- [ ] 文件行数 < 150 行
- [ ] 圆角使用 var(--radius-2xl) 或 var(--radius-full)
- [ ] 包含完整的 ARIA 属性
- [ ] 支持键盘导航
- [ ] WCAG 2.2 AA 对比度达标
- [ ] 添加 focus-visible 样式
- [ ] 包含 PropTypes 或 TypeScript 接口
- [ ] 响应式断点测试通过
```
### 2. **使用 pre-commit hooks**
```bash
# .husky/pre-commit
npm run lint
npm run type-check
npm run test:a11y
```
### 3. **定期进行可访问性审计**
```bash
# 安装工具
npm install --save-dev @axe-core/react
npm install --save-dev eslint-plugin-jsx-a11y
# 运行审计
npm run audit:a11y
```
---
## 📖 参考资源
1. **Liquid Glass Design System**: 项目根目录的系统提示词
2. **WCAG 2.2**: https://www.w3.org/WAI/WCAG22/quickref/
3. **ARIA Authoring Practices**: https://www.w3.org/WAI/ARIA/apg/
4. **React Accessibility**: https://react.dev/learn/accessibility
5. **Tailwind CSS Accessibility**: https://tailwindcss.com/docs/screen-readers
---
## ✅ 结论
KVideo 项目在视觉设计和核心 UI 实现上展现了**高水准的专业性**,Liquid Glass 设计系统的核心理念得到了充分体现。然而,**可访问性和代码架构方面存在明显改进空间**。
**建议优先级**:
1. **立即修复**: 文件拆分、focus-visible、对比度验证 (1-2 周)
2. **短期完成**: ARIA 属性、键盘导航 (2-4 周)
3. **中期改进**: Hooks 提取、确认对话框、图片优化 (1-2 月)
4. **长期优化**: Storybook、完整文档、自动化测试 (2-3 月)
遵循此审计报告,项目可在 **3 个月内达到 95+ 分的合规度**,成为 Liquid Glass 设计系统的**标杆实现**。
---
**审计人**: GitHub Copilot (Liquid Glass Design Architect)
**审计版本**: v1.0
**下次审计建议日期**: 2025-12-18
+63 -14
View File
@@ -1,29 +1,64 @@
'use client';
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef, useCallback } from 'react';
interface SearchLoadingAnimationProps {
currentSource?: string;
checkedSources?: number;
totalSources?: number;
isPaused?: boolean;
onComplete?: (checkedSources: number, totalSources: number) => void;
}
export function SearchLoadingAnimation({
currentSource,
checkedSources = 0,
totalSources = 16,
isPaused = false,
onComplete,
}: SearchLoadingAnimationProps) {
const [dots, setDots] = useState('');
useEffect(() => {
const dotInterval = setInterval(() => {
setDots((prev) => (prev.length >= 3 ? '' : prev + '.'));
}, 500);
return () => clearInterval(dotInterval);
}, []);
const dotIntervalRef = useRef<NodeJS.Timeout | null>(null);
const hasCalledComplete = useRef(false);
// Calculate progress (0-100%)
const progress = totalSources > 0 ? (checkedSources / totalSources) * 100 : 0;
const isComplete = progress >= 100;
// Animation pause/resume logic
useEffect(() => {
if (isPaused || isComplete) {
if (dotIntervalRef.current) {
clearInterval(dotIntervalRef.current);
dotIntervalRef.current = null;
}
return;
}
dotIntervalRef.current = setInterval(() => {
setDots((prev) => (prev.length >= 3 ? '' : prev + '.'));
}, 500);
return () => {
if (dotIntervalRef.current) {
clearInterval(dotIntervalRef.current);
dotIntervalRef.current = null;
}
};
}, [isPaused, isComplete]);
// Call onComplete callback when animation finishes
useEffect(() => {
if (isComplete && onComplete && !hasCalledComplete.current) {
hasCalledComplete.current = true;
// Small delay to allow animation to settle
const timeout = setTimeout(() => {
onComplete(checkedSources, totalSources);
}, 300);
return () => clearTimeout(timeout);
}
}, [isComplete, onComplete, checkedSources, totalSources]);
const statusText = `${checkedSources}/${totalSources} 个源`;
return (
@@ -55,26 +90,40 @@ export function SearchLoadingAnimation({
className="h-1 bg-[color-mix(in_srgb,var(--glass-bg)_50%,transparent)] overflow-hidden rounded-[var(--radius-full)]"
>
<div
className="h-full bg-[var(--accent-color)] transition-all duration-500 ease-out relative will-change-[width] rounded-[var(--radius-full)]"
className="h-full bg-[var(--accent-color)] transition-all duration-500 ease-out relative will-change-transform rounded-[var(--radius-full)]"
style={{
width: `${progress}%`,
transform: 'translateZ(0)'
}}
>
{/* Shimmer Effect - Optimized for GPU */}
{/* Shimmer Effect - Optimized for GPU with contain for better performance */}
<div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer"
className="absolute inset-0 animate-shimmer"
style={{
background: 'linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%)',
willChange: 'transform',
transform: 'translateZ(0)'
transform: 'translateZ(0)',
contain: 'strict'
}}
></div>
</div>
</div>
{/* Progress Info - Real-time count */}
{/* Progress Info - Real-time count with pause indicator */}
<div className="flex items-center justify-between mt-2 text-xs text-[var(--text-color-secondary)]">
<span>{statusText}</span>
<span className="flex items-center gap-2">
{statusText}
{isPaused && (
<span className="px-2 py-0.5 rounded-[var(--radius-full)] bg-[var(--glass-bg)] text-[10px]">
</span>
)}
{isComplete && (
<span className="px-2 py-0.5 rounded-[var(--radius-full)] bg-[var(--accent-color)] text-white text-[10px]">
</span>
)}
</span>
<span className="font-medium">{Math.round(progress)}%</span>
</div>
</div>
+26 -1
View File
@@ -4,14 +4,37 @@ interface BadgeProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
className?: string;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
}
export function Badge({ children, variant = 'primary', className = '' }: BadgeProps) {
export function Badge({
children,
variant = 'primary',
className = '',
icon,
iconPosition = 'left'
}: BadgeProps) {
const variants = {
primary: "bg-[var(--accent-color)] text-white shadow-[var(--shadow-sm)]",
secondary: "bg-[var(--glass-bg)] backdrop-blur-[10px] [-webkit-backdrop-filter:blur(10px)] border border-[var(--glass-border)] text-[var(--text-color)]",
};
const iconElement = icon && (
<span
className={`inline-flex items-center justify-center transition-transform duration-200 ${
iconPosition === 'left' ? 'mr-1' : 'ml-1'
}`}
style={{
width: '0.875em',
height: '0.875em',
transform: 'translateZ(0)'
}}
>
{icon}
</span>
);
return (
<span
className={`
@@ -24,7 +47,9 @@ export function Badge({ children, variant = 'primary', className = '' }: BadgePr
${className}
`}
>
{icon && iconPosition === 'left' && iconElement}
{children}
{icon && iconPosition === 'right' && iconElement}
</span>
);
}