video-flow-b/components/task-monitor/task-status-chart.tsx
qikongjian 9258f50f8e feat: 添加任务监控功能
- 新增任务统计数据接口和消息队列状态接口
- 添加任务监控页面 (app/task-monitor/page.tsx)
- 实现任务监控相关组件:
  - 项目选择器 (project-selector.tsx)
  - 任务统计卡片 (task-statistics-cards.tsx)
  - 任务状态图表 (task-status-chart.tsx)
  - 任务时间线图表 (task-timeline-chart.tsx)
  - 任务详情表格 (task-detail-table.tsx)
  - 消息队列面板 (message-queue-panel.tsx)
- 扩展 video_flow.ts API 接口,支持项目任务统计和消息队列管理
2025-08-26 22:03:57 +08:00

206 lines
7.7 KiB
TypeScript

'use client';
import React from 'react';
import { TaskStatistics } from '@/api/video_flow';
interface TaskStatusChartProps {
statistics: TaskStatistics[];
}
export default function TaskStatusChart({ statistics }: TaskStatusChartProps) {
// 聚合所有项目的状态数据
const aggregatedData = React.useMemo(() => {
if (statistics.length === 0) {
return [];
}
const totals = statistics.reduce(
(acc, stat) => ({
completed: acc.completed + stat.status_stats.completed,
in_progress: acc.in_progress + stat.status_stats.in_progress,
pending: acc.pending + stat.status_stats.pending,
failed: acc.failed + stat.status_stats.failed,
blocked: acc.blocked + stat.status_stats.blocked,
}),
{
completed: 0,
in_progress: 0,
pending: 0,
failed: 0,
blocked: 0,
}
);
const total = Object.values(totals).reduce((sum, val) => sum + val, 0);
return [
{ name: '已完成', value: totals.completed, color: '#10b981', percentage: total > 0 ? (totals.completed / total) * 100 : 0 },
{ name: '进行中', value: totals.in_progress, color: '#3b82f6', percentage: total > 0 ? (totals.in_progress / total) * 100 : 0 },
{ name: '等待中', value: totals.pending, color: '#f59e0b', percentage: total > 0 ? (totals.pending / total) * 100 : 0 },
{ name: '失败', value: totals.failed, color: '#ef4444', percentage: total > 0 ? (totals.failed / total) * 100 : 0 },
{ name: '阻塞', value: totals.blocked, color: '#f97316', percentage: total > 0 ? (totals.blocked / total) * 100 : 0 },
].filter(item => item.value > 0);
}, [statistics]);
// 简单的饼图实现
const PieChart = ({ data }: { data: typeof aggregatedData }) => {
const total = data.reduce((sum, item) => sum + item.value, 0);
let cumulativePercentage = 0;
return (
<div className="relative w-48 h-48 mx-auto">
<svg width="192" height="192" viewBox="0 0 192 192" className="transform -rotate-90">
<circle
cx="96"
cy="96"
r="80"
fill="none"
stroke="#374151"
strokeWidth="2"
/>
{data.map((item, index) => {
const percentage = (item.value / total) * 100;
const strokeDasharray = `${percentage * 5.03} 502`;
const strokeDashoffset = -cumulativePercentage * 5.03;
cumulativePercentage += percentage;
return (
<circle
key={index}
cx="96"
cy="96"
r="80"
fill="none"
stroke={item.color}
strokeWidth="16"
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
className="transition-all duration-300"
/>
);
})}
</svg>
{/* 中心文字 */}
<div className="absolute inset-0 flex flex-col items-center justify-center">
<div className="text-2xl font-bold text-white">{total.toLocaleString()}</div>
<div className="text-sm text-gray-400"></div>
</div>
</div>
);
};
// 柱状图实现
const BarChart = ({ data }: { data: typeof aggregatedData }) => {
const maxValue = Math.max(...data.map(item => item.value));
return (
<div className="space-y-3">
{data.map((item, index) => (
<div key={index} className="flex items-center gap-3">
<div className="w-16 text-sm text-gray-400 text-right">{item.name}</div>
<div className="flex-1 relative">
<div className="h-6 bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
style={{
backgroundColor: item.color,
width: `${maxValue > 0 ? (item.value / maxValue) * 100 : 0}%`,
}}
/>
</div>
<div className="absolute inset-0 flex items-center justify-between px-2">
<span className="text-xs text-white font-medium">{item.value}</span>
<span className="text-xs text-gray-300">{item.percentage.toFixed(1)}%</span>
</div>
</div>
</div>
))}
</div>
);
};
if (statistics.length === 0) {
return (
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-6 backdrop-blur-sm">
<h3 className="text-lg font-semibold text-white mb-4"></h3>
<div className="flex items-center justify-center h-48 text-gray-400">
<div className="text-center">
<div className="text-4xl mb-2">📊</div>
<div></div>
</div>
</div>
</div>
);
}
return (
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-6 backdrop-blur-sm">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-white"></h3>
<div className="text-sm text-gray-400">
{statistics.length > 1 ? `${statistics.length} 个项目` : '单项目视图'}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 饼图 */}
<div className="text-center">
<h4 className="text-sm font-medium text-gray-300 mb-4"></h4>
<PieChart data={aggregatedData} />
</div>
{/* 柱状图 */}
<div>
<h4 className="text-sm font-medium text-gray-300 mb-4"></h4>
<BarChart data={aggregatedData} />
</div>
</div>
{/* 图例 */}
<div className="mt-6 pt-4 border-t border-gray-700">
<div className="flex flex-wrap gap-4 justify-center">
{aggregatedData.map((item, index) => (
<div key={index} className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: item.color }}
/>
<span className="text-sm text-gray-300">{item.name}</span>
<span className="text-sm text-gray-500">({item.value})</span>
</div>
))}
</div>
</div>
{/* 多项目详情 */}
{statistics.length > 1 && (
<div className="mt-6 pt-4 border-t border-gray-700">
<h4 className="text-sm font-medium text-gray-300 mb-3"></h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{statistics.map((stat, index) => (
<div key={index} className="bg-gray-700/30 rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<div className="text-sm font-medium text-white truncate">
{stat.project_name || stat.project_id}
</div>
<div className="text-sm text-gray-400">{stat.total_tasks}</div>
</div>
<div className="flex gap-2 text-xs">
<span className="text-emerald-400">{stat.status_stats.completed}</span>
<span className="text-blue-400">{stat.status_stats.in_progress}</span>
<span className="text-yellow-400">{stat.status_stats.pending}</span>
<span className="text-red-400">{stat.status_stats.failed}</span>
{stat.status_stats.blocked > 0 && (
<span className="text-orange-400">🚫{stat.status_stats.blocked}</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}