# 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 时保持逻辑不变