From 32bdaca391b83a5674414114b9dc11a8dc3ca7ce Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 7 Aug 2026 11:50:18 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E9=87=8D=E5=86=99=20Step5VoiceSele?= =?UTF-8?q?ct=EF=BC=8C=E4=BB=8E=E9=9F=B3=E8=89=B2=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=B1=95=E7=A4=BA=E7=94=A8=E6=88=B7=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E7=9A=84=E9=85=8D=E9=9F=B3=E7=B4=A0=E6=9D=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generate/components/Step5VoiceSelect.tsx | 344 ++++++++++++------ 1 file changed, 225 insertions(+), 119 deletions(-) diff --git a/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx b/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx index 1de7b049e..c974be0c3 100644 --- a/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx +++ b/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx @@ -1,137 +1,243 @@ /** * Step 5 配音选择组件 + * 展示用户已上传的配音素材,支持选中、预览播放 */ -import React from "react" -import type { VoiceClone } from "@/api/voice-clone" -import { useStep5Voice } from "../hooks/useStep5Voice" -import VoiceRecommendSection from "./voice/VoiceRecommendSection" -import VoiceChoiceCard from "./voice/VoiceChoiceCard" -import PresetVoiceDetail from "./voice/PresetVoiceDetail" -import CustomVoicePanel from "./voice/CustomVoicePanel" -import SaveVoiceModal from "./voice/SaveVoiceModal" -import CloneVoiceSection from "./voice/CloneVoiceSection" +import React, { useState, useRef, useCallback } from "react" +import { useQuery } from "@tanstack/react-query" +import { AudioOutlined, SoundOutlined } from "@ant-design/icons" +import { getAssetsByKind } from "@/api/assets" +import type { AssetItem } from "@/api/assets" interface Step5VoiceSelectProps { selectedVoice: string - onSelectedVoiceChange: (voiceId: string) => void - voiceMode: "preset" | "custom" | "clone" - onVoiceModeChange: (mode: "preset" | "custom" | "clone") => void - selectedClonedVoice: string - onSelectedClonedVoiceChange: (voiceId: string) => void - clonedVoices: VoiceClone[] - addClone: (voice: VoiceClone) => void - hasProcessing: boolean - cloneModalOpen: boolean - onCloneModalOpenChange: (open: boolean) => void - titleText: string + onSelectedVoiceChange: (id: string) => void } -const Step5VoiceSelect: React.FC = (props) => { - const v = useStep5Voice(props) +/** 格式化时长 mm:ss */ +const formatDuration = (seconds?: number): string => { + if (!seconds || seconds <= 0) return "00:00" + const m = Math.floor(seconds / 60) + const s = Math.floor(seconds % 60) + return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}` +} + +/** 格式化文件大小 */ +const formatFileSize = (bytes?: number): string => { + if (!bytes || bytes <= 0) return "未知" + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB` +} + +const Step5VoiceSelect: React.FC = ({ + selectedVoice, + onSelectedVoiceChange, +}) => { + const [playingId, setPlayingId] = useState(null) + const audioRef = useRef(null) + + // 获取用户上传的配音素材 + const { data: materials = [], isLoading } = useQuery({ + queryKey: ["assets", "voice"], + queryFn: () => getAssetsByKind("voice", { limit: 50 }), + }) + + /** 切换播放/暂停 */ + const togglePlay = useCallback( + (material: AssetItem) => { + // 如果当前正在播放同一个素材,则暂停 + if (playingId === material.id && audioRef.current) { + audioRef.current.pause() + setPlayingId(null) + return + } + + // 停止之前的播放 + if (audioRef.current) { + audioRef.current.pause() + } + + // 创建新的 audio 元素播放 + const audio = new Audio(material.file_url) + audioRef.current = audio + setPlayingId(material.id) + + audio.onended = () => { + setPlayingId(null) + audioRef.current = null + } + + audio.play().catch(() => { + setPlayingId(null) + audioRef.current = null + }) + }, + [playingId], + ) + + /** 选中素材 */ + const handleSelect = useCallback( + (id: string) => { + onSelectedVoiceChange(id) + }, + [onSelectedVoiceChange], + ) + + /** 跳转到配音库上传 */ + const handleGoToUpload = useCallback(() => { + window.location.href = "/voices" + }, []) + + // 加载中状态 + if (isLoading) { + return ( +
+

🎙️ 选择配音

+
+ 加载配音素材中... +
+
+ ) + } + + // 空状态 + if (materials.length === 0) { + return ( +
+

🎙️ 选择配音

+
+ +

暂无配音素材

+ +
+
+ ) + } return (

🎙️ 选择配音

+

+ 从配音库中选择已上传的素材,点击卡片可预览播放 +

+
+ {materials.map((item) => { + const isSelected = selectedVoice === item.id + const isPlaying = playingId === item.id - + return ( +
handleSelect(item.id)} + style={{ + padding: 16, + borderRadius: 8, + border: isSelected ? "2px solid #1677ff" : "1px solid #e8e8e8", + background: isSelected ? "#e6f4ff" : "#fff", + cursor: "pointer", + transition: "all 0.2s", + display: "flex", + flexDirection: "column", + gap: 8, + }} + > + {/* 顶部:图标 + 播放按钮 */} +
+
+ +
+ {item.file_url && ( + + )} +
-
- 全部音色 + {/* 名称 */} +
+ {item.name} +
+ + {/* 时长 + 大小 */} +
+ {formatDuration(item.duration)} + {formatFileSize(item.file_size)} +
+
+ ) + })}
- -
- {v.presetVoices.slice(0, 3).map((pv) => ( - v.handleSelectPresetVoice(pv.voice_id)} - avatar={v.VOICE_GENDER_ICON[pv.gender] ?? "✨"} - title={pv.name} - description={pv.description} - /> - ))} - {v.presetVoicesLoading && ( - {}} - avatar="⏳" - title="加载中…" - loading - /> - )} - -
- - {v.voiceMode === "preset" && ( - - )} - - {v.voiceMode === "custom" && ( - <> - - v.setSaveModalOpen(false)} - saveName={v.saveName} - onNameChange={v.setSaveName} - saveTagIds={v.saveTagIds} - onTagIdsChange={v.setSaveTagIds} - saveNewTag={v.saveNewTag} - onNewTagChange={v.setSaveNewTag} - onAddTag={v.handleAddTagInModal} - allTags={v.allTags} - savePending={v.saveToLibraryMutation.isPending} - onConfirm={v.handleConfirmSave} - /> - - )} - - {v.voiceMode === "clone" && ( - - )}
) } -- 2.54.0 From ef8c8c0dc6d3c28bf48ba38610b2e8cd7a86d98d Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 7 Aug 2026 11:50:18 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20Step5=20=E4=BB=85=E4=BC=A0=20select?= =?UTF-8?q?edVoice=20=E5=92=8C=20onSelectedVoiceChange=20=E7=BB=99=20Step5?= =?UTF-8?q?VoiceSelect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/generate/components/GenerateStepContent.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/web/src/pages/generate/components/GenerateStepContent.tsx b/apps/web/src/pages/generate/components/GenerateStepContent.tsx index 007b85b02..bef3f173b 100644 --- a/apps/web/src/pages/generate/components/GenerateStepContent.tsx +++ b/apps/web/src/pages/generate/components/GenerateStepContent.tsx @@ -170,16 +170,6 @@ export const GenerateStepContent: React.FC = (props) = ) case 6: -- 2.54.0 From 3079fe1aece623d42670522de35b4f2752c331a4 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 7 Aug 2026 11:50:19 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20voiceName=20=E4=BB=8E=E9=85=8D?= =?UTF-8?q?=E9=9F=B3=E7=B4=A0=E6=9D=90=E5=BA=93=E6=8C=89=20selectedVoice?= =?UTF-8?q?=20ID=20=E6=9F=A5=E6=89=BE=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/generate/hooks/useStep7Generate.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useStep7Generate.ts b/apps/web/src/pages/generate/hooks/useStep7Generate.ts index 560e0aefd..a7729ebd8 100644 --- a/apps/web/src/pages/generate/hooks/useStep7Generate.ts +++ b/apps/web/src/pages/generate/hooks/useStep7Generate.ts @@ -3,11 +3,13 @@ * 封装生成确认页的展示逻辑 */ import { useMemo } from "react" +import { useQuery } from "@tanstack/react-query" import type { EditingTemplate } from "@/api/editing-planner" import type { GeneratedVideo } from "@/api/template-editor" import type { CoverConfig } from "../../editing-planner/types" import type { VoiceClone } from "@/api/voice-clone" import type { PresetVoiceItem } from "@/api/voices" +import { getAssetsByKind } from "@/api/assets" import { COVER_MODE_LABELS } from "../constants" interface UseStep7GenerateProps { @@ -65,14 +67,16 @@ export function useStep7Generate({ return `${selectedMaterials.length} 个素材` }, [materialMode, selectedMaterials.length, smartSelectedIds.length]) + // 从配音素材库中查找 voiceName + const { data: voiceMaterials = [] } = useQuery({ + queryKey: ["assets", "voice"], + queryFn: () => getAssetsByKind("voice", { limit: 50 }), + }) + const voiceName = useMemo(() => { - if (voiceMode === "clone") { - const cv = clonedVoices.find((v) => v.id === selectedClonedVoice) - return cv ? cv.name : "未选择" - } - const pv = presetVoices.find((v) => v.voice_id === selectedVoice) - return pv ? pv.name : "未选择" - }, [voiceMode, selectedVoice, selectedClonedVoice, presetVoices, clonedVoices]) + const asset = voiceMaterials.find((v) => v.id === selectedVoice) + return asset ? asset.name : "未选择" + }, [voiceMaterials, selectedVoice]) const coverSummary = useMemo(() => { if (!coverSettings.enabled) return "不使用" -- 2.54.0 From 233c10a77d8f9df7ae4a8fcc0fe3c19f080ebfd6 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 7 Aug 2026 11:54:07 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=E8=A7=A3=E6=9E=84=E5=8F=98=E9=87=8F=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20ESLint=20no-unused-vars=20=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/generate/components/GenerateStepContent.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/web/src/pages/generate/components/GenerateStepContent.tsx b/apps/web/src/pages/generate/components/GenerateStepContent.tsx index bef3f173b..eb6863906 100644 --- a/apps/web/src/pages/generate/components/GenerateStepContent.tsx +++ b/apps/web/src/pages/generate/components/GenerateStepContent.tsx @@ -94,14 +94,8 @@ export const GenerateStepContent: React.FC = (props) = selectedVoice, onSelectedVoiceChange, voiceMode, - onVoiceModeChange, selectedClonedVoice, - onSelectedClonedVoiceChange, clonedVoices, - addClone, - hasProcessing, - cloneModalOpen, - onCloneModalOpenChange, generateCount, onGenerateCountChange, generating, -- 2.54.0 From db3b07547e582ba6199b88e5494e2f09ca2a5831 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 7 Aug 2026 11:54:08 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=B7=BB=E5=8A=A0=20=5F=20=E5=89=8D=E7=BC=80?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=20ESLint=20no-unused-vars=20?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/generate/hooks/useStep7Generate.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useStep7Generate.ts b/apps/web/src/pages/generate/hooks/useStep7Generate.ts index a7729ebd8..a3ef3ff99 100644 --- a/apps/web/src/pages/generate/hooks/useStep7Generate.ts +++ b/apps/web/src/pages/generate/hooks/useStep7Generate.ts @@ -41,11 +41,11 @@ export function useStep7Generate({ selectedMaterials, smartSelectedIds, title, - voiceMode, + voiceMode: _voiceMode, selectedVoice, - selectedClonedVoice, - presetVoices, - clonedVoices, + selectedClonedVoice: _selectedClonedVoice, + presetVoices: _presetVoices, + clonedVoices: _clonedVoices, coverSettings, generateCount, onGenerateCountChange, -- 2.54.0 From 7bf6e2f75c8ddb66fad5e0784879a23095f66337 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 7 Aug 2026 11:58:13 +0800 Subject: [PATCH 6/6] =?UTF-8?q?style:=20Prettier=20=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96=E4=BF=AE=E5=A4=8D=20Step5VoiceSelect.tsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generate/components/Step5VoiceSelect.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx b/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx index c974be0c3..ca27184b7 100644 --- a/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx +++ b/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx @@ -169,7 +169,9 @@ const Step5VoiceSelect: React.FC = ({ }} > {/* 顶部:图标 + 播放按钮 */} -
+
= ({ justifyContent: "center", }} > - +
{item.file_url && (
{/* 时长 + 大小 */} -
+
{formatDuration(item.duration)} {formatFileSize(item.file_size)}
-- 2.54.0