'use client' import { logError as _ulogError } from '@/lib/logging/core' import { useState, useRef, useEffect } from 'react' import { createPortal } from 'react-dom' import { useTranslations } from 'next-intl' import { useGlobalVoices } from '@/lib/query/hooks' import TaskStatusInline from '@/components/task/TaskStatusInline' import { resolveTaskPresentationState } from '@/lib/task/presentation' import { AppIcon } from '@/components/ui/icons' interface Voice { id: string name: string description: string | null voiceId: string | null voiceType: string customVoiceUrl: string | null voicePrompt: string | null gender: string | null language: string folderId: string | null } interface VoicePickerDialogProps { isOpen: boolean onClose: () => void onSelect: (voice: Voice) => void } export default function VoicePickerDialog({ isOpen, onClose, onSelect }: VoicePickerDialogProps) { const t = useTranslations('assetHub') const tv = useTranslations('voice.voiceDesign') const voicesQuery = useGlobalVoices() const [selectedVoice, setSelectedVoice] = useState(null) const [playingId, setPlayingId] = useState(null) const audioRef = useRef(null) const voices = (voicesQuery.data || []) as Voice[] const loading = isOpen ? voicesQuery.isFetching : false const loadingState = loading ? resolveTaskPresentationState({ phase: 'processing', intent: 'process', resource: 'audio', hasOutput: false, }) : null const refetchVoices = voicesQuery.refetch useEffect(() => { if (!isOpen) return refetchVoices().catch((error) => { _ulogError('加载音色失败:', error) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]) // 播放预览 const handlePlay = (voice: Voice) => { if (!voice.customVoiceUrl) return if (playingId === voice.id && audioRef.current) { audioRef.current.pause() setPlayingId(null) return } if (audioRef.current) { audioRef.current.pause() } const audio = new Audio(voice.customVoiceUrl) audioRef.current = audio audio.onended = () => setPlayingId(null) audio.onerror = () => setPlayingId(null) audio.play() setPlayingId(voice.id) } // 确认选择 const handleConfirm = () => { if (selectedVoice) { onSelect(selectedVoice) onClose() } } // 关闭时清理 const handleClose = () => { if (audioRef.current) { audioRef.current.pause() } setSelectedVoice(null) setPlayingId(null) onClose() } if (!isOpen) return null if (typeof document === 'undefined') return null const dialogContent = ( <> {/* 背景遮罩 */}
{/* 对话框 */}
e.stopPropagation()} > {/* 头部 */}

{t('voicePickerTitle')}

{/* 内容区 */}
{loading ? (
) : voices.length === 0 ? (

{t('voicePickerEmpty')}

) : (
{voices.map(voice => { const isSelected = selectedVoice?.id === voice.id const isPlaying = playingId === voice.id const genderIcon = voice.gender === 'male' ? 'M' : voice.gender === 'female' ? 'F' : '' return (
setSelectedVoice(voice)} className={`relative p-4 rounded-xl border-2 cursor-pointer transition-all ${isSelected ? 'border-[var(--glass-stroke-focus)] bg-[var(--glass-tone-info-bg)]' : 'border-[var(--glass-stroke-base)] hover:border-[var(--glass-stroke-focus)] bg-[var(--glass-bg-surface)]' }`} > {/* 选中标记 */} {isSelected && (
)} {/* 音色信息 */}
{voice.name} {genderIcon && {genderIcon}}
{voice.description && (

{voice.description}

)}
{/* 试听按钮 */} {voice.customVoiceUrl && ( )}
) })}
)}
{/* 底部操作 */}
) return createPortal(dialogContent, document.body) }