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 接口,支持项目任务统计和消息队列管理
213 lines
6.9 KiB
TypeScript
213 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { TaskStatistics } from '@/api/video_flow';
|
|
import { cn } from '@/public/lib/utils';
|
|
|
|
interface TaskStatisticsCardsProps {
|
|
statistics: TaskStatistics[];
|
|
}
|
|
|
|
export default function TaskStatisticsCards({ statistics }: TaskStatisticsCardsProps) {
|
|
// 聚合多个项目的统计数据
|
|
const aggregatedStats = React.useMemo(() => {
|
|
if (statistics.length === 0) {
|
|
return {
|
|
total_tasks: 0,
|
|
status_stats: {
|
|
completed: 0,
|
|
in_progress: 0,
|
|
pending: 0,
|
|
failed: 0,
|
|
blocked: 0,
|
|
},
|
|
success_rate: 0,
|
|
avg_execution_time: 0,
|
|
project_count: 0,
|
|
};
|
|
}
|
|
|
|
const totals = statistics.reduce(
|
|
(acc, stat) => ({
|
|
total_tasks: acc.total_tasks + stat.total_tasks,
|
|
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,
|
|
total_execution_time: acc.total_execution_time + (stat.avg_execution_time * stat.total_tasks),
|
|
}),
|
|
{
|
|
total_tasks: 0,
|
|
completed: 0,
|
|
in_progress: 0,
|
|
pending: 0,
|
|
failed: 0,
|
|
blocked: 0,
|
|
total_execution_time: 0,
|
|
}
|
|
);
|
|
|
|
const success_rate = totals.total_tasks > 0
|
|
? (totals.completed / totals.total_tasks) * 100
|
|
: 0;
|
|
|
|
const avg_execution_time = totals.total_tasks > 0
|
|
? totals.total_execution_time / totals.total_tasks
|
|
: 0;
|
|
|
|
return {
|
|
total_tasks: totals.total_tasks,
|
|
status_stats: {
|
|
completed: totals.completed,
|
|
in_progress: totals.in_progress,
|
|
pending: totals.pending,
|
|
failed: totals.failed,
|
|
blocked: totals.blocked,
|
|
},
|
|
success_rate,
|
|
avg_execution_time,
|
|
project_count: statistics.length,
|
|
};
|
|
}, [statistics]);
|
|
|
|
const cards = [
|
|
{
|
|
title: '总任务数',
|
|
value: aggregatedStats.total_tasks.toLocaleString(),
|
|
icon: '📊',
|
|
color: 'blue',
|
|
subtitle: `${aggregatedStats.project_count} 个项目`,
|
|
},
|
|
{
|
|
title: '成功率',
|
|
value: `${aggregatedStats.success_rate.toFixed(1)}%`,
|
|
icon: '✅',
|
|
color: aggregatedStats.success_rate >= 90 ? 'green' : aggregatedStats.success_rate >= 70 ? 'yellow' : 'red',
|
|
subtitle: `${aggregatedStats.status_stats.completed} 个成功`,
|
|
},
|
|
{
|
|
title: '进行中',
|
|
value: aggregatedStats.status_stats.in_progress.toLocaleString(),
|
|
icon: '⚡',
|
|
color: 'blue',
|
|
subtitle: '正在执行',
|
|
},
|
|
{
|
|
title: '失败任务',
|
|
value: aggregatedStats.status_stats.failed.toLocaleString(),
|
|
icon: '❌',
|
|
color: 'red',
|
|
subtitle: '需要处理',
|
|
},
|
|
{
|
|
title: '平均耗时',
|
|
value: `${(aggregatedStats.avg_execution_time / 60).toFixed(1)}分`,
|
|
icon: '⏱️',
|
|
color: 'purple',
|
|
subtitle: '执行时间',
|
|
},
|
|
{
|
|
title: '阻塞任务',
|
|
value: aggregatedStats.status_stats.blocked.toLocaleString(),
|
|
icon: '🚫',
|
|
color: 'orange',
|
|
subtitle: '等待处理',
|
|
},
|
|
];
|
|
|
|
const getColorClasses = (color: string) => {
|
|
switch (color) {
|
|
case 'green':
|
|
return 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400';
|
|
case 'red':
|
|
return 'bg-red-500/10 border-red-500/20 text-red-400';
|
|
case 'yellow':
|
|
return 'bg-yellow-500/10 border-yellow-500/20 text-yellow-400';
|
|
case 'blue':
|
|
return 'bg-blue-500/10 border-blue-500/20 text-blue-400';
|
|
case 'purple':
|
|
return 'bg-purple-500/10 border-purple-500/20 text-purple-400';
|
|
case 'orange':
|
|
return 'bg-orange-500/10 border-orange-500/20 text-orange-400';
|
|
default:
|
|
return 'bg-gray-500/10 border-gray-500/20 text-gray-400';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
|
|
{cards.map((card, index) => (
|
|
<div
|
|
key={index}
|
|
className={cn(
|
|
'p-4 rounded-lg border backdrop-blur-sm transition-all duration-200 hover:scale-105',
|
|
getColorClasses(card.color)
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="text-2xl">{card.icon}</div>
|
|
<div className="text-xs opacity-60">{card.title}</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<div className="text-2xl font-bold">{card.value}</div>
|
|
<div className="text-xs opacity-80">{card.subtitle}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 单个项目的详细统计卡片
|
|
export function ProjectStatisticsCard({ statistics }: { statistics: TaskStatistics }) {
|
|
const statusItems = [
|
|
{ label: '已完成', value: statistics.status_stats.completed, color: 'text-emerald-400' },
|
|
{ label: '进行中', value: statistics.status_stats.in_progress, color: 'text-blue-400' },
|
|
{ label: '等待中', value: statistics.status_stats.pending, color: 'text-yellow-400' },
|
|
{ label: '失败', value: statistics.status_stats.failed, color: 'text-red-400' },
|
|
{ label: '阻塞', value: statistics.status_stats.blocked, color: 'text-orange-400' },
|
|
];
|
|
|
|
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-4">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-white">
|
|
{statistics.project_name || statistics.project_id}
|
|
</h3>
|
|
<p className="text-sm text-gray-400">项目ID: {statistics.project_id}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-2xl font-bold text-white">{statistics.total_tasks}</div>
|
|
<div className="text-sm text-gray-400">总任务数</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-5 gap-3 mb-4">
|
|
{statusItems.map((item, index) => (
|
|
<div key={index} className="text-center">
|
|
<div className={cn('text-lg font-semibold', item.color)}>{item.value}</div>
|
|
<div className="text-xs text-gray-500">{item.label}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-gray-400">
|
|
成功率: <span className="text-emerald-400 font-semibold">{statistics.success_rate.toFixed(1)}%</span>
|
|
</span>
|
|
<span className="text-gray-400">
|
|
平均耗时: <span className="text-blue-400 font-semibold">{(statistics.avg_execution_time / 60).toFixed(1)}分</span>
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
更新于 {new Date(statistics.last_updated).toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|