feat: 任务中心页面 — 列表/筛选/重试/错误详情/分页
- 扩展 tasks API 模块:分页查询(getTasks)、重试(retryTask)、错误详情类型 - 新建 TaskCenter 页面:Ant Design Table 展示任务列表 - 顶部 Tabs 状态筛选(全部/等待中/进行中/已完成/失败/已取消) - 下拉类型筛选(全部/生成任务/素材导入) - 失败任务行显示重试按钮(Popconfirm 确认) - 可展开行显示错误详情(错误类型/信息/失败阶段/堆栈) - 分页(showSizeChanger/showQuickJumper/showTotal) - 自动轮询:有 running/waiting 任务时每5秒刷新 - 添加路由 /app/tasks 和导航配置
This commit is contained in:
+63
-12
@@ -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<TaskItem[]> => {
|
||||
const { data } = await apiClient.get("/tasks");
|
||||
return data.items || [];
|
||||
/** 获取任务列表(支持分页和筛选) */
|
||||
export const getTasks = async (
|
||||
params?: TaskListParams,
|
||||
): Promise<TaskListResponse> => {
|
||||
const { data } = await apiClient.get<TaskListResponse>("/tasks", {
|
||||
params,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
/** 获取单个任务详情(用于轮询进度) */
|
||||
/** 获取当前用户的所有任务(兼容旧接口,跨 project) */
|
||||
export const getUserTasks = async (): Promise<TaskItem[]> => {
|
||||
const { data } = await apiClient.get("/tasks");
|
||||
return data.items || data || [];
|
||||
};
|
||||
|
||||
/** 获取单个任务详情(含 error_info) */
|
||||
export const getTask = async (taskId: string): Promise<TaskItem> => {
|
||||
const { data } = await apiClient.get(`/tasks/${taskId}`);
|
||||
return data;
|
||||
|
||||
@@ -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: "查重",
|
||||
|
||||
@@ -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: <ClockCircleOutlined />,
|
||||
},
|
||||
waiting: {
|
||||
label: "排队中",
|
||||
color: "processing",
|
||||
icon: <SyncOutlined spin />,
|
||||
},
|
||||
running: {
|
||||
label: "进行中",
|
||||
color: "processing",
|
||||
icon: <SyncOutlined spin />,
|
||||
},
|
||||
completed: {
|
||||
label: "已完成",
|
||||
color: "success",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
failed: {
|
||||
label: "失败",
|
||||
color: "error",
|
||||
icon: <CloseCircleOutlined />,
|
||||
},
|
||||
cancelled: {
|
||||
label: "已取消",
|
||||
color: "default",
|
||||
icon: <MinusCircleOutlined />,
|
||||
},
|
||||
};
|
||||
|
||||
/** 任务类型标签 */
|
||||
const TYPE_LABELS: Record<string, { label: string; color: string }> = {
|
||||
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<TaskStatus | "all">("all");
|
||||
const [typeFilter, setTypeFilter] = useState<string>("all");
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null);
|
||||
const [expandedTaskDetail, setExpandedTaskDetail] = useState<TaskItem | null>(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<TaskItem> = [
|
||||
{
|
||||
title: "任务ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
render: (id: string) => (
|
||||
<Tooltip title={id}>
|
||||
<span className="task-id">{id.slice(0, 8)}...</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "task_type",
|
||||
key: "task_type",
|
||||
width: 100,
|
||||
render: (type: string) => {
|
||||
const config = TYPE_LABELS[type] || { label: type, color: "default" };
|
||||
return <Tag color={config.color}>{config.label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 120,
|
||||
render: (status: TaskStatus, record: TaskItem) => {
|
||||
const config = STATUS_CONFIG[status] || {
|
||||
label: status,
|
||||
color: "default",
|
||||
icon: null,
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
color={config.color}
|
||||
icon={config.icon}
|
||||
className="task-status-tag"
|
||||
>
|
||||
{config.label}
|
||||
{status === "running" && record.progress > 0 && (
|
||||
<span className="task-progress"> {record.progress}%</span>
|
||||
)}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "当前步骤",
|
||||
dataIndex: "current_step",
|
||||
key: "current_step",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (step: string) => (
|
||||
<span className="task-step">{step || "-"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "耗时",
|
||||
dataIndex: "duration_seconds",
|
||||
key: "duration_seconds",
|
||||
width: 100,
|
||||
render: (seconds: number) => (
|
||||
<span className="task-duration">{formatDuration(seconds)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
width: 120,
|
||||
render: (time: string) => (
|
||||
<span className="task-time">{formatTime(time)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 100,
|
||||
fixed: "right",
|
||||
render: (_: unknown, record: TaskItem) => {
|
||||
if (record.status === "failed" && record.retryable) {
|
||||
return (
|
||||
<Popconfirm
|
||||
title="确认重试"
|
||||
description="确定要重试这个失败的任务吗?"
|
||||
onConfirm={() => retryMutation.mutate(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<RedoOutlined />}
|
||||
loading={retryMutation.isPending}
|
||||
className="task-retry-btn"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
if (record.status === "failed") {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<InfoCircleOutlined />}
|
||||
onClick={() => {
|
||||
setExpandedTaskId(record.id);
|
||||
setExpandedTaskDetail(record);
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return <span className="task-action-placeholder">-</span>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 展开行渲染(错误详情)
|
||||
const expandedRowRender = (record: TaskItem) => {
|
||||
const detail = expandedTaskDetail || record;
|
||||
const errorInfo = detail.error_info;
|
||||
|
||||
if (!errorInfo && !detail.error_message) {
|
||||
return <div className="task-expand-empty">暂无错误详情</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-error-detail">
|
||||
<div className="task-error-header">
|
||||
<ExclamationCircleOutlined className="task-error-icon" />
|
||||
<span>错误详情</span>
|
||||
</div>
|
||||
<div className="task-error-body">
|
||||
{errorInfo?.error_type && (
|
||||
<div className="task-error-row">
|
||||
<span className="task-error-label">错误类型:</span>
|
||||
<Tag color="error">{errorInfo.error_type}</Tag>
|
||||
</div>
|
||||
)}
|
||||
{(errorInfo?.error_message || detail.error_message) && (
|
||||
<div className="task-error-row">
|
||||
<span className="task-error-label">错误信息:</span>
|
||||
<span className="task-error-message">
|
||||
{errorInfo?.error_message || detail.error_message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{errorInfo?.failed_step && (
|
||||
<div className="task-error-row">
|
||||
<span className="task-error-label">失败阶段:</span>
|
||||
<span>{errorInfo.failed_step}</span>
|
||||
</div>
|
||||
)}
|
||||
{errorInfo?.stack_trace && (
|
||||
<div className="task-error-row task-error-stack">
|
||||
<span className="task-error-label">堆栈信息:</span>
|
||||
<pre>{errorInfo.stack_trace}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 错误处理
|
||||
if (error) {
|
||||
return (
|
||||
<div className="task-center">
|
||||
<div className="task-error">
|
||||
<CloseCircleOutlined />
|
||||
<p>加载任务列表失败</p>
|
||||
<Button onClick={() => window.location.reload()}>刷新页面</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-center">
|
||||
{/* 页面标题 */}
|
||||
<div className="task-header">
|
||||
<h1 className="task-title">任务中心</h1>
|
||||
<p className="task-subtitle">查看和管理所有生成任务与素材导入任务</p>
|
||||
</div>
|
||||
|
||||
{/* 筛选栏 */}
|
||||
<div className="task-filters">
|
||||
{/* 状态 Tab */}
|
||||
<Tabs
|
||||
activeKey={statusFilter}
|
||||
onChange={(key) => {
|
||||
setStatusFilter(key as TaskStatus | "all");
|
||||
setPage(1);
|
||||
}}
|
||||
items={STATUS_TABS.map((tab) => ({
|
||||
key: tab.key,
|
||||
label: tab.label,
|
||||
}))}
|
||||
className="task-status-tabs"
|
||||
/>
|
||||
|
||||
{/* 类型筛选 */}
|
||||
<div className="task-type-filter">
|
||||
<Select
|
||||
value={typeFilter}
|
||||
onChange={(value) => {
|
||||
setTypeFilter(value);
|
||||
setPage(1);
|
||||
}}
|
||||
options={TYPE_OPTIONS}
|
||||
style={{ width: 140 }}
|
||||
placeholder="选择类型"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 任务表格 */}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data?.items || []}
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total: data?.total || 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (p, ps) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
}}
|
||||
expandable={{
|
||||
expandedRowRender,
|
||||
expandedRowKeys: expandedTaskId ? [expandedTaskId] : [],
|
||||
onExpand: handleExpand,
|
||||
rowExpandable: (record) =>
|
||||
record.status === "failed" &&
|
||||
(!!record.error_info || !!record.error_message),
|
||||
}}
|
||||
scroll={{ x: 800 }}
|
||||
className="task-table"
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div className="task-empty">
|
||||
<ClockCircleOutlined />
|
||||
<p>暂无任务记录</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/* ──────────── 任务中心 ──────────── */
|
||||
|
||||
.task-center {
|
||||
padding: var(--space-lg);
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
.task-header {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--space-xs) 0;
|
||||
}
|
||||
|
||||
.task-subtitle {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.task-filters {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-lg);
|
||||
padding: var(--space-md);
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.task-status-tabs {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-status-tabs .ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.task-status-tabs .ant-tabs-tab {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.task-type-filter {
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--space-md);
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.task-table {
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.task-table .ant-table {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.task-table .ant-table-thead > tr > th {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.task-table .ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.task-table .ant-table-tbody > tr:hover > td {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* 任务 ID */
|
||||
.task-id {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.task-status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.task-progress {
|
||||
font-size: var(--font-size-xs);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 当前步骤 */
|
||||
.task-step {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 耗时 */
|
||||
.task-duration {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 时间 */
|
||||
.task-time {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.task-retry-btn {
|
||||
color: var(--primary-500);
|
||||
}
|
||||
|
||||
.task-retry-btn:hover {
|
||||
color: var(--primary-600);
|
||||
}
|
||||
|
||||
.task-action-placeholder {
|
||||
color: var(--text-disabled, var(--text-secondary));
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* 展开行 - 错误详情 */
|
||||
.task-error-detail {
|
||||
padding: var(--space-md);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.task-error-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--error-500, #ef4444);
|
||||
}
|
||||
|
||||
.task-error-icon {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.task-error-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.task-error-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.task-error-label {
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.task-error-message {
|
||||
color: var(--error-500, #ef4444);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.task-error-stack {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-error-stack pre {
|
||||
margin: var(--space-xs) 0 0 0;
|
||||
padding: var(--space-sm);
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
overflow-x: auto;
|
||||
max-height: 200px;
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
.task-expand-empty {
|
||||
padding: var(--space-md);
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.task-empty {
|
||||
padding: var(--space-2xl) 0;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.task-empty .anticon {
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--space-md);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.task-empty p {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.task-error {
|
||||
padding: var(--space-2xl);
|
||||
text-align: center;
|
||||
color: var(--error-500, #ef4444);
|
||||
}
|
||||
|
||||
.task-error .anticon {
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.task-error p {
|
||||
margin: 0 0 var(--space-md) 0;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.task-center {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.task-filters {
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.task-type-filter {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-type-filter .ant-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.task-status-tabs .ant-tabs-nav {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.task-status-tabs .ant-tabs-tab {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,13 @@ export const router = createBrowserRouter([
|
||||
Component: m.default,
|
||||
})),
|
||||
},
|
||||
{
|
||||
path: "tasks",
|
||||
lazy: () =>
|
||||
import("@/pages/tasks/TaskCenter").then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
},
|
||||
{
|
||||
path: "editing-planner",
|
||||
lazy: () =>
|
||||
|
||||
Reference in New Issue
Block a user