diff --git a/web/src/components/accounts/MonitoringAccountList.tsx b/web/src/components/accounts/MonitoringAccountList.tsx index b12e1b1..c0b5a8e 100644 --- a/web/src/components/accounts/MonitoringAccountList.tsx +++ b/web/src/components/accounts/MonitoringAccountList.tsx @@ -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 = [ + { + title: '账号', + dataIndex: 'nickname', + render: (_, account) => ( + + {(account.nickname || '账').slice(0, 1).toUpperCase()} + history.push(`/accounts/monitoring/${account.id}`)}> + {account.nickname || '未命名账号'} + + + ), + }, + { + title: '账号数据', + key: 'stats', + render: (_, account) => ( + + 粉丝 {formatCount(account.follower_count)} + 关注 {formatCount(account.following_count)} + 作品 {formatCount(account.work_count ?? account.aweme_count ?? null)} + + ), + }, + { + 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 ( + +
{meta.label}
+ + 上次同步 {account.last_sync_at ? dateTime(account.last_sync_at) : '—'} + + {account.sync_error ? {account.sync_error} : null} +
+ ); + }, + }, + { + title: '标签', + dataIndex: 'tags', + render: (tags?: string[]) => tags?.length ? tags.map((tag) => {tag}) : 未设置, + }, + { + title: '操作', + key: 'actions', + align: 'right', + render: (_, account) => ( + + + + + deleteAccount(account)} + disabled={actionBusy === account.id} + > + + + + ), + }, + ]; return ( @@ -145,86 +212,18 @@ export default function MonitoringAccountList() { {error ? ( 重试} /> ) : null} - row.id} - items={rows} - itemRender={(row) => { - const meta = syncStatusMeta[row.sync_status ?? ''] || { label: row.sync_status || '—' }; - return ( -
history.push(`/accounts/monitoring/${row.id}`)}> - - - - {(row.nickname || row.platform_account_key).slice(0, 1).toUpperCase()} - -
- - {row.nickname || row.platform_account_key} - {platformLabel(row.platform)} - {meta.label} - {(row.tags ?? []).map((tag) => ( - {tag} - ))} - - - {row.platform_account_key} - -
-
- e.stopPropagation()}> - - - - deleteAccount(row)} - disabled={actionBusy === row.id} - > - - - -
- {row.sync_error} - ) : ( - '—' - ) }, - ]} - /> -
- ); - }} + - {pending ? ( -
- -
- ) : null} - {!pending && !error && rows.length === 0 ? : null} { if (!tagBusy) setTagTarget(null); }} diff --git a/web/src/pages/accounts/monitoring/index.tsx b/web/src/pages/accounts/monitoring/index.tsx index 0806767..9b6b2d5 100644 --- a/web/src/pages/accounts/monitoring/index.tsx +++ b/web/src/pages/accounts/monitoring/index.tsx @@ -1,4 +1,4 @@ -// 监控账号列表:一行一个账号的 card list(含头像与粉丝/关注/作品/最近发布画像)。 +// 监控账号列表:表格展示账号画像与同步状态。 import MonitoringAccountList from '@/components/accounts/MonitoringAccountList'; export default function Page() { diff --git a/web/tests/monitoring-account-list.test.cjs b/web/tests/monitoring-account-list.test.cjs index 8808453..39f1b0b 100644 --- a/web/tests/monitoring-account-list.test.cjs +++ b/web/tests/monitoring-account-list.test.cjs @@ -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, / { + 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, /