/** * 后处理状态面板 — 通用可内嵌组件 * * 展示后处理流水线各步骤的成功/失败状态, * 并提供单步重试和全部重试入口。 * * 使用场景: * - 草稿箱章节卡片(定稿后处理状态) * - 故事架构页(角色卡提取状态) */ import { useEffect, useState, useCallback } from 'react' import { RefreshCw, CheckCircle2, XCircle, AlertTriangle, Clock, ChevronDown, ChevronUp } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '../ui/Button' import { useProjectStore } from '../../stores/project-store' import { readPostProcessStatus, type PostProcessStatus } from '../../services/workflows/workflow-utils' import { cn } from '../../lib/utils' import { globalEventBus } from '../../shared/event-bus' interface PostProcessStatusPanelProps { /** 状态文件 scope 标识,如 'chapter_1_finalize' */ scope: string /** 重试回调(传 stepKey 则单步重试,不传则全部重试) */ onRetry?: (stepKey?: string) => void /** 是否默认展开(默认 false,折叠显示摘要) */ defaultExpanded?: boolean /** 加载状态后的回调,通知父组件是否有失败项 */ onStatusLoad?: (hasFailure: boolean) => void /** 额外 CSS 类名 */ className?: string } export function PostProcessStatusPanel({ scope, onRetry, defaultExpanded = false, onStatusLoad, className, }: PostProcessStatusPanelProps) { const { t } = useTranslation('common') const [status, setStatus] = useState(null) const [expanded, setExpanded] = useState(defaultExpanded) const [loading, setLoading] = useState(true) const project = useProjectStore(s => s.currentProject) // 加载状态文件 const loadStatus = useCallback(async () => { if (!project) return const s = await readPostProcessStatus(project.path, scope) setStatus(s) setLoading(false) }, [project, scope]) // Initial load useEffect(() => { let mounted = true const init = async () => { if (!project) return setLoading(true) const s = await readPostProcessStatus(project.path, scope) if (mounted) { setStatus(s) setLoading(false) } } init() return () => { mounted = false } }, [project, scope]) // 监听 EventBus 事件,自动刷新后处理状态 useEffect(() => { const unsub1 = globalEventBus.on('FINALIZE_COMPLETE', () => { loadStatus() }) const unsub2 = globalEventBus.on('WORKFLOW_COMPLETE', () => { loadStatus() }) return () => { unsub1(); unsub2() } }, [loadStatus]) // 状态变化时回调给父组件 useEffect(() => { if (!status) return const isFailed = Object.values(status.steps).some(s => !s.ok) if (onStatusLoad) { onStatusLoad(isFailed) } }, [status, onStatusLoad]) // 无状态文件 → 不渲染 if (loading || !status) return null const steps = Object.entries(status.steps) const failedSteps = steps.filter(([, s]) => !s.ok) const successCount = steps.filter(([, s]) => s.ok).length const totalCount = steps.length const hasFailure = failedSteps.length > 0 const hasCriticalFailure = failedSteps.some(([, s]) => s.critical) if (!hasFailure) { return (
{status.sourceLabel} {t('completed')}({successCount}/{totalCount})
) } // 有失败 → 显示带折叠的详情面板 return (
{/* 折叠头部 */} {/* 展开详情 */} {expanded && (
{steps.map(([key, step]) => (
{step.ok ? ( ) : ( )} {step.label} {step.critical && !step.ok && ( {t('completed')} )}
{step.ok ? ( {step.completedAt ? new Date(step.completedAt).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) : ''} ) : ( <> {step.error || t('failed')} {onRetry && ( )} )}
))} {/* 底部操作栏 */}
{t('lastAttempt')} {new Date(status.updatedAt).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
{onRetry && ( )}
)}
) }