forked from 77media/video-flow
1562 lines
63 KiB
TypeScript
1562 lines
63 KiB
TypeScript
'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) => {
|
||
const status = task.task_status;
|
||
|
||
// 成功状态
|
||
if (['COMPLETED', 'SUCCESS', 'FINISHED'].includes(status)) {
|
||
stats.completed++;
|
||
}
|
||
// 进行中状态
|
||
else if (['IN_PROGRESS', 'RUNNING', 'PROCESSING', 'EXECUTING', 'PAUSED', 'SUSPENDED'].includes(status)) {
|
||
stats.inProgress++;
|
||
}
|
||
// 失败状态
|
||
else if (['FAILED', 'FAILURE', 'ERROR', 'CANCELLED', 'TIMEOUT'].includes(status)) {
|
||
stats.failed++;
|
||
}
|
||
// 等待状态
|
||
else if (['PENDING', 'QUEUED', 'WAITING', 'SCHEDULED', 'INIT'].includes(status)) {
|
||
stats.pending++;
|
||
}
|
||
// 未知状态默认为待处理
|
||
else {
|
||
stats.pending++;
|
||
}
|
||
});
|
||
|
||
return stats;
|
||
}, [tasks]);
|
||
|
||
// 主任务列表 - 不依赖展开状态,避免重复计算
|
||
const mainTaskExecutions = useMemo((): TaskExecution[] => {
|
||
if (!tasks || tasks.length === 0) return [];
|
||
|
||
// 获取所有任务的真实开始时间,用于计算时间线的基准点
|
||
const allStartTimes = tasks.map(task => {
|
||
const startTime = task.start_time || task.created_at;
|
||
return new Date(startTime).getTime();
|
||
}).filter(time => !isNaN(time));
|
||
|
||
const globalStartTime = allStartTimes.length > 0 ? Math.min(...allStartTimes) : Date.now();
|
||
const result: TaskExecution[] = [];
|
||
|
||
tasks.forEach((task: any) => {
|
||
// 使用真实的开始时间和结束时间
|
||
const realStartTime = task.start_time || task.created_at;
|
||
const realEndTime = task.end_time || task.updated_at;
|
||
|
||
const taskStartTime = new Date(realStartTime).getTime();
|
||
const taskEndTime = new Date(realEndTime).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)
|
||
};
|
||
|
||
// 主任务 - 直接使用新API数据
|
||
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 - globalStartTime,
|
||
endTime: taskEndTime - globalStartTime,
|
||
progress: getTaskProgress(task),
|
||
level: 0,
|
||
isExpanded: false, // 初始状态,后续动态更新
|
||
subTasks: [],
|
||
phases,
|
||
taskResult: parseTaskResult(task.task_result) // 解析JSON字符串
|
||
};
|
||
|
||
// 预处理子任务数据但不添加到结果中
|
||
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 - globalStartTime + (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;
|
||
}
|
||
|
||
// 处理真实的子任务(新API的sub_tasks字段)- 优先使用真实子任务
|
||
if (task.sub_tasks && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0) {
|
||
const realSubTasks = task.sub_tasks.map((subTask: any, subIndex: number) => {
|
||
const subRealStartTime = subTask.start_time || subTask.created_at;
|
||
const subRealEndTime = subTask.end_time || subTask.updated_at;
|
||
|
||
const subTaskStartTime = new Date(subRealStartTime).getTime();
|
||
const subTaskEndTime = new Date(subRealEndTime).getTime();
|
||
const subTaskDuration = subTaskEndTime - subTaskStartTime;
|
||
|
||
const subPhases = {
|
||
initialization: subTaskDuration * 0.05,
|
||
dataLoading: subTaskDuration * 0.1,
|
||
aiProcessing: subTaskDuration * 0.7,
|
||
resultGeneration: subTaskDuration * 0.1,
|
||
dataTransfer: subTaskDuration * 0.03,
|
||
completion: subTaskDuration * 0.02
|
||
};
|
||
|
||
return {
|
||
id: subTask.task_id,
|
||
name: subTask.task_name,
|
||
displayName: getTaskDisplayName(subTask.task_name),
|
||
status: subTask.task_status, // 保持原始状态
|
||
statusCode: getTaskStatusCode(subTask.task_status),
|
||
type: getTaskType(subTask.task_name),
|
||
dataSize: getTaskDataSize(subTask),
|
||
executionTime: subTaskDuration,
|
||
startTime: subTaskStartTime - globalStartTime,
|
||
endTime: subTaskEndTime - globalStartTime,
|
||
progress: getTaskProgress(subTask),
|
||
level: 1,
|
||
parentId: task.task_id,
|
||
isExpanded: false,
|
||
subTasks: [],
|
||
phases: subPhases,
|
||
taskResult: parseTaskResult(subTask.task_result) // 解析JSON字符串
|
||
};
|
||
});
|
||
|
||
// 真实子任务优先:覆盖之前的subTasks
|
||
mainTask.subTasks = realSubTasks;
|
||
}
|
||
|
||
result.push(mainTask);
|
||
});
|
||
|
||
return result;
|
||
}, [tasks]);
|
||
|
||
// 完整的任务执行列表 - 包含展开的子任务
|
||
const taskExecutions = useMemo((): TaskExecution[] => {
|
||
const result: TaskExecution[] = [];
|
||
|
||
mainTaskExecutions.forEach(mainTask => {
|
||
// 更新展开状态
|
||
mainTask.isExpanded = expandedTasks.has(mainTask.id);
|
||
result.push(mainTask);
|
||
|
||
// 如果展开,添加子任务
|
||
if (mainTask.isExpanded && mainTask.subTasks && mainTask.subTasks.length > 0) {
|
||
result.push(...mainTask.subTasks);
|
||
}
|
||
});
|
||
|
||
return result;
|
||
}, [mainTaskExecutions, expandedTasks]);
|
||
|
||
// 动态获取所有任务类型 - 基于接口数据
|
||
const availableTaskTypes = useMemo(() => {
|
||
const types = new Set<string>();
|
||
taskExecutions.forEach(task => {
|
||
if (task.type) {
|
||
types.add(task.type);
|
||
}
|
||
});
|
||
return Array.from(types).sort();
|
||
}, [taskExecutions]);
|
||
|
||
// 过滤后的任务执行列表
|
||
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 formatDateTime(dateString: string | null | undefined): string {
|
||
if (!dateString) return '未知';
|
||
|
||
try {
|
||
const date = new Date(dateString);
|
||
if (isNaN(date.getTime())) return '无效时间';
|
||
|
||
// 格式化为本地时间:YYYY-MM-DD HH:mm:ss
|
||
return date.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
hour12: false
|
||
});
|
||
} catch (error) {
|
||
console.warn('时间格式化失败:', error, dateString);
|
||
return '格式错误';
|
||
}
|
||
}
|
||
|
||
// 解析任务结果 - 处理JSON字符串
|
||
function parseTaskResult(taskResult: any) {
|
||
if (!taskResult) return null;
|
||
|
||
if (typeof taskResult === 'string') {
|
||
try {
|
||
return JSON.parse(taskResult);
|
||
} catch (error) {
|
||
console.warn('解析task_result失败:', error, taskResult);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
return taskResult;
|
||
}
|
||
|
||
// 获取子任务完成状况文本 (如: "10/12")
|
||
function getSubTaskStatus(task: any): string | null {
|
||
if (!task.sub_tasks || !Array.isArray(task.sub_tasks) || task.sub_tasks.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const completedSubTasks = task.sub_tasks.filter((sub: any) =>
|
||
['SUCCESS', 'COMPLETED', 'FINISHED'].includes(sub.task_status)
|
||
).length;
|
||
|
||
return `${completedSubTasks}/${task.sub_tasks.length}`;
|
||
}
|
||
|
||
// 获取任务进度
|
||
function getTaskProgress(task: any): number {
|
||
// 如果有子任务,优先基于子任务完成情况计算进度
|
||
if (task.sub_tasks && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0) {
|
||
const completedSubTasks = task.sub_tasks.filter((sub: any) =>
|
||
['SUCCESS', 'COMPLETED', 'FINISHED'].includes(sub.task_status)
|
||
).length;
|
||
return Math.round((completedSubTasks / task.sub_tasks.length) * 100);
|
||
}
|
||
|
||
// 其次使用解析后的结果中的进度
|
||
const parsedResult = parseTaskResult(task.task_result);
|
||
if (parsedResult?.progress_percentage) {
|
||
return parsedResult.progress_percentage;
|
||
}
|
||
|
||
// 最后根据状态推断进度
|
||
switch (task.task_status) {
|
||
case 'SUCCESS':
|
||
case 'COMPLETED':
|
||
case 'FINISHED':
|
||
return 100;
|
||
case 'FAILED':
|
||
case 'FAILURE':
|
||
case 'ERROR':
|
||
case 'CANCELLED':
|
||
return 0;
|
||
case 'IN_PROGRESS':
|
||
case 'RUNNING':
|
||
case 'PROCESSING':
|
||
return 50; // 默认50%
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// 获取任务的开始和结束时间
|
||
function getTaskTimes(task: TaskExecution) {
|
||
// 首先尝试从主任务列表中查找
|
||
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
||
|
||
if (originalTask) {
|
||
return {
|
||
startTime: originalTask.start_time || originalTask.created_at,
|
||
endTime: originalTask.end_time || originalTask.updated_at,
|
||
createdAt: originalTask.created_at,
|
||
updatedAt: originalTask.updated_at
|
||
};
|
||
}
|
||
|
||
// 如果是子任务,从父任务的sub_tasks中查找
|
||
if (task.level === 1 && task.parentId) {
|
||
const parentTask = tasks.find((t: any) => t.task_id === task.parentId);
|
||
if (parentTask && parentTask.sub_tasks) {
|
||
const subTask = parentTask.sub_tasks.find((st: any) => st.task_id === task.id);
|
||
if (subTask) {
|
||
return {
|
||
startTime: subTask.start_time || subTask.created_at,
|
||
endTime: subTask.end_time || subTask.updated_at,
|
||
createdAt: subTask.created_at,
|
||
updatedAt: subTask.updated_at
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果都找不到,返回空值
|
||
return {
|
||
startTime: null,
|
||
endTime: null,
|
||
createdAt: null,
|
||
updatedAt: null
|
||
};
|
||
}
|
||
|
||
// 辅助函数
|
||
function getTaskDisplayName(taskName: string): string {
|
||
const nameMap: Record<string, string> = {
|
||
'generate_character': '角色生成',
|
||
'generate_character_image': '角色图片生成',
|
||
'generate_sketch': '草图生成',
|
||
'generate_shot_sketch': '分镜生成',
|
||
'generate_video': '视频生成',
|
||
'generate_videos': '视频生成',
|
||
'generate_music': '音乐生成',
|
||
'final_composition': '最终合成',
|
||
'refine_orginal_script': '脚本优化',
|
||
'generate_production_bible': '制作手册生成',
|
||
'generate_production_bible_json': '制作手册JSON生成'
|
||
};
|
||
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,
|
||
'SUCCESS': 200,
|
||
'FINISHED': 200,
|
||
|
||
// 进行中状态
|
||
'IN_PROGRESS': 202,
|
||
'RUNNING': 202,
|
||
'PROCESSING': 202,
|
||
'EXECUTING': 202,
|
||
|
||
// 等待状态
|
||
'PENDING': 100,
|
||
'QUEUED': 100,
|
||
'WAITING': 100,
|
||
'SCHEDULED': 100,
|
||
'INIT': 100, // 新增:初始化状态
|
||
|
||
// 失败状态
|
||
'FAILED': 500,
|
||
'FAILURE': 500,
|
||
'ERROR': 500,
|
||
'CANCELLED': 499,
|
||
'TIMEOUT': 408,
|
||
|
||
// 暂停状态
|
||
'PAUSED': 202,
|
||
'SUSPENDED': 202
|
||
};
|
||
return statusMap[status] || 200;
|
||
}
|
||
|
||
function getTaskType(taskName: string): string {
|
||
const typeMap: Record<string, string> = {
|
||
'generate_character': 'AI',
|
||
'generate_character_image': 'AI',
|
||
'generate_sketch': 'AI',
|
||
'generate_shot_sketch': 'AI',
|
||
'generate_video': 'Video',
|
||
'generate_videos': 'Video',
|
||
'generate_music': 'Audio',
|
||
'final_composition': 'Comp',
|
||
'refine_orginal_script': 'Script',
|
||
'generate_production_bible': 'Doc',
|
||
'generate_production_bible_json': 'Data'
|
||
};
|
||
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);
|
||
}
|
||
|
||
// 获取任务进度数量显示 - 基于sub_tasks的实际完成状态
|
||
function getTaskProgressCount(task: any): string {
|
||
// 如果有子任务,基于子任务的完成状态计算进度
|
||
if (task.sub_tasks && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0) {
|
||
const totalSubTasks = task.sub_tasks.length;
|
||
const completedSubTasks = task.sub_tasks.filter((subTask: any) =>
|
||
['SUCCESS', 'COMPLETED', 'FINISHED'].includes(subTask.task_status)
|
||
).length;
|
||
|
||
return `${completedSubTasks}/${totalSubTasks}`;
|
||
}
|
||
|
||
// 如果没有子任务,基于主任务状态
|
||
if (['SUCCESS', 'COMPLETED', 'FINISHED'].includes(task.task_status)) {
|
||
return '1/1'; // 单任务已完成
|
||
}
|
||
|
||
if (['IN_PROGRESS', 'RUNNING', 'PROCESSING'].includes(task.task_status)) {
|
||
return '0/1'; // 单任务进行中
|
||
}
|
||
|
||
if (['FAILED', 'FAILURE', 'ERROR'].includes(task.task_status)) {
|
||
return '0/1'; // 单任务失败
|
||
}
|
||
|
||
// 其他情况(等待、初始化等)
|
||
return '0/1';
|
||
}
|
||
|
||
// 获取任务进度百分比显示 - 与任务执行状态面板完全一致
|
||
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 {
|
||
const statusTextMap: Record<string, string> = {
|
||
// 成功状态
|
||
'SUCCESS': '已完成',
|
||
'COMPLETED': '已完成',
|
||
'FINISHED': '已完成',
|
||
|
||
// 进行中状态
|
||
'IN_PROGRESS': '进行中',
|
||
'RUNNING': '运行中',
|
||
'PROCESSING': '处理中',
|
||
'EXECUTING': '执行中',
|
||
|
||
// 等待状态
|
||
'PENDING': '待处理',
|
||
'QUEUED': '队列中',
|
||
'WAITING': '等待中',
|
||
'SCHEDULED': '已调度',
|
||
'INIT': '初始化', // 新增:接口实际返回的状态
|
||
|
||
// 失败状态
|
||
'FAILED': '失败',
|
||
'FAILURE': '失败',
|
||
'ERROR': '错误',
|
||
'CANCELLED': '已取消',
|
||
'TIMEOUT': '超时',
|
||
|
||
// 暂停状态
|
||
'PAUSED': '已暂停',
|
||
'SUSPENDED': '已挂起'
|
||
};
|
||
|
||
return statusTextMap[status] || status;
|
||
}
|
||
|
||
// 获取任务状态颜色 - 更明显的颜色区分
|
||
function getTaskStatusColor(status: string): string {
|
||
// 成功状态 - 绿色系
|
||
if (['SUCCESS', 'COMPLETED', 'FINISHED'].includes(status)) {
|
||
return 'text-emerald-400';
|
||
}
|
||
// 进行中状态 - 蓝色系
|
||
else if (['IN_PROGRESS', 'RUNNING', 'PROCESSING', 'EXECUTING'].includes(status)) {
|
||
return 'text-cyan-400';
|
||
}
|
||
// 等待状态 - 黄色系
|
||
else if (['PENDING', 'QUEUED', 'WAITING', 'SCHEDULED', 'INIT'].includes(status)) {
|
||
return 'text-amber-400';
|
||
}
|
||
// 失败状态 - 红色系
|
||
else if (['FAILED', 'FAILURE', 'ERROR'].includes(status)) {
|
||
return 'text-rose-400';
|
||
}
|
||
// 取消状态 - 橙色系
|
||
else if (['CANCELLED', 'TIMEOUT'].includes(status)) {
|
||
return 'text-orange-400';
|
||
}
|
||
// 暂停状态 - 紫色系
|
||
else if (['PAUSED', 'SUSPENDED'].includes(status)) {
|
||
return 'text-purple-400';
|
||
}
|
||
// 默认状态
|
||
else {
|
||
return 'text-gray-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 = React.useCallback((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>
|
||
{availableTaskTypes.map(type => (
|
||
<option key={type} value={type}>
|
||
{type === 'AI' ? 'AI 任务' :
|
||
type === 'Video' ? '视频任务' :
|
||
type === 'Audio' ? '音频任务' :
|
||
type === 'Comp' ? '合成任务' :
|
||
type === 'Script' ? '脚本任务' :
|
||
type === 'Doc' ? '文档任务' :
|
||
type === 'Data' ? '数据任务' :
|
||
`${type} 任务`}
|
||
</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-32 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) => (
|
||
<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 transition-all duration-200 ease-out",
|
||
selectedTask === task.id && "bg-blue-600/20",
|
||
task.level === 1 && "ml-4 bg-gray-900/30" // 子任务缩进和背景区分
|
||
)}
|
||
onClick={() => setSelectedTask(selectedTask === task.id ? null : task.id)}
|
||
>
|
||
{/* 状态图标 */}
|
||
<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 && (
|
||
<motion.button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
toggleTaskExpansion(task.id);
|
||
}}
|
||
className="p-0.5 hover:bg-gray-700 rounded text-gray-400 hover:text-white transition-colors duration-150"
|
||
whileHover={{ scale: 1.1 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<motion.div
|
||
animate={{
|
||
rotate: expandedTasks.has(task.id) ? 90 : 0
|
||
}}
|
||
transition={{
|
||
duration: 0.2,
|
||
ease: "easeOut"
|
||
}}
|
||
>
|
||
<ChevronRight className="w-3 h-3" />
|
||
</motion.div>
|
||
</motion.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">
|
||
{/* 错误信息悬停提示 */}
|
||
<div className="relative group">
|
||
<div className="p-1 hover:bg-gray-700 rounded text-red-400 hover:text-red-300 cursor-help">
|
||
<Info className="w-4 h-4" />
|
||
</div>
|
||
|
||
{/* 悬停时显示的错误信息卡片 */}
|
||
<div className="absolute left-0 top-8 z-50 invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all duration-200">
|
||
<div className="bg-gray-900 border border-red-500/30 rounded-lg shadow-xl p-4 min-w-80 max-w-96">
|
||
{/* 错误标题 */}
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<XCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
|
||
<h4 className="text-red-400 font-semibold text-base">任务执行失败</h4>
|
||
</div>
|
||
|
||
{/* 错误详情 */}
|
||
{(() => {
|
||
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
||
if (!originalTask) return null;
|
||
|
||
const errorInfo = getTaskErrorInfo(originalTask);
|
||
return (
|
||
<div className="space-y-3">
|
||
{/* 错误消息 */}
|
||
<div>
|
||
<div className="text-gray-400 text-sm mb-1">错误信息:</div>
|
||
<div className="text-white text-base leading-relaxed bg-gray-800 rounded p-2 border-l-4 border-red-500">
|
||
{errorInfo.errorMessage || '未知错误'}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 错误代码 */}
|
||
{errorInfo.errorCode && (
|
||
<div>
|
||
<div className="text-gray-400 text-sm mb-1">错误代码:</div>
|
||
<div className="text-red-300 font-mono text-sm bg-gray-800 rounded px-2 py-1 inline-block">
|
||
{errorInfo.errorCode}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 任务信息 */}
|
||
<div className="pt-2 border-t border-gray-700">
|
||
<div className="text-gray-400 text-sm mb-1">任务详情:</div>
|
||
<div className="text-gray-300 text-sm space-y-1">
|
||
<div>任务ID: <span className="font-mono text-xs">{task.id}</span></div>
|
||
<div>任务类型: <span className="text-white">{task.type}</span></div>
|
||
<div>失败时间: <span className="text-white">{(() => {
|
||
const taskTimes = getTaskTimes(task);
|
||
return taskTimes.endTime ? formatDateTime(taskTimes.endTime) : '未知';
|
||
})()}</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})()}
|
||
|
||
{/* 小三角箭头 */}
|
||
<div className="absolute -top-2 left-4 w-4 h-4 bg-gray-900 border-l border-t border-red-500/30 transform rotate-45"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 重试按钮 */}
|
||
{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-4 h-4" />
|
||
</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 ? (
|
||
// 主任务显示 completed/total 格式 (如: 14/16 或 16/16)
|
||
getTaskProgressCount(tasks.find((t: any) => t.task_id === task.id) || {})
|
||
) : (
|
||
// 子任务显示百分比格式
|
||
task.status === 'SUCCESS' || task.status === 'COMPLETED' ? '100%' :
|
||
task.status === 'IN_PROGRESS' ? `${task.progress}%` :
|
||
task.status === 'FAILED' ? '0%' : '0%'
|
||
)}
|
||
</span>
|
||
</div>
|
||
|
||
{/* 时间信息 */}
|
||
<div className="w-32 text-center">
|
||
<div className="flex flex-col items-center">
|
||
<span className={cn("text-sm font-medium", getTimeDisplayColor(task.executionTime))}>
|
||
{formatTime(task.executionTime)}
|
||
</span>
|
||
{/* 显示开始时间 */}
|
||
{(() => {
|
||
const taskTimes = getTaskTimes(task);
|
||
return taskTimes.startTime ? (
|
||
<span className="text-xs text-green-400" title={`开始时间: ${formatDateTime(taskTimes.startTime)}`}>
|
||
{new Date(taskTimes.startTime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
|
||
</span>
|
||
) : null;
|
||
})()}
|
||
{/* 显示结束时间 */}
|
||
{(() => {
|
||
const taskTimes = getTaskTimes(task);
|
||
return taskTimes.endTime ? (
|
||
<span className="text-xs text-blue-400" title={`结束时间: ${formatDateTime(taskTimes.endTime)}`}>
|
||
{new Date(taskTimes.endTime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
|
||
</span>
|
||
) : null;
|
||
})()}
|
||
{/* 剩余时间估算 */}
|
||
{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>
|
||
</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={(() => {
|
||
const taskTimes = getTaskTimes(task);
|
||
return `${task.displayName}
|
||
执行时间: ${formatTime(task.executionTime)}
|
||
状态: ${getTaskStatusText(task.status)}
|
||
${task.status === 'IN_PROGRESS' ? `进度: ${task.progress}%` : ''}
|
||
开始时间: ${formatDateTime(taskTimes.startTime)}
|
||
结束时间: ${formatDateTime(taskTimes.endTime)}`;
|
||
})()}
|
||
>
|
||
{/* 基于状态的单色进度条 */}
|
||
<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="等待开始"
|
||
/>
|
||
)}
|
||
|
||
{/* 智能百分比文字显示 - 只在主任务且有足够空间时显示 */}
|
||
{(() => {
|
||
const barWidth = (task.executionTime / maxTime) * 100;
|
||
const showPercentage = task.level === 0 && barWidth > 6; // 主任务且宽度足够
|
||
|
||
if (!showPercentage) return null;
|
||
|
||
const getPercentageText = () => {
|
||
switch (task.status) {
|
||
case 'IN_PROGRESS':
|
||
return `${task.progress}%`;
|
||
case 'COMPLETED':
|
||
return '100%';
|
||
case 'FAILED':
|
||
return `${task.progress}%`;
|
||
case 'PENDING':
|
||
return '0%';
|
||
default:
|
||
return '';
|
||
}
|
||
};
|
||
|
||
const percentageText = getPercentageText();
|
||
if (!percentageText) return null;
|
||
|
||
// 根据进度条宽度和进度值决定文字位置
|
||
const progressWidth = task.status === 'IN_PROGRESS' ? task.progress :
|
||
task.status === 'COMPLETED' ? 100 :
|
||
task.status === 'FAILED' ? Math.max(task.progress, 15) : 8;
|
||
|
||
const shouldCenterInProgress = task.status === 'IN_PROGRESS' && progressWidth > 30;
|
||
const shouldCenterInBar = (task.status === 'COMPLETED') ||
|
||
(task.status === 'FAILED' && progressWidth > 25);
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
"absolute top-0 flex items-center pointer-events-none h-full",
|
||
shouldCenterInProgress || shouldCenterInBar ? "justify-center inset-0" : "justify-end right-1"
|
||
)}
|
||
>
|
||
<span
|
||
className={cn(
|
||
"font-semibold transition-all duration-300 select-none",
|
||
// 智能颜色选择
|
||
task.status === 'COMPLETED' ? "text-white" :
|
||
task.status === 'IN_PROGRESS' && shouldCenterInProgress ? "text-white" :
|
||
task.status === 'IN_PROGRESS' && !shouldCenterInProgress ? "text-cyan-200" :
|
||
task.status === 'FAILED' && shouldCenterInBar ? "text-white" :
|
||
task.status === 'FAILED' && !shouldCenterInBar ? "text-rose-200" :
|
||
"text-amber-200"
|
||
)}
|
||
style={{
|
||
textShadow: '0 1px 3px rgba(0,0,0,0.9), 0 0 8px rgba(0,0,0,0.5)',
|
||
fontSize: barWidth > 12 ? '11px' : '10px',
|
||
fontWeight: '600'
|
||
}}
|
||
>
|
||
{percentageText}
|
||
</span>
|
||
</div>
|
||
);
|
||
})()}
|
||
</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}%` }}
|
||
/>
|
||
{/* 子任务百分比 - 只在有足够空间时显示 */}
|
||
{(() => {
|
||
const barWidth = (task.executionTime / maxTime) * 100;
|
||
if (barWidth < 4) return null; // 子任务条太窄不显示
|
||
|
||
return (
|
||
<div className="absolute inset-0 flex items-center justify-end pr-1 pointer-events-none">
|
||
<span
|
||
className="text-white font-medium"
|
||
style={{
|
||
textShadow: '0 1px 2px rgba(0,0,0,0.8)',
|
||
fontSize: '9px'
|
||
}}
|
||
>
|
||
{task.progress}%
|
||
</span>
|
||
</div>
|
||
);
|
||
})()}
|
||
</>
|
||
)}
|
||
</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.level === 0 && (() => {
|
||
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
||
const subTaskStatus = originalTask ? getSubTaskStatus(originalTask) : null;
|
||
return subTaskStatus ? (
|
||
<div><span className="text-gray-400">子任务进度:</span> <span className="text-cyan-400 font-mono">{subTaskStatus}</span></div>
|
||
) : null;
|
||
})()}
|
||
|
||
{/* 时间信息显示 */}
|
||
{(() => {
|
||
const taskTimes = getTaskTimes(task);
|
||
return (
|
||
<>
|
||
<div className="border-t border-gray-700 pt-2 mt-2">
|
||
<div className="text-gray-300 font-medium text-xs mb-1">⏰ 时间信息</div>
|
||
<div><span className="text-gray-400">开始时间:</span> <span className="text-green-400 text-[10px]">{formatDateTime(taskTimes.startTime)}</span></div>
|
||
<div><span className="text-gray-400">结束时间:</span> <span className="text-blue-400 text-[10px]">{formatDateTime(taskTimes.endTime)}</span></div>
|
||
{taskTimes.createdAt && taskTimes.createdAt !== taskTimes.startTime && (
|
||
<div><span className="text-gray-400">创建时间:</span> <span className="text-gray-300 text-[10px]">{formatDateTime(taskTimes.createdAt)}</span></div>
|
||
)}
|
||
{taskTimes.updatedAt && taskTimes.updatedAt !== taskTimes.endTime && (
|
||
<div><span className="text-gray-400">更新时间:</span> <span className="text-gray-300 text-[10px]">{formatDateTime(taskTimes.updatedAt)}</span></div>
|
||
)}
|
||
{taskTimes.startTime && taskTimes.endTime && (
|
||
<div><span className="text-gray-400">执行时长:</span> <span className="text-yellow-400 text-[10px]">{formatTime(task.executionTime)}</span></div>
|
||
)}
|
||
</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;
|