Merge branch 'fix/ui-review-20260927'——UI 走查问题全量修复:详情页 Flex vertical、工作台假加载、列表按钮规范、App.useApp 统一、任务筛选下拉、竞品素材 Drawer
douyin-release-gate / verify (push) Failing after 3m9s

This commit is contained in:
2026-09-28 08:57:20 +08:00
22 changed files with 386 additions and 518 deletions
@@ -3,32 +3,16 @@
// 数据走 GET /creator/accounts/monitor-views(画像 + 作品数/最近发布聚合),对齐竞品列表画像形态。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import {
Flex,
Alert,
Button,
Card,
Form,
Modal,
Popconfirm,
Select,
Space,
Table,
Tag,
Typography,
message,
} from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { Alert, App, Button, Card, Flex, Form, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { remove, creatorUpdate, creatorGet } from '@/services/api';
import { conflictMessage, dateTime } from '@/utils/helpers';
import { usePageActions } from '@/components/PageActions';
// 仅用 antd 默认组件:Table/Tag/Modal/Popconfirm/Select(tags)。
interface Row {
id: string;
name: string;
platform: string;
platform_account_key: string;
tags?: string[];
runtime_status?: string;
@@ -45,7 +29,7 @@ export default function AccountManagementList() {
const [tagForm] = Form.useForm<{ tags: string[] }>();
const [tagBusy, setTagBusy] = useState(false);
const [actionBusy, setActionBusy] = useState('');
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
@@ -56,8 +40,7 @@ export default function AccountManagementList() {
(rows ?? []).map((account: any) => ({
...account,
name: account.name,
tags: account.tags ?? [],
})),
tags: account.tags ?? [] })),
);
} catch (loadError) {
setError(loadError);
@@ -115,9 +98,7 @@ export default function AccountManagementList() {
{account.platform_account_key}
</Typography.Paragraph>
</div>
),
},
{ title: '平台', dataIndex: 'platform', render: () => '抖音' },
) },
{
title: '作品数据',
dataIndex: 'work_count',
@@ -128,13 +109,11 @@ export default function AccountManagementList() {
最近发布 {account.latest_published_at ? dateTime(account.latest_published_at) : '—'}
</Typography.Text>
</Flex>
),
},
) },
{
title: '标签',
dataIndex: 'tags',
render: (tags?: string[]) => (tags?.length ? tags.map((tag) => <Tag key={tag}>{tag}</Tag>) : <Typography.Text type="secondary">未设置</Typography.Text>),
},
render: (tags?: string[]) => (tags?.length ? tags.map((tag) => <Tag key={tag}>{tag}</Tag>) : <Typography.Text type="secondary">未设置</Typography.Text>) },
{
title: '状态',
dataIndex: 'status',
@@ -145,8 +124,7 @@ export default function AccountManagementList() {
</Tag>
<Typography.Text type="secondary">{account.authorization_status === 'authorized' ? '已授权' : '已撤销'}</Typography.Text>
</Flex>
),
},
) },
{
title: '操作',
key: 'actions',
@@ -159,9 +137,6 @@ export default function AccountManagementList() {
<Button size="small" onClick={() => history.push(`/accounts/${account.id}/edit`)}>
编辑账号
</Button>
<Button size="small" onClick={() => history.push(`/accounts/${account.id}`)}>
查看账号
</Button>
<Popconfirm
title={`确定删除自有账号“${account.name}”吗?相关专属资源也会被清理。`}
onConfirm={() => deleteAccount(account)}
@@ -172,21 +147,19 @@ export default function AccountManagementList() {
</Button>
</Popconfirm>
</Space>
),
},
) },
];
usePageActions(
<Button type="primary" icon={<PlusOutlined />} onClick={() => history.push('/accounts/new')}>
创建账号
</Button>,
);
return (
<Card variant="outlined">
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Typography.Text type="secondary">共 {rows.length} 个自有账号</Typography.Text>
<Space>
<Button icon={<ReloadOutlined />} onClick={load} disabled={pending} aria-label="刷新" />
<Button type="primary" icon={<PlusOutlined />} onClick={() => history.push('/accounts/new')}>
创建账号
</Button>
</Space>
</Flex>
{error ? (
<Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={load}>重试</Button>} />
@@ -2,29 +2,25 @@
// 展示作者头像与粉丝/关注/作品/最近发布等账号画像属性(migration 038 字段 + work 聚合)。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import {
Alert,
Avatar,
Button,
Card,
Descriptions,
Empty,
Flex,
Form,
Listy,
Modal,
Popconfirm,
Select,
Space,
Spin,
Tag,
Typography,
message,
} from 'antd';
import { Alert, App, Avatar, Button, Card, Descriptions, Empty, Flex, Form, Listy, Modal, Popconfirm, Select, Space, Spin, Tag, theme, Typography } from 'antd';
import { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
import { getList, remove, creatorAction, creatorUpdate } from '@/services/api';
import { conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
const useRowHoverStyle = () => {
const { token } = theme.useToken();
return {
padding: 16,
borderBottom: `1px solid ${token.colorSplit}`,
borderRadius: token.borderRadiusLG,
cursor: 'pointer',
transition: 'background-color 0.2s',
['@media (hover: hover)']: {
'&:hover': { backgroundColor: token.colorFillQuaternary },
},
} as const;
};
interface CompetitorRow {
id: string;
platform: string;
@@ -50,8 +46,7 @@ const syncStatusMeta: Record<string, { color?: string; label: string }> = {
running: { color: 'processing', label: '同步中' },
paused: { label: '已暂停' },
failed: { color: 'error', label: '同步失败' },
blocked: { color: 'warning', label: '同步受阻' },
};
blocked: { color: 'warning', label: '同步受阻' } };
function formatCount(value?: number | null): string {
if (value === null || value === undefined) return '—';
@@ -66,7 +61,7 @@ export default function MonitoringAccountList() {
const [tagForm] = Form.useForm<{ tags: string[] }>();
const [tagBusy, setTagBusy] = useState(false);
const [actionBusy, setActionBusy] = useState('');
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
@@ -148,11 +143,12 @@ export default function MonitoringAccountList() {
}
}
const rowHoverStyle = useRowHoverStyle();
const syncMeta = syncStatusMeta[tagTarget?.sync_status ?? ''] || { label: tagTarget?.sync_status || '—' };
return (
<Card variant="outlined">
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Typography.Text type="secondary">共 {rows.length} 个监控账号</Typography.Text>
<Space>
@@ -171,10 +167,7 @@ export default function MonitoringAccountList() {
itemRender={(row) => {
const meta = syncStatusMeta[row.sync_status ?? ''] || { label: row.sync_status || '—' };
return (
<div
style={{ padding: '16px 0', borderBottom: '1px solid rgba(5,5,5,0.06)', cursor: 'pointer' }}
onClick={() => history.push(`/accounts/monitoring/${row.id}`)}
>
<div style={rowHoverStyle} onClick={() => history.push(`/accounts/monitoring/${row.id}`)}>
<Flex justify="space-between" align="flex-start" gap={16}>
<Flex align="flex-start" gap={12}>
<Avatar size={44} src={row.avatar_url || undefined}>
@@ -198,7 +191,7 @@ export default function MonitoringAccountList() {
<Button size="small" onClick={() => openTags(row)}>
编辑标签
</Button>
<Button size="small" disabled={actionBusy === row.id} onClick={() => syncCompetitor(row)}>
<Button size="small" loading={actionBusy === row.id} disabled={actionBusy === row.id} onClick={() => syncCompetitor(row)}>
强制同步
</Button>
<Button size="small" disabled={actionBusy === row.id} onClick={() => toggleCompetitor(row)}>
@@ -232,8 +225,7 @@ export default function MonitoringAccountList() {
<Typography.Text type="danger" style={{ fontSize: 12 }}>{row.sync_error}</Typography.Text>
) : (
'—'
),
},
) },
]}
/>
</div>
+5 -24
View File
@@ -4,30 +4,14 @@
// 指标 GET /creator/works/:id/metrics,评论 GET /creator/comments?work_id=(定时采集由后端调度器维护)。
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Line } from '@ant-design/charts';
import {
Alert,
Button,
Card,
Col,
Drawer,
Empty,
Flex,
InputNumber,
Row,
Space,
Spin,
Table,
Typography,
message,
} from 'antd';
import { Alert, App, Button, Card, Col, Drawer, Empty, Flex, InputNumber, Row, Space, Spin, Table, Typography } from 'antd';
import {
CommentOutlined,
EyeOutlined,
FireOutlined,
LikeOutlined,
ReloadOutlined,
ShareAltOutlined,
} from '@ant-design/icons';
ShareAltOutlined } from '@ant-design/icons';
import { creatorGet, getList } from '@/services/api';
import { conflictMessage, dateTime } from '@/utils/helpers';
@@ -49,8 +33,7 @@ function GrowthChart({ metrics }: { metrics: any[] }) {
.map((series) => ({
time: dateTime(point.collected_at),
metric: series.label,
value: point[series.key],
})),
value: point[series.key] })),
),
[metrics],
);
@@ -88,7 +71,7 @@ export default function WorksGallery({ sourceId, sourceType, emptyHint }: WorksG
const [commentPage, setCommentPage] = useState(1);
const [commentTotal, setCommentTotal] = useState(0);
const [commentsPending, setCommentsPending] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const loadWorks = useCallback(async () => {
setWorksPending(true);
@@ -149,7 +132,6 @@ export default function WorksGallery({ sourceId, sourceType, emptyHint }: WorksG
return (
<>
{contextHolder}
<Card
title="作品列表"
extra={
@@ -290,8 +272,7 @@ export default function WorksGallery({ sourceId, sourceType, emptyHint }: WorksG
setCommentPage(page);
loadComments(detailWork.id, page);
},
hideOnSinglePage: true,
}}
hideOnSinglePage: true }}
columns={[
{ title: '作者', dataIndex: 'author_name', width: 140, render: (v) => v || '—' },
{ title: '内容', dataIndex: 'content', ellipsis: true },
+9 -34
View File
@@ -2,24 +2,7 @@
// 布局改用 antd Tabs(账号资料 / 自动响应策略)——归档版三区块信息量超过单页栅格,Tabs 是 antd 标准组织方式。
import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import {
Flex,
Alert,
Button,
Card,
Checkbox,
Descriptions,
Form,
Input,
InputNumber,
Popconfirm,
Select,
Space,
Tabs,
Tag,
Typography,
message,
} from 'antd';
import { Alert, App, Button, Card, Checkbox, Descriptions, Flex, Form, Input, InputNumber, Popconfirm, Select, Space, Tabs, Tag, Typography } from 'antd';
import { creatorAction, creatorCreate, creatorGet, creatorRequest, creatorUpdate, getList } from '@/services/api';
import { conflictMessage, dateTime } from '@/utils/helpers';
@@ -41,8 +24,7 @@ function profileForm(profile: any) {
note: profile.note || '',
business_status: profile.business_status || 'normal',
reply_requirements: profile.reply_requirements || '',
cooldown_seconds: profile.cooldown_seconds || 86400,
};
cooldown_seconds: profile.cooldown_seconds || 86400 };
}
export default function Page() {
@@ -61,7 +43,7 @@ export default function Page() {
const [relationError, setRelationError] = useState<any>(null);
const [listener, setListener] = useState<any>(null);
const [listenerError, setListenerError] = useState<any>(null);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const selected = profiles.find((item) => item.id === selectedID);
@@ -115,8 +97,7 @@ export default function Page() {
try {
const result = await creatorUpdate(`/creator/accounts/${encodeURIComponent(selected.id)}/profile`, {
...values,
cooldown_seconds: Number(values.cooldown_seconds),
});
cooldown_seconds: Number(values.cooldown_seconds) });
setProfiles((items) => items.map((item) => (item.id === selected.id ? result : item)));
form.setFieldsValue(profileForm(result));
messageApi.success('账号资料已保存。');
@@ -170,8 +151,7 @@ export default function Page() {
setBusy(true);
try {
const result = await creatorAction(`/creator/accounts/${encodeURIComponent(selected.id)}/big-account`, {
enabled: !selected.big_account,
});
enabled: !selected.big_account });
setProfiles((items) => items.map((item) => (item.id === selected.id ? result : item)));
form.setFieldsValue(profileForm(result));
messageApi.success(result.big_account ? '已开启大号模式。' : '已关闭大号模式。');
@@ -192,13 +172,11 @@ export default function Page() {
candidate_texts: String(values.candidate_texts || '')
.split(',')
.map((item: string) => item.trim())
.filter(Boolean),
};
.filter(Boolean) };
await creatorCreate('/creator/relations', {
big_account_id: selected.id,
small_account_id: values.execution_account_id,
enabled: true,
});
enabled: true });
const result = editingStrategyID
? await creatorUpdate(`/creator/strategies/${encodeURIComponent(editingStrategyID)}`, payload)
: await creatorCreate(`/creator/accounts/${encodeURIComponent(selected.id)}/strategies`, payload);
@@ -235,8 +213,7 @@ export default function Page() {
await creatorCreate('/creator/relations', {
big_account_id: relation.big_account_id,
small_account_id: relation.small_account_id,
enabled: false,
});
enabled: false });
setRelations((items) => items.filter((item) => item.small_account_id !== relation.small_account_id));
messageApi.success('账号关系已解除;已有策略不会自动恢复。');
} catch (actionError) {
@@ -451,8 +428,7 @@ export default function Page() {
enabled: strategy.enabled,
event_types: strategy.event_types || [],
action: strategy.action,
candidate_texts: (strategy.candidate_texts || []).join(', '),
});
candidate_texts: (strategy.candidate_texts || []).join(', ') });
}}
disabled={busy}
>
@@ -479,7 +455,6 @@ export default function Page() {
return (
<div>
{contextHolder}
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Typography.Title level={4} style={{ marginBottom: 0 }}>
+28 -44
View File
@@ -6,20 +6,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { history, useParams } from '@umijs/max';
import { Line } from '@ant-design/charts';
import {
Flex,
Alert,
Button,
Card,
Descriptions,
Form,
Input,
Modal,
Space,
Tag,
Typography,
message,
} from 'antd';
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 { accountReadiness, conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
import { usePageActions, usePageInfo } from '@/components/PageActions';
@@ -31,8 +18,7 @@ const collectionStatusMeta: Record<string, { color?: string; label: string }> =
succeeded: { color: 'success', label: '采集中' },
running: { color: 'processing', label: '采集运行中' },
failed: { color: 'error', label: '采集失败' },
blocked: { color: 'warning', label: '采集受阻' },
};
blocked: { color: 'warning', label: '采集受阻' } };
// 粉丝趋势:把账号画像时序展开成长表(时间 × 指标 × 数值)供折线图渲染。
const accountSeriesMeta = [
@@ -56,8 +42,7 @@ function AccountTrendChart({ metrics }: { metrics: any[] }) {
.map((series) => ({
time: dateTime(point.collected_at),
metric: series.label,
value: point[series.key],
})),
value: point[series.key] })),
),
[metrics],
);
@@ -89,7 +74,7 @@ export default function Page() {
const [draftBusy, setDraftBusy] = useState(false);
const [collectionStatus, setCollectionStatus] = useState<any>(null);
const [accountMetrics, setAccountMetrics] = useState<any[]>([]);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const readiness = account ? accountReadiness(account, binding, browsersError) : null;
@@ -167,30 +152,30 @@ export default function Page() {
// 页头 content 行右侧:返回/强制同步/编辑注册到 Layout 页头槽,不在卡片内重复渲染。
usePageActions(
account ? (
<Space>
<Button onClick={() => history.push('/accounts')}>返回社媒账号</Button>
<Button
disabled={actionBusy}
onClick={async () => {
// 强制同步:后端复用账号自身运行环境采集,需已登录+环境就绪(否则 409/503 提示原因)。
setActionBusy(true);
try {
await creatorAction(`/creator/accounts/${encodeURIComponent(account.id)}/sync`);
messageApi.success('强制同步已执行。');
await loadAll();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '强制同步失败;账号需已登录且运行环境就绪。'));
} finally {
setActionBusy(false);
}
}}
>
强制同步
</Button>
<Button type="primary" onClick={() => history.push(`/accounts/${account.id}/edit`)}>编辑账号</Button>
</Space>
) : null,
<Space>
<Button onClick={() => history.push('/accounts')}>返回社媒账号</Button>
<Button
disabled={actionBusy || !account}
onClick={async () => {
// 强制同步:后端复用账号自身运行环境采集,需已登录+环境就绪(否则 409/503 提示原因)。
setActionBusy(true);
try {
await creatorAction(`/creator/accounts/${encodeURIComponent(account.id)}/sync`);
messageApi.success('强制同步已执行。');
await loadAll();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '强制同步失败;账号需已登录且运行环境就绪。'));
} finally {
setActionBusy(false);
}
}}
>
强制同步
</Button>
<Button type="primary" disabled={!account} onClick={() => account && history.push(`/accounts/${account.id}/edit`)}>
编辑账号
</Button>
</Space>,
[account?.id],
);
@@ -246,7 +231,6 @@ export default function Page() {
return (
<Flex vertical gap={16}>
{contextHolder}
<Card title="账号状态" extra={pauseResume}>
<Flex vertical gap={16}>
<Descriptions column={1} size="small">
+8 -30
View File
@@ -1,21 +1,6 @@
// 账号导入:语义对齐 web.archived AccountImportPage(分享链接解析任务列表 + 新建导入任务 Modal)。
import { useCallback, useEffect, useState } from 'react';
import {
Alert,
Avatar,
Button,
Card,
Flex,
Form,
Input,
Modal,
Select,
Space,
Table,
Tag,
Typography,
message,
} from 'antd';
import { Alert, App, Avatar, Button, Card, Flex, Form, Input, Modal, Select, Space, Table, Tag, Typography } from 'antd';
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { create, getList, creatorAction } from '@/services/api';
@@ -47,7 +32,7 @@ export default function Page() {
const [form] = Form.useForm<FormValues>();
const [busy, setBusy] = useState(false);
const [retryID, setRetryID] = useState<string | null>(null);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const loadShareJobs = useCallback(async () => {
setPending(true);
@@ -90,8 +75,7 @@ export default function Page() {
try {
const variables: Record<string, unknown> = {
platform: sharePlatform,
share_url: extractedShareURL,
};
share_url: extractedShareURL };
if (values.tags?.length) variables.tags = values.tags;
await create('creator-competitor-share-jobs', variables);
await loadShareJobs();
@@ -144,8 +128,7 @@ export default function Page() {
) : (
inner
);
},
},
} },
{
title: '分享链接',
dataIndex: 'share_url',
@@ -157,8 +140,7 @@ export default function Page() {
</Typography.Link>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>{dateTime(job.created_at)}</Typography.Text>
</div>
),
},
) },
{ title: '平台', dataIndex: 'platform', render: (value?: string) => platformLabel(value) },
{
title: '状态',
@@ -166,14 +148,12 @@ export default function Page() {
render: (value?: string) => {
const status = shareJobStatus[value ?? ''] || { label: value || '未知', tone: 'default' as const };
return <Tag color={status.tone === 'error' ? 'error' : status.tone === 'success' ? 'success' : status.tone === 'warning' ? 'warning' : 'default'}>{status.label}</Tag>;
},
},
} },
{ title: '尝试次数', dataIndex: 'attempts', render: (value?: number) => <Typography.Text type="secondary">{value ?? 0} / 3</Typography.Text> },
{
title: '失败原因',
dataIndex: 'failure_reason',
render: (value?: string) => (value ? <Typography.Text type="danger" style={{ fontSize: 12 }}>{value}</Typography.Text> : '—'),
},
render: (value?: string) => (value ? <Typography.Text type="danger" style={{ fontSize: 12 }}>{value}</Typography.Text> : '—') },
{
title: '操作',
key: 'actions',
@@ -185,13 +165,11 @@ export default function Page() {
</Button>
) : (
'—'
),
},
) },
];
return (
<Card variant="outlined">
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Typography.Text type="secondary">共 {jobs.length} 个导入任务</Typography.Text>
<Space>
+47 -61
View File
@@ -4,18 +4,7 @@
// 作品列表走 GET /creator/works?source_id=<competitor_id>&source_type=competitor,定时采集由后端调度器维护。
import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import {
Alert,
Avatar,
Button,
Card,
Descriptions,
Flex,
Space,
Tag,
Typography,
message,
} from 'antd';
import { Alert, App, Avatar, Button, Card, Descriptions, Flex, Space, Tag, Typography } from 'antd';
import { creatorAction, getOne } from '@/services/api';
import { conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
import { usePageActions, usePageInfo } from '@/components/PageActions';
@@ -45,8 +34,7 @@ const syncStatusMeta: Record<string, { color?: string; label: string }> = {
running: { color: 'processing', label: '同步中' },
paused: { label: '已暂停' },
failed: { color: 'error', label: '同步失败' },
blocked: { color: 'warning', label: '同步受阻' },
};
blocked: { color: 'warning', label: '同步受阻' } };
function formatCount(value?: number | null): string {
if (value === null || value === undefined) return '—';
@@ -59,7 +47,7 @@ export default function Page() {
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [actionBusy, setActionBusy] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const loadCompetitor = useCallback(async () => {
setPending(true);
@@ -99,49 +87,49 @@ export default function Page() {
);
usePageActions(
competitor ? (
<Space>
<Button onClick={() => history.push('/accounts/monitoring')}>返回监控账号</Button>
<Button
disabled={actionBusy || !competitor.enabled}
onClick={async () => {
// 强制同步:后端用临时匿名浏览器采集公开作品,无需自有账号身份。
setActionBusy(true);
try {
await creatorAction(`/creator/competitors/${encodeURIComponent(competitor.id)}/sync`);
messageApi.success('强制同步已执行。');
await loadCompetitor();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '强制同步失败'));
} finally {
setActionBusy(false);
}
}}
>
强制同步
</Button>
<Button
type="primary"
disabled={actionBusy}
onClick={async () => {
setActionBusy(true);
try {
await creatorAction(
`/creator/competitors/${encodeURIComponent(competitor.id)}/${competitor.enabled ? 'pause' : 'resume'}`,
);
messageApi.success(competitor.enabled ? '监测账号已暂停。' : '监测账号已恢复。');
await loadCompetitor();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '监测账号状态更新失败'));
} finally {
setActionBusy(false);
}
}}
>
{competitor.enabled ? '暂停监控' : '恢复监控'}
</Button>
</Space>
) : null,
<Space>
<Button onClick={() => history.push('/accounts/monitoring')}>返回监控账号</Button>
<Button
disabled={actionBusy || !competitor}
onClick={async () => {
// 强制同步:后端用临时匿名浏览器采集公开作品,无需自有账号身份。
if (!competitor) return;
setActionBusy(true);
try {
await creatorAction(`/creator/competitors/${encodeURIComponent(competitor.id)}/sync`);
messageApi.success('强制同步已执行。');
await loadCompetitor();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '强制同步失败'));
} finally {
setActionBusy(false);
}
}}
>
强制同步
</Button>
<Button
type="primary"
disabled={actionBusy || !competitor}
onClick={async () => {
if (!competitor) return;
setActionBusy(true);
try {
await creatorAction(
`/creator/competitors/${encodeURIComponent(competitor.id)}/${competitor.enabled ? 'pause' : 'resume'}`,
);
messageApi.success(competitor.enabled ? '监测账号已暂停。' : '监测账号已恢复。');
await loadCompetitor();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '监测账号状态更新失败'));
} finally {
setActionBusy(false);
}
}}
>
{competitor ? (competitor.enabled ? '暂停监控' : '恢复监控') : '暂停监控'}
</Button>
</Space>,
[competitor?.id, competitor?.enabled],
);
@@ -150,7 +138,6 @@ export default function Page() {
return (
<Flex vertical gap={16}>
{contextHolder}
<Card title="账号画像">
<Descriptions
column={{ xs: 1, sm: 2, md: 3 }}
@@ -171,8 +158,7 @@ export default function Page() {
<Typography.Text type="danger" style={{ fontSize: 12 }}>{competitor.sync_error}</Typography.Text>
) : (
'—'
),
},
) },
]}
/>
</Card>
+1 -1
View File
@@ -42,7 +42,7 @@ export default function Page() {
</Typography.Text>
</div>
<Card>
<Flex>
<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>
+2 -2
View File
@@ -1,7 +1,7 @@
// 运行环境详情:语义对齐 web.archived BrowsersPage.jsx 的 BrowserDetail。
import { useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import { Flex, Alert, App, Button, Card, Descriptions, Space, Tag, Typography } from 'antd';
import { Alert, App, Button, Card, Descriptions, Flex, Space, Tag, Typography } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import { getOne } from '@/services/api';
@@ -57,7 +57,7 @@ export default function Page() {
<Button onClick={() => history.push('/browsers')}>返回运行环境</Button>
</div>
<Card>
<Flex>
<Flex vertical gap={12}>
<Tag color={runtime.state === 'running' ? 'success' : runtime.state === 'stopped' ? 'warning' : 'default'}>
{stateText[runtime.state] ?? `未知状态:${runtime.state}`}
</Tag>
+9 -17
View File
@@ -2,7 +2,7 @@
// 动作保持显式动词(start/stop/recycle),启停禁用条件与归档版一致。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import { Alert, Button, Card, Flex, Popconfirm, Space, Table, Tag, Typography, message } from 'antd';
import { Alert, App, Button, Card, Flex, Popconfirm, Space, Table, Tag, Typography } from 'antd';
import { CopyOutlined, PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { browserAction, getList } from '@/services/api';
@@ -17,8 +17,7 @@ const stateText: Record<string, string> = {
gateway_unreachable: '网关不可达',
runtime_missing: '运行实例缺失',
recorded: '已记录',
missing: '未部署',
};
missing: '未部署' };
const blockText: Record<string, string> = {
account_revoked: '授权已撤销',
account_paused: '账号已暂停',
@@ -26,8 +25,7 @@ const blockText: Record<string, string> = {
network_exit_missing: '固定出口缺失',
network_exit_unhealthy: '固定出口不健康',
runtime_stop_pending: '停止结果待确认',
runtime_missing: '运行实例缺失',
};
runtime_missing: '运行实例缺失' };
const scheduleBlock = (runtime: any) =>
runtime.schedule_status === 'ready' ? '' : blockText[runtime.schedule_block_reason] || runtime.schedule_block_reason || '不可调度';
@@ -37,7 +35,7 @@ export default function Page() {
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [busy, setBusy] = useState('');
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
@@ -83,8 +81,7 @@ export default function Page() {
{runtime.alias} · seed {runtime.fingerprint?.seed}
</Typography.Paragraph>
</div>
),
},
) },
{
title: '状态',
dataIndex: 'state',
@@ -93,8 +90,7 @@ export default function Page() {
return (
<Tag color={state === 'running' ? 'success' : state === 'stopped' ? 'warning' : 'default'}>{known ? stateText[state] : `未知状态:${state}`}</Tag>
);
},
},
} },
{
title: '固定绑定',
dataIndex: 'account_id',
@@ -109,8 +105,7 @@ export default function Page() {
<Tag color={block ? 'warning' : 'success'}>{block || '可调度'}</Tag>
</div>
);
},
},
} },
{
title: 'CDP',
dataIndex: 'endpoint',
@@ -124,8 +119,7 @@ export default function Page() {
</Space>
) : (
<Typography.Text type="secondary">—</Typography.Text>
),
},
) },
{
title: '操作',
key: 'actions',
@@ -155,13 +149,11 @@ export default function Page() {
</Popconfirm>
</Space>
);
},
},
} },
];
return (
<Card variant="outlined">
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Typography.Text type="secondary">共 {runtimes.length} 个运行环境</Typography.Text>
<Button type="primary" icon={<PlusOutlined />} onClick={() => history.push('/browsers/new')}>
+39 -13
View File
@@ -2,7 +2,7 @@
// 高级指纹参数用 antd Collapse 收纳;空值不提交,收敛到 seed 驱动。
import { useEffect, useState } from 'react';
import { history } from '@umijs/max';
import { Alert, Button, Card, Checkbox, Collapse, Form, Input, InputNumber, Select, Space, Typography, message } from 'antd';
import { Alert, App, Button, Card, Checkbox, Col, Collapse, Form, Input, InputNumber, Row, Select, Space, Typography } from 'antd';
import { create, getList } from '@/services/api';
import { conflictMessage } from '@/utils/helpers';
@@ -21,8 +21,7 @@ function fingerprintPayload(form: any) {
brand_version: form.brand_version,
lang: form.lang,
accept_lang: form.accept_lang,
timezone: form.timezone,
};
timezone: form.timezone };
for (const [key, value] of Object.entries(advanced)) if (value) payload[key] = value;
if (form.hardware_concurrency) payload.hardware_concurrency = Number(form.hardware_concurrency);
if (form.disable_spoofing?.length > 0) payload.disable_spoofing = form.disable_spoofing.join(',');
@@ -44,7 +43,7 @@ export default function Page() {
const [accounts, setAccounts] = useState<any[]>([]);
const [networkExits, setNetworkExits] = useState<any[]>([]);
const [submitBusy, setSubmitBusy] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
useEffect(() => {
Promise.allSettled([
@@ -88,8 +87,7 @@ export default function Page() {
gateway: values.gateway,
account_id: values.account_id,
network_exit_id: values.network_exit_id || '',
fingerprint: fingerprintPayload(values),
});
fingerprint: fingerprintPayload(values) });
messageApi.success('运行环境已创建。');
history.push('/browsers');
} catch (reason: any) {
@@ -103,16 +101,18 @@ export default function Page() {
return (
<div>
{contextHolder}
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={() => history.push('/browsers')}>返回运行环境</Button>
</div>
<Card variant="outlined">
<Form form={form} layout="vertical" onFinish={submit}>
<Space size={16} wrap style={{ display: 'flex' }}>
<Row gutter={[16, 0]}>
<Col xs={24} md={12} xl={8}>
<Form.Item name="name" label="环境名称" required rules={[{ required: true, message: '请输入环境名称' }]} extra="展示名称,支持中文,如:店铺一号">
<Input maxLength={64} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item
name="alias"
label="别名"
@@ -125,9 +125,13 @@ export default function Page() {
>
<Input placeholder="如:shop-01" />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="gateway" label="网关" required rules={[{ required: true, message: '请选择网关' }]} extra={gateways.length === 0 ? '请先在「网关管理」注册网关' : '环境由该 native gateway 管理'}>
<Select style={{ width: 200 }} placeholder="选择网关" disabled={gateways.length === 0} options={gateways.map((item) => ({ value: item.name, label: item.name }))} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item
name="seed"
label="Fingerprint Seed"
@@ -140,9 +144,13 @@ export default function Page() {
>
<InputNumber min={1} max={2147483647} style={{ width: 180 }} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="account_id" label="社媒账号" required rules={[{ required: true, message: '请选择账号' }]} extra={pausedAccounts.length === 0 ? '请先准备已授权且暂停的账号' : '绑定后该账号固定使用此环境'}>
<Select style={{ width: 240 }} placeholder="选择账号" disabled={pausedAccounts.length === 0} options={pausedAccounts.map((item) => ({ value: item.id, label: `${item.id} · ${item.platform}` }))} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="network_exit_id" label="网络出口" extra="留空时使用当前机器网络出口直连">
<Select
style={{ width: 240 }}
@@ -151,7 +159,8 @@ export default function Page() {
options={healthyExits.map((item) => ({ value: item.id, label: `${item.id} · ${item.protocol}://${item.host}:${item.port}` }))}
/>
</Form.Item>
</Space>
</Col>
</Row>
<Collapse
ghost
items={[
@@ -159,37 +168,54 @@ export default function Page() {
key: 'advanced',
label: '高级指纹参数',
children: (
<Space size={16} wrap style={{ display: 'flex' }}>
<Row gutter={[16, 0]}>
<Col xs={24} md={12} xl={8}>
<Form.Item name="platform" label="操作系统 platform">
<Select style={{ width: 140 }} allowClear placeholder="默认" options={platformOptions} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="platform_version" label="系统版本 platform-version" extra="如 11.0.0">
<Input style={{ width: 140 }} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="brand" label="浏览器品牌 brand">
<Select style={{ width: 140 }} allowClear placeholder="默认 Chromium" options={brandOptions} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="brand_version" label="品牌版本 brand-version" extra="如 132.0.6834.159">
<Input style={{ width: 160 }} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="hardware_concurrency" label="CPU 核心数 hardware-concurrency" extra="留空由种子生成">
<InputNumber min={1} max={128} style={{ width: 100 }} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="lang" label="语言 lang" extra="如 zh-CN">
<Input style={{ width: 120 }} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="accept_lang" label="接受语言 accept-lang" extra="如 zh-CN,en-US">
<Input style={{ width: 160 }} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="timezone" label="时区 timezone" extra="如 Asia/Shanghai">
<Input style={{ width: 160 }} />
</Form.Item>
</Col>
<Col xs={24} md={12} xl={8}>
<Form.Item name="disable_spoofing" label="禁用指纹伪装 disable-spoofing">
<Checkbox.Group options={spoofingFields.map((field) => ({ value: field, label: field }))} />
</Form.Item>
</Space>
),
},
</Col>
</Row>
) },
]}
/>
<Space>
+96 -76
View File
@@ -1,9 +1,10 @@
// 竞品分析:作品列表对齐 ant-design-pro card-list——antd Row/Col 响应式栅格 + Card 原样组合;
// 作品详情改走 Drawer;素材三步处理与仿写流程内联卡片原样保留;筛选自动刷新、手动翻页不变。
// 作品详情与素材处理均走 Drawer;日期筛选用 antd DatePicker;筛选自动刷新、手动翻页不变。
// 平台筛选已随 migration 038(platform 收敛 douyin)移除。
import { useEffect, useState } from 'react';
import { CommentOutlined, LikeOutlined, ShareAltOutlined } from '@ant-design/icons';
import { Flex, Alert, Button, Card, Col, Drawer, Empty, Input, InputNumber, Modal, Row, Select, Space, Spin, Table, Tag, Typography, message } from 'antd';
import { Alert, App, Button, Card, Col, DatePicker, Drawer, Empty, Flex, Input, InputNumber, Modal, Row, Select, Space, Spin, Table, Tag, Typography } from 'antd';
import type { Dayjs } from 'dayjs';
import { creatorAction, creatorGet, creatorUpdate, getList } from '@/services/api';
import { conflictMessage, dateTime } from '@/utils/helpers';
@@ -18,16 +19,15 @@ const publishedAtStatusMeta: Record<string, { color?: string; label: string }> =
verified: { color: 'green', label: '已核验' },
future: { color: 'orange', label: '未来时间' },
pending_verification: { label: '待核验' },
invalid: { color: 'red', label: '无效时间' },
};
invalid: { color: 'red', label: '无效时间' } };
interface Filters {
minLikes?: number;
minComments?: number;
minShares?: number;
publishedAtStatus?: string;
publishedAfter?: string;
publishedBefore?: string;
publishedAfter?: Dayjs;
publishedBefore?: Dayjs;
}
function statusTag(status?: string) {
@@ -55,7 +55,7 @@ export default function Page() {
const [rewriteTitle, setRewriteTitle] = useState('');
const [rewriteScript, setRewriteScript] = useState('');
const [pendingSwitchWork, setPendingSwitchWork] = useState<string | null>(null);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
async function load() {
setPending(true);
@@ -71,9 +71,7 @@ export default function Page() {
min_shares: filters.minShares === undefined ? undefined : Number(filters.minShares),
published_at_status: filters.publishedAtStatus,
published_after: filters.publishedAfter ? `${filters.publishedAfter}T00:00:00Z` : undefined,
published_before: filters.publishedBefore ? `${filters.publishedBefore}T23:59:59Z` : undefined,
},
});
published_before: filters.publishedBefore ? `${filters.publishedBefore}T23:59:59Z` : undefined } });
setWorks(result.data);
setWorkPageInfo({ total: result.total, hasNext: Boolean(result.hasNext) });
} catch (loadError) {
@@ -163,8 +161,7 @@ export default function Page() {
setMaterialPending(true);
try {
const result = await creatorAction(`/creator/works/${encodeURIComponent(material.work_id)}/material/rewrite/confirm`, {
requirement: rewriteRequirement,
});
requirement: rewriteRequirement });
setMaterial(result);
messageApi.info('已确认仿写要求,请在可编辑字段中复核并保存结果。');
} catch (actionError) {
@@ -196,8 +193,7 @@ export default function Page() {
try {
const result = await creatorUpdate(`/creator/works/${encodeURIComponent(material.work_id)}/material/rewrite`, {
title: rewriteTitle,
script: rewriteScript,
});
script: rewriteScript });
setMaterial(result);
messageApi.info('仿写标题和口播已保存,可继续人工复核。');
} catch (actionError) {
@@ -215,7 +211,6 @@ export default function Page() {
return (
<>
{contextHolder}
<Flex vertical gap={16}>
<Card title="筛选作品" extra={<Typography.Text type="secondary" style={{ fontSize: 12 }}>调整条件后自动刷新</Typography.Text>}>
<Space size={12} wrap style={{ display: 'flex' }}>
@@ -230,8 +225,20 @@ export default function Page() {
onChange={(value) => setFilters((f) => ({ ...f, publishedAtStatus: value }))}
options={publishedAtStatusOptions}
/>
<Input type="date" style={{ width: 160 }} value={filters.publishedAfter} onChange={(e) => setFilters((f) => ({ ...f, publishedAfter: e.target.value }))} />
<Input type="date" style={{ width: 160 }} value={filters.publishedBefore} onChange={(e) => setFilters((f) => ({ ...f, publishedBefore: e.target.value }))} />
<DatePicker
style={{ width: 160 }}
placeholder="发布开始日期"
value={filters.publishedAfter}
onChange={(value) => setFilters((f) => ({ ...f, publishedAfter: value ?? undefined }))}
allowClear
/>
<DatePicker
style={{ width: 160 }}
placeholder="发布结束日期"
value={filters.publishedBefore}
onChange={(value) => setFilters((f) => ({ ...f, publishedBefore: value ?? undefined }))}
allowClear
/>
</Space>
</Card>
<Card title="作品与指标">
@@ -261,8 +268,13 @@ export default function Page() {
<Button key="detail" type="link" size="small" onClick={() => openWorkDetail(work.id)} disabled={workDetailPending}>
详情
</Button>,
<Button key="select" type="primary" size="small" onClick={() => selectMaterial(work.id)}>
选择素材
<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>] : []),
]}
@@ -312,65 +324,73 @@ export default function Page() {
</Flex>
) : null}
</Card>
<Drawer
open={Boolean(material)}
onClose={() => setMaterial(null)}
size="large"
title="素材处理"
destroyOnHidden
>
{material ? (
<Card size="small" title="素材处理">
<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>
</Card>
<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 ? (
+6 -11
View File
@@ -1,7 +1,7 @@
// 采集设置:语义对齐 web.archived CreatorSettingsPage.jsx(提交 d97cade)。
// 四类配置用 antd Tabs 组织;仅用 antd 默认组件。
import { useCallback, useEffect, useState } from 'react';
import { Alert, Button, Card, Checkbox, Form, Input, InputNumber, Tabs, Typography, message } from 'antd';
import { Alert, App, Button, Card, Checkbox, Form, Input, InputNumber, Tabs, Typography } from 'antd';
import { creatorGet, creatorUpdate } from '@/services/api';
import { conflictMessage } from '@/utils/helpers';
@@ -36,7 +36,7 @@ export default function Page() {
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [busy, setBusy] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
@@ -75,7 +75,6 @@ export default function Page() {
return (
<div>
{contextHolder}
<Typography.Paragraph type="secondary" style={{ marginBottom: 16 }}>按配置类别管理 UTC 采集窗口、指标采集和已批准的服务。</Typography.Paragraph>
<Card variant="outlined">
<Form form={form} layout="vertical" onFinish={save} initialValues={{}}>
@@ -93,8 +92,7 @@ export default function Page() {
<InputNumber min={1} style={{ width: 200 }} />
</Form.Item>
</div>
),
},
) },
{
key: 'metrics',
label: '指标采集',
@@ -113,8 +111,7 @@ export default function Page() {
<InputNumber min={1} style={{ width: 200 }} />
</Form.Item>
</div>
),
},
) },
{
key: 'ai',
label: 'AI 服务',
@@ -130,8 +127,7 @@ export default function Page() {
<Checkbox>已完成 AI 配置审批</Checkbox>
</Form.Item>
</div>
),
},
) },
{
key: 'transcription',
label: '转写服务',
@@ -147,8 +143,7 @@ export default function Page() {
<Checkbox>已完成转写配置审批</Checkbox>
</Form.Item>
</div>
),
},
) },
]}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
+13 -25
View File
@@ -2,7 +2,7 @@
// 六个页签:评论/线索/规则/事件监听/私信/操作记录;实时更新走 /creator/updates SSE;仅用 antd 默认组件。
import { useEffect, useRef, useState } from 'react';
import type { ReactNode } from 'react';
import { Flex, Alert, Button, Card, Checkbox, Col, Empty, Input, Modal, Row, Select, Space, Spin, Table, Tabs, Tag, Typography, message } from 'antd';
import { Alert, App, Button, Card, Checkbox, Col, Empty, Flex, Input, Modal, Row, Select, Space, Spin, Table, Tabs, Tag, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { create, creatorAction, creatorCreate, creatorGet, creatorSubscribe, creatorSyncConversation, creatorUpdate, getList } from '@/services/api';
import { conflictMessage, dateTime } from '@/utils/helpers';
@@ -53,8 +53,7 @@ function operationStatus(state?: string, reason?: string): string {
failed: '失败(平台明确拒绝或执行前失败)',
blocked: '已阻止(未发送)',
processing: '处理中(尚未获得最终结果)',
uncertain: '结果不明(禁止自动重试)',
};
uncertain: '结果不明(禁止自动重试)' };
return `${labels[state || ''] || state || '未知状态'}${reason ? `:${reason}` : ''}`;
}
@@ -129,7 +128,7 @@ export default function Page() {
const [pendingDMSwitch, setPendingDMSwitch] = useState<{ kind: 'account' | 'conversation'; id: string } | null>(null);
const loadSequence = useRef(0);
const messageSequence = useRef(0);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const visibleData = dataTab === tab ? data : [];
const visibleConversations = conversationAccountID === dmAccountID ? conversations : [];
@@ -245,8 +244,7 @@ export default function Page() {
setMessages(items);
setMessagePageInfo({
total: Array.isArray(result) ? items.length : (result.total ?? items.length),
hasNext: Array.isArray(result) ? false : Boolean(result.has_next),
});
hasNext: Array.isArray(result) ? false : Boolean(result.has_next) });
})
.catch((loadError) => {
if (sequence === messageSequence.current) setMessageError(loadError);
@@ -378,8 +376,7 @@ export default function Page() {
topic: ruleForm.topic,
include_keywords: parseKeywords(ruleForm.include_keywords),
exclude_keywords: parseKeywords(ruleForm.exclude_keywords),
ai_requirement: ruleForm.ai_requirement,
};
ai_requirement: ruleForm.ai_requirement };
const result = editingRuleID
? await creatorUpdate(`/creator/rules/${encodeURIComponent(editingRuleID)}`, variables)
: (await create('creator-rules', variables)).data;
@@ -418,8 +415,7 @@ export default function Page() {
text: draft?.text || '',
target_uid: comment.author_uid || '',
target_comment_id: comment.id,
operation_key: draft?.operation_key || '',
});
operation_key: draft?.operation_key || '' });
messageApi.info(mode === 'dm' ? '请填写发送账号和私信文案,再进行逐次确认。' : '请填写发送账号和文案,再进行逐次确认。');
}
@@ -448,8 +444,7 @@ export default function Page() {
account_id: reply.account_id,
target_uid: reply.target_uid,
...(replyMode === 'comment' ? { target_comment_id: reply.target_comment_id } : {}),
text: reply.text,
});
text: reply.text });
const result = await creatorAction(`/creator/operations/${encodeURIComponent(created.id)}/execute`);
messageApi[result.state === 'succeeded' ? 'success' : 'warning'](operationStatus(result.state, result.reason));
if (result.state === 'succeeded') {
@@ -532,8 +527,7 @@ export default function Page() {
platform: dmConfirm.platform,
account_id: dmAccountID,
target_uid: dmConfirm.peer_uid,
text: dmText,
});
text: dmText });
const result = await creatorAction(`/creator/operations/${encodeURIComponent(created.id)}/execute`);
messageApi[result.state === 'succeeded' ? 'success' : 'warning'](operationStatus(result.state, result.reason));
if (result.state === 'succeeded') {
@@ -826,8 +820,7 @@ export default function Page() {
topic: rule.topic,
include_keywords: (rule.include_keywords || []).join(', '),
exclude_keywords: (rule.exclude_keywords || []).join(', '),
ai_requirement: rule.ai_requirement,
});
ai_requirement: rule.ai_requirement });
}}
disabled={busy}
>
@@ -877,8 +870,7 @@ export default function Page() {
padding: '8px 8px',
textAlign: 'left',
whiteSpace: 'normal',
background: conversation.id === conversationID ? '#e6f4ff' : undefined,
}}
background: conversation.id === conversationID ? '#e6f4ff' : undefined }}
type={conversation.id === conversationID ? 'primary' : 'default'}
ghost={conversation.id === conversationID}
onClick={() => switchConversation(conversation.id)}
@@ -935,8 +927,7 @@ export default function Page() {
maxWidth: '80%',
background: msg.direction === 'outbound' ? '#e6f4ff' : '#fafafa',
borderRadius: 6,
padding: 12,
}}
padding: 12 }}
>
<Typography.Paragraph style={{ marginBottom: 0 }}>{msg.text || `[${msg.message_type}]`}</Typography.Paragraph>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
@@ -992,8 +983,7 @@ export default function Page() {
statusTag(
item.state === 'succeeded' ? 'success' : item.state === 'uncertain' || item.state === 'processing' ? 'warning' : item.state === 'failed' ? 'error' : 'default',
operationStatus(item.state, item.reason),
),
},
) },
{ title: '时间', dataIndex: 'created_at', render: (value: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{dateTime(value)}</Typography.Text> },
{ title: '原因', dataIndex: 'reason', render: (value?: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{value || '—'}</Typography.Text> },
];
@@ -1013,7 +1003,6 @@ export default function Page() {
return (
<Card variant="outlined">
{contextHolder}
<Tabs
activeKey={tab}
onChange={switchTab}
@@ -1031,8 +1020,7 @@ export default function Page() {
? eventsTab
: key === 'dms'
? dmsTab
: operationsTab,
}))}
: operationsTab }))}
/>
<Modal
open={Boolean(confirm)}
+9 -28
View File
@@ -1,34 +1,15 @@
// 工作台:对标 pro 官方 workplace 预览的占位页。
// 统计卡片与快捷入口暂不接真实数据(后期规划),仅布局占位;不引入兜底数据。
import { Card, Col, Empty, Row, Typography } from 'antd';
// 工作台:占位页。数据看板未实现,不渲染假加载骨架,直接以 Empty 说明现状;不引入兜底数据。
import { Card, Empty, Typography } from 'antd';
// 统计卡片与快捷入口暂不接真实数据(后期规划),仅布局占位;不引入兜底数据。
// 页面标题由全局布局的 PageContainer 提供,这里只渲染内容区。
export default function Page() {
return (
<>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card variant="outlined" loading />
</Col>
<Col xs={24} sm={12} lg={6}>
<Card variant="outlined" loading />
</Col>
<Col xs={24} sm={12} lg={6}>
<Card variant="outlined" loading />
</Col>
<Col xs={24} sm={12} lg={6}>
<Card variant="outlined" loading />
</Col>
</Row>
<Card
variant="outlined"
style={{ marginTop: 16 }}
title="运营概况"
extra={<Typography.Text type="secondary">统计指标规划中</Typography.Text>}
>
<Empty description="数据看板规划中,敬请期待。" />
</Card>
</>
<Card
variant="outlined"
title="运营概况"
extra={<Typography.Text type="secondary">统计指标规划中</Typography.Text>}
>
<Empty description="数据看板尚未实现。" />
</Card>
);
}
+7 -20
View File
@@ -1,19 +1,7 @@
// 草稿核对:语义对齐 web.archived DraftPage.jsx(只读快照 / 版本链 / 显式核对与入队 / 关联任务)。
import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import {
Flex,
Alert,
Button,
Card,
Checkbox,
Descriptions,
Modal,
Space,
Tag,
Typography,
message,
} from 'antd';
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';
@@ -37,7 +25,7 @@ export default function Page() {
const [reviewed, setReviewed] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
@@ -112,7 +100,6 @@ export default function Page() {
return (
<div>
{contextHolder}
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography.Text type="secondary" ellipsis>
{draft.account.platform_account_key} · 草稿版本 {draft.version}
@@ -135,12 +122,12 @@ export default function Page() {
}
/>
) : null}
<Flex>
<Flex vertical gap={16}>
<Card title="只读内容快照">
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>{draft.content}</Typography.Paragraph>
</Card>
<Card title="版本与当前资源">
<Flex>
<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>
@@ -151,7 +138,7 @@ export default function Page() {
</Flex>
</Card>
<Card title="版本链">
<Flex>
<Flex vertical gap={4}>
{versions.map((item: any) =>
item.id === draft.id ? (
<Typography.Text key={item.id} strong>
@@ -166,7 +153,7 @@ export default function Page() {
</Flex>
</Card>
<Card title="显式核对与入队">
<Flex>
<Flex vertical gap={12}>
<Checkbox checked={reviewed} onChange={(e) => setReviewed(e.target.checked)}>
我已核对当前账号、草稿内容、运行环境和网络出口
</Checkbox>
@@ -193,7 +180,7 @@ export default function Page() {
{tasks.length === 0 ? (
<Typography.Text type="secondary">尚未入队。</Typography.Text>
) : (
<Flex>
<Flex vertical gap={8}>
{tasks.map((task: any) => (
<Card key={task.id} size="small">
<Typography.Text strong>{task.id}</Typography.Text>
+5 -9
View File
@@ -1,6 +1,6 @@
// 网关管理:语义对齐 web.archived GatewaysPage.jsx(注册/编辑 Modal、令牌显隐复制、删除)。
import { useCallback, useEffect, useState } from 'react';
import { Alert, Button, Card, Flex, Form, Input, Modal, Popconfirm, Space, Table, Typography, message } from 'antd';
import { Alert, App, Button, Card, Flex, Form, Input, Modal, Popconfirm, Space, Table, Typography } from 'antd';
import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { create, getList, remove, update } from '@/services/api';
@@ -15,8 +15,7 @@ function GatewayFormModal({
busy,
error,
onCancel,
onSubmit,
}: {
onSubmit }: {
open: boolean;
initial: any | null;
busy: boolean;
@@ -79,7 +78,7 @@ export default function Page() {
const [editing, setEditing] = useState<any | null>(null);
const [busy, setBusy] = useState('');
const [created, setCreated] = useState<any>(null);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
@@ -148,8 +147,7 @@ export default function Page() {
{
title: '令牌',
dataIndex: 'token',
render: (token: string, gateway) => <TokenCell token={token} name={gateway.name} />,
},
render: (token: string, gateway) => <TokenCell token={token} name={gateway.name} /> },
{
title: '操作',
key: 'actions',
@@ -165,13 +163,11 @@ export default function Page() {
</Button>
</Popconfirm>
</Space>
),
},
) },
];
return (
<Card variant="outlined">
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Typography.Text type="secondary">共 {gateways.length} 个网关</Typography.Text>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
+1 -1
View File
@@ -52,7 +52,7 @@ export default function Page() {
<Button onClick={() => history.push('/network-exits')}>返回出口列表</Button>
</div>
<Card>
<Flex>
<Flex vertical gap={12}>
<Tag color={exit.health_status === 'healthy' ? 'success' : exit.health_status === 'disabled' ? 'default' : exit.health_status === 'unchecked' ? 'warning' : 'error'}>
{known ? healthText[exit.health_status] : `未知状态:${exit.health_status}`}
</Tag>
+8 -15
View File
@@ -1,7 +1,7 @@
// 网络出口:语义对齐 web.archived NetworkExitsPage.jsx(列表 + 创建/编辑 Modal + 检测/启停/删除动作 + 出口详情页)。
import { useCallback, useEffect, useMemo, useState } from 'react';
import { history } from '@umijs/max';
import { Alert, Button, Card, Flex, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message } from 'antd';
import { Alert, App, Button, Card, Flex, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { create, getOne, getList, networkExitAction, remove, update } from '@/services/api';
@@ -11,8 +11,7 @@ const healthText: Record<string, string> = {
unchecked: '未检测',
healthy: '健康',
unhealthy: '不健康',
disabled: '已停用',
};
disabled: '已停用' };
const protocolOptions = ['http', 'https', 'socks4', 'socks5'].map((value) => ({ value, label: value }));
function healthTag(status?: string) {
@@ -39,8 +38,7 @@ function ExitFormModal({
busy,
error,
onCancel,
onSubmit,
}: {
onSubmit }: {
open: boolean;
initial: any | null;
title: string;
@@ -87,8 +85,7 @@ function ExitFormModal({
validator: (_rule, value) =>
!value || getFieldValue('username')
? Promise.resolve()
: Promise.reject(new Error('填写密码时必须同时填写用户名')),
}),
: Promise.reject(new Error('填写密码时必须同时填写用户名')) }),
]}
>
<Input maxLength={255} style={{ width: 160 }} />
@@ -114,7 +111,7 @@ export default function Page() {
const [createOpen, setCreateOpen] = useState(false);
const [editing, setEditing] = useState<any | null>(null);
const [busy, setBusy] = useState('');
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
@@ -225,8 +222,7 @@ export default function Page() {
{`${exit.protocol}://${exit.host}:${exit.port}`}
</Typography.Paragraph>
</div>
),
},
) },
{ title: '状态', dataIndex: 'health_status', render: (value?: string) => healthTag(value) },
{ title: '用户名', dataIndex: 'username', render: (value?: string) => value || '无' },
{ title: '密码', dataIndex: 'password', render: (value?: string) => value || '无' },
@@ -242,8 +238,7 @@ export default function Page() {
<Typography.Text ellipsis style={{ maxWidth: 200, display: 'inline-block' }}>
{accountsByExit.get(exit.id)?.join('、') || '未绑定'}
</Typography.Text>
),
},
) },
{
title: '操作',
key: 'actions',
@@ -273,13 +268,11 @@ export default function Page() {
</Button>
</Popconfirm>
</Space>
),
},
) },
];
return (
<Card variant="outlined">
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Typography.Text type="secondary">共 {exits.length} 个网络出口</Typography.Text>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
+14 -22
View File
@@ -2,7 +2,7 @@
// 四卡改用 Tabs(当前状态 / 确认快照 / 执行关联 / 唯一安全动作)——antd 默认组件的信息组织方式。
import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import { Flex, Alert, Button, Card, Descriptions, Select, Space, Tabs, Tag, Typography, message } from 'antd';
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';
@@ -13,8 +13,7 @@ const stateText: Record<string, string> = {
failed: '失败',
needs_confirmation: '需要人工确认',
policy_hold: '策略暂停',
cancelled: '已取消',
};
cancelled: '已取消' };
const stateColor: Record<string, string> = {
queued: 'default',
executing: 'processing',
@@ -22,8 +21,7 @@ const stateColor: Record<string, string> = {
failed: 'error',
needs_confirmation: 'warning',
policy_hold: 'warning',
cancelled: 'default',
};
cancelled: 'default' };
const holdText: Record<string, string> = {
account_paused: '账号已暂停',
@@ -43,8 +41,7 @@ const holdText: Record<string, string> = {
confirmation_missing: '确认快照缺失',
confirmation_version_changed: '确认版本已变化',
binding_version_changed: '环境或出口版本已变化',
legacy_confirmation_required: '历史任务结果未知,等待人工核验',
};
legacy_confirmation_required: '历史任务结果未知,等待人工核验' };
const holdLabel = (reason?: string) => (reason ? holdText[reason] || `未知停止原因:${reason}` : '无');
function actionErrorText(error: any): string {
@@ -61,7 +58,7 @@ export default function Page() {
const [error, setError] = useState<any>(null);
const [verification, setVerification] = useState('not_executed');
const [busy, setBusy] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
@@ -110,20 +107,19 @@ export default function Page() {
key: 'status',
label: '当前状态',
children: (
<Flex>
<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>
<Flex vertical gap={12}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="确认">
<Typography.Text copyable>{confirmation.id || '未记录'}</Typography.Text> · v{confirmation.version || '—'}
@@ -142,13 +138,12 @@ export default function Page() {
</Button>
</Space>
</Flex>
),
},
) },
{
key: 'execution',
label: '执行关联',
children: (
<Flex>
<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>
@@ -169,13 +164,12 @@ export default function Page() {
) : null}
</Space>
</Flex>
),
},
) },
{
key: 'action',
label: '唯一安全动作',
children: (
<Flex>
<Flex vertical gap={12}>
{task.readiness_reason ? <Alert type="info" showIcon title={`当前仍被阻断:${holdLabel(task.readiness_reason)}。修复原资源并刷新后,才会开放恢复。`} /> : null}
{task.allowed_action === 'verify' ? (
<>
@@ -217,13 +211,11 @@ export default function Page() {
) : null}
{task.allowed_action ? null : <Typography.Text type="secondary">当前没有可安全执行的动作。</Typography.Text>}
</Flex>
),
},
) },
];
return (
<Card variant="outlined">
{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>
@@ -237,7 +229,7 @@ export default function Page() {
<Tabs defaultActiveKey="status" items={items} />
<Typography.Title level={5} style={{ marginTop: 24, marginBottom: 12 }}>执行尝试</Typography.Title>
{task.attempts?.length ? (
<Flex>
<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>
+41 -11
View File
@@ -1,7 +1,7 @@
// 任务中心列表:语义对齐 web.archived TasksPage.jsx 的 TaskList。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import { Alert, Button, Card, Flex, Input, Select, Space, Table, Tag, Typography } from 'antd';
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';
@@ -48,9 +48,11 @@ const holdText: Record<string, string> = {
export const holdLabel = (reason?: string) => (reason ? holdText[reason] || `未知停止原因:${reason}` : '无');
export default function Page() {
const [accountFilter, setAccountFilter] = useState('');
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);
@@ -60,7 +62,7 @@ export default function Page() {
try {
const result = await getList({
resource: 'tasks',
filters: { account_id: accountFilter || undefined, state: stateFilter || undefined },
filters: { account_id: accountFilter, state: stateFilter || undefined },
});
setRows(result.data ?? []);
} catch (loadError) {
@@ -74,16 +76,42 @@ export default function Page() {
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) => <Typography.Link onClick={() => history.push(`/tasks/${value}`)}>{value}</Typography.Link>,
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) => <Typography.Link onClick={() => history.push(`/accounts/${value}`)}>{value}</Typography.Link>,
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 || '未确认' },
{
@@ -115,12 +143,15 @@ export default function Page() {
<Flex justify="space-between" align="center">
<Typography.Text type="secondary">共 {rows.length} 个任务</Typography.Text>
<Space wrap>
<Input
<Select
style={{ width: 280 }}
placeholder="账号 ID"
value={accountFilter}
onChange={(event) => setAccountFilter(event.target.value)}
placeholder="全部账号"
allowClear
showSearch={{ optionFilterProp: 'label' }}
loading={accountsPending && accountOptions.length === 0}
value={accountFilter}
onChange={setAccountFilter}
options={accountOptions}
/>
<Select
style={{ width: 200 }}
@@ -141,8 +172,7 @@ export default function Page() {
dataSource={rows}
loading={pending}
pagination={false}
locale={{ emptyText: accountFilter || stateFilter ? '没有符合筛选条件的任务' : '尚无任务' }}
/>
locale={{ emptyText: accountFilter || stateFilter ? '没有符合筛选条件的任务' : '尚无任务' }} />
</Card>
</div>
);
+1 -2
View File
@@ -26,7 +26,7 @@ const rules: Array<{ test: (path: string) => boolean; title: string; subtitle: s
{ test: (p) => p === '/creator/competitors', title: '竞品分析', subtitle: '查看竞品作品与指标,分享链接导入请前往账号导入。' },
{ test: (p) => p === '/creator/workbench', title: '运营工作台', subtitle: '评论、线索、私信与写操作均保留来源和明确结果。' },
{ test: (p) => p === '/creator/settings', title: '采集设置', subtitle: '统一配置采集窗口、指标采集和已批准的服务。' },
{ test: (p) => p === '/competitors' || p.startsWith('/competitors/'), 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: '逐条处理暂停、版本变化与未知执行结果。' },
@@ -37,7 +37,6 @@ const rules: Array<{ test: (path: string) => boolean; title: string; subtitle: s
{ test: (p) => p === '/network-exits', title: '网络出口', subtitle: '维护固定出口、健康观测与账号绑定。' },
{ test: (p) => p.startsWith('/network-exits/'), title: '网络出口详情', subtitle: '查看出口配置、健康状态和绑定关系。' },
{ test: (p) => p === '/gateways', title: '网关管理', subtitle: '注册并检查宿主机 native browser gateway。' },
{ test: (p) => p.startsWith('/gateways/'), title: '网关详情', subtitle: '查看网关连接信息和运行状态。' },
];
// 菜单结构:工作台置顶(对标 pro 官方 workplace 预览),其余与 web.archived/src/app/Layout.jsx 一致(运营 / 资源 两组);