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

290 lines
9.8 KiB
TypeScript

'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<string[]>([initialProjectId]);
const [timeRange, setTimeRange] = useState<number>(24); // 24小时
const [taskStatistics, setTaskStatistics] = useState<TaskStatistics[]>([]);
const [queueStatus, setQueueStatus] = useState<MessageQueueStatus[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdateTime, setLastUpdateTime] = useState<Date | null>(null);
const [autoRefresh, setAutoRefresh] = useState(true);
// 刷新控制
const refreshIntervalRef = useRef<NodeJS.Timeout | null>(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 (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4 text-lg text-gray-600">...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
{/* 顶部导航栏 */}
<div className="bg-gray-900 border-b border-gray-800 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-emerald-400 animate-pulse"></div>
<h1 className="text-xl font-bold text-white"></h1>
</div>
<div className="text-sm text-gray-400">
</div>
</div>
{/* 控制面板 */}
<div className="flex items-center gap-4">
{/* 自动刷新开关 */}
<label className="flex items-center gap-2 text-sm text-gray-400">
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
className="rounded"
/>
</label>
{/* 手动刷新按钮 */}
<button
onClick={handleRefresh}
disabled={loading}
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white rounded-lg transition-colors flex items-center gap-2"
>
<svg className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
{/* 最后更新时间 */}
{lastUpdateTime && (
<div className="text-sm text-gray-500">
{lastUpdateTime.toLocaleTimeString()}
</div>
)}
</div>
</div>
</div>
{/* 错误提示 */}
{error && (
<div className="mx-6 mt-4 p-4 bg-red-500/10 border border-red-500/20 rounded-lg">
<div className="flex items-center gap-2 text-red-400">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{error}</span>
</div>
</div>
)}
{/* 主要内容区域 */}
<div className="p-6 space-y-6">
{/* 项目选择和时间范围控制 */}
<div className="flex items-center gap-4">
<ProjectSelector
selectedProjectIds={selectedProjectIds}
onProjectChange={handleProjectChange}
/>
<div className="flex items-center gap-2">
<label className="text-sm text-gray-400">:</label>
<select
value={timeRange}
onChange={(e) => handleTimeRangeChange(Number(e.target.value))}
className="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm"
>
<option value={1}>1</option>
<option value={6}>6</option>
<option value={24}>24</option>
<option value={72}>3</option>
<option value={168}>7</option>
</select>
</div>
</div>
{/* 核心指标卡片 */}
<TaskStatisticsCards statistics={taskStatistics} />
{/* 数据可视化和控制面板 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 图表区域 */}
<div className="lg:col-span-2 space-y-6">
<TaskStatusChart statistics={taskStatistics} />
<TaskTimelineChart projectIds={selectedProjectIds} timeRange={timeRange} />
</div>
{/* 控制面板 */}
<div className="space-y-6">
<MessageQueuePanel
queueStatus={queueStatus}
onRestartQueue={handleRestartQueue}
/>
</div>
</div>
{/* 任务详情表格 */}
<TaskDetailTable
projectIds={selectedProjectIds}
timeRange={timeRange}
/>
</div>
</div>
);
}