feat(web): task-4 tasks 模块迁移(任务中心列表/详情/attempts/drafts,Tabs 信息组织,动作与筛选语义对齐归档版)

This commit is contained in:
2026-09-22 12:45:06 +08:00
parent 04ac91874a
commit 3e67556428
4 changed files with 691 additions and 8 deletions
+69 -2
View File
@@ -1,4 +1,71 @@
// TODO: 从 web.archived 迁移 attempts/$id
// Attempt 详情:语义对齐 web.archived TracePages.jsx 的 AttemptDetail。
import { useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import { Alert, Button, Card, Descriptions, Space, Typography } from 'antd';
import { getOne } from '@/services/api';
import { dateTime } from '@/utils/helpers';
export default function Page() {
return null;
const { id = '' } = useParams<{ id: string }>();
const [attempt, setAttempt] = useState<any>(null);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
useEffect(() => {
let cancelled = false;
setPending(true);
setError(null);
getOne('attempts', id)
.then((result) => {
if (!cancelled) setAttempt(result);
})
.catch((loadError) => {
if (!cancelled) setError(loadError);
})
.finally(() => {
if (!cancelled) setPending(false);
});
return () => {
cancelled = true;
};
}, [id]);
if (pending) return <Card loading />;
if (error) return <Alert type="error" showIcon message={error.message} />;
if (!attempt) return null;
return (
<div>
<div style={{ marginBottom: 16 }}>
<Typography.Title level={4} style={{ marginBottom: 0 }}>
Attempt 详情
</Typography.Title>
<Typography.Text type="secondary" copyable>
{attempt.id}
</Typography.Text>
</div>
<Card>
<Space direction="vertical" style={{ width: '100%' }} size={16}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="结果">{attempt.outcome || '执行中'}</Descriptions.Item>
<Descriptions.Item label="开始">{dateTime(attempt.started_at)}</Descriptions.Item>
<Descriptions.Item label="环境 / 出口 / 绑定版本">
{`${attempt.browser_env_alias || '未记录'} · ${attempt.network_exit_id || '未记录'} · ${attempt.binding_version || '未记录'}`}
</Descriptions.Item>
</Descriptions>
<Space wrap>
<Button type="primary" onClick={() => history.push(`/tasks/${encodeURIComponent(attempt.task_id)}`)}>
返回任务
</Button>
{attempt.browser_env_alias ? (
<Button onClick={() => history.push(`/browsers/${encodeURIComponent(attempt.browser_env_alias)}`)}>查看环境</Button>
) : null}
{attempt.network_exit_id ? (
<Button onClick={() => history.push(`/network-exits/${encodeURIComponent(attempt.network_exit_id)}`)}>查看出口</Button>
) : null}
</Space>
</Space>
</Card>
</div>
);
}
+225 -2
View File
@@ -1,4 +1,227 @@
// TODO: 从 web.archived 迁移 drafts/$id
// 草稿核对:语义对齐 web.archived DraftPage.jsx(只读快照 / 版本链 / 显式核对与入队 / 关联任务)。
import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import {
Alert,
Button,
Card,
Checkbox,
Descriptions,
Modal,
Space,
Tag,
Typography,
message,
} from 'antd';
import { confirmDraft, enqueueConfirmation, getOne, getList } from '@/services/api';
import { conflictMessage } from '@/utils/helpers';
// 与账号 readiness 同构的草稿侧判定:只依赖绑定健康与调度状态。
function draftReadiness(draft: any, binding: any, bindingsError = false) {
if (draft.account.authorization_status !== 'authorized') return { label: '授权已撤销', ready: false };
if (bindingsError) return { label: '环境状态未知', ready: false };
if (!binding) return { label: '未绑定运行环境', ready: false };
if (binding.network_exit_id && binding.network_exit_health !== 'healthy') return { label: '固定出口不健康', ready: false };
const schedule = binding.schedule_block_reason || '';
return { label: schedule ? `不可调度:${schedule}` : '可调度', ready: !schedule };
}
export default function Page() {
return null;
const { id = '' } = useParams<{ id: string }>();
const [draft, setDraft] = useState<any>(null);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [binding, setBinding] = useState<any>(null);
const [browsersError, setBrowsersError] = useState<any>(null);
const [reviewed, setReviewed] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const load = useCallback(async () => {
setPending(true);
setError(null);
try {
const result = await getOne('drafts', id);
setDraft(result);
try {
const browsersResult = await getList({ resource: 'browsers' });
setBrowsersError(null);
setBinding((browsersResult.data ?? []).find((item: any) => item.account_id === result.account_id) ?? null);
} catch (loadBindingError) {
setBrowsersError(loadBindingError);
setBinding(undefined);
}
} catch (loadError) {
setError(loadError);
} finally {
setPending(false);
}
}, [id]);
useEffect(() => {
load();
}, [load]);
if (pending) return <Card loading />;
if (error || !draft) return <Alert type="error" showIcon message={error?.message || '草稿不存在'} />;
const versions = draft.versions || [];
const confirmations = draft.confirmations || [];
const tasks = draft.tasks || [];
const latestVersion = versions[0]?.version || draft.version;
const isLatest = draft.version === latestVersion;
const confirmation = confirmations.find(
(item: any) => item.account_version === draft.account.version && item.draft_version === draft.version,
);
const readiness = draftReadiness(draft, binding, Boolean(browsersError));
const canConfirm = isLatest && Boolean(binding) && !browsersError;
const canEnqueue = Boolean(confirmation) && isLatest && readiness.ready;
async function confirmSnapshot() {
setBusy(true);
try {
await confirmDraft(draft.id, draft.account.version, draft.version);
await load();
setConfirmOpen(false);
messageApi.success('确认快照已保存,可回溯账号、草稿与当前固定资源版本。');
} catch (reason) {
setConfirmOpen(false);
messageApi.error(conflictMessage(reason));
await load();
} finally {
setBusy(false);
}
}
async function enqueue() {
if (!confirmation) return;
setBusy(true);
try {
const task = await enqueueConfirmation(confirmation.id);
await load();
messageApi.success(`任务已排队:${task.id}。重复提交会返回同一任务。`);
} catch (reason) {
messageApi.error(conflictMessage(reason));
await load();
} finally {
setBusy(false);
}
}
return (
<div>
{contextHolder}
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ minWidth: 0 }}>
<Typography.Title level={4} style={{ marginBottom: 0 }}>草稿核对</Typography.Title>
<Typography.Text type="secondary" ellipsis>
{draft.account.platform_account_key} · 草稿版本 {draft.version}
</Typography.Text>
</div>
<Button onClick={() => history.push(`/accounts/${draft.account_id}`)}>返回账号详情</Button>
</div>
{browsersError ? (
<Alert type="error" showIcon style={{ marginBottom: 16 }} message="环境不可用:当前运行环境与网络出口状态未知,不能确认或排队。" />
) : null}
{!isLatest ? (
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message={`当前只读快照已不是最新草稿版本,请刷新到版本 ${latestVersion} 后重新核对。`}
action={
<Button size="small" onClick={() => history.push(`/drafts/${versions[0]?.id}`)}>
打开最新版本
</Button>
}
/>
) : null}
<Space direction="vertical" style={{ width: '100%' }} size={16}>
<Card title="只读内容快照">
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>{draft.content}</Typography.Paragraph>
</Card>
<Card title="版本与当前资源">
<Space direction="vertical" style={{ width: '100%' }} size={16}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="账号版本">{draft.account.version}</Descriptions.Item>
<Descriptions.Item label="草稿版本">{draft.version}</Descriptions.Item>
<Descriptions.Item label="运行环境">{browsersError ? '状态未知' : binding?.name || '未绑定'}</Descriptions.Item>
<Descriptions.Item label="网络出口">{browsersError ? '状态未知' : binding?.network_exit_id || '当前机器直连'}</Descriptions.Item>
</Descriptions>
<Tag color={readiness.ready ? 'success' : 'warning'}>{readiness.label}</Tag>
</Space>
</Card>
<Card title="版本链">
<Space direction="vertical" size={4}>
{versions.map((item: any) =>
item.id === draft.id ? (
<Typography.Text key={item.id} strong>
版本 {item.version}(当前快照)
</Typography.Text>
) : (
<Button key={item.id} type="link" size="small" style={{ padding: 0 }} onClick={() => history.push(`/drafts/${item.id}`)}>
查看版本 {item.version}
</Button>
),
)}
</Space>
</Card>
<Card title="显式核对与入队">
<Space direction="vertical" style={{ width: '100%' }} size={16}>
<Checkbox checked={reviewed} onChange={(e) => setReviewed(e.target.checked)}>
我已核对当前账号、草稿内容、运行环境和网络出口
</Checkbox>
<Space wrap>
<Button disabled={!reviewed || !canConfirm || busy} onClick={() => setConfirmOpen(true)}>
确认当前快照
</Button>
<Button type="primary" disabled={!canEnqueue || busy} loading={busy} onClick={enqueue}>
加入队列
</Button>
</Space>
{confirmation ? (
<Card size="small" title={`确认快照 v${confirmation.version}`}>
<Typography.Text>
账号版本 {confirmation.account_version} · 草稿版本 {confirmation.draft_version}
</Typography.Text>
</Card>
) : (
<Typography.Text type="secondary">保存有效确认后才可加入队列。</Typography.Text>
)}
</Space>
</Card>
<Card title="关联任务">
{tasks.length === 0 ? (
<Typography.Text type="secondary">尚未入队。</Typography.Text>
) : (
<Space direction="vertical" style={{ width: '100%' }} size={8}>
{tasks.map((task: any) => (
<Card key={task.id} size="small">
<Typography.Text strong>{task.id}</Typography.Text>
<Typography.Paragraph style={{ marginBottom: 0 }}>任务状态:{task.state}</Typography.Paragraph>
</Card>
))}
</Space>
)}
</Card>
</Space>
<Modal
open={confirmOpen}
title="确认草稿版本"
onCancel={() => {
if (!busy) setConfirmOpen(false);
}}
onOk={confirmSnapshot}
confirmLoading={busy}
okText={busy ? '确认中…' : '保存确认'}
cancelText="返回核对"
>
<p>
将保存账号版本 {draft.account.version}、草稿版本 {draft.version}、运行环境 {binding?.alias} 与网络出口{' '}
{binding?.network_exit_id || '当前机器直连'} 的只读确认快照。版本变化后必须重新确认。
</p>
</Modal>
</div>
);
}
+256 -2
View File
@@ -1,4 +1,258 @@
// TODO: 从 web.archived 迁移 tasks/$id
// 任务详情:语义对齐 web.archived TasksPage.jsx 的 TaskDetail。
// 四卡改用 Tabs(当前状态 / 确认快照 / 执行关联 / 唯一安全动作)——antd 默认组件的信息组织方式。
import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import { Alert, Button, Card, Descriptions, Select, Space, Tabs, Tag, Typography, message } from 'antd';
import { getOne, taskAction } 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: '历史任务结果未知,等待人工核验',
};
const holdLabel = (reason?: string) => (reason ? holdText[reason] || `未知停止原因:${reason}` : '无');
function actionErrorText(error: any): string {
const reason = error?.body?.reason_code;
if (error?.status === 409) return `当前版本仍不允许恢复(409):${holdLabel(reason)}`;
if (error?.status === 503) return `原资源尚未恢复(503):${holdLabel(reason)}`;
return error?.message || '操作失败';
}
export default function Page() {
return null;
const { id = '' } = useParams<{ id: string }>();
const [task, setTask] = useState<any>(null);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [verification, setVerification] = useState('not_executed');
const [busy, setBusy] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const load = useCallback(async () => {
setPending(true);
setError(null);
try {
const result = await getOne('tasks', id);
setTask(result);
} catch (loadError) {
setError(loadError);
} finally {
setPending(false);
}
}, [id]);
useEffect(() => {
load();
}, [load]);
async function runAction(action: 'verify' | 'resume' | 'finish', data?: unknown) {
setBusy(true);
try {
await taskAction(task.id, action, data);
messageApi.success(
action === 'verify'
? '人工核验已记录;现在只能按该结论恢复或结束。'
: action === 'resume'
? '任务已恢复排队。'
: '任务已按核验结论结束。',
);
await load();
} catch (reason) {
messageApi.error(actionErrorText(reason));
} finally {
setBusy(false);
}
}
if (pending) return <Card loading />;
if (error || !task) return <Alert type="error" showIcon message={error?.message || '任务不存在'} />;
const confirmation = task.confirmation || {};
const unknownHold = task.hold_reason && !holdText[task.hold_reason];
const items = [
{
key: 'status',
label: '当前状态',
children: (
<Space direction="vertical" style={{ width: '100%' }} size={16}>
<Tag color={stateColor[task.state] || 'default'}>{stateText[task.state] || `未知状态:${task.state}`}</Tag>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="停止原因">{holdLabel(task.hold_reason)}</Descriptions.Item>
<Descriptions.Item label="人工核验">{task.verification_result || '尚未记录'}</Descriptions.Item>
</Descriptions>
</Space>
),
},
{
key: 'confirmation',
label: '确认快照',
children: (
<Space direction="vertical" style={{ width: '100%' }} size={16}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="确认">
<Typography.Text copyable>{confirmation.id || '未记录'}</Typography.Text> · v{confirmation.version || '—'}
</Descriptions.Item>
<Descriptions.Item label="版本">{`账号版本 ${confirmation.account_version || '—'} · 草稿版本 ${confirmation.draft_version || '—'}`}</Descriptions.Item>
<Descriptions.Item label="环境与出口">
{`${confirmation.browser_env_alias || '未记录'} · ${confirmation.network_exit_id || '未记录'} · 绑定版本:${confirmation.binding_version || '未记录'}`}
</Descriptions.Item>
</Descriptions>
<Space wrap>
<Button size="small" onClick={() => history.push(`/accounts/${task.account_id}`)}>
查看账号
</Button>
<Button size="small" onClick={() => history.push(`/drafts/${task.draft_id}`)}>
查看草稿
</Button>
</Space>
</Space>
),
},
{
key: 'execution',
label: '执行关联',
children: (
<Space direction="vertical" style={{ width: '100%' }} size={16}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="环境">{task.browser_env_alias || '未记录'}</Descriptions.Item>
<Descriptions.Item label="固定出口">{task.network_exit_id || '未记录'}</Descriptions.Item>
<Descriptions.Item label="运行实例 / 绑定版本">
<Typography.Text copyable>{task.runtime_instance_id || '未记录'}</Typography.Text> · {task.binding_version || '未记录'}
</Descriptions.Item>
</Descriptions>
<Space wrap>
{task.browser_env_alias ? (
<Button size="small" onClick={() => history.push(`/browsers/${encodeURIComponent(task.browser_env_alias)}`)}>
查看原环境
</Button>
) : null}
{task.network_exit_id ? (
<Button size="small" onClick={() => history.push(`/network-exits/${encodeURIComponent(task.network_exit_id)}`)}>
查看原出口
</Button>
) : null}
</Space>
</Space>
),
},
{
key: 'action',
label: '唯一安全动作',
children: (
<Space direction="vertical" style={{ width: '100%' }} size={16}>
{task.readiness_reason ? <Alert type="info" showIcon message={`当前仍被阻断:${holdLabel(task.readiness_reason)}。修复原资源并刷新后,才会开放恢复。`} /> : null}
{task.allowed_action === 'verify' ? (
<>
<Alert type="info" showIcon message='先在原环境和原出口核验真实结果。选择"未执行"不会立即重试,仍需下一步显式恢复。' />
<Select
style={{ width: 280 }}
value={verification}
onChange={setVerification}
options={[
{ value: 'not_executed', label: '确认未执行,可评估恢复' },
{ value: 'succeeded', label: '确认已成功' },
{ value: 'failed', label: '确认已失败' },
]}
/>
<div>
<Button type="primary" disabled={busy} onClick={() => runAction('verify', { result: verification })}>
记录人工核验
</Button>
</div>
</>
) : null}
{task.allowed_action === 'resume' ? (
<Button type="primary" disabled={busy} onClick={() => runAction('resume')}>
按核验结论恢复排队
</Button>
) : null}
{task.allowed_action === 'finish' ? (
<Button type="primary" disabled={busy} onClick={() => runAction('finish')}>
按核验结论结束任务
</Button>
) : null}
{task.allowed_action === 'reconfirm' ? (
<>
<Alert type="info" showIcon message="版本已变化,旧确认不可继续使用。" />
<Button type="primary" onClick={() => history.push(`/drafts/${task.draft_id}`)}>
重新核对并确认草稿
</Button>
</>
) : null}
{task.allowed_action ? null : <Typography.Text type="secondary">当前没有可安全执行的动作。</Typography.Text>}
</Space>
),
},
];
return (
<div>
{contextHolder}
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ minWidth: 0 }}>
<Typography.Title level={4} style={{ marginBottom: 0 }}>任务详情</Typography.Title>
<Typography.Text type="secondary" copyable>
{task.id}
</Typography.Text>
</div>
<Button onClick={() => history.push('/tasks')}>返回任务中心</Button>
</div>
{unknownHold ? <Alert type="warning" showIcon style={{ marginBottom: 16 }} message={`后端返回了未知停止原因"${task.hold_reason}";为避免不安全操作,本页不提供恢复动作。`} /> : null}
<Tabs defaultActiveKey="status" items={items} />
<Typography.Title level={5} style={{ marginTop: 24, marginBottom: 12 }}>执行尝试</Typography.Title>
{task.attempts?.length ? (
<Space direction="vertical" style={{ width: '100%' }} size={12}>
{task.attempts.map((attempt: any) => (
<Card key={attempt.id} size="small">
<Typography.Link onClick={() => history.push(`/attempts/${encodeURIComponent(attempt.id)}`)}>{attempt.id}</Typography.Link>
<Typography.Paragraph style={{ marginTop: 6, marginBottom: 0 }}>
{attempt.outcome || '执行中'} · {dateTime(attempt.started_at)}
</Typography.Paragraph>
<Typography.Text type="secondary">脱敏证据:{attempt.evidence?.mock_outcome || '无可公开证据'}</Typography.Text>
</Card>
))}
</Space>
) : (
<Card size="small">
<Typography.Text type="secondary">尚未产生执行尝试。</Typography.Text>
</Card>
)}
</div>
);
}
+141 -2
View File
@@ -1,4 +1,143 @@
// TODO: 从 web.archived 迁移 tasks/index
// 任务中心列表:语义对齐 web.archived TasksPage.jsx 的 TaskList。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import { Alert, Button, Input, 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() {
return null;
const [accountFilter, setAccountFilter] = useState('');
const [stateFilter, setStateFilter] = useState<string | undefined>(undefined);
const [rows, setRows] = useState<any[]>([]);
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 || undefined, state: stateFilter || undefined },
});
setRows(result.data ?? []);
} catch (loadError) {
setError(loadError);
} finally {
setPending(false);
}
}, [accountFilter, stateFilter]);
useEffect(() => {
load();
}, [load]);
const columns: ColumnsType<any> = [
{
title: '任务',
dataIndex: 'id',
render: (value: string) => <Typography.Link onClick={() => history.push(`/tasks/${value}`)}>{value}</Typography.Link>,
},
{
title: '账号',
dataIndex: 'account_id',
render: (value: string) => <Typography.Link onClick={() => history.push(`/accounts/${value}`)}>{value}</Typography.Link>,
},
{ 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>
<Typography.Paragraph type="secondary">逐条处理暂停、版本变化与未知执行结果;不会自动重试或更换出口</Typography.Paragraph>
<Space wrap style={{ marginBottom: 16 }}>
<Input
style={{ width: 280 }}
placeholder="账号 ID"
value={accountFilter}
onChange={(event) => setAccountFilter(event.target.value)}
allowClear
/>
<Select
style={{ width: 200 }}
placeholder="全部状态"
allowClear
value={stateFilter}
onChange={setStateFilter}
options={Object.entries(stateText).map(([value, label]) => ({ value, label }))}
/>
</Space>
{error ? <Alert type="error" showIcon style={{ marginBottom: 16 }} message={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 ? '没有符合筛选条件的任务' : '尚无任务' }}
/>
</div>
);
}