40776cd4fa
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m36s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 4m40s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m22s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m21s
CI/CD Pipeline / Unit Tests (push) Failing after 6m50s
CI/CD Pipeline / Integration Tests (push) Successful in 2m30s
CI/CD Pipeline / Frontend Lint (push) Successful in 42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m43s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m59s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m13s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m22s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 32s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 6m9s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 6m45s
8.4 KiB
Executable File
8.4 KiB
Executable File
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/组件可单测 |
四、实施顺序 & 风险
- Phase 1:最低风险,纯抽离,零逻辑变化
- Phase 2:低风险,组件本来就是独立定义的,只是挪位置
- Phase 3:中风险,Hook 拆分需要仔细梳理状态依赖
- Phase 4:低风险,JSX 拆分,纯结构调整
每个 Phase 完成后提 PR,CI 全绿再合,跟 GeneratePage 同样节奏。