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(); +}