364 lines
14 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 } 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 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);
}
};
// 深度比较数据是否发生变化
const hasDataChanged = (newData: any, oldData: any) => {
if (!oldData || !newData) return true;
try {
return JSON.stringify(newData) !== JSON.stringify(oldData);
} catch {
return true; // 如果比较失败,认为数据已变化
}
};
// 后台静默刷新数据
const refreshDataSilently = async () => {
try {
setIsBackgroundRefreshing(true);
console.log('后台刷新数据...');
// 调用新的任务列表API
const response = await getProjectTaskList({ project_id: projectId });
if (response.code === 0 && response.data) {
// 直接使用新API数据检查数据是否真正发生变化
if (hasDataChanged(response.data, dashboardData)) {
// 只有数据变化时才更新UI
setDashboardData(response.data);
setIsUsingMockData(false);
setLastUpdateTime(new Date());
setConnectionStatus('connected');
setError(null); // 清除之前的错误
console.log('后台数据更新成功 - 数据已变化');
} else {
// 数据未变化,只更新时间戳
setLastUpdateTime(new Date());
setConnectionStatus('connected');
console.log('后台数据检查完成 - 数据无变化');
}
} else {
console.warn('后台刷新失败,保持当前数据');
setConnectionStatus('disconnected');
}
} catch (err: any) {
console.error('后台刷新失败:', err);
setConnectionStatus('disconnected');
// 后台刷新失败时不影响当前显示,只记录日志和更新连接状态
} finally {
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.sub_tasks && Array.isArray(task.sub_tasks) &&
task.sub_tasks.some((subTask: any) =>
subTask.task_status === 'IN_PROGRESS' ||
subTask.task_status === 'INIT'
))
);
return hasRunningTasks ? 10000 : 30000; // 有运行任务时10秒否则30秒
}, [dashboardData, isUsingMockData]);
// 初始加载数据 - 只在 projectId 变化时执行
useEffect(() => {
fetchInitialData();
}, [projectId]); // 只依赖 projectId
// 更新 ref 中的状态
useEffect(() => {
stateRef.current = { isUsingMockData, dashboardData };
}, [isUsingMockData, dashboardData]);
// 智能定时刷新 - 根据数据状态动态调整刷新间隔
useEffect(() => {
if (!dashboardData) return; // 没有数据时不设置定时器
const refreshInterval = getRefreshInterval();
console.log(`设置刷新间隔: ${refreshInterval / 1000}`);
const interval = setInterval(() => {
// 使用 ref 中的最新状态
if (!stateRef.current.isUsingMockData) {
refreshDataSilently();
}
}, refreshInterval);
return () => 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">
<div className="w-4 h-4 animate-spin rounded-full border-b-2 border-white"></div>
<span className="text-sm">...</span>
</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 className="bg-black/50 backdrop-blur-sm text-white/70 px-3 py-1 rounded text-xs whitespace-nowrap flex items-center gap-2">
{/* 连接状态指示器 */}
<div className="flex items-center gap-1">
<div className={`w-2 h-2 rounded-full ${
connectionStatus === 'connected' ? 'bg-green-400' :
connectionStatus === 'disconnected' ? 'bg-red-400' :
'bg-yellow-400 animate-pulse'
}`} />
<span className="text-xs">
{connectionStatus === 'connected' ? '已连接' :
connectionStatus === 'disconnected' ? '离线模式' :
'连接中...'}
</span>
</div>
{/* 分隔符 */}
{lastUpdateTime && <span>|</span>}
{/* 最后更新时间 */}
{lastUpdateTime && (
<span>: {lastUpdateTime.toLocaleTimeString()}</span>
)}
</div>
</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);
await fetchInitialData();
}}
/>
</div>
</div>
);
}