diff --git a/web/src/components/accounts/AccountManagementList.tsx b/web/src/components/accounts/AccountManagementList.tsx index 5e9b423..ad09396 100644 --- a/web/src/components/accounts/AccountManagementList.tsx +++ b/web/src/components/accounts/AccountManagementList.tsx @@ -1,5 +1,6 @@ // 我的账号列表(owned):语义对齐 web.archived AccountManagementList(mode="owned")。 // 监控账号列表已拆分至 components/accounts/MonitoringAccountList.tsx(card list)。 +// 数据走 GET /creator/accounts/monitor-views(画像 + 作品数/最近发布聚合),对齐竞品列表画像形态。 import { useCallback, useEffect, useState } from 'react'; import { history } from '@umijs/max'; import { @@ -19,8 +20,8 @@ import { } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; -import { getList, remove, creatorUpdate } from '@/services/api'; -import { conflictMessage } from '@/utils/helpers'; +import { remove, creatorUpdate, creatorGet } from '@/services/api'; +import { conflictMessage, dateTime } from '@/utils/helpers'; import { usePageActions } from '@/components/PageActions'; // 仅用 antd 默认组件:Table/Tag/Modal/Popconfirm/Select(tags)。 @@ -32,6 +33,8 @@ interface Row { tags?: string[]; runtime_status?: string; authorization_status?: string; + work_count?: number; + latest_published_at?: string | null; } export default function AccountManagementList() { @@ -48,9 +51,9 @@ export default function AccountManagementList() { setPending(true); setError(null); try { - const result = await getList({ resource: 'accounts' }); + const rows = await creatorGet('/creator/accounts/monitor-views'); setRows( - (result.data ?? []).map((account: any) => ({ + (rows ?? []).map((account: any) => ({ ...account, name: account.name, tags: account.tags ?? [], @@ -115,6 +118,18 @@ export default function AccountManagementList() { ), }, { title: '平台', dataIndex: 'platform', render: () => '抖音' }, + { + title: '作品数据', + dataIndex: 'work_count', + render: (_, account) => ( + + {account.work_count ?? 0} 个作品 + + 最近发布 {account.latest_published_at ? dateTime(account.latest_published_at) : '—'} + + + ), + }, { title: '标签', dataIndex: 'tags', diff --git a/web/src/pages/accounts/$id/index.tsx b/web/src/pages/accounts/$id/index.tsx index c8f1aa9..c09cef6 100644 --- a/web/src/pages/accounts/$id/index.tsx +++ b/web/src/pages/accounts/$id/index.tsx @@ -1,8 +1,11 @@ // 账号详情:样式对齐 Pro Advanced Profile(卡片流)—— -// 主体为三张带标题 Card 纵向堆叠(账号状态 / 固定资源 / 文本草稿); +// 主体为带标题 Card 纵向堆叠(账号状态含采集状态 / 账号数据粉丝趋势 / 固定资源 / 作品数据 / 文本草稿); // 实体标识(账号名 + 状态 Tag + 平台明细)经 usePageInfo 进页头 content 行左侧,操作按钮在页头右侧。 -import { useCallback, useEffect, useState } from 'react'; +// 数据监控:采集状态走 GET /creator/accounts/:id/collection-status,粉丝时序走 :id/metrics, +// 作品网格走共享 WorksGallery(source_type='owned'),强制同步直调 POST /creator/accounts/:id/sync。 +import { useCallback, useEffect, useMemo, useState } from 'react'; import { history, useParams } from '@umijs/max'; +import { Line } from '@ant-design/charts'; import { Flex, Alert, @@ -17,9 +20,60 @@ import { Typography, message, } from 'antd'; -import { accountAction, createDraft, getOne, getList } from '@/services/api'; -import { accountReadiness, conflictMessage, platformLabel } from '@/utils/helpers'; +import { accountAction, createDraft, getOne, getList, creatorGet, creatorAction } from '@/services/api'; +import { accountReadiness, conflictMessage, dateTime, platformLabel } from '@/utils/helpers'; import { usePageActions, usePageInfo } from '@/components/PageActions'; +import WorksGallery from '@/components/creator/WorksGallery'; + +// 采集状态映射:checkpoint 形态(pending=尚未采集过;其余与竞品 sync_status 语义一致)。 +const collectionStatusMeta: Record = { + pending: { label: '待采集' }, + succeeded: { color: 'success', label: '采集中' }, + running: { color: 'processing', label: '采集运行中' }, + failed: { color: 'error', label: '采集失败' }, + blocked: { color: 'warning', label: '采集受阻' }, +}; + +// 粉丝趋势:把账号画像时序展开成长表(时间 × 指标 × 数值)供折线图渲染。 +const accountSeriesMeta = [ + { key: 'follower_count', label: '粉丝' }, + { key: 'following_count', label: '关注' }, + { key: 'total_favorited', label: '获赞' }, + { key: 'aweme_count', label: '作品总数' }, +] as const; + +function formatCount(value?: number | null): string { + if (value === null || value === undefined) return '—'; + return value >= 10000 ? `${(value / 10000).toFixed(1)}w` : `${value}`; +} + +function AccountTrendChart({ metrics }: { metrics: any[] }) { + const data = useMemo( + () => + metrics.flatMap((point) => + accountSeriesMeta + .filter((series) => point[series.key] != null) + .map((series) => ({ + time: dateTime(point.collected_at), + metric: series.label, + value: point[series.key], + })), + ), + [metrics], + ); + if (!data.length) return null; + return ( + (v >= 10000 ? `${(v / 10000).toFixed(1)}w` : `${v}`) }} + legend={{ position: 'top' }} + /> + ); +} export default function Page() { const { id = '' } = useParams<{ id: string }>(); @@ -33,6 +87,8 @@ export default function Page() { const [actionBusy, setActionBusy] = useState(false); const [draftForm] = Form.useForm<{ content: string }>(); const [draftBusy, setDraftBusy] = useState(false); + const [collectionStatus, setCollectionStatus] = useState(null); + const [accountMetrics, setAccountMetrics] = useState([]); const [messageApi, contextHolder] = message.useMessage(); const readiness = account ? accountReadiness(account, binding, browsersError) : null; @@ -62,17 +118,6 @@ export default function Page() { [account?.id, account?.name, account?.runtime_status, binding, browsersError], ); - // 页头 content 行右侧:返回/编辑注册到 Layout 页头槽,不在卡片内重复渲染。 - usePageActions( - account ? ( - - - - - ) : null, - [account?.id], - ); - const loadAll = useCallback(async () => { setPending(true); setError(null); @@ -100,6 +145,17 @@ export default function Page() { } catch { setDrafts([]); } + // 监控数据:采集失败不阻塞页面主体,采集状态为空时按待采集展示。 + try { + setCollectionStatus(await creatorGet(`/creator/accounts/${encodeURIComponent(id)}/collection-status`)); + } catch { + setCollectionStatus(null); + } + try { + setAccountMetrics((await creatorGet(`/creator/accounts/${encodeURIComponent(id)}/metrics`)) || []); + } catch { + setAccountMetrics([]); + } } finally { setPending(false); } @@ -109,6 +165,35 @@ export default function Page() { loadAll(); }, [loadAll]); + // 页头 content 行右侧:返回/强制同步/编辑注册到 Layout 页头槽,不在卡片内重复渲染。 + usePageActions( + account ? ( + + + + + + ) : null, + [account?.id], + ); + async function runAction(action: 'pause' | 'resume') { setActionBusy(true); try { @@ -143,6 +228,10 @@ export default function Page() { if (pending) return ; if (error || !account) return ; + const worksCollection = collectionStatus?.works; + const worksMeta = collectionStatusMeta[worksCollection?.status ?? 'pending'] || { label: worksCollection?.status || '待采集' }; + const latestMetric = accountMetrics.length ? accountMetrics[accountMetrics.length - 1] : null; + // 账号状态卡局部操作:暂停/恢复留在状态卡右上角(Card extra),不进页头槽。 const pauseResume = ( @@ -164,10 +253,46 @@ export default function Page() { {account.authorization_status === 'authorized' ? '已授权' : '已撤销'} {`${account.runtime_status === 'active' ? '启用' : '暂停'} · 版本 ${account.version}`} {account.tags?.join('、') || '无'} + + + {worksMeta.label} + {worksCollection?.last_completed_at ? ( + + 上次完成 {dateTime(worksCollection.last_completed_at)} + + ) : null} + {worksCollection?.next_window_end ? ( + + 下次窗口至 {dateTime(worksCollection.next_window_end)} + + ) : null} + + {worksCollection?.last_error ? ( + {worksCollection.last_error} + ) : null} + + + + + {formatCount(latestMetric?.follower_count)} + {formatCount(latestMetric?.following_count)} + {formatCount(latestMetric?.total_favorited)} + {formatCount(latestMetric?.aweme_count)} + + {accountMetrics.length >= 2 ? ( + + ) : ( + + {accountMetrics.length === 1 ? '已记录首个数据快照;随自动采集积累后展示趋势曲线。' : '数据随自动采集积累;账号登录且运行环境就绪后每轮采集记一次快照。'} + + )} + + + {browsersError ? ( 仅支持单用户 Phase A Mock 文本;内部 ID 与版本由系统生成。
- - + + - - {drafts.map((draft) => ( - - -
- 草稿版本 {draft.version} - - {draft.content} - -
- history.push(`/drafts/${draft.id}`)} style={{ flexShrink: 0 }}> - 核对草稿 - -
-
- ))} + {drafts.length ? ( + + {drafts.map((draft: any) => ( + + {draft.content} + v{draft.version} + + ))} + + ) : null}
setPauseOpen(false)} - onOk={() => { - setPauseOpen(false); - runAction('pause'); - }} okText="确认暂停" okButtonProps={{ danger: true }} + cancelText="取消" + onOk={async () => { + setPauseOpen(false); + await runAction('pause'); + }} + onCancel={() => setPauseOpen(false)} > -

{`暂停账号 ${account.name}?待领取任务将进入 hold,恢复账号不会自动恢复既有 hold。`}

+ + 暂停后运行环境不再调度该账号;确认继续? +
);