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 接口,支持项目任务统计和消息队列管理
290 lines
9.8 KiB
TypeScript
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>
|
|
);
|
|
}
|