From 8748f899d4c9cddb0df7011220bf10537a5aaed3 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 28 Sep 2026 11:19:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E7=9B=91=E6=8E=A7=E4=BD=9C?= =?UTF-8?q?=E5=93=81=E5=8D=A1=E7=89=87=E5=B1=95=E7=A4=BA=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=EF=BC=8C=E8=A7=82=E7=9C=8B/=E7=82=B9=E8=B5=9E/=E8=AF=84?= =?UTF-8?q?=E8=AE=BA/=E5=88=86=E4=BA=AB=E5=85=A8=E5=AE=BD=20GRID=20?= =?UTF-8?q?=E8=B4=B4=E5=B0=81=E9=9D=A2=E4=B8=8B=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorksGallery 卡片封面改走本地缓存 /works/:id/cover(WorkCover 与竞品分析页同链路, 替换会过期的 douyinpic 签名 URL) - 卡片新增作品描述(body,两行省略) - 指标从标题下横排小字改为封面下方全宽四列 GRID(观看/点赞/评论/分享,上值下图标行) - WorksGallery 为监控账号详情与自有账号详情共用,两处同步生效 - 测试:works-gallery-card 3 例 + 既有 1 例全绿,tsc/antd lint 通过 --- web/src/components/creator/WorksGallery.tsx | 104 ++++++++++++++------ web/tests/works-gallery-card.test.cjs | 42 ++++++++ 2 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 web/tests/works-gallery-card.test.cjs diff --git a/web/src/components/creator/WorksGallery.tsx b/web/src/components/creator/WorksGallery.tsx index f39af03..dceb41c 100644 --- a/web/src/components/creator/WorksGallery.tsx +++ b/web/src/components/creator/WorksGallery.tsx @@ -12,9 +12,62 @@ import { LikeOutlined, ReloadOutlined, ShareAltOutlined } from '@ant-design/icons'; -import { creatorGet, getList } from '@/services/api'; +import { creatorAction, creatorGet, fetchCreatorBinary, getList } from '@/services/api'; import { conflictMessage, dateTime } from '@/utils/helpers'; +// 作品封面:从本地缓存接口读取(认证 fetch + blob URL);缓存缺失时展示占位。 +// 原始 douyinpic URL 带签名会过期,与竞品分析页共用同一条 /works/:id/cover 链路。 +export 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; + } + return ( + + {missing ? '封面未缓存' : '封面加载中'} + + ); +} + +// 观看/点赞/评论/分享全宽四列 GRID,贴在封面下方(body padding 置 0,本行自带上下边框)。 +function WorkStatsGrid({ work }: { work: any }) { + const items = [ + { key: 'play_count', label: '观看', icon: EyeOutlined }, + { key: 'likes', label: '点赞', icon: LikeOutlined }, + { key: 'comments_count', label: '评论', icon: CommentOutlined }, + { key: 'shares', label: '分享', icon: ShareAltOutlined }, + ]; + return ( + + {items.map(({ key, label, icon: Icon }) => ( + + + {label} + + + {work[key] ?? '—'} + + + ))} + + ); +} + const metricSeriesMeta = [ { key: 'play_count', label: '播放' }, { key: 'likes', label: '点赞' }, @@ -174,36 +227,31 @@ export default function WorksGallery({ sourceId, sourceType, emptyHint }: WorksG openWorkDetail(work)} - cover={ - work.cover_url ? ( - {work.title - ) : ( - - 无封面 - - ) - } + cover={} > - - - {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} +
+ + + {work.title || '无标题'} + + {work.body ? ( + + {work.body} + + ) : null} + + {work.published_at ? dateTime(work.published_at) : '发布时间待核验'} - ) : null} - + +
+ + {work.likes_growth != null && work.likes_growth > 0 ? ( + + 24h +{work.likes_growth} + + ) : null}
))} diff --git a/web/tests/works-gallery-card.test.cjs b/web/tests/works-gallery-card.test.cjs new file mode 100644 index 0000000..494b2bc --- /dev/null +++ b/web/tests/works-gallery-card.test.cjs @@ -0,0 +1,42 @@ +const assert = require('node:assert/strict'); +const { readFileSync } = require('node:fs'); +const { test } = require('node:test'); +const ts = require('typescript'); + +const source = ts.createSourceFile( + 'WorksGallery.tsx', + readFileSync(`${__dirname}/../src/components/creator/WorksGallery.tsx`, 'utf8'), + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, +); + +test('作品卡片封面读本地缓存 /works/:id/cover(签名 URL 会过期)', () => { + assert.match(sourceText(), /cover`/); + assert.match(sourceText(), /fetchCreatorBinary/); + assert.doesNotMatch(sourceText(), /cover_url/); +}); + +test('观看/点赞/评论/分享四项指标全宽 GRID 贴封面下方', () => { + const grid = /function WorkStatsGrid/.exec(sourceText()); + assert.ok(grid, '应有 WorkStatsGrid 组件'); + const body = sourceText().slice(grid.index, grid.index + 1200); + for (const key of ['play_count', 'likes', 'comments_count', 'shares']) { + assert.match(body, new RegExp(key), `GRID 应包含指标 ${key}`); + } + // 四列均分占满整行:每列 flex:1,容器自带上下边框贴封面。 + assert.match(body, /flex: 1/); + assert.match(body, /borderTop/); + assert.match(body, /borderBottom/); +}); + +test('卡片展示作品描述(body 省略两行)', () => { + const card = /work\.body \? \(/.exec(sourceText()); + assert.ok(card, '卡片应有 body 描述分支'); + const body = sourceText().slice(card.index - 120, card.index + 200); + assert.match(body, /ellipsis=\{\{ rows: 2 \}\}/); +}); + +function sourceText() { + return source.getFullText(); +}