fix(web): restore monitoring account table without platform account key

This commit is contained in:
2026-09-28 12:44:38 +08:00
parent b625562c9a
commit ae7dec7e88
3 changed files with 102 additions and 98 deletions
@@ -1,20 +1,16 @@
// 监控账号列表(monitoring):一行一个账号的 card list,对齐 pro table-list 的卡片列表形态。
// 展示作者头像与粉丝/关注/作品/最近发布等账号画像属性(migration 038 字段 + work 聚合)。
// 监控账号列表:表格呈现账号画像、作品数据与同步状态。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import { Alert, App, Avatar, Button, Card, Descriptions, Empty, Flex, Form, Listy, Modal, Popconfirm, Select, Space, Spin, Tag, Typography } from 'antd';
import { Alert, App, Avatar, Button, Card, Flex, Form, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
import { getList, remove, creatorAction, creatorUpdate } from '@/services/api';
import { conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
import { conflictMessage, dateTime } 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;
@@ -129,7 +125,78 @@ export default function MonitoringAccountList() {
}
}
const syncMeta = syncStatusMeta[tagTarget?.sync_status ?? ''] || { label: tagTarget?.sync_status || '—' };
const columns: ColumnsType<CompetitorRow> = [
{
title: '账号',
dataIndex: 'nickname',
render: (_, account) => (
<Flex align="center" gap={8}>
<Avatar src={account.avatar_url || undefined}>{(account.nickname || '账').slice(0, 1).toUpperCase()}</Avatar>
<Typography.Link onClick={() => history.push(`/accounts/monitoring/${account.id}`)}>
{account.nickname || '未命名账号'}
</Typography.Link>
</Flex>
),
},
{
title: '账号数据',
key: 'stats',
render: (_, account) => (
<Space wrap>
<Typography.Text type="secondary">粉丝 {formatCount(account.follower_count)}</Typography.Text>
<Typography.Text type="secondary">关注 {formatCount(account.following_count)}</Typography.Text>
<Typography.Text type="secondary">作品 {formatCount(account.work_count ?? account.aweme_count ?? null)}</Typography.Text>
</Space>
),
},
{
title: '最近发布',
dataIndex: 'latest_published_at',
render: (_, account) => account.latest_published_at ? dateTime(account.latest_published_at) : '—',
},
{
title: '同步状态',
key: 'sync',
render: (_, account) => {
const meta = syncStatusMeta[account.sync_status ?? ''] || { label: account.sync_status || '—' };
return (
<Flex vertical>
<div><Tag color={meta.color}>{meta.label}</Tag></div>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
上次同步 {account.last_sync_at ? dateTime(account.last_sync_at) : '—'}
</Typography.Text>
{account.sync_error ? <Typography.Text type="danger" style={{ fontSize: 12 }}>{account.sync_error}</Typography.Text> : null}
</Flex>
);
},
},
{
title: '标签',
dataIndex: 'tags',
render: (tags?: string[]) => tags?.length ? tags.map((tag) => <Tag key={tag}>{tag}</Tag>) : <Typography.Text type="secondary">未设置</Typography.Text>,
},
{
title: '操作',
key: 'actions',
align: 'right',
render: (_, account) => (
<Space wrap>
<Button size="small" onClick={() => openTags(account)}>编辑标签</Button>
<Button size="small" loading={actionBusy === account.id} disabled={actionBusy === account.id} onClick={() => syncCompetitor(account)}>强制同步</Button>
<Button size="small" disabled={actionBusy === account.id} onClick={() => toggleCompetitor(account)}>
{account.enabled ? '暂停' : '恢复'}
</Button>
<Popconfirm
title={`确定删除监测账号“${account.nickname || '未命名账号'}”吗?相关专属资源也会被清理。`}
onConfirm={() => deleteAccount(account)}
disabled={actionBusy === account.id}
>
<Button size="small" danger disabled={actionBusy === account.id}>删除</Button>
</Popconfirm>
</Space>
),
},
];
return (
<Card variant="outlined">
@@ -145,86 +212,18 @@ export default function MonitoringAccountList() {
{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={{ 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" loading={actionBusy === row.id} 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>
);
}}
<Table
rowKey="id"
columns={columns}
dataSource={rows}
loading={pending}
pagination={false}
locale={{ emptyText: '暂无监控账号。' }}
scroll={{ x: 1100 }}
/>
{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 || '账号'}`}
title={`编辑标签 · ${tagTarget?.nickname || '账号'}`}
onCancel={() => {
if (!tagBusy) setTagTarget(null);
}}
+1 -1
View File
@@ -1,4 +1,4 @@
// 监控账号列表:一行一个账号的 card list(含头像与粉丝/关注/作品/最近发布画像)。
// 监控账号列表:表格展示账号画像与同步状态。
import MonitoringAccountList from '@/components/accounts/MonitoringAccountList';
export default function Page() {
+16 -11
View File
@@ -10,17 +10,22 @@ const source = ts.createSourceFile(
true,
ts.ScriptKind.TSX,
);
const code = source.getFullText();
test('监控账号行由 Listy 独占内边距和分隔线,保留详情入口', () => {
let itemRender;
function visit(node) {
if (ts.isJsxAttribute(node) && node.name.getText(source) === 'itemRender') itemRender = node;
ts.forEachChild(node, visit);
test('监控账号列表使用 Table,一行一个账号,保留详情入口和操作', () => {
assert.match(code, /<Table\b/);
assert.match(code, /rowKey="id"/);
assert.match(code, /loading=\{pending\}/);
assert.match(code, /history\.push\(`\/accounts\/monitoring\/\$\{account\.id\}`\)/);
for (const action of ['openTags(account)', 'syncCompetitor(account)', 'toggleCompetitor(account)', 'deleteAccount(account)']) {
assert.ok(code.includes(action), `${action} 应留在表格行操作中`);
}
assert.doesNotMatch(code, /<Listy\b/);
});
test('列表不展示冗长的平台账号,保留画像统计及同步异常', () => {
assert.doesNotMatch(code, /row\.platform_account_key|account\.platform_account_key/);
for (const field of ['follower_count', 'following_count', 'work_count', 'latest_published_at', 'last_sync_at', 'sync_error']) {
assert.ok(code.includes(`account.${field}`), `${field} 应保留在表格中`);
}
visit(source);
assert.ok(itemRender, '应使用 Listy itemRender');
const row = itemRender.getText(source);
assert.match(row, /history\.push\(`\/accounts\/monitoring\/\$\{row\.id\}`\)/);
assert.match(row, /<div style=\{\{ cursor: 'pointer' \}\}/);
assert.doesNotMatch(row, /padding|borderBottom|rowHoverStyle/);
});