505 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useState, useEffect, useRef } from 'react';
import NetworkTimeline from '@/components/dashboard/network-timeline';
import { useSearchParams } from 'next/navigation';
import { getProjectTaskList, retryTask } from '@/api/video_flow';
import { mockDashboardData } from '@/components/dashboard/demo-data';
import { cn } from '@/public/lib/utils';
export default function DashboardPage() {
const searchParams = useSearchParams();
// 使用真实项目ID如果URL没有提供则使用默认的真实项目ID
const projectId = searchParams.get('project_id') || 'bc43bc81-c781-4caa-8256-9710fd5bee80';
const [dashboardData, setDashboardData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isUsingMockData, setIsUsingMockData] = useState(false);
const [isBackgroundRefreshing, setIsBackgroundRefreshing] = useState(false);
const [lastUpdateTime, setLastUpdateTime] = useState<Date | null>(null);
const [connectionStatus, setConnectionStatus] = useState<'connected' | 'disconnected' | 'checking'>('checking');
// 使用 ref 来存储最新的状态,避免定时器闭包问题
const stateRef = useRef({ isUsingMockData, dashboardData });
// 智能请求控制和性能监控
const requestInProgress = useRef(false);
const lastRequestTime = useRef(0);
const lastRequestDuration = useRef(0); // 记录上次请求耗时
const performanceHistory = useRef<number[]>([]); // 性能历史记录
// 初始加载数据
const fetchInitialData = async () => {
try {
setLoading(true);
setError(null);
setConnectionStatus('checking');
console.log('正在获取项目数据项目ID:', projectId);
// 调用新的任务列表API
const response = await getProjectTaskList({ project_id: projectId });
console.log('API响应:', response);
if (response.code === 0 && response.data) {
// 直接使用新API数据不进行格式转换
setDashboardData(response.data);
setIsUsingMockData(false);
setLastUpdateTime(new Date());
setConnectionStatus('connected');
console.log('成功获取真实数据:', response.data);
} else {
console.warn('API返回错误或无数据使用演示数据');
setDashboardData(mockDashboardData);
setIsUsingMockData(true);
setLastUpdateTime(new Date());
setConnectionStatus('disconnected');
setError(`API返回: ${response.message || '无数据'} (已切换到演示模式)`);
}
} catch (err: any) {
console.error('获取数据失败:', err);
// 详细的错误分析
let errorMessage = '未知错误';
if (err.code === 'NETWORK_ERROR' || err.message?.includes('Network Error')) {
errorMessage = '网络连接失败,请检查网络连接';
} else if (err.code === 'ECONNABORTED' || err.message?.includes('timeout')) {
errorMessage = '请求超时,服务器响应缓慢';
} else if (err.response?.status === 404) {
errorMessage = 'API接口不存在';
} else if (err.response?.status === 500) {
errorMessage = '服务器内部错误';
} else if (err.response?.status === 403) {
errorMessage = '访问被拒绝,请检查权限';
} else if (err.message) {
errorMessage = err.message;
}
// 如果API调用失败回退到演示数据
console.log('API调用失败使用演示数据');
setDashboardData(mockDashboardData);
setIsUsingMockData(true);
setLastUpdateTime(new Date());
setConnectionStatus('disconnected');
setError(`${errorMessage} (已切换到演示模式)`);
} finally {
setLoading(false);
}
};
// 优化的数据变化检测避免深度JSON比较但保持准确性
const hasDataChanged = (newData: any, oldData: any) => {
if (!oldData || !newData) return true;
if (!Array.isArray(newData) || !Array.isArray(oldData)) return true;
if (newData.length !== oldData.length) return true;
// 添加性能监控
const startTime = Date.now();
try {
// 快速比较:只检查关键字段,但保持完整性
for (let i = 0; i < newData.length; i++) {
const newTask = newData[i];
const oldTask = oldData[i];
if (!newTask || !oldTask) return true;
// 比较关键字段
if (newTask.task_id !== oldTask.task_id ||
newTask.task_status !== oldTask.task_status ||
newTask.progress !== oldTask.progress ||
newTask.end_time !== oldTask.end_time ||
newTask.start_time !== oldTask.start_time) {
return true;
}
// 检查子任务变化(保持原有逻辑)
const newSubCount = newTask.sub_tasks?.length || 0;
const oldSubCount = oldTask.sub_tasks?.length || 0;
if (newSubCount !== oldSubCount) return true;
// 检查task_result的关键变化
if (newTask.task_result?.total_count !== oldTask.task_result?.total_count ||
newTask.task_result?.completed_count !== oldTask.task_result?.completed_count) {
return true;
}
}
return false; // 没有关键变化
} finally {
const endTime = Date.now();
if (endTime - startTime > 10) { // 只记录超过10ms的比较
console.log(`[性能] 数据比较耗时: ${endTime - startTime}ms`);
}
}
};
// 智能后台刷新支持10秒间隔但避免重叠
const refreshDataSilently = async () => {
// 防止重复请求
if (requestInProgress.current) {
console.log('[刷新] 请求进行中,跳过本次刷新');
return;
}
// 智能频率控制:根据上次请求耗时动态调整最小间隔
const now = Date.now();
const timeSinceLastRequest = now - lastRequestTime.current;
// 如果上次请求还没完成足够长时间,跳过本次请求
// 这样可以支持10秒间隔但避免在后端处理慢时重叠
if (timeSinceLastRequest < 2000) {
console.log(`[刷新] 距离上次请求仅${timeSinceLastRequest}ms跳过本次刷新`);
return;
}
try {
requestInProgress.current = true;
lastRequestTime.current = now;
setIsBackgroundRefreshing(true);
console.log('[刷新] 开始后台数据刷新...');
const startTime = Date.now();
// 调用优化后的任务列表API
const response = await getProjectTaskList({ project_id: projectId });
const endTime = Date.now();
const duration = endTime - startTime;
// 更新性能监控数据
lastRequestDuration.current = duration;
performanceHistory.current.push(duration);
// 只保留最近10次的性能数据
if (performanceHistory.current.length > 10) {
performanceHistory.current.shift();
}
// 计算平均响应时间
const avgDuration = performanceHistory.current.reduce((a, b) => a + b, 0) / performanceHistory.current.length;
console.log(`[刷新] API调用完成耗时: ${duration}ms (平均: ${Math.round(avgDuration)}ms)`);
// 智能性能警告:基于历史数据判断
if (duration > 5000) {
console.warn(`[性能警告] 后端API处理过慢: ${duration}ms平均耗时: ${Math.round(avgDuration)}ms`);
}
// 如果平均响应时间超过8秒建议调整轮询策略
if (avgDuration > 8000) {
console.warn(`[轮询建议] 平均响应时间${Math.round(avgDuration)}ms建议考虑延长轮询间隔`);
}
if (response.code === 0 && response.data) {
// 使用优化的数据比较
if (hasDataChanged(response.data, dashboardData)) {
console.log('[刷新] 检测到数据变化更新UI');
setDashboardData(response.data);
setIsUsingMockData(false);
setLastUpdateTime(new Date());
setConnectionStatus('connected');
setError(null);
} else {
console.log('[刷新] 数据无变化,仅更新时间戳');
setLastUpdateTime(new Date());
setConnectionStatus('connected');
}
} else {
console.warn('[刷新] API返回错误保持当前数据');
setConnectionStatus('disconnected');
}
} catch (err: any) {
console.error('[刷新] 后台刷新失败:', err);
setConnectionStatus('disconnected');
// 如果是超时错误,给用户更明确的提示
if (err.message?.includes('超时')) {
setError('网络连接超时,请检查网络状态');
}
} finally {
requestInProgress.current = false;
setIsBackgroundRefreshing(false);
}
};
// 智能轮询策略:动态调整间隔,避免请求重叠
const getRefreshInterval = React.useCallback(() => {
if (!dashboardData || isUsingMockData) return 60000; // mock数据时60秒刷新一次
// 检查是否有正在运行的任务
const hasRunningTasks = Array.isArray(dashboardData) &&
dashboardData.some((task: any) =>
task.task_status === 'IN_PROGRESS' ||
task.task_status === 'INIT' ||
task.task_status === 'RETRYING' || // 包含重试状态
(task.sub_tasks && Array.isArray(task.sub_tasks) &&
task.sub_tasks.some((subTask: any) =>
subTask.task_status === 'IN_PROGRESS' ||
subTask.task_status === 'INIT' ||
subTask.task_status === 'RETRYING'
))
);
// 根据连接状态调整间隔
if (connectionStatus === 'disconnected') {
return 60000; // 连接断开时降低频率
}
// 智能间隔策略基于实际API性能动态调整
// 目标10秒间隔但要避免请求重叠
if (hasRunningTasks) {
// 有运行任务时使用10秒间隔但通过请求去重避免重叠
return 10000;
} else {
// 无运行任务时使用较长间隔节省资源
return 30000;
}
}, [dashboardData, isUsingMockData, connectionStatus]);
// 初始加载数据 - 只在 projectId 变化时执行
useEffect(() => {
fetchInitialData();
}, [projectId]); // 只依赖 projectId
// 更新 ref 中的状态
useEffect(() => {
stateRef.current = { isUsingMockData, dashboardData };
}, [isUsingMockData, dashboardData]);
// 优化的智能定时刷新 - 根据数据状态动态调整刷新间隔
useEffect(() => {
if (!dashboardData) return; // 没有数据时不设置定时器
const refreshInterval = getRefreshInterval();
const hasRunningTasks = Array.isArray(dashboardData) && dashboardData.some((task: any) =>
task.task_status === 'IN_PROGRESS' || task.task_status === 'INIT' || task.task_status === 'RETRYING'
);
// 计算当前平均响应时间用于日志显示
const avgDuration = performanceHistory.current.length > 0
? Math.round(performanceHistory.current.reduce((a, b) => a + b, 0) / performanceHistory.current.length)
: 0;
console.log(`[轮询] 设置刷新间隔: ${refreshInterval / 1000}秒 (有运行任务: ${hasRunningTasks}, 平均响应: ${avgDuration}ms)`);
const interval = setInterval(() => {
// 使用 ref 中的最新状态
if (!stateRef.current.isUsingMockData) {
console.log('[轮询] 执行定时刷新');
refreshDataSilently();
} else {
console.log('[轮询] 跳过刷新 (使用Mock数据)');
}
}, refreshInterval);
return () => {
console.log('[轮询] 清理定时器');
clearInterval(interval);
};
}, [getRefreshInterval]); // 只依赖 getRefreshInterval
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>
);
}
if (error && !dashboardData) {
return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
<div className="text-center max-w-md mx-auto p-8 bg-black/20 backdrop-blur-sm rounded-xl border border-white/10">
<div className="text-red-400 text-6xl mb-6"></div>
<h2 className="text-white text-2xl font-bold mb-4"></h2>
<p className="text-gray-300 mb-6 leading-relaxed">{error}</p>
<div className="space-y-3">
<button
onClick={fetchInitialData}
disabled={loading}
className="w-full px-6 py-3 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<>
<div className="w-4 h-4 animate-spin rounded-full border-b-2 border-white"></div>
<span>...</span>
</>
) : (
<>
<svg className="w-4 h-4" 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>
<span></span>
</>
)}
</button>
<button
onClick={() => {
setDashboardData(mockDashboardData);
setIsUsingMockData(true);
setError(null);
setConnectionStatus('disconnected');
}}
className="w-full px-6 py-3 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors"
>
使
</button>
</div>
<div className="mt-6 text-sm text-gray-400">
<p></p>
<ul className="mt-2 text-left space-y-1">
<li> </li>
<li> 访</li>
<li> ID是否正确</li>
</ul>
</div>
</div>
</div>
);
}
return (
<div className="h-full bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex flex-col overflow-hidden">
{/* 后台刷新指示器 - 优化用户体验 */}
{isBackgroundRefreshing && (
<div className="fixed top-4 right-4 z-50 bg-blue-500/90 backdrop-blur-sm text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 max-w-xs">
<div className="w-4 h-4 animate-spin rounded-full border-b-2 border-white"></div>
<div className="flex flex-col">
<span className="text-sm font-medium">...</span>
</div>
</div>
)}
{/* 手动刷新按钮和状态指示器 */}
<div className="fixed bottom-4 right-4 z-40 flex flex-col items-end gap-2">
{/* 手动刷新按钮 */}
<button
onClick={refreshDataSilently}
disabled={isBackgroundRefreshing}
className="bg-blue-500/80 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed backdrop-blur-sm text-white p-2 rounded-full shadow-lg transition-all duration-200 hover:scale-105"
title="手动刷新数据"
>
<svg className={`w-4 h-4 ${isBackgroundRefreshing ? '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>
</div>
{/* 顶部状态栏 */}
<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-cyan-400 animate-pulse"></div>
<h1 className="text-xl font-bold text-white">Video Flow Dashboard</h1>
</div>
<div className="text-sm text-gray-400">
ID: <span className="font-mono text-gray-300">{projectId}</span>
</div>
</div>
{/* 连接状态和统计 */}
<div className="flex items-center gap-6">
{/* 连接状态指示器 */}
<div className="flex items-center gap-2">
<div className={cn(
"w-2 h-2 rounded-full",
connectionStatus === 'connected' ? 'bg-emerald-400' :
connectionStatus === 'checking' ? 'bg-amber-400 animate-pulse' :
'bg-rose-400'
)}></div>
<span className="text-sm text-gray-400">
{connectionStatus === 'connected' ? '实时连接' :
connectionStatus === 'checking' ? '连接中...' :
'离线模式'}
</span>
</div>
{/* 最后更新时间 */}
{lastUpdateTime && (
<div className="text-sm text-gray-500">
{lastUpdateTime.toLocaleTimeString()}
</div>
)}
</div>
</div>
</div>
{/* 主内容区域 */}
<div className="flex-1 min-h-0">
<NetworkTimeline
tasks={dashboardData || []}
onRefresh={fetchInitialData}
isRefreshing={loading}
isPolling={!isBackgroundRefreshing}
lastUpdate={lastUpdateTime || undefined}
onTogglePolling={() => {
console.log('切换轮询状态');
}}
onRetryTask={async (taskId: string) => {
console.log('重试任务:', taskId);
try {
// 1. 乐观更新:立即更新任务状态为重试中
setDashboardData(prevData => {
if (!Array.isArray(prevData)) return prevData;
return prevData.map(task => {
if (task.task_id === taskId) {
return { ...task, task_status: 'RETRYING' };
}
// 检查子任务
if (task.sub_tasks && Array.isArray(task.sub_tasks)) {
const updatedSubTasks = task.sub_tasks.map(subTask =>
subTask.task_id === taskId
? { ...subTask, task_status: 'RETRYING' }
: subTask
);
return { ...task, sub_tasks: updatedSubTasks };
}
return task;
});
});
// 2. 调用重试任务API
const retryResponse = await retryTask({ task_id: taskId });
if (retryResponse.code === 0 && retryResponse.data?.success) {
console.log('任务重试成功:', retryResponse.data);
// 3. 重试成功后,使用静默刷新获取最新状态
setTimeout(() => {
refreshDataSilently();
}, 2000); // 给后端一些处理时间
} else {
console.error('任务重试失败:', retryResponse.message);
// 4. 重试失败,恢复原状态并静默刷新
setTimeout(() => {
refreshDataSilently();
}, 1000);
}
} catch (error) {
console.error('重试任务时发生错误:', error);
// 5. 发生错误时,恢复状态并静默刷新
setTimeout(() => {
refreshDataSilently();
}, 1000);
}
}}
/>
</div>
</div>
);
}