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