forked from 77media/video-flow
- 新增任务统计数据接口和消息队列状态接口 - 添加任务监控页面 (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 接口,支持项目任务统计和消息队列管理
206 lines
7.7 KiB
TypeScript
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>
|
|
);
|
|
}
|