From 127c3e70ee644bc99c115dbd4f6c17b3026fad63 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 27 Sep 2026 22:08:25 +0800 Subject: [PATCH] =?UTF-8?q?refactor(web):=20=E4=BD=9C=E5=93=81=E7=BD=91?= =?UTF-8?q?=E6=A0=BC=E4=B8=8E=20Drawer=20=E6=8A=BD=E5=8F=96=E4=B8=BA?= =?UTF-8?q?=E5=85=B1=E4=BA=AB=20WorksGallery=EF=BC=8C=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=BA=8F=E5=88=97=E4=B8=8E=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - components/creator/WorksGallery:作品 Card 网格(分页+爆品筛选)+ Drawer(详情/增长曲线含播放/指标表含播放列/评论分页列表) - 监控账号详情页改用共享组件,sourceType='competitor' --- web/src/components/creator/WorksGallery.tsx | 307 ++++++++++++++++++ .../pages/accounts/monitoring/$id/index.tsx | 228 +------------ 2 files changed, 313 insertions(+), 222 deletions(-) create mode 100644 web/src/components/creator/WorksGallery.tsx diff --git a/web/src/components/creator/WorksGallery.tsx b/web/src/components/creator/WorksGallery.tsx new file mode 100644 index 0000000..b6c885b --- /dev/null +++ b/web/src/components/creator/WorksGallery.tsx @@ -0,0 +1,307 @@ +// 作品数据共享组件:作品 Card 网格(分页 + 24h 点赞增速爆品筛选) +// + 作品 Drawer(作品详情、增长曲线含播放序列、指标采集记录表含播放列、评论列表)。 +// 竞品账号详情页与自有账号详情页共用;数据通道 GET /creator/works?source_id=&source_type=, +// 指标 GET /creator/works/:id/metrics,评论 GET /creator/comments?work_id=(定时采集由后端调度器维护)。 +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Line } from '@ant-design/charts'; +import { + Alert, + Button, + Card, + Col, + Drawer, + Empty, + Flex, + InputNumber, + Row, + Space, + Spin, + Table, + Typography, + message, +} from 'antd'; +import { + CommentOutlined, + EyeOutlined, + FireOutlined, + LikeOutlined, + ReloadOutlined, + ShareAltOutlined, +} from '@ant-design/icons'; +import { creatorGet, getList } from '@/services/api'; +import { conflictMessage, dateTime } from '@/utils/helpers'; + +const metricSeriesMeta = [ + { key: 'play_count', label: '播放' }, + { key: 'likes', label: '点赞' }, + { key: 'collect_count', label: '收藏' }, + { key: 'comments_count', label: '评论' }, + { key: 'shares', label: '转发' }, +] as const; + +// 增长曲线:把指标时序点展开成长表(时间 × 指标 × 数值)供折线图渲染。 +function GrowthChart({ metrics }: { metrics: any[] }) { + const data = useMemo( + () => + metrics.flatMap((point) => + metricSeriesMeta + .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' }} + /> + ); +} + +interface WorksGalleryProps { + sourceId: string; + sourceType?: 'owned' | 'competitor'; + emptyHint?: string; +} + +export default function WorksGallery({ sourceId, sourceType, emptyHint }: WorksGalleryProps) { + const [works, setWorks] = useState([]); + const [workPage, setWorkPage] = useState(1); + const [workPageInfo, setWorkPageInfo] = useState({ total: 0, hasNext: false }); + const [worksPending, setWorksPending] = useState(true); + const [worksError, setWorksError] = useState(null); + const [minGrowth, setMinGrowth] = useState(null); + const [detailWork, setDetailWork] = useState(null); + const [workMetrics, setWorkMetrics] = useState([]); + const [metricsPending, setMetricsPending] = useState(false); + const [comments, setComments] = useState([]); + const [commentPage, setCommentPage] = useState(1); + const [commentTotal, setCommentTotal] = useState(0); + const [commentsPending, setCommentsPending] = useState(false); + const [messageApi, contextHolder] = message.useMessage(); + + const loadWorks = useCallback(async () => { + setWorksPending(true); + setWorksError(null); + try { + const filters: Record = { source_id: sourceId }; + if (sourceType) filters.source_type = sourceType; + if (minGrowth !== null) { + // 爆品筛选:growth_hours 开启后端 24h 点赞增量计算,min_likes_growth 设下限。 + filters.growth_hours = 24; + filters.min_likes_growth = minGrowth; + } + const result = await getList({ resource: 'creator-works', page: workPage, pageSize: 25, filters }); + setWorks(result.data); + setWorkPageInfo({ total: result.total, hasNext: Boolean(result.hasNext) }); + } catch (loadError) { + setWorksError(loadError); + } finally { + setWorksPending(false); + } + }, [sourceId, sourceType, workPage, minGrowth]); + + useEffect(() => { + loadWorks(); + }, [loadWorks]); + + const loadComments = useCallback(async (workID: string, page: number) => { + setCommentsPending(true); + try { + const result = await getList({ resource: 'creator-comments', page, pageSize: 10, filters: { work_id: workID } }); + setComments(result.data); + setCommentTotal(result.total); + } catch { + // 评论加载失败不打断 Drawer 主体(指标数据已在上方),清空并提示。 + setComments([]); + setCommentTotal(0); + } finally { + setCommentsPending(false); + } + }, []); + + async function openWorkDetail(work: any) { + setDetailWork(work); + setWorkMetrics([]); + setComments([]); + setCommentTotal(0); + setCommentPage(1); + setMetricsPending(true); + try { + setWorkMetrics((await creatorGet(`/creator/works/${encodeURIComponent(work.id)}/metrics`)) || []); + } catch (actionError) { + messageApi.error(conflictMessage(actionError, '作品统计读取失败')); + } finally { + setMetricsPending(false); + } + loadComments(work.id, 1); + } + + return ( + <> + {contextHolder} + } size="small" onClick={loadWorks} disabled={worksPending} aria-label="刷新作品" /> + } + > + + 共 {workPageInfo.total} 个作品;作品由系统按采集间隔自动同步。 + + + 24h 点赞增量 ≥ + { + setMinGrowth(value ?? null); + setWorkPage(1); + }} + /> + + + {worksError ? ( + 重试} + /> + ) : null} + + {works.length ? ( + + {works.map((work) => ( + + openWorkDetail(work)} + cover={ + work.cover_url ? ( + {work.title + ) : ( + + 无封面 + + ) + } + > + + + {work.title || '无标题'} + + + {work.published_at ? dateTime(work.published_at) : '发布时间待核验'} + + + {work.play_count ?? '—'} + {work.likes ?? '—'} + {work.comments_count ?? '—'} + {work.shares ?? '—'} + + {work.likes_growth != null && work.likes_growth > 0 ? ( + + 24h +{work.likes_growth} + + ) : null} + + + + ))} + + ) : ( + + )} + + {workPageInfo.total > works.length ? ( + + + + + + + ) : null} + + setDetailWork(null)} size="large" title="作品统计"> + {detailWork ? ( + + {detailWork.cover_url ? ( + 作品封面 + ) : null} + ID:{detailWork.work_key || detailWork.id} + {detailWork.title || '无标题'} + + {detailWork.body || '暂无正文'} + + + 发布:{detailWork.published_at ? dateTime(detailWork.published_at) : '—'} + {detailWork.original_url ? ( + 打开原文 + ) : null} + + {workMetrics.length >= 2 ? : null} + dateTime(value) }, + { title: '播放', dataIndex: 'play_count', render: (v) => v ?? '—' }, + { title: '点赞', dataIndex: 'likes', render: (v) => v ?? '—' }, + { title: '收藏', dataIndex: 'collect_count', render: (v) => v ?? '—' }, + { title: '评论', dataIndex: 'comments_count', render: (v) => v ?? '—' }, + { title: '转发', dataIndex: 'shares', render: (v) => v ?? '—' }, + ]} + locale={{ emptyText: '暂无指标记录。' }} + /> + 评论 +
{ + setCommentPage(page); + loadComments(detailWork.id, page); + }, + hideOnSinglePage: true, + }} + columns={[ + { title: '作者', dataIndex: 'author_name', width: 140, render: (v) => v || '—' }, + { title: '内容', dataIndex: 'content', ellipsis: true }, + { title: '时间', dataIndex: 'published_at', width: 160, render: (v) => (v ? dateTime(v) : '—') }, + ]} + locale={{ emptyText: '暂无评论。' }} + /> + + ) : null} + + + ); +} diff --git a/web/src/pages/accounts/monitoring/$id/index.tsx b/web/src/pages/accounts/monitoring/$id/index.tsx index b14bb7f..d25cadb 100644 --- a/web/src/pages/accounts/monitoring/$id/index.tsx +++ b/web/src/pages/accounts/monitoring/$id/index.tsx @@ -1,34 +1,25 @@ // 监控账号详情:形态对齐账号详情页(页头槽 + 卡片流)—— // 页头 info 为账号标识(头像 + 昵称 + 同步状态 + 平台),actions 为返回/同步/暂停恢复; -// 主体两张 Card:账号画像(Descriptions)与作品列表(Row/Col card 网格,分页,支持按点赞增速筛选爆品)。 -// 点击作品弹出 Drawer 展示统计信息:作品详情 + 增长曲线折线图 + 指标采集记录表。 -// 作品列表走 GET /creator/works?source_id=,定时采集由后端调度器维护。 -import { useCallback, useEffect, useMemo, useState } from 'react'; +// 主体:账号画像 Card + 作品数据(共享 WorksGallery:作品网格 + Drawer 增长曲线/指标记录/评论)。 +// 作品列表走 GET /creator/works?source_id=&source_type=competitor,定时采集由后端调度器维护。 +import { useCallback, useEffect, useState } from 'react'; import { history, useParams } from '@umijs/max'; -import { Line } from '@ant-design/charts'; import { Alert, Avatar, Button, Card, - Col, Descriptions, - Drawer, - Empty, Flex, - InputNumber, - Row, Space, - Spin, - Table, Tag, Typography, message, } from 'antd'; -import { CommentOutlined, FireOutlined, LikeOutlined, ReloadOutlined, ShareAltOutlined } from '@ant-design/icons'; -import { creatorAction, creatorGet, getOne, getList } from '@/services/api'; +import { creatorAction, getOne } from '@/services/api'; import { conflictMessage, dateTime, platformLabel } from '@/utils/helpers'; import { usePageActions, usePageInfo } from '@/components/PageActions'; +import WorksGallery from '@/components/creator/WorksGallery'; interface CompetitorDetail { id: string; @@ -62,57 +53,12 @@ function formatCount(value?: number | null): string { return value >= 10000 ? `${(value / 10000).toFixed(1)}w` : `${value}`; } -const metricSeriesMeta = [ - { key: 'likes', label: '点赞' }, - { key: 'collect_count', label: '收藏' }, - { key: 'comments_count', label: '评论' }, - { key: 'shares', label: '转发' }, -] as const; - -// 增长曲线:把指标时序点展开成长表(时间 × 指标 × 数值)供折线图渲染。 -function GrowthChart({ metrics }: { metrics: any[] }) { - const data = useMemo( - () => - metrics.flatMap((point) => - metricSeriesMeta - .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 }>(); const [competitor, setCompetitor] = useState(null); const [pending, setPending] = useState(true); const [error, setError] = useState(null); const [actionBusy, setActionBusy] = useState(false); - const [works, setWorks] = useState([]); - const [workPage, setWorkPage] = useState(1); - const [workPageInfo, setWorkPageInfo] = useState({ total: 0, hasNext: false }); - const [worksPending, setWorksPending] = useState(true); - const [worksError, setWorksError] = useState(null); - const [detailWork, setDetailWork] = useState(null); - const [workMetrics, setWorkMetrics] = useState([]); - const [metricsPending, setMetricsPending] = useState(false); - const [minGrowth, setMinGrowth] = useState(null); const [messageApi, contextHolder] = message.useMessage(); const loadCompetitor = useCallback(async () => { @@ -127,32 +73,9 @@ export default function Page() { } }, [id]); - const loadWorks = useCallback(async () => { - setWorksPending(true); - setWorksError(null); - try { - const filters: Record = { source_id: id }; - if (minGrowth !== null) { - // 爆品筛选:growth_hours 开启后端 24h 点赞增量计算,min_likes_growth 设下限。 - filters.growth_hours = 24; - filters.min_likes_growth = minGrowth; - } - const result = await getList({ resource: 'creator-works', page: workPage, pageSize: 25, filters }); - setWorks(result.data); - setWorkPageInfo({ total: result.total, hasNext: Boolean(result.hasNext) }); - } catch (loadError) { - setWorksError(loadError); - } finally { - setWorksPending(false); - } - }, [id, workPage, minGrowth]); - useEffect(() => { loadCompetitor(); }, [loadCompetitor]); - useEffect(() => { - loadWorks(); - }, [loadWorks]); const syncMeta = syncStatusMeta[competitor?.sync_status ?? ''] || { label: competitor?.sync_status || '—' }; @@ -222,19 +145,6 @@ export default function Page() { [competitor?.id, competitor?.enabled], ); - async function openWorkDetail(work: any) { - setDetailWork(work); - setWorkMetrics([]); - setMetricsPending(true); - try { - setWorkMetrics((await creatorGet(`/creator/works/${encodeURIComponent(work.id)}/metrics`)) || []); - } catch (actionError) { - messageApi.error(conflictMessage(actionError, '作品统计读取失败')); - } finally { - setMetricsPending(false); - } - } - if (pending) return ; if (error || !competitor) return ; @@ -266,133 +176,7 @@ export default function Page() { ]} /> - } size="small" onClick={loadWorks} disabled={worksPending} aria-label="刷新作品" /> - } - > - - 共 {workPageInfo.total} 个作品;作品由系统按采集间隔自动同步。 - - - 24h 点赞增量 ≥ - { - setMinGrowth(value ?? null); - setWorkPage(1); - }} - /> - - - {worksError ? ( - 重试} - /> - ) : null} - - {works.length ? ( - - {works.map((work) => ( - - openWorkDetail(work)} - cover={ - work.cover_url ? ( - {work.title - ) : ( - - 无封面 - - ) - } - > - - - {work.title || '无标题'} - - - {work.published_at ? dateTime(work.published_at) : '发布时间待核验'} - - - {work.likes ?? '—'} - {work.comments_count ?? '—'} - {work.shares ?? '—'} - - {work.likes_growth != null && work.likes_growth > 0 ? ( - - 24h +{work.likes_growth} - - ) : null} - - - - ))} - - ) : ( - - )} - - {workPageInfo.total > works.length ? ( - - - - - - - ) : null} - - setDetailWork(null)} size="large" title="作品统计"> - {detailWork ? ( - - {detailWork.cover_url ? ( - 作品封面 - ) : null} - ID:{detailWork.work_key || detailWork.id} - {detailWork.title || '无标题'} - - {detailWork.body || '暂无正文'} - - - 发布:{detailWork.published_at ? dateTime(detailWork.published_at) : '—'} - {detailWork.original_url ? ( - 打开原文 - ) : null} - - {workMetrics.length >= 2 ? : null} -
dateTime(value) }, - { title: '点赞', dataIndex: 'likes', render: (v) => v ?? '—' }, - { title: '收藏', dataIndex: 'collect_count', render: (v) => v ?? '—' }, - { title: '评论', dataIndex: 'comments_count', render: (v) => v ?? '—' }, - { title: '转发', dataIndex: 'shares', render: (v) => v ?? '—' }, - ]} - locale={{ emptyText: '暂无指标记录。' }} - /> - - ) : null} - + ); }