crush-level-web/src/app/(main)/character/[id]/chat/Sider/VoiceActor.tsx

171 lines
5.2 KiB
TypeScript
Raw Normal View History

2025-12-09 09:13:46 +00:00
'use client'
import { SiderHeader } from '.'
import { useChatStore } from '../store'
import { useState } from 'react'
import { Checkbox } from '@/components/ui/checkbox'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
type VoiceGender = 'all' | 'male' | 'female'
type VoiceActorItem = {
id: number
name: string
description: string
avatarUrl: string
gender: 'male' | 'female'
}
export default function VoiceActor() {
const setSideBar = useChatStore((store) => store.setSideBar)
// 语音演员列表(静态数据)
const voiceActors: VoiceActorItem[] = [
{
id: 1,
name: 'Voice Actor 1',
description: 'Have a role-playing conversation with AI',
avatarUrl: 'https://i.pravatar.cc/150?img=1',
gender: 'female',
},
{
id: 2,
name: 'Voice Actor 2',
description: 'Have a role-playing conversation with AI',
avatarUrl: 'https://i.pravatar.cc/150?img=2',
gender: 'female',
},
{
id: 3,
name: 'Voice Actor 3',
description: 'Have a role-playing conversation with AI',
avatarUrl: 'https://i.pravatar.cc/150?img=3',
gender: 'male',
},
{
id: 4,
name: 'Voice Actor 4',
description: 'Have a role-playing conversation with AI',
avatarUrl: 'https://i.pravatar.cc/150?img=4',
gender: 'female',
},
{
id: 5,
name: 'Voice Actor 5',
description: 'Have a role-playing conversation with AI',
avatarUrl: 'https://i.pravatar.cc/150?img=5',
gender: 'male',
},
{
id: 6,
name: 'Voice Actor 6',
description: 'Have a role-playing conversation with AI',
avatarUrl: 'https://i.pravatar.cc/150?img=6',
gender: 'female',
},
{
id: 7,
name: 'Voice Actor 7',
description: 'Have a role-playing conversation with AI',
avatarUrl: 'https://i.pravatar.cc/150?img=7',
gender: 'male',
},
]
const [selectedGender, setSelectedGender] = useState<VoiceGender>('all')
const [selectedActorId, setSelectedActorId] = useState(1)
const [loading, setLoading] = useState(false)
// 根据性别过滤演员列表
const filteredActors = voiceActors.filter((actor) => {
if (selectedGender === 'all') return true
return actor.gender === selectedGender
})
const handleConfirm = async () => {
setLoading(true)
try {
// TODO: 调用实际的 API 保存语音演员设置
// await updateVoiceActor({ voiceActorId: selectedActorId })
console.log('Selected voice actor:', selectedActorId)
// 模拟延迟
await new Promise((resolve) => setTimeout(resolve, 500))
setSideBar('profile')
} catch (error) {
console.error(error)
} finally {
setLoading(false)
}
}
return (
<div className="flex h-full flex-col">
<SiderHeader title="Voice Artist" />
{/* Gender Tabs */}
<div className="mb-6 flex gap-6">
{[
{ value: 'all' as const, label: 'All' },
{ value: 'male' as const, label: 'Male' },
{ value: 'female' as const, label: 'Female' },
].map((tab) => (
<button
key={tab.value}
className={cn(
'txt-title-s relative pb-2 transition-colors',
selectedGender === tab.value ? 'text-txt-primary-normal' : 'text-txt-secondary-normal'
)}
onClick={() => setSelectedGender(tab.value)}
>
{tab.label}
{selectedGender === tab.value && (
<div className="bg-primary-normal absolute bottom-0 left-0 right-0 h-0.5 rounded-full" />
)}
</button>
))}
</div>
{/* Voice Actor List */}
<div className="flex-1 overflow-y-auto">
<div className="flex flex-col gap-3">
{filteredActors.map((actor) => (
<div
key={actor.id}
className={cn(
'bg-surface-element-normal flex cursor-pointer items-center gap-3 rounded-lg p-4 transition-colors',
selectedActorId === actor.id && 'bg-surface-element-hover'
)}
onClick={() => setSelectedActorId(actor.id)}
>
<Avatar className="h-14 w-14">
<AvatarImage src={actor.avatarUrl} alt={actor.name} />
<AvatarFallback>{actor.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="txt-title-s text-txt-primary-normal">{actor.name}</div>
<div className="txt-body-s text-txt-secondary-normal mt-1">{actor.description}</div>
</div>
<Checkbox shape="round" checked={selectedActorId === actor.id} />
</div>
))}
</div>
</div>
{/* Footer Buttons */}
<div className="mt-6 flex justify-end gap-3">
<Button variant="tertiary" size="large" onClick={() => setSideBar('profile')}>
Cancel
</Button>
<Button size="large" variant="primary" loading={loading} onClick={handleConfirm}>
Select
</Button>
</div>
</div>
)
}