在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1,255 @@
/**
* 任务管理页面。
*
* 三个 Tab队列、调度、历史
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
Tabs, Table, Tag, Button, Popconfirm, Space, message, Drawer,
Typography, Descriptions, Empty,
} from 'antd';
import {
ReloadOutlined, DeleteOutlined, StopOutlined,
UnorderedListOutlined, ClockCircleOutlined, HistoryOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { QueuedTask, ExecutionLog } from '../types';
import {
fetchQueue, fetchHistory, deleteFromQueue, cancelExecution,
} from '../api/execution';
import ScheduleTab from '../components/ScheduleTab';
const { Title, Text } = Typography;
/* ------------------------------------------------------------------ */
/* 状态颜色映射 */
/* ------------------------------------------------------------------ */
const STATUS_COLOR: Record<string, string> = {
pending: 'default',
running: 'processing',
success: 'success',
failed: 'error',
cancelled: 'warning',
};
/* ------------------------------------------------------------------ */
/* 工具函数 */
/* ------------------------------------------------------------------ */
function fmtTime(iso: string | null | undefined): string {
if (!iso) return '—';
return new Date(iso).toLocaleString('zh-CN');
}
function fmtDuration(ms: number | null | undefined): string {
if (ms == null) return '—';
if (ms < 1000) return `${ms}ms`;
const sec = ms / 1000;
if (sec < 60) return `${sec.toFixed(1)}s`;
const min = Math.floor(sec / 60);
const remainSec = Math.round(sec % 60);
return `${min}m${remainSec}s`;
}
/* ------------------------------------------------------------------ */
/* 队列 Tab */
/* ------------------------------------------------------------------ */
const QueueTab: React.FC = () => {
const [data, setData] = useState<QueuedTask[]>([]);
const [loading, setLoading] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try { setData(await fetchQueue()); }
catch { message.error('加载队列失败'); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, [load]);
const handleDelete = async (id: string) => {
try { await deleteFromQueue(id); message.success('已删除'); load(); }
catch { message.error('删除失败'); }
};
const handleCancel = async (id: string) => {
try { await cancelExecution(id); message.success('已取消'); load(); }
catch { message.error('取消失败'); }
};
const columns: ColumnsType<QueuedTask> = [
{
title: '任务', dataIndex: ['config', 'tasks'], key: 'tasks',
render: (tasks: string[]) => (
<Text style={{ maxWidth: 300 }} ellipsis={{ tooltip: tasks?.join(', ') }}>
{tasks?.join(', ') ?? '—'}
</Text>
),
},
{
title: 'Flow', dataIndex: ['config', 'pipeline'], key: 'pipeline', width: 120,
render: (v: string) => <Tag>{v}</Tag>,
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 90,
render: (s: string) => <Tag color={STATUS_COLOR[s] ?? 'default'}>{s}</Tag>,
},
{ title: '位置', dataIndex: 'position', key: 'position', width: 60, align: 'center' },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170, render: fmtTime },
{
title: '操作', key: 'action', width: 100, align: 'center',
render: (_: unknown, record: QueuedTask) => {
if (record.status === 'pending') {
return (
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" danger icon={<DeleteOutlined />} size="small"></Button>
</Popconfirm>
);
}
if (record.status === 'running') {
return (
<Popconfirm title="确认取消执行?" onConfirm={() => handleCancel(record.id)}>
<Button type="link" danger icon={<StopOutlined />} size="small"></Button>
</Popconfirm>
);
}
return null;
},
},
];
return (
<>
<div style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text type="secondary"> {data.length} </Text>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}></Button>
</div>
<Table<QueuedTask>
rowKey="id" columns={columns} dataSource={data}
loading={loading} pagination={false} size="small"
locale={{ emptyText: <Empty description="队列为空" /> }}
/>
</>
);
};
/* ------------------------------------------------------------------ */
/* 历史 Tab */
/* ------------------------------------------------------------------ */
const HistoryTab: React.FC = () => {
const [data, setData] = useState<ExecutionLog[]>([]);
const [loading, setLoading] = useState(false);
const [detail, setDetail] = useState<ExecutionLog | null>(null);
const load = useCallback(async () => {
setLoading(true);
try { setData(await fetchHistory()); }
catch { message.error('加载历史记录失败'); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, [load]);
const columns: ColumnsType<ExecutionLog> = [
{
title: '任务', dataIndex: 'task_codes', key: 'task_codes',
render: (codes: string[]) => (
<Text style={{ maxWidth: 300 }} ellipsis={{ tooltip: codes?.join(', ') }}>
{codes?.join(', ') ?? '—'}
</Text>
),
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 90,
render: (s: string) => <Tag color={STATUS_COLOR[s] ?? 'default'}>{s}</Tag>,
},
{ title: '开始时间', dataIndex: 'started_at', key: 'started_at', width: 170, render: fmtTime },
{ title: '时长', dataIndex: 'duration_ms', key: 'duration_ms', width: 90, render: fmtDuration },
{
title: '退出码', dataIndex: 'exit_code', key: 'exit_code', width: 70, align: 'center',
render: (v: number | null) => v != null ? (
<Tag color={v === 0 ? 'success' : 'error'}>{v}</Tag>
) : '—',
},
];
return (
<>
<div style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text type="secondary"> {data.length} </Text>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}></Button>
</div>
<Table<ExecutionLog>
rowKey="id" columns={columns} dataSource={data}
loading={loading} pagination={{ pageSize: 20, showTotal: (t) => `${t}` }}
size="small"
onRow={(record) => ({ onClick: () => setDetail(record), style: { cursor: 'pointer' } })}
/>
<Drawer
title="执行详情" open={!!detail} onClose={() => setDetail(null)}
width={520}
>
{detail && (
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="任务">{detail.task_codes?.join(', ')}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={STATUS_COLOR[detail.status] ?? 'default'}>{detail.status}</Tag>
</Descriptions.Item>
<Descriptions.Item label="开始时间">{fmtTime(detail.started_at)}</Descriptions.Item>
<Descriptions.Item label="结束时间">{fmtTime(detail.finished_at)}</Descriptions.Item>
<Descriptions.Item label="时长">{fmtDuration(detail.duration_ms)}</Descriptions.Item>
<Descriptions.Item label="退出码">
{detail.exit_code != null ? (
<Tag color={detail.exit_code === 0 ? 'success' : 'error'}>{detail.exit_code}</Tag>
) : '—'}
</Descriptions.Item>
<Descriptions.Item label="命令">
<code style={{ wordBreak: 'break-all', fontSize: 12 }}>{detail.command || '—'}</code>
</Descriptions.Item>
</Descriptions>
)}
</Drawer>
</>
);
};
/* ------------------------------------------------------------------ */
/* 主组件 */
/* ------------------------------------------------------------------ */
const TaskManager: React.FC = () => {
const items = [
{
key: 'queue',
label: <Space><UnorderedListOutlined /></Space>,
children: <QueueTab />,
},
{
key: 'schedule',
label: <Space><ClockCircleOutlined /></Space>,
children: <ScheduleTab />,
},
{
key: 'history',
label: <Space><HistoryOutlined /></Space>,
children: <HistoryTab />,
},
];
return (
<div>
<Title level={4} style={{ marginBottom: 16 }}>
<UnorderedListOutlined style={{ marginRight: 8 }} />
</Title>
<Tabs defaultActiveKey="queue" items={items} />
</div>
);
};
export default TaskManager;