diff --git a/apps/web/src/api/assets.ts b/apps/web/src/api/assets.ts index 8d1f04450..8cb2aa49b 100755 --- a/apps/web/src/api/assets.ts +++ b/apps/web/src/api/assets.ts @@ -184,13 +184,19 @@ export const getAssetsByKind = async ( gender?: string style?: string tag_ids?: string[] + limit?: number + page?: number + page_size?: number }, ): Promise => { - const params: Record = { kind } + const params: Record = { kind } if (filters?.keyword) params.keyword = filters.keyword if (filters?.gender) params.gender = filters.gender if (filters?.style) params.style = filters.style if (filters?.tag_ids?.length) params.tag_ids = filters.tag_ids.join(",") + if (filters?.limit) params.limit = filters.limit + if (filters?.page) params.page = filters.page + if (filters?.page_size) params.page_size = filters.page_size const response = await apiClient.get("/assets", { params }) return response.data.items || [] } diff --git a/apps/web/src/pages/voices/VoiceLibrary.tsx b/apps/web/src/pages/voices/VoiceLibrary.tsx old mode 100644 new mode 100755 index bea1a7105..7e3ae0242 --- a/apps/web/src/pages/voices/VoiceLibrary.tsx +++ b/apps/web/src/pages/voices/VoiceLibrary.tsx @@ -36,7 +36,13 @@ import { type VoiceClone, } from "@/api/voiceClone" import { synthesizeSpeech, getTTSJobStatus, saveTtsToLibrary } from "@/api/tts" -import { uploadAssetDirect, getAssetLibraries, createAsset } from "@/api/assets" +import { + getAssetsByKind, + type AssetItem, + uploadAssetDirect, + getAssetLibraries, + createAsset, +} from "@/api/assets" import CloneModal from "@/components/voice/CloneModal" import "./voices.css" @@ -45,7 +51,7 @@ import "./voices.css" * ============================================================ */ type VoiceGender = "male" | "female" | "child" | "elderly" type VoiceLanguage = "zh" | "en" | "ja" | "ko" -type TabKey = "preset" | "cloned" +type TabKey = "preset" | "cloned" | "material" /** 前端展示用的预置音色(从 PresetVoiceItem 映射) */ interface PresetVoiceDisplay { @@ -798,6 +804,12 @@ const VoiceLibrary: React.FC = () => { queryFn: () => getVoiceClonesWithTotal({ limit: 50 }), }) + /** 配音素材列表(用户上传音频) */ + const { data: materialData, isLoading: materialLoading } = useQuery({ + queryKey: ["voice-materials"], + queryFn: () => getAssetsByKind("voice", { limit: 50 }), + }) + /** 统一统计(preset_count / clone_count) */ const { data: unifiedStats } = useQuery({ queryKey: ["voices-unified"], @@ -814,6 +826,7 @@ const VoiceLibrary: React.FC = () => { ) const presetCount = unifiedStats?.preset_count ?? presetData?.total ?? 0 const cloneCount = unifiedStats?.clone_count ?? cloneData?.total ?? 0 + const materialCount = materialData?.length ?? 0 const filteredPreset = useMemo(() => { let list = presetVoices @@ -984,6 +997,17 @@ const VoiceLibrary: React.FC = () => { 我的克隆 {cloneCount} + {activeTab === "preset" && ( @@ -1133,6 +1157,72 @@ const VoiceLibrary: React.FC = () => { )} + {activeTab === "material" && ( +
+ {/* 骨架屏加载 */} + {materialLoading && ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+
+
+
+ ))} +
+ )} + + {/* 卡片列表 */} + {!materialLoading && (materialData?.length || 0) > 0 && ( +
+ {(materialData || []).map((asset: AssetItem) => { + const duration = (asset.metadata?.duration as number) || 0 + const minutes = Math.floor(duration / 60) + const seconds = Math.floor(duration % 60) + return ( +
+
+ + + {minutes}:{seconds.toString().padStart(2, "0")} + +
+
+
+ {asset.name} +
+
+ + {asset.file_size + ? `${(asset.file_size / 1024 / 1024).toFixed(1)} MB` + : "--"} + +
+
+
+ ) + })} +
+ )} + + {/* 空状态 */} + {!materialLoading && (materialData?.length || 0) === 0 && ( +
+
+ +
+

暂无配音素材

+

上传您的音频素材,用于视频配音

+ +
+ )} +
+ )} + {/* 克隆音色弹窗 */}