feat: add edit character dialog with localization support

This commit is contained in:
2026-04-07 10:50:29 +08:00
parent d170ba3362
commit 96b2eaf774
6 changed files with 71 additions and 62 deletions

View File

@@ -84,6 +84,7 @@
"descPlaceholder": "Character description",
"voiceDesign": "Voice #{{id}}",
"noVoice": "Unassigned",
"editTitle": "Edit Character: {{name}}",
"savedSuccess": "Character saved"
},

View File

@@ -83,6 +83,7 @@
"descPlaceholder": "キャラクター説明",
"voiceDesign": "音声 #{{id}}",
"noVoice": "未割り当て",
"editTitle": "キャラクターを編集:{{name}}",
"savedSuccess": "キャラクターを保存しました"
},

View File

@@ -83,6 +83,7 @@
"descPlaceholder": "캐릭터 설명",
"voiceDesign": "음성 #{{id}}",
"noVoice": "미할당",
"editTitle": "캐릭터 편집: {{name}}",
"savedSuccess": "캐릭터가 저장되었습니다"
},

View File

@@ -84,6 +84,7 @@
"descPlaceholder": "角色描述",
"voiceDesign": "音色 #{{id}}",
"noVoice": "未分配",
"editTitle": "编辑角色:{{name}}",
"savedSuccess": "角色已保存",
"regeneratingPreview": "重新生成试听中...",
"regeneratePreview": "重生试听",

View File

@@ -83,6 +83,7 @@
"descPlaceholder": "角色描述",
"voiceDesign": "音色 #{{id}}",
"noVoice": "未分配",
"editTitle": "編輯角色:{{name}}",
"savedSuccess": "角色已儲存"
},

View File

@@ -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')