Compare commits

...

4 Commits

Author SHA1 Message Date
xiaoxia 3ec1fb4129 fix(useVoiceMaterialActions): 修复主Hook的types相对路径层级 2026-07-28 12:53:09 +08:00
xiaoxia ab9b7409b2 fix(useVoiceMaterialActions): 修复子 Hook 相对路径层级错误
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 8s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 43s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m11s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m12s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m7s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m47s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 36s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m37s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m34s
AI Code Review / AI Code Review (pull_request) Successful in 3m57s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 5m57s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 24m49s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 32s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 51s
子 Hook 位于 actions/ 子目录,引用 types/utils 需要上三级(../../../)
而非上两级(../../),导致 TS 编译失败。
2026-07-28 12:16:56 +08:00
xiaoxia 3eef40bbf8 fix(useVoiceMaterialActions): fix prettier formatting
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 27s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m40s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 59s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 42s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m7s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 47s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m16s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 41s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 5m22s
AI Code Review / AI Code Review (pull_request) Failing after 5m49s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 26s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 46m16s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-28 10:19:32 +08:00
xiaoxia 18f99ba32b refactor(voice-materials): 拆分 useVoiceMaterialActions Hook
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m27s
三阶段重构:
- Phase 1: 拆分为 3 个领域子 Hook
  - useVoiceUpload: 上传流程(库→上传→时长→创建→标签)
  - useVoiceEdit: 编辑流程(更新信息+标签差异同步)
  - useVoiceDelete: 删除操作
- Phase 2: 主 Hook 组合子 Hook + UI 弹窗状态
- Phase 3: 主文件 216→80 行(-63%)

保持导出签名向后兼容
2026-07-28 10:14:24 +08:00
5 changed files with 268 additions and 174 deletions
@@ -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,
}
}
@@ -0,0 +1,78 @@
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<VoiceMaterial, "id" | "createdAt"> & { 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,
}
}
@@ -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<AssetLibraryItem>; isPending: boolean }
}
/**
* 配音素材上传 Hook
* 封装上传流程:获取库 → 上传文件 → 获取时长 → 创建记录 → 打标签
*/
export function useVoiceUpload({ voiceLibrary, createLibMutation }: UseVoiceUploadOptions) {
const queryClient = useQueryClient()
const [uploadProgress, setUploadProgress] = useState<number | null>(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<VoiceMaterial, "id" | "createdAt"> & { 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,
}
}
@@ -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<VoiceMaterial | null>(null)
// ── 上传进度 ──────────────────────────────────────────────
const [uploadProgress, setUploadProgress] = useState<number | null>(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<VoiceMaterial, "id" | "createdAt"> & { 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<VoiceMaterial, "id" | "createdAt"> & { file?: File }) => {
if (!editingMaterial) return
editMutation.mutate({
id: editingMaterial.id,
const handleUpload = (data: Omit<VoiceMaterial, "id" | "createdAt"> & { 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<VoiceMaterial, "id" | "createdAt"> & { 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,
+3
View File
@@ -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"