# VoiceLibrary 页面重构方案

## 现状分析

**当前文件：** `apps/web/src/pages/voices/VoiceLibrary.tsx` — 1783 行

**代码质量：**
- `any`: 0 处
- `ts-ignore`: 0 处
- `eslint-disable`: 0 处
- `TODO`: 1 处
- 整体质量良好，重构阻力小

## 目录结构（重构后）

```
apps/web/src/pages/voices/
├── VoiceLibrary.tsx        # 主组件（目标：~550 行，-69%）
├── types.ts                 # 类型定义
├── constants.ts             # 常量 + 配置
├── utils/
│   ├── format.ts          # 格式化工具函数
│   └── audio.ts           # 音频工具函数
├── components/
│   ├── VoiceCard.tsx       # 预设音色卡片
│   ├── CloneVoiceCard.tsx  # 克隆音色卡片
│   ├── CloneDetailModal.tsx # 克隆详情弹窗
│   ├── CloneCardSkeleton.tsx # 克隆卡片骨架屏
│   ├── UploadModal.tsx    # 上传音色弹窗
│   ├── TTSModal.tsx       # TTS合成弹窗
│   ├── VoiceFilterBar.tsx # 筛选栏（搜索/性别/语言）
│   └── VoiceTabBar.tsx     # Tab切换栏
└── hooks/
    ├── useVoices.ts        # 音色数据查询 + 筛选
    ├── useAudioPlayer.ts   # 音频播放控制
    ├── useVoiceUpload.ts  # 音色上传逻辑
    ├── useTTS.ts           # TTS合成逻辑
    └── useCloneVoice.ts    # 克隆音色操作
```

## 三阶段渐进式重构

### Phase 1：抽离类型、常量、工具函数

**目标：** 主文件 1783 → ~1550 行（-13%）

**抽出内容：**

1. **`types.ts`** — 类型定义（~60行）
   - `PresetVoiceDisplay` / `ClonedVoiceDisplay` / `VoiceCardProps` / `CloneVoiceCardProps`
   - `TabKey` / `Toast` / `VoiceUploadMetadata`
   - 现有 `mapPresetToDisplay` / `mapCloneToDisplay` 数据映射函数

2. **`constants.ts`** — 常量配置（~40行）
   - `CLONE_STATUS_CONFIG` 克隆状态配置
   - `GENDER_LABEL_MAP` / `LANGUAGE_LABEL_MAP` 性别/语言标签映射
   - Tab 配置项

3. **`utils/format.ts`** — 格式化工具（~25行）
   - `formatTime` 时长格式化
   - `formatFileSize` 文件大小格式化
   - `genderLabel` / `languageLabel` / `genderClass`

4. **`utils/audio.ts`** — 音频工具（~15行）
   - `getAudioDuration` 获取音频文件时长

5. **`format.test.ts`** — 工具函数单测
   - 覆盖 formatTime / formatFileSize 等纯函数

**Phase 1 交付：**
- 新增文件：6 个（types.ts / constants.ts / utils/format.ts / utils/audio.ts / format.test.ts）
- 主文件减少：~230 行
- 纯机械抽离，无逻辑改动

---

### Phase 2：抽离子组件

**目标：** 主文件 1550 → ~950 行（-39%）

**抽出组件：**

1. **`components/VoiceCard.tsx`** — 预设音色卡片（~120行）
   - 卡片渲染：头像、名称、性别标签、播放按钮、进度条、收藏
   - Props：voice / playingId / currentTime / onPlay / onPause / onSeek / onToggleStar

2. **`components/CloneVoiceCard.tsx`** — 克隆音色卡片（~160行）
   - 三种状态：processing / success / failed
   - 操作：播放、详情、删除、重试、使用
   - Props：voice / playingId / currentTime / onPlayPause / onShowDetail / onDelete / onRetry / onUse

3. **`components/CloneDetailModal.tsx`** — 克隆详情弹窗（~90行）
   - 展示克隆音色详细信息
   - 状态展示、音频播放、操作按钮

4. **`components/CloneCardSkeleton.tsx`** — 克隆卡片骨架屏（~20行）
   - 加载状态占位

5. **`components/UploadModal.tsx`** — 上传音色弹窗（~180行）
   - 文件选择、名称/性别/描述填写
   - 上传进度展示
   - Props：open / onClose / onUpload

6. **`components/TTSModal.tsx`** — TTS合成弹窗（~170行）
   - 文本输入、音色选择、语速调节
   - 合成状态轮询（idle/synthesizing/done/error）
   - 保存到素材库
   - Props：open / onClose / onSave

7. **`components/VoiceFilterBar.tsx`** — 筛选栏（~80行）
   - 搜索框、性别筛选、语言筛选
   - Props：searchText / filterGender / filterLang / onChange handlers

**Phase 2 交付：**
- 新增组件：7 个
- 主文件减少：~600 行
- 纯UI抽离，业务逻辑保留在主组件

---

### Phase 3：抽离业务逻辑 Hook

**目标：** 主文件 950 → ~550 行（-42%）

**抽出 Hook：**

1. **`hooks/useVoices.ts`** — 音色数据 Hook（~200行）
   - 三个 useQuery：presetVoices / cloneVoices / materialVoices / unifiedStats
   - 筛选逻辑：searchText / filterGender / filterLang → filteredPreset / filteredClone
   - 统计数据：presetCount / cloneCount / materialCount
   - 收藏切换 handleToggleStar
   - 返回：data / loading / filtered / counts / handlers

2. **`hooks/useAudioPlayer.ts`** — 音频播放控制 Hook（~120行）
   - playingId / currentTime / intervalRef 状态
   - handlePlay / handlePause / handleSeek
   - 自动停止（切换新音频时停止旧的）
   - 组件卸载清理
   - 注意：与 VoiceMaterialLibrary 的 useAudioPlayer 类似但有差异（一个操作DOM音频，一个操作audio元素），评估是否复用还是独立

3. **`hooks/useVoiceUpload.ts`** — 音色上传 Hook（~120行）
   - uploadFile / uploadName / uploadGender / uploadDesc / uploadProgress 状态
   - uploadMutation（上传文件 + 创建音色）
   - buildVoiceMetadata 元数据构建
   - getAudioDuration 时长获取
   - 成功后刷新列表 + toast 关闭弹窗

4. **`hooks/useTTS.ts`** — TTS合成 Hook（~110行）
   - ttsOpen / ttsText / ttsVoiceId / ttsSpeed 弹窗状态
   - ttsJobId / ttsStatus / ttsAudioUrl / ttsError 合成状态
   - handleTtsSynthesize 发起合成 + 轮询
   - handleTtsSave 保存到素材库
   - handleTtsClose 清理状态

5. **`hooks/useCloneVoice.ts`** — 克隆音色操作 Hook（~80行）
   - deleteMutation / retryMutation
   - handleCloneDelete / handleCloneRetry
   - detailVoice 详情状态
   - 操作后刷新列表

**Phase 3 交付：**
- 新增 Hook：5 个
- 主文件减少：~400 行
- 主文件只剩：Tab切换逻辑 + JSX 组装 + 顶层状态编排

---

## 最终效果汇总

| 阶段 | 主文件行数 | 减少行数 | 减少比例 | 新增文件 |
|------|-----------|---------|----------|----------|
| 初始 | 1783 | — | — | — |
| Phase 1 | ~1550 | -233 | -13.1% | 5 |
| Phase 2 | ~950 | -600 | -38.7% | 7 |
| Phase 3 | ~550 | -400 | -42.1% | 5 |
| **总计** | **~550** | **-1233** | **-69.2%** | **17** |

## 与 GeneratePage / VoiceMaterialLibrary 的异同

**相同点：**
- 三阶段渐进式重构（类型常量 → 组件 → Hook）
- 每阶段独立PR，独立验证
- 每阶段加 smoke test 保证 vitest related 模式可用
- 代码质量基线高（0 any / 0 ts-ignore）

**不同点：**
- VoiceLibrary 有两个数据源（预设音色 + 克隆音色），还有TTS和上传功能
- 音频播放逻辑与 VoiceMaterialLibrary 类似但操作的音频类型不同
- 有 CloneModal 是外部组件（已在 @/components/voice/CloneModal），不需要重写
- 骨架屏组件比较简单

## 风险与注意事项

1. **vitest related 模式**：每阶段新增文件需建立测试覆盖，避免 "No test files found"
2. **Preview Deploy**：环境问题导致失败，不影响代码质量
3. **音频播放逻辑**：VoiceLibrary 和 VoiceMaterialLibrary 的播放控制类似但是两套独立实现，后续可考虑抽象为共享 Hook
4. **克隆状态**：CLONE_STATUS_CONFIG 是核心常量，抽离时注意类型完整
5. **上传逻辑**：uploadMutation 内部逻辑较复杂（上传文件 + 获取时长 + 创建音色），抽 Hook 时保持逻辑不变
