From 9258f50f8e1c8154ba40b191d40db082be019a24 Mon Sep 17 00:00:00 2001 From: qikongjian Date: Tue, 26 Aug 2025 22:03:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增任务统计数据接口和消息队列状态接口 - 添加任务监控页面 (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 接口,支持项目任务统计和消息队列管理 --- api/video_flow.ts | 110 +++++ app/task-monitor/page.tsx | 289 +++++++++++++ .../task-monitor/message-queue-panel.tsx | 267 ++++++++++++ components/task-monitor/project-selector.tsx | 219 ++++++++++ components/task-monitor/task-detail-table.tsx | 394 ++++++++++++++++++ .../task-monitor/task-statistics-cards.tsx | 212 ++++++++++ components/task-monitor/task-status-chart.tsx | 205 +++++++++ .../task-monitor/task-timeline-chart.tsx | 285 +++++++++++++ 8 files changed, 1981 insertions(+) create mode 100644 app/task-monitor/page.tsx create mode 100644 components/task-monitor/message-queue-panel.tsx create mode 100644 components/task-monitor/project-selector.tsx create mode 100644 components/task-monitor/task-detail-table.tsx create mode 100644 components/task-monitor/task-statistics-cards.tsx create mode 100644 components/task-monitor/task-status-chart.tsx create mode 100644 components/task-monitor/task-timeline-chart.tsx diff --git a/api/video_flow.ts b/api/video_flow.ts index 990c629..194d2fe 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -823,6 +823,116 @@ export const retryTask = async (request: { return post("/task/retry_task", request); }; +/** + * 任务统计数据接口定义 + */ +export interface TaskStatistics { + /** 项目ID */ + project_id: string; + /** 项目名称 */ + project_name?: string; + /** 总任务数 */ + total_tasks: number; + /** 各状态任务统计 */ + status_stats: { + /** 已完成任务数 */ + completed: number; + /** 进行中任务数 */ + in_progress: number; + /** 等待中任务数 */ + pending: number; + /** 失败任务数 */ + failed: number; + /** 阻塞任务数 */ + blocked: number; + }; + /** 成功率 */ + success_rate: number; + /** 平均执行时间(秒) */ + avg_execution_time: number; + /** 最长执行时间(秒) */ + max_execution_time: number; + /** 最短执行时间(秒) */ + min_execution_time: number; + /** 最近更新时间 */ + last_updated: string; +} + +/** + * 消息队列状态接口定义 + */ +export interface MessageQueueStatus { + /** 队列名称 */ + queue_name: string; + /** 队列状态 */ + status: 'healthy' | 'warning' | 'error'; + /** 队列中消息数量 */ + message_count: number; + /** 消费者数量 */ + consumer_count: number; + /** 最后心跳时间 */ + last_heartbeat: string; + /** 错误信息 */ + error_message?: string; +} + +/** + * 获取项目任务统计数据 + * @param request - 请求参数 + * @returns Promise> + */ +export const getProjectTaskStatistics = async (request: { + /** 项目ID */ + project_id: string; + /** 时间范围(小时),默认24小时 */ + time_range?: number; +}): Promise> => { + return post("/task/get_project_statistics", request); +}; + +/** + * 获取多个项目的任务统计数据 + * @param request - 请求参数 + * @returns Promise> + */ +export const getMultiProjectTaskStatistics = async (request: { + /** 项目ID列表 */ + project_ids: string[]; + /** 时间范围(小时),默认24小时 */ + time_range?: number; +}): Promise> => { + return post("/task/get_multi_project_statistics", request); +}; + +/** + * 获取消息队列状态 + * @returns Promise> + */ +export const getMessageQueueStatus = async (): Promise> => { + return post("/system/get_queue_status", {}); +}; + +/** + * 重启消息队列 + * @param request - 请求参数 + * @returns Promise> + */ +export const restartMessageQueue = async (request: { + /** 队列名称 */ + queue_name: string; +}): Promise> => { + return post("/system/restart_queue", request); +}; + export const resumePlanFlow = async (request: { /** 项目ID */ project_id: string; diff --git a/app/task-monitor/page.tsx b/app/task-monitor/page.tsx new file mode 100644 index 0000000..81910cf --- /dev/null +++ b/app/task-monitor/page.tsx @@ -0,0 +1,289 @@ +'use client'; + +import React, { useState, useEffect, useRef } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { + getProjectTaskStatistics, + getMultiProjectTaskStatistics, + getMessageQueueStatus, + restartMessageQueue, + TaskStatistics, + MessageQueueStatus +} from '@/api/video_flow'; +import { cn } from '@/public/lib/utils'; +import TaskStatisticsCards from '@/components/task-monitor/task-statistics-cards'; +import TaskStatusChart from '@/components/task-monitor/task-status-chart'; +import TaskTimelineChart from '@/components/task-monitor/task-timeline-chart'; +import MessageQueuePanel from '@/components/task-monitor/message-queue-panel'; +import TaskDetailTable from '@/components/task-monitor/task-detail-table'; +import ProjectSelector from '@/components/task-monitor/project-selector'; + +export default function TaskMonitorPage() { + const searchParams = useSearchParams(); + const initialProjectId = searchParams.get('project_id') || 'bc43bc81-c781-4caa-8256-9710fd5bee80'; + + // 状态管理 + const [selectedProjectIds, setSelectedProjectIds] = useState([initialProjectId]); + const [timeRange, setTimeRange] = useState(24); // 24小时 + const [taskStatistics, setTaskStatistics] = useState([]); + const [queueStatus, setQueueStatus] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [lastUpdateTime, setLastUpdateTime] = useState(null); + const [autoRefresh, setAutoRefresh] = useState(true); + + // 刷新控制 + const refreshIntervalRef = useRef(null); + + // 获取任务统计数据 + const fetchTaskStatistics = async () => { + try { + setError(null); + + if (selectedProjectIds.length === 1) { + const response = await getProjectTaskStatistics({ + project_id: selectedProjectIds[0], + time_range: timeRange + }); + + if (response.code === 0 && response.data) { + setTaskStatistics([response.data]); + } else { + throw new Error(response.message || '获取任务统计失败'); + } + } else if (selectedProjectIds.length > 1) { + const response = await getMultiProjectTaskStatistics({ + project_ids: selectedProjectIds, + time_range: timeRange + }); + + if (response.code === 0 && response.data) { + setTaskStatistics(response.data); + } else { + throw new Error(response.message || '获取多项目统计失败'); + } + } + + setLastUpdateTime(new Date()); + } catch (err: any) { + console.error('获取任务统计失败:', err); + setError(err.message || '获取数据失败'); + } + }; + + // 获取消息队列状态 + const fetchQueueStatus = async () => { + try { + const response = await getMessageQueueStatus(); + + if (response.code === 0 && response.data) { + setQueueStatus(response.data); + } else { + console.warn('获取队列状态失败:', response.message); + } + } catch (err: any) { + console.error('获取队列状态失败:', err); + } + }; + + // 初始化数据加载 + const initializeData = async () => { + setLoading(true); + try { + await Promise.all([ + fetchTaskStatistics(), + fetchQueueStatus() + ]); + } finally { + setLoading(false); + } + }; + + // 手动刷新 + const handleRefresh = async () => { + await initializeData(); + }; + + // 重启消息队列 + const handleRestartQueue = async (queueName: string) => { + try { + const response = await restartMessageQueue({ queue_name: queueName }); + + if (response.code === 0 && response.data?.success) { + console.log('队列重启成功:', response.data); + // 延迟刷新队列状态 + setTimeout(() => { + fetchQueueStatus(); + }, 2000); + } else { + throw new Error(response.message || '重启失败'); + } + } catch (err: any) { + console.error('重启队列失败:', err); + alert(`重启队列失败: ${err.message}`); + } + }; + + // 项目选择变化 + const handleProjectChange = (projectIds: string[]) => { + setSelectedProjectIds(projectIds); + }; + + // 时间范围变化 + const handleTimeRangeChange = (range: number) => { + setTimeRange(range); + }; + + // 自动刷新控制 + useEffect(() => { + if (autoRefresh) { + refreshIntervalRef.current = setInterval(() => { + fetchTaskStatistics(); + fetchQueueStatus(); + }, 30000); // 30秒刷新一次 + } else { + if (refreshIntervalRef.current) { + clearInterval(refreshIntervalRef.current); + refreshIntervalRef.current = null; + } + } + + return () => { + if (refreshIntervalRef.current) { + clearInterval(refreshIntervalRef.current); + } + }; + }, [autoRefresh, selectedProjectIds, timeRange]); + + // 初始化和依赖变化时重新加载数据 + useEffect(() => { + initializeData(); + }, [selectedProjectIds, timeRange]); + + if (loading) { + return ( +
+
+
+

加载任务监控数据...

+
+
+ ); + } + + return ( +
+ {/* 顶部导航栏 */} +
+
+
+
+
+

任务监控中心

+
+
+ 实时监控项目任务状态和系统健康度 +
+
+ + {/* 控制面板 */} +
+ {/* 自动刷新开关 */} + + + {/* 手动刷新按钮 */} + + + {/* 最后更新时间 */} + {lastUpdateTime && ( +
+ 更新于 {lastUpdateTime.toLocaleTimeString()} +
+ )} +
+
+
+ + {/* 错误提示 */} + {error && ( +
+
+ + + + {error} +
+
+ )} + + {/* 主要内容区域 */} +
+ {/* 项目选择和时间范围控制 */} +
+ + +
+ + +
+
+ + {/* 核心指标卡片 */} + + + {/* 数据可视化和控制面板 */} +
+ {/* 图表区域 */} +
+ + +
+ + {/* 控制面板 */} +
+ +
+
+ + {/* 任务详情表格 */} + +
+
+ ); +} diff --git a/components/task-monitor/message-queue-panel.tsx b/components/task-monitor/message-queue-panel.tsx new file mode 100644 index 0000000..0afaf17 --- /dev/null +++ b/components/task-monitor/message-queue-panel.tsx @@ -0,0 +1,267 @@ +'use client'; + +import React, { useState } from 'react'; +import { MessageQueueStatus } from '@/api/video_flow'; +import { cn } from '@/public/lib/utils'; + +interface MessageQueuePanelProps { + queueStatus: MessageQueueStatus[]; + onRestartQueue: (queueName: string) => Promise; +} + +export default function MessageQueuePanel({ queueStatus, onRestartQueue }: MessageQueuePanelProps) { + const [restartingQueues, setRestartingQueues] = useState>(new Set()); + + const handleRestart = async (queueName: string) => { + if (restartingQueues.has(queueName)) return; + + setRestartingQueues(prev => new Set(prev).add(queueName)); + + try { + await onRestartQueue(queueName); + } finally { + setRestartingQueues(prev => { + const newSet = new Set(prev); + newSet.delete(queueName); + return newSet; + }); + } + }; + + const getStatusColor = (status: MessageQueueStatus['status']) => { + switch (status) { + case 'healthy': + return 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20'; + case 'warning': + return 'text-yellow-400 bg-yellow-500/10 border-yellow-500/20'; + case 'error': + return 'text-red-400 bg-red-500/10 border-red-500/20'; + default: + return 'text-gray-400 bg-gray-500/10 border-gray-500/20'; + } + }; + + const getStatusIcon = (status: MessageQueueStatus['status']) => { + switch (status) { + case 'healthy': + return '✅'; + case 'warning': + return '⚠️'; + case 'error': + return '❌'; + default: + return '❓'; + } + }; + + const getStatusText = (status: MessageQueueStatus['status']) => { + switch (status) { + case 'healthy': + return '健康'; + case 'warning': + return '警告'; + case 'error': + return '错误'; + default: + return '未知'; + } + }; + + // 计算整体健康度 + const overallHealth = React.useMemo(() => { + if (queueStatus.length === 0) return { status: 'unknown', percentage: 0 }; + + const healthyCount = queueStatus.filter(q => q.status === 'healthy').length; + const warningCount = queueStatus.filter(q => q.status === 'warning').length; + const errorCount = queueStatus.filter(q => q.status === 'error').length; + + const percentage = (healthyCount / queueStatus.length) * 100; + + if (errorCount > 0) return { status: 'error', percentage }; + if (warningCount > 0) return { status: 'warning', percentage }; + return { status: 'healthy', percentage }; + }, [queueStatus]); + + return ( +
+ {/* 消息队列总览 */} +
+
+

消息队列监控

+
+
+ + {queueStatus.length} 个队列 + +
+
+ + {/* 整体健康度指示器 */} +
+
+ 系统健康度 + + {overallHealth.percentage.toFixed(0)}% + +
+
+
+
+
+ + {/* 队列列表 */} + {queueStatus.length === 0 ? ( +
+
🔄
+
暂无队列数据
+
+ ) : ( +
+ {queueStatus.map((queue, index) => ( +
+
+
+ {getStatusIcon(queue.status)} +
+
{queue.queue_name}
+
+ {getStatusText(queue.status)} +
+
+
+ + +
+ +
+
+
消息数量
+
{queue.message_count.toLocaleString()}
+
+
+
消费者
+
{queue.consumer_count}
+
+
+ +
+ 最后心跳: {new Date(queue.last_heartbeat).toLocaleString()} +
+ + {queue.error_message && ( +
+ {queue.error_message} +
+ )} +
+ ))} +
+ )} +
+ + {/* 快速操作面板 */} +
+

快速操作

+ +
+ + + + +
+
+
• 健康队列无需重启
+
• 重启操作会中断当前处理
+
• 建议在低峰期执行批量重启
+
+
+
+
+ + {/* 队列统计 */} +
+

队列统计

+ +
+
+
+ {queueStatus.filter(q => q.status === 'healthy').length} +
+
健康队列
+
+ +
+
+ {queueStatus.filter(q => q.status === 'error').length} +
+
异常队列
+
+
+ +
+
+ {queueStatus.reduce((sum, q) => sum + q.message_count, 0).toLocaleString()} +
+
总消息数
+
+
+
+ ); +} diff --git a/components/task-monitor/project-selector.tsx b/components/task-monitor/project-selector.tsx new file mode 100644 index 0000000..0b5e65e --- /dev/null +++ b/components/task-monitor/project-selector.tsx @@ -0,0 +1,219 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { cn } from '@/public/lib/utils'; + +interface Project { + id: string; + name: string; + status: 'active' | 'inactive'; + last_activity: string; +} + +interface ProjectSelectorProps { + selectedProjectIds: string[]; + onProjectChange: (projectIds: string[]) => void; +} + +export default function ProjectSelector({ selectedProjectIds, onProjectChange }: ProjectSelectorProps) { + const [isOpen, setIsOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + + // 模拟项目数据 - 在实际应用中应该从API获取 + const mockProjects: Project[] = [ + { + id: 'bc43bc81-c781-4caa-8256-9710fd5bee80', + name: '视频制作项目 A', + status: 'active', + last_activity: '2024-01-15T10:30:00Z' + }, + { + id: 'd203016d-6f7e-4d1c-b66b-1b7d33632800', + name: '视频制作项目 B', + status: 'active', + last_activity: '2024-01-15T09:15:00Z' + }, + { + id: '029bbc09-6d83-440b-97fe-e2aa37b8042d', + name: '视频制作项目 C', + status: 'active', + last_activity: '2024-01-14T16:45:00Z' + }, + { + id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + name: '视频制作项目 D', + status: 'inactive', + last_activity: '2024-01-10T14:20:00Z' + }, + ]; + + useEffect(() => { + // 模拟加载项目列表 + setLoading(true); + setTimeout(() => { + setProjects(mockProjects); + setLoading(false); + }, 500); + }, []); + + const filteredProjects = projects.filter(project => + project.name.toLowerCase().includes(searchTerm.toLowerCase()) || + project.id.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleProjectToggle = (projectId: string) => { + if (selectedProjectIds.includes(projectId)) { + // 移除项目 + onProjectChange(selectedProjectIds.filter(id => id !== projectId)); + } else { + // 添加项目 + onProjectChange([...selectedProjectIds, projectId]); + } + }; + + const handleSelectAll = () => { + const activeProjects = filteredProjects.filter(p => p.status === 'active'); + onProjectChange(activeProjects.map(p => p.id)); + }; + + const handleClearAll = () => { + onProjectChange([]); + }; + + const getSelectedProjectNames = () => { + return selectedProjectIds + .map(id => projects.find(p => p.id === id)?.name || id.slice(0, 8)) + .join(', '); + }; + + return ( +
+ {/* 选择器按钮 */} + + + {/* 下拉面板 */} + {isOpen && ( +
+ {/* 搜索框 */} +
+ setSearchTerm(e.target.value)} + className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-white placeholder-gray-400 text-sm focus:outline-none focus:border-blue-500" + /> +
+ + {/* 批量操作 */} +
+ + +
+ 已选 {selectedProjectIds.length} 个 +
+
+ + {/* 项目列表 */} +
+ {loading ? ( +
+
+ 加载中... +
+ ) : filteredProjects.length === 0 ? ( +
+ {searchTerm ? '未找到匹配的项目' : '暂无项目'} +
+ ) : ( +
+ {filteredProjects.map((project) => ( +
handleProjectToggle(project.id)} + > + {}} // 由父级点击处理 + className="rounded" + /> + +
+
+ {project.name} +
+
+
+ ID: {project.id} +
+
+ 最后活动: {new Date(project.last_activity).toLocaleDateString()} +
+
+
+ ))} +
+ )} +
+ + {/* 底部信息 */} +
+
+ 共 {projects.length} 个项目 + 支持多选对比 +
+
+
+ )} + + {/* 点击外部关闭 */} + {isOpen && ( +
setIsOpen(false)} + /> + )} +
+ ); +} diff --git a/components/task-monitor/task-detail-table.tsx b/components/task-monitor/task-detail-table.tsx new file mode 100644 index 0000000..c9e2931 --- /dev/null +++ b/components/task-monitor/task-detail-table.tsx @@ -0,0 +1,394 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { getProjectTaskList, retryTask, TaskItem } from '@/api/video_flow'; +import { cn } from '@/public/lib/utils'; + +interface TaskDetailTableProps { + projectIds: string[]; + timeRange: number; +} + +export default function TaskDetailTable({ projectIds, timeRange }: TaskDetailTableProps) { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [sortBy, setSortBy] = useState<'created_at' | 'updated_at' | 'task_name' | 'task_status'>('updated_at'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + const [filterStatus, setFilterStatus] = useState('all'); + const [searchTerm, setSearchTerm] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [retryingTasks, setRetryingTasks] = useState>(new Set()); + + const itemsPerPage = 20; + + // 获取任务列表 + const fetchTasks = async () => { + if (projectIds.length === 0) { + setTasks([]); + return; + } + + setLoading(true); + setError(null); + + try { + const allTasks: TaskItem[] = []; + + // 并行获取所有项目的任务 + const promises = projectIds.map(async (projectId) => { + try { + const response = await getProjectTaskList({ project_id: projectId }); + if (response.code === 0 && response.data) { + return response.data.map(task => ({ ...task, project_id: projectId })); + } + return []; + } catch (err) { + console.error(`获取项目 ${projectId} 任务失败:`, err); + return []; + } + }); + + const results = await Promise.all(promises); + results.forEach(projectTasks => { + allTasks.push(...projectTasks); + }); + + setTasks(allTasks); + } catch (err: any) { + console.error('获取任务列表失败:', err); + setError(err.message || '获取任务列表失败'); + } finally { + setLoading(false); + } + }; + + // 重试任务 + const handleRetryTask = async (taskId: string) => { + if (retryingTasks.has(taskId)) return; + + setRetryingTasks(prev => new Set(prev).add(taskId)); + + try { + const response = await retryTask({ task_id: taskId }); + + if (response.code === 0 && response.data?.success) { + // 乐观更新任务状态 + setTasks(prevTasks => + prevTasks.map(task => + task.task_id === taskId + ? { ...task, task_status: 'IN_PROGRESS' as const } + : task + ) + ); + + // 延迟刷新获取最新状态 + setTimeout(() => { + fetchTasks(); + }, 2000); + } else { + throw new Error(response.message || '重试失败'); + } + } catch (err: any) { + console.error('重试任务失败:', err); + alert(`重试任务失败: ${err.message}`); + } finally { + setRetryingTasks(prev => { + const newSet = new Set(prev); + newSet.delete(taskId); + return newSet; + }); + } + }; + + // 过滤和排序任务 + const filteredAndSortedTasks = React.useMemo(() => { + let filtered = tasks; + + // 状态过滤 + if (filterStatus !== 'all') { + filtered = filtered.filter(task => task.task_status === filterStatus); + } + + // 搜索过滤 + if (searchTerm) { + filtered = filtered.filter(task => + task.task_name.toLowerCase().includes(searchTerm.toLowerCase()) || + task.task_id.toLowerCase().includes(searchTerm.toLowerCase()) || + task.task_message.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + // 排序 + filtered.sort((a, b) => { + let aValue: any = a[sortBy]; + let bValue: any = b[sortBy]; + + if (sortBy === 'created_at' || sortBy === 'updated_at') { + aValue = new Date(aValue).getTime(); + bValue = new Date(bValue).getTime(); + } + + if (sortOrder === 'asc') { + return aValue > bValue ? 1 : -1; + } else { + return aValue < bValue ? 1 : -1; + } + }); + + return filtered; + }, [tasks, filterStatus, searchTerm, sortBy, sortOrder]); + + // 分页 + const paginatedTasks = React.useMemo(() => { + const startIndex = (currentPage - 1) * itemsPerPage; + return filteredAndSortedTasks.slice(startIndex, startIndex + itemsPerPage); + }, [filteredAndSortedTasks, currentPage]); + + const totalPages = Math.ceil(filteredAndSortedTasks.length / itemsPerPage); + + useEffect(() => { + fetchTasks(); + }, [projectIds, timeRange]); + + useEffect(() => { + setCurrentPage(1); + }, [filterStatus, searchTerm]); + + const getStatusColor = (status: string) => { + switch (status) { + case 'COMPLETED': + return 'text-emerald-400 bg-emerald-500/10'; + case 'IN_PROGRESS': + return 'text-blue-400 bg-blue-500/10'; + case 'PENDING': + return 'text-yellow-400 bg-yellow-500/10'; + case 'FAILED': + return 'text-red-400 bg-red-500/10'; + default: + return 'text-gray-400 bg-gray-500/10'; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'COMPLETED': + return '已完成'; + case 'IN_PROGRESS': + return '进行中'; + case 'PENDING': + return '等待中'; + case 'FAILED': + return '失败'; + default: + return status; + } + }; + + const formatDuration = (startTime: string, endTime?: string) => { + const start = new Date(startTime); + const end = endTime ? new Date(endTime) : new Date(); + const duration = end.getTime() - start.getTime(); + + const minutes = Math.floor(duration / 60000); + const seconds = Math.floor((duration % 60000) / 1000); + + if (minutes > 0) { + return `${minutes}分${seconds}秒`; + } + return `${seconds}秒`; + }; + + return ( +
+ {/* 表格头部控制 */} +
+
+

任务详情列表

+ +
+ +
+ {/* 搜索框 */} +
+ setSearchTerm(e.target.value)} + className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-white placeholder-gray-400 text-sm focus:outline-none focus:border-blue-500" + /> +
+ + {/* 状态过滤 */} + + + {/* 排序 */} + +
+ + {/* 统计信息 */} +
+ 显示 {paginatedTasks.length} / {filteredAndSortedTasks.length} 个任务 + {projectIds.length > 1 && ` (来自 ${projectIds.length} 个项目)`} +
+
+ + {/* 错误提示 */} + {error && ( +
+
+ + + + {error} +
+
+ )} + + {/* 表格内容 */} +
+ {loading ? ( +
+
+
+
加载任务数据...
+
+
+ ) : paginatedTasks.length === 0 ? ( +
+
+
📋
+
{filteredAndSortedTasks.length === 0 ? '暂无任务数据' : '没有匹配的任务'}
+
+
+ ) : ( + + + + + + + + + + + + {paginatedTasks.map((task, index) => ( + + + + + + + + ))} + +
任务信息状态进度耗时操作
+
+
{task.task_name}
+
{task.task_id}
+ {task.task_message && ( +
+ {task.task_message} +
+ )} +
+
+ + {getStatusText(task.task_status)} + + + {task.task_result?.progress_percentage !== undefined ? ( +
+
{task.task_result.progress_percentage.toFixed(1)}%
+
+
+
+
+ ) : ( + - + )} +
+
+ {formatDuration(task.created_at, task.updated_at)} +
+
+ {new Date(task.updated_at).toLocaleString()} +
+
+ {task.task_status === 'FAILED' && ( + + )} +
+ )} +
+ + {/* 分页控制 */} + {totalPages > 1 && ( +
+
+ 第 {currentPage} 页,共 {totalPages} 页 +
+
+ + +
+
+ )} +
+ ); +} diff --git a/components/task-monitor/task-statistics-cards.tsx b/components/task-monitor/task-statistics-cards.tsx new file mode 100644 index 0000000..a300af8 --- /dev/null +++ b/components/task-monitor/task-statistics-cards.tsx @@ -0,0 +1,212 @@ +'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 ( +
+ {cards.map((card, index) => ( +
+
+
{card.icon}
+
{card.title}
+
+ +
+
{card.value}
+
{card.subtitle}
+
+
+ ))} +
+ ); +} + +// 单个项目的详细统计卡片 +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 ( +
+
+
+

+ {statistics.project_name || statistics.project_id} +

+

项目ID: {statistics.project_id}

+
+
+
{statistics.total_tasks}
+
总任务数
+
+
+ +
+ {statusItems.map((item, index) => ( +
+
{item.value}
+
{item.label}
+
+ ))} +
+ +
+
+ + 成功率: {statistics.success_rate.toFixed(1)}% + + + 平均耗时: {(statistics.avg_execution_time / 60).toFixed(1)}分 + +
+
+ 更新于 {new Date(statistics.last_updated).toLocaleTimeString()} +
+
+
+ ); +} diff --git a/components/task-monitor/task-status-chart.tsx b/components/task-monitor/task-status-chart.tsx new file mode 100644 index 0000000..4b4ede3 --- /dev/null +++ b/components/task-monitor/task-status-chart.tsx @@ -0,0 +1,205 @@ +'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 ( +
+ + + {data.map((item, index) => { + const percentage = (item.value / total) * 100; + const strokeDasharray = `${percentage * 5.03} 502`; + const strokeDashoffset = -cumulativePercentage * 5.03; + cumulativePercentage += percentage; + + return ( + + ); + })} + + + {/* 中心文字 */} +
+
{total.toLocaleString()}
+
总任务数
+
+
+ ); + }; + + // 柱状图实现 + const BarChart = ({ data }: { data: typeof aggregatedData }) => { + const maxValue = Math.max(...data.map(item => item.value)); + + return ( +
+ {data.map((item, index) => ( +
+
{item.name}
+
+
+
0 ? (item.value / maxValue) * 100 : 0}%`, + }} + /> +
+
+ {item.value} + {item.percentage.toFixed(1)}% +
+
+
+ ))} +
+ ); + }; + + if (statistics.length === 0) { + return ( +
+

任务状态分布

+
+
+
📊
+
暂无数据
+
+
+
+ ); + } + + return ( +
+
+

任务状态分布

+
+ {statistics.length > 1 ? `${statistics.length} 个项目` : '单项目视图'} +
+
+ +
+ {/* 饼图 */} +
+

状态占比

+ +
+ + {/* 柱状图 */} +
+

详细数据

+ +
+
+ + {/* 图例 */} +
+
+ {aggregatedData.map((item, index) => ( +
+
+ {item.name} + ({item.value}) +
+ ))} +
+
+ + {/* 多项目详情 */} + {statistics.length > 1 && ( +
+

项目详情

+
+ {statistics.map((stat, index) => ( +
+
+
+ {stat.project_name || stat.project_id} +
+
{stat.total_tasks}
+
+
+ ✓{stat.status_stats.completed} + ⚡{stat.status_stats.in_progress} + ⏳{stat.status_stats.pending} + ✗{stat.status_stats.failed} + {stat.status_stats.blocked > 0 && ( + 🚫{stat.status_stats.blocked} + )} +
+
+ ))} +
+
+ )} +
+ ); +} diff --git a/components/task-monitor/task-timeline-chart.tsx b/components/task-monitor/task-timeline-chart.tsx new file mode 100644 index 0000000..8a903ae --- /dev/null +++ b/components/task-monitor/task-timeline-chart.tsx @@ -0,0 +1,285 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; + +interface TaskTimelineData { + timestamp: string; + completed: number; + failed: number; + in_progress: number; + total: number; +} + +interface TaskTimelineChartProps { + projectIds: string[]; + timeRange: number; // 小时 +} + +export default function TaskTimelineChart({ projectIds, timeRange }: TaskTimelineChartProps) { + const [timelineData, setTimelineData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedMetric, setSelectedMetric] = useState<'total' | 'completed' | 'failed' | 'success_rate'>('total'); + + // 模拟生成时间线数据 + const generateMockData = () => { + const data: TaskTimelineData[] = []; + const now = new Date(); + const intervalHours = timeRange <= 24 ? 1 : timeRange <= 72 ? 3 : 6; + const points = Math.floor(timeRange / intervalHours); + + for (let i = points; i >= 0; i--) { + const timestamp = new Date(now.getTime() - i * intervalHours * 60 * 60 * 1000); + + // 模拟数据波动 + const baseCompleted = 50 + Math.random() * 30; + const baseFailed = 5 + Math.random() * 10; + const baseInProgress = 10 + Math.random() * 15; + + // 根据项目数量调整 + const multiplier = projectIds.length; + + data.push({ + timestamp: timestamp.toISOString(), + completed: Math.floor(baseCompleted * multiplier), + failed: Math.floor(baseFailed * multiplier), + in_progress: Math.floor(baseInProgress * multiplier), + total: Math.floor((baseCompleted + baseFailed + baseInProgress) * multiplier), + }); + } + + return data; + }; + + useEffect(() => { + setLoading(true); + // 模拟API调用 + setTimeout(() => { + setTimelineData(generateMockData()); + setLoading(false); + }, 500); + }, [projectIds, timeRange]); + + const getMetricValue = (data: TaskTimelineData, metric: string) => { + switch (metric) { + case 'total': + return data.total; + case 'completed': + return data.completed; + case 'failed': + return data.failed; + case 'success_rate': + return data.total > 0 ? (data.completed / data.total) * 100 : 0; + default: + return 0; + } + }; + + const getMetricColor = (metric: string) => { + switch (metric) { + case 'total': + return '#3b82f6'; + case 'completed': + return '#10b981'; + case 'failed': + return '#ef4444'; + case 'success_rate': + return '#8b5cf6'; + default: + return '#6b7280'; + } + }; + + const formatTimestamp = (timestamp: string) => { + const date = new Date(timestamp); + if (timeRange <= 24) { + return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + } else { + return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); + } + }; + + const LineChart = () => { + if (timelineData.length === 0) return null; + + const values = timelineData.map(d => getMetricValue(d, selectedMetric)); + const maxValue = Math.max(...values); + const minValue = Math.min(...values); + const range = maxValue - minValue || 1; + + const width = 600; + const height = 200; + const padding = 40; + + const points = timelineData.map((data, index) => { + const x = padding + (index / (timelineData.length - 1)) * (width - 2 * padding); + const value = getMetricValue(data, selectedMetric); + const y = height - padding - ((value - minValue) / range) * (height - 2 * padding); + return { x, y, value, data }; + }); + + const pathData = points.map((point, index) => + `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}` + ).join(' '); + + return ( +
+ + {/* 网格线 */} + + + + + + + + {/* Y轴标签 */} + {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => { + const y = height - padding - ratio * (height - 2 * padding); + const value = minValue + ratio * range; + return ( + + + + {selectedMetric === 'success_rate' ? `${value.toFixed(0)}%` : Math.round(value)} + + + ); + })} + + {/* 主线条 */} + + + {/* 填充区域 */} + + + {/* 数据点 */} + {points.map((point, index) => ( + + + {formatTimestamp(point.data.timestamp)}: { + selectedMetric === 'success_rate' + ? `${point.value.toFixed(1)}%` + : Math.round(point.value) + } + + + ))} + + {/* X轴标签 */} + {points.filter((_, index) => index % Math.ceil(points.length / 6) === 0).map((point, index) => ( + + {formatTimestamp(point.data.timestamp)} + + ))} + +
+ ); + }; + + const metrics = [ + { key: 'total', label: '总任务数', color: '#3b82f6' }, + { key: 'completed', label: '完成数', color: '#10b981' }, + { key: 'failed', label: '失败数', color: '#ef4444' }, + { key: 'success_rate', label: '成功率', color: '#8b5cf6' }, + ]; + + return ( +
+
+

任务趋势分析

+
+ {metrics.map((metric) => ( + + ))} +
+
+ + {loading ? ( +
+
+
+
加载趋势数据...
+
+
+ ) : timelineData.length === 0 ? ( +
+
+
📈
+
暂无趋势数据
+
+
+ ) : ( +
+ + + {/* 统计信息 */} +
+
+ {metrics.map((metric) => { + const values = timelineData.map(d => getMetricValue(d, metric.key)); + const latest = values[values.length - 1] || 0; + const previous = values[values.length - 2] || 0; + const change = previous !== 0 ? ((latest - previous) / previous) * 100 : 0; + + return ( +
+
+
+ {metric.label} +
+
+ {metric.key === 'success_rate' ? `${latest.toFixed(1)}%` : Math.round(latest)} +
+
= 0 ? 'text-emerald-400' : 'text-red-400'}`}> + {change >= 0 ? '↗' : '↘'} {Math.abs(change).toFixed(1)}% +
+
+ ); + })} +
+
+
+ )} +
+ ); +}