feat(web): 自有账号监控接入——详情页账号数据/作品数据/采集状态/强制同步,列表作品统计列

- 账号详情页:账号状态卡加作品采集行(状态 Tag + 上次完成 + 下次窗口 + 最后错误);新增账号数据 Card(粉丝/关注/获赞/作品总数当前值 + 趋势折线);接入共享 WorksGallery(source_type='owned');页头槽加强制同步按钮
- 我的账号列表:改走 monitor-views 聚合接口,加作品数据列(作品数/最近发布)
This commit is contained in:
2026-09-27 22:11:02 +08:00
parent 127c3e70ee
commit fb1db68f13
2 changed files with 181 additions and 52 deletions
@@ -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) => (
<Flex vertical>
<Typography.Text>{account.work_count ?? 0} 个作品</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
最近发布 {account.latest_published_at ? dateTime(account.latest_published_at) : '—'}
</Typography.Text>
</Flex>
),
},
{
title: '标签',
dataIndex: 'tags',
+162 -48
View File
@@ -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<string, { color?: string; label: string }> = {
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 (
<Line
data={data}
xField="time"
yField="value"
seriesField="metric"
height={240}
yAxis={{ labelFormatter: (v: number) => (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<any>(null);
const [accountMetrics, setAccountMetrics] = useState<any[]>([]);
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 ? (
<Space>
<Button onClick={() => history.push('/accounts')}>返回社媒账号</Button>
<Button type="primary" onClick={() => history.push(`/accounts/${account.id}/edit`)}>编辑账号</Button>
</Space>
) : 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 ? (
<Space>
<Button onClick={() => history.push('/accounts')}>返回社媒账号</Button>
<Button
disabled={actionBusy}
onClick={async () => {
// 强制同步:后端复用账号自身运行环境采集,需已登录+环境就绪(否则 409/503 提示原因)。
setActionBusy(true);
try {
await creatorAction(`/creator/accounts/${encodeURIComponent(account.id)}/sync`);
messageApi.success('强制同步已执行。');
await loadAll();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '强制同步失败;账号需已登录且运行环境就绪。'));
} finally {
setActionBusy(false);
}
}}
>
强制同步
</Button>
<Button type="primary" onClick={() => history.push(`/accounts/${account.id}/edit`)}>编辑账号</Button>
</Space>
) : null,
[account?.id],
);
async function runAction(action: 'pause' | 'resume') {
setActionBusy(true);
try {
@@ -143,6 +228,10 @@ export default function Page() {
if (pending) return <Card loading />;
if (error || !account) return <Alert type="error" showIcon title={error?.message || '账号不存在'} />;
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 = (
<Space wrap>
@@ -164,10 +253,46 @@ export default function Page() {
<Descriptions.Item label="授权">{account.authorization_status === 'authorized' ? '已授权' : '已撤销'}</Descriptions.Item>
<Descriptions.Item label="运行">{`${account.runtime_status === 'active' ? '启用' : '暂停'} · 版本 ${account.version}`}</Descriptions.Item>
<Descriptions.Item label="标签">{account.tags?.join('、') || '无'}</Descriptions.Item>
<Descriptions.Item label="作品采集">
<Flex align="center" gap={8} wrap="wrap">
<Tag color={worksMeta.color}>{worksMeta.label}</Tag>
{worksCollection?.last_completed_at ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
上次完成 {dateTime(worksCollection.last_completed_at)}
</Typography.Text>
) : null}
{worksCollection?.next_window_end ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
下次窗口至 {dateTime(worksCollection.next_window_end)}
</Typography.Text>
) : null}
</Flex>
{worksCollection?.last_error ? (
<Typography.Text type="danger" style={{ fontSize: 12 }}>{worksCollection.last_error}</Typography.Text>
) : null}
</Descriptions.Item>
</Descriptions>
<Alert type="info" showIcon title="暂停会把待领取任务置为 hold;恢复账号只恢复账号可用性,不会自动恢复既有 hold。" />
</Flex>
</Card>
<Card title="账号数据">
<Flex vertical gap={12}>
<Descriptions column={{ xs: 2, sm: 4 }} size="small">
<Descriptions.Item label="粉丝">{formatCount(latestMetric?.follower_count)}</Descriptions.Item>
<Descriptions.Item label="关注">{formatCount(latestMetric?.following_count)}</Descriptions.Item>
<Descriptions.Item label="获赞">{formatCount(latestMetric?.total_favorited)}</Descriptions.Item>
<Descriptions.Item label="作品总数">{formatCount(latestMetric?.aweme_count)}</Descriptions.Item>
</Descriptions>
{accountMetrics.length >= 2 ? (
<AccountTrendChart metrics={accountMetrics} />
) : (
<Typography.Text type="secondary">
{accountMetrics.length === 1 ? '已记录首个数据快照;随自动采集积累后展示趋势曲线。' : '数据随自动采集积累;账号登录且运行环境就绪后每轮采集记一次快照。'}
</Typography.Text>
)}
</Flex>
</Card>
<WorksGallery sourceId={account.id} sourceType="owned" />
<Card title="固定资源">
{browsersError ? (
<Alert
@@ -207,51 +332,40 @@ export default function Page() {
<Flex vertical gap={16}>
<Typography.Text type="secondary">仅支持单用户 Phase A Mock 文本;内部 ID 与版本由系统生成。</Typography.Text>
<Form form={draftForm} layout="vertical">
<Form.Item
name="content"
label="草稿内容"
extra={readiness?.ready ? '创建后进入只读快照核对并显式确认' : `资源未就绪:${readiness?.label || '正在加载'}`}
>
<Input.TextArea rows={4} disabled={!readiness?.ready || draftBusy} />
<Form.Item name="content" rules={[{ required: true, message: '请输入草稿内容' }]}>
<Input.TextArea rows={4} placeholder="输入要发布的文本草稿" />
</Form.Item>
<Button
type="primary"
loading={draftBusy}
disabled={!readiness?.ready}
onClick={onCreateDraft}
>
<Button type="primary" disabled={!readiness?.ready} loading={draftBusy} onClick={onCreateDraft}>
创建草稿
</Button>
</Form>
{drafts.map((draft) => (
<Card key={draft.id} size="small">
<Flex justify="space-between" align="center" gap={12}>
<div style={{ minWidth: 0 }}>
<Typography.Text strong>草稿版本 {draft.version}</Typography.Text>
<Typography.Paragraph type="secondary" ellipsis style={{ marginBottom: 0, fontSize: 12 }}>
{draft.content}
</Typography.Paragraph>
</div>
<Typography.Link onClick={() => history.push(`/drafts/${draft.id}`)} style={{ flexShrink: 0 }}>
核对草稿
</Typography.Link>
</Flex>
</Card>
))}
{drafts.length ? (
<Flex vertical gap={8}>
{drafts.map((draft: any) => (
<Flex key={draft.id} justify="space-between" align="center" wrap="wrap" gap={8}>
<Typography.Text ellipsis style={{ maxWidth: '70%' }}>{draft.content}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>v{draft.version}</Typography.Text>
</Flex>
))}
</Flex>
) : null}
</Flex>
</Card>
<Modal
open={pauseOpen}
title="暂停账号"
onCancel={() => setPauseOpen(false)}
onOk={() => {
setPauseOpen(false);
runAction('pause');
}}
okText="确认暂停"
okButtonProps={{ danger: true }}
cancelText="取消"
onOk={async () => {
setPauseOpen(false);
await runAction('pause');
}}
onCancel={() => setPauseOpen(false)}
>
<p>{`暂停账号 ${account.name}?待领取任务将进入 hold,恢复账号不会自动恢复既有 hold。`}</p>
<Typography.Paragraph style={{ marginBottom: 0 }}>
暂停后运行环境不再调度该账号;确认继续?
</Typography.Paragraph>
</Modal>
</Flex>
);