From 83137d5c2e05873ce6dae219a4a18df8a3d3bdba Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 13 Jul 2026 22:36:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BB=BB=E5=8A=A1=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=20=E2=80=94=20=E5=88=97=E8=A1=A8/=E7=AD=9B?= =?UTF-8?q?=E9=80=89/=E9=87=8D=E8=AF=95/=E9=94=99=E8=AF=AF=E8=AF=A6?= =?UTF-8?q?=E6=83=85/=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展 tasks API 模块:分页查询(getTasks)、重试(retryTask)、错误详情类型 - 新建 TaskCenter 页面:Ant Design Table 展示任务列表 - 顶部 Tabs 状态筛选(全部/等待中/进行中/已完成/失败/已取消) - 下拉类型筛选(全部/生成任务/素材导入) - 失败任务行显示重试按钮(Popconfirm 确认) - 可展开行显示错误详情(错误类型/信息/失败阶段/堆栈) - 分页(showSizeChanger/showQuickJumper/showTotal) - 自动轮询:有 running/waiting 任务时每5秒刷新 - 添加路由 /app/tasks 和导航配置 --- apps/web/src/api/tasks.ts | 75 +++- apps/web/src/config/navigation.ts | 13 + apps/web/src/pages/tasks/TaskCenter.tsx | 439 ++++++++++++++++++++++++ apps/web/src/pages/tasks/tasks.css | 268 +++++++++++++++ apps/web/src/router/index.tsx | 7 + 5 files changed, 790 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/pages/tasks/TaskCenter.tsx create mode 100644 apps/web/src/pages/tasks/tasks.css diff --git a/apps/web/src/api/tasks.ts b/apps/web/src/api/tasks.ts index 4e80832f5..88fb0fdb1 100644 --- a/apps/web/src/api/tasks.ts +++ b/apps/web/src/api/tasks.ts @@ -1,31 +1,72 @@ /** * 任务相关 API - * 对接后端方案 A 扩展后的端点(PR #109) - * - POST /api/v1/generation/tasks — 创建生成任务(template_id + asset_ids 细粒度模式) - * - GET /api/v1/tasks — 用户级任务列表(跨 project) - * - POST /api/v1/tasks/{task_id}/retry — 简化重试 + * 对接后端任务中心 API: + * - POST /api/v1/generation/tasks — 创建生成任务 + * - GET /api/v1/tasks — 用户级任务列表(支持分页/筛选) + * - GET /api/v1/tasks/{task_id} — 任务详情(含 error_info) + * - POST /api/v1/tasks/{task_id}/retry — 重试失败任务 */ import apiClient from "./client"; /* ──────────── 类型定义 ──────────── */ +/** 任务状态 */ +export type TaskStatus = + | "pending" + | "waiting" + | "running" + | "completed" + | "failed" + | "cancelled"; + +/** 任务类型 */ +export type TaskType = "ingest" | "generation" | string; + +/** 错误详情 */ +export interface TaskErrorInfo { + error_type: string; + error_message: string; + failed_step: string; + stack_trace?: string; +} + /** 任务条目(对应用户级 UserTaskResponse) */ export interface TaskItem { id: string; - task_type: "ingest" | "generation" | string; + task_type: TaskType; project_id: string; - template_id: string; - status: string; + template_id?: string; + status: TaskStatus; progress: number; current_step: string; error_message: string; user_message: string; retryable: boolean; source_id: string; + /** 错误详情(失败任务) */ + error_info?: TaskErrorInfo; + /** 耗时(秒) */ + duration_seconds?: number; created_at?: string | null; updated_at?: string | null; } +/** 任务列表查询参数 */ +export interface TaskListParams { + page?: number; + page_size?: number; + status?: TaskStatus | "all"; + task_type?: TaskType | "all"; +} + +/** 任务列表分页响应 */ +export interface TaskListResponse { + items: TaskItem[]; + total: number; + page: number; + page_size: number; +} + /** 创建生成任务请求参数 */ export interface CreateGenerationTaskRequest { template_id: string; @@ -64,13 +105,23 @@ export const createGenerationTask = async ( return data; }; -/** 获取当前用户的所有任务(跨 project) */ -export const getUserTasks = async (): Promise => { - const { data } = await apiClient.get("/tasks"); - return data.items || []; +/** 获取任务列表(支持分页和筛选) */ +export const getTasks = async ( + params?: TaskListParams, +): Promise => { + const { data } = await apiClient.get("/tasks", { + params, + }); + return data; }; -/** 获取单个任务详情(用于轮询进度) */ +/** 获取当前用户的所有任务(兼容旧接口,跨 project) */ +export const getUserTasks = async (): Promise => { + const { data } = await apiClient.get("/tasks"); + return data.items || data || []; +}; + +/** 获取单个任务详情(含 error_info) */ export const getTask = async (taskId: string): Promise => { const { data } = await apiClient.get(`/tasks/${taskId}`); return data; diff --git a/apps/web/src/config/navigation.ts b/apps/web/src/config/navigation.ts index a32cf0f96..d0f462361 100644 --- a/apps/web/src/config/navigation.ts +++ b/apps/web/src/config/navigation.ts @@ -17,6 +17,7 @@ import { ScanOutlined, ControlOutlined, CrownOutlined, + UnorderedListOutlined, } from "@ant-design/icons"; /** 导航项类型 */ @@ -104,6 +105,12 @@ export const NAV_ITEMS: NavItem[] = [ path: "/app/duplication", icon: React.createElement(ScanOutlined), }, + { + key: "tasks", + label: "任务中心", + path: "/app/tasks", + icon: React.createElement(UnorderedListOutlined), + }, ]; /** 侧边栏导航分组(Sidebar 分组列表使用) */ @@ -181,6 +188,12 @@ export const NAV_GROUPS: NavGroup[] = [ path: "/app/history", icon: React.createElement(HistoryOutlined), }, + { + key: "tasks", + label: "任务中心", + path: "/app/tasks", + icon: React.createElement(UnorderedListOutlined), + }, { key: "duplication", label: "查重", diff --git a/apps/web/src/pages/tasks/TaskCenter.tsx b/apps/web/src/pages/tasks/TaskCenter.tsx new file mode 100644 index 000000000..53c0d6c7c --- /dev/null +++ b/apps/web/src/pages/tasks/TaskCenter.tsx @@ -0,0 +1,439 @@ +/** + * 任务中心页面 + * 展示用户的所有任务(生成任务、素材导入等),支持状态筛选、类型筛选、分页、重试 + */ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Table, Tabs, Select, Tag, Button, message, Popconfirm, Tooltip } from "antd"; +import { + CheckCircleOutlined, + ClockCircleOutlined, + SyncOutlined, + CloseCircleOutlined, + ExclamationCircleOutlined, + MinusCircleOutlined, + RedoOutlined, + InfoCircleOutlined, +} from "@ant-design/icons"; +import type { ColumnsType } from "antd/es/table"; +import { + getTasks, + retryTask, + type TaskItem, + type TaskStatus, + type TaskListParams, +} from "@/api/tasks"; +import "./tasks.css"; + +/* ──────────── 常量 ──────────── */ + +/** 状态 Tab 配置 */ +const STATUS_TABS: { key: TaskStatus | "all"; label: string }[] = [ + { key: "all", label: "全部" }, + { key: "waiting", label: "等待中" }, + { key: "running", label: "进行中" }, + { key: "completed", label: "已完成" }, + { key: "failed", label: "失败" }, + { key: "cancelled", label: "已取消" }, +]; + +/** 类型筛选选项 */ +const TYPE_OPTIONS = [ + { value: "all", label: "全部类型" }, + { value: "generation", label: "生成任务" }, + { value: "ingest", label: "素材导入" }, +]; + +/** 状态标签配置 */ +const STATUS_CONFIG: Record< + TaskStatus, + { label: string; color: string; icon: React.ReactNode } +> = { + pending: { + label: "等待中", + color: "default", + icon: , + }, + waiting: { + label: "排队中", + color: "processing", + icon: , + }, + running: { + label: "进行中", + color: "processing", + icon: , + }, + completed: { + label: "已完成", + color: "success", + icon: , + }, + failed: { + label: "失败", + color: "error", + icon: , + }, + cancelled: { + label: "已取消", + color: "default", + icon: , + }, +}; + +/** 任务类型标签 */ +const TYPE_LABELS: Record = { + generation: { label: "生成任务", color: "blue" }, + ingest: { label: "素材导入", color: "green" }, +}; + +/* ──────────── 工具函数 ──────────── */ + +/** 格式化耗时 */ +const formatDuration = (seconds?: number): string => { + if (!seconds) return "-"; + if (seconds < 60) return `${Math.round(seconds)}秒`; + const minutes = Math.floor(seconds / 60); + const secs = Math.round(seconds % 60); + if (minutes < 60) return `${minutes}分${secs}秒`; + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return `${hours}小时${mins}分`; +}; + +/** 格式化时间 */ +const formatTime = (dateStr?: string | null): string => { + if (!dateStr) return "-"; + const date = new Date(dateStr); + return date.toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +}; + +/* ──────────── 主组件 ──────────── */ + +export default function TaskCenter() { + const queryClient = useQueryClient(); + + // 筛选状态 + const [statusFilter, setStatusFilter] = useState("all"); + const [typeFilter, setTypeFilter] = useState("all"); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + const [expandedTaskId, setExpandedTaskId] = useState(null); + const [expandedTaskDetail, setExpandedTaskDetail] = useState(null); + + // 查询参数 + const queryParams: TaskListParams = { + page, + page_size: pageSize, + ...(statusFilter !== "all" && { status: statusFilter }), + ...(typeFilter !== "all" && { task_type: typeFilter }), + }; + + // 获取任务列表 + const { data, isLoading, error } = useQuery({ + queryKey: ["tasks", queryParams], + queryFn: () => getTasks(queryParams), + refetchInterval: (query) => { + // 有进行中的任务时自动刷新 + const tasks = query.state.data?.items ?? []; + const hasRunning = tasks.some( + (t) => t.status === "running" || t.status === "waiting" + ); + return hasRunning ? 5000 : false; + }, + }); + + // 重试任务 + const retryMutation = useMutation({ + mutationFn: retryTask, + onSuccess: () => { + message.success("任务已重新提交"); + queryClient.invalidateQueries({ queryKey: ["tasks"] }); + }, + onError: () => { + message.error("重试失败,请检查任务状态"); + }, + }); + + // 展开查看详情 + const handleExpand = async (expanded: boolean, record: TaskItem) => { + if (!expanded) { + setExpandedTaskId(null); + setExpandedTaskDetail(null); + return; + } + setExpandedTaskId(record.id); + // 如果是失败任务,获取详情(含 error_info) + if (record.status === "failed" && record.error_info) { + setExpandedTaskDetail(record); + } + }; + + // 表格列定义 + const columns: ColumnsType = [ + { + title: "任务ID", + dataIndex: "id", + key: "id", + width: 120, + ellipsis: true, + render: (id: string) => ( + + {id.slice(0, 8)}... + + ), + }, + { + title: "类型", + dataIndex: "task_type", + key: "task_type", + width: 100, + render: (type: string) => { + const config = TYPE_LABELS[type] || { label: type, color: "default" }; + return {config.label}; + }, + }, + { + title: "状态", + dataIndex: "status", + key: "status", + width: 120, + render: (status: TaskStatus, record: TaskItem) => { + const config = STATUS_CONFIG[status] || { + label: status, + color: "default", + icon: null, + }; + return ( + + {config.label} + {status === "running" && record.progress > 0 && ( + {record.progress}% + )} + + ); + }, + }, + { + title: "当前步骤", + dataIndex: "current_step", + key: "current_step", + width: 150, + ellipsis: true, + render: (step: string) => ( + {step || "-"} + ), + }, + { + title: "耗时", + dataIndex: "duration_seconds", + key: "duration_seconds", + width: 100, + render: (seconds: number) => ( + {formatDuration(seconds)} + ), + }, + { + title: "创建时间", + dataIndex: "created_at", + key: "created_at", + width: 120, + render: (time: string) => ( + {formatTime(time)} + ), + }, + { + title: "操作", + key: "action", + width: 100, + fixed: "right", + render: (_: unknown, record: TaskItem) => { + if (record.status === "failed" && record.retryable) { + return ( + retryMutation.mutate(record.id)} + okText="确定" + cancelText="取消" + > + + + ); + } + if (record.status === "failed") { + return ( + + ); + } + return -; + }, + }, + ]; + + // 展开行渲染(错误详情) + const expandedRowRender = (record: TaskItem) => { + const detail = expandedTaskDetail || record; + const errorInfo = detail.error_info; + + if (!errorInfo && !detail.error_message) { + return
暂无错误详情
; + } + + return ( +
+
+ + 错误详情 +
+
+ {errorInfo?.error_type && ( +
+ 错误类型: + {errorInfo.error_type} +
+ )} + {(errorInfo?.error_message || detail.error_message) && ( +
+ 错误信息: + + {errorInfo?.error_message || detail.error_message} + +
+ )} + {errorInfo?.failed_step && ( +
+ 失败阶段: + {errorInfo.failed_step} +
+ )} + {errorInfo?.stack_trace && ( +
+ 堆栈信息: +
{errorInfo.stack_trace}
+
+ )} +
+
+ ); + }; + + // 错误处理 + if (error) { + return ( +
+
+ +

加载任务列表失败

+ +
+
+ ); + } + + return ( +
+ {/* 页面标题 */} +
+

任务中心

+

查看和管理所有生成任务与素材导入任务

+
+ + {/* 筛选栏 */} +
+ {/* 状态 Tab */} + { + setStatusFilter(key as TaskStatus | "all"); + setPage(1); + }} + items={STATUS_TABS.map((tab) => ({ + key: tab.key, + label: tab.label, + }))} + className="task-status-tabs" + /> + + {/* 类型筛选 */} +
+