feat(web): task-3 accounts 模块全量迁移(列表/监控/导入/新建/详情/编辑,antd 默认组件 + 约定式路由 + terser 压缩绕开 esbuild helper 冲突)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user