Files
xiaoxia-saas/docs/voice-material-library-refactor-plan.md
xiaoxia 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
refactor(voices): Phase 1 - extract types, constants and utils (#873)
2026-07-25 12:25:14 +08:00

8.4 KiB
Executable File
Raw Permalink Blame History

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
    • TTSttsText / 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 4UI 组件细化 — 工具栏 & 弹窗拆分

目标: 把主组件里的大块 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 完成后提 PRCI 全绿再合,跟 GeneratePage 同样节奏。