import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Search, Volume2, CheckCircle, Loader2, RefreshCw, ChevronDown, ChevronUp, } from 'lucide-react'; import { SystemVoice, SystemVoiceType, SystemVoiceQuery, VoiceSelectorProps, VOICE_TYPE_LABELS, GENDER_LABELS, GENDER_ICONS } from '../types/systemVoice'; import { SystemVoiceService } from '../services/systemVoiceService'; import { useNotifications } from './NotificationSystem'; /** * 系统音色选择器组件 * 支持搜索、筛选、分类显示系统音色 */ const SystemVoiceSelector: React.FC = ({ selectedVoiceId, onVoiceSelect, showSearch = true, showTypeFilter = true, showGenderFilter = true, className = '', disabled = false }) => { const { addNotification } = useNotifications(); // ============= 状态管理 ============= const [voices, setVoices] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [expanded, setExpanded] = useState(true); // 筛选和搜索状态 const [filters, setFilters] = useState({ keyword: '', voice_type: '', gender: '', language: '' }); // ============= 数据加载 ============= const loadSystemVoices = useCallback(async () => { console.log('🎵 开始加载系统音色...'); setLoading(true); setError(null); try { const systemVoices = await SystemVoiceService.getAllSystemVoices(); console.log('🎵 成功加载系统音色:', systemVoices.length, '个音色'); console.log('🎵 音色详情:', systemVoices); console.log('🎵 第一个音色示例:', systemVoices[0]); console.log('🎵 音色类型:', typeof systemVoices, Array.isArray(systemVoices)); setVoices(systemVoices); } catch (err) { console.error('❌ 加载系统音色失败:', err); const errorMessage = err instanceof Error ? err.message : '加载系统音色失败'; setError(errorMessage); addNotification({ type: 'error', title: '加载失败', message: errorMessage }); } finally { setLoading(false); } }, [addNotification]); // ============= 筛选逻辑 ============= const filteredVoices = useMemo(() => { console.log('🔍 开始筛选音色,原始数据:', voices.length, '个'); console.log('🔍 筛选条件:', filters); let filtered = SystemVoiceService.filterVoices(voices, filters); console.log('🔍 筛选后数据:', filtered.length, '个'); let sorted = SystemVoiceService.sortVoices(filtered, 'order'); console.log('🔍 排序后数据:', sorted.length, '个'); return sorted; }, [voices, filters]); // 按类型分组 const groupedVoices = useMemo(() => { console.log('📊 开始分组,筛选后数据:', filteredVoices.length, '个'); const grouped = SystemVoiceService.groupVoicesByType(filteredVoices); console.log('📊 分组结果:', grouped); console.log('📊 各组数量:', Object.entries(grouped).map(([type, voices]) => `${type}: ${voices.length}`)); return grouped; }, [filteredVoices]); // ============= 事件处理 ============= const handleVoiceSelect = useCallback((voice: SystemVoice) => { if (disabled) return; onVoiceSelect(voice.voice_id, voice); }, [onVoiceSelect, disabled]); const handleFilterChange = useCallback((key: keyof SystemVoiceQuery, value: string) => { setFilters(prev => ({ ...prev, [key]: value })); }, []); const clearFilters = useCallback(() => { setFilters({ keyword: '', voice_type: '', gender: '', language: '' }); }, []); // ============= 初始化 ============= useEffect(() => { loadSystemVoices(); }, [loadSystemVoices]); // ============= 渲染函数 ============= const getTypeBadgeClassName = (voiceType: SystemVoiceType) => { const baseClass = 'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium'; switch (voiceType) { case 'system': return baseClass + ' bg-blue-100 text-blue-800'; case 'premium': return baseClass + ' bg-purple-100 text-purple-800'; case 'child': return baseClass + ' bg-pink-100 text-pink-800'; case 'character': return baseClass + ' bg-green-100 text-green-800'; case 'holiday': return baseClass + ' bg-red-100 text-red-800'; case 'english': return baseClass + ' bg-orange-100 text-orange-800'; default: return baseClass + ' bg-gray-100 text-gray-800'; } }; const renderVoiceCard = (voice: SystemVoice) => { const isSelected = selectedVoiceId === voice.voice_id; const genderIcon = GENDER_ICONS[voice.gender] || '🎭'; // 使用固定的CSS类名而不是动态生成 const getCardClassName = () => { let baseClass = 'p-3 border rounded-lg cursor-pointer transition-all duration-200'; if (disabled) { baseClass += ' opacity-50 cursor-not-allowed'; } if (isSelected) { switch (voice.voice_type) { case 'system': return baseClass + ' border-blue-500 bg-blue-50 shadow-md'; case 'premium': return baseClass + ' border-purple-500 bg-purple-50 shadow-md'; case 'child': return baseClass + ' border-pink-500 bg-pink-50 shadow-md'; case 'character': return baseClass + ' border-green-500 bg-green-50 shadow-md'; case 'holiday': return baseClass + ' border-red-500 bg-red-50 shadow-md'; case 'english': return baseClass + ' border-orange-500 bg-orange-50 shadow-md'; default: return baseClass + ' border-gray-500 bg-gray-50 shadow-md'; } } else { return baseClass + ' border-gray-200 hover:border-gray-300 hover:shadow-sm'; } }; return (
handleVoiceSelect(voice)} >
{genderIcon}

{voice.voice_name}

{isSelected && ( )}
{voice.voice_name_en && (

{voice.voice_name_en}

)} {voice.description && (

{voice.description}

)}
{VOICE_TYPE_LABELS[voice.voice_type]} {GENDER_LABELS[voice.gender]}
); }; const renderTypeGroup = (type: SystemVoiceType, voices: SystemVoice[]) => { if (voices.length === 0) return null; const typeLabel = VOICE_TYPE_LABELS[type]; const getTypeGroupClassName = () => { switch (type) { case 'system': return 'flex items-center gap-2 mb-3 pb-2 border-b border-blue-200'; case 'premium': return 'flex items-center gap-2 mb-3 pb-2 border-b border-purple-200'; case 'child': return 'flex items-center gap-2 mb-3 pb-2 border-b border-pink-200'; case 'character': return 'flex items-center gap-2 mb-3 pb-2 border-b border-green-200'; case 'holiday': return 'flex items-center gap-2 mb-3 pb-2 border-b border-red-200'; case 'english': return 'flex items-center gap-2 mb-3 pb-2 border-b border-orange-200'; default: return 'flex items-center gap-2 mb-3 pb-2 border-b border-gray-200'; } }; const getTypeDotClassName = () => { switch (type) { case 'system': return 'w-3 h-3 rounded-full bg-blue-500'; case 'premium': return 'w-3 h-3 rounded-full bg-purple-500'; case 'child': return 'w-3 h-3 rounded-full bg-pink-500'; case 'character': return 'w-3 h-3 rounded-full bg-green-500'; case 'holiday': return 'w-3 h-3 rounded-full bg-red-500'; case 'english': return 'w-3 h-3 rounded-full bg-orange-500'; default: return 'w-3 h-3 rounded-full bg-gray-500'; } }; return (

{typeLabel}

({voices.length})
{voices.map(renderVoiceCard)}
); }; return (
{/* 头部 */}

系统音色

({filteredVoices.length}/{voices.length})
{/* 筛选器 */} {expanded && (showSearch || showTypeFilter || showGenderFilter) && (
{/* 搜索框 */} {showSearch && (
handleFilterChange('keyword', e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
)} {/* 筛选选项 */} {(showTypeFilter || showGenderFilter) && (
{showTypeFilter && ( )} {showGenderFilter && ( )} {(filters.keyword || filters.voice_type || filters.gender) && ( )}
)}
)}
{/* 内容区域 */} {expanded && (
{loading ? (
加载中...
) : error ? (
加载失败
{error}
) : filteredVoices.length === 0 ? (

没有找到匹配的音色

请尝试调整筛选条件

) : (
{Object.entries(groupedVoices).map(([type, voices]) => renderTypeGroup(type as SystemVoiceType, voices) )}
)}
)}
); }; export default SystemVoiceSelector;