video-flow-b/components/dashboard/network-timeline.tsx
qikongjian cf5d86c840 feat: Dashboard optimization with custom scrollbar styles and layout improvements
- Added custom scrollbar styles for better UX
- Optimized dashboard layout components
- Enhanced network timeline functionality
- Cleaned up redundant dashboard components
- Improved API integration for video flow
2025-08-23 23:26:06 +08:00

1066 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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, useMemo, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Network,
Clock,
CheckCircle,
Search,
ChevronRight,
ChevronDown,
RefreshCw,
XCircle,
Info,
Activity,
Pause
} from 'lucide-react';
import { cn } from '@/public/lib/utils';
interface TaskExecution {
id: string;
name: string;
displayName: string;
status: string;
statusCode: number;
type: string;
dataSize: number;
executionTime: number;
startTime: number;
endTime: number;
progress: number;
level: number; // 层级0=主任务1=子任务
parentId?: string; // 父任务ID
isExpanded?: boolean; // 是否展开子任务
subTasks?: TaskExecution[]; // 子任务列表
phases: {
initialization?: number;
dataLoading?: number;
aiProcessing?: number;
resultGeneration?: number;
dataTransfer?: number;
completion?: number;
};
taskResult?: any;
}
interface TaskTimelineProps {
tasks: any[];
className?: string;
onRefresh?: () => void;
isRefreshing?: boolean;
onRetryTask?: (taskId: string) => Promise<void>;
// 实时监控相关
isPolling?: boolean;
lastUpdate?: Date;
onTogglePolling?: () => void;
}
export function NetworkTimeline({
tasks,
className,
onRefresh,
isRefreshing = false,
onRetryTask,
isPolling = false,
lastUpdate,
onTogglePolling
}: TaskTimelineProps) {
const [selectedTask, setSelectedTask] = useState<string | null>(null);
const [filterType, setFilterType] = useState<string>('all');
const [searchTerm, setSearchTerm] = useState('');
const [expandedTasks, setExpandedTasks] = useState<Set<string>>(new Set());
const [timeAgo, setTimeAgo] = useState<string>('');
// 更新时间显示
useEffect(() => {
if (!lastUpdate) return;
const updateTimeAgo = () => {
const now = new Date();
const diff = Math.floor((now.getTime() - lastUpdate.getTime()) / 1000);
if (diff < 60) {
setTimeAgo(`${diff}秒前`);
} else if (diff < 3600) {
setTimeAgo(`${Math.floor(diff / 60)}分钟前`);
} else {
setTimeAgo(`${Math.floor(diff / 3600)}小时前`);
}
};
updateTimeAgo();
const interval = setInterval(updateTimeAgo, 1000);
return () => clearInterval(interval);
}, [lastUpdate]);
// 计算任务统计信息
const taskStats = useMemo(() => {
if (!tasks || tasks.length === 0) {
return { total: 0, completed: 0, inProgress: 0, failed: 0, pending: 0 };
}
const stats = {
total: tasks.length,
completed: 0,
inProgress: 0,
failed: 0,
pending: 0
};
tasks.forEach((task: any) => {
switch (task.task_status) {
case 'COMPLETED':
stats.completed++;
break;
case 'IN_PROGRESS':
stats.inProgress++;
break;
case 'FAILED':
stats.failed++;
break;
case 'PENDING':
default:
stats.pending++;
break;
}
});
return stats;
}, [tasks]);
// 将任务转换为执行时间线格式(支持层级结构)
const taskExecutions = useMemo((): TaskExecution[] => {
if (!tasks || tasks.length === 0) return [];
const startTime = Math.min(...tasks.map(task => new Date(task.created_at).getTime()));
const result: TaskExecution[] = [];
tasks.forEach((task: any) => {
const taskStartTime = new Date(task.created_at).getTime();
const taskEndTime = new Date(task.updated_at).getTime();
const duration = taskEndTime - taskStartTime;
// 任务执行阶段分解
const phases = {
initialization: Math.min(duration * 0.05, 1000),
dataLoading: Math.min(duration * 0.1, 2000),
aiProcessing: duration * 0.7,
resultGeneration: Math.min(duration * 0.1, 1000),
dataTransfer: Math.min(duration * 0.05, 500),
completion: Math.min(duration * 0.05, 500)
};
// 主任务
const mainTask: TaskExecution = {
id: task.task_id,
name: task.task_name,
displayName: getTaskDisplayName(task.task_name),
status: task.task_status,
statusCode: getTaskStatusCode(task.task_status),
type: getTaskType(task.task_name),
dataSize: getTaskDataSize(task),
executionTime: duration,
startTime: taskStartTime - startTime,
endTime: taskEndTime - startTime,
progress: task.task_result?.progress_percentage || 0,
level: 0,
isExpanded: expandedTasks.has(task.task_id),
subTasks: [],
phases,
taskResult: task.task_result
};
result.push(mainTask);
// 处理子任务(如果有的话)
if (task.task_result?.data && Array.isArray(task.task_result.data)) {
const subTasks = task.task_result.data.map((subItem: any, subIndex: number) => {
// 为子任务计算时间分布
const totalCount = task.task_result.total_count || task.task_result.data.length;
const subDuration = duration / totalCount;
const subStartTime = taskStartTime - startTime + (subIndex * subDuration);
const subEndTime = subStartTime + subDuration;
const subPhases = {
initialization: subDuration * 0.05,
dataLoading: subDuration * 0.1,
aiProcessing: subDuration * 0.7,
resultGeneration: subDuration * 0.1,
dataTransfer: subDuration * 0.03,
completion: subDuration * 0.02
};
return {
id: subItem.video_id || `${task.task_id}-sub-${subIndex}`,
name: `${task.task_name}_item_${subIndex + 1}`,
displayName: `${getTaskDisplayName(task.task_name)} - 项目 ${subIndex + 1}`,
status: subItem.video_status === 1 ? 'COMPLETED' :
subIndex < (task.task_result.completed_count || 0) ? 'COMPLETED' :
subIndex === (task.task_result.completed_count || 0) ? 'IN_PROGRESS' : 'PENDING',
statusCode: subItem.video_status === 1 ? 200 :
subIndex < (task.task_result.completed_count || 0) ? 200 :
subIndex === (task.task_result.completed_count || 0) ? 202 : 100,
type: getTaskType(task.task_name),
dataSize: getSubTaskDataSize(subItem),
executionTime: subDuration,
startTime: subStartTime,
endTime: subEndTime,
progress: subItem.video_status === 1 ? 100 :
subIndex < (task.task_result.completed_count || 0) ? 100 :
subIndex === (task.task_result.completed_count || 0) ? (task.task_result.progress_percentage || 0) : 0,
level: 1,
parentId: task.task_id,
phases: subPhases,
taskResult: subItem
};
});
mainTask.subTasks = subTasks;
// 如果主任务展开,将子任务添加到结果中
if (expandedTasks.has(task.task_id)) {
result.push(...subTasks);
}
}
});
return result;
}, [tasks, expandedTasks]);
// 过滤后的任务执行列表
const filteredTaskExecutions = useMemo(() => {
let filtered = taskExecutions;
// 按类型过滤
if (filterType !== 'all') {
filtered = filtered.filter(task => task.type === filterType);
}
// 按搜索词过滤
if (searchTerm.trim()) {
const searchLower = searchTerm.toLowerCase();
filtered = filtered.filter(task =>
task.displayName.toLowerCase().includes(searchLower) ||
task.name.toLowerCase().includes(searchLower) ||
task.status.toLowerCase().includes(searchLower) ||
task.type.toLowerCase().includes(searchLower)
);
}
return filtered;
}, [taskExecutions, filterType, searchTerm]);
// 辅助函数
function getTaskDisplayName(taskName: string): string {
const nameMap: Record<string, string> = {
'generate_character': '角色生成',
'generate_sketch': '草图生成',
'generate_shot_sketch': '分镜生成',
'generate_video': '视频生成',
'generate_videos': '视频生成',
'generate_music': '音乐生成',
'final_composition': '最终合成'
};
return nameMap[taskName] || taskName;
}
function getTaskMethod(taskName: string): string {
return taskName.includes('generate') ? 'POST' : 'GET';
}
function getTaskStatusCode(status: string): number {
const statusMap: Record<string, number> = {
'COMPLETED': 200,
'IN_PROGRESS': 202,
'PENDING': 100,
'FAILED': 500
};
return statusMap[status] || 200;
}
function getTaskType(taskName: string): string {
const typeMap: Record<string, string> = {
'generate_character': 'AI',
'generate_sketch': 'AI',
'generate_shot_sketch': 'AI',
'generate_video': 'Video',
'generate_videos': 'Video',
'generate_music': 'Audio',
'final_composition': 'Comp'
};
return typeMap[taskName] || 'Task';
}
function getTaskDataSize(task: any): number {
const baseSize = 1024; // 1KB base
const dataCount = task.task_result?.total_count || 1;
const completedCount = task.task_result?.completed_count || 0;
return baseSize * completedCount * (Math.random() * 5 + 1);
}
function getSubTaskDataSize(subItem: any): number {
const baseSize = 512; // 0.5KB base for sub-items
if (subItem.urls && subItem.urls.length > 0) {
// 如果有URL估算为较大的文件
return baseSize * 10 * (Math.random() * 3 + 1);
}
return baseSize * (Math.random() * 2 + 1);
}
// 获取任务进度数量显示 - 与任务执行状态面板完全一致
function getTaskProgressCount(task: any): string {
const completed = task.task_result?.completed_count;
const total = task.task_result?.total_count || 0;
// 如果任务已完成但没有子任务数据,显示 1/1
if (task.task_status === 'COMPLETED' && total === 0) {
return '1/1';
}
// 如果任务已完成且有子任务数据确保completed等于total
if (task.task_status === 'COMPLETED' && total > 0) {
// 如果没有completed_count字段但任务已完成说明全部完成
const actualCompleted = completed !== undefined ? completed : total;
return `${actualCompleted}/${total}`;
}
// 其他情况显示实际数据
const actualCompleted = completed || 0;
return `${actualCompleted}/${total}`;
}
// 获取任务进度百分比显示 - 与任务执行状态面板完全一致
function getTaskProgressPercentage(task: any): string {
const completed = task.task_result?.completed_count || 0;
const total = task.task_result?.total_count || 0;
const progress = task.task_result?.progress_percentage || 0;
// 如果任务已完成显示100%
if (task.task_status === 'COMPLETED') {
return '100%';
}
// 如果任务进行中优先使用API返回的progress_percentage
if (task.task_status === 'IN_PROGRESS') {
if (progress > 0) {
return `${progress}%`;
}
// 如果没有progress_percentage根据completed/total计算
if (total > 0) {
return `${Math.round((completed / total) * 100)}%`;
}
}
// 其他情况显示0%
return '0%';
}
// 获取任务状态显示文本 - 与任务执行状态面板完全一致
function getTaskStatusText(status: string): string {
switch (status) {
case 'COMPLETED':
return 'COMPLETED';
case 'IN_PROGRESS':
return 'IN_PROGRESS';
case 'FAILED':
return 'FAILED';
case 'PENDING':
default:
return 'PENDING';
}
}
// 获取任务状态颜色 - 更明显的颜色区分
function getTaskStatusColor(status: string): string {
switch (status) {
case 'COMPLETED':
return 'text-emerald-400'; // 更亮的绿色
case 'IN_PROGRESS':
return 'text-cyan-400'; // 更亮的蓝色
case 'FAILED':
return 'text-rose-400'; // 更亮的红色
case 'PENDING':
default:
return 'text-amber-400'; // 黄色表示等待
}
}
// 获取进度条背景颜色类名
function getProgressBarColor(status: string): string {
switch (status) {
case 'COMPLETED':
return 'bg-emerald-500'; // 绿色
case 'IN_PROGRESS':
return 'bg-cyan-500'; // 蓝色
case 'FAILED':
return 'bg-rose-500'; // 红色
case 'PENDING':
default:
return 'bg-amber-500'; // 黄色
}
}
// 获取任务错误信息
function getTaskErrorInfo(task: any): { hasError: boolean; errorMessage: string; errorCode?: string } {
if (task.task_status !== 'FAILED') {
return { hasError: false, errorMessage: '' };
}
// 从task_result中提取错误信息
const errorMessage = task.task_result?.error_message ||
task.task_result?.message ||
task.error_message ||
'任务执行失败,请重试';
const errorCode = task.task_result?.error_code ||
task.error_code ||
'UNKNOWN_ERROR';
return {
hasError: true,
errorMessage,
errorCode
};
}
// 处理重试任务
const handleRetryTask = async (taskId: string) => {
if (onRetryTask) {
try {
await onRetryTask(taskId);
// 重试成功后可以显示成功提示
} catch (error) {
console.error('重试任务失败:', error);
// 可以显示错误提示
}
}
};
// 格式化文件大小
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
// 智能时间格式化 - 提升用户体验
function formatTime(ms: number): string {
if (ms < 1000) {
return `${Math.round(ms)}ms`;
}
const seconds = Math.floor(ms / 1000);
// 小于1分钟显示秒
if (seconds < 60) {
return `${seconds}s`;
}
// 1分钟到59分钟显示分钟和秒
if (seconds < 3600) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (remainingSeconds === 0) {
return `${minutes}m`;
}
return `${minutes}m ${remainingSeconds}s`;
}
// 1小时到23小时显示小时和分钟
if (seconds < 86400) {
const hours = Math.floor(seconds / 3600);
const remainingMinutes = Math.floor((seconds % 3600) / 60);
if (remainingMinutes === 0) {
return `${hours}h`;
}
return `${hours}h ${remainingMinutes}m`;
}
// 24小时以上显示天和小时
const days = Math.floor(seconds / 86400);
const remainingHours = Math.floor((seconds % 86400) / 3600);
if (remainingHours === 0) {
return `${days}d`;
}
return `${days}d ${remainingHours}h`;
}
// 获取时间显示的颜色类名(基于时长)
function getTimeDisplayColor(ms: number): string {
const seconds = Math.floor(ms / 1000);
if (ms < 1000) return 'text-gray-400'; // 毫秒 - 灰色
if (seconds < 60) return 'text-gray-200'; // 秒 - 浅灰
if (seconds < 3600) return 'text-white'; // 分钟 - 白色
if (seconds < 86400) return 'text-amber-300'; // 小时 - 琥珀色
return 'text-rose-300'; // 天 - 玫瑰色(警示长时间)
}
// 估算剩余时间(基于当前进度和已用时间)
function estimateRemainingTime(task: any): string | null {
if (task.task_status !== 'IN_PROGRESS') return null;
const progress = task.task_result?.progress_percentage || 0;
if (progress <= 0 || progress >= 100) return null;
const now = new Date().getTime();
const startTime = new Date(task.created_at).getTime();
const elapsedTime = now - startTime;
// 基于当前进度估算总时间
const estimatedTotalTime = (elapsedTime / progress) * 100;
const remainingTime = estimatedTotalTime - elapsedTime;
if (remainingTime <= 0) return null;
return formatTime(remainingTime);
}
// 获取状态颜色
function getStatusColor(status: number): string {
if (status >= 200 && status < 300) return 'text-green-400';
if (status >= 300 && status < 400) return 'text-yellow-400';
if (status >= 400) return 'text-red-400';
return 'text-blue-400';
}
// 展开/折叠任务
const toggleTaskExpansion = (taskId: string) => {
setExpandedTasks(prev => {
const newSet = new Set(prev);
if (newSet.has(taskId)) {
newSet.delete(taskId);
} else {
newSet.add(taskId);
}
return newSet;
});
};
// 计算时间线位置
const maxTime = Math.max(...(filteredTaskExecutions.length > 0 ? filteredTaskExecutions.map((task: TaskExecution) => task.endTime) : [0]));
return (
<div className={cn("h-full flex flex-col bg-gray-950 overflow-hidden", className)}>
{/* 工具栏 */}
<div className="flex items-center justify-between px-6 py-4 bg-gray-900 border-b border-gray-800 flex-shrink-0">
<div className="flex items-center gap-4">
<Network className="w-5 h-5 text-cyan-400" />
<h3 className="text-lg font-semibold text-white">线</h3>
{/* 任务统计 */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-1 px-3 py-1.5 bg-gray-800 rounded-md">
<span className="text-sm text-gray-400">:</span>
<span className="text-sm font-bold text-white">{taskStats.total}</span>
</div>
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-600/20 rounded-md border border-emerald-600/30">
<CheckCircle className="w-4 h-4 text-emerald-400" />
<span className="text-sm text-emerald-400 font-bold">{taskStats.completed}</span>
</div>
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-cyan-600/20 rounded-md border border-cyan-600/30">
<Clock className="w-4 h-4 text-cyan-400 animate-pulse" />
<span className="text-sm text-cyan-400 font-bold">{taskStats.inProgress}</span>
</div>
{taskStats.failed > 0 && (
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-rose-600/20 rounded-md border border-rose-600/30">
<XCircle className="w-4 h-4 text-rose-400" />
<span className="text-sm text-rose-400 font-bold">{taskStats.failed}</span>
</div>
)}
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-amber-600/20 rounded-md border border-amber-600/30">
<Pause className="w-4 h-4 text-amber-400" />
<span className="text-sm text-amber-400 font-bold">{taskStats.pending}</span>
</div>
</div>
</div>
<div className="flex items-center gap-3">
{/* 实时监控控制 */}
{onTogglePolling && (
<div className="flex items-center gap-3">
<button
onClick={onTogglePolling}
className={cn(
"flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all",
isPolling
? "bg-green-600/20 text-green-400 hover:bg-green-600/30"
: "bg-gray-600/20 text-gray-400 hover:bg-gray-600/30"
)}
>
{isPolling ? (
<>
<Activity className="w-4 h-4 animate-pulse" />
<span></span>
</>
) : (
<>
<Pause className="w-4 h-4" />
<span></span>
</>
)}
</button>
{lastUpdate && (
<span className="text-xs text-gray-500">
: {timeAgo}
</span>
)}
</div>
)}
{/* 搜索框 */}
<div className="relative group">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400 group-focus-within:text-cyan-400 transition-colors" />
<input
type="text"
placeholder="搜索任务名称、状态、类型..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-10 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-white placeholder-gray-400 focus:outline-none focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20 transition-all duration-200 hover:border-gray-600"
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400 hover:text-white transition-colors"
title="清除搜索"
>
<XCircle className="w-4 h-4" />
</button>
)}
</div>
{/* 类型过滤 */}
<div className="relative">
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-white focus:outline-none focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20 appearance-none pr-10 hover:border-gray-600 transition-all duration-200"
>
<option value="all"></option>
<option value="AI">AI </option>
<option value="Video"></option>
<option value="Audio"></option>
<option value="Comp"></option>
<option value="Task"></option>
</select>
<ChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
</div>
{/* 重置过滤器按钮 */}
{(searchTerm || filterType !== 'all') && (
<button
onClick={() => {
setSearchTerm('');
setFilterType('all');
}}
className="flex items-center gap-2 px-3 py-2 bg-gray-700 hover:bg-gray-600 text-gray-300 hover:text-white text-sm rounded-lg transition-all duration-200 hover:scale-105"
title="重置所有过滤器"
>
<RefreshCw className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* 主内容区域 */}
<div className="flex-1 flex min-h-0">
{/* 左侧任务列表 */}
<div className="w-1/2 border-r border-gray-800 flex flex-col min-h-0">
{/* 列表头部 */}
<div className="flex items-center px-4 py-2 bg-gray-800/50 text-xs font-medium text-gray-400 border-b border-gray-800 flex-shrink-0">
<div className="w-8"></div>
<div className="w-6"></div> {/* 展开/折叠按钮列 */}
<div className="flex-1"></div>
<div className="w-24 text-center"></div>
<div className="w-16 text-center"></div>
<div className="w-20 text-center"></div>
<div className="w-20 text-center"></div>
</div>
{/* 任务列表 */}
<div className="flex-1 overflow-y-auto min-h-0" style={{ maxHeight: 'calc(100vh - 200px)' }}>
{filteredTaskExecutions.length === 0 ? (
<div className="flex items-center justify-center py-8 text-gray-400">
<div className="text-center">
<Search className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
{(searchTerm || filterType !== 'all') && (
<p className="text-xs mt-1"></p>
)}
</div>
</div>
) : (
filteredTaskExecutions.map((task, index) => (
<motion.div
key={task.id}
className={cn(
"flex items-center px-4 py-2 text-sm border-b border-gray-800/30 cursor-pointer hover:bg-gray-800/50",
selectedTask === task.id && "bg-blue-600/20"
)}
onClick={() => setSelectedTask(selectedTask === task.id ? null : task.id)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: index * 0.05 }}
>
{/* 状态图标 */}
<div className="w-8 flex justify-center">
{task.statusCode === 200 && <CheckCircle className="w-5 h-5 text-emerald-400" />}
{task.statusCode === 202 && <Clock className="w-5 h-5 text-cyan-400 animate-pulse" />}
{task.statusCode === 100 && <Pause className="w-5 h-5 text-amber-400" />}
{task.statusCode >= 400 && <XCircle className="w-5 h-5 text-rose-400" />}
</div>
{/* 展开/折叠按钮 */}
<div className="w-6 flex justify-center">
{task.level === 0 && task.subTasks && task.subTasks.length > 0 && (
<button
onClick={(e) => {
e.stopPropagation();
toggleTaskExpansion(task.id);
}}
className="p-0.5 hover:bg-gray-700 rounded text-gray-400 hover:text-white"
>
{expandedTasks.has(task.id) ?
<ChevronDown className="w-3 h-3" /> :
<ChevronRight className="w-3 h-3" />
}
</button>
)}
</div>
{/* 任务名称 */}
<div className={cn(
"flex-1 flex items-center gap-2",
task.level === 1 && "pl-4"
)}>
<span className={cn(
"truncate font-medium",
task.level === 0 ? "text-white text-base" : "text-sm text-gray-300"
)}>
{task.level === 1 && "└ "}
{task.displayName}
</span>
{/* 错误信息和重试按钮 */}
{task.statusCode >= 400 && (
<div className="flex items-center gap-1">
<button
onClick={(e) => {
e.stopPropagation();
const originalTask = tasks.find((t: any) => t.task_id === task.id);
if (originalTask) {
const errorInfo = getTaskErrorInfo(originalTask);
alert(`错误信息: ${errorInfo.errorMessage}\n错误代码: ${errorInfo.errorCode || 'UNKNOWN'}`);
}
}}
className="p-1 hover:bg-gray-700 rounded text-red-400 hover:text-red-300"
title="查看错误详情"
>
<Info className="w-3 h-3" />
</button>
{onRetryTask && (
<button
onClick={(e) => {
e.stopPropagation();
handleRetryTask(task.id);
}}
className="p-1 hover:bg-gray-700 rounded text-blue-400 hover:text-blue-300"
title="重试任务"
>
<RefreshCw className="w-3 h-3" />
</button>
)}
</div>
)}
</div>
{/* 状态 */}
<div className="w-24 text-center">
<span className={cn("text-sm font-bold", getTaskStatusColor(task.status))}>
{getTaskStatusText(task.status)}
</span>
</div>
{/* 类型 */}
<div className="w-16 text-center">
<span className="text-sm font-medium text-gray-300">{task.type}</span>
</div>
{/* 进度 */}
<div className="w-20 text-center">
<span className="text-sm font-medium text-gray-200">
{task.level === 0 ?
getTaskProgressCount(tasks.find((t: any) => t.task_id === task.id) || {}) :
task.status === 'IN_PROGRESS' ? `${task.progress}%` :
task.status === 'COMPLETED' ? '100%' : '0%'
}
</span>
</div>
{/* 时间 */}
<div className="w-20 text-center">
<div className="flex flex-col items-center">
<span className={cn("text-sm font-medium", getTimeDisplayColor(task.executionTime))}>
{formatTime(task.executionTime)}
</span>
{/* 剩余时间估算 */}
{task.level === 0 && (() => {
const originalTask = tasks.find((t: any) => t.task_id === task.id);
const remainingTime = originalTask ? estimateRemainingTime(originalTask) : null;
return remainingTime ? (
<span className="text-xs text-cyan-400" title="预计剩余时间">
~{remainingTime}
</span>
) : null;
})()}
</div>
</div>
</motion.div>
))
)}
</div>
</div>
{/* 右侧时间线 */}
<div className="w-1/2 flex flex-col min-h-0">
<div className="px-4 py-3 bg-gray-900/50 border-b border-gray-800 flex-shrink-0">
<div className="text-sm font-medium text-gray-300 text-center">
线 (: {formatTime(maxTime)})
</div>
</div>
<div className="flex-1 p-4 overflow-y-auto min-h-0" style={{ maxHeight: 'calc(100vh - 200px)' }}>
{/* 时间刻度 */}
<div className="relative mb-6">
<div className="flex justify-between text-xs mb-3">
{Array.from({ length: 6 }, (_, i) => {
const timeValue = (maxTime / 5) * i;
return (
<span
key={i}
className={cn("font-medium", getTimeDisplayColor(timeValue))}
title={`${formatTime(timeValue)} (${Math.round(timeValue)}ms)`}
>
{formatTime(timeValue)}
</span>
);
})}
</div>
<div className="h-px bg-gradient-to-r from-gray-700 via-gray-600 to-gray-700"></div>
{/* 时间刻度线 */}
<div className="absolute top-8 left-0 right-0 flex justify-between">
{Array.from({ length: 6 }, (_, i) => (
<div key={i} className="w-px h-2 bg-gray-600"></div>
))}
</div>
</div>
{/* 瀑布图 */}
<div className="space-y-1">
{filteredTaskExecutions.length === 0 ? (
<div className="flex items-center justify-center py-8 text-gray-400">
<div className="text-center">
<Activity className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
</div>
</div>
) : (
filteredTaskExecutions.map((task) => (
<div key={task.id} className={cn(
"relative",
task.level === 0 ? "h-6" : "h-4"
)}>
{/* 任务条 */}
<div
className={cn(
"absolute rounded-sm cursor-pointer transition-opacity",
task.level === 0 ? "top-1 h-4" : "top-0.5 h-3",
task.statusCode === 200 ? "bg-emerald-500" :
task.statusCode === 202 ? "bg-cyan-500" :
task.statusCode >= 400 ? "bg-rose-500" : "bg-amber-500",
selectedTask === task.id ? "opacity-100" : "opacity-80 hover:opacity-95",
task.level === 1 && "opacity-70"
)}
style={{
left: `${(task.startTime / maxTime) * 100}%`,
width: `${Math.max((task.executionTime / maxTime) * 100, 0.5)}%`
}}
onClick={() => setSelectedTask(task.id)}
title={`${task.displayName}
执行时间: ${formatTime(task.executionTime)}
状态: ${getTaskStatusText(task.status)}
${task.status === 'IN_PROGRESS' ? `进度: ${task.progress}%` : ''}`}
>
{/* 基于状态的单色进度条 */}
<div className="w-full h-full rounded-sm overflow-hidden relative">
{/* 背景条 */}
<div className="w-full h-full bg-gray-700/30"></div>
{/* 进度条 - 根据任务状态和进度显示 */}
{task.status === 'IN_PROGRESS' && (
<div
className={cn(
"absolute top-0 left-0 h-full transition-all duration-500 ease-out",
getProgressBarColor(task.status)
)}
style={{ width: `${task.progress}%` }}
title={`进度: ${task.progress}%`}
/>
)}
{/* 完成状态显示满进度 */}
{task.status === 'COMPLETED' && (
<div
className={cn(
"absolute top-0 left-0 h-full w-full",
getProgressBarColor(task.status)
)}
title="已完成"
/>
)}
{/* 失败状态显示部分进度 */}
{task.status === 'FAILED' && (
<div
className={cn(
"absolute top-0 left-0 h-full opacity-70",
getProgressBarColor(task.status)
)}
style={{ width: `${Math.max(task.progress, 15)}%` }}
title={`失败于 ${task.progress}%`}
/>
)}
{/* 等待状态显示微弱指示 */}
{task.status === 'PENDING' && (
<div
className={cn(
"absolute top-0 left-0 h-full opacity-40",
getProgressBarColor(task.status)
)}
style={{ width: '8%' }}
title="等待开始"
/>
)}
</div>
{/* 子任务进度指示器 */}
{task.level === 1 && task.status === 'IN_PROGRESS' && (
<div
className="absolute top-0 left-0 h-full bg-white/30 rounded-sm"
style={{ width: `${task.progress}%` }}
/>
)}
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
{/* 详细信息面板 */}
<AnimatePresence>
{selectedTask && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="border-t border-gray-800 p-4"
>
{(() => {
const task = taskExecutions.find((t: TaskExecution) => t.id === selectedTask);
if (!task) return null;
return (
<div className="grid grid-cols-2 gap-6">
<div>
<h4 className="text-sm font-medium text-white mb-2">
{task.level === 0 ? '主任务详情' : '子任务详情'}
</h4>
<div className="space-y-1 text-xs">
<div><span className="text-gray-400">:</span> <span className="text-white">{task.displayName}</span></div>
<div><span className="text-gray-400">ID:</span> <span className="text-white font-mono text-[10px]">{task.name}</span></div>
<div><span className="text-gray-400">:</span> <span className={getTaskStatusColor(task.status)}>{getTaskStatusText(task.status)}</span></div>
<div><span className="text-gray-400">:</span> <span className="text-white">{task.type}</span></div>
<div><span className="text-gray-400">:</span> <span className="text-white">{task.level === 0 ? '主任务' : '子任务'}</span></div>
{task.status === 'IN_PROGRESS' && (
<>
<div><span className="text-gray-400">:</span> <span className="text-white">{task.progress}%</span></div>
{(() => {
const originalTask = tasks.find((t: any) => t.task_id === task.id);
const remainingTime = originalTask ? estimateRemainingTime(originalTask) : null;
return remainingTime ? (
<div><span className="text-gray-400">:</span> <span className="text-cyan-400">{remainingTime}</span></div>
) : null;
})()}
</>
)}
{task.parentId && (
<div><span className="text-gray-400">:</span> <span className="text-white font-mono text-[10px]">{task.parentId}</span></div>
)}
{/* 错误信息显示 */}
{task.statusCode >= 400 && (() => {
const originalTask = tasks.find((t: any) => t.task_id === task.id);
if (originalTask) {
const errorInfo = getTaskErrorInfo(originalTask);
return (
<div className="mt-2 p-2 bg-red-600/10 border border-red-600/20 rounded">
<div className="flex items-center gap-1 mb-1">
<XCircle className="w-3 h-3 text-red-400" />
<span className="text-red-400 font-medium"></span>
</div>
<div className="text-red-300 text-[10px] break-words">{errorInfo.errorMessage}</div>
{errorInfo.errorCode && (
<div className="text-red-400 text-[10px] font-mono mt-1">: {errorInfo.errorCode}</div>
)}
{onRetryTask && (
<button
onClick={() => handleRetryTask(task.id)}
className="mt-2 flex items-center gap-1 px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white text-[10px] rounded transition-colors"
>
<RefreshCw className="w-3 h-3" />
</button>
)}
</div>
);
}
return null;
})()}
</div>
</div>
<div>
<h4 className="text-sm font-medium text-white mb-2"></h4>
<div className="space-y-1 text-xs">
<div><span className="text-gray-400">:</span> <span className="text-white">{formatTime(task.executionTime)}</span></div>
<div><span className="text-gray-400">AI处理:</span> <span className="text-white">{formatTime(task.phases.aiProcessing || 0)}</span></div>
<div><span className="text-gray-400">:</span> <span className="text-white">{formatSize(task.dataSize)}</span></div>
{task.level === 0 && task.taskResult?.total_count && (
<div><span className="text-gray-400">:</span> <span className="text-white">{task.taskResult.total_count}</span></div>
)}
{task.level === 0 && task.taskResult?.completed_count !== undefined && (
<div><span className="text-gray-400">:</span> <span className="text-white">{task.taskResult.completed_count}/{task.taskResult.total_count}</span></div>
)}
{task.level === 1 && task.taskResult?.urls && (
<div><span className="text-gray-400">:</span> <span className="text-white">{task.taskResult.urls.length} </span></div>
)}
</div>
</div>
</div>
);
})()}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export default NetworkTimeline;