refactor(web): 删除前端空转链路(任务中心/草稿/attempt/运营工作台/素材面板)
- 删页面目录 tasks/ drafts/ attempts/ creator/workbench/ - 竞品页移除素材处理 Drawer 与素材操作 - metadata 菜单与页头规则同步收敛 - api.ts/listQuery.ts 移除对应 resource 与函数 - 固定表头测试文件清单同步
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import { Line } from '@ant-design/charts';
|
||||
import { Alert, App, Button, Card, Descriptions, Flex, Form, Input, Modal, Space, Tag, Typography } from 'antd';
|
||||
import { accountAction, createDraft, getOne, getList, creatorGet, creatorAction } from '@/services/api';
|
||||
import { Alert, App, Button, Card, Descriptions, Flex, Modal, Space, Tag, Typography } from 'antd';
|
||||
import { accountAction, getOne, getList, creatorGet, creatorAction } from '@/services/api';
|
||||
import { accountReadiness, conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
|
||||
import { usePageActions, usePageInfo } from '@/components/PageActions';
|
||||
import WorksGallery from '@/components/creator/WorksGallery';
|
||||
@@ -65,13 +65,10 @@ export default function Page() {
|
||||
const [account, setAccount] = useState<any>(null);
|
||||
const [binding, setBinding] = useState<any>(null);
|
||||
const [browsersError, setBrowsersError] = useState<any>(null);
|
||||
const [drafts, setDrafts] = useState<any[]>([]);
|
||||
const [pending, setPending] = useState(true);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [pauseOpen, setPauseOpen] = useState(false);
|
||||
const [actionBusy, setActionBusy] = useState(false);
|
||||
const [draftForm] = Form.useForm<{ content: string }>();
|
||||
const [draftBusy, setDraftBusy] = useState(false);
|
||||
const [collectionStatus, setCollectionStatus] = useState<any>(null);
|
||||
const [accountMetrics, setAccountMetrics] = useState<any[]>([]);
|
||||
const { message: messageApi } = App.useApp();
|
||||
@@ -124,12 +121,6 @@ export default function Page() {
|
||||
setBrowsersError(browsersResult.reason);
|
||||
setBinding(undefined);
|
||||
}
|
||||
try {
|
||||
const draftResult = await getList({ resource: 'drafts', filters: { account_id: id } });
|
||||
setDrafts(draftResult.data ?? []);
|
||||
} catch {
|
||||
setDrafts([]);
|
||||
}
|
||||
// 监控数据:采集失败不阻塞页面主体,采集状态为空时按待采集展示。
|
||||
try {
|
||||
setCollectionStatus(await creatorGet(`/creator/accounts/${encodeURIComponent(id)}/collection-status`));
|
||||
@@ -184,7 +175,7 @@ export default function Page() {
|
||||
try {
|
||||
await accountAction(account.id, action);
|
||||
messageApi.success(
|
||||
action === 'pause' ? '账号已暂停;既有 hold 任务需后续逐条核验。' : '账号已恢复;既有 hold 任务没有自动恢复。',
|
||||
action === 'pause' ? '账号已暂停,运行环境已停止。' : '账号已恢复。',
|
||||
);
|
||||
await loadAll();
|
||||
} catch (reason) {
|
||||
@@ -194,22 +185,6 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateDraft() {
|
||||
if (!readiness?.ready) return;
|
||||
const { content } = await draftForm.validateFields();
|
||||
setDraftBusy(true);
|
||||
try {
|
||||
const draft = await createDraft(account.id, content);
|
||||
await loadAll();
|
||||
draftForm.resetFields();
|
||||
messageApi.success(`草稿版本 ${draft.version} 已创建,请进入只读快照核对。`);
|
||||
} catch (reason) {
|
||||
messageApi.error(conflictMessage(reason));
|
||||
} finally {
|
||||
setDraftBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (pending) return <Card loading />;
|
||||
if (error || !account) return <Alert type="error" showIcon title={error?.message || '账号不存在'} />;
|
||||
|
||||
@@ -256,7 +231,7 @@ export default function Page() {
|
||||
) : null}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Alert type="info" showIcon title="暂停会把待领取任务置为 hold;恢复账号只恢复账号可用性,不会自动恢复既有 hold。" />
|
||||
<Alert type="info" showIcon title="暂停账号会同时停止其运行环境;恢复账号后可在运行环境页重新启动。" />
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card title="账号数据">
|
||||
@@ -306,35 +281,11 @@ export default function Page() {
|
||||
) : (
|
||||
<Flex vertical gap={12}>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
尚未绑定运行环境。账号暂停且资源就绪后,可在运行环境页创建绑定。
|
||||
尚未绑定运行环境。账号创建时若绑定失败,可联系管理员排查后重试。
|
||||
</Typography.Paragraph>
|
||||
<Typography.Link onClick={() => history.push('/browsers')}>前往运行环境</Typography.Link>
|
||||
</Flex>
|
||||
)}
|
||||
</Card>
|
||||
<Card title="文本草稿">
|
||||
<Flex vertical gap={16}>
|
||||
<Typography.Text type="secondary">仅支持单用户 Phase A Mock 文本;内部 ID 与版本由系统生成。</Typography.Text>
|
||||
<Form form={draftForm} layout="vertical">
|
||||
<Form.Item name="content" rules={[{ required: true, message: '请输入草稿内容' }]}>
|
||||
<Input.TextArea rows={4} placeholder="输入要发布的文本草稿" />
|
||||
</Form.Item>
|
||||
<Button type="primary" disabled={!readiness?.ready} loading={draftBusy} onClick={onCreateDraft}>
|
||||
创建草稿
|
||||
</Button>
|
||||
</Form>
|
||||
{drafts.length ? (
|
||||
<Flex vertical gap={8}>
|
||||
{drafts.map((draft: any) => (
|
||||
<Flex key={draft.id} justify="space-between" align="center" wrap="wrap" gap={8}>
|
||||
<Typography.Text ellipsis style={{ maxWidth: '70%' }}>{draft.content}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>v{draft.version}</Typography.Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
) : null}
|
||||
</Flex>
|
||||
</Card>
|
||||
<Modal
|
||||
open={pauseOpen}
|
||||
title="暂停账号"
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// Attempt 详情:语义对齐 web.archived TracePages.jsx 的 AttemptDetail。
|
||||
import { useEffect, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import { Flex, Alert, Button, Card, Descriptions, Space, Typography } from 'antd';
|
||||
import { getOne } from '@/services/api';
|
||||
import { dateTime } from '@/utils/helpers';
|
||||
|
||||
export default function Page() {
|
||||
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 title={error.message} />;
|
||||
if (!attempt) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary" copyable>
|
||||
{attempt.id}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Card>
|
||||
<Flex vertical gap={12}>
|
||||
<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>
|
||||
</Flex>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// 竞品分析:作品列表对齐 ant-design-pro card-list——antd Row/Col 响应式栅格 + Card 原样组合;
|
||||
// 作品详情与素材处理均走 Drawer;日期筛选用 antd DatePicker;筛选自动刷新、手动翻页不变。
|
||||
// 作品详情走 Drawer;日期筛选用 antd DatePicker;筛选自动刷新、手动翻页不变。
|
||||
// 封面读本地缓存(/works/:id/cover,认证 fetch + blob);发布时间状态详情见 publishedAtStatusMeta 注释。
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CommentOutlined, LikeOutlined, ShareAltOutlined } from '@ant-design/icons';
|
||||
import { Alert, App, Button, Card, Col, DatePicker, Drawer, Empty, Flex, Input, InputNumber, Modal, Row, Select, Space, Spin, Table, Tag, Tooltip, Typography } from 'antd';
|
||||
import { Alert, App, Button, Card, Col, DatePicker, Drawer, Empty, Flex, InputNumber, Row, Select, Space, Spin, Table, Tag, Tooltip, Typography } from 'antd';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import { creatorAction, creatorGet, creatorUpdate, fetchCreatorBinary, getList } from '@/services/api';
|
||||
import { creatorGet, fetchCreatorBinary, getList } from '@/services/api';
|
||||
import { conflictMessage, dateTime } from '@/utils/helpers';
|
||||
|
||||
const publishedAtStatusOptions = [
|
||||
@@ -65,10 +65,6 @@ function WorkCover({ workID, title }: { workID: string; title?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function stepTag(status: string, label: string) {
|
||||
return <Tag color={status === 'succeeded' ? 'success' : status === 'failed' ? 'error' : undefined}>{label}</Tag>;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const [works, setWorks] = useState<any[]>([]);
|
||||
const [workPage, setWorkPage] = useState(1);
|
||||
@@ -79,12 +75,6 @@ export default function Page() {
|
||||
const [workDetail, setWorkDetail] = useState<any>(null);
|
||||
const [workMetrics, setWorkMetrics] = useState<any[]>([]);
|
||||
const [workDetailPending, setWorkDetailPending] = useState(false);
|
||||
const [material, setMaterial] = useState<any>(null);
|
||||
const [materialPending, setMaterialPending] = useState(false);
|
||||
const [rewriteRequirement, setRewriteRequirement] = useState('');
|
||||
const [rewriteTitle, setRewriteTitle] = useState('');
|
||||
const [rewriteScript, setRewriteScript] = useState('');
|
||||
const [pendingSwitchWork, setPendingSwitchWork] = useState<string | null>(null);
|
||||
const { message: messageApi } = App.useApp();
|
||||
|
||||
async function load() {
|
||||
@@ -119,12 +109,6 @@ export default function Page() {
|
||||
setWorkPage(1);
|
||||
}, [filters]);
|
||||
|
||||
const materialDirty =
|
||||
material &&
|
||||
(rewriteRequirement !== (material.rewrite_requirement || '') ||
|
||||
rewriteTitle !== (material.generated_title || '') ||
|
||||
rewriteScript !== (material.generated_script || ''));
|
||||
|
||||
async function openWorkDetail(workID: string) {
|
||||
setWorkDetailPending(true);
|
||||
try {
|
||||
@@ -141,104 +125,6 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
|
||||
function selectMaterial(workID: string) {
|
||||
if (materialDirty) {
|
||||
setPendingSwitchWork(workID);
|
||||
return;
|
||||
}
|
||||
doSelectMaterial(workID);
|
||||
}
|
||||
|
||||
async function doSelectMaterial(workID: string) {
|
||||
setPendingSwitchWork(null);
|
||||
setMaterialPending(true);
|
||||
try {
|
||||
const result = await creatorAction(`/creator/works/${encodeURIComponent(workID)}/material/select`);
|
||||
setMaterial(result);
|
||||
setRewriteRequirement(result.rewrite_requirement || '');
|
||||
setRewriteTitle(result.generated_title || '');
|
||||
setRewriteScript(result.generated_script || '');
|
||||
messageApi.info('素材已选择;点击开始处理后才会执行真实下载、音轨提取和转写。');
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '素材选择失败'));
|
||||
} finally {
|
||||
setMaterialPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function processMaterial() {
|
||||
if (!material) return;
|
||||
setMaterialPending(true);
|
||||
try {
|
||||
const result = await creatorAction(`/creator/works/${encodeURIComponent(material.work_id)}/material/process`);
|
||||
setMaterial(result);
|
||||
const failedStep =
|
||||
result.failed_step || ['download', 'audio', 'transcription'].find((step) => result[`${step}_status`] === 'failed');
|
||||
if (failedStep) {
|
||||
messageApi.warning(`素材处理未完成(${failedStep}):${result.failure_reason || '请查看步骤状态后重试。'}`);
|
||||
} else {
|
||||
messageApi.success('素材处理完成。');
|
||||
}
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '素材处理失败'));
|
||||
} finally {
|
||||
setMaterialPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRewrite() {
|
||||
if (!material) return;
|
||||
setMaterialPending(true);
|
||||
try {
|
||||
const result = await creatorAction(`/creator/works/${encodeURIComponent(material.work_id)}/material/rewrite/confirm`, {
|
||||
requirement: rewriteRequirement });
|
||||
setMaterial(result);
|
||||
messageApi.info('已确认仿写要求,请在可编辑字段中复核并保存结果。');
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '仿写确认失败'));
|
||||
} finally {
|
||||
setMaterialPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateRewrite() {
|
||||
if (!material) return;
|
||||
setMaterialPending(true);
|
||||
try {
|
||||
const result = await creatorAction(`/creator/works/${encodeURIComponent(material.work_id)}/material/rewrite/generate`);
|
||||
setMaterial(result);
|
||||
setRewriteTitle(result.generated_title || '');
|
||||
setRewriteScript(result.generated_script || '');
|
||||
messageApi.info('仿写草稿已生成,请人工复核后保存。');
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '仿写草稿生成失败'));
|
||||
} finally {
|
||||
setMaterialPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRewrite() {
|
||||
if (!material) return;
|
||||
setMaterialPending(true);
|
||||
try {
|
||||
const result = await creatorUpdate(`/creator/works/${encodeURIComponent(material.work_id)}/material/rewrite`, {
|
||||
title: rewriteTitle,
|
||||
script: rewriteScript });
|
||||
setMaterial(result);
|
||||
messageApi.info('仿写标题和口播已保存,可继续人工复核。');
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '仿写结果保存失败'));
|
||||
} finally {
|
||||
setMaterialPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
const materialReadyForConfirm =
|
||||
material &&
|
||||
material.download_status === 'succeeded' &&
|
||||
!['not_started', 'running', 'failed'].includes(material.audio_status) &&
|
||||
!['not_started', 'running', 'failed'].includes(material.transcription_status);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex vertical gap={16}>
|
||||
@@ -288,14 +174,6 @@ export default function Page() {
|
||||
<Button key="detail" type="link" size="small" onClick={() => openWorkDetail(work.id)} disabled={workDetailPending}>
|
||||
详情
|
||||
</Button>,
|
||||
<Button
|
||||
key="select"
|
||||
type={material?.work_id === work.id ? 'primary' : 'default'}
|
||||
size="small"
|
||||
onClick={() => selectMaterial(work.id)}
|
||||
>
|
||||
{material?.work_id === work.id ? '编辑素材' : '选择素材'}
|
||||
</Button>,
|
||||
...(work.original_url ? [<Typography.Link key="origin" href={work.original_url} target="_blank">原文</Typography.Link>] : []),
|
||||
]}
|
||||
>
|
||||
@@ -342,73 +220,7 @@ export default function Page() {
|
||||
</Flex>
|
||||
) : null}
|
||||
</Card>
|
||||
<Drawer
|
||||
open={Boolean(material)}
|
||||
onClose={() => setMaterial(null)}
|
||||
size="large"
|
||||
title="素材处理"
|
||||
destroyOnHidden
|
||||
>
|
||||
{material ? (
|
||||
<Flex vertical gap={12}>
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>每一步都保留真实状态;失败不会被显示为成功。</Typography.Paragraph>
|
||||
<Flex vertical gap={12}>
|
||||
<div>
|
||||
<Button type="primary" onClick={processMaterial} loading={materialPending || material.download_status === 'running'}>
|
||||
开始处理素材
|
||||
</Button>
|
||||
</div>
|
||||
<Space wrap>
|
||||
{stepTag(material.download_status, `下载:${material.download_status}`)}
|
||||
{stepTag(material.audio_status, `音轨:${material.audio_status}`)}
|
||||
{stepTag(material.transcription_status, `转写:${material.transcription_status}`)}
|
||||
</Space>
|
||||
{material.failure_reason ? (
|
||||
<Alert type="error" showIcon title={`失败步骤:${material.failed_step || '未记录'};${material.failure_reason}`} />
|
||||
) : null}
|
||||
<div>
|
||||
<Typography.Text strong>仿写要求</Typography.Text>
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 4 }}>必须先确认要求,才允许保存可编辑结果。</Typography.Paragraph>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
value={rewriteRequirement}
|
||||
onChange={(event) => setRewriteRequirement(event.target.value)}
|
||||
disabled={Boolean(material.rewrite_confirmed_at) || materialPending}
|
||||
/>
|
||||
</div>
|
||||
{material.rewrite_confirmed_at ? (
|
||||
<>
|
||||
<Flex gap={8} align="center">
|
||||
<Button onClick={generateRewrite} disabled={materialPending}>
|
||||
生成仿写草稿
|
||||
</Button>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>生成结果不会自动发布,保存前必须人工复核。</Typography.Text>
|
||||
</Flex>
|
||||
<div>
|
||||
<Typography.Text>可编辑标题</Typography.Text>
|
||||
<Input value={rewriteTitle} onChange={(event) => setRewriteTitle(event.target.value)} style={{ marginTop: 4 }} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text>可编辑口播</Typography.Text>
|
||||
<Input.TextArea rows={5} value={rewriteScript} onChange={(event) => setRewriteScript(event.target.value)} style={{ marginTop: 4 }} />
|
||||
</div>
|
||||
<div>
|
||||
<Button type="primary" onClick={saveRewrite} disabled={materialPending}>
|
||||
保存仿写结果
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<Button type="primary" onClick={confirmRewrite} disabled={materialPending || !materialReadyForConfirm}>
|
||||
确认仿写要求
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : null}
|
||||
</Drawer>
|
||||
|
||||
</Flex>
|
||||
<Drawer open={Boolean(workDetail)} onClose={() => setWorkDetail(null)} size="large" title="作品详情">
|
||||
{workDetail ? (
|
||||
@@ -441,16 +253,7 @@ export default function Page() {
|
||||
</Flex>
|
||||
) : null}
|
||||
</Drawer>
|
||||
<Modal
|
||||
open={Boolean(pendingSwitchWork)}
|
||||
title="放弃未保存的素材文稿"
|
||||
okText="放弃并切换"
|
||||
cancelText="继续编辑"
|
||||
onOk={() => pendingSwitchWork && doSelectMaterial(pendingSwitchWork)}
|
||||
onCancel={() => setPendingSwitchWork(null)}
|
||||
>
|
||||
当前素材文稿尚未保存,确定放弃并切换吗?
|
||||
</Modal>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,212 +0,0 @@
|
||||
// 草稿核对:语义对齐 web.archived DraftPage.jsx(只读快照 / 版本链 / 显式核对与入队 / 关联任务)。
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import { Alert, App, Button, Card, Checkbox, Descriptions, Flex, Modal, Space, Tag, Typography } 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() {
|
||||
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 { message: messageApi } = App.useApp();
|
||||
|
||||
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 title={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>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography.Text type="secondary" ellipsis>
|
||||
{draft.account.platform_account_key} · 草稿版本 {draft.version}
|
||||
</Typography.Text>
|
||||
<Button onClick={() => history.push(`/accounts/${draft.account_id}`)}>返回账号详情</Button>
|
||||
</div>
|
||||
{browsersError ? (
|
||||
<Alert type="error" showIcon style={{ marginBottom: 16 }} title="环境不可用:当前运行环境与网络出口状态未知,不能确认或排队。" />
|
||||
) : null}
|
||||
{!isLatest ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
title={`当前只读快照已不是最新草稿版本,请刷新到版本 ${latestVersion} 后重新核对。`}
|
||||
action={
|
||||
<Button size="small" onClick={() => history.push(`/drafts/${versions[0]?.id}`)}>
|
||||
打开最新版本
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Flex vertical gap={16}>
|
||||
<Card title="只读内容快照">
|
||||
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>{draft.content}</Typography.Paragraph>
|
||||
</Card>
|
||||
<Card title="版本与当前资源">
|
||||
<Flex vertical gap={8}>
|
||||
<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>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card title="版本链">
|
||||
<Flex vertical gap={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>
|
||||
),
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card title="显式核对与入队">
|
||||
<Flex vertical gap={12}>
|
||||
<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>
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card title="关联任务">
|
||||
{tasks.length === 0 ? (
|
||||
<Typography.Text type="secondary">尚未入队。</Typography.Text>
|
||||
) : (
|
||||
<Flex vertical gap={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>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Card>
|
||||
</Flex>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
// 任务详情:语义对齐 web.archived TasksPage.jsx 的 TaskDetail。
|
||||
// 四卡改用 Tabs(当前状态 / 确认快照 / 执行关联 / 唯一安全动作)——antd 默认组件的信息组织方式。
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import { Alert, App, Button, Card, Descriptions, Flex, Select, Space, Tabs, Tag, Typography } 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() {
|
||||
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 { message: messageApi } = App.useApp();
|
||||
|
||||
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 title={error?.message || '任务不存在'} />;
|
||||
|
||||
const confirmation = task.confirmation || {};
|
||||
const unknownHold = task.hold_reason && !holdText[task.hold_reason];
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'status',
|
||||
label: '当前状态',
|
||||
children: (
|
||||
<Flex vertical gap={12}>
|
||||
<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>
|
||||
</Flex>
|
||||
) },
|
||||
{
|
||||
key: 'confirmation',
|
||||
label: '确认快照',
|
||||
children: (
|
||||
<Flex vertical gap={12}>
|
||||
<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>
|
||||
</Flex>
|
||||
) },
|
||||
{
|
||||
key: 'execution',
|
||||
label: '执行关联',
|
||||
children: (
|
||||
<Flex vertical gap={12}>
|
||||
<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>
|
||||
</Flex>
|
||||
) },
|
||||
{
|
||||
key: 'action',
|
||||
label: '唯一安全动作',
|
||||
children: (
|
||||
<Flex vertical gap={12}>
|
||||
{task.readiness_reason ? <Alert type="info" showIcon title={`当前仍被阻断:${holdLabel(task.readiness_reason)}。修复原资源并刷新后,才会开放恢复。`} /> : null}
|
||||
{task.allowed_action === 'verify' ? (
|
||||
<>
|
||||
<Alert type="info" showIcon title='先在原环境和原出口核验真实结果。选择"未执行"不会立即重试,仍需下一步显式恢复。' />
|
||||
<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 title="版本已变化,旧确认不可继续使用。" />
|
||||
<Button type="primary" onClick={() => history.push(`/drafts/${task.draft_id}`)}>
|
||||
重新核对并确认草稿
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{task.allowed_action ? null : <Typography.Text type="secondary">当前没有可安全执行的动作。</Typography.Text>}
|
||||
</Flex>
|
||||
) },
|
||||
];
|
||||
|
||||
return (
|
||||
<Card variant="outlined">
|
||||
<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 }} title={`后端返回了未知停止原因"${task.hold_reason}";为避免不安全操作,本页不提供恢复动作。`} /> : null}
|
||||
<Tabs defaultActiveKey="status" items={items} />
|
||||
<Typography.Title level={5} style={{ marginTop: 24, marginBottom: 12 }}>执行尝试</Typography.Title>
|
||||
{task.attempts?.length ? (
|
||||
<Flex vertical gap={8}>
|
||||
{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>
|
||||
))}
|
||||
</Flex>
|
||||
) : (
|
||||
<Card size="small">
|
||||
<Typography.Text type="secondary">尚未产生执行尝试。</Typography.Text>
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
// 任务中心列表:语义对齐 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';
|
||||
import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
|
||||
|
||||
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 { vertical, horizontal } = useOverflowGrid(rows.length);
|
||||
|
||||
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',
|
||||
...fixedLeft<any>({}),
|
||||
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',
|
||||
...fixedRight<any>({}),
|
||||
render: (_, task) => (
|
||||
<Button type="text" 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}
|
||||
sticky
|
||||
pagination={tablePagination()}
|
||||
scroll={tableScroll(horizontal ? 960 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)}
|
||||
locale={{ emptyText: accountFilter || stateFilter ? '没有符合筛选条件的任务' : '尚无任务' }} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
-33
@@ -6,10 +6,6 @@ export const resourcePaths = {
|
||||
browsers: '/browsers',
|
||||
gateways: '/gateways',
|
||||
accounts: '/phase-a/accounts',
|
||||
drafts: '/phase-a/drafts',
|
||||
confirmations: '/phase-a/confirmations',
|
||||
tasks: '/phase-a/tasks',
|
||||
attempts: '/phase-a/attempts',
|
||||
'network-exits': '/network-exits',
|
||||
'creator-accounts': '/creator/accounts',
|
||||
'creator-competitors': '/creator/competitors',
|
||||
@@ -18,27 +14,17 @@ export const resourcePaths = {
|
||||
'creator-comments': '/creator/comments',
|
||||
'creator-leads': '/creator/leads',
|
||||
'creator-rules': '/creator/rules',
|
||||
'creator-operations': '/creator/operations',
|
||||
'creator-conversations': '/creator/conversations',
|
||||
'creator-events': '/creator/events',
|
||||
'creator-listeners': '/creator/listeners',
|
||||
} as const;
|
||||
|
||||
export type Resource = keyof typeof resourcePaths;
|
||||
|
||||
// 与归档版 filterKeys 一致:资源 → 允许透传的查询字段。
|
||||
const filterKeys: Partial<Record<Resource, string[]>> = {
|
||||
drafts: ['account_id'],
|
||||
confirmations: ['draft_id'],
|
||||
tasks: ['account_id', 'draft_id', 'state'],
|
||||
'creator-competitors': ['platform'],
|
||||
'creator-competitor-share-jobs': ['platform', 'status'],
|
||||
'creator-works': ['platform', 'source_id', 'source_type', 'published_at_status', 'published_after', 'published_before', 'min_likes', 'min_comments', 'min_shares', 'growth_hours', 'min_likes_growth'],
|
||||
'creator-comments': ['platform', 'work_id'],
|
||||
'creator-leads': ['platform'],
|
||||
'creator-operations': ['account_id'],
|
||||
'creator-conversations': ['account_id'],
|
||||
'creator-events': ['account_id'],
|
||||
};
|
||||
|
||||
export interface ListParams {
|
||||
@@ -70,7 +56,7 @@ export async function getList<T = any>({ resource, page, pageSize, filters = {}
|
||||
};
|
||||
}
|
||||
|
||||
const GETONE_ALLOWED: Resource[] = ['accounts', 'network-exits', 'browsers', 'drafts', 'confirmations', 'tasks', 'attempts', 'creator-accounts', 'creator-competitors', 'creator-competitor-share-jobs', 'creator-works', 'creator-comments', 'creator-rules', 'creator-operations'];
|
||||
const GETONE_ALLOWED: Resource[] = ['accounts', 'network-exits', 'browsers', 'creator-accounts', 'creator-competitors', 'creator-competitor-share-jobs', 'creator-works', 'creator-comments', 'creator-rules'];
|
||||
|
||||
export async function getOne<T = any>(resource: Resource, id: string | number): Promise<T> {
|
||||
if (!GETONE_ALLOWED.includes(resource)) throw new Error(`${resource} 不支持 getOne`);
|
||||
@@ -109,24 +95,6 @@ export async function accountAction(id: string, action: 'pause' | 'resume'): Pro
|
||||
await request(`/phase-a/accounts/${encodeURIComponent(id)}/${action}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function createDraft(accountID: string, content: string) {
|
||||
return request('/phase-a/drafts', jsonOptions('POST', { account_id: accountID, content }));
|
||||
}
|
||||
|
||||
export function confirmDraft(draftID: string, accountVersion: number, draftVersion: number) {
|
||||
return request('/phase-a/confirmations', jsonOptions('POST', { draft_id: draftID, account_version: accountVersion, draft_version: draftVersion }));
|
||||
}
|
||||
|
||||
export function enqueueConfirmation(confirmationID: string) {
|
||||
return request('/phase-a/tasks', jsonOptions('POST', { confirmation_id: confirmationID }));
|
||||
}
|
||||
|
||||
export async function taskAction(id: string, action: 'verify' | 'resume' | 'finish' | 'cancel', data?: unknown): Promise<void> {
|
||||
if (!['verify', 'resume', 'finish', 'cancel'].includes(action)) throw new Error(`未知任务操作: ${action}`);
|
||||
const options = data ? jsonOptions('POST', data) : { method: 'POST' };
|
||||
await request(`/phase-a/tasks/${encodeURIComponent(id)}/${action}`, options);
|
||||
}
|
||||
|
||||
export async function networkExitAction(id: string, action: 'check' | 'disable' | 'enable'): Promise<any> {
|
||||
if (!['check', 'disable', 'enable'].includes(action)) throw new Error(`未知网络出口操作: ${action}`);
|
||||
return request(`/network-exits/${encodeURIComponent(id)}/${action}`, { method: 'POST' });
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
// 列表页共享工具:资源 → 查询字段白名单,对齐 web.archived/src/shared/api/dataProvider.js 的 filterKeys。
|
||||
// creator-* 资源走 page/page_size 分页;其余资源后端返回全量或自带 total。
|
||||
export const filterKeys: Record<string, string[]> = {
|
||||
drafts: ['account_id'],
|
||||
confirmations: ['draft_id'],
|
||||
tasks: ['account_id', 'draft_id', 'state'],
|
||||
'creator-competitors': ['platform'],
|
||||
'creator-competitor-share-jobs': ['platform', 'status'],
|
||||
'creator-works': ['platform', 'source_id', 'source_type', 'published_after', 'published_before', 'min_likes', 'min_comments', 'min_shares'],
|
||||
'creator-comments': ['platform', 'work_id'],
|
||||
'creator-leads': ['platform'],
|
||||
'creator-operations': ['account_id'],
|
||||
'creator-conversations': ['account_id'],
|
||||
'creator-events': ['account_id'],
|
||||
};
|
||||
|
||||
export function isCreatorResource(resource: string): boolean {
|
||||
|
||||
@@ -24,13 +24,8 @@ const rules: Array<{ test: (path: string) => boolean; title: string; subtitle: s
|
||||
{ test: (p) => /\/edit$/.test(p) && p.startsWith('/accounts/'), title: '编辑社媒账号', subtitle: '维护账号资料、登录核验与自动响应策略。' },
|
||||
{ test: (p) => p.startsWith('/accounts/'), title: '账号详情', subtitle: '查看账号状态、登录身份与运行环境绑定。' },
|
||||
{ test: (p) => p === '/creator/competitors', title: '竞品分析', subtitle: '查看竞品作品与指标,分享链接导入请前往账号导入。' },
|
||||
{ test: (p) => p === '/creator/workbench', title: '运营工作台', subtitle: '评论、线索、私信与写操作均保留来源和明确结果。' },
|
||||
{ test: (p) => p === '/creator/settings', title: '采集设置', subtitle: '统一配置采集窗口、指标采集和已批准的服务。' },
|
||||
{ test: (p) => p.startsWith('/creator/competitors/'), title: '竞品账号详情', subtitle: '查看竞品画像与作品列表,点击作品可追溯指标统计。' },
|
||||
{ test: (p) => p.startsWith('/drafts/'), title: '草稿核对', subtitle: '核对版本化草稿,确认后再将操作加入队列。' },
|
||||
{ test: (p) => p.startsWith('/attempts/'), title: 'Attempt 详情', subtitle: '查看执行结果、运行环境与网络出口。' },
|
||||
{ test: (p) => p === '/tasks', title: '任务中心', subtitle: '逐条处理暂停、版本变化与未知执行结果。' },
|
||||
{ test: (p) => p.startsWith('/tasks/'), title: '任务详情', subtitle: '查看任务状态、执行结果与允许的下一步操作。' },
|
||||
{ test: (p) => p === '/browsers', title: '运行环境', subtitle: '启动、停止并回收隔离的指纹浏览器运行环境。' },
|
||||
{ test: (p) => p === '/browsers/new', title: '创建运行环境', subtitle: '为已授权且暂停的账号绑定网关、指纹与网络出口。' },
|
||||
{ test: (p) => p.startsWith('/browsers/'), title: '运行环境详情', subtitle: '查看环境状态、账号绑定和运行实例信息。' },
|
||||
@@ -50,9 +45,7 @@ export const menu: MenuDataItem[] = [
|
||||
{ path: '/accounts', name: '我的账号', icon: <UserOutlined /> },
|
||||
{ path: '/accounts/monitoring', name: '监控账号', icon: <EyeOutlined /> },
|
||||
{ path: '/creator/competitors', name: '竞品分析', icon: <LineChartOutlined /> },
|
||||
{ path: '/creator/workbench', name: '运营工作台', icon: <CommentOutlined /> },
|
||||
{ path: '/creator/settings', name: '采集设置', icon: <SettingOutlined /> },
|
||||
{ path: '/tasks', name: '任务中心', icon: <ProfileOutlined /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user