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), }; }