feat: update status handling in Audiobook component with enhanced visual indicators

This commit is contained in:
2026-03-13 17:27:37 +08:00
parent dbfcff3476
commit 6c91c24e49

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Book, BookOpen, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2, Zap, Settings2, PanelLeftClose, PanelLeftOpen, Wand2, Volume2, Bot, Flame, Headphones } from 'lucide-react'
import { Book, BookOpen, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2, Zap, Settings2, PanelLeftClose, PanelLeftOpen, Wand2, Volume2, Bot, Flame, Headphones, Clock, CheckCircle2, AlertCircle, CircleDot } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
@@ -32,16 +32,16 @@ function LazyAudioPlayer({ audioUrl, jobId, compact }: { audioUrl: string; jobId
return <div ref={ref}>{visible && <AudioPlayer audioUrl={audioUrl} jobId={jobId} compact={compact} />}</div>
}
const STATUS_COLORS: Record<string, string> = {
pending: 'secondary',
analyzing: 'default',
characters_ready: 'default',
parsing: 'default',
processing: 'default',
ready: 'default',
generating: 'default',
done: 'outline',
error: 'destructive',
const STATUS_CONFIG: Record<string, { Icon: React.ElementType; iconCls: string; cardBg: string; cardBgSel: string }> = {
pending: { Icon: Clock, iconCls: 'h-3 w-3 text-muted-foreground', cardBg: 'bg-background/50', cardBgSel: 'bg-muted' },
analyzing: { Icon: Loader2, iconCls: 'h-3 w-3 text-blue-500 animate-spin', cardBg: 'bg-blue-500/5', cardBgSel: 'bg-blue-500/20' },
characters_ready: { Icon: Loader2, iconCls: 'h-3 w-3 text-blue-500 animate-spin', cardBg: 'bg-blue-500/5', cardBgSel: 'bg-blue-500/20' },
parsing: { Icon: Loader2, iconCls: 'h-3 w-3 text-amber-500 animate-spin', cardBg: 'bg-amber-500/5', cardBgSel: 'bg-amber-500/20' },
processing: { Icon: Loader2, iconCls: 'h-3 w-3 text-amber-500 animate-spin', cardBg: 'bg-amber-500/5', cardBgSel: 'bg-amber-500/20' },
ready: { Icon: CircleDot, iconCls: 'h-3 w-3 text-green-500', cardBg: 'bg-green-500/5', cardBgSel: 'bg-green-500/20' },
generating: { Icon: Loader2, iconCls: 'h-3 w-3 text-amber-500 animate-spin', cardBg: 'bg-amber-500/5', cardBgSel: 'bg-amber-500/20' },
done: { Icon: CheckCircle2, iconCls: 'h-3 w-3 text-emerald-500', cardBg: 'bg-emerald-500/5', cardBgSel: 'bg-emerald-500/20' },
error: { Icon: AlertCircle, iconCls: 'h-3 w-3 text-destructive', cardBg: 'bg-destructive/5', cardBgSel: 'bg-destructive/20' },
}
const STEP_HINT_STATUSES = ['pending', 'analyzing', 'characters_ready', 'ready', 'generating']
@@ -1073,34 +1073,23 @@ function ProjectListSidebar({
? Wand2
: Book
const iconClass = isNsfw
? 'h-3.5 w-3.5 shrink-0 mt-0.5 text-orange-500'
? 'h-3.5 w-3.5 shrink-0 text-orange-500'
: p.source_type === 'ai_generated'
? 'h-3.5 w-3.5 shrink-0 mt-0.5 text-violet-500'
: 'h-3.5 w-3.5 shrink-0 mt-0.5 text-muted-foreground'
const showProgress = p.segment_total > 0 && ['processing', 'generating', 'done'].includes(p.status)
? 'h-3.5 w-3.5 shrink-0 text-violet-500'
: 'h-3.5 w-3.5 shrink-0 text-muted-foreground'
return (
<button
key={p.id}
onClick={() => onSelect(p.id)}
className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ${
className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ${
selectedId === p.id
? 'bg-background border-border shadow-sm'
: 'bg-background/50 border-border/40'
? `${STATUS_CONFIG[p.status]?.cardBgSel ?? 'bg-background'} border-border shadow-sm`
: `${STATUS_CONFIG[p.status]?.cardBg ?? 'bg-background/50'} border-border/40 hover:opacity-90`
}`}
>
<div className="flex items-start gap-2">
<div className="flex items-center gap-2">
<ProjectIcon className={iconClass} />
<span className="text-sm font-medium leading-snug break-words min-w-0 flex-1">{p.title}</span>
</div>
<div className="flex items-center gap-1.5 ml-5">
<Badge variant={(STATUS_COLORS[p.status] || 'secondary') as any} className="text-[10px] h-4 px-1">
{t(`status.${p.status}`, { defaultValue: p.status })}
</Badge>
{showProgress && (
<span className="text-[10px] text-muted-foreground tabular-nums">
{p.segment_done}/{p.segment_total}
</span>
)}
<span className="text-sm font-medium truncate flex-1 text-center" title={p.title}>{p.title}</span>
</div>
</button>
)
@@ -1194,12 +1183,6 @@ function CharactersPanel({
}
}
const genderLabel = (gender: string) => {
if (gender === '男') return t('projectCard.characters.genderMale')
if (gender === '女') return t('projectCard.characters.genderFemale')
if (gender === '未知') return t('projectCard.characters.genderUnknown')
return gender
}
const charCount = detail?.characters.length ?? 0
const hasChaptersOutline = (detail?.chapters.length ?? 0) > 0
@@ -1313,12 +1296,7 @@ function CharactersPanel({
<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.name}</span>
{char.gender && (
<Badge variant="outline" className={`text-xs shrink-0 ${char.gender === '男' ? 'border-blue-400/50 text-blue-400' : char.gender === '女' ? 'border-pink-400/50 text-pink-400' : 'border-muted-foreground/40 text-muted-foreground'}`}>
{genderLabel(char.gender)}
</Badge>
)}
<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 && (
@@ -2320,15 +2298,19 @@ export default function Audiobook() {
<EmptyState />
) : (
<>
<div className="shrink-0 border-b px-4 py-2 flex items-start justify-between gap-2">
<div className="flex items-start gap-2 min-w-0 flex-1">
<div className="shrink-0 border-b px-4 py-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0 flex-1">
{(() => {
const isNsfw = selectedProject.source_type === 'ai_generated' && !!(selectedProject.script_config as any)?.nsfw_mode
const Icon = selectedProject.source_type === 'epub' ? BookOpen : isNsfw ? Flame : selectedProject.source_type === 'ai_generated' ? Wand2 : Book
const cls = isNsfw ? 'h-4 w-4 shrink-0 mt-0.5 text-orange-500' : selectedProject.source_type === 'ai_generated' ? 'h-4 w-4 shrink-0 mt-0.5 text-violet-500' : 'h-4 w-4 shrink-0 mt-0.5 text-muted-foreground'
const cls = isNsfw ? 'h-4 w-4 shrink-0 text-orange-500' : selectedProject.source_type === 'ai_generated' ? 'h-4 w-4 shrink-0 text-violet-500' : 'h-4 w-4 shrink-0 text-muted-foreground'
return <Icon className={cls} />
})()}
<span className="font-medium break-words">{selectedProject.title}</span>
<span className="font-medium break-words min-w-0">{selectedProject.title}</span>
{(() => {
const sc = STATUS_CONFIG[displayStatus]
return sc ? <sc.Icon className={`${sc.iconCls} shrink-0`} /> : null
})()}
</div>
<div className="flex items-center gap-1 shrink-0 flex-wrap justify-end">
{isTurboMode && (
@@ -2336,22 +2318,19 @@ export default function Audiobook() {
{t('status.turboActive')}
</Badge>
)}
<Badge variant={(STATUS_COLORS[displayStatus] || 'secondary') as any}>
{t(`status.${displayStatus}`, { defaultValue: displayStatus })}
</Badge>
{status === 'pending' && (
<Button size="sm" onClick={handleAnalyze} disabled={loadingAction}>
<Button size="sm" variant="ghost" onClick={handleAnalyze} disabled={loadingAction}>
{t('projectCard.analyze')}
</Button>
)}
{status === 'ready' && (
<Button size="sm" onClick={handleProcessAll} disabled={loadingAction}>
<Button size="sm" variant="ghost" onClick={handleProcessAll} disabled={loadingAction}>
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : null}
{t('projectCard.processAll')}
</Button>
)}
{status === 'done' && (
<Button size="sm" variant="outline" onClick={() => handleDownload()} disabled={loadingAction}>
<Button size="sm" variant="ghost" onClick={() => handleDownload()} disabled={loadingAction}>
<Download className="h-3 w-3" />{t('projectCard.downloadAll')}
</Button>
)}