Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0717aea36e | |||
| db83222dd7 | |||
| d3373144aa | |||
| 9aab0f5dca | |||
| 542ea59869 |
Regular → Executable
+5
-123
@@ -1,11 +1,8 @@
|
||||
import React from "react"
|
||||
import { Table, Tag, Button, Popconfirm, Tooltip } from "antd"
|
||||
import { RedoOutlined, InfoCircleOutlined, ClockCircleOutlined } from "@ant-design/icons"
|
||||
import type { ColumnsType } from "antd/es/table"
|
||||
import type { TaskItem, TaskStatus } from "@/api/tasks"
|
||||
import { STATUS_CONFIG, TYPE_LABELS } from "../constants"
|
||||
import { formatDuration, formatTime } from "../utils"
|
||||
import { Table } from "antd"
|
||||
import type { TaskItem } from "@/api/tasks"
|
||||
import { TaskErrorDetail } from "./TaskErrorDetail"
|
||||
import { useTaskTableColumns, TaskEmptyState } from "./task-table"
|
||||
|
||||
interface TaskTableProps {
|
||||
dataSource: TaskItem[]
|
||||
@@ -24,7 +21,6 @@ interface TaskTableProps {
|
||||
|
||||
/**
|
||||
* 任务列表表格
|
||||
* 含列定义、分页、展开行
|
||||
*/
|
||||
export const TaskTable: React.FC<TaskTableProps> = ({
|
||||
dataSource,
|
||||
@@ -40,116 +36,7 @@ export const TaskTable: React.FC<TaskTableProps> = ({
|
||||
onRetry,
|
||||
onViewDetail,
|
||||
}) => {
|
||||
// 表格列定义
|
||||
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={() => onRetry(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<RedoOutlined />}
|
||||
loading={retryLoading}
|
||||
className="task-retry-btn"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)
|
||||
}
|
||||
if (record.status === "failed") {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<InfoCircleOutlined />}
|
||||
onClick={() => onViewDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return <span className="task-action-placeholder">-</span>
|
||||
},
|
||||
},
|
||||
]
|
||||
const columns = useTaskTableColumns({ retryLoading, onRetry, onViewDetail })
|
||||
|
||||
return (
|
||||
<Table
|
||||
@@ -178,12 +65,7 @@ export const TaskTable: React.FC<TaskTableProps> = ({
|
||||
scroll={{ x: 800 }}
|
||||
className="task-table"
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div className="task-empty">
|
||||
<ClockCircleOutlined />
|
||||
<p>暂无任务记录</p>
|
||||
</div>
|
||||
),
|
||||
emptyText: <TaskEmptyState />,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react"
|
||||
import { ClockCircleOutlined } from "@ant-design/icons"
|
||||
|
||||
export const TaskEmptyState: React.FC = () => (
|
||||
<div className="task-empty">
|
||||
<ClockCircleOutlined />
|
||||
<p>暂无任务记录</p>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useTaskTableColumns } from "./useTaskTableColumns"
|
||||
export { TaskEmptyState } from "./TaskEmptyState"
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Tag, Button, Popconfirm, Tooltip } from "antd"
|
||||
import { RedoOutlined, InfoCircleOutlined } from "@ant-design/icons"
|
||||
import type { ColumnsType } from "antd/es/table"
|
||||
import type { TaskItem, TaskStatus } from "@/api/tasks"
|
||||
import { STATUS_CONFIG, TYPE_LABELS } from "../../constants"
|
||||
import { formatDuration, formatTime } from "../../utils"
|
||||
|
||||
interface UseTaskTableColumnsOptions {
|
||||
retryLoading: boolean
|
||||
onRetry: (id: string) => void
|
||||
onViewDetail: (record: TaskItem) => void
|
||||
}
|
||||
|
||||
export function useTaskTableColumns({
|
||||
retryLoading,
|
||||
onRetry,
|
||||
onViewDetail,
|
||||
}: UseTaskTableColumnsOptions): ColumnsType<TaskItem> {
|
||||
return [
|
||||
{
|
||||
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={() => onRetry(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<RedoOutlined />}
|
||||
loading={retryLoading}
|
||||
className="task-retry-btn"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)
|
||||
}
|
||||
if (record.status === "failed") {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<InfoCircleOutlined />}
|
||||
onClick={() => onViewDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return <span className="task-action-placeholder">-</span>
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Tasks 模块 smoke test
|
||||
* 建立依赖链,确保 vitest related 能匹配到 tasks 目录下的改动
|
||||
*/
|
||||
import { describe, it, expect } from "vitest"
|
||||
|
||||
import "@/pages/tasks/components/TaskTable"
|
||||
import "@/pages/tasks/components/task-table/useTaskTableColumns"
|
||||
import "@/pages/tasks/components/task-table/TaskEmptyState"
|
||||
import "@/pages/tasks/components/TaskFilterBar"
|
||||
import "@/pages/tasks/components/TaskErrorDetail"
|
||||
|
||||
describe("Tasks module smoke test", () => {
|
||||
it("should load all task modules", () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user