import React, { useState, useEffect } from 'react'; import { Activity, AlertTriangle, Clock, Folders, CheckCircle2, Info, Loader2, RotateCcw, XCircle } from 'lucide-react'; import { progressWS, type ConnectionStatus, type ProgressMessage } from '../lib/websocket'; import { cn } from '../lib/utils'; import { cancelTask, getDashboardOverview, getTask, retryTask, type DashboardActivity, type DashboardOverview, type DashboardTask, type ProcessingTask, } from '../lib/api'; const emptySummary: DashboardOverview['summary'] = { project_count: 0, parsing_task_count: 0, annotation_count: 0, frame_count: 0, template_count: 0, system_load_percent: 0, }; export function Dashboard() { const [summary, setSummary] = useState(emptySummary); const [tasks, setTasks] = useState([]); const [isConnected, setIsConnected] = useState(false); const [activityLog, setActivityLog] = useState([]); const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(''); const [selectedTask, setSelectedTask] = useState(null); const [taskActionMessage, setTaskActionMessage] = useState(''); const [busyTaskId, setBusyTaskId] = useState(null); const taskFromProcessingTask = (task: ProcessingTask, name = `任务 ${task.id}`): DashboardTask => ({ id: `task-${task.id}`, task_id: task.id, project_id: task.project_id ?? 0, name, progress: task.progress, status: task.message || task.status, raw_status: task.status, error: task.error, frame_count: Number(task.result?.frames_extracted || 0), updated_at: task.updated_at, }); const prependActivity = (message: string, project = '系统') => { setActivityLog((prev) => [ { id: `task-action-${Date.now()}`, kind: 'task', time: new Date().toISOString(), message, project }, ...prev.slice(0, 9), ]); }; useEffect(() => { let cancelled = false; const loadOverview = () => { getDashboardOverview() .then((overview) => { if (cancelled) return; setSummary(overview.summary); setTasks((prev) => { if (prev.length === 0) return overview.tasks; const overviewIds = new Set(overview.tasks.map((task) => task.id)); const wsOnly = prev.filter((task) => !task.id.startsWith('task-') && !overviewIds.has(task.id) && task.progress < 100); return [...overview.tasks, ...wsOnly]; }); setActivityLog((prev) => { if (prev.length === 0) return overview.activity; const byId = new Map(prev.map((item) => [item.id, item])); overview.activity.forEach((item) => byId.set(item.id, item)); return Array.from(byId.values()).slice(0, 10); }); setLoadError(''); }) .catch((err) => { console.error('Failed to load dashboard overview:', err); if (!cancelled) setLoadError('Dashboard 数据加载失败'); }) .finally(() => { if (!cancelled) setIsLoading(false); }); }; loadOverview(); const overviewInterval = setInterval(loadOverview, 5000); return () => { cancelled = true; clearInterval(overviewInterval); }; }, []); useEffect(() => { let mounted = true; const taskTitle = (data: ProgressMessage) => data.filename || data.projectName || data.taskId || '后台任务'; const timer = setTimeout(() => { if (mounted) progressWS.connect(); }, 500); const unsubscribe = progressWS.onProgress((data: ProgressMessage) => { if (!mounted) return; setIsConnected(progressWS.isConnected()); if (data.type === 'progress' && data.taskId) { setTasks((prev) => { const exists = prev.find((t) => t.id === data.taskId); if (exists) { return prev.map((t) => t.id === data.taskId ? { ...t, progress: data.progress ?? t.progress, status: data.status ?? t.status } : t ); } return [ ...prev, { id: data.taskId!, project_id: data.project_id ?? Number(data.task_id || 0), name: taskTitle(data), progress: data.progress ?? 0, status: data.status ?? '处理中', raw_status: 'running', error: data.error, frame_count: 0, updated_at: new Date().toISOString(), }, ]; }); } if (data.type === 'complete' && data.taskId) { setTasks((prev) => prev.map((t) => t.id === data.taskId ? { ...t, progress: 100, status: '已完成', raw_status: 'success' } : t ) ); setActivityLog((prev) => [ { id: `ws-complete-${Date.now()}`, kind: 'websocket', time: new Date().toISOString(), message: data.message || `解析完成: ${taskTitle(data)}`, project: data.projectName || '系统' }, ...prev.slice(0, 9), ]); } if (data.type === 'cancelled' && data.taskId) { setTasks((prev) => prev.map((t) => t.id === data.taskId ? { ...t, progress: 100, status: data.message || '任务已取消', raw_status: 'cancelled', error: data.error } : t ) ); setActivityLog((prev) => [ { id: `ws-cancelled-${Date.now()}`, kind: 'websocket', time: new Date().toISOString(), message: data.message || `任务已取消: ${taskTitle(data)}`, project: data.projectName || '系统' }, ...prev.slice(0, 9), ]); } if (data.type === 'error' && data.taskId) { setTasks((prev) => prev.map((t) => t.id === data.taskId ? { ...t, progress: data.progress ?? t.progress, status: `错误: ${data.error || data.message || '未知错误'}`, raw_status: 'failed', error: data.error } : t ) ); setActivityLog((prev) => [ { id: `ws-error-${Date.now()}`, kind: 'websocket', time: new Date().toISOString(), message: data.message || `解析失败: ${taskTitle(data)}`, project: data.projectName || '系统' }, ...prev.slice(0, 9), ]); } if (data.type === 'status') { setActivityLog((prev) => [ { id: `ws-status-${Date.now()}`, kind: 'websocket', time: new Date().toISOString(), message: data.message || '状态更新', project: '系统' }, ...prev.slice(0, 9), ]); } }); const unsubscribeStatus = progressWS.onStatus((status: ConnectionStatus) => { if (mounted) setIsConnected(status === 'connected'); }); const checkConnection = setInterval(() => { if (mounted) setIsConnected(progressWS.isConnected()); }, 5000); return () => { mounted = false; unsubscribe(); unsubscribeStatus(); clearInterval(checkConnection); progressWS.disconnect(); }; }, []); const stats = [ { label: '项目总数', value: summary.project_count.toString(), icon: Folders, color: 'text-blue-400', bg: 'bg-blue-400/10' }, { label: '处理任务', value: summary.parsing_task_count.toString(), icon: Clock, color: 'text-orange-400', bg: 'bg-orange-400/10' }, { label: '已存标注', value: summary.annotation_count.toString(), icon: CheckCircle2, color: 'text-emerald-400', bg: 'bg-emerald-400/10' }, { label: '系统负载', value: `${summary.system_load_percent}%`, icon: Activity, color: 'text-cyan-400', bg: 'bg-cyan-400/10' }, ]; function formatActivityTime(value: string | null): string { if (!value) return '未知时间'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } const taskRawStatus = (task: DashboardTask): string => task.raw_status || ( task.status.includes('取消') ? 'cancelled' : task.status.includes('失败') || task.status.includes('错误') ? 'failed' : task.progress >= 100 ? 'success' : 'running' ); const canCancel = (task: DashboardTask): boolean => ['queued', 'running'].includes(taskRawStatus(task)) && Boolean(task.task_id); const canRetry = (task: DashboardTask): boolean => ['failed', 'cancelled'].includes(taskRawStatus(task)) && Boolean(task.task_id); const handleCancelTask = async (task: DashboardTask) => { if (!task.task_id) return; setBusyTaskId(task.id); setTaskActionMessage(''); try { const updated = await cancelTask(task.task_id); setTasks((prev) => prev.map((item) => ( item.id === task.id ? taskFromProcessingTask(updated, task.name) : item ))); prependActivity(`任务已取消 #${updated.id}`, task.name); } catch (err) { console.error('Cancel task failed:', err); setTaskActionMessage('任务取消失败,请检查后端服务'); } finally { setBusyTaskId(null); } }; const handleRetryTask = async (task: DashboardTask) => { if (!task.task_id) return; setBusyTaskId(task.id); setTaskActionMessage(''); try { const retried = await retryTask(task.task_id); const dashboardTask = taskFromProcessingTask(retried, task.name); setTasks((prev) => [dashboardTask, ...prev.filter((item) => item.id !== dashboardTask.id)]); prependActivity(`重试任务已入队 #${retried.id}`, task.name); } catch (err) { console.error('Retry task failed:', err); setTaskActionMessage('任务重试失败,请检查后端服务'); } finally { setBusyTaskId(null); } }; const handleOpenTaskDetail = async (task: DashboardTask) => { if (!task.task_id) return; setBusyTaskId(task.id); setTaskActionMessage(''); try { setSelectedTask(await getTask(task.task_id)); } catch (err) { console.error('Load task detail failed:', err); setTaskActionMessage('失败详情加载失败'); } finally { setBusyTaskId(null); } }; return (

系统整体概况

{isConnected ? 'WebSocket 已连接' : 'WebSocket 断开'}

系统全局数据吞吐状态与所有接入项目进度实时洞察驾驶舱。

{loadError &&

{loadError}

} {taskActionMessage &&

{taskActionMessage}

}
{stats.map((stat, i) => { const Icon = stat.icon; return (
{stat.value}
{stat.label}
); })}

任务进度 (当前 / 最近)

{isLoading && (
正在读取后端 Dashboard 数据...
)} {tasks.map((task) => (
{task.name} {task.progress}%
{taskRawStatus(task) === 'success' || task.status === '已完成' ? ( ) : taskRawStatus(task) === 'failed' ? ( ) : taskRawStatus(task) === 'cancelled' ? ( ) : ( )} {task.status} 帧: {task.frame_count}
{canCancel(task) && ( )} {canRetry(task) && ( )} {task.task_id && ( )}
))} {!isLoading && tasks.length === 0 && (
当前无处理任务;生成帧或传播任务开始后会在这里显示进度。
)}

近期实时流转记录

{isLoading && (
正在读取近期流转记录...
)} {activityLog.map((log) => (
{formatActivityTime(log.time)}
{log.message}
归属项目: {log.project}
))} {!isLoading && activityLog.length === 0 && (
暂无近期流转记录
)}
{selectedTask && (

任务详情 #{selectedTask.id}

{selectedTask.message || selectedTask.status}

状态: {selectedTask.status}
进度: {selectedTask.progress}%
项目 ID: {selectedTask.project_id ?? '-'}
Celery ID: {selectedTask.celery_task_id || '-'}
创建: {selectedTask.created_at}
结束: {selectedTask.finished_at || '-'}
{selectedTask.error && (
{selectedTask.error}
)}
                {JSON.stringify(selectedTask.payload || {}, null, 2)}
              
                {JSON.stringify(selectedTask.result || {}, null, 2)}
              
)}
); }