From d6d39133208f70f8c8de0dff87de7f6ce725283f Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 6 Oct 2026 04:30:41 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=9F=E4=B8=8A=E4=B8=BB=E9=A2=98=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=A5=91=E7=BA=A6=E5=8F=98=E6=9B=B4=EF=BC=9AmemberCou?= =?UTF-8?q?nt=20=E4=B8=BA=E7=9C=9F=E5=AE=9E=E5=8F=AF=E8=A7=81=E6=95=B0=20+?= =?UTF-8?q?=20rootsTruncated=20=E6=88=AA=E6=96=AD=E4=BF=A1=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 客户端:GameDistributionThemeDetail 增加必填 rootsTruncated;getTheme 的响应类型与返回值透传该键(缺键按 false,空成员时服务端也是 false 而不是缺键) - 主题页:新增「共 N 棵作品树」,总数一律用 memberCount(真实可见成员数,不受 50 上限截断),不再用 roots.length;rootsTruncated 为 true 时给出明确提示「这里只显示前 A 个,共 B 个」,与族谱页的 truncated 同一约定 - 顺带确认:全仓没有别处按 memberCount === roots.length 做不变式(此前只有主题列表页展示 memberCount,主题页未展示计数) - 测试:新增一条——rootsTruncated=true 且 memberCount=120 / roots 只有 1 个时,截断提示出现且总数展示 120,不得出现用 roots.length 算出的「共 1 棵作品树」 --- .../GameDistributionPages.test.tsx | 30 +++++++++++++++++++ .../game-distribution/GameThemePage.tsx | 16 ++++++++++ .../game-distribution/gameDistribution.css | 6 ++++ src/services/gameDistributionClient.ts | 10 +++++++ 4 files changed, 62 insertions(+) diff --git a/src/components/game-distribution/GameDistributionPages.test.tsx b/src/components/game-distribution/GameDistributionPages.test.tsx index 094492635..e9a9552a2 100644 --- a/src/components/game-distribution/GameDistributionPages.test.tsx +++ b/src/components/game-distribution/GameDistributionPages.test.tsx @@ -1911,6 +1911,36 @@ describe('GameThemePage', () => { expect(await screen.findByText('主题不存在')).toBeTruthy(); expect(screen.getByRole('button', { name: '重试' })).toBeTruthy(); }); + + it('roots 被截断时给出明确提示,且总数用 memberCount 而不是 roots 长度', async () => { + getThemeMock.mockResolvedValue({ + themeId: 'theme-3', + name: '大主题', + summary: '', + badge: '', + memberCount: 120, + rootsTruncated: true, + roots: [rootGame('game-root-1', '根一')], + }); + getGameLineageMock.mockResolvedValue({ + rootGameId: 'game-root-1', + root: null, + nodes: [], + truncated: false, + }); + render( + , + ); + + // 总数是真实可见成员数(120),不是这一页拿到的 1 个。 + expect(await screen.findByText('共 120 棵作品树')).toBeTruthy(); + expect(screen.getByText(/这里只显示前 1 个,共 120 个/u)).toBeTruthy(); + expect(screen.queryByText('共 1 棵作品树')).toBeNull(); + }); }); describe('GameDetailPage 共创主题入口', () => { diff --git a/src/components/game-distribution/GameThemePage.tsx b/src/components/game-distribution/GameThemePage.tsx index 7928b50cd..0bd54af67 100644 --- a/src/components/game-distribution/GameThemePage.tsx +++ b/src/components/game-distribution/GameThemePage.tsx @@ -158,6 +158,22 @@ export function GameThemePage({ ) : null} + {theme ? ( +

+ 共 {theme.memberCount} 棵作品树 +

+ ) : null} + {theme?.rootsTruncated ? ( + /* + 总数一律用 `memberCount`(真实可见成员数,不截断);`roots.length` 只是这一页拿到几个。 + 两者不再保证相等,所以这里显式区分「显示前 N 个」与「共 M 个」。 + */ +

+ 这个主题的根较多:这里只显示前 {theme.roots.length} 个,共{' '} + {theme.memberCount} 个。 +

+ ) : null} + {isLoading && !theme ? (
正在加载共创主题…
) : null} diff --git a/src/components/game-distribution/gameDistribution.css b/src/components/game-distribution/gameDistribution.css index 9c2d0d939..85ad651db 100644 --- a/src/components/game-distribution/gameDistribution.css +++ b/src/components/game-distribution/gameDistribution.css @@ -636,6 +636,12 @@ line-height: 1.6; overflow-wrap: anywhere; } +.game-theme-summary__meta { + margin: 0; + color: var(--platform-text-soft); + font-size: 0.8rem; + overflow-wrap: anywhere; +} .game-theme-trees { display: flex; flex-direction: column; diff --git a/src/services/gameDistributionClient.ts b/src/services/gameDistributionClient.ts index c0dbcc563..7d5f6dae3 100644 --- a/src/services/gameDistributionClient.ts +++ b/src/services/gameDistributionClient.ts @@ -400,6 +400,13 @@ export type GameDistributionThemeSummary = { /** 主题详情:摘要 + 成员根清单(逐条是公开目录同一份投影)。 */ export type GameDistributionThemeDetail = GameDistributionThemeSummary & { roots: GameDistributionGame[]; + /** + * `roots` 是否被服务端的响应体积上限截断(与族谱响应的 `truncated` 同一种约定)。 + * + * **`memberCount` 与 `roots.length` 不再保证相等**:`memberCount` 是真实可见成员数(不截断), + * `roots` 可能只含前 50 个。展示总数一律用 `memberCount`,这个布尔只负责决定要不要提示。 + */ + rootsTruncated: boolean; }; function normalizeThemeList(value: unknown): GameDistributionThemeSummary[] { @@ -464,6 +471,8 @@ export async function getTheme( summary?: string; badge?: string; memberCount?: number; + /** `roots` 被响应体积上限截断:与 `roots` 同层,空成员时也是 `false`(不是缺键)。 */ + rootsTruncated?: boolean; roots?: unknown; }>( `/themes/${encodeURIComponent(normalizedThemeId)}`, @@ -477,6 +486,7 @@ export async function getTheme( summary: payload.summary ?? '', badge: payload.badge ?? '', memberCount: payload.memberCount ?? 0, + rootsTruncated: payload.rootsTruncated === true, roots: normalizeGameList(payload.roots), }; }