diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/index.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/index.ts new file mode 100644 index 000000000..fe29409a7 --- /dev/null +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/index.ts @@ -0,0 +1,107 @@ +import { useMemo } from "react" +import type { TagItem } from "@/api/tags" +import { useVoiceTags } from "./useVoiceTags" +import { useVoiceMaterialFilterState } from "./useVoiceMaterialFilterState" +import { useVoiceMaterialData } from "./useVoiceMaterialData" +import { useVoiceMaterialActions } from "./useVoiceMaterialActions" +import { type VoiceMaterial } from "../../types" + +/** + * 配音素材数据 Hook + * 封装素材列表查询、筛选状态管理、增删改等数据操作逻辑 + */ +export function useVoiceMaterials() { + // 标签管理 + const { tags, tagMap, handleCreateTag } = useVoiceTags() + + // 筛选视图状态 + const filterState = useVoiceMaterialFilterState() + const { searchText, filterGender, filterTagId } = filterState + + // 后端查询参数 + const keyword = searchText.trim() || undefined + const gender = filterGender !== "all" ? filterGender : undefined + const tagIds = filterTagId !== "all" ? [filterTagId] : undefined + + // 数据查询 + const { libraries, voiceLibrary, materials, isLoading, createLibMutation } = useVoiceMaterialData( + { keyword, gender, tagIds }, + ) + + // 操作层 + const actions = useVoiceMaterialActions({ + voiceLibrary, + materials, + createLibMutation, + }) + + // 前端二次筛选(与后端筛选同时存在,保证即时响应) + const filtered = useMemo(() => { + let list: VoiceMaterial[] = materials + if (filterGender !== "all") { + list = list.filter((m) => m.gender === filterGender) + } + if (filterTagId !== "all") { + list = list.filter((m) => m.tagIds.includes(filterTagId)) + } + if (searchText.trim()) { + const q = searchText.trim().toLowerCase() + list = list.filter( + (m) => + m.name.toLowerCase().includes(q) || + m.description.toLowerCase().includes(q) || + m.tagIds.some((id) => + (tagMap as Map).get(id)?.name?.toLowerCase().includes(q), + ), + ) + } + return list + }, [materials, filterGender, filterTagId, searchText, tagMap]) + + // 标签使用计数 + const tagCountMap = useMemo(() => { + const map: Record = {} + materials.forEach((m) => + m.tagIds.forEach((id) => { + map[id] = (map[id] || 0) + 1 + }), + ) + return map + }, [materials]) + + return { + // 数据 + libraries, + voiceLibrary, + tags, + tagMap, + materials, + filtered, + tagCountMap, + isLoading, + // 视图 & 筛选状态 + viewMode: filterState.viewMode, + searchText: filterState.searchText, + filterGender: filterState.filterGender, + filterTagId: filterState.filterTagId, + // 上传 & 编辑状态 + uploadProgress: actions.uploadProgress, + isUploading: actions.isUploading, + isEditing: actions.isEditing, + // 弹窗状态 + uploadOpen: actions.uploadOpen, + editingMaterial: actions.editingMaterial, + // 视图控制 + setViewMode: filterState.setViewMode, + setSearchText: filterState.setSearchText, + setFilterGender: filterState.setFilterGender, + setFilterTagId: filterState.setFilterTagId, + setUploadOpen: actions.setUploadOpen, + setEditingMaterial: actions.setEditingMaterial, + // 操作 + handleCreateTag, + handleUpload: actions.handleUpload, + handleEdit: actions.handleEdit, + handleDelete: actions.handleDelete, + } +} diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions.ts similarity index 53% rename from apps/web/src/pages/voice-materials/hooks/useVoiceMaterials.ts rename to apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions.ts index 0066b1b86..f59b2e114 100644 --- a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials.ts +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions.ts @@ -1,117 +1,35 @@ -import { useState, useMemo, useCallback, useEffect } from "react" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { useState, useCallback } from "react" +import { useMutation, useQueryClient } from "@tanstack/react-query" import { message } from "antd" import { - getAssetsByKind, createAsset, updateAsset, deleteAsset, uploadAssetDirect, getAssetLibraries, - createAssetLibrary, + type AssetLibraryItem, } from "@/api/assets" -import { type TagItem, getTags, createTag, tagAsset, untagAsset } from "@/api/tags" -import { - type VoiceGender, - type ViewMode, - type VoiceMaterial, - mapAssetToMaterial, - buildMetadata, -} from "../types" -import { getAudioDuration } from "../utils/audio" +import { tagAsset, untagAsset } from "@/api/tags" +import { type VoiceGender, type VoiceMaterial, buildMetadata } from "../../types" +import { getAudioDuration } from "../../utils/audio" + +interface UseVoiceMaterialActionsOptions { + voiceLibrary?: { id: string; kind: string } + materials: VoiceMaterial[] + createLibMutation: { mutateAsync: () => Promise; isPending: boolean } +} /** - * 配音素材数据 Hook - * 封装素材列表查询、筛选状态管理、增删改等数据操作逻辑 + * 配音素材操作 Hook + * 封装上传、编辑、删除等变更操作及相关 UI 状态 */ -export function useVoiceMaterials() { +export function useVoiceMaterialActions({ + voiceLibrary, + materials, + createLibMutation, +}: UseVoiceMaterialActionsOptions) { const queryClient = useQueryClient() - // ── 获取 voice 类型素材库(用于上传) ────────────────────── - const { data: libraries = [] } = useQuery({ - queryKey: ["asset-libraries"], - queryFn: getAssetLibraries, - staleTime: 60_000, - }) - - const voiceLibrary = useMemo(() => libraries.find((lib) => lib.kind === "voice"), [libraries]) - - // 自动创建 voice 素材库(如果不存在) - const createLibMutation = useMutation({ - mutationFn: () => createAssetLibrary({ name: "配音库", kind: "voice" }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["asset-libraries"] }) - }, - }) - - useEffect(() => { - if (libraries.length > 0 && !voiceLibrary && !createLibMutation.isPending) { - createLibMutation.mutate() - } - }, [libraries, voiceLibrary, createLibMutation]) - - // ── 获取标签列表 ─────────────────────────────────────────── - const { data: tags = [] } = useQuery({ - queryKey: ["tags"], - queryFn: getTags, - staleTime: 60_000, - }) - - /** 标签 ID → TagItem 映射(用于卡片/行渲染) */ - const tagMap = useMemo(() => { - const m = new Map() - tags.forEach((t) => m.set(t.id, t)) - return m - }, [tags]) - - /** 创建标签 mutation(供 TagSelector 调用) */ - const createTagMutation = useMutation({ - mutationFn: (name: string) => createTag(name), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["tags"] }) - }, - }) - - /** 创建标签并返回 TagItem(供 TagSelector 使用) */ - const handleCreateTag = useCallback( - async (name: string): Promise => { - return createTagMutation.mutateAsync(name) - }, - [createTagMutation], - ) - - // ── 视图 & 筛选状态 ──────────────────────────────────────── - const [viewMode, setViewMode] = useState("card") - const [searchText, setSearchText] = useState("") - const [filterGender, setFilterGender] = useState("all") - const [filterTagId, setFilterTagId] = useState("all") - - // ── 获取配音素材列表(筛选参数透传后端) ───────────────── - const filterKeyword = searchText.trim() || undefined - const filterGenderParam = filterGender !== "all" ? filterGender : undefined - const filterTagIdsParam = filterTagId !== "all" ? [filterTagId] : undefined - - const { data: assets = [], isLoading } = useQuery({ - queryKey: [ - "assets", - "voice", - { - keyword: filterKeyword, - gender: filterGenderParam, - tag_ids: filterTagIdsParam, - }, - ], - queryFn: () => - getAssetsByKind("voice", { - keyword: filterKeyword, - gender: filterGenderParam, - tag_ids: filterTagIdsParam, - }), - staleTime: 30_000, - }) - - const materials: VoiceMaterial[] = useMemo(() => assets.map(mapAssetToMaterial), [assets]) - // ── 弹窗状态 ────────────────────────────────────────────── const [uploadOpen, setUploadOpen] = useState(false) const [editingMaterial, setEditingMaterial] = useState(null) @@ -140,7 +58,7 @@ export function useVoiceMaterials() { queryKey: ["asset-libraries"], queryFn: getAssetLibraries, }) - lib = libs.find((l) => l.kind === "voice") + lib = libs.find((l: AssetLibraryItem) => l.kind === "voice") if (!lib) throw new Error("无法创建配音库") } @@ -231,40 +149,6 @@ export function useVoiceMaterials() { }, }) - /* ── 前端二次筛选(与后端筛选同时存在) ──────────────────── */ - - const filtered = useMemo(() => { - let list = materials - if (filterGender !== "all") { - list = list.filter((m) => m.gender === filterGender) - } - if (filterTagId !== "all") { - list = list.filter((m) => m.tagIds.includes(filterTagId)) - } - if (searchText.trim()) { - const q = searchText.trim().toLowerCase() - list = list.filter( - (m) => - m.name.toLowerCase().includes(q) || - m.description.toLowerCase().includes(q) || - m.tagIds.some((id) => tagMap.get(id)?.name?.toLowerCase().includes(q)), - ) - } - return list - }, [materials, filterGender, filterTagId, searchText, tagMap]) - - /* ── 标签使用计数(药丸条展示,按 tag ID 统计) ──────────── */ - - const tagCountMap = useMemo(() => { - const map: Record = {} - materials.forEach((m) => - m.tagIds.forEach((id) => { - map[id] = (map[id] || 0) + 1 - }), - ) - return map - }, [materials]) - /* ── 数据操作 handlers ──────────────────────────────────── */ const handleUpload = useCallback( @@ -314,20 +198,6 @@ export function useVoiceMaterials() { ) return { - // 数据 - libraries, - voiceLibrary, - tags, - tagMap, - materials, - filtered, - tagCountMap, - isLoading, - // 视图 & 筛选状态 - viewMode, - searchText, - filterGender, - filterTagId, // 上传 & 编辑状态 uploadProgress, isUploading: uploadMutation.isPending, @@ -336,16 +206,11 @@ export function useVoiceMaterials() { uploadOpen, editingMaterial, // 视图控制 - setViewMode, - setSearchText, - setFilterGender, - setFilterTagId, setUploadOpen, setEditingMaterial, // 操作 - handleCreateTag, handleUpload, handleEdit, handleDelete, } -} +} \ No newline at end of file diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialData.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialData.ts new file mode 100644 index 000000000..27951d090 --- /dev/null +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialData.ts @@ -0,0 +1,58 @@ +import { useMemo, useEffect } from "react" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { getAssetsByKind, getAssetLibraries, createAssetLibrary } from "@/api/assets" +import { type VoiceMaterial, mapAssetToMaterial } from "../../types" + +interface UseVoiceMaterialDataOptions { + keyword?: string + gender?: string + tagIds?: string[] +} + +/** + * 配音素材数据查询 Hook + * 封装素材库获取、自动创建 voice 库、素材列表查询 + */ +export function useVoiceMaterialData({ keyword, gender, tagIds }: UseVoiceMaterialDataOptions) { + const queryClient = useQueryClient() + + // ── 获取 voice 类型素材库 ───────────────────────────────── + const { data: libraries = [] } = useQuery({ + queryKey: ["asset-libraries"], + queryFn: getAssetLibraries, + staleTime: 60_000, + }) + + const voiceLibrary = useMemo(() => libraries.find((lib) => lib.kind === "voice"), [libraries]) + + // 自动创建 voice 素材库(如果不存在) + const createLibMutation = useMutation({ + mutationFn: () => createAssetLibrary({ name: "配音库", kind: "voice" }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["asset-libraries"] }) + }, + }) + + useEffect(() => { + if (libraries.length > 0 && !voiceLibrary && !createLibMutation.isPending) { + createLibMutation.mutate() + } + }, [libraries, voiceLibrary, createLibMutation]) + + // ── 获取配音素材列表 ───────────────────────────────────── + const { data: assets = [], isLoading } = useQuery({ + queryKey: ["assets", "voice", { keyword, gender, tag_ids: tagIds }], + queryFn: () => getAssetsByKind("voice", { keyword, gender, tag_ids: tagIds }), + staleTime: 30_000, + }) + + const materials: VoiceMaterial[] = useMemo(() => assets.map(mapAssetToMaterial), [assets]) + + return { + libraries, + voiceLibrary, + materials, + isLoading, + createLibMutation, + } +} diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialFilterState.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialFilterState.ts new file mode 100644 index 000000000..470292a5f --- /dev/null +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialFilterState.ts @@ -0,0 +1,24 @@ +import { useState } from "react" +import { type ViewMode } from "../../types" + +/** + * 配音素材筛选视图状态 Hook + * 管理视图模式、搜索、性别/标签筛选的状态 + */ +export function useVoiceMaterialFilterState() { + const [viewMode, setViewMode] = useState("card") + const [searchText, setSearchText] = useState("") + const [filterGender, setFilterGender] = useState("all") + const [filterTagId, setFilterTagId] = useState("all") + + return { + viewMode, + searchText, + filterGender, + filterTagId, + setViewMode, + setSearchText, + setFilterGender, + setFilterTagId, + } +} diff --git a/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceTags.ts b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceTags.ts new file mode 100644 index 000000000..9c0cb29a2 --- /dev/null +++ b/apps/web/src/pages/voice-materials/hooks/useVoiceMaterials/useVoiceTags.ts @@ -0,0 +1,44 @@ +import { useMemo, useCallback } from "react" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { type TagItem, getTags, createTag } from "@/api/tags" + +/** + * 配音素材标签管理 Hook + * 封装标签列表查询、标签映射、创建标签等逻辑 + */ +export function useVoiceTags() { + const queryClient = useQueryClient() + + const { data: tags = [] } = useQuery({ + queryKey: ["tags"], + queryFn: getTags, + staleTime: 60_000, + }) + + /** 标签 ID → TagItem 映射(用于卡片/行渲染) */ + const tagMap = useMemo(() => { + const m = new Map() + tags.forEach((t) => m.set(t.id, t)) + return m + }, [tags]) + + const createTagMutation = useMutation({ + mutationFn: (name: string) => createTag(name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["tags"] }) + }, + }) + + const handleCreateTag = useCallback( + async (name: string): Promise => { + return createTagMutation.mutateAsync(name) + }, + [createTagMutation], + ) + + return { + tags, + tagMap, + handleCreateTag, + } +}