feat(audiobook): enhance LogStream component and add bulk processing for chapter tasks
This commit is contained in:
@@ -142,7 +142,7 @@ function SequentialPlayer({
|
|||||||
function LogStream({ projectId, active }: { projectId: number; active: boolean }) {
|
function LogStream({ projectId, active }: { projectId: number; active: boolean }) {
|
||||||
const [lines, setLines] = useState<string[]>([])
|
const [lines, setLines] = useState<string[]>([])
|
||||||
const [done, setDone] = useState(false)
|
const [done, setDone] = useState(false)
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const activeRef = useRef(active)
|
const activeRef = useRef(active)
|
||||||
activeRef.current = active
|
activeRef.current = active
|
||||||
|
|
||||||
@@ -192,20 +192,20 @@ function LogStream({ projectId, active }: { projectId: number; active: boolean }
|
|||||||
}, [projectId, active])
|
}, [projectId, active])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
const el = containerRef.current
|
||||||
|
if (el) el.scrollTop = el.scrollHeight
|
||||||
}, [lines])
|
}, [lines])
|
||||||
|
|
||||||
if (lines.length === 0) return null
|
if (lines.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded border border-green-900/40 bg-black/90 text-green-400 font-mono text-xs p-3 max-h-52 overflow-y-auto leading-relaxed">
|
<div ref={containerRef} className="rounded border border-green-900/40 bg-black/90 text-green-400 font-mono text-xs p-3 max-h-52 overflow-y-auto leading-relaxed">
|
||||||
{lines.map((line, i) => (
|
{lines.map((line, i) => (
|
||||||
<div key={i} className="whitespace-pre-wrap">{line}</div>
|
<div key={i} className="whitespace-pre-wrap">{line}</div>
|
||||||
))}
|
))}
|
||||||
{!done && (
|
{!done && (
|
||||||
<span className="inline-block w-2 h-3 bg-green-400 animate-pulse ml-0.5 align-middle" />
|
<span className="inline-block w-2 h-3 bg-green-400 animate-pulse ml-0.5 align-middle" />
|
||||||
)}
|
)}
|
||||||
<div ref={bottomRef} />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -429,6 +429,29 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleProcessAll = async () => {
|
||||||
|
if (!detail) return
|
||||||
|
setLoadingAction(true)
|
||||||
|
setIsPolling(true)
|
||||||
|
try {
|
||||||
|
const pending = detail.chapters.filter(c => c.status === 'pending' || c.status === 'error')
|
||||||
|
const ready = detail.chapters.filter(c => c.status === 'ready')
|
||||||
|
await Promise.all([
|
||||||
|
...pending.map(c => audiobookApi.parseChapter(project.id, c.id)),
|
||||||
|
...ready.map(c => audiobookApi.generate(project.id, c.chapter_index)),
|
||||||
|
])
|
||||||
|
toast.success('全部任务已触发')
|
||||||
|
onRefresh()
|
||||||
|
fetchDetail()
|
||||||
|
fetchSegments()
|
||||||
|
} catch (e: any) {
|
||||||
|
setIsPolling(false)
|
||||||
|
toast.error(formatApiError(e))
|
||||||
|
} finally {
|
||||||
|
setLoadingAction(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDownload = async (chapterIndex?: number) => {
|
const handleDownload = async (chapterIndex?: number) => {
|
||||||
setLoadingAction(true)
|
setLoadingAction(true)
|
||||||
try {
|
try {
|
||||||
@@ -622,9 +645,21 @@ 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>
|
<div>
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
章节列表(共 {detail.chapters.length} 章)
|
章节列表(共 {detail.chapters.length} 章)
|
||||||
</div>
|
</div>
|
||||||
|
{detail.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="h-6 text-xs px-2"
|
||||||
|
disabled={loadingAction}
|
||||||
|
onClick={handleProcessAll}
|
||||||
|
>
|
||||||
|
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : '一键全部处理'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{detail.chapters.map(ch => {
|
{detail.chapters.map(ch => {
|
||||||
const chSegs = segments.filter(s => s.chapter_index === ch.chapter_index)
|
const chSegs = segments.filter(s => s.chapter_index === ch.chapter_index)
|
||||||
|
|||||||
Reference in New Issue
Block a user