diff --git a/web/src/components/creator/WorksGallery.tsx b/web/src/components/creator/WorksGallery.tsx index 8280ae2..44f7dfe 100644 --- a/web/src/components/creator/WorksGallery.tsx +++ b/web/src/components/creator/WorksGallery.tsx @@ -105,12 +105,15 @@ function GrowthChart({ metrics }: { metrics: any[] }) { } interface WorksGalleryProps { - sourceId: string; + /** 不传则不过滤来源(如竞品分析页跨全部竞品);监控/自有账号详情传具体账号 ID。 */ + sourceId?: string; sourceType?: 'owned' | 'competitor'; emptyHint?: string; + /** 页面自有筛选(如竞品分析页的互动量/发布时间条件)透传到作品列表查询。 */ + extraFilters?: Record; } -export default function WorksGallery({ sourceId, sourceType, emptyHint }: WorksGalleryProps) { +export default function WorksGallery({ sourceId, sourceType, emptyHint, extraFilters }: WorksGalleryProps) { const [works, setWorks] = useState([]); const [workPage, setWorkPage] = useState(1); const [workPageInfo, setWorkPageInfo] = useState({ total: 0, hasNext: false }); @@ -131,7 +134,8 @@ export default function WorksGallery({ sourceId, sourceType, emptyHint }: WorksG setWorksPending(true); setWorksError(null); try { - const filters: Record = { source_id: sourceId }; + const filters: Record = { ...extraFilters }; + if (sourceId) filters.source_id = sourceId; if (sourceType) filters.source_type = sourceType; if (minGrowth !== null) { // 爆品筛选:growth_hours 开启后端 24h 点赞增量计算,min_likes_growth 设下限。 @@ -146,7 +150,7 @@ export default function WorksGallery({ sourceId, sourceType, emptyHint }: WorksG } finally { setWorksPending(false); } - }, [sourceId, sourceType, workPage, minGrowth]); + }, [sourceId, sourceType, workPage, minGrowth, extraFilters]); useEffect(() => { loadWorks(); diff --git a/web/src/pages/creator/competitors/index.tsx b/web/src/pages/creator/competitors/index.tsx index f66a44a..ffb19ef 100644 --- a/web/src/pages/creator/competitors/index.tsx +++ b/web/src/pages/creator/competitors/index.tsx @@ -1,12 +1,10 @@ -// 竞品分析:作品列表对齐 ant-design-pro card-list——antd Row/Col 响应式栅格 + Card 原样组合; -// 作品详情走 Drawer;日期筛选用 antd DatePicker;筛选自动刷新、手动翻页不变。 -// 封面读本地缓存(/works/:id/cover,认证 fetch + blob)。发布状态仅用于筛选,不在卡片上展示(对齐监控账号作品列表)。 -import { useEffect, useState } from 'react'; -import { CommentOutlined, LikeOutlined, ShareAltOutlined } from '@ant-design/icons'; -import { Alert, App, Button, Card, Col, DatePicker, Drawer, Empty, Flex, InputNumber, Row, Select, Space, Spin, Table, Typography } from 'antd'; +// 竞品分析:跨全部竞品的作品筛选与列表。 +// 作品列表/详情/指标/评论复用共享组件 WorksGallery(与监控账号、自有账号详情页同一套展现), +// 本页仅保留作品筛选卡,筛选条件经 extraFilters 透传(后端 workFilter 支持同名 query 参数)。 +import { useMemo, useState } from 'react'; +import { Card, DatePicker, Flex, InputNumber, Select, Space, Typography } from 'antd'; import type { Dayjs } from 'dayjs'; -import { creatorGet, fetchCreatorBinary, getList } from '@/services/api'; -import { conflictMessage, dateTime } from '@/utils/helpers'; +import WorksGallery from '@/components/creator/WorksGallery'; const publishedAtStatusOptions = [ { value: 'verified', label: '已核验' }, @@ -24,223 +22,54 @@ interface Filters { publishedBefore?: Dayjs; } -// 作品封面:从本地缓存接口读取(认证 fetch + blob URL);缓存缺失时展示占位。 -function WorkCover({ workID, title }: { workID: string; title?: string }) { - const [src, setSrc] = useState(null); - const [missing, setMissing] = useState(false); - useEffect(() => { - let objectURL: string | null = null; - setSrc(null); - setMissing(false); - fetchCreatorBinary(`/creator/works/${encodeURIComponent(workID)}/cover`) - .then((blob) => { - objectURL = URL.createObjectURL(blob); - setSrc(objectURL); - }) - .catch(() => setMissing(true)); - return () => { - if (objectURL) URL.revokeObjectURL(objectURL); - }; - }, [workID]); - if (src) { - return {title; - } +export default function Page() { + const [filters, setFilters] = useState({}); + + // 序列化为后端 workFilter 的 query 参数;日期取本地日历日的 00:00/23:59(UTC 边界)。 + const extraFilters = useMemo( + () => ({ + min_likes: filters.minLikes, + min_comments: filters.minComments, + min_shares: filters.minShares, + published_at_status: filters.publishedAtStatus, + published_after: filters.publishedAfter ? `${filters.publishedAfter.format('YYYY-MM-DD')}T00:00:00Z` : undefined, + published_before: filters.publishedBefore ? `${filters.publishedBefore.format('YYYY-MM-DD')}T23:59:59Z` : undefined, + }), + [filters], + ); + return ( - - {missing ? '封面未缓存' : '封面加载中'} + + 调整条件后自动刷新}> + + setFilters((f) => ({ ...f, minLikes: value ?? undefined }))} /> + setFilters((f) => ({ ...f, minComments: value ?? undefined }))} /> + setFilters((f) => ({ ...f, minShares: value ?? undefined }))} /> + setFilters((f) => ({ ...f, publishedAtStatus: value }))} - options={publishedAtStatusOptions} - /> - setFilters((f) => ({ ...f, publishedAfter: value ?? undefined }))} - allowClear - /> - setFilters((f) => ({ ...f, publishedBefore: value ?? undefined }))} - allowClear - /> - - - 共 {workPageInfo.total} 个作品}> - {error ? ( - 重试} /> - ) : null} - - {works.length ? ( - - {works.map((work) => ( - - } - actions={[ - , - ...(work.original_url ? [原文] : []), - ]} - > -
- - {work.title ? ( - - {work.title} - - ) : null} - - 作者:{work.author_name || '—'} - - - - {work.published_at ? dateTime(work.published_at) : '—'} - - - -
- {/* 点赞/评论/分享全宽贴在封面下方(body padding 置 0,本行自带上下边框) */} - - {work.likes ?? '—'} - {work.comments_count ?? '—'} - {work.shares ?? '—'} - -
- - ))} -
- ) : ( - - )} -
- {workPageInfo.total > works.length ? ( - - - - - - - ) : null} -
- -
- setWorkDetail(null)} size="large" title="作品详情"> - {workDetail ? ( - - - ID:{workDetail.work_key || workDetail.id} - {workDetail.body || workDetail.title || '暂无正文'} - - 来源:{workDetail.source_type || '—'} - {workDetail.original_url ? ( - - 打开原文 - - ) : null} - - dateTime(value) }, - { title: '点赞', dataIndex: 'likes', render: (v) => v ?? '—' }, - { title: '评论', dataIndex: 'comments_count', render: (v) => v ?? '—' }, - { title: '转发', dataIndex: 'shares', render: (v) => v ?? '—' }, - ]} - locale={{ emptyText: '暂无指标记录。' }} - /> - - ) : null} - - - - ); -} diff --git a/web/tests/works-gallery-card.test.cjs b/web/tests/works-gallery-card.test.cjs index 4cf3f75..4af8218 100644 --- a/web/tests/works-gallery-card.test.cjs +++ b/web/tests/works-gallery-card.test.cjs @@ -48,3 +48,10 @@ function sourceText() { return source.getFullText(); } +const competitorsPageText = readFileSync(`${__dirname}/../src/pages/creator/competitors/index.tsx`, 'utf8'); + +test('竞品分析页复用 WorksGallery,不再自建作品卡片/详情抽屉', () => { + assert.match(competitorsPageText, /import WorksGallery from '@\/components\/creator\/WorksGallery'/); + assert.doesNotMatch(competitorsPageText, /\bWorkCover\b|