Merge branch 'fix/ui-review-20260927'——UI 走查问题全量修复:详情页 Flex vertical、工作台假加载、列表按钮规范、App.useApp 统一、任务筛选下拉、竞品素材 Drawer
douyin-release-gate / verify (push) Failing after 3m9s
douyin-release-gate / verify (push) Failing after 3m9s
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 一致(运营 / 资源 两组);
|
||||
|
||||
Reference in New Issue
Block a user