From 3e3e009b11f6ff2b337306b3c3dfa27c4cbee9a1 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 6 Oct 2026 17:16:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B1=E5=88=9B=E6=A8=A1=E5=9D=97C:=20?= =?UTF-8?q?=E6=88=91=E7=9A=84=E4=BD=9C=E5=93=81=E5=88=86=E7=B1=BB=E6=A0=87?= =?UTF-8?q?=E7=AD=BE(=E5=85=A8=E9=83=A8/=E5=8E=9F=E5=88=9B/=E6=88=91?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E7=9A=84=E5=85=B1=E5=88=9B/=E8=8D=89?= =?UTF-8?q?=E7=A8=BF)=20+=20=E4=B8=96=E4=BB=A3=E4=B8=8E=E5=AD=90=E8=A1=8D?= =?UTF-8?q?=E7=94=9F=E6=95=B0=20+=20=E6=95=B0=E6=8D=AE=E6=A6=82=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game-distribution/MyGamesPage.test.tsx | 52 ++++++++++ .../game-distribution/MyGamesPage.tsx | 94 ++++++++++++++++++- 2 files changed, 142 insertions(+), 4 deletions(-) diff --git a/src/components/game-distribution/MyGamesPage.test.tsx b/src/components/game-distribution/MyGamesPage.test.tsx index 57090c7b5..2a5bd3dae 100644 --- a/src/components/game-distribution/MyGamesPage.test.tsx +++ b/src/components/game-distribution/MyGamesPage.test.tsx @@ -763,6 +763,58 @@ test('衍生列表读取失败时行内给出错误与重试', async () => { ).toBeTruthy(); }); +test('分类标签按文档口径过滤,并给出总作品数 / 累计被 Fork 次数 / 世代数', async () => { + vi.mocked(listMyGames).mockResolvedValue([ + createGame({ id: 'g-original', title: '母版甲', status: 'published', forkCount: 2 }), + createGame({ + id: 'g-fork', + title: '共创乙', + status: 'published', + forkCount: 1, + lineage: { + generation: 1, + rootGameId: 'g-original', + rootTitle: '母版甲', + parentGameId: 'g-original', + parentTitle: '母版甲', + parentAuthorName: '原作者', + }, + }), + createGame({ id: 'g-draft', title: '草稿丙', status: 'unpublished', forkCount: 0 }), + ]); + + renderPage(createAuthValue()); + + // 数据概览:总作品数 + 累计被 Fork 次数(世代数字段缺失 → 该项不渲染)。 + expect(await screen.findByText('总作品数 3')).toBeTruthy(); + expect(screen.getByText('累计被 Fork 次数 3')).toBeTruthy(); + expect(screen.queryByText(/累计衍生世代数/u)).toBeNull(); + + // 卡片上的世代数与子衍生作品数量。 + expect(screen.getByText('子衍生作品 2')).toBeTruthy(); + expect(screen.getByText('子衍生作品 1')).toBeTruthy(); + + // 分类:原创作品只看无父作品的;我发布的共创只看有父且已公开的;草稿只看非公开的。 + fireEvent.click(screen.getByRole('tab', { name: '原创作品' })); + expect(screen.getByText('母版甲')).toBeTruthy(); + expect(screen.queryByText('共创乙')).toBeNull(); + expect(screen.queryByText('草稿丙')).toBeNull(); + + fireEvent.click(screen.getByRole('tab', { name: '我发布的共创' })); + expect(screen.getByText('共创乙')).toBeTruthy(); + expect(screen.queryByText('母版甲')).toBeNull(); + expect(screen.queryByText('草稿丙')).toBeNull(); + + fireEvent.click(screen.getByRole('tab', { name: '我的草稿' })); + expect(screen.getByText('草稿丙')).toBeTruthy(); + expect(screen.queryByText('母版甲')).toBeNull(); + + fireEvent.click(screen.getByRole('tab', { name: '全部作品' })); + expect(screen.getByText('母版甲')).toBeTruthy(); + expect(screen.getByText('共创乙')).toBeTruthy(); + expect(screen.getByText('草稿丙')).toBeTruthy(); +}); + test('未公开作品不渲染「被改编 N」入口', async () => { vi.mocked(listMyGames).mockResolvedValue([ createGame({ status: 'unpublished', forkCount: 0 }), diff --git a/src/components/game-distribution/MyGamesPage.tsx b/src/components/game-distribution/MyGamesPage.tsx index a0e9d2de6..03786e99b 100644 --- a/src/components/game-distribution/MyGamesPage.tsx +++ b/src/components/game-distribution/MyGamesPage.tsx @@ -34,6 +34,7 @@ import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; +import { coCreationGenerationShortLabel } from './coCreationCopy'; import { GameCover } from './GameCover'; import { type GamePublishDraft, @@ -221,6 +222,15 @@ export function MyGamesPage({ * 不要求作者身份,门禁只要求登录,与这一页的既有门禁一致。 */ const [tab, setTab] = useState<'works' | 'collections'>('works'); + /** + * 「我的作品」下的分类标签(文档:全部作品 / 原创作品 / 我发布的共创 / 我的草稿)。 + * + * 口径说明:原创 = 没有父作品(`lineage` 缺省或 generation 0);我发布的共创 = 有父作品 + * 且已公开;我的草稿 = 状态非 `published`(与卡片上的状态标签同一口径)。 + */ + const [workCategory, setWorkCategory] = useState< + 'all' | 'original' | 'coCreated' | 'draft' + >('all'); const [collections, setCollections] = useState([]); const [isLoadingCollections, setIsLoadingCollections] = useState(false); const [collectionsError, setCollectionsError] = useState(''); @@ -330,7 +340,40 @@ export function MyGamesPage({ }), [games, statusFilter, versionFilter], ); - const hasActiveFilter = statusFilter !== 'all' || versionFilter !== 'all'; + /** 分类标签(文档口径):原创 = 无父作品;我发布的共创 = 有父作品且已公开;草稿 = 非公开。 */ + const categoryGames = useMemo(() => { + if (workCategory === 'all') return visibleGames; + return visibleGames.filter((game) => { + const isCoCreated = (game.lineage?.generation ?? 0) > 0; + // 三个分类互斥:草稿优先(非公开),其余按是否二创分原创 / 共创。 + if (workCategory === 'draft') return game.status !== 'published'; + if (game.status !== 'published') return false; + return workCategory === 'original' ? !isCoCreated : isCoCreated; + }); + }, [visibleGames, workCategory]); + const totalForkCount = useMemo( + () => games.reduce((sum, game) => sum + (game.forkCount ?? 0), 0), + [games], + ); + /** + * 累计衍生世代数:`my-games` 目前没有这个字段(后端待补),因此**有才渲染**, + * 缺失时概览少一项而不是拿别的数字凑。 + */ + const totalDerivedGenerations = useMemo(() => { + const values = games + .map((game) => + 'descendantGenerationCount' in game && + typeof game.descendantGenerationCount === 'number' + ? game.descendantGenerationCount + : null, + ) + .filter((value): value is number => value !== null); + return values.length > 0 + ? values.reduce((sum, value) => sum + value, 0) + : null; + }, [games]); + const hasActiveFilter = + statusFilter !== 'all' || versionFilter !== 'all' || workCategory !== 'all'; function resolveLatestRecoveryAction(game: GameDistributionMyGame) { const latest = game.latestVersion; @@ -645,6 +688,42 @@ export function MyGamesPage({ {tab === 'works' ? ( <> + {/* + 分类标签 + 数据概览(文档「个人二创作品管理页」)。 + 概览里的「累计衍生世代数」等后端字段存在时才渲染,缺失不拿别的数字凑。 + */} +
+ {( + [ + ['all', '全部作品'], + ['original', '原创作品'], + ['coCreated', '我发布的共创'], + ['draft', '我的草稿'], + ] as const + ).map(([key, label]) => ( + + ))} +
+
+ 总作品数 {games.length} + + 累计被 Fork 次数 {totalForkCount} + + {totalDerivedGenerations === null ? null : ( + + 累计衍生世代数 {totalDerivedGenerations} + + )} +
{error ? ( {error} @@ -729,16 +808,16 @@ export function MyGamesPage({ {canLoad && games.length > 0 && - visibleGames.length === 0 && + categoryGames.length === 0 && !isLoading ? ( 没有符合当前筛选条件的作品。 ) : null} - {canLoad && visibleGames.length > 0 ? ( + {canLoad && categoryGames.length > 0 ? (
- {visibleGames.map((game) => { + {categoryGames.map((game) => { const version = resolveVersionStatus(game); const latestVersion = game.latestVersion; const actionCopy = resolveLatestActionCopy(game); @@ -813,6 +892,13 @@ export function MyGamesPage({ ) : null} · 共创:{forkAuthorizationLabel} + {/* 文档要求卡片显示世代数与子衍生作品数量(世代取血缘 generation)。 */} + · + + {coCreationGenerationShortLabel(game.lineage?.generation ?? 0)} + + · + 子衍生作品 {forkCount}
{version?.reviewReason ? (