跟上主题详情契约变更:memberCount 为真实可见数 + rootsTruncated 截断信号
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled

- 客户端: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 棵作品树」
This commit is contained in:
2026-10-06 04:30:41 +08:00
parent 0d0166c2d9
commit d6d3913320
4 changed files with 62 additions and 0 deletions
@@ -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(
<GameThemePage
themeId="theme-3"
onBack={vi.fn()}
onOpenDetail={vi.fn()}
/>,
);
// 总数是真实可见成员数(120),不是这一页拿到的 1 个。
expect(await screen.findByText('共 120 棵作品树')).toBeTruthy();
expect(screen.getByText(/这里只显示前 1 个,共 120 个/u)).toBeTruthy();
expect(screen.queryByText('共 1 棵作品树')).toBeNull();
});
});
describe('GameDetailPage 共创主题入口', () => {
@@ -158,6 +158,22 @@ export function GameThemePage({
</div>
) : null}
{theme ? (
<p className="game-theme-summary__meta">
共 {theme.memberCount} 棵作品树
</p>
) : null}
{theme?.rootsTruncated ? (
/*
总数一律用 `memberCount`(真实可见成员数,不截断);`roots.length` 只是这一页拿到几个。
两者不再保证相等,所以这里显式区分「显示前 N 个」与「共 M 个」。
*/
<p className="game-lineage-notice" role="status">
这个主题的根较多:这里只显示前 {theme.roots.length} 个,共{' '}
{theme.memberCount} 个。
</p>
) : null}
{isLoading && !theme ? (
<div className="game-loading-panel">正在加载共创主题…</div>
) : null}
@@ -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;
+10
View File
@@ -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),
};
}