267 lines
10 KiB
TypeScript
267 lines
10 KiB
TypeScript
// 监控账号列表(monitoring):一行一个账号的 card list,对齐 pro table-list 的卡片列表形态。
|
||
// 展示作者头像与粉丝/关注/作品/最近发布等账号画像属性(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 { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
|
||
import { getList, remove, creatorAction, creatorUpdate, syncCompetitorAuto } from '@/services/api';
|
||
import { conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
|
||
|
||
interface CompetitorRow {
|
||
id: string;
|
||
platform: string;
|
||
platform_account_key: string;
|
||
unique_id?: string;
|
||
nickname: string;
|
||
avatar_url: string;
|
||
homepage_url?: string;
|
||
tags?: string[];
|
||
enabled: boolean;
|
||
follower_count?: number | null;
|
||
following_count?: number | null;
|
||
aweme_count?: number | null;
|
||
sync_status?: string;
|
||
sync_error?: string;
|
||
last_sync_at?: string | null;
|
||
work_count?: number;
|
||
latest_published_at?: string | null;
|
||
}
|
||
|
||
const syncStatusMeta: Record<string, { color?: string; label: string }> = {
|
||
idle: { color: 'success', label: '监控中' },
|
||
running: { color: 'processing', label: '同步中' },
|
||
paused: { label: '已暂停' },
|
||
failed: { color: 'error', label: '同步失败' },
|
||
blocked: { color: 'warning', label: '同步受阻' },
|
||
};
|
||
|
||
function formatCount(value?: number | null): string {
|
||
if (value === null || value === undefined) return '—';
|
||
return value >= 10000 ? `${(value / 10000).toFixed(1)}w` : `${value}`;
|
||
}
|
||
|
||
export default function MonitoringAccountList() {
|
||
const [rows, setRows] = useState<CompetitorRow[]>([]);
|
||
const [pending, setPending] = useState(true);
|
||
const [error, setError] = useState<any>(null);
|
||
const [tagTarget, setTagTarget] = useState<CompetitorRow | 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 result = await getList({ resource: 'creator-competitors' });
|
||
setRows(result.data ?? []);
|
||
} catch (loadError) {
|
||
setError(loadError);
|
||
} finally {
|
||
setPending(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
load();
|
||
}, [load]);
|
||
|
||
async function syncCompetitor(competitor: CompetitorRow) {
|
||
setActionBusy(competitor.id);
|
||
try {
|
||
await syncCompetitorAuto(competitor);
|
||
messageApi.success('监测账号同步任务已提交。');
|
||
await load();
|
||
} catch (actionError) {
|
||
messageApi.error(conflictMessage(actionError, '监测账号同步失败'));
|
||
} finally {
|
||
setActionBusy('');
|
||
}
|
||
}
|
||
|
||
async function toggleCompetitor(competitor: CompetitorRow) {
|
||
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: CompetitorRow) {
|
||
setActionBusy(row.id);
|
||
try {
|
||
await remove('creator-competitors', row.id);
|
||
messageApi.success('监测账号已删除。');
|
||
await load();
|
||
} catch (deleteError) {
|
||
messageApi.error(conflictMessage(deleteError, '监测账号删除失败'));
|
||
} finally {
|
||
setActionBusy('');
|
||
}
|
||
}
|
||
|
||
function openTags(row: CompetitorRow) {
|
||
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 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 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>
|
||
<Button icon={<ReloadOutlined />} onClick={load} disabled={pending} aria-label="刷新" />
|
||
<Button type="primary" icon={<CloudUploadOutlined />} onClick={() => history.push('/accounts/import')}>
|
||
导入竞品账号
|
||
</Button>
|
||
</Space>
|
||
</Flex>
|
||
{error ? (
|
||
<Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={load}>重试</Button>} />
|
||
) : null}
|
||
<Listy
|
||
rowKey={(row) => row.id}
|
||
items={rows}
|
||
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}`)}
|
||
>
|
||
<Flex justify="space-between" align="flex-start" gap={16}>
|
||
<Flex align="flex-start" gap={12}>
|
||
<Avatar size={44} src={row.avatar_url || undefined}>
|
||
{(row.nickname || row.platform_account_key).slice(0, 1).toUpperCase()}
|
||
</Avatar>
|
||
<div>
|
||
<Space wrap size={8}>
|
||
<Typography.Text strong>{row.nickname || row.platform_account_key}</Typography.Text>
|
||
<Tag>{platformLabel(row.platform)}</Tag>
|
||
<Tag color={meta.color}>{meta.label}</Tag>
|
||
{(row.tags ?? []).map((tag) => (
|
||
<Tag key={tag}>{tag}</Tag>
|
||
))}
|
||
</Space>
|
||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }} ellipsis>
|
||
{row.platform_account_key}
|
||
</Typography.Paragraph>
|
||
</div>
|
||
</Flex>
|
||
<Space wrap onClick={(e) => e.stopPropagation()}>
|
||
<Button size="small" onClick={() => openTags(row)}>
|
||
编辑标签
|
||
</Button>
|
||
<Button size="small" disabled={actionBusy === row.id} onClick={() => syncCompetitor(row)}>
|
||
同步
|
||
</Button>
|
||
<Button size="small" disabled={actionBusy === row.id} onClick={() => toggleCompetitor(row)}>
|
||
{row.enabled ? '暂停' : '恢复'}
|
||
</Button>
|
||
<Popconfirm
|
||
title={`确定删除监测账号“${row.nickname || row.platform_account_key}”吗?相关专属资源也会被清理。`}
|
||
onConfirm={() => deleteAccount(row)}
|
||
disabled={actionBusy === row.id}
|
||
>
|
||
<Button size="small" danger disabled={actionBusy === row.id}>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
</Flex>
|
||
<Descriptions
|
||
style={{ marginTop: 12 }}
|
||
size="small"
|
||
column={{ xs: 2, sm: 3, md: 6 }}
|
||
items={[
|
||
{ key: 'follower', label: '粉丝', children: formatCount(row.follower_count) },
|
||
{ key: 'following', label: '关注', children: formatCount(row.following_count) },
|
||
{ key: 'works', label: '作品', children: formatCount(row.work_count ?? row.aweme_count ?? null) },
|
||
{ key: 'published', label: '最近发布', children: row.latest_published_at ? dateTime(row.latest_published_at) : '—' },
|
||
{ key: 'lastSync', label: '上次同步', children: row.last_sync_at ? dateTime(row.last_sync_at) : '—' },
|
||
{
|
||
key: 'syncError',
|
||
label: '同步异常',
|
||
children: row.sync_error ? (
|
||
<Typography.Text type="danger" style={{ fontSize: 12 }}>{row.sync_error}</Typography.Text>
|
||
) : (
|
||
'—'
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</div>
|
||
);
|
||
}}
|
||
/>
|
||
{pending ? (
|
||
<div style={{ textAlign: 'center', padding: '32px 0' }}>
|
||
<Spin />
|
||
</div>
|
||
) : null}
|
||
{!pending && !error && rows.length === 0 ? <Empty description="暂无监控账号。" /> : null}
|
||
<Modal
|
||
open={Boolean(tagTarget)}
|
||
title={`编辑标签 · ${tagTarget?.nickname || tagTarget?.platform_account_key || '账号'}`}
|
||
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>
|
||
</Card>
|
||
);
|
||
}
|