# 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 同样节奏。**