/** * 随机混剪 — 配音选择弹窗(#1970) * 内容复用 Step5VoiceSelect 的配音库音频卡片(图标+文件名+时长/大小+▶试听), * 无 TTS / 克隆音色入口;确认后进入 Step2。 */ import React from "react" import { Modal } from "antd" import { AudioOutlined } from "@ant-design/icons" import { useNavigate } from "react-router-dom" import { useQuery } from "@tanstack/react-query" import { useState, useRef, useCallback } from "react" import { getAssetsByKind } from "@/api/assets" import type { AssetItem } from "@/api/assets" interface VoiceSelectModalProps { open: boolean selectedVoice: string onCancel: () => void onConfirm: (voiceAssetId: string) => void } const getDuration = (item: AssetItem): number => item.duration ?? (item.metadata?.duration as number) ?? 0 const getFileSize = (item: AssetItem): number => item.file_size ?? (item.metadata?.file_size as number) ?? 0 const isAiVoice = (item: AssetItem): boolean => { const d = getDuration(item) const s = getFileSize(item) return (!d || d <= 0) && (!s || s <= 0) } const fmtDur = (s?: number): string => { if (!s || s <= 0) return "时长未知" return `${s.toFixed(1)}秒` } const fmtSize = (b?: number): string => { if (!b || b <= 0) return "未知" if (b < 1024) return `${b} B` if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB` if (b < 1024 * 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(1)} MB` return `${(b / (1024 * 1024 * 1024)).toFixed(1)} GB` } const VoiceSelectModal: React.FC = ({ open, selectedVoice, onCancel, onConfirm, }) => { const navigate = useNavigate() const [innerSelected, setInnerSelected] = React.useState(selectedVoice) const [playingId, setPlayingId] = useState(null) const audioRef = useRef(null) React.useEffect(() => { if (open) setInnerSelected(selectedVoice) }, [open, selectedVoice]) const { data: materials = [], isLoading } = useQuery({ queryKey: ["assets", "voice", "modal"], queryFn: () => getAssetsByKind("voice", { limit: 50 }), enabled: open, }) const togglePlay = useCallback( (item: AssetItem) => { if (playingId === item.id && audioRef.current) { audioRef.current.pause() setPlayingId(null) return } if (audioRef.current) audioRef.current.pause() if (!item.file_url) return const audio = new Audio(item.file_url) audioRef.current = audio setPlayingId(item.id) audio.onended = () => { setPlayingId(null) audioRef.current = null } audio.play().catch(() => { setPlayingId(null) audioRef.current = null }) }, [playingId], ) const handleGoUpload = () => navigate("/app/voices?tab=material&upload=1") const handleConfirm = () => { if (!innerSelected) return onConfirm(innerSelected) } return (

从配音库中选择已上传的音频素材,点击 ▶ 可试听

{isLoading ? (
加载中...
) : materials.length === 0 ? (

暂无配音素材

) : (
{materials.map((item) => { const isSel = innerSelected === item.id const isPlaying = playingId === item.id return (
setInnerSelected(item.id)} style={{ padding: 14, borderRadius: 8, border: isSel ? "2px solid #7c3aed" : "1px solid #e8e8e8", background: isSel ? "#ede9fe" : "#fff", cursor: "pointer", transition: "all 0.2s", }} >
{item.file_url && ( )}
{item.name}
{isAiVoice(item) ? ( AI 音色 ) : ( {fmtDur(getDuration(item))} )} {isAiVoice(item) ? "按文本合成" : fmtSize(getFileSize(item))}
) })}
)}
) } export default VoiceSelectModal