forked from 77media/video-flow
任务执行时间线数据面板页接口数据绑定
This commit is contained in:
parent
18908ebf9a
commit
c0d3a0dfd3
@ -3,7 +3,7 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import NetworkTimeline from '@/components/dashboard/network-timeline';
|
import NetworkTimeline from '@/components/dashboard/network-timeline';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { getRunningStreamData } from '@/api/video_flow';
|
import { getProjectTaskList } from '@/api/video_flow';
|
||||||
import { mockDashboardData } from '@/components/dashboard/demo-data';
|
import { mockDashboardData } from '@/components/dashboard/demo-data';
|
||||||
import { cn } from '@/public/lib/utils';
|
import { cn } from '@/public/lib/utils';
|
||||||
|
|
||||||
@ -23,6 +23,8 @@ export default function DashboardPage() {
|
|||||||
// 使用 ref 来存储最新的状态,避免定时器闭包问题
|
// 使用 ref 来存储最新的状态,避免定时器闭包问题
|
||||||
const stateRef = useRef({ isUsingMockData, dashboardData });
|
const stateRef = useRef({ isUsingMockData, dashboardData });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 初始加载数据
|
// 初始加载数据
|
||||||
const fetchInitialData = async () => {
|
const fetchInitialData = async () => {
|
||||||
try {
|
try {
|
||||||
@ -32,17 +34,18 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
console.log('正在获取项目数据,项目ID:', projectId);
|
console.log('正在获取项目数据,项目ID:', projectId);
|
||||||
|
|
||||||
// 调用真实API
|
// 调用新的任务列表API
|
||||||
const response = await getRunningStreamData({ project_id: projectId });
|
const response = await getProjectTaskList({ project_id: projectId });
|
||||||
|
|
||||||
console.log('API响应:', response);
|
console.log('API响应:', response);
|
||||||
|
|
||||||
if (response.code === 0 && response.data) {
|
if (response.code === 0 && response.data) {
|
||||||
|
// 直接使用新API数据,不进行格式转换
|
||||||
setDashboardData(response.data);
|
setDashboardData(response.data);
|
||||||
setIsUsingMockData(false);
|
setIsUsingMockData(false);
|
||||||
setLastUpdateTime(new Date());
|
setLastUpdateTime(new Date());
|
||||||
setConnectionStatus('connected');
|
setConnectionStatus('connected');
|
||||||
console.log('成功获取真实数据');
|
console.log('成功获取真实数据:', response.data);
|
||||||
} else {
|
} else {
|
||||||
console.warn('API返回错误或无数据,使用演示数据');
|
console.warn('API返回错误或无数据,使用演示数据');
|
||||||
setDashboardData(mockDashboardData);
|
setDashboardData(mockDashboardData);
|
||||||
@ -99,11 +102,11 @@ export default function DashboardPage() {
|
|||||||
setIsBackgroundRefreshing(true);
|
setIsBackgroundRefreshing(true);
|
||||||
console.log('后台刷新数据...');
|
console.log('后台刷新数据...');
|
||||||
|
|
||||||
// 调用真实API
|
// 调用新的任务列表API
|
||||||
const response = await getRunningStreamData({ project_id: projectId });
|
const response = await getProjectTaskList({ project_id: projectId });
|
||||||
|
|
||||||
if (response.code === 0 && response.data) {
|
if (response.code === 0 && response.data) {
|
||||||
// 检查数据是否真正发生变化
|
// 直接使用新API数据,检查数据是否真正发生变化
|
||||||
if (hasDataChanged(response.data, dashboardData)) {
|
if (hasDataChanged(response.data, dashboardData)) {
|
||||||
// 只有数据变化时才更新UI
|
// 只有数据变化时才更新UI
|
||||||
setDashboardData(response.data);
|
setDashboardData(response.data);
|
||||||
@ -135,9 +138,19 @@ export default function DashboardPage() {
|
|||||||
const getRefreshInterval = React.useCallback(() => {
|
const getRefreshInterval = React.useCallback(() => {
|
||||||
if (!dashboardData || isUsingMockData) return 60000; // mock数据时60秒刷新一次
|
if (!dashboardData || isUsingMockData) return 60000; // mock数据时60秒刷新一次
|
||||||
|
|
||||||
// 检查是否有正在运行的任务
|
// 检查是否有正在运行的任务 - 基于接口实际返回的状态
|
||||||
const hasRunningTasks = Array.isArray(dashboardData) &&
|
const hasRunningTasks = Array.isArray(dashboardData) &&
|
||||||
dashboardData.some((task: any) => task.task_status === 'RUNNING');
|
dashboardData.some((task: any) =>
|
||||||
|
// 接口实际返回的活跃状态
|
||||||
|
task.task_status === 'IN_PROGRESS' ||
|
||||||
|
task.task_status === 'INIT' ||
|
||||||
|
// 检查子任务状态
|
||||||
|
(task.sub_tasks && Array.isArray(task.sub_tasks) &&
|
||||||
|
task.sub_tasks.some((subTask: any) =>
|
||||||
|
subTask.task_status === 'IN_PROGRESS' ||
|
||||||
|
subTask.task_status === 'INIT'
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
return hasRunningTasks ? 10000 : 30000; // 有运行任务时10秒,否则30秒
|
return hasRunningTasks ? 10000 : 30000; // 有运行任务时10秒,否则30秒
|
||||||
}, [dashboardData, isUsingMockData]);
|
}, [dashboardData, isUsingMockData]);
|
||||||
|
|||||||
@ -223,3 +223,19 @@ body {
|
|||||||
.ant-spin-nested-loading .ant-spin {
|
.ant-spin-nested-loading .ant-spin {
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 任务展开动画 */
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fade-in 0.2s ease-out forwards;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// 演示数据 - 模拟get_status.txt接口返回的数据结构
|
// 演示数据 - 模拟新API get_project_task_list接口返回的数据结构
|
||||||
export const mockDashboardData = [
|
export const mockDashboardData = [
|
||||||
{
|
{
|
||||||
"task_id": "689d8ba7-837b-4770-8656-5c8e797e88cb",
|
"task_id": "689d8ba7-837b-4770-8656-5c8e797e88cb",
|
||||||
|
|||||||
@ -109,36 +109,53 @@ export function NetworkTimeline({
|
|||||||
};
|
};
|
||||||
|
|
||||||
tasks.forEach((task: any) => {
|
tasks.forEach((task: any) => {
|
||||||
switch (task.task_status) {
|
const status = task.task_status;
|
||||||
case 'COMPLETED':
|
|
||||||
stats.completed++;
|
// 成功状态
|
||||||
break;
|
if (['COMPLETED', 'SUCCESS', 'FINISHED'].includes(status)) {
|
||||||
case 'IN_PROGRESS':
|
stats.completed++;
|
||||||
stats.inProgress++;
|
}
|
||||||
break;
|
// 进行中状态
|
||||||
case 'FAILED':
|
else if (['IN_PROGRESS', 'RUNNING', 'PROCESSING', 'EXECUTING', 'PAUSED', 'SUSPENDED'].includes(status)) {
|
||||||
stats.failed++;
|
stats.inProgress++;
|
||||||
break;
|
}
|
||||||
case 'PENDING':
|
// 失败状态
|
||||||
default:
|
else if (['FAILED', 'FAILURE', 'ERROR', 'CANCELLED', 'TIMEOUT'].includes(status)) {
|
||||||
stats.pending++;
|
stats.failed++;
|
||||||
break;
|
}
|
||||||
|
// 等待状态
|
||||||
|
else if (['PENDING', 'QUEUED', 'WAITING', 'SCHEDULED', 'INIT'].includes(status)) {
|
||||||
|
stats.pending++;
|
||||||
|
}
|
||||||
|
// 未知状态默认为待处理
|
||||||
|
else {
|
||||||
|
stats.pending++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
|
|
||||||
// 将任务转换为执行时间线格式(支持层级结构)
|
// 主任务列表 - 不依赖展开状态,避免重复计算
|
||||||
const taskExecutions = useMemo((): TaskExecution[] => {
|
const mainTaskExecutions = useMemo((): TaskExecution[] => {
|
||||||
if (!tasks || tasks.length === 0) return [];
|
if (!tasks || tasks.length === 0) return [];
|
||||||
|
|
||||||
const startTime = Math.min(...tasks.map(task => new Date(task.created_at).getTime()));
|
// 获取所有任务的真实开始时间,用于计算时间线的基准点
|
||||||
|
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[] = [];
|
const result: TaskExecution[] = [];
|
||||||
|
|
||||||
tasks.forEach((task: any) => {
|
tasks.forEach((task: any) => {
|
||||||
const taskStartTime = new Date(task.created_at).getTime();
|
// 使用真实的开始时间和结束时间
|
||||||
const taskEndTime = new Date(task.updated_at).getTime();
|
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 duration = taskEndTime - taskStartTime;
|
||||||
|
|
||||||
// 任务执行阶段分解
|
// 任务执行阶段分解
|
||||||
@ -151,35 +168,32 @@ export function NetworkTimeline({
|
|||||||
completion: Math.min(duration * 0.05, 500)
|
completion: Math.min(duration * 0.05, 500)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 主任务
|
// 主任务 - 直接使用新API数据
|
||||||
const mainTask: TaskExecution = {
|
const mainTask: TaskExecution = {
|
||||||
id: task.task_id,
|
id: task.task_id,
|
||||||
name: task.task_name,
|
name: task.task_name,
|
||||||
displayName: getTaskDisplayName(task.task_name),
|
displayName: getTaskDisplayName(task.task_name),
|
||||||
status: task.task_status,
|
status: task.task_status, // 保持原始状态,不转换
|
||||||
statusCode: getTaskStatusCode(task.task_status),
|
statusCode: getTaskStatusCode(task.task_status),
|
||||||
type: getTaskType(task.task_name),
|
type: getTaskType(task.task_name),
|
||||||
dataSize: getTaskDataSize(task),
|
dataSize: getTaskDataSize(task),
|
||||||
executionTime: duration,
|
executionTime: duration,
|
||||||
startTime: taskStartTime - startTime,
|
startTime: taskStartTime - globalStartTime,
|
||||||
endTime: taskEndTime - startTime,
|
endTime: taskEndTime - globalStartTime,
|
||||||
progress: task.task_result?.progress_percentage || 0,
|
progress: getTaskProgress(task),
|
||||||
level: 0,
|
level: 0,
|
||||||
isExpanded: expandedTasks.has(task.task_id),
|
isExpanded: false, // 初始状态,后续动态更新
|
||||||
subTasks: [],
|
subTasks: [],
|
||||||
phases,
|
phases,
|
||||||
taskResult: task.task_result
|
taskResult: parseTaskResult(task.task_result) // 解析JSON字符串
|
||||||
};
|
};
|
||||||
|
|
||||||
result.push(mainTask);
|
// 预处理子任务数据但不添加到结果中
|
||||||
|
|
||||||
// 处理子任务(如果有的话)
|
|
||||||
if (task.task_result?.data && Array.isArray(task.task_result.data)) {
|
if (task.task_result?.data && Array.isArray(task.task_result.data)) {
|
||||||
const subTasks = task.task_result.data.map((subItem: any, subIndex: number) => {
|
const subTasks = task.task_result.data.map((subItem: any, subIndex: number) => {
|
||||||
// 为子任务计算时间分布
|
|
||||||
const totalCount = task.task_result.total_count || task.task_result.data.length;
|
const totalCount = task.task_result.total_count || task.task_result.data.length;
|
||||||
const subDuration = duration / totalCount;
|
const subDuration = duration / totalCount;
|
||||||
const subStartTime = taskStartTime - startTime + (subIndex * subDuration);
|
const subStartTime = taskStartTime - globalStartTime + (subIndex * subDuration);
|
||||||
const subEndTime = subStartTime + subDuration;
|
const subEndTime = subStartTime + subDuration;
|
||||||
|
|
||||||
const subPhases = {
|
const subPhases = {
|
||||||
@ -215,18 +229,87 @@ export function NetworkTimeline({
|
|||||||
taskResult: subItem
|
taskResult: subItem
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
mainTask.subTasks = subTasks;
|
mainTask.subTasks = subTasks;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果主任务展开,将子任务添加到结果中
|
// 处理真实的子任务(新API的sub_tasks字段)- 优先使用真实子任务
|
||||||
if (expandedTasks.has(task.task_id)) {
|
if (task.sub_tasks && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0) {
|
||||||
result.push(...subTasks);
|
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;
|
return result;
|
||||||
}, [tasks, expandedTasks]);
|
}, [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(() => {
|
const filteredTaskExecutions = useMemo(() => {
|
||||||
@ -251,16 +334,148 @@ export function NetworkTimeline({
|
|||||||
return filtered;
|
return filtered;
|
||||||
}, [taskExecutions, filterType, searchTerm]);
|
}, [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 {
|
function getTaskDisplayName(taskName: string): string {
|
||||||
const nameMap: Record<string, string> = {
|
const nameMap: Record<string, string> = {
|
||||||
'generate_character': '角色生成',
|
'generate_character': '角色生成',
|
||||||
|
'generate_character_image': '角色图片生成',
|
||||||
'generate_sketch': '草图生成',
|
'generate_sketch': '草图生成',
|
||||||
'generate_shot_sketch': '分镜生成',
|
'generate_shot_sketch': '分镜生成',
|
||||||
'generate_video': '视频生成',
|
'generate_video': '视频生成',
|
||||||
'generate_videos': '视频生成',
|
'generate_videos': '视频生成',
|
||||||
'generate_music': '音乐生成',
|
'generate_music': '音乐生成',
|
||||||
'final_composition': '最终合成'
|
'final_composition': '最终合成',
|
||||||
|
'refine_orginal_script': '脚本优化',
|
||||||
|
'generate_production_bible': '制作手册生成',
|
||||||
|
'generate_production_bible_json': '制作手册JSON生成'
|
||||||
};
|
};
|
||||||
return nameMap[taskName] || taskName;
|
return nameMap[taskName] || taskName;
|
||||||
}
|
}
|
||||||
@ -271,10 +486,34 @@ export function NetworkTimeline({
|
|||||||
|
|
||||||
function getTaskStatusCode(status: string): number {
|
function getTaskStatusCode(status: string): number {
|
||||||
const statusMap: Record<string, number> = {
|
const statusMap: Record<string, number> = {
|
||||||
|
// 成功状态
|
||||||
'COMPLETED': 200,
|
'COMPLETED': 200,
|
||||||
|
'SUCCESS': 200,
|
||||||
|
'FINISHED': 200,
|
||||||
|
|
||||||
|
// 进行中状态
|
||||||
'IN_PROGRESS': 202,
|
'IN_PROGRESS': 202,
|
||||||
|
'RUNNING': 202,
|
||||||
|
'PROCESSING': 202,
|
||||||
|
'EXECUTING': 202,
|
||||||
|
|
||||||
|
// 等待状态
|
||||||
'PENDING': 100,
|
'PENDING': 100,
|
||||||
'FAILED': 500
|
'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;
|
return statusMap[status] || 200;
|
||||||
}
|
}
|
||||||
@ -282,12 +521,16 @@ export function NetworkTimeline({
|
|||||||
function getTaskType(taskName: string): string {
|
function getTaskType(taskName: string): string {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
'generate_character': 'AI',
|
'generate_character': 'AI',
|
||||||
|
'generate_character_image': 'AI',
|
||||||
'generate_sketch': 'AI',
|
'generate_sketch': 'AI',
|
||||||
'generate_shot_sketch': 'AI',
|
'generate_shot_sketch': 'AI',
|
||||||
'generate_video': 'Video',
|
'generate_video': 'Video',
|
||||||
'generate_videos': 'Video',
|
'generate_videos': 'Video',
|
||||||
'generate_music': 'Audio',
|
'generate_music': 'Audio',
|
||||||
'final_composition': 'Comp'
|
'final_composition': 'Comp',
|
||||||
|
'refine_orginal_script': 'Script',
|
||||||
|
'generate_production_bible': 'Doc',
|
||||||
|
'generate_production_bible_json': 'Data'
|
||||||
};
|
};
|
||||||
return typeMap[taskName] || 'Task';
|
return typeMap[taskName] || 'Task';
|
||||||
}
|
}
|
||||||
@ -308,26 +551,33 @@ export function NetworkTimeline({
|
|||||||
return baseSize * (Math.random() * 2 + 1);
|
return baseSize * (Math.random() * 2 + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取任务进度数量显示 - 与任务执行状态面板完全一致
|
// 获取任务进度数量显示 - 基于sub_tasks的实际完成状态
|
||||||
function getTaskProgressCount(task: any): string {
|
function getTaskProgressCount(task: any): string {
|
||||||
const completed = task.task_result?.completed_count;
|
// 如果有子任务,基于子任务的完成状态计算进度
|
||||||
const total = task.task_result?.total_count || 0;
|
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;
|
||||||
|
|
||||||
// 如果任务已完成但没有子任务数据,显示 1/1
|
return `${completedSubTasks}/${totalSubTasks}`;
|
||||||
if (task.task_status === 'COMPLETED' && total === 0) {
|
|
||||||
return '1/1';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果任务已完成且有子任务数据,确保completed等于total
|
// 如果没有子任务,基于主任务状态
|
||||||
if (task.task_status === 'COMPLETED' && total > 0) {
|
if (['SUCCESS', 'COMPLETED', 'FINISHED'].includes(task.task_status)) {
|
||||||
// 如果没有completed_count字段,但任务已完成,说明全部完成
|
return '1/1'; // 单任务已完成
|
||||||
const actualCompleted = completed !== undefined ? completed : total;
|
|
||||||
return `${actualCompleted}/${total}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他情况显示实际数据
|
if (['IN_PROGRESS', 'RUNNING', 'PROCESSING'].includes(task.task_status)) {
|
||||||
const actualCompleted = completed || 0;
|
return '0/1'; // 单任务进行中
|
||||||
return `${actualCompleted}/${total}`;
|
}
|
||||||
|
|
||||||
|
if (['FAILED', 'FAILURE', 'ERROR'].includes(task.task_status)) {
|
||||||
|
return '0/1'; // 单任务失败
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他情况(等待、初始化等)
|
||||||
|
return '0/1';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取任务进度百分比显示 - 与任务执行状态面板完全一致
|
// 获取任务进度百分比显示 - 与任务执行状态面板完全一致
|
||||||
@ -358,31 +608,69 @@ export function NetworkTimeline({
|
|||||||
|
|
||||||
// 获取任务状态显示文本 - 与任务执行状态面板完全一致
|
// 获取任务状态显示文本 - 与任务执行状态面板完全一致
|
||||||
function getTaskStatusText(status: string): string {
|
function getTaskStatusText(status: string): string {
|
||||||
switch (status) {
|
const statusTextMap: Record<string, string> = {
|
||||||
case 'COMPLETED':
|
// 成功状态
|
||||||
return 'COMPLETED';
|
'SUCCESS': '已完成',
|
||||||
case 'IN_PROGRESS':
|
'COMPLETED': '已完成',
|
||||||
return 'IN_PROGRESS';
|
'FINISHED': '已完成',
|
||||||
case 'FAILED':
|
|
||||||
return 'FAILED';
|
// 进行中状态
|
||||||
case 'PENDING':
|
'IN_PROGRESS': '进行中',
|
||||||
default:
|
'RUNNING': '运行中',
|
||||||
return 'PENDING';
|
'PROCESSING': '处理中',
|
||||||
}
|
'EXECUTING': '执行中',
|
||||||
|
|
||||||
|
// 等待状态
|
||||||
|
'PENDING': '待处理',
|
||||||
|
'QUEUED': '队列中',
|
||||||
|
'WAITING': '等待中',
|
||||||
|
'SCHEDULED': '已调度',
|
||||||
|
'INIT': '初始化', // 新增:接口实际返回的状态
|
||||||
|
|
||||||
|
// 失败状态
|
||||||
|
'FAILED': '失败',
|
||||||
|
'FAILURE': '失败',
|
||||||
|
'ERROR': '错误',
|
||||||
|
'CANCELLED': '已取消',
|
||||||
|
'TIMEOUT': '超时',
|
||||||
|
|
||||||
|
// 暂停状态
|
||||||
|
'PAUSED': '已暂停',
|
||||||
|
'SUSPENDED': '已挂起'
|
||||||
|
};
|
||||||
|
|
||||||
|
return statusTextMap[status] || status;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取任务状态颜色 - 更明显的颜色区分
|
// 获取任务状态颜色 - 更明显的颜色区分
|
||||||
function getTaskStatusColor(status: string): string {
|
function getTaskStatusColor(status: string): string {
|
||||||
switch (status) {
|
// 成功状态 - 绿色系
|
||||||
case 'COMPLETED':
|
if (['SUCCESS', 'COMPLETED', 'FINISHED'].includes(status)) {
|
||||||
return 'text-emerald-400'; // 更亮的绿色
|
return 'text-emerald-400';
|
||||||
case 'IN_PROGRESS':
|
}
|
||||||
return 'text-cyan-400'; // 更亮的蓝色
|
// 进行中状态 - 蓝色系
|
||||||
case 'FAILED':
|
else if (['IN_PROGRESS', 'RUNNING', 'PROCESSING', 'EXECUTING'].includes(status)) {
|
||||||
return 'text-rose-400'; // 更亮的红色
|
return 'text-cyan-400';
|
||||||
case 'PENDING':
|
}
|
||||||
default:
|
// 等待状态 - 黄色系
|
||||||
return 'text-amber-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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -525,8 +813,8 @@ export function NetworkTimeline({
|
|||||||
return 'text-blue-400';
|
return 'text-blue-400';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 展开/折叠任务
|
// 展开/折叠任务 - 优化版本
|
||||||
const toggleTaskExpansion = (taskId: string) => {
|
const toggleTaskExpansion = React.useCallback((taskId: string) => {
|
||||||
setExpandedTasks(prev => {
|
setExpandedTasks(prev => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
if (newSet.has(taskId)) {
|
if (newSet.has(taskId)) {
|
||||||
@ -536,7 +824,7 @@ export function NetworkTimeline({
|
|||||||
}
|
}
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 计算时间线位置
|
// 计算时间线位置
|
||||||
const maxTime = Math.max(...(filteredTaskExecutions.length > 0 ? filteredTaskExecutions.map((task: TaskExecution) => task.endTime) : [0]));
|
const maxTime = Math.max(...(filteredTaskExecutions.length > 0 ? filteredTaskExecutions.map((task: TaskExecution) => task.endTime) : [0]));
|
||||||
@ -631,7 +919,7 @@ export function NetworkTimeline({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 类型过滤 */}
|
{/* 类型过滤 - 基于接口数据动态生成 */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
value={filterType}
|
value={filterType}
|
||||||
@ -639,11 +927,18 @@ export function NetworkTimeline({
|
|||||||
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"
|
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-white focus:outline-none focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20 appearance-none pr-10 hover:border-gray-600 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<option value="all">所有类型</option>
|
<option value="all">所有类型</option>
|
||||||
<option value="AI">AI 任务</option>
|
{availableTaskTypes.map(type => (
|
||||||
<option value="Video">视频任务</option>
|
<option key={type} value={type}>
|
||||||
<option value="Audio">音频任务</option>
|
{type === 'AI' ? 'AI 任务' :
|
||||||
<option value="Comp">合成任务</option>
|
type === 'Video' ? '视频任务' :
|
||||||
<option value="Task">其他任务</option>
|
type === 'Audio' ? '音频任务' :
|
||||||
|
type === 'Comp' ? '合成任务' :
|
||||||
|
type === 'Script' ? '脚本任务' :
|
||||||
|
type === 'Doc' ? '文档任务' :
|
||||||
|
type === 'Data' ? '数据任务' :
|
||||||
|
`${type} 任务`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
<ChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
|
<ChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
@ -677,7 +972,7 @@ export function NetworkTimeline({
|
|||||||
<div className="w-24 text-center">状态</div>
|
<div className="w-24 text-center">状态</div>
|
||||||
<div className="w-16 text-center">类型</div>
|
<div className="w-16 text-center">类型</div>
|
||||||
<div className="w-20 text-center">进度</div>
|
<div className="w-20 text-center">进度</div>
|
||||||
<div className="w-20 text-center">时间</div>
|
<div className="w-32 text-center">时间信息</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 任务列表 */}
|
{/* 任务列表 */}
|
||||||
@ -694,16 +989,14 @@ export function NetworkTimeline({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredTaskExecutions.map((task, index) => (
|
filteredTaskExecutions.map((task, index) => (
|
||||||
<motion.div
|
<div
|
||||||
key={task.id}
|
key={task.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center px-4 py-2 text-sm border-b border-gray-800/30 cursor-pointer hover:bg-gray-800/50",
|
"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"
|
selectedTask === task.id && "bg-blue-600/20",
|
||||||
|
task.level === 1 && "ml-4 bg-gray-900/30" // 子任务缩进和背景区分
|
||||||
)}
|
)}
|
||||||
onClick={() => setSelectedTask(selectedTask === task.id ? null : task.id)}
|
onClick={() => setSelectedTask(selectedTask === task.id ? null : task.id)}
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ delay: index * 0.05 }}
|
|
||||||
>
|
>
|
||||||
{/* 状态图标 */}
|
{/* 状态图标 */}
|
||||||
<div className="w-8 flex justify-center">
|
<div className="w-8 flex justify-center">
|
||||||
@ -716,18 +1009,27 @@ export function NetworkTimeline({
|
|||||||
{/* 展开/折叠按钮 */}
|
{/* 展开/折叠按钮 */}
|
||||||
<div className="w-6 flex justify-center">
|
<div className="w-6 flex justify-center">
|
||||||
{task.level === 0 && task.subTasks && task.subTasks.length > 0 && (
|
{task.level === 0 && task.subTasks && task.subTasks.length > 0 && (
|
||||||
<button
|
<motion.button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleTaskExpansion(task.id);
|
toggleTaskExpansion(task.id);
|
||||||
}}
|
}}
|
||||||
className="p-0.5 hover:bg-gray-700 rounded text-gray-400 hover:text-white"
|
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 }}
|
||||||
>
|
>
|
||||||
{expandedTasks.has(task.id) ?
|
<motion.div
|
||||||
<ChevronDown className="w-3 h-3" /> :
|
animate={{
|
||||||
|
rotate: expandedTasks.has(task.id) ? 90 : 0
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ChevronRight className="w-3 h-3" />
|
<ChevronRight className="w-3 h-3" />
|
||||||
}
|
</motion.div>
|
||||||
</button>
|
</motion.button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -747,20 +1049,70 @@ export function NetworkTimeline({
|
|||||||
{/* 错误信息和重试按钮 */}
|
{/* 错误信息和重试按钮 */}
|
||||||
{task.statusCode >= 400 && (
|
{task.statusCode >= 400 && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
{/* 错误信息悬停提示 */}
|
||||||
onClick={(e) => {
|
<div className="relative group">
|
||||||
e.stopPropagation();
|
<div className="p-1 hover:bg-gray-700 rounded text-red-400 hover:text-red-300 cursor-help">
|
||||||
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
<Info className="w-4 h-4" />
|
||||||
if (originalTask) {
|
</div>
|
||||||
const errorInfo = getTaskErrorInfo(originalTask);
|
|
||||||
alert(`错误信息: ${errorInfo.errorMessage}\n错误代码: ${errorInfo.errorCode || 'UNKNOWN'}`);
|
{/* 悬停时显示的错误信息卡片 */}
|
||||||
}
|
<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">
|
||||||
className="p-1 hover:bg-gray-700 rounded text-red-400 hover:text-red-300"
|
{/* 错误标题 */}
|
||||||
title="查看错误详情"
|
<div className="flex items-center gap-2 mb-3">
|
||||||
>
|
<XCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
|
||||||
<Info className="w-3 h-3" />
|
<h4 className="text-red-400 font-semibold text-base">任务执行失败</h4>
|
||||||
</button>
|
</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 && (
|
{onRetryTask && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -770,7 +1122,7 @@ export function NetworkTimeline({
|
|||||||
className="p-1 hover:bg-gray-700 rounded text-blue-400 hover:text-blue-300"
|
className="p-1 hover:bg-gray-700 rounded text-blue-400 hover:text-blue-300"
|
||||||
title="重试任务"
|
title="重试任务"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-3 h-3" />
|
<RefreshCw className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -792,20 +1144,42 @@ export function NetworkTimeline({
|
|||||||
{/* 进度 */}
|
{/* 进度 */}
|
||||||
<div className="w-20 text-center">
|
<div className="w-20 text-center">
|
||||||
<span className="text-sm font-medium text-gray-200">
|
<span className="text-sm font-medium text-gray-200">
|
||||||
{task.level === 0 ?
|
{task.level === 0 ? (
|
||||||
getTaskProgressCount(tasks.find((t: any) => t.task_id === task.id) || {}) :
|
// 主任务显示 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 === 'IN_PROGRESS' ? `${task.progress}%` :
|
||||||
task.status === 'COMPLETED' ? '100%' : '0%'
|
task.status === 'FAILED' ? '0%' : '0%'
|
||||||
}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 时间 */}
|
{/* 时间信息 */}
|
||||||
<div className="w-20 text-center">
|
<div className="w-32 text-center">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className={cn("text-sm font-medium", getTimeDisplayColor(task.executionTime))}>
|
<span className={cn("text-sm font-medium", getTimeDisplayColor(task.executionTime))}>
|
||||||
{formatTime(task.executionTime)}
|
{formatTime(task.executionTime)}
|
||||||
</span>
|
</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 && (() => {
|
{task.level === 0 && (() => {
|
||||||
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
||||||
@ -818,7 +1192,7 @@ export function NetworkTimeline({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -889,10 +1263,15 @@ export function NetworkTimeline({
|
|||||||
width: `${Math.max((task.executionTime / maxTime) * 100, 0.5)}%`
|
width: `${Math.max((task.executionTime / maxTime) * 100, 0.5)}%`
|
||||||
}}
|
}}
|
||||||
onClick={() => setSelectedTask(task.id)}
|
onClick={() => setSelectedTask(task.id)}
|
||||||
title={`${task.displayName}
|
title={(() => {
|
||||||
|
const taskTimes = getTaskTimes(task);
|
||||||
|
return `${task.displayName}
|
||||||
执行时间: ${formatTime(task.executionTime)}
|
执行时间: ${formatTime(task.executionTime)}
|
||||||
状态: ${getTaskStatusText(task.status)}
|
状态: ${getTaskStatusText(task.status)}
|
||||||
${task.status === 'IN_PROGRESS' ? `进度: ${task.progress}%` : ''}`}
|
${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 rounded-sm overflow-hidden relative">
|
||||||
@ -1073,6 +1452,38 @@ ${task.status === 'IN_PROGRESS' ? `进度: ${task.progress}%` : ''}`}
|
|||||||
<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={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.type}</span></div>
|
||||||
<div><span className="text-gray-400">层级:</span> <span className="text-white">{task.level === 0 ? '主任务' : '子任务'}</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' && (
|
{task.status === 'IN_PROGRESS' && (
|
||||||
<>
|
<>
|
||||||
<div><span className="text-gray-400">进度:</span> <span className="text-white">{task.progress}%</span></div>
|
<div><span className="text-gray-400">进度:</span> <span className="text-white">{task.progress}%</span></div>
|
||||||
|
|||||||
746
docs/dashboard-comprehensive-analysis.md
Normal file
746
docs/dashboard-comprehensive-analysis.md
Normal file
@ -0,0 +1,746 @@
|
|||||||
|
# Dashboard页面功能与接口调用全面分析
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
基于资深全栈开发视角,对Dashboard页面的功能架构、接口调用、数据流程和技术实现进行全面分析。
|
||||||
|
|
||||||
|
## 🏗️ 架构概览
|
||||||
|
|
||||||
|
### 1. 页面结构
|
||||||
|
```
|
||||||
|
Dashboard页面 (app/dashboard/page.tsx)
|
||||||
|
├── 页面容器组件
|
||||||
|
├── NetworkTimeline组件 (核心功能组件)
|
||||||
|
├── 数据获取逻辑
|
||||||
|
├── 实时刷新机制
|
||||||
|
└── 错误处理机制
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 核心组件
|
||||||
|
- **主页面**: `app/dashboard/page.tsx` - 页面入口和数据管理
|
||||||
|
- **时间线组件**: `components/dashboard/network-timeline.tsx` - 核心功能实现
|
||||||
|
- **API层**: `api/video_flow.ts` - 接口调用封装
|
||||||
|
|
||||||
|
## 🔌 接口调用分析
|
||||||
|
|
||||||
|
### 1. 主要接口
|
||||||
|
|
||||||
|
#### 1.1 获取项目任务列表 (核心接口)
|
||||||
|
```typescript
|
||||||
|
// 接口定义
|
||||||
|
export const getProjectTaskList = async (data: {
|
||||||
|
project_id: string;
|
||||||
|
}): Promise<ApiResponse<TaskItem[]>>
|
||||||
|
|
||||||
|
// 接口地址
|
||||||
|
POST https://77.smartvideo.py.qikongjian.com/task/get_project_task_list
|
||||||
|
|
||||||
|
// 请求参数
|
||||||
|
{
|
||||||
|
"project_id": "d203016d-6f7e-4d1c-b66b-1b7d33632800"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应结构
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "操作成功",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"plan_id": "string",
|
||||||
|
"task_id": "string",
|
||||||
|
"start_time": "2025-08-24T17:37:31",
|
||||||
|
"end_time": "2025-08-24T17:37:31",
|
||||||
|
"task_name": "string",
|
||||||
|
"task_status": "SUCCESS|FAILED|COMPLETED|IN_PROGRESS|INIT",
|
||||||
|
"task_result": "string|object",
|
||||||
|
"task_params": "string",
|
||||||
|
"task_message": "string",
|
||||||
|
"created_at": "2025-08-23T02:40:58",
|
||||||
|
"updated_at": "2025-08-24T17:37:31",
|
||||||
|
"error_message": "string|null",
|
||||||
|
"error_traceback": "string|null",
|
||||||
|
"parent_task_id": "string|null",
|
||||||
|
"sub_tasks": [...]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"successful": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 其他相关接口
|
||||||
|
```typescript
|
||||||
|
// 项目详情
|
||||||
|
detailScriptEpisodeNew(data: { project_id: string })
|
||||||
|
|
||||||
|
// 项目标题
|
||||||
|
getScriptTitle(data: { project_id: string })
|
||||||
|
|
||||||
|
// 运行状态数据
|
||||||
|
getRunningStreamData(data: { project_id: string })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 接口调用策略
|
||||||
|
|
||||||
|
#### 2.1 数据获取流程
|
||||||
|
```typescript
|
||||||
|
// 主页面数据获取逻辑
|
||||||
|
const fetchDashboardData = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await getProjectTaskList({ project_id: projectId });
|
||||||
|
|
||||||
|
if (response.code === 0) {
|
||||||
|
setDashboardData(response.data);
|
||||||
|
setLastUpdate(new Date());
|
||||||
|
} else {
|
||||||
|
// 降级到Mock数据
|
||||||
|
setDashboardData(mockTaskData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务数据失败:', error);
|
||||||
|
setDashboardData(mockTaskData);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 智能刷新机制
|
||||||
|
```typescript
|
||||||
|
// 智能轮询逻辑
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPolling || !dashboardData) return;
|
||||||
|
|
||||||
|
// 检查是否有活跃任务
|
||||||
|
const hasRunningTasks = dashboardData.some((task: any) =>
|
||||||
|
task.task_status === 'IN_PROGRESS' ||
|
||||||
|
task.task_status === 'INIT' ||
|
||||||
|
(task.sub_tasks && task.sub_tasks.some((subTask: any) =>
|
||||||
|
subTask.task_status === 'IN_PROGRESS' ||
|
||||||
|
subTask.task_status === 'INIT'
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasRunningTasks) {
|
||||||
|
const interval = setInterval(fetchDashboardData, 5000); // 5秒轮询
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [isPolling, dashboardData]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 数据处理分析
|
||||||
|
|
||||||
|
### 1. 数据结构转换
|
||||||
|
|
||||||
|
#### 1.1 任务执行对象定义
|
||||||
|
```typescript
|
||||||
|
interface TaskExecution {
|
||||||
|
id: string; // 任务ID
|
||||||
|
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; // 任务结果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 数据转换逻辑
|
||||||
|
```typescript
|
||||||
|
// 主任务处理
|
||||||
|
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),
|
||||||
|
// ... 其他字段映射
|
||||||
|
};
|
||||||
|
|
||||||
|
// 子任务处理
|
||||||
|
if (task.sub_tasks && Array.isArray(task.sub_tasks)) {
|
||||||
|
const realSubTasks = task.sub_tasks.map((subTask: any) => ({
|
||||||
|
id: subTask.task_id,
|
||||||
|
name: subTask.task_name,
|
||||||
|
status: subTask.task_status,
|
||||||
|
level: 1,
|
||||||
|
parentId: task.task_id,
|
||||||
|
// ... 其他字段映射
|
||||||
|
}));
|
||||||
|
mainTask.subTasks = realSubTasks;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据映射函数
|
||||||
|
|
||||||
|
#### 2.1 任务类型映射
|
||||||
|
```typescript
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 状态映射
|
||||||
|
```typescript
|
||||||
|
function getTaskStatusCode(status: string): number {
|
||||||
|
const statusCodeMap: Record<string, number> = {
|
||||||
|
'SUCCESS': 200,
|
||||||
|
'COMPLETED': 200,
|
||||||
|
'FINISHED': 200,
|
||||||
|
'IN_PROGRESS': 202,
|
||||||
|
'RUNNING': 202,
|
||||||
|
'PROCESSING': 202,
|
||||||
|
'INIT': 100,
|
||||||
|
'PENDING': 100,
|
||||||
|
'QUEUED': 100,
|
||||||
|
'FAILED': 500,
|
||||||
|
'FAILURE': 500,
|
||||||
|
'ERROR': 500,
|
||||||
|
'CANCELLED': 499,
|
||||||
|
'TIMEOUT': 408
|
||||||
|
};
|
||||||
|
return statusCodeMap[status] || 500;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 功能特性分析
|
||||||
|
|
||||||
|
### 1. 核心功能
|
||||||
|
|
||||||
|
#### 1.1 任务监控面板
|
||||||
|
- **实时状态显示**: 显示所有任务的执行状态
|
||||||
|
- **层级结构**: 支持主任务和子任务的层级显示
|
||||||
|
- **进度跟踪**: 实时显示任务执行进度
|
||||||
|
- **时间信息**: 显示开始时间、结束时间、执行时长
|
||||||
|
|
||||||
|
#### 1.2 交互功能
|
||||||
|
- **展开/收起**: 主任务可展开查看子任务
|
||||||
|
- **任务详情**: 点击任务查看详细信息
|
||||||
|
- **搜索过滤**: 支持按名称、状态、类型搜索
|
||||||
|
- **类型过滤**: 动态下拉选择过滤任务类型
|
||||||
|
|
||||||
|
#### 1.3 可视化展示
|
||||||
|
- **网络时间线**: 可视化任务执行时间线
|
||||||
|
- **状态图标**: 不同状态使用不同颜色和图标
|
||||||
|
- **进度条**: 直观显示任务完成进度
|
||||||
|
- **错误提示**: 悬停显示详细错误信息
|
||||||
|
|
||||||
|
### 2. 高级功能
|
||||||
|
|
||||||
|
#### 2.1 智能刷新
|
||||||
|
```typescript
|
||||||
|
// 基于任务状态的智能轮询
|
||||||
|
const hasRunningTasks = dashboardData.some((task: any) =>
|
||||||
|
task.task_status === 'IN_PROGRESS' ||
|
||||||
|
task.task_status === 'INIT'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasRunningTasks) {
|
||||||
|
// 启动5秒轮询
|
||||||
|
const interval = setInterval(fetchDashboardData, 5000);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 错误处理
|
||||||
|
```typescript
|
||||||
|
// 多层级错误处理
|
||||||
|
try {
|
||||||
|
const response = await getProjectTaskList({ project_id });
|
||||||
|
// 处理成功响应
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API调用失败:', error);
|
||||||
|
// 降级到Mock数据
|
||||||
|
setDashboardData(mockTaskData);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 性能优化
|
||||||
|
```typescript
|
||||||
|
// 数据计算优化
|
||||||
|
const mainTaskExecutions = useMemo(() => {
|
||||||
|
// 主任务计算,不依赖展开状态
|
||||||
|
}, [tasks]);
|
||||||
|
|
||||||
|
const taskExecutions = useMemo(() => {
|
||||||
|
// 轻量级展开状态处理
|
||||||
|
}, [mainTaskExecutions, expandedTasks]);
|
||||||
|
|
||||||
|
// 函数缓存
|
||||||
|
const toggleTaskExpansion = React.useCallback((taskId: string) => {
|
||||||
|
// 展开切换逻辑
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 统计信息
|
||||||
|
|
||||||
|
### 1. 任务统计
|
||||||
|
```typescript
|
||||||
|
const taskStats = useMemo(() => {
|
||||||
|
const stats = { total: 0, completed: 0, inProgress: 0, failed: 0, pending: 0 };
|
||||||
|
|
||||||
|
tasks.forEach((task: any) => {
|
||||||
|
stats.total++;
|
||||||
|
const status = task.task_status;
|
||||||
|
|
||||||
|
if (['COMPLETED', 'SUCCESS'].includes(status)) {
|
||||||
|
stats.completed++;
|
||||||
|
} else if (['IN_PROGRESS', 'RUNNING', 'PROCESSING'].includes(status)) {
|
||||||
|
stats.inProgress++;
|
||||||
|
} else if (['FAILED', 'FAILURE', 'ERROR'].includes(status)) {
|
||||||
|
stats.failed++;
|
||||||
|
} else {
|
||||||
|
stats.pending++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}, [tasks]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 进度计算
|
||||||
|
```typescript
|
||||||
|
// 基于子任务的进度计算
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他进度计算逻辑...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 技术实现
|
||||||
|
|
||||||
|
### 1. 状态管理
|
||||||
|
```typescript
|
||||||
|
// React状态管理
|
||||||
|
const [dashboardData, setDashboardData] = useState<any[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
|
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
||||||
|
const [selectedTask, setSelectedTask] = useState<string | null>(null);
|
||||||
|
const [expandedTasks, setExpandedTasks] = useState<Set<string>>(new Set());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 动画系统
|
||||||
|
```typescript
|
||||||
|
// Framer Motion动画
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
animate={{ rotate: expandedTasks.has(task.id) ? 90 : 0 }}
|
||||||
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 样式系统
|
||||||
|
```typescript
|
||||||
|
// Tailwind CSS + 条件样式
|
||||||
|
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",
|
||||||
|
selectedTask === task.id && "bg-blue-600/20",
|
||||||
|
task.level === 1 && "ml-4 bg-gray-900/30"
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 数据流程图
|
||||||
|
|
||||||
|
```
|
||||||
|
用户访问Dashboard
|
||||||
|
↓
|
||||||
|
获取project_id (URL参数)
|
||||||
|
↓
|
||||||
|
调用getProjectTaskList接口
|
||||||
|
↓
|
||||||
|
数据转换和处理
|
||||||
|
↓
|
||||||
|
渲染NetworkTimeline组件
|
||||||
|
↓
|
||||||
|
用户交互 (展开/搜索/过滤)
|
||||||
|
↓
|
||||||
|
智能刷新检测
|
||||||
|
↓
|
||||||
|
条件性轮询更新
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 核心价值
|
||||||
|
|
||||||
|
### 1. 用户价值
|
||||||
|
- **实时监控**: 实时了解任务执行状态
|
||||||
|
- **层次清晰**: 主任务和子任务层级分明
|
||||||
|
- **操作便捷**: 直观的交互和搜索功能
|
||||||
|
- **信息完整**: 详细的任务信息和错误提示
|
||||||
|
|
||||||
|
### 2. 技术价值
|
||||||
|
- **性能优化**: 智能轮询和数据缓存
|
||||||
|
- **用户体验**: 流畅的动画和交互
|
||||||
|
- **可维护性**: 清晰的组件结构和数据流
|
||||||
|
- **扩展性**: 支持新的任务类型和状态
|
||||||
|
|
||||||
|
### 3. 业务价值
|
||||||
|
- **提升效率**: 快速定位和解决问题
|
||||||
|
- **降低成本**: 减少人工监控成本
|
||||||
|
- **增强体验**: 专业的监控界面
|
||||||
|
- **数据驱动**: 基于真实数据的决策支持
|
||||||
|
|
||||||
|
## 🚀 总结
|
||||||
|
|
||||||
|
Dashboard页面是一个功能完整、技术先进的任务监控系统,具备:
|
||||||
|
|
||||||
|
1. **完整的数据流程**: 从API调用到UI展示的完整链路
|
||||||
|
2. **智能的交互设计**: 基于用户需求的功能设计
|
||||||
|
3. **优秀的性能表现**: 通过多种优化策略确保流畅体验
|
||||||
|
4. **强大的扩展能力**: 支持未来功能扩展和数据结构变化
|
||||||
|
|
||||||
|
该系统充分体现了现代前端开发的最佳实践,是一个高质量的企业级应用。
|
||||||
|
|
||||||
|
## 🔍 深度技术分析
|
||||||
|
|
||||||
|
### 1. 接口设计模式
|
||||||
|
|
||||||
|
#### 1.1 RESTful API设计
|
||||||
|
```typescript
|
||||||
|
// 统一的API响应格式
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: number; // 状态码:0=成功,非0=失败
|
||||||
|
message: string; // 响应消息
|
||||||
|
data: T; // 响应数据
|
||||||
|
successful: boolean; // 成功标识
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理策略
|
||||||
|
const handleApiError = (error: any) => {
|
||||||
|
if (error.code === 'NETWORK_ERROR') {
|
||||||
|
return '网络连接失败,请检查网络连接';
|
||||||
|
} else if (error.response?.status === 404) {
|
||||||
|
return 'API接口不存在';
|
||||||
|
} else if (error.response?.status === 500) {
|
||||||
|
return '服务器内部错误';
|
||||||
|
}
|
||||||
|
return error.message || '未知错误';
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 数据缓存策略
|
||||||
|
```typescript
|
||||||
|
// 智能数据比较
|
||||||
|
const hasDataChanged = (newData: any, oldData: any) => {
|
||||||
|
if (!oldData || !newData) return true;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(newData) !== JSON.stringify(oldData);
|
||||||
|
} catch {
|
||||||
|
return true; // 比较失败时认为数据已变化
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 后台静默刷新
|
||||||
|
const refreshDataSilently = async () => {
|
||||||
|
const response = await getProjectTaskList({ project_id: projectId });
|
||||||
|
if (hasDataChanged(response.data, dashboardData)) {
|
||||||
|
setDashboardData(response.data);
|
||||||
|
console.log('数据已更新');
|
||||||
|
} else {
|
||||||
|
console.log('数据无变化');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 组件架构设计
|
||||||
|
|
||||||
|
#### 2.1 组件职责分离
|
||||||
|
```typescript
|
||||||
|
// 页面组件 (app/dashboard/page.tsx)
|
||||||
|
// 职责:数据获取、状态管理、错误处理
|
||||||
|
export default function DashboardPage() {
|
||||||
|
// 数据管理逻辑
|
||||||
|
// 轮询逻辑
|
||||||
|
// 错误处理逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 展示组件 (components/dashboard/network-timeline.tsx)
|
||||||
|
// 职责:UI渲染、用户交互、数据展示
|
||||||
|
export function NetworkTimeline({ tasks, onRefresh, ... }) {
|
||||||
|
// UI渲染逻辑
|
||||||
|
// 交互处理逻辑
|
||||||
|
// 数据过滤和搜索逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 Hook设计模式
|
||||||
|
```typescript
|
||||||
|
// 自定义Hook示例
|
||||||
|
const useTaskPolling = (projectId: string, interval: number) => {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPolling) return;
|
||||||
|
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
const response = await getProjectTaskList({ project_id: projectId });
|
||||||
|
setData(response.data);
|
||||||
|
}, interval);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [projectId, interval, isPolling]);
|
||||||
|
|
||||||
|
return { data, isPolling, setIsPolling };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 性能优化策略
|
||||||
|
|
||||||
|
#### 3.1 渲染优化
|
||||||
|
```typescript
|
||||||
|
// 虚拟化长列表 (如果任务数量很大)
|
||||||
|
import { FixedSizeList as List } from 'react-window';
|
||||||
|
|
||||||
|
const TaskList = ({ tasks }) => (
|
||||||
|
<List
|
||||||
|
height={600}
|
||||||
|
itemCount={tasks.length}
|
||||||
|
itemSize={60}
|
||||||
|
itemData={tasks}
|
||||||
|
>
|
||||||
|
{TaskItem}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 防抖搜索
|
||||||
|
const debouncedSearch = useMemo(
|
||||||
|
() => debounce((term: string) => {
|
||||||
|
setFilteredTasks(tasks.filter(task =>
|
||||||
|
task.name.toLowerCase().includes(term.toLowerCase())
|
||||||
|
));
|
||||||
|
}, 300),
|
||||||
|
[tasks]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 内存管理
|
||||||
|
```typescript
|
||||||
|
// 清理定时器和事件监听
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(fetchData, 5000);
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 错误边界和容错机制
|
||||||
|
|
||||||
|
#### 4.1 React错误边界
|
||||||
|
```typescript
|
||||||
|
class TaskErrorBoundary extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, errorInfo) {
|
||||||
|
console.error('任务组件错误:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return <div>任务加载失败,请刷新页面</div>;
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 API容错机制
|
||||||
|
```typescript
|
||||||
|
// 重试机制
|
||||||
|
const fetchWithRetry = async (url: string, options: any, retries = 3) => {
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
if (response.ok) return response;
|
||||||
|
} catch (error) {
|
||||||
|
if (i === retries - 1) throw error;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 降级策略
|
||||||
|
const fetchDataWithFallback = async () => {
|
||||||
|
try {
|
||||||
|
return await getProjectTaskList({ project_id });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('API调用失败,使用Mock数据');
|
||||||
|
return { code: 0, data: mockTaskData };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 测试策略
|
||||||
|
|
||||||
|
#### 5.1 单元测试
|
||||||
|
```typescript
|
||||||
|
// 数据转换函数测试
|
||||||
|
describe('getTaskType', () => {
|
||||||
|
it('should return correct task type', () => {
|
||||||
|
expect(getTaskType('generate_character')).toBe('AI');
|
||||||
|
expect(getTaskType('generate_video')).toBe('Video');
|
||||||
|
expect(getTaskType('unknown_task')).toBe('Task');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件测试
|
||||||
|
describe('NetworkTimeline', () => {
|
||||||
|
it('should render task list correctly', () => {
|
||||||
|
const mockTasks = [{ task_id: '1', task_name: 'test' }];
|
||||||
|
render(<NetworkTimeline tasks={mockTasks} />);
|
||||||
|
expect(screen.getByText('test')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2 集成测试
|
||||||
|
```typescript
|
||||||
|
// API集成测试
|
||||||
|
describe('Dashboard API Integration', () => {
|
||||||
|
it('should fetch and display task data', async () => {
|
||||||
|
const mockResponse = { code: 0, data: mockTasks };
|
||||||
|
jest.spyOn(api, 'getProjectTaskList').mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
render(<DashboardPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('任务列表')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 监控和日志
|
||||||
|
|
||||||
|
#### 6.1 性能监控
|
||||||
|
```typescript
|
||||||
|
// 性能指标收集
|
||||||
|
const usePerformanceMonitor = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new PerformanceObserver((list) => {
|
||||||
|
list.getEntries().forEach((entry) => {
|
||||||
|
if (entry.entryType === 'measure') {
|
||||||
|
console.log(`${entry.name}: ${entry.duration}ms`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe({ entryTypes: ['measure'] });
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染时间测量
|
||||||
|
const measureRenderTime = (componentName: string) => {
|
||||||
|
performance.mark(`${componentName}-start`);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
performance.mark(`${componentName}-end`);
|
||||||
|
performance.measure(
|
||||||
|
`${componentName}-render`,
|
||||||
|
`${componentName}-start`,
|
||||||
|
`${componentName}-end`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2 错误日志
|
||||||
|
```typescript
|
||||||
|
// 错误上报
|
||||||
|
const reportError = (error: Error, context: string) => {
|
||||||
|
const errorInfo = {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
context,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
url: window.location.href
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送到错误监控服务
|
||||||
|
fetch('/api/errors', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(errorInfo)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 最佳实践总结
|
||||||
|
|
||||||
|
### 1. 代码质量
|
||||||
|
- **TypeScript**: 完整的类型定义和类型安全
|
||||||
|
- **ESLint/Prettier**: 代码规范和格式化
|
||||||
|
- **组件化**: 高内聚低耦合的组件设计
|
||||||
|
- **Hook复用**: 自定义Hook提取公共逻辑
|
||||||
|
|
||||||
|
### 2. 用户体验
|
||||||
|
- **加载状态**: 清晰的加载和错误状态提示
|
||||||
|
- **响应式设计**: 适配不同屏幕尺寸
|
||||||
|
- **无障碍访问**: 键盘导航和屏幕阅读器支持
|
||||||
|
- **性能优化**: 快速响应和流畅动画
|
||||||
|
|
||||||
|
### 3. 可维护性
|
||||||
|
- **模块化**: 清晰的文件结构和模块划分
|
||||||
|
- **文档化**: 完整的注释和文档
|
||||||
|
- **测试覆盖**: 单元测试和集成测试
|
||||||
|
- **版本控制**: 规范的Git提交和分支管理
|
||||||
|
|
||||||
|
这个Dashboard系统展现了现代React应用开发的最佳实践,是一个技术先进、用户友好、可维护性强的企业级应用。
|
||||||
460
docs/get_project_task_list.txt
Normal file
460
docs/get_project_task_list.txt
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user