feat: add edit character dialog with localization support
This commit is contained in:
@@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Navbar } from '@/components/Navbar'
|
||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||
import { ChapterPlayer } from '@/components/ChapterPlayer'
|
||||
@@ -1245,6 +1245,65 @@ function CharactersPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={editingCharId !== null} onOpenChange={open => { if (!open) setEditingCharId(null) }}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('projectCard.characters.editTitle', { name: detail?.characters.find(c => c.id === editingCharId)?.name ?? '' })}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">{t('projectCard.characters.namePlaceholder')}</label>
|
||||
<Input
|
||||
value={editFields.name}
|
||||
onChange={e => setEditFields(f => ({ ...f, name: e.target.value }))}
|
||||
placeholder={t('projectCard.characters.namePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">{t('projectCard.characters.genderPlaceholder')}</label>
|
||||
<select
|
||||
className="w-full h-9 rounded-md border border-input bg-background px-3 py-1 text-sm"
|
||||
value={editFields.gender}
|
||||
onChange={e => setEditFields(f => ({ ...f, gender: e.target.value }))}
|
||||
>
|
||||
<option value="">{t('projectCard.characters.genderPlaceholder')}</option>
|
||||
<option value="男">{t('projectCard.characters.genderMale')}</option>
|
||||
<option value="女">{t('projectCard.characters.genderFemale')}</option>
|
||||
<option value="未知">{t('projectCard.characters.genderUnknown')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">{t('projectCard.characters.instructPlaceholder')}</label>
|
||||
<Textarea
|
||||
value={editFields.instruct}
|
||||
onChange={e => setEditFields(f => ({ ...f, instruct: e.target.value }))}
|
||||
placeholder={t('projectCard.characters.instructPlaceholder')}
|
||||
rows={4}
|
||||
className="resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">{t('projectCard.characters.descPlaceholder')}</label>
|
||||
<Input
|
||||
value={editFields.description}
|
||||
onChange={e => setEditFields(f => ({ ...f, description: e.target.value }))}
|
||||
placeholder={t('projectCard.characters.descPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setEditingCharId(null)}>{t('common:cancel')}</Button>
|
||||
{editingCharId !== null && detail && (() => {
|
||||
const editingChar = detail.characters.find(c => c.id === editingCharId)
|
||||
return editingChar ? (
|
||||
<Button onClick={() => saveEditChar(editingChar)}>
|
||||
<Check className="h-3 w-3 mr-1" />{t('common:save')}
|
||||
</Button>
|
||||
) : null
|
||||
})()}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{(!detail || charCount === 0) ? (
|
||||
<div className="flex-1 flex items-center justify-center text-xs text-muted-foreground px-3 text-center">
|
||||
{status === 'pending' ? t('stepHints.pending') : t('projectCard.characters.title', { count: 0 })}
|
||||
@@ -1253,65 +1312,13 @@ function CharactersPanel({
|
||||
<div className="flex-1 overflow-y-auto px-2 py-2 space-y-1.5">
|
||||
{detail.characters.map(char => (
|
||||
<div key={char.id} className="border rounded-lg px-3 py-2 bg-muted/30 shadow-sm">
|
||||
{editingCharId === char.id ? (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={editFields.name}
|
||||
onChange={e => setEditFields(f => ({ ...f, name: e.target.value }))}
|
||||
placeholder={t('projectCard.characters.namePlaceholder')}
|
||||
/>
|
||||
<select
|
||||
className="w-full h-9 rounded-md border border-input bg-background px-3 py-1 text-sm"
|
||||
value={editFields.gender}
|
||||
onChange={e => setEditFields(f => ({ ...f, gender: e.target.value }))}
|
||||
>
|
||||
<option value="">{t('projectCard.characters.genderPlaceholder')}</option>
|
||||
<option value="男">{t('projectCard.characters.genderMale')}</option>
|
||||
<option value="女">{t('projectCard.characters.genderFemale')}</option>
|
||||
<option value="未知">{t('projectCard.characters.genderUnknown')}</option>
|
||||
</select>
|
||||
<Input
|
||||
value={editFields.instruct}
|
||||
onChange={e => setEditFields(f => ({ ...f, instruct: e.target.value }))}
|
||||
placeholder={t('projectCard.characters.instructPlaceholder')}
|
||||
/>
|
||||
<Input
|
||||
value={editFields.description}
|
||||
onChange={e => setEditFields(f => ({ ...f, description: e.target.value }))}
|
||||
placeholder={t('projectCard.characters.descPlaceholder')}
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editFields.use_indextts2}
|
||||
onChange={e => setEditFields(f => ({ ...f, use_indextts2: e.target.checked }))}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<Zap className="h-3 w-3 text-amber-400" />
|
||||
<span>使用 IndexTTS2(需要音色克隆参考音频)</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => saveEditChar(char)}>
|
||||
<Check className="h-3 w-3 mr-1" />{t('common:save')}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => setEditingCharId(null)}>
|
||||
<X className="h-3 w-3 mr-1" />{t('common:cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1 text-sm">
|
||||
<div className="flex flex-col gap-1 text-sm">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span className={`font-medium truncate ${char.gender === '男' ? 'text-blue-400' : char.gender === '女' ? 'text-pink-400' : ''}`}>{char.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{char.use_indextts2 && (
|
||||
<Badge variant="outline" className="text-xs border-amber-400/50 text-amber-400">
|
||||
<Zap className="h-2.5 w-2.5 mr-0.5" />IndexTTS2
|
||||
</Badge>
|
||||
)}
|
||||
{status === 'characters_ready' && (
|
||||
{status === 'characters_ready' && (
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => startEditChar(char)}>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -1320,13 +1327,10 @@ function CharactersPanel({
|
||||
</div>
|
||||
{char.description && <span className="text-xs text-muted-foreground">{char.description}</span>}
|
||||
{char.instruct && <span className="text-xs text-muted-foreground/70">{char.instruct}</span>}
|
||||
<div className="text-xs text-muted-foreground/60">
|
||||
{char.voice_design_id
|
||||
? (char.voice_design_name || `#${char.voice_design_id}`)
|
||||
: t('projectCard.characters.noVoice')}
|
||||
</div>
|
||||
{!char.voice_design_id && (
|
||||
<div className="text-xs text-muted-foreground/60">{t('projectCard.characters.noVoice')}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!editingCharId && char.voice_design_id && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -1363,7 +1367,7 @@ function CharactersPanel({
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
disabled={loadingAction || editingCharId !== null}
|
||||
disabled={loadingAction}
|
||||
>
|
||||
{loadingAction
|
||||
? t('projectCard.confirm.loading')
|
||||
|
||||
Reference in New Issue
Block a user