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 NetworkTimeline from '@/components/dashboard/network-timeline';
|
||||
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 { cn } from '@/public/lib/utils';
|
||||
|
||||
@ -23,6 +23,8 @@ export default function DashboardPage() {
|
||||
// 使用 ref 来存储最新的状态,避免定时器闭包问题
|
||||
const stateRef = useRef({ isUsingMockData, dashboardData });
|
||||
|
||||
|
||||
|
||||
// 初始加载数据
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
@ -32,17 +34,18 @@ export default function DashboardPage() {
|
||||
|
||||
console.log('正在获取项目数据,项目ID:', projectId);
|
||||
|
||||
// 调用真实API
|
||||
const response = await getRunningStreamData({ project_id: projectId });
|
||||
// 调用新的任务列表API
|
||||
const response = await getProjectTaskList({ project_id: projectId });
|
||||
|
||||
console.log('API响应:', response);
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
// 直接使用新API数据,不进行格式转换
|
||||
setDashboardData(response.data);
|
||||
setIsUsingMockData(false);
|
||||
setLastUpdateTime(new Date());
|
||||
setConnectionStatus('connected');
|
||||
console.log('成功获取真实数据');
|
||||
console.log('成功获取真实数据:', response.data);
|
||||
} else {
|
||||
console.warn('API返回错误或无数据,使用演示数据');
|
||||
setDashboardData(mockDashboardData);
|
||||
@ -99,11 +102,11 @@ export default function DashboardPage() {
|
||||
setIsBackgroundRefreshing(true);
|
||||
console.log('后台刷新数据...');
|
||||
|
||||
// 调用真实API
|
||||
const response = await getRunningStreamData({ project_id: projectId });
|
||||
// 调用新的任务列表API
|
||||
const response = await getProjectTaskList({ project_id: projectId });
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
// 检查数据是否真正发生变化
|
||||
// 直接使用新API数据,检查数据是否真正发生变化
|
||||
if (hasDataChanged(response.data, dashboardData)) {
|
||||
// 只有数据变化时才更新UI
|
||||
setDashboardData(response.data);
|
||||
@ -135,9 +138,19 @@ export default function DashboardPage() {
|
||||
const getRefreshInterval = React.useCallback(() => {
|
||||
if (!dashboardData || isUsingMockData) return 60000; // mock数据时60秒刷新一次
|
||||
|
||||
// 检查是否有正在运行的任务
|
||||
// 检查是否有正在运行的任务 - 基于接口实际返回的状态
|
||||
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秒
|
||||
}, [dashboardData, isUsingMockData]);
|
||||
|
||||
@ -223,3 +223,19 @@ body {
|
||||
.ant-spin-nested-loading .ant-spin {
|
||||
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 = [
|
||||
{
|
||||
"task_id": "689d8ba7-837b-4770-8656-5c8e797e88cb",
|
||||
|
||||
@ -109,36 +109,53 @@ export function NetworkTimeline({
|
||||
};
|
||||
|
||||
tasks.forEach((task: any) => {
|
||||
switch (task.task_status) {
|
||||
case 'COMPLETED':
|
||||
const status = task.task_status;
|
||||
|
||||
// 成功状态
|
||||
if (['COMPLETED', 'SUCCESS', 'FINISHED'].includes(status)) {
|
||||
stats.completed++;
|
||||
break;
|
||||
case 'IN_PROGRESS':
|
||||
}
|
||||
// 进行中状态
|
||||
else if (['IN_PROGRESS', 'RUNNING', 'PROCESSING', 'EXECUTING', 'PAUSED', 'SUSPENDED'].includes(status)) {
|
||||
stats.inProgress++;
|
||||
break;
|
||||
case 'FAILED':
|
||||
}
|
||||
// 失败状态
|
||||
else if (['FAILED', 'FAILURE', 'ERROR', 'CANCELLED', 'TIMEOUT'].includes(status)) {
|
||||
stats.failed++;
|
||||
break;
|
||||
case 'PENDING':
|
||||
default:
|
||||
}
|
||||
// 等待状态
|
||||
else if (['PENDING', 'QUEUED', 'WAITING', 'SCHEDULED', 'INIT'].includes(status)) {
|
||||
stats.pending++;
|
||||
}
|
||||
// 未知状态默认为待处理
|
||||
else {
|
||||
stats.pending++;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}, [tasks]);
|
||||
|
||||
// 将任务转换为执行时间线格式(支持层级结构)
|
||||
const taskExecutions = useMemo((): TaskExecution[] => {
|
||||
// 主任务列表 - 不依赖展开状态,避免重复计算
|
||||
const mainTaskExecutions = useMemo((): TaskExecution[] => {
|
||||
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[] = [];
|
||||
|
||||
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;
|
||||
|
||||
// 任务执行阶段分解
|
||||
@ -151,35 +168,32 @@ export function NetworkTimeline({
|
||||
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,
|
||||
status: task.task_status, // 保持原始状态,不转换
|
||||
statusCode: getTaskStatusCode(task.task_status),
|
||||
type: getTaskType(task.task_name),
|
||||
dataSize: getTaskDataSize(task),
|
||||
executionTime: duration,
|
||||
startTime: taskStartTime - startTime,
|
||||
endTime: taskEndTime - startTime,
|
||||
progress: task.task_result?.progress_percentage || 0,
|
||||
startTime: taskStartTime - globalStartTime,
|
||||
endTime: taskEndTime - globalStartTime,
|
||||
progress: getTaskProgress(task),
|
||||
level: 0,
|
||||
isExpanded: expandedTasks.has(task.task_id),
|
||||
isExpanded: false, // 初始状态,后续动态更新
|
||||
subTasks: [],
|
||||
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)) {
|
||||
const subTasks = task.task_result.data.map((subItem: any, subIndex: number) => {
|
||||
// 为子任务计算时间分布
|
||||
const totalCount = task.task_result.total_count || task.task_result.data.length;
|
||||
const subDuration = duration / totalCount;
|
||||
const subStartTime = taskStartTime - startTime + (subIndex * subDuration);
|
||||
const subStartTime = taskStartTime - globalStartTime + (subIndex * subDuration);
|
||||
const subEndTime = subStartTime + subDuration;
|
||||
|
||||
const subPhases = {
|
||||
@ -215,18 +229,87 @@ export function NetworkTimeline({
|
||||
taskResult: subItem
|
||||
};
|
||||
});
|
||||
|
||||
mainTask.subTasks = subTasks;
|
||||
|
||||
// 如果主任务展开,将子任务添加到结果中
|
||||
if (expandedTasks.has(task.task_id)) {
|
||||
result.push(...subTasks);
|
||||
}
|
||||
|
||||
// 处理真实的子任务(新API的sub_tasks字段)- 优先使用真实子任务
|
||||
if (task.sub_tasks && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0) {
|
||||
const realSubTasks = task.sub_tasks.map((subTask: any, subIndex: number) => {
|
||||
const subRealStartTime = subTask.start_time || subTask.created_at;
|
||||
const subRealEndTime = subTask.end_time || subTask.updated_at;
|
||||
|
||||
const subTaskStartTime = new Date(subRealStartTime).getTime();
|
||||
const subTaskEndTime = new Date(subRealEndTime).getTime();
|
||||
const subTaskDuration = subTaskEndTime - subTaskStartTime;
|
||||
|
||||
const subPhases = {
|
||||
initialization: subTaskDuration * 0.05,
|
||||
dataLoading: subTaskDuration * 0.1,
|
||||
aiProcessing: subTaskDuration * 0.7,
|
||||
resultGeneration: subTaskDuration * 0.1,
|
||||
dataTransfer: subTaskDuration * 0.03,
|
||||
completion: subTaskDuration * 0.02
|
||||
};
|
||||
|
||||
return {
|
||||
id: subTask.task_id,
|
||||
name: subTask.task_name,
|
||||
displayName: getTaskDisplayName(subTask.task_name),
|
||||
status: subTask.task_status, // 保持原始状态
|
||||
statusCode: getTaskStatusCode(subTask.task_status),
|
||||
type: getTaskType(subTask.task_name),
|
||||
dataSize: getTaskDataSize(subTask),
|
||||
executionTime: subTaskDuration,
|
||||
startTime: subTaskStartTime - globalStartTime,
|
||||
endTime: subTaskEndTime - globalStartTime,
|
||||
progress: getTaskProgress(subTask),
|
||||
level: 1,
|
||||
parentId: task.task_id,
|
||||
isExpanded: false,
|
||||
subTasks: [],
|
||||
phases: subPhases,
|
||||
taskResult: parseTaskResult(subTask.task_result) // 解析JSON字符串
|
||||
};
|
||||
});
|
||||
|
||||
// 真实子任务优先:覆盖之前的subTasks
|
||||
mainTask.subTasks = realSubTasks;
|
||||
}
|
||||
|
||||
result.push(mainTask);
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [tasks]);
|
||||
|
||||
// 完整的任务执行列表 - 包含展开的子任务
|
||||
const taskExecutions = useMemo((): TaskExecution[] => {
|
||||
const result: TaskExecution[] = [];
|
||||
|
||||
mainTaskExecutions.forEach(mainTask => {
|
||||
// 更新展开状态
|
||||
mainTask.isExpanded = expandedTasks.has(mainTask.id);
|
||||
result.push(mainTask);
|
||||
|
||||
// 如果展开,添加子任务
|
||||
if (mainTask.isExpanded && mainTask.subTasks && mainTask.subTasks.length > 0) {
|
||||
result.push(...mainTask.subTasks);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [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(() => {
|
||||
@ -251,16 +334,148 @@ export function NetworkTimeline({
|
||||
return filtered;
|
||||
}, [taskExecutions, filterType, searchTerm]);
|
||||
|
||||
// 格式化时间显示
|
||||
function formatDateTime(dateString: string | null | undefined): string {
|
||||
if (!dateString) return '未知';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) return '无效时间';
|
||||
|
||||
// 格式化为本地时间:YYYY-MM-DD HH:mm:ss
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('时间格式化失败:', error, dateString);
|
||||
return '格式错误';
|
||||
}
|
||||
}
|
||||
|
||||
// 解析任务结果 - 处理JSON字符串
|
||||
function parseTaskResult(taskResult: any) {
|
||||
if (!taskResult) return null;
|
||||
|
||||
if (typeof taskResult === 'string') {
|
||||
try {
|
||||
return JSON.parse(taskResult);
|
||||
} catch (error) {
|
||||
console.warn('解析task_result失败:', error, taskResult);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return taskResult;
|
||||
}
|
||||
|
||||
// 获取子任务完成状况文本 (如: "10/12")
|
||||
function getSubTaskStatus(task: any): string | null {
|
||||
if (!task.sub_tasks || !Array.isArray(task.sub_tasks) || task.sub_tasks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const completedSubTasks = task.sub_tasks.filter((sub: any) =>
|
||||
['SUCCESS', 'COMPLETED', 'FINISHED'].includes(sub.task_status)
|
||||
).length;
|
||||
|
||||
return `${completedSubTasks}/${task.sub_tasks.length}`;
|
||||
}
|
||||
|
||||
// 获取任务进度
|
||||
function getTaskProgress(task: any): number {
|
||||
// 如果有子任务,优先基于子任务完成情况计算进度
|
||||
if (task.sub_tasks && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0) {
|
||||
const completedSubTasks = task.sub_tasks.filter((sub: any) =>
|
||||
['SUCCESS', 'COMPLETED', 'FINISHED'].includes(sub.task_status)
|
||||
).length;
|
||||
return Math.round((completedSubTasks / task.sub_tasks.length) * 100);
|
||||
}
|
||||
|
||||
// 其次使用解析后的结果中的进度
|
||||
const parsedResult = parseTaskResult(task.task_result);
|
||||
if (parsedResult?.progress_percentage) {
|
||||
return parsedResult.progress_percentage;
|
||||
}
|
||||
|
||||
// 最后根据状态推断进度
|
||||
switch (task.task_status) {
|
||||
case 'SUCCESS':
|
||||
case 'COMPLETED':
|
||||
case 'FINISHED':
|
||||
return 100;
|
||||
case 'FAILED':
|
||||
case 'FAILURE':
|
||||
case 'ERROR':
|
||||
case 'CANCELLED':
|
||||
return 0;
|
||||
case 'IN_PROGRESS':
|
||||
case 'RUNNING':
|
||||
case 'PROCESSING':
|
||||
return 50; // 默认50%
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务的开始和结束时间
|
||||
function getTaskTimes(task: TaskExecution) {
|
||||
// 首先尝试从主任务列表中查找
|
||||
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
||||
|
||||
if (originalTask) {
|
||||
return {
|
||||
startTime: originalTask.start_time || originalTask.created_at,
|
||||
endTime: originalTask.end_time || originalTask.updated_at,
|
||||
createdAt: originalTask.created_at,
|
||||
updatedAt: originalTask.updated_at
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是子任务,从父任务的sub_tasks中查找
|
||||
if (task.level === 1 && task.parentId) {
|
||||
const parentTask = tasks.find((t: any) => t.task_id === task.parentId);
|
||||
if (parentTask && parentTask.sub_tasks) {
|
||||
const subTask = parentTask.sub_tasks.find((st: any) => st.task_id === task.id);
|
||||
if (subTask) {
|
||||
return {
|
||||
startTime: subTask.start_time || subTask.created_at,
|
||||
endTime: subTask.end_time || subTask.updated_at,
|
||||
createdAt: subTask.created_at,
|
||||
updatedAt: subTask.updated_at
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都找不到,返回空值
|
||||
return {
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
createdAt: null,
|
||||
updatedAt: null
|
||||
};
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
function getTaskDisplayName(taskName: string): string {
|
||||
const nameMap: Record<string, string> = {
|
||||
'generate_character': '角色生成',
|
||||
'generate_character_image': '角色图片生成',
|
||||
'generate_sketch': '草图生成',
|
||||
'generate_shot_sketch': '分镜生成',
|
||||
'generate_video': '视频生成',
|
||||
'generate_videos': '视频生成',
|
||||
'generate_music': '音乐生成',
|
||||
'final_composition': '最终合成'
|
||||
'final_composition': '最终合成',
|
||||
'refine_orginal_script': '脚本优化',
|
||||
'generate_production_bible': '制作手册生成',
|
||||
'generate_production_bible_json': '制作手册JSON生成'
|
||||
};
|
||||
return nameMap[taskName] || taskName;
|
||||
}
|
||||
@ -271,10 +486,34 @@ export function NetworkTimeline({
|
||||
|
||||
function getTaskStatusCode(status: string): number {
|
||||
const statusMap: Record<string, number> = {
|
||||
// 成功状态
|
||||
'COMPLETED': 200,
|
||||
'SUCCESS': 200,
|
||||
'FINISHED': 200,
|
||||
|
||||
// 进行中状态
|
||||
'IN_PROGRESS': 202,
|
||||
'RUNNING': 202,
|
||||
'PROCESSING': 202,
|
||||
'EXECUTING': 202,
|
||||
|
||||
// 等待状态
|
||||
'PENDING': 100,
|
||||
'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;
|
||||
}
|
||||
@ -282,12 +521,16 @@ export function NetworkTimeline({
|
||||
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'
|
||||
'final_composition': 'Comp',
|
||||
'refine_orginal_script': 'Script',
|
||||
'generate_production_bible': 'Doc',
|
||||
'generate_production_bible_json': 'Data'
|
||||
};
|
||||
return typeMap[taskName] || 'Task';
|
||||
}
|
||||
@ -308,26 +551,33 @@ export function NetworkTimeline({
|
||||
return baseSize * (Math.random() * 2 + 1);
|
||||
}
|
||||
|
||||
// 获取任务进度数量显示 - 与任务执行状态面板完全一致
|
||||
// 获取任务进度数量显示 - 基于sub_tasks的实际完成状态
|
||||
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
|
||||
if (task.task_status === 'COMPLETED' && total === 0) {
|
||||
return '1/1';
|
||||
return `${completedSubTasks}/${totalSubTasks}`;
|
||||
}
|
||||
|
||||
// 如果任务已完成且有子任务数据,确保completed等于total
|
||||
if (task.task_status === 'COMPLETED' && total > 0) {
|
||||
// 如果没有completed_count字段,但任务已完成,说明全部完成
|
||||
const actualCompleted = completed !== undefined ? completed : total;
|
||||
return `${actualCompleted}/${total}`;
|
||||
// 如果没有子任务,基于主任务状态
|
||||
if (['SUCCESS', 'COMPLETED', 'FINISHED'].includes(task.task_status)) {
|
||||
return '1/1'; // 单任务已完成
|
||||
}
|
||||
|
||||
// 其他情况显示实际数据
|
||||
const actualCompleted = completed || 0;
|
||||
return `${actualCompleted}/${total}`;
|
||||
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';
|
||||
}
|
||||
|
||||
// 获取任务进度百分比显示 - 与任务执行状态面板完全一致
|
||||
@ -358,31 +608,69 @@ export function NetworkTimeline({
|
||||
|
||||
// 获取任务状态显示文本 - 与任务执行状态面板完全一致
|
||||
function getTaskStatusText(status: string): string {
|
||||
switch (status) {
|
||||
case 'COMPLETED':
|
||||
return 'COMPLETED';
|
||||
case 'IN_PROGRESS':
|
||||
return 'IN_PROGRESS';
|
||||
case 'FAILED':
|
||||
return 'FAILED';
|
||||
case 'PENDING':
|
||||
default:
|
||||
return 'PENDING';
|
||||
}
|
||||
const statusTextMap: Record<string, string> = {
|
||||
// 成功状态
|
||||
'SUCCESS': '已完成',
|
||||
'COMPLETED': '已完成',
|
||||
'FINISHED': '已完成',
|
||||
|
||||
// 进行中状态
|
||||
'IN_PROGRESS': '进行中',
|
||||
'RUNNING': '运行中',
|
||||
'PROCESSING': '处理中',
|
||||
'EXECUTING': '执行中',
|
||||
|
||||
// 等待状态
|
||||
'PENDING': '待处理',
|
||||
'QUEUED': '队列中',
|
||||
'WAITING': '等待中',
|
||||
'SCHEDULED': '已调度',
|
||||
'INIT': '初始化', // 新增:接口实际返回的状态
|
||||
|
||||
// 失败状态
|
||||
'FAILED': '失败',
|
||||
'FAILURE': '失败',
|
||||
'ERROR': '错误',
|
||||
'CANCELLED': '已取消',
|
||||
'TIMEOUT': '超时',
|
||||
|
||||
// 暂停状态
|
||||
'PAUSED': '已暂停',
|
||||
'SUSPENDED': '已挂起'
|
||||
};
|
||||
|
||||
return statusTextMap[status] || status;
|
||||
}
|
||||
|
||||
// 获取任务状态颜色 - 更明显的颜色区分
|
||||
function getTaskStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'COMPLETED':
|
||||
return 'text-emerald-400'; // 更亮的绿色
|
||||
case 'IN_PROGRESS':
|
||||
return 'text-cyan-400'; // 更亮的蓝色
|
||||
case 'FAILED':
|
||||
return 'text-rose-400'; // 更亮的红色
|
||||
case 'PENDING':
|
||||
default:
|
||||
return 'text-amber-400'; // 黄色表示等待
|
||||
// 成功状态 - 绿色系
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@ -525,8 +813,8 @@ export function NetworkTimeline({
|
||||
return 'text-blue-400';
|
||||
}
|
||||
|
||||
// 展开/折叠任务
|
||||
const toggleTaskExpansion = (taskId: string) => {
|
||||
// 展开/折叠任务 - 优化版本
|
||||
const toggleTaskExpansion = React.useCallback((taskId: string) => {
|
||||
setExpandedTasks(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(taskId)) {
|
||||
@ -536,7 +824,7 @@ export function NetworkTimeline({
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 计算时间线位置
|
||||
const maxTime = Math.max(...(filteredTaskExecutions.length > 0 ? filteredTaskExecutions.map((task: TaskExecution) => task.endTime) : [0]));
|
||||
@ -631,7 +919,7 @@ export function NetworkTimeline({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 类型过滤 */}
|
||||
{/* 类型过滤 - 基于接口数据动态生成 */}
|
||||
<div className="relative">
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<option value="all">所有类型</option>
|
||||
<option value="AI">AI 任务</option>
|
||||
<option value="Video">视频任务</option>
|
||||
<option value="Audio">音频任务</option>
|
||||
<option value="Comp">合成任务</option>
|
||||
<option value="Task">其他任务</option>
|
||||
{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>
|
||||
@ -677,7 +972,7 @@ export function NetworkTimeline({
|
||||
<div className="w-24 text-center">状态</div>
|
||||
<div className="w-16 text-center">类型</div>
|
||||
<div className="w-20 text-center">进度</div>
|
||||
<div className="w-20 text-center">时间</div>
|
||||
<div className="w-32 text-center">时间信息</div>
|
||||
</div>
|
||||
|
||||
{/* 任务列表 */}
|
||||
@ -694,16 +989,14 @@ export function NetworkTimeline({
|
||||
</div>
|
||||
) : (
|
||||
filteredTaskExecutions.map((task, index) => (
|
||||
<motion.div
|
||||
<div
|
||||
key={task.id}
|
||||
className={cn(
|
||||
"flex items-center px-4 py-2 text-sm border-b border-gray-800/30 cursor-pointer hover:bg-gray-800/50",
|
||||
selectedTask === task.id && "bg-blue-600/20"
|
||||
"flex items-center px-4 py-2 text-sm border-b border-gray-800/30 cursor-pointer hover:bg-gray-800/50 transition-all duration-200 ease-out",
|
||||
selectedTask === task.id && "bg-blue-600/20",
|
||||
task.level === 1 && "ml-4 bg-gray-900/30" // 子任务缩进和背景区分
|
||||
)}
|
||||
onClick={() => setSelectedTask(selectedTask === task.id ? null : task.id)}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
{/* 状态图标 */}
|
||||
<div className="w-8 flex justify-center">
|
||||
@ -716,18 +1009,27 @@ export function NetworkTimeline({
|
||||
{/* 展开/折叠按钮 */}
|
||||
<div className="w-6 flex justify-center">
|
||||
{task.level === 0 && task.subTasks && task.subTasks.length > 0 && (
|
||||
<button
|
||||
<motion.button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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 }}
|
||||
>
|
||||
<motion.div
|
||||
animate={{
|
||||
rotate: expandedTasks.has(task.id) ? 90 : 0
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
>
|
||||
{expandedTasks.has(task.id) ?
|
||||
<ChevronDown className="w-3 h-3" /> :
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
}
|
||||
</button>
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -747,20 +1049,70 @@ export function NetworkTimeline({
|
||||
{/* 错误信息和重试按钮 */}
|
||||
{task.statusCode >= 400 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
{/* 错误信息悬停提示 */}
|
||||
<div className="relative group">
|
||||
<div className="p-1 hover:bg-gray-700 rounded text-red-400 hover:text-red-300 cursor-help">
|
||||
<Info className="w-4 h-4" />
|
||||
</div>
|
||||
|
||||
{/* 悬停时显示的错误信息卡片 */}
|
||||
<div className="absolute left-0 top-8 z-50 invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all duration-200">
|
||||
<div className="bg-gray-900 border border-red-500/30 rounded-lg shadow-xl p-4 min-w-80 max-w-96">
|
||||
{/* 错误标题 */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<XCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
|
||||
<h4 className="text-red-400 font-semibold text-base">任务执行失败</h4>
|
||||
</div>
|
||||
|
||||
{/* 错误详情 */}
|
||||
{(() => {
|
||||
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
||||
if (originalTask) {
|
||||
if (!originalTask) return null;
|
||||
|
||||
const errorInfo = getTaskErrorInfo(originalTask);
|
||||
alert(`错误信息: ${errorInfo.errorMessage}\n错误代码: ${errorInfo.errorCode || 'UNKNOWN'}`);
|
||||
}
|
||||
}}
|
||||
className="p-1 hover:bg-gray-700 rounded text-red-400 hover:text-red-300"
|
||||
title="查看错误详情"
|
||||
>
|
||||
<Info className="w-3 h-3" />
|
||||
</button>
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* 错误消息 */}
|
||||
<div>
|
||||
<div className="text-gray-400 text-sm mb-1">错误信息:</div>
|
||||
<div className="text-white text-base leading-relaxed bg-gray-800 rounded p-2 border-l-4 border-red-500">
|
||||
{errorInfo.errorMessage || '未知错误'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误代码 */}
|
||||
{errorInfo.errorCode && (
|
||||
<div>
|
||||
<div className="text-gray-400 text-sm mb-1">错误代码:</div>
|
||||
<div className="text-red-300 font-mono text-sm bg-gray-800 rounded px-2 py-1 inline-block">
|
||||
{errorInfo.errorCode}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 任务信息 */}
|
||||
<div className="pt-2 border-t border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">任务详情:</div>
|
||||
<div className="text-gray-300 text-sm space-y-1">
|
||||
<div>任务ID: <span className="font-mono text-xs">{task.id}</span></div>
|
||||
<div>任务类型: <span className="text-white">{task.type}</span></div>
|
||||
<div>失败时间: <span className="text-white">{(() => {
|
||||
const taskTimes = getTaskTimes(task);
|
||||
return taskTimes.endTime ? formatDateTime(taskTimes.endTime) : '未知';
|
||||
})()}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 小三角箭头 */}
|
||||
<div className="absolute -top-2 left-4 w-4 h-4 bg-gray-900 border-l border-t border-red-500/30 transform rotate-45"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 重试按钮 */}
|
||||
{onRetryTask && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@ -770,7 +1122,7 @@ export function NetworkTimeline({
|
||||
className="p-1 hover:bg-gray-700 rounded text-blue-400 hover:text-blue-300"
|
||||
title="重试任务"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -792,20 +1144,42 @@ export function NetworkTimeline({
|
||||
{/* 进度 */}
|
||||
<div className="w-20 text-center">
|
||||
<span className="text-sm font-medium text-gray-200">
|
||||
{task.level === 0 ?
|
||||
getTaskProgressCount(tasks.find((t: any) => t.task_id === task.id) || {}) :
|
||||
{task.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 === 'COMPLETED' ? '100%' : '0%'
|
||||
}
|
||||
task.status === 'FAILED' ? '0%' : '0%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 时间 */}
|
||||
<div className="w-20 text-center">
|
||||
{/* 时间信息 */}
|
||||
<div className="w-32 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={cn("text-sm font-medium", getTimeDisplayColor(task.executionTime))}>
|
||||
{formatTime(task.executionTime)}
|
||||
</span>
|
||||
{/* 显示开始时间 */}
|
||||
{(() => {
|
||||
const taskTimes = getTaskTimes(task);
|
||||
return taskTimes.startTime ? (
|
||||
<span className="text-xs text-green-400" title={`开始时间: ${formatDateTime(taskTimes.startTime)}`}>
|
||||
{new Date(taskTimes.startTime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
) : null;
|
||||
})()}
|
||||
{/* 显示结束时间 */}
|
||||
{(() => {
|
||||
const taskTimes = getTaskTimes(task);
|
||||
return taskTimes.endTime ? (
|
||||
<span className="text-xs text-blue-400" title={`结束时间: ${formatDateTime(taskTimes.endTime)}`}>
|
||||
{new Date(taskTimes.endTime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
) : null;
|
||||
})()}
|
||||
{/* 剩余时间估算 */}
|
||||
{task.level === 0 && (() => {
|
||||
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
||||
@ -818,7 +1192,7 @@ export function NetworkTimeline({
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
@ -889,10 +1263,15 @@ export function NetworkTimeline({
|
||||
width: `${Math.max((task.executionTime / maxTime) * 100, 0.5)}%`
|
||||
}}
|
||||
onClick={() => setSelectedTask(task.id)}
|
||||
title={`${task.displayName}
|
||||
title={(() => {
|
||||
const taskTimes = getTaskTimes(task);
|
||||
return `${task.displayName}
|
||||
执行时间: ${formatTime(task.executionTime)}
|
||||
状态: ${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">
|
||||
@ -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="text-white">{task.type}</span></div>
|
||||
<div><span className="text-gray-400">层级:</span> <span className="text-white">{task.level === 0 ? '主任务' : '子任务'}</span></div>
|
||||
|
||||
{/* 子任务完成状况 */}
|
||||
{task.level === 0 && (() => {
|
||||
const originalTask = tasks.find((t: any) => t.task_id === task.id);
|
||||
const subTaskStatus = originalTask ? getSubTaskStatus(originalTask) : null;
|
||||
return subTaskStatus ? (
|
||||
<div><span className="text-gray-400">子任务进度:</span> <span className="text-cyan-400 font-mono">{subTaskStatus}</span></div>
|
||||
) : null;
|
||||
})()}
|
||||
|
||||
{/* 时间信息显示 */}
|
||||
{(() => {
|
||||
const taskTimes = getTaskTimes(task);
|
||||
return (
|
||||
<>
|
||||
<div className="border-t border-gray-700 pt-2 mt-2">
|
||||
<div className="text-gray-300 font-medium text-xs mb-1">⏰ 时间信息</div>
|
||||
<div><span className="text-gray-400">开始时间:</span> <span className="text-green-400 text-[10px]">{formatDateTime(taskTimes.startTime)}</span></div>
|
||||
<div><span className="text-gray-400">结束时间:</span> <span className="text-blue-400 text-[10px]">{formatDateTime(taskTimes.endTime)}</span></div>
|
||||
{taskTimes.createdAt && taskTimes.createdAt !== taskTimes.startTime && (
|
||||
<div><span className="text-gray-400">创建时间:</span> <span className="text-gray-300 text-[10px]">{formatDateTime(taskTimes.createdAt)}</span></div>
|
||||
)}
|
||||
{taskTimes.updatedAt && taskTimes.updatedAt !== taskTimes.endTime && (
|
||||
<div><span className="text-gray-400">更新时间:</span> <span className="text-gray-300 text-[10px]">{formatDateTime(taskTimes.updatedAt)}</span></div>
|
||||
)}
|
||||
{taskTimes.startTime && taskTimes.endTime && (
|
||||
<div><span className="text-gray-400">执行时长:</span> <span className="text-yellow-400 text-[10px]">{formatTime(task.executionTime)}</span></div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
{task.status === 'IN_PROGRESS' && (
|
||||
<>
|
||||
<div><span className="text-gray-400">进度:</span> <span className="text-white">{task.progress}%</span></div>
|
||||
|
||||
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