forked from 77media/video-flow
364 lines
14 KiB
TypeScript
364 lines
14 KiB
TypeScript
'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>
|
||
);
|
||
}
|