186 lines
8.6 KiB
TypeScript
186 lines
8.6 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Activity, Clock, Folders, CheckCircle2, Loader2 } from 'lucide-react';
|
|
import { progressWS, type ProgressMessage } from '../lib/websocket';
|
|
import { cn } from '../lib/utils';
|
|
|
|
interface QueueTask {
|
|
id: string;
|
|
name: string;
|
|
progress: number;
|
|
status: string;
|
|
}
|
|
|
|
export function Dashboard() {
|
|
const [tasks, setTasks] = useState<QueueTask[]>([
|
|
{ id: '1', name: 'City_Driving_Dataset_004.mp4', progress: 85, status: '正在截取帧 (30fps)' },
|
|
{ id: '2', name: 'Pedestrian_Night_Vision_02.mkv', progress: 32, status: '正在截取帧 (60fps)' },
|
|
{ id: '3', name: 'Drone_Mapping_Sector_7.avi', progress: 0, status: '队列排队等待中' },
|
|
]);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [activityLog, setActivityLog] = useState<Array<{ time: string; message: string; project?: string }>>([
|
|
{ time: '10 分钟前', message: '语义归档完成 54 帧', project: 'Highway_Data' },
|
|
{ time: '25 分钟前', message: '项目解析开始', project: 'City_Driving_Dataset_004' },
|
|
{ time: '1 小时前', message: '模板库更新: Cityscapes_v2', project: '系统' },
|
|
{ time: '2 小时前', message: 'AI 推理完成 12 个实例', project: 'Nav_Cam_Left' },
|
|
]);
|
|
|
|
useEffect(() => {
|
|
progressWS.connect();
|
|
|
|
const unsubscribe = progressWS.onProgress((data: ProgressMessage) => {
|
|
setIsConnected(progressWS.isConnected());
|
|
|
|
if (data.type === 'progress' && data.taskId && data.filename) {
|
|
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!,
|
|
name: data.filename!,
|
|
progress: data.progress ?? 0,
|
|
status: data.status ?? '处理中',
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
if (data.type === 'complete' && data.taskId) {
|
|
setTasks((prev) =>
|
|
prev.map((t) =>
|
|
t.id === data.taskId ? { ...t, progress: 100, status: '已完成' } : t
|
|
)
|
|
);
|
|
setActivityLog((prev) => [
|
|
{ time: '刚刚', message: `解析完成: ${data.filename || data.taskId}`, project: '系统' },
|
|
...prev.slice(0, 9),
|
|
]);
|
|
}
|
|
|
|
if (data.type === 'error' && data.taskId) {
|
|
setTasks((prev) =>
|
|
prev.map((t) =>
|
|
t.id === data.taskId ? { ...t, status: `错误: ${data.message || '未知错误'}` } : t
|
|
)
|
|
);
|
|
}
|
|
|
|
if (data.type === 'status') {
|
|
setActivityLog((prev) => [
|
|
{ time: '刚刚', message: data.message || '状态更新', project: '系统' },
|
|
...prev.slice(0, 9),
|
|
]);
|
|
}
|
|
});
|
|
|
|
const checkConnection = setInterval(() => {
|
|
setIsConnected(progressWS.isConnected());
|
|
}, 5000);
|
|
|
|
return () => {
|
|
unsubscribe();
|
|
clearInterval(checkConnection);
|
|
progressWS.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
const stats = [
|
|
{ label: '运行中项目', value: '14', icon: Folders, color: 'text-blue-400', bg: 'bg-blue-400/10' },
|
|
{ label: '排队处理任务', value: tasks.length.toString(), icon: Clock, color: 'text-orange-400', bg: 'bg-orange-400/10' },
|
|
{ label: '已归档批次', value: '128', icon: CheckCircle2, color: 'text-emerald-400', bg: 'bg-emerald-400/10' },
|
|
{ label: '系统负载', value: '78%', icon: Activity, color: 'text-cyan-400', bg: 'bg-cyan-400/10' },
|
|
];
|
|
|
|
return (
|
|
<div className="p-8 w-full h-full overflow-y-auto bg-[#0a0a0a]">
|
|
<header className="mb-8">
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-3xl font-medium tracking-tight text-white">系统整体概况</h1>
|
|
<div className={cn(
|
|
"flex items-center gap-1.5 text-[10px] uppercase font-mono px-2 py-1 rounded border",
|
|
isConnected
|
|
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
|
|
: "bg-amber-500/10 text-amber-400 border-amber-500/20"
|
|
)}>
|
|
<div className={cn("w-1.5 h-1.5 rounded-full", isConnected ? "bg-emerald-500" : "bg-amber-500 animate-pulse")} />
|
|
{isConnected ? 'WebSocket 已连接' : 'WebSocket 断开'}
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-400 text-sm mt-1">系统全局数据吞吐状态与所有接入项目进度实时洞察驾驶舱。</p>
|
|
</header>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
{stats.map((stat, i) => {
|
|
const Icon = stat.icon;
|
|
return (
|
|
<div key={i} className="bg-[#111] border border-white/5 p-5 rounded-xl block transition-all hover:border-white/20">
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${stat.bg} ${stat.color}`}>
|
|
<Icon size={20} />
|
|
</div>
|
|
<div className="text-xl font-mono text-gray-100">{stat.value}</div>
|
|
</div>
|
|
<div className="text-sm font-medium text-gray-500 uppercase tracking-widest">{stat.label}</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-2 bg-[#111] border border-white/5 rounded-xl p-6 min-h-[400px]">
|
|
<h2 className="text-sm font-medium text-gray-400 uppercase tracking-widest mb-6">解析队列 (FFmpeg 挂起任务)</h2>
|
|
<div className="space-y-4">
|
|
{tasks.map((task) => (
|
|
<div key={task.id} className="bg-[#0d0d0d] border border-white/5 p-4 rounded-lg">
|
|
<div className="flex justify-between items-center mb-2">
|
|
<span className="font-mono text-sm text-gray-200">{task.name}</span>
|
|
<span className="text-xs text-cyan-400 font-mono">{task.progress}%</span>
|
|
</div>
|
|
<div className="w-full h-1.5 bg-white/5 rounded-full overflow-hidden mb-2">
|
|
<div className="h-full bg-gradient-to-r from-cyan-600 to-cyan-400 rounded-full transition-all duration-500" style={{ width: `${task.progress}%` }} />
|
|
</div>
|
|
<div className="text-xs text-gray-500 flex items-center gap-2">
|
|
{task.status === '已完成' ? (
|
|
<CheckCircle2 size={12} className="text-emerald-400" />
|
|
) : task.status.includes('错误') ? (
|
|
<span className="text-red-400">●</span>
|
|
) : (
|
|
<Loader2 size={12} className="text-cyan-400 animate-spin" />
|
|
)}
|
|
{task.status}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{tasks.length === 0 && (
|
|
<div className="text-sm text-gray-500 text-center py-12">当前无处理任务</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-[#111] border border-white/5 rounded-xl p-6 min-h-[400px]">
|
|
<h2 className="text-sm font-medium text-gray-400 uppercase tracking-widest mb-6">近期实时流转记录</h2>
|
|
<div className="space-y-6 relative before:absolute before:inset-0 before:ml-[11px] before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-white/10 before:to-transparent">
|
|
{activityLog.map((log, i) => (
|
|
<div key={i} className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group is-active">
|
|
<div className="flex items-center justify-center w-6 h-6 rounded-full border border-white/10 bg-[#111] group-[.is-active]:bg-cyan-500 group-[.is-active]:border-cyan-400 text-slate-500 group-[.is-active]:text-black shadow shrink-0 md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2 z-10" />
|
|
<div className="w-[calc(100%-4rem)] md:w-[calc(50%-2.5rem)] bg-[#0d0d0d] p-3 rounded border border-white/5">
|
|
<div className="text-xs text-gray-400 mb-1">{log.time}</div>
|
|
<div className="text-sm font-medium text-gray-200">{log.message}</div>
|
|
<div className="text-xs text-gray-500">归属项目: {log.project}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|