181 lines
6.4 KiB
TypeScript
181 lines
6.4 KiB
TypeScript
// 任务中心列表:语义对齐 web.archived TasksPage.jsx 的 TaskList。
|
||
import { useCallback, useEffect, useState } from 'react';
|
||
import { history } from '@umijs/max';
|
||
import { Alert, Button, Card, Flex, Select, Space, Table, Tag, Typography } from 'antd';
|
||
import type { ColumnsType } from 'antd/es/table';
|
||
import { getList } from '@/services/api';
|
||
import { dateTime } from '@/utils/helpers';
|
||
|
||
const stateText: Record<string, string> = {
|
||
queued: '已排队',
|
||
executing: '执行中',
|
||
succeeded: '已成功',
|
||
failed: '失败',
|
||
needs_confirmation: '需要人工确认',
|
||
policy_hold: '策略暂停',
|
||
cancelled: '已取消',
|
||
};
|
||
const stateColor: Record<string, string> = {
|
||
queued: 'default',
|
||
executing: 'processing',
|
||
succeeded: 'success',
|
||
failed: 'error',
|
||
needs_confirmation: 'warning',
|
||
policy_hold: 'warning',
|
||
cancelled: 'default',
|
||
};
|
||
|
||
const holdText: Record<string, string> = {
|
||
account_paused: '账号已暂停',
|
||
account_revoked: '账号授权已撤销',
|
||
binding_missing: '运行环境绑定缺失',
|
||
environment_missing: '原运行环境不可用',
|
||
exit_missing: '固定出口缺失',
|
||
exit_unhealthy: '固定出口失败或发生漂移',
|
||
runtime_stop_pending: '停止结果未知,等待人工核验',
|
||
runtime_missing: '原运行实例不可用',
|
||
runtime_lease_expired: '运行实例租约已过期',
|
||
execution_lease_expired: '执行租约过期,结果未知',
|
||
task_result_uncertain: '执行结果不确定',
|
||
task_policy_hold: '执行器请求策略暂停',
|
||
account_version_changed: '账号版本已变化',
|
||
draft_version_changed: '草稿版本已变化',
|
||
confirmation_missing: '确认快照缺失',
|
||
confirmation_version_changed: '确认版本已变化',
|
||
binding_version_changed: '环境或出口版本已变化',
|
||
legacy_confirmation_required: '历史任务结果未知,等待人工核验',
|
||
};
|
||
export const holdLabel = (reason?: string) => (reason ? holdText[reason] || `未知停止原因:${reason}` : '无');
|
||
|
||
export default function Page() {
|
||
const [accountFilter, setAccountFilter] = useState<string | undefined>(undefined);
|
||
const [stateFilter, setStateFilter] = useState<string | undefined>(undefined);
|
||
const [rows, setRows] = useState<any[]>([]);
|
||
const [accountOptions, setAccountOptions] = useState<{ value: string; label: string }[]>([]);
|
||
const [accountsPending, setAccountsPending] = useState(false);
|
||
const [pending, setPending] = useState(true);
|
||
const [error, setError] = useState<any>(null);
|
||
|
||
const load = useCallback(async () => {
|
||
setPending(true);
|
||
setError(null);
|
||
try {
|
||
const result = await getList({
|
||
resource: 'tasks',
|
||
filters: { account_id: accountFilter, state: stateFilter || undefined },
|
||
});
|
||
setRows(result.data ?? []);
|
||
} catch (loadError) {
|
||
setError(loadError);
|
||
} finally {
|
||
setPending(false);
|
||
}
|
||
}, [accountFilter, stateFilter]);
|
||
|
||
useEffect(() => {
|
||
load();
|
||
}, [load]);
|
||
|
||
// 账号筛选下拉:一次性拉取账号列表,无需手输 UUID。
|
||
useEffect(() => {
|
||
setAccountsPending(true);
|
||
getList({ resource: 'accounts' })
|
||
.then((result) =>
|
||
setAccountOptions(
|
||
(result.data ?? []).map((account: any) => ({
|
||
value: account.id,
|
||
label: account.name ? `${account.name}(${account.id})` : account.id,
|
||
})),
|
||
),
|
||
)
|
||
.catch(() => setAccountOptions([]))
|
||
.finally(() => setAccountsPending(false));
|
||
}, []);
|
||
|
||
const columns: ColumnsType<any> = [
|
||
{
|
||
title: '任务',
|
||
dataIndex: 'id',
|
||
render: (value: string) => (
|
||
<Space size={4}>
|
||
<Typography.Link onClick={() => history.push(`/tasks/${value}`)}>{value}</Typography.Link>
|
||
<Typography.Text copyable={{ text: value }} style={{ fontSize: 12 }} />
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '账号',
|
||
dataIndex: 'account_id',
|
||
render: (value: string) => (
|
||
<Space size={4}>
|
||
<Typography.Link onClick={() => history.push(`/accounts/${value}`)}>{value}</Typography.Link>
|
||
<Typography.Text copyable={{ text: value }} style={{ fontSize: 12 }} />
|
||
</Space>
|
||
),
|
||
},
|
||
{ title: '确认', dataIndex: 'confirmation_id', render: (value?: string) => value || '未确认' },
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'state',
|
||
render: (value: string) => <Tag color={stateColor[value] || 'default'}>{stateText[value] || `未知状态:${value}`}</Tag>,
|
||
},
|
||
{ title: '停止原因', dataIndex: 'hold_reason', render: (value?: string) => <Typography.Text type="secondary">{holdLabel(value)}</Typography.Text> },
|
||
{
|
||
title: '更新时间',
|
||
dataIndex: 'updated_at',
|
||
render: (_, task) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{dateTime(task.updated_at || task.created_at)}</Typography.Text>,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
align: 'right',
|
||
render: (_, task) => (
|
||
<Button size="small" onClick={() => history.push(`/tasks/${task.id}`)}>
|
||
查看
|
||
</Button>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<Card variant="outlined" style={{ marginBottom: 16 }}>
|
||
<Flex justify="space-between" align="center">
|
||
<Typography.Text type="secondary">共 {rows.length} 个任务</Typography.Text>
|
||
<Space wrap>
|
||
<Select
|
||
style={{ width: 280 }}
|
||
placeholder="全部账号"
|
||
allowClear
|
||
showSearch
|
||
optionFilterProp="label"
|
||
loading={accountsPending && accountOptions.length === 0}
|
||
value={accountFilter}
|
||
onChange={setAccountFilter}
|
||
options={accountOptions}
|
||
/>
|
||
<Select
|
||
style={{ width: 200 }}
|
||
placeholder="全部状态"
|
||
allowClear
|
||
value={stateFilter}
|
||
onChange={setStateFilter}
|
||
options={Object.entries(stateText).map(([value, label]) => ({ value, label }))}
|
||
/>
|
||
</Space>
|
||
</Flex>
|
||
</Card>
|
||
<Card variant="outlined">
|
||
{error ? <Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={load}>重试</Button>} /> : null}
|
||
<Table
|
||
rowKey="id"
|
||
columns={columns}
|
||
dataSource={rows}
|
||
loading={pending}
|
||
pagination={false}
|
||
locale={{ emptyText: accountFilter || stateFilter ? '没有符合筛选条件的任务' : '尚无任务' }} />
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|