From 18f99ba32b59bb8c16a7412a3259d6580c1f51aa Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 10:14:24 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(voice-materials):=20=E6=8B=86?= =?UTF-8?q?=E5=88=86=20useVoiceMaterialActions=20Hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三阶段重构: - Phase 1: 拆分为 3 个领域子 Hook - useVoiceUpload: 上传流程(库→上传→时长→创建→标签) - useVoiceEdit: 编辑流程(更新信息+标签差异同步) - useVoiceDelete: 删除操作 - Phase 2: 主 Hook 组合子 Hook + UI 弹窗状态 - Phase 3: 主文件 216→80 行(-63%) 保持导出签名向后兼容 --- .../actions/useVoiceDelete.ts | 38 ++++ .../useVoiceMaterials/actions/useVoiceEdit.ts | 75 +++++++ .../actions/useVoiceUpload.ts | 111 +++++++++ .../useVoiceMaterialActions.ts | 212 ++++-------------- .../test/pages/voice-materials/smoke.test.tsx | 3 + 5 files changed, 265 insertions(+), 174 deletions(-) create mode 100755 apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceDelete.ts create mode 100755 apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts create mode 100755 apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceUpload.ts mode change 100644 => 100755 apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions.ts mode change 100644 => 100755 apps/web/src/test/pages/voice-materials/smoke.test.tsx diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceDelete.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceDelete.ts new file mode 100755 index 000000000..df7fb7a1a --- /dev/null +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceDelete.ts @@ -0,0 +1,38 @@ +import { useCallback } from "react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { deleteAsset } from "@/api/assets" +import { type VoiceMaterial } from "../../types" + +interface UseVoiceDeleteOptions { + materials: VoiceMaterial[] +} + +/** + * 配音素材删除 Hook + */ +export function useVoiceDelete({ materials }: UseVoiceDeleteOptions) { + const queryClient = useQueryClient() + + const deleteMutation = useMutation({ + mutationFn: (assetId: string) => deleteAsset(assetId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["assets", "voice"] }) + }, + }) + + const handleDelete = useCallback( + (id: string, onBeforeDelete?: () => void) => { + const material = materials.find((m) => m.id === id) + if (!material) return + if (onBeforeDelete) onBeforeDelete() + deleteMutation.mutate(id) + }, + [materials, deleteMutation], + ) + + return { + isDeleting: deleteMutation.isPending, + deleteMutation, + handleDelete, + } +} diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts new file mode 100755 index 000000000..37e990a69 --- /dev/null +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts @@ -0,0 +1,75 @@ +import { useCallback } from "react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { updateAsset } from "@/api/assets" +import { tagAsset, untagAsset } from "@/api/tags" +import { type VoiceGender, type VoiceMaterial, buildMetadata } from "../../types" + +interface UseVoiceEditOptions { + materials: VoiceMaterial[] +} + +/** + * 配音素材编辑 Hook + * 封装编辑流程:更新基础信息 + 同步标签差异 + */ +export function useVoiceEdit({ materials }: UseVoiceEditOptions) { + const queryClient = useQueryClient() + + const editMutation = useMutation({ + mutationFn: async (data: { + id: string + name: string + gender: VoiceGender + description: string + tagIds: string[] + }) => { + // 1. 更新基础信息 + await updateAsset(data.id, { + name: data.name, + metadata: buildMetadata({ + gender: data.gender, + description: data.description, + }), + }) + + // 2. 对比标签差异,调用 tag/untag API + const currentAsset = materials.find((m) => m.id === data.id) + const oldTagIds = currentAsset?.tagIds ?? [] + const newTagIds = data.tagIds + + const toAdd = newTagIds.filter((id) => !oldTagIds.includes(id)) + const toRemove = oldTagIds.filter((id) => !newTagIds.includes(id)) + + if (toAdd.length > 0) { + await tagAsset(data.id, toAdd) + } + for (const tagId of toRemove) { + await untagAsset(data.id, tagId) + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["assets", "voice"] }) + queryClient.invalidateQueries({ queryKey: ["tags"] }) + }, + }) + + const handleEdit = useCallback( + (editingMaterial: VoiceMaterial | null, data: Omit & { file?: File }) => { + if (!editingMaterial) return + editMutation.mutate({ + id: editingMaterial.id, + name: data.name, + gender: data.gender, + description: data.description, + tagIds: data.tagIds, + }) + }, + [editMutation], + ) + + return { + isEditing: editMutation.isPending, + editMutation, + handleEdit, + } +} diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceUpload.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceUpload.ts new file mode 100755 index 000000000..af1f5665a --- /dev/null +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceUpload.ts @@ -0,0 +1,111 @@ +import { useState, useCallback } from "react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { message } from "antd" +import { + createAsset, + uploadAssetDirect, + getAssetLibraries, + type AssetLibraryItem, +} from "@/api/assets" +import { tagAsset } from "@/api/tags" +import { type VoiceGender, type VoiceMaterial, buildMetadata } from "../../types" +import { getAudioDuration } from "../../utils/audio" + +interface UseVoiceUploadOptions { + voiceLibrary?: { id: string; kind: string } + createLibMutation: { mutateAsync: () => Promise; isPending: boolean } +} + +/** + * 配音素材上传 Hook + * 封装上传流程:获取库 → 上传文件 → 获取时长 → 创建记录 → 打标签 + */ +export function useVoiceUpload({ voiceLibrary, createLibMutation }: UseVoiceUploadOptions) { + const queryClient = useQueryClient() + const [uploadProgress, setUploadProgress] = useState(null) + + const uploadMutation = useMutation({ + mutationFn: async (data: { + file: File + name: string + gender: VoiceGender + description: string + tagIds: string[] + }) => { + setUploadProgress(0) + try { + // 1. 获取或等待 voice library + let lib = voiceLibrary + if (!lib) { + if (createLibMutation.isPending) { + await createLibMutation.mutateAsync() + } + const libs = await queryClient.fetchQuery({ + queryKey: ["asset-libraries"], + queryFn: getAssetLibraries, + }) + lib = libs.find((l: AssetLibraryItem) => l.kind === "voice") + if (!lib) throw new Error("无法创建配音库") + } + + // 2. 上传文件(带进度) + const { storage_key } = await uploadAssetDirect({ + file: data.file, + library_id: lib.id, + onProgress: (p) => setUploadProgress(p), + }) + + // 3. 获取音频时长 + const duration = await getAudioDuration(data.file) + + // 4. 创建素材记录 + const asset = await createAsset({ + library_id: lib.id, + name: data.name, + storage_key, + mime_type: data.file.type || "audio/mpeg", + metadata: buildMetadata({ + gender: data.gender, + description: data.description, + duration, + }), + }) + + // 5. 打标签(标签走独立 API) + if (data.tagIds.length > 0) { + await tagAsset(asset.id, data.tagIds) + } + } finally { + setUploadProgress(null) + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["assets", "voice"] }) + queryClient.invalidateQueries({ queryKey: ["tags"] }) + }, + onError: (err: Error) => { + message.error(err.message || "上传失败,请重试") + }, + }) + + const handleUpload = useCallback( + (data: Omit & { file?: File }) => { + if (!data.file) return + uploadMutation.mutate({ + file: data.file, + name: data.name, + gender: data.gender, + description: data.description, + tagIds: data.tagIds, + }) + }, + [uploadMutation], + ) + + return { + uploadProgress, + isUploading: uploadMutation.isPending, + uploadMutation, + handleUpload, + } +} diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions.ts old mode 100644 new mode 100755 index bb2be1a54..37dbb1dd5 --- a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions.ts +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions.ts @@ -1,17 +1,9 @@ -import { useState, useCallback } from "react" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { message } from "antd" -import { - createAsset, - updateAsset, - deleteAsset, - uploadAssetDirect, - getAssetLibraries, - type AssetLibraryItem, -} from "@/api/assets" -import { tagAsset, untagAsset } from "@/api/tags" -import { type VoiceGender, type VoiceMaterial, buildMetadata } from "../../types" -import { getAudioDuration } from "../../utils/audio" +import { useState } from "react" +import { type VoiceMaterial } from "../../../types" +import { type AssetLibraryItem } from "@/api/assets" +import { useVoiceUpload } from "./actions/useVoiceUpload" +import { useVoiceEdit } from "./actions/useVoiceEdit" +import { useVoiceDelete } from "./actions/useVoiceDelete" interface UseVoiceMaterialActionsOptions { voiceLibrary?: { id: string; kind: string } @@ -21,184 +13,56 @@ interface UseVoiceMaterialActionsOptions { /** * 配音素材操作 Hook - * 封装上传、编辑、删除等变更操作及相关 UI 状态 + * 组合上传、编辑、删除三个子 Hook,统一管理弹窗状态 */ export function useVoiceMaterialActions({ voiceLibrary, materials, createLibMutation, }: UseVoiceMaterialActionsOptions) { - const queryClient = useQueryClient() - - // ── 弹窗状态 ────────────────────────────────────────────── + // ── 弹窗状态 ── const [uploadOpen, setUploadOpen] = useState(false) const [editingMaterial, setEditingMaterial] = useState(null) - // ── 上传进度 ────────────────────────────────────────────── - const [uploadProgress, setUploadProgress] = useState(null) + // ── 子领域 Hooks ── + const { uploadProgress, uploadMutation } = useVoiceUpload({ voiceLibrary, createLibMutation }) + const { editMutation } = useVoiceEdit({ materials }) + const { handleDelete } = useVoiceDelete({ materials }) - // ── 上传 mutation ───────────────────────────────────────── - const uploadMutation = useMutation({ - mutationFn: async (data: { - file: File - name: string - gender: VoiceGender - description: string - tagIds: string[] - }) => { - setUploadProgress(0) - try { - // 1. 获取或等待 voice library - let lib = voiceLibrary - if (!lib) { - if (createLibMutation.isPending) { - await createLibMutation.mutateAsync() - } - const libs = await queryClient.fetchQuery({ - queryKey: ["asset-libraries"], - queryFn: getAssetLibraries, - }) - lib = libs.find((l: AssetLibraryItem) => l.kind === "voice") - if (!lib) throw new Error("无法创建配音库") - } + /* ── 操作 handlers(关联弹窗状态) ── */ - // 2. 上传文件(带进度) - const { storage_key } = await uploadAssetDirect({ - file: data.file, - library_id: lib.id, - onProgress: (p) => setUploadProgress(p), - }) - - // 3. 获取音频时长 - const duration = await getAudioDuration(data.file) - - // 4. 创建素材记录 - const asset = await createAsset({ - library_id: lib.id, - name: data.name, - storage_key, - mime_type: data.file.type || "audio/mpeg", - metadata: buildMetadata({ - gender: data.gender, - description: data.description, - duration, - }), - }) - - // 5. 打标签(标签走独立 API) - if (data.tagIds.length > 0) { - await tagAsset(asset.id, data.tagIds) - } - } finally { - setUploadProgress(null) - } - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["assets", "voice"] }) - queryClient.invalidateQueries({ queryKey: ["tags"] }) - }, - onError: (err: Error) => { - message.error(err.message || "上传失败,请重试") - }, - }) - - // ── 编辑 mutation ───────────────────────────────────────── - const editMutation = useMutation({ - mutationFn: async (data: { - id: string - name: string - gender: VoiceGender - description: string - tagIds: string[] - }) => { - // 1. 更新基础信息 - await updateAsset(data.id, { - name: data.name, - metadata: buildMetadata({ - gender: data.gender, - description: data.description, - }), - }) - - // 2. 对比标签差异,调用 tag/untag API - const currentAsset = materials.find((m) => m.id === data.id) - const oldTagIds = currentAsset?.tagIds ?? [] - const newTagIds = data.tagIds - - const toAdd = newTagIds.filter((id) => !oldTagIds.includes(id)) - const toRemove = oldTagIds.filter((id) => !newTagIds.includes(id)) - - if (toAdd.length > 0) { - await tagAsset(data.id, toAdd) - } - for (const tagId of toRemove) { - await untagAsset(data.id, tagId) - } - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["assets", "voice"] }) - queryClient.invalidateQueries({ queryKey: ["tags"] }) - }, - }) - - // ── 删除 mutation ───────────────────────────────────────── - const deleteMutation = useMutation({ - mutationFn: (assetId: string) => deleteAsset(assetId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["assets", "voice"] }) - }, - }) - - /* ── 数据操作 handlers ──────────────────────────────────── */ - - const handleUpload = useCallback( - (data: Omit & { file?: File }) => { - if (!data.file) return - uploadMutation.mutate( - { - file: data.file, - name: data.name, - gender: data.gender, - description: data.description, - tagIds: data.tagIds, - }, - { - onSuccess: () => { - setUploadOpen(false) - }, - }, - ) - }, - [uploadMutation], - ) - - const handleEdit = useCallback( - (data: Omit & { file?: File }) => { - if (!editingMaterial) return - editMutation.mutate({ - id: editingMaterial.id, + const handleUpload = (data: Omit & { file?: File }) => { + if (!data.file) return + uploadMutation.mutate( + { + file: data.file, name: data.name, gender: data.gender, description: data.description, tagIds: data.tagIds, - }) - setEditingMaterial(null) - }, - [editingMaterial, editMutation], - ) + }, + { + onSuccess: () => { + setUploadOpen(false) + }, + }, + ) + } - const handleDelete = useCallback( - (id: string, onBeforeDelete?: () => void) => { - const material = materials.find((m) => m.id === id) - if (!material) return - if (onBeforeDelete) onBeforeDelete() - deleteMutation.mutate(id) - }, - [materials, deleteMutation], - ) + const handleEdit = (data: Omit & { file?: File }) => { + if (!editingMaterial) return + editMutation.mutate({ + id: editingMaterial.id, + name: data.name, + gender: data.gender, + description: data.description, + tagIds: data.tagIds, + }) + setEditingMaterial(null) + } return { - // 上传 & 编辑状态 + // 上传 & 编辑 loading 状态 uploadProgress, isUploading: uploadMutation.isPending, isEditing: editMutation.isPending, diff --git a/apps/web/src/test/pages/voice-materials/smoke.test.tsx b/apps/web/src/test/pages/voice-materials/smoke.test.tsx old mode 100644 new mode 100755 index cafe25e2b..fff55db19 --- a/apps/web/src/test/pages/voice-materials/smoke.test.tsx +++ b/apps/web/src/test/pages/voice-materials/smoke.test.tsx @@ -33,6 +33,9 @@ describe("VoiceMaterialLibrary module smoke test", () => { // Hooks import "@/pages/voice-materials/hooks/useVoiceMaterials" import "@/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions" +import "@/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceUpload" +import "@/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit" +import "@/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceDelete" import "@/pages/voice-materials/hooks/useTtsSynthesize" import "@/pages/voice-materials/hooks/useAudioPlayer" import "@/pages/voice-materials/hooks/useBatchOperations" -- 2.54.0 From 3eef40bbf839ce064921761a4bb4ff36e8971722 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 10:19:32 +0800 Subject: [PATCH 2/3] fix(useVoiceMaterialActions): fix prettier formatting --- .../hooks/useVoiceMaterials/actions/useVoiceEdit.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts index 37e990a69..e4f245953 100755 --- a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts @@ -54,7 +54,10 @@ export function useVoiceEdit({ materials }: UseVoiceEditOptions) { }) const handleEdit = useCallback( - (editingMaterial: VoiceMaterial | null, data: Omit & { file?: File }) => { + ( + editingMaterial: VoiceMaterial | null, + data: Omit & { file?: File }, + ) => { if (!editingMaterial) return editMutation.mutate({ id: editingMaterial.id, -- 2.54.0 From ab9b7409b28f37205f521b3ca908fecc66a4a65e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 12:16:56 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(useVoiceMaterialActions):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AD=90=20Hook=20=E7=9B=B8=E5=AF=B9=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=B1=82=E7=BA=A7=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 子 Hook 位于 actions/ 子目录,引用 types/utils 需要上三级(../../../) 而非上两级(../../),导致 TS 编译失败。 --- .../hooks/useVoiceMaterials/actions/useVoiceDelete.ts | 2 +- .../hooks/useVoiceMaterials/actions/useVoiceEdit.ts | 2 +- .../hooks/useVoiceMaterials/actions/useVoiceUpload.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceDelete.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceDelete.ts index df7fb7a1a..e89b32149 100755 --- a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceDelete.ts +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceDelete.ts @@ -1,7 +1,7 @@ import { useCallback } from "react" import { useMutation, useQueryClient } from "@tanstack/react-query" import { deleteAsset } from "@/api/assets" -import { type VoiceMaterial } from "../../types" +import { type VoiceMaterial } from "../../../types" interface UseVoiceDeleteOptions { materials: VoiceMaterial[] diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts index e4f245953..c53473a27 100755 --- a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceEdit.ts @@ -2,7 +2,7 @@ import { useCallback } from "react" import { useMutation, useQueryClient } from "@tanstack/react-query" import { updateAsset } from "@/api/assets" import { tagAsset, untagAsset } from "@/api/tags" -import { type VoiceGender, type VoiceMaterial, buildMetadata } from "../../types" +import { type VoiceGender, type VoiceMaterial, buildMetadata } from "../../../types" interface UseVoiceEditOptions { materials: VoiceMaterial[] diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceUpload.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceUpload.ts index af1f5665a..84ddecc4e 100755 --- a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceUpload.ts +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/actions/useVoiceUpload.ts @@ -8,8 +8,8 @@ import { type AssetLibraryItem, } from "@/api/assets" import { tagAsset } from "@/api/tags" -import { type VoiceGender, type VoiceMaterial, buildMetadata } from "../../types" -import { getAudioDuration } from "../../utils/audio" +import { type VoiceGender, type VoiceMaterial, buildMetadata } from "../../../types" +import { getAudioDuration } from "../../../utils/audio" interface UseVoiceUploadOptions { voiceLibrary?: { id: string; kind: string } -- 2.54.0