# VoiceMaterialLibrary 页面拆分方案

## 一、现状摸底

**文件：** `apps/web/src/pages/voice-materials/VoiceMaterialLibrary.tsx`
**总行数：** 1966 行
**CSS 文件：** `apps/web/src/pages/voice-materials/voice-materials.css` (1262 行)

### 代码质量

| 指标 | 数量 | 评价 |
|------|------|------|
| `any` 类型 | 0 | ✅ 优秀 |
| `@ts-ignore` | 0 | ✅ 优秀 |
| `eslint-disable` | 0 | ✅ 优秀 |
| TODO/FIXME | 0 | ✅ 干净 |

### 内联组件分布

| 组件 | 行数 | 职责 |
|------|------|------|
| `TagSelector` | 148行 (197-345) | 标签选择器（输入+建议+选择） |
| `MaterialForm` | 187行 (356-543) | 上传/编辑表单（名称/描述/性别/标签/文件） |
| `VoiceMaterialCard` | 220行 (543-763) | 卡片视图 + 音频播放进度条 |
| `VoiceMaterialRow` | 153行 (763-916) | 列表视图 + 音频播放进度条 |
| **VoiceMaterialLibrary（主组件）** | **1051行 (916-1966)** | **页面主逻辑 + 渲染** |

### 主组件内部结构（1051行）

- **状态 & hooks**：约 230行
  - 数据查询：assets / libraries / tags / presetVoices
  - 视图状态：viewMode / searchText / filterGender / filterTagId
  - 播放状态：playingId / currentTime / audioRef / volume / pausedMaterial
  - 弹窗：uploadOpen / editingMaterial / ttsOpen
  - 批量：selectedIds / uploadProgress / batchCustomTag
  - TTS：ttsText / ttsVoiceId / ttsSpeed / ttsJobId / ttsStatus / ttsAudioUrl / ttsError
- **播放控制函数**：约 100行（stopPlayback/startPlayback/handlePlay/handlePause/handleSeek/handleVolumeChange/toggleMute）
- **数据操作函数**：约 60行（handleUpload/handleEdit/handleDelete）
- **筛选 & 统计**：约 40行（filtered/tagCountMap）
- **批量操作**：约 80行（handleToggleSelect/handleSelectAll/handleBatchDelete/handleBatchTag/handleBatchCustomTag）
- **TTS 合成**：约 70行（handleTtsSynthesize/handleTtsSave）
- **渲染 JSX**：约 473行

## 二、拆分策略

跟 GeneratePage 同样的思路：**按职责拆分，功能不变，纯结构优化。**

拆分四阶段，每个阶段独立 PR，逐步降低主文件复杂度。

---

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

**目标：** 把纯数据定义和纯函数抽出去，主文件只留组件。

### 抽离内容

| 新文件 | 内容 | 行数估计 |
|--------|------|---------|
| `types.ts` | `VoiceGender` / `ViewMode` / `VoiceMaterial` / `VoiceAssetMetadata` 等类型定义 | ~40行 |
| `constants.ts` | `MAX_CARD_TAGS` / `MAX_ROW_TAGS` / `TAG_VARIANTS` / `GENDER_OPTIONS` | ~25行 |
| `utils/format.ts` | `formatDuration` / `formatFileSize` / `formatDate` / `genderLabel` / `genderIcon` / `genderClass` | ~35行 |
| `utils/audio.ts` | `getAudioDuration`（纯工具，跟组件无关） | ~20行 |
| `utils/mappers.ts` | `mapAssetToMaterial` / `buildMetadata`（数据映射） | ~35行 |

### 预期效果

- 主文件减少约 **155 行**（1966 → ~1811）
- 工具函数可复用、可单测

---

## Phase 2：抽离已有内联子组件

**目标：** 把 4 个已经定义好的内联组件拆成独立文件，主文件只保留 VoiceMaterialLibrary 主组件。

### 抽离内容

| 新文件 | 原位置 | 行数 | 备注 |
|--------|--------|------|------|
| `components/TagSelector.tsx` | 197-345行 | ~148行 | 标签选择器 |
| `components/MaterialForm.tsx` | 356-543行 | ~187行 | 上传/编辑表单 |
| `components/VoiceMaterialCard.tsx` | 543-763行 | ~220行 | 卡片视图，带播放控制 |
| `components/VoiceMaterialRow.tsx` | 763-916行 | ~153行 | 列表视图，带播放控制 |

### 播放状态共享方案

Card 和 Row 都有播放进度条（共 ~60行重复代码），但播放状态在主组件里。

**方案：** 播放状态提升到主组件，Card/Row 只接收 prop 并回调：
- `isPlaying` / `currentTime` / `onPlay` / `onPause` / `onSeek`
- 主组件统一管理 audioRef 和播放状态

这样 Card 和 Row 是纯展示组件，播放逻辑集中在主组件。

### 预期效果

- 主文件减少约 **700 行**（1811 → ~1111）
- 主组件专注页面逻辑，子组件专注展示

---

## Phase 3：主组件拆分 — 业务逻辑抽 Hook

**目标：** 把主组件里的业务逻辑按领域拆成自定义 Hook，主组件只负责组装。

### 抽离 Hooks

| Hook 文件 | 职责 | 管理的状态 |
|-----------|------|-----------|
| `hooks/useVoiceMaterials.ts` | 素材列表查询 + 筛选 + 增删改 | assets / searchText / filterGender / filterTagId / uploadMutation / editMutation / deleteMutation |
| `hooks/useAudioPlayer.ts` | 音频播放控制 | playingId / currentTime / volume / audioRef / pausedMaterial |
| `hooks/useBatchOperations.ts` | 批量操作 | selectedIds / handleToggleSelect / handleSelectAll / handleBatchDelete / handleBatchTag |
| `hooks/useTtsSynthesize.ts` | TTS 合成逻辑 | ttsText / ttsVoiceId / ttsSpeed / ttsJobId / ttsStatus / ttsAudioUrl / ttsError |

### 预期效果

- 主组件减少约 **500 行**（1111 → ~611）
- 每个 Hook 职责单一，可独立测试
- 主组件变成"组装器"，可读性大幅提升

---

## Phase 4：UI 组件细化 — 工具栏 & 弹窗拆分

**目标：** 把主组件里的大块 JSX 拆成独立 UI 组件。

### 抽离 UI 组件

| 组件文件 | 内容 | 行数估计 |
|----------|------|---------|
| `components/LibraryToolbar.tsx` | 顶部工具栏：搜索框 + 性别筛选 + 视图切换 + 结果计数 | ~60行 |
| `components/TagFilterBar.tsx` | 标签筛选药丸条（全部 + 各标签计数） | ~45行 |
| `components/BatchActionBar.tsx` | 批量操作栏：全选 + 计数 + 批量打标签 + 批量删除 | ~70行 |
| `components/UploadModal.tsx` | 上传弹窗（包裹 MaterialForm） | ~30行 |
| `components/EditModal.tsx` | 编辑弹窗（包裹 MaterialForm） | ~30行 |
| `components/TtsSynthesizeModal.tsx` | AI 配音合成弹窗：文本输入 + 音色选择 + 语速 + 合成结果预览 + 保存 | ~150行 |
| `components/EmptyState.tsx` | 空状态展示 | ~20行 |
| `components/VoiceMaterialGrid.tsx` | 卡片视图网格容器 | ~25行 |
| `components/VoiceMaterialList.tsx` | 列表视图表格容器 | ~25行 |

### 预期效果

- 主组件 JSX 部分减少约 **400 行**（611 → ~211）
- 每个 UI 组件职责清晰，方便后续迭代

---

## 三、拆分后文件结构

```
apps/web/src/pages/voice-materials/
├── VoiceMaterialLibrary.tsx      # 主组件（~211行，组装器）
├── voice-materials.css           # 样式（保持不变，后续再拆）
├── types.ts                      # 类型定义
├── constants.ts                  # 常量
├── utils/
│   ├── format.ts                 # 格式化工具
│   ├── audio.ts                  # 音频工具
│   └── mappers.ts                # 数据映射
├── hooks/
│   ├── useVoiceMaterials.ts      # 素材数据 + 筛选 + 增删改
│   ├── useAudioPlayer.ts         # 音频播放控制
│   ├── useBatchOperations.ts     # 批量操作
│   └── useTtsSynthesize.ts       # TTS合成
└── components/
    ├── TagSelector.tsx           # 标签选择器
    ├── MaterialForm.tsx          # 上传/编辑表单
    ├── VoiceMaterialCard.tsx     # 卡片视图
    ├── VoiceMaterialRow.tsx      # 列表视图
    ├── VoiceMaterialGrid.tsx     # 卡片网格容器
    ├── VoiceMaterialList.tsx     # 列表容器
    ├── LibraryToolbar.tsx        # 顶部工具栏
    ├── TagFilterBar.tsx          # 标签筛选条
    ├── BatchActionBar.tsx        # 批量操作栏
    ├── UploadModal.tsx           # 上传弹窗
    ├── EditModal.tsx             # 编辑弹窗
    ├── TtsSynthesizeModal.tsx    # TTS合成弹窗
    └── EmptyState.tsx            # 空状态
```

### 拆分前后对比

| 指标 | 拆分前 | 拆分后 | 变化 |
|------|--------|--------|------|
| 主文件行数 | 1966 | ~211 | **-89%** |
| 文件数量 | 2 | 22 | +20 |
| 最大文件 | 1966行 | ~220行 | -89% |
| 可测试性 | 低 | 高 | 每个Hook/组件可单测 |

## 四、实施顺序 & 风险

1. **Phase 1**：最低风险，纯抽离，零逻辑变化
2. **Phase 2**：低风险，组件本来就是独立定义的，只是挪位置
3. **Phase 3**：中风险，Hook 拆分需要仔细梳理状态依赖
4. **Phase 4**：低风险，JSX 拆分，纯结构调整

**每个 Phase 完成后提 PR，CI 全绿再合，跟 GeneratePage 同样节奏。**
