Files
creator-hub/web/src/pages/tasks/index.tsx
T

181 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 任务中心列表:语义对齐 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>
);
}