feat(web): task-3 accounts 模块全量迁移(列表/监控/导入/新建/详情/编辑,antd 默认组件 + 约定式路由 + terser 压缩绕开 esbuild helper 冲突)

This commit is contained in:
2026-09-22 12:40:09 +08:00
parent 542eac9e0a
commit 04ac91874a
22 changed files with 1551 additions and 133 deletions
@@ -0,0 +1,271 @@
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import {
Alert,
Button,
Form,
Input,
Modal,
Popconfirm,
Select,
Space,
Table,
Tag,
Typography,
message,
} from 'antd';
import { PlusOutlined, CloudUploadOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { getList, remove, creatorAction, creatorUpdate } from '@/services/api';
import { conflictMessage, isCollectionAccount, platformLabel } from '@/utils/helpers';
// 语义对齐 web.archived/src/features/accounts/AccountsPage.jsx 的 AccountManagementList(mode: owned/monitoring)。
// 仅用 antd 默认组件:Table/Tag/Modal/Popconfirm/Select(tags)。
interface Row {
id: string;
name: string;
platform: string;
platform_account_key: string;
tags?: string[];
source_type: 'owned' | 'monitoring';
runtime_status?: string;
authorization_status?: string;
enabled?: boolean;
nickname?: string;
}
export default function AccountManagementList({ mode }: { mode: 'owned' | 'monitoring' }) {
const isMonitoring = mode === 'monitoring';
const [rows, setRows] = useState<Row[]>([]);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [tagTarget, setTagTarget] = useState<Row | null>(null);
const [tagForm] = Form.useForm<{ tags: string[] }>();
const [tagBusy, setTagBusy] = useState(false);
const [actionBusy, setActionBusy] = useState('');
const [messageApi, contextHolder] = message.useMessage();
const load = useCallback(async () => {
setPending(true);
setError(null);
try {
const resource = isMonitoring ? 'creator-competitors' : 'accounts';
const result = await getList({ resource: resource as any });
setRows(
(result.data ?? []).map((account: any) => ({
...account,
name: isMonitoring ? account.nickname || account.platform_account_key : account.name,
source_type: isMonitoring ? 'monitoring' : 'owned',
tags: account.tags ?? [],
})),
);
} catch (loadError) {
setError(loadError);
} finally {
setPending(false);
}
}, [isMonitoring]);
useEffect(() => {
load();
}, [load]);
async function syncCompetitor(competitor: Row) {
setActionBusy(competitor.id);
try {
const result = await getList({ resource: 'creator-accounts' });
const account = (result.data ?? []).find(
(candidate: any) => candidate.platform === competitor.platform && isCollectionAccount(candidate),
);
if (!account) {
throw new Error(`没有可用于同步${platformLabel(competitor.platform)}监控账号的已登录自有账号。`);
}
await creatorAction(`/creator/competitors/${encodeURIComponent(competitor.id)}/sync`, { account_id: account.id });
messageApi.success('监测账号同步任务已提交。');
await load();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '监测账号同步失败'));
} finally {
setActionBusy('');
}
}
async function toggleCompetitor(competitor: Row) {
setActionBusy(competitor.id);
try {
await creatorAction(
`/creator/competitors/${encodeURIComponent(competitor.id)}/${competitor.enabled ? 'pause' : 'resume'}`,
);
messageApi.success(competitor.enabled ? '监测账号已暂停。' : '监测账号已恢复。');
await load();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '监测账号状态更新失败'));
} finally {
setActionBusy('');
}
}
async function deleteAccount(row: Row) {
const label = row.source_type === 'owned' ? '自有账号' : '监测账号';
setActionBusy(`${row.source_type}:${row.id}`);
try {
await remove(row.source_type === 'owned' ? 'accounts' : 'creator-competitors', row.id);
messageApi.success(`${label}已删除。`);
await load();
} catch (deleteError) {
messageApi.error(conflictMessage(deleteError, `${label}删除失败`));
} finally {
setActionBusy('');
}
}
function openTags(row: Row) {
setTagTarget(row);
tagForm.setFieldsValue({ tags: Array.isArray(row.tags) ? row.tags : [] });
}
async function saveTags() {
if (!tagTarget) return;
const { tags } = await tagForm.validateFields();
setTagBusy(true);
try {
await (tagTarget.source_type === 'owned'
? creatorUpdate(`/creator/accounts/${encodeURIComponent(tagTarget.id)}/tags`, { tags })
: creatorUpdate(`/creator/competitors/${encodeURIComponent(tagTarget.id)}`, { tags }));
setRows((current) => current.map((row) => (row.id === tagTarget.id ? { ...row, tags } : row)));
setTagTarget(null);
messageApi.success('标签已保存。');
} catch (saveError) {
messageApi.error(conflictMessage(saveError, '标签保存失败'));
} finally {
setTagBusy(false);
}
}
const columns: ColumnsType<Row> = [
{
title: '账号',
dataIndex: 'name',
render: (_, account) => (
<div>
{account.source_type === 'owned' ? (
<Typography.Link onClick={() => history.push(`/accounts/${account.id}`)}>{account.name}</Typography.Link>
) : (
<span>{account.name}</span>
)}
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }} ellipsis>
{account.platform_account_key}
</Typography.Paragraph>
</div>
),
},
{ title: '平台', dataIndex: 'platform', render: (value: string) => platformLabel(value) },
{
title: '标签',
dataIndex: 'tags',
render: (tags?: string[]) => (tags?.length ? tags.map((tag) => <Tag key={tag}>{tag}</Tag>) : <Typography.Text type="secondary">未设置</Typography.Text>),
},
{
title: '状态',
dataIndex: 'status',
render: (_, account) =>
account.source_type === 'owned' ? (
<Space direction="vertical" size={0}>
<Tag color={account.runtime_status === 'active' ? 'success' : 'default'}>
{account.runtime_status === 'active' ? '启用' : '暂停'}
</Tag>
<Typography.Text type="secondary">{account.authorization_status === 'authorized' ? '已授权' : '已撤销'}</Typography.Text>
</Space>
) : (
<Tag color={account.enabled ? 'success' : 'default'}>{account.enabled ? '监控中' : '已暂停'}</Tag>
),
},
{
title: '操作',
key: 'actions',
align: 'right',
render: (_, account) => (
<Space wrap>
<Button size="small" onClick={() => openTags(account)}>
编辑标签
</Button>
{account.source_type === 'owned' ? (
<>
<Button size="small" onClick={() => history.push(`/accounts/${account.id}/edit`)}>
编辑账号
</Button>
<Button size="small" onClick={() => history.push(`/accounts/${account.id}`)}>
查看账号
</Button>
</>
) : (
<>
<Button size="small" disabled={actionBusy === account.id} onClick={() => syncCompetitor(account)}>
同步
</Button>
<Button size="small" disabled={actionBusy === account.id} onClick={() => toggleCompetitor(account)}>
{account.enabled ? '暂停' : '恢复'}
</Button>
</>
)}
<Popconfirm
title={`确定删除${account.source_type === 'owned' ? '自有账号' : '监测账号'}“${account.name}”吗?相关专属资源也会被清理。`}
onConfirm={() => deleteAccount(account)}
disabled={actionBusy === `${account.source_type}:${account.id}`}
>
<Button size="small" danger disabled={actionBusy === `${account.source_type}:${account.id}`}>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
{contextHolder}
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography.Text type="secondary">
共 {rows.length} 个{isMonitoring ? '监控' : '自有'}账号
</Typography.Text>
{isMonitoring ? (
<Button type="primary" icon={<CloudUploadOutlined />} onClick={() => history.push('/accounts/import')}>
导入竞品账号
</Button>
) : (
<Button type="primary" icon={<PlusOutlined />} onClick={() => history.push('/accounts/new')}>
创建账号
</Button>
)}
</div>
{error ? (
<Alert type="error" showIcon style={{ marginBottom: 16 }} message={error.message} action={<Button size="small" onClick={load}>重试</Button>} />
) : null}
<Table
rowKey={(row) => `${row.source_type}:${row.id}`}
columns={columns}
dataSource={rows}
loading={pending}
pagination={false}
locale={{ emptyText: isMonitoring ? '暂无监控账号。' : '暂无自有账号。' }}
/>
<Modal
open={Boolean(tagTarget)}
title={`编辑标签 · ${tagTarget?.name || '账号'}`}
onCancel={() => {
if (!tagBusy) setTagTarget(null);
}}
onOk={saveTags}
confirmLoading={tagBusy}
okText={tagBusy ? '保存中…' : '保存标签'}
>
<Form form={tagForm} layout="vertical">
<Form.Item name="tags" label="标签" extra="回车或逗号确认,最多 20 个标签,每个最多 32 个字符。">
<Select mode="tags" tokenSeparators={[',', ',']} placeholder="如:主账号" open={false} />
</Form.Item>
</Form>
</Modal>
</div>
);
}