video-flow-b/components/dashboard/network-timeline.tsx
2025-09-18 16:51:50 +08:00

1824 lines
73 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,
Copy,
Check
} 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>('');
const [retryingTasks, setRetryingTasks] = useState<Set<string>>(new Set());
const [copiedTaskId, setCopiedTaskId] = useState<string | null>(null);
// 自动清理重试状态当任务状态不再是RETRYING时移除重试状态
useEffect(() => {
if (retryingTasks.size === 0) return;
const currentRetryingTaskIds = Array.from(retryingTasks);
const tasksToRemove: string[] = [];
currentRetryingTaskIds.forEach(taskId => {
// 检查任务是否还在重试状态
const isStillRetrying = tasks.some(task => {
// 检查主任务
if (task.task_id === taskId && task.task_status === 'RETRYING') {
return true;
}
// 检查子任务
if (task.sub_tasks && Array.isArray(task.sub_tasks)) {
return task.sub_tasks.some((subTask: { task_id: string; task_status: string }) =>
subTask.task_id === taskId && subTask.task_status === 'RETRYING'
);
}
return false;
});
if (!isStillRetrying) {
tasksToRemove.push(taskId);
}
});
// 移除不再重试的任务
if (tasksToRemove.length > 0) {
setRetryingTasks(prev => {
const newSet = new Set(prev);
tasksToRemove.forEach(taskId => newSet.delete(taskId));
return newSet;
});
}
}, [tasks, retryingTasks]);
// 更新时间显示
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]);
// 复制任务ID
const copyTaskId = async (taskId: string) => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(taskId);
setCopiedTaskId(taskId);
setTimeout(() => setCopiedTaskId(null), 1500);
} else {
const textArea = document.createElement('textarea');
textArea.value = taskId;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopiedTaskId(taskId);
setTimeout(() => setCopiedTaskId(null), 1500);
}
} catch (e) {
// noop
}
};
// 计算任务统计信息
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 startTime = Date.now();
console.log(`[性能] 开始计算mainTaskExecutions任务数量: ${tasks.length}`);
// 获取所有任务的真实开始时间,用于计算时间线的基准点
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: task.task_result // 直接使用原始数据,避免不必要的解析
};
// 预处理子任务数据但不添加到结果中(性能优化:限制处理数量)
if (task.task_result?.data && Array.isArray(task.task_result.data)) {
// 性能优化如果子任务过多只处理前20个但保留总数信息
const maxSubTasks = 20;
const subTaskData = task.task_result.data.length > maxSubTasks
? task.task_result.data.slice(0, maxSubTasks)
: task.task_result.data;
if (task.task_result.data.length > maxSubTasks) {
console.log(`[性能] 任务 ${task.task_id}${task.task_result.data.length} 个子任务,只处理前 ${maxSubTasks}`);
}
const subTasks = subTaskData.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: subTask.task_result // 直接使用原始数据,避免不必要的解析
};
});
// 真实子任务优先覆盖之前的subTasks
mainTask.subTasks = realSubTasks;
}
result.push(mainTask);
});
const endTime = Date.now();
const duration = endTime - startTime;
console.log(`[性能] mainTaskExecutions计算完成耗时: ${duration}ms`);
// 如果计算时间超过1秒记录警告
if (duration > 1000) {
console.warn(`[性能警告] mainTaskExecutions计算耗时过长: ${duration}ms任务数量: ${tasks.length}`);
}
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') {
// 性能优化快速检查是否为JSON格式
const trimmed = taskResult.trim();
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
// 不是JSON格式直接返回原始字符串避免JSON.parse异常
return { raw_text: trimmed };
}
try {
return JSON.parse(taskResult);
} catch (error) {
// 静默处理解析错误,避免大量日志输出影响性能
return { raw_text: trimmed, parse_error: true };
}
}
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);
}
// 性能优化直接检查task.progress字段避免JSON解析
if (typeof task.progress === 'number' && task.progress >= 0 && task.progress <= 100) {
return task.progress;
}
// 检查task_result是否已经是对象格式
if (task.task_result && typeof task.task_result === 'object' && task.task_result.progress_percentage) {
return task.task_result.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':
case 'RETRYING':
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,
'RETRYING': 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': '执行中',
'RETRYING': '重试中', // 新增:重试状态
// 等待状态
'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
};
}
// 获取子任务错误信息
function getSubTaskErrorInfo(taskId: string): { hasError: boolean; errorMessage: string; errorCode?: string } {
// 遍历所有主任务,查找对应的子任务
for (const mainTask of tasks) {
if (mainTask.sub_tasks && Array.isArray(mainTask.sub_tasks)) {
const subTask = mainTask.sub_tasks.find((st: any) => st.task_id === taskId);
if (subTask && subTask.task_status === 'FAILED') {
// 从子任务的task_result中提取错误信息
const errorMessage = subTask.task_result?.error_message ||
subTask.task_result?.message ||
subTask.error_message ||
'子任务执行失败,请重试';
const errorCode = subTask.task_result?.error_code ||
subTask.error_code ||
'UNKNOWN_ERROR';
return {
hasError: true,
errorMessage,
errorCode
};
}
}
}
return { hasError: false, errorMessage: '' };
}
// 处理重试任务
const handleRetryTask = async (taskId: string) => {
if (onRetryTask) {
try {
// 添加到重试中的任务集合
setRetryingTasks(prev => new Set(prev).add(taskId));
// 调用父组件的重试逻辑(包含乐观更新)
await onRetryTask(taskId);
console.log(`任务 ${taskId} 重试请求已发送`);
} catch (error) {
console.error('重试任务失败:', error);
// 重试失败时立即移除重试状态
setRetryingTasks(prev => {
const newSet = new Set(prev);
newSet.delete(taskId);
return newSet;
});
}
// 注意成功情况下不立即移除重试状态让乐观更新的RETRYING状态显示一段时间
// 重试状态会在数据刷新后自然消失当任务状态从RETRYING变为其他状态时
}
};
// 复制错误信息到剪贴板
const copyErrorInfo = async (taskId: string, errorMessage: string, errorCode?: string) => {
try {
const errorText = `任务ID: ${taskId}\n错误信息: ${errorMessage}${errorCode ? `\n错误代码: ${errorCode}` : ''}`;
await navigator.clipboard.writeText(errorText);
// 显示复制成功反馈
setCopiedTaskId(taskId);
setTimeout(() => {
setCopiedTaskId(null);
}, 2000);
} catch (error) {
console.error('复制失败:', error);
// 降级方案:使用传统的复制方法
const textArea = document.createElement('textarea');
textArea.value = `任务ID: ${taskId}\n错误信息: ${errorMessage}${errorCode ? `\n错误代码: ${errorCode}` : ''}`;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopiedTaskId(taskId);
setTimeout(() => {
setCopiedTaskId(null);
}, 2000);
}
};
// 格式化文件大小
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 {
// 统一兜底:避免负值/NaN 导致显示 "-Xs"
if (!Number.isFinite(ms) || ms <= 0) {
return '0s';
}
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 getTaskLogContent(viewTask: TaskExecution): string {
// 优先从原始任务中提取完整字段
let origin: any | null = null;
if (viewTask.level === 0) {
origin = tasks.find((t: any) => t.task_id === viewTask.id) || null;
} else if (viewTask.level === 1 && viewTask.parentId) {
const parent = tasks.find((t: any) => t.task_id === viewTask.parentId);
origin = parent?.sub_tasks?.find((st: any) => st.task_id === viewTask.id) || null;
}
if (!origin) return '无可用日志';
const resultObj = parseTaskResult(origin.task_result);
const payload: Record<string, any> = {
task_id: origin.task_id,
task_name: origin.task_name,
task_status: origin.task_status,
task_message: origin.task_message || null,
error_message: origin.error_message || resultObj?.error_message || null,
error_traceback: origin.error_traceback || resultObj?.error_traceback || null,
progress_percentage: resultObj?.progress_percentage ?? origin.progress ?? null,
params: origin.task_params || null,
result: resultObj || origin.task_result || null,
timestamps: {
created_at: origin.created_at || null,
updated_at: origin.updated_at || null,
start_time: origin.start_time || null,
end_time: origin.end_time || null,
},
};
try {
return JSON.stringify(payload, null, 2);
} catch {
return typeof origin.task_result === 'string' ? origin.task_result : '无可用日志';
}
}
// 获取状态颜色
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]));
// 计算“项目总耗时”应独立于过滤条件,直接基于原始 tasks
// 规则:
// 1) 全局开始时间 = 所有任务 start_time/created_at 的最小值
// 2) 全局结束时间 =
// - 若存在已结束任务:取已结束任务 end_time 的最大值
// - 否则:取所有任务 updated_at 的最大值(表示仍在进行中的最近更新时间)
const projectDurationMs = React.useMemo(() => {
if (!tasks || tasks.length === 0) return 0;
const toMs = (v: any) => {
const n = new Date(v as string).getTime();
return Number.isNaN(n) ? NaN : n;
};
// 1) 全局开始
const allStarts = tasks
.map((t: any) => toMs(t.start_time || t.created_at))
.filter((n: number) => !Number.isNaN(n));
const globalStart = allStarts.length > 0 ? Math.min(...allStarts) : Date.now();
// 2) 终止态集合
const terminal = new Set(['COMPLETED','SUCCESS','FINISHED','FAILED','FAILURE','ERROR','CANCELLED','TIMEOUT']);
// 3) 已结束任务的结束时间
const endedEnds = tasks
.map((t: any) => terminal.has(t.task_status) ? toMs(t.end_time) : NaN)
.filter((n: number) => !Number.isNaN(n));
let globalEnd: number | null = null;
if (endedEnds.length > 0) {
globalEnd = Math.max(...endedEnds);
} else {
// 4) 无已结束任务,则取最近一次 updated_at 代表“当前进度”
const latestUpdated = tasks
.map((t: any) => toMs(t.updated_at || t.created_at))
.filter((n: number) => !Number.isNaN(n));
if (latestUpdated.length > 0) globalEnd = Math.max(...latestUpdated);
}
if (!globalEnd || Number.isNaN(globalEnd)) return 0;
return Math.max(0, globalEnd - globalStart);
}, [tasks]);
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-3 bg-gray-900 border-b border-gray-800 flex-shrink-0">
<div className="flex items-center gap-3">
<Network className="w-5 h-5 text-cyan-400" />
<h3 className="text-lg font-semibold text-white">线</h3>
{/* 任务统计 */}
<div className="flex items-center gap-2.5">
<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-full flex flex-col min-h-0">
{/* 列表头部 */}
<div className="flex items-center px-4 py-1.5 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="w-64"></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-48 text-center"></div>
<div className="flex-1 text-center">
<div className="flex items-center justify-center gap-2">
<span>线</span>
<span className="text-cyan-400 font-bold">
(: {formatTime(projectDurationMs)})
</span>
</div>
</div>
</div>
{/* 任务列表 */}
<div className="flex-1 overflow-y-auto min-h-0">
{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-1.5 text-sm border-b border-gray-800/30 transition-all duration-200 ease-out",
task.level === 1 && "ml-4 bg-gray-900/30" // 子任务缩进和背景区分
)}
>
{/* 状态图标 */}
<div className="w-8 flex justify-center">
{task.statusCode === 200 && <CheckCircle className="w-4 h-4 text-emerald-400" />}
{task.statusCode === 202 && <Clock className="w-4 h-4 text-cyan-400 animate-pulse" />}
{task.statusCode === 100 && <Pause className="w-4 h-4 text-amber-400" />}
{task.statusCode >= 400 && <XCircle className="w-4 h-4 text-rose-400" />}
</div>
{/* 展开/折叠按钮 */}
<div className="w-6 flex justify-center">
{task.level === 0 && task.subTasks && task.subTasks.length > 0 && (
<motion.div
className="p-0.5 text-gray-400 transition-colors duration-150 cursor-pointer hover:text-white"
onClick={(e) => {
e.stopPropagation();
toggleTaskExpansion(task.id);
}}
title={expandedTasks.has(task.id) ? '收起子任务' : '展开子任务'}
data-alt="expand-toggle"
whileHover={{ scale: 1.1 }}
>
<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.div>
)}
</div>
{/* 任务名称 */}
<div
className={cn(
"w-64 flex items-center gap-2 cursor-pointer hover:bg-gray-800/50 rounded px-2 py-1 transition-all duration-200",
task.level === 1 && "pl-4",
selectedTask === task.id && "bg-blue-600/20"
)}
onClick={() => {
// 如果是主任务且有子任务,切换展开状态
if (task.level === 0 && task.subTasks && task.subTasks.length > 0) {
toggleTaskExpansion(task.id);
}
// 切换选中状态
setSelectedTask(selectedTask === task.id ? null : task.id);
}}
>
<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 || task.status === 'RETRYING') && (
<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 justify-between mb-3">
<div className="flex items-center gap-2">
<XCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
<h4 className="text-red-400 font-semibold text-base"></h4>
</div>
{/* 复制按钮 */}
{(() => {
let errorInfo: { hasError: boolean; errorMessage: string; errorCode?: string };
if (task.level === 0) {
const originalTask = tasks.find((t: any) => t.task_id === task.id);
if (!originalTask) return null;
errorInfo = getTaskErrorInfo(originalTask);
} else {
errorInfo = getSubTaskErrorInfo(task.id);
}
if (!errorInfo.hasError) return null;
return (
<button
onClick={(e) => {
e.stopPropagation();
copyErrorInfo(task.id, errorInfo.errorMessage, errorInfo.errorCode);
}}
className={cn(
"flex items-center gap-1 px-2 py-1 text-xs rounded transition-colors",
copiedTaskId === task.id
? "bg-green-600/20 text-green-400"
: "bg-gray-700/50 text-gray-300 hover:bg-gray-600/50 hover:text-white"
)}
title="复制错误信息"
>
{copiedTaskId === task.id ? (
<>
<Check className="w-3 h-3" />
<span></span>
</>
) : (
<>
<Copy className="w-3 h-3" />
<span></span>
</>
)}
</button>
);
})()}
</div>
{/* 错误详情 */}
{(() => {
// 根据任务层级选择不同的错误信息获取方式
let errorInfo;
if (task.level === 0) {
// 主任务
const originalTask = tasks.find((t: any) => t.task_id === task.id);
if (!originalTask) return null;
errorInfo = getTaskErrorInfo(originalTask);
} else {
// 子任务
errorInfo = getSubTaskErrorInfo(task.id);
}
if (!errorInfo.hasError) return null;
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-sm">{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);
}}
disabled={retryingTasks.has(task.id) || task.status === 'RETRYING'}
className={cn(
"p-1 hover:bg-gray-700 rounded transition-colors",
(retryingTasks.has(task.id) || task.status === 'RETRYING')
? "text-yellow-400 cursor-not-allowed"
: "text-blue-400 hover:text-blue-300"
)}
title={
(retryingTasks.has(task.id) || task.status === 'RETRYING')
? "重试中..."
: "重试任务"
}
>
<RefreshCw className={cn(
"w-4 h-4",
(retryingTasks.has(task.id) || task.status === 'RETRYING') && "animate-spin"
)} />
</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-48 text-center">
<div className="flex flex-col items-center">
<span className={cn("text-sm font-medium", getTimeDisplayColor(task.executionTime))}>
{formatTime(task.executionTime)}
</span>
{/* 开始和结束时间一行显示 */}
<div className="flex items-center gap-1 text-xs">
{(() => {
const taskTimes = getTaskTimes(task);
return taskTimes.startTime ? (
<span className="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.startTime && taskTimes.endTime ? (
<span className="text-gray-500">-</span>
) : null;
})()}
{(() => {
const taskTimes = getTaskTimes(task);
return taskTimes.endTime ? (
<span className="text-blue-400" title={`结束时间: ${formatDateTime(taskTimes.endTime)}`}>
{new Date(taskTimes.endTime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
</span>
) : null;
})()}
</div>
{/* 剩余时间估算 */}
{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 className="flex-1 px-2">
<div className="relative h-6 bg-gray-800/50 rounded border border-gray-700/50 overflow-hidden">
{/* 时间线背景 */}
<div className="absolute inset-0 bg-gradient-to-r from-gray-700 via-gray-600 to-gray-700 opacity-30"></div>
{/* 任务进度条 */}
<div
className={cn(
"absolute top-0 h-full rounded-sm transition-all duration-200",
task.statusCode === 200 ? "bg-emerald-500" :
task.statusCode === 202 ? "bg-cyan-500" :
task.statusCode >= 400 ? "bg-rose-500" : "bg-amber-500",
selectedTask === task.id && "ring-2 ring-cyan-300/90 shadow-[0_0_8px_rgba(34,211,238,0.6)]"
)}
style={{
left: `${(task.startTime / maxTime) * 100}%`,
width: `${Math.max((task.executionTime / maxTime) * 100, 0.5)}%`
}}
title={`${task.displayName} - ${formatTime(task.executionTime)}`}
>
{/* 进度百分比显示 */}
{(() => {
const barWidth = (task.executionTime / maxTime) * 100;
const showPercentage = barWidth > 8; // 只在有足够空间时显示
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;
return (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<span
className="text-xs font-semibold text-white"
style={{
textShadow: '0 1px 2px rgba(0,0,0,0.8)',
fontSize: '10px'
}}
>
{percentageText}
</span>
</div>
);
})()}
</div>
{/* 时间刻度标记 */}
<div className="absolute top-0 left-0 right-0 h-full flex justify-between items-end pb-1">
{Array.from({ length: 5 }, (_, i) => (
<div key={i} className="w-px h-2 bg-gray-500/50"></div>
))}
</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-3"
>
{(() => {
const task = taskExecutions.find((t: TaskExecution) => t.id === selectedTask);
if (!task) return null;
return (
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-base font-medium text-white mb-3">
{task.level === 0 ? '主任务详情' : '子任务详情'}
</h4>
{/* 顶部一行:任务关键信息 + 时间信息 */}
{(() => {
const t = getTaskTimes(task);
return (
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm mb-2">
<span className="px-2 py-1 bg-gray-800/60 border border-gray-700 rounded text-white">
{task.displayName}
</span>
<span className={cn("px-2 py-1 rounded border", getTaskStatusColor(task.status), "border-gray-700/60 bg-gray-800/40 ")}>{getTaskStatusText(task.status)}</span>
<span className="px-2 py-1 bg-gray-800/40 border border-gray-700 rounded text-gray-300">{task.type}</span>
{t.startTime && (
<span className="px-2 py-1 bg-gray-900/40 border border-gray-800 rounded text-green-400" title={`开始时间: ${formatDateTime(t.startTime)}`}>
{new Date(t.startTime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
</span>
)}
{t.endTime && (
<span className="px-2 py-1 bg-gray-900/40 border border-gray-800 rounded text-blue-400" title={`结束时间: ${formatDateTime(t.endTime)}`}>
{new Date(t.endTime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
</span>
)}
{t.startTime && t.endTime && (
<span className="px-2 py-1 bg-gray-900/40 border border-gray-800 rounded text-yellow-400">
{formatTime(task.executionTime)}
</span>
)}
</div>
);
})()}
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<span className="text-gray-400">ID:</span>
<span className="text-white font-mono text-sm" title={task.id}>{task.id}</span>
<button
onClick={() => copyTaskId(task.id)}
className={cn(
"inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] transition-colors",
copiedTaskId === task.id
? "bg-green-600/20 text-green-400"
: "bg-gray-700/50 text-gray-300 hover:bg-gray-600/50 hover:text-white"
)}
title="复制任务ID"
>
{copiedTaskId === task.id ? (
<>
<Check className="w-3 h-3" />
<span></span>
</>
) : (
<>
<Copy className="w-3 h-3" />
<span></span>
</>
)}
</button>
</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;
})()}
{/* 时间信息已并入顶部一行,此处不再重复 */}
{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 && (() => {
// 根据任务层级选择不同的错误信息获取方式
let errorInfo: { hasError: boolean; errorMessage: string; errorCode?: string };
if (task.level === 0) {
// 主任务
const originalTask = tasks.find((t: any) => t.task_id === task.id);
if (!originalTask) return null;
errorInfo = getTaskErrorInfo(originalTask);
} else {
// 子任务
errorInfo = getSubTaskErrorInfo(task.id);
}
if (!errorInfo.hasError) return null;
return (
<div className="mt-2 p-2 bg-red-600/10 border border-red-600/20 rounded flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1">
<XCircle className="w-3 h-3 text-red-400" />
<span className="text-red-400 font-medium text-[11px]"></span>
</div>
<span className="text-red-300 text-[10px] truncate max-w-[40%]" title={errorInfo.errorMessage}>{errorInfo.errorMessage}</span>
{errorInfo.errorCode && (
<span className="text-red-400 text-[10px] font-mono">: {errorInfo.errorCode}</span>
)}
<div className="flex items-center gap-1 ml-auto">
{/* 复制错误信息按钮 */}
<button
onClick={() => copyErrorInfo(task.id, errorInfo.errorMessage, errorInfo.errorCode)}
className={cn(
"flex items-center gap-1 px-2 py-1 text-[10px] rounded transition-colors",
copiedTaskId === task.id
? "bg-green-600/20 text-green-400"
: "bg-gray-700/50 text-gray-300 hover:bg-gray-600/50 hover:text-white"
)}
title="复制错误信息"
>
{copiedTaskId === task.id ? (
<>
<Check className="w-3 h-3" />
<span></span>
</>
) : (
<>
<Copy className="w-3 h-3" />
<span></span>
</>
)}
</button>
{/* 重试按钮 */}
{onRetryTask && (
<button
onClick={() => handleRetryTask(task.id)}
disabled={retryingTasks.has(task.id) || task.status === 'RETRYING'}
className={cn(
"flex items-center gap-1 px-2 py-1 text-white text-[10px] rounded transition-colors",
(retryingTasks.has(task.id) || task.status === 'RETRYING')
? "bg-yellow-600 cursor-not-allowed"
: "bg-blue-600 hover:bg-blue-700"
)}
>
<RefreshCw className={cn(
"w-3 h-3",
(retryingTasks.has(task.id) || task.status === 'RETRYING') && "animate-spin"
)} />
{(retryingTasks.has(task.id) || task.status === 'RETRYING') ? "重试中..." : "重试任务"}
</button>
)}
</div>
</div>
);
})()}
</div>
</div>
<div>
<h4 className="text-base font-medium text-white mb-3"></h4>
{/* <div className="space-y-2 text-sm">
<div className="bg-gray-900/50 border border-gray-800 rounded p-3 min-h-40 max-h-64 overflow-y-auto">
<pre className="text-xs text-gray-300 font-mono whitespace-pre-wrap break-words">
{getTaskLogContent(task)}
</pre>
</div>
<div className="text-xs text-gray-500">仅展示与该任务相关的最新字段(状态、消息、错误、参数与结果)。</div>
</div> */}
</div>
</div>
);
})()}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export default NetworkTimeline;