feat(audiobook): enhance ProjectCard layout and improve status display with additional error handling

This commit is contained in:
2026-03-10 18:56:48 +08:00
parent 7d285e2ee1
commit c6ecfe668b

View File

@@ -547,52 +547,18 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
return ( return (
<div className="border rounded-lg p-4 space-y-3"> <div className="border rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-start gap-2 min-w-0 flex-1">
<Book className="h-4 w-4 shrink-0 text-muted-foreground" /> <Book className="h-4 w-4 shrink-0 text-muted-foreground mt-0.5" />
<span className="font-medium truncate">{project.title}</span> <span className="font-medium break-words">{project.title}</span>
<Badge variant={(STATUS_COLORS[status] || 'secondary') as any} className="shrink-0"> </div>
<div className="flex items-center gap-1 shrink-0">
<Badge variant={(STATUS_COLORS[status] || 'secondary') as any}>
{STATUS_LABELS[status] || status} {STATUS_LABELS[status] || status}
</Badge> </Badge>
</div> <Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => setExpanded(!expanded)}>
<div className="flex gap-1 shrink-0">
{!isActive && (
<div className="flex items-center gap-1">
<label className="flex items-center gap-1 text-xs text-muted-foreground cursor-pointer select-none">
<input
type="checkbox"
className="h-3 w-3"
checked={turbo}
onChange={e => setTurbo(e.target.checked)}
/>
</label>
<Button
size="sm"
variant={status === 'pending' ? 'default' : 'outline'}
onClick={handleAnalyze}
disabled={loadingAction}
>
{status === 'pending' ? '分析' : '重新分析'}
</Button>
</div>
)}
{status === 'ready' && (
<Button size="sm" onClick={() => handleGenerate()} disabled={loadingAction}>
</Button>
)}
{status === 'done' && (
<Button size="sm" variant="outline" onClick={() => handleDownload()} disabled={loadingAction}>
<Download className="h-3 w-3 mr-1" />
</Button>
)}
<Button size="icon" variant="ghost" onClick={() => setExpanded(!expanded)}>
{expanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />} {expanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button> </Button>
<Button size="icon" variant="ghost" onClick={handleDelete}>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div> </div>
</div> </div>
@@ -617,6 +583,46 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
</div> </div>
)} )}
<div className="flex items-center justify-between gap-2 pt-1 border-t">
<div className="flex items-center gap-1 flex-wrap">
{!isActive && (
<>
<label className="flex items-center gap-1 text-xs text-muted-foreground cursor-pointer select-none">
<input
type="checkbox"
className="h-3 w-3"
checked={turbo}
onChange={e => setTurbo(e.target.checked)}
/>
</label>
<Button
size="sm"
variant={status === 'pending' ? 'default' : 'outline'}
className="h-7 text-xs px-2"
onClick={handleAnalyze}
disabled={loadingAction}
>
{status === 'pending' ? '分析' : '重新分析'}
</Button>
</>
)}
{status === 'ready' && (
<Button size="sm" className="h-7 text-xs px-2" onClick={() => handleGenerate()} disabled={loadingAction}>
</Button>
)}
{status === 'done' && (
<Button size="sm" variant="outline" className="h-7 text-xs px-2" onClick={() => handleDownload()} disabled={loadingAction}>
<Download className="h-3 w-3 mr-1" />
</Button>
)}
</div>
<Button size="icon" variant="ghost" className="h-7 w-7 shrink-0" onClick={handleDelete}>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
{expanded && ( {expanded && (
<div className="space-y-3 pt-2 border-t"> <div className="space-y-3 pt-2 border-t">
{detail && detail.characters.length > 0 && ( {detail && detail.characters.length > 0 && (
@@ -661,9 +667,9 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex items-center justify-between text-sm"> <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between text-sm">
<span className="font-medium shrink-0 w-20 truncate">{char.name}</span> <span className="font-medium shrink-0 truncate">{char.name}</span>
<span className="text-xs text-muted-foreground truncate mx-2 flex-1">{char.instruct}</span> <span className="text-xs text-muted-foreground truncate sm:mx-2 sm:flex-1">{char.instruct}</span>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
{char.voice_design_id {char.voice_design_id
? <Badge variant="outline" className="text-xs"> #{char.voice_design_id}</Badge> ? <Badge variant="outline" className="text-xs"> #{char.voice_design_id}</Badge>
@@ -694,7 +700,7 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
{detail && detail.chapters.length > 0 && ['ready', 'generating', 'done'].includes(status) && ( {detail && detail.chapters.length > 0 && ['ready', 'generating', 'done'].includes(status) && (
<div className="rounded-lg border border-emerald-500/20 bg-emerald-500/5 px-3 py-2"> <div className="rounded-lg border border-emerald-500/20 bg-emerald-500/5 px-3 py-2">
<div className="flex items-center justify-between mb-2"> <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between mb-2">
<button <button
className="flex items-center gap-1 text-xs font-medium text-emerald-400/80 hover:text-emerald-300 transition-colors text-left" className="flex items-center gap-1 text-xs font-medium text-emerald-400/80 hover:text-emerald-300 transition-colors text-left"
onClick={() => setChaptersCollapsed(v => !v)} onClick={() => setChaptersCollapsed(v => !v)}
@@ -705,7 +711,7 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
{detail.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && ( {detail.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && (
<Button <Button
size="sm" size="sm"
className="h-6 text-xs px-2" className="h-6 text-xs px-2 self-start sm:self-auto"
disabled={loadingAction} disabled={loadingAction}
onClick={handleProcessAll} onClick={handleProcessAll}
> >
@@ -730,9 +736,9 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
}) })
return ( return (
<div key={ch.id} className="border rounded px-3 py-2 space-y-2"> <div key={ch.id} className="border rounded px-3 py-2 space-y-2">
<div className="flex items-center justify-between text-sm gap-2"> <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between text-sm">
<span className="text-xs font-medium truncate">{chTitle}</span> <span className="text-xs font-medium truncate">{chTitle}</span>
<div className="flex gap-1 items-center shrink-0"> <div className="flex gap-1 items-center flex-wrap shrink-0">
{ch.status === 'pending' && ( {ch.status === 'pending' && (
<Button size="sm" variant="outline" className="h-6 text-xs px-2" onClick={() => handleParseChapter(ch.id, ch.title)}> <Button size="sm" variant="outline" className="h-6 text-xs px-2" onClick={() => handleParseChapter(ch.id, ch.title)}>
@@ -841,9 +847,9 @@ export default function Audiobook() {
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
<Navbar /> <Navbar />
<main className="flex-1 container max-w-3xl mx-auto px-4 py-6 space-y-4"> <main className="flex-1 container max-w-3xl mx-auto px-4 py-6 space-y-4">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h1 className="text-2xl font-bold"></h1> <h1 className="text-xl sm:text-2xl font-bold"></h1>
<div className="flex gap-2"> <div className="flex gap-2 flex-wrap">
<Button size="sm" variant="outline" onClick={() => setShowLLM(!showLLM)}>LLM </Button> <Button size="sm" variant="outline" onClick={() => setShowLLM(!showLLM)}>LLM </Button>
<Button size="sm" onClick={() => setShowCreate(!showCreate)}> <Button size="sm" onClick={() => setShowCreate(!showCreate)}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />