实现游戏共创 M3:创作族谱树与衍生列表
- 新增 module-game-distribution/src/lineage.rs:锚点可读性、可见性过滤、稳定排序、上限截断的纯函数,附 15 个单测(复用 M1 的软删除判定,不重写第二套规则)
- 新增公开只读 procedure get_game_distribution_lineage / list_game_distribution_derived_games;锚点必须公开可读(未公开/已软删除/不存在一律按不存在处理),树内只出现未软删除且已公开的节点,不补 null 占位节点
- api-server 新增 GET /games/{id}/lineage 与 /games/{id}/derived(匿名可读、no-store),响应走结构化 DTO,节点键集合由测试钉死,杜绝下发对象键/素材键
- 作者自有游戏聚合追加 forkCount(与公开详情 forkCount 同口径),供「被改编 N」入口使用
- shared-contracts 与 TS 契约新增 LineageNode / LineageResponse / DerivedResponse 三型并登记 DTO 一致性检查
- 前端新增 /games/lineage 族谱页,登记 stage 类型、路由表、页面标题与壳层渲染分支;详情页共创卡新增「查看创作族谱」入口(仅已公开作品)
- 我的作品页新增「被改编 N」行内展开,列出直接子代的标题/作者/代际/状态,含空态与失败重试(仅已公开作品)
- 补前端组件/路由/标题/客户端用例与 api-server 路由、负载映射用例;同步里程碑验收标准与技术方案 §3.4 的锚点可读性口径
This commit is contained in:
@@ -49,7 +49,9 @@ const gameDistributionMock = vi.hoisted(() => ({
|
||||
createGame: vi.fn(),
|
||||
createGameVersion: vi.fn(),
|
||||
deleteMyGame: vi.fn(),
|
||||
getDerivedGames: vi.fn(),
|
||||
getGame: vi.fn(),
|
||||
getGameLineage: vi.fn(),
|
||||
getGameVersion: vi.fn(),
|
||||
getMyGame: vi.fn(),
|
||||
listGames: vi.fn(),
|
||||
@@ -1172,3 +1174,116 @@ describe('作品管理 owner 作用域', () => {
|
||||
expect(gameDistributionMock.getMyGame).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('创作族谱', () => {
|
||||
const lineageTree = {
|
||||
rootGameId: 'game-root',
|
||||
root: {
|
||||
gameId: 'game-root',
|
||||
title: '母版',
|
||||
authorName: '原作者',
|
||||
generation: 0,
|
||||
parentGameId: null,
|
||||
playCount: 3,
|
||||
status: 'published',
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
gameId: 'game-root',
|
||||
title: '母版',
|
||||
authorName: '原作者',
|
||||
generation: 0,
|
||||
parentGameId: null,
|
||||
playCount: 3,
|
||||
status: 'published',
|
||||
},
|
||||
{
|
||||
gameId: 'game-leaf',
|
||||
title: '叶节点',
|
||||
authorName: '改编者',
|
||||
generation: 1,
|
||||
parentGameId: 'game-root',
|
||||
playCount: 1,
|
||||
status: 'published',
|
||||
},
|
||||
],
|
||||
truncated: false,
|
||||
};
|
||||
|
||||
it('族谱页按根作品渲染树,未登录也能浏览并跳转节点', async () => {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/games/lineage?id=game-root&from=game-leaf',
|
||||
);
|
||||
gameDistributionMock.getGameLineage.mockResolvedValue(lineageTree);
|
||||
const setSelectionStage = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="game-lineage"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('叶节点')).toBeTruthy();
|
||||
expect(gameDistributionMock.getGameLineage).toHaveBeenCalledWith(
|
||||
'game-root',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '母版' }));
|
||||
expect(setSelectionStage).toHaveBeenCalledWith('game-detail', {
|
||||
path: '/games/detail?id=game-root',
|
||||
});
|
||||
});
|
||||
|
||||
it('详情页的族谱入口带上根作品与当前作品', async () => {
|
||||
window.history.replaceState(null, '', '/games/detail?id=game-1');
|
||||
gameDistributionMock.getGame.mockResolvedValue({
|
||||
id: 'game-1',
|
||||
title: '衍生作品',
|
||||
summary: '已发布',
|
||||
description: '',
|
||||
category: '动作',
|
||||
tags: [],
|
||||
coverColor: '#d77a51',
|
||||
icon: '✦',
|
||||
author: { id: 'author-1', name: '测试作者' },
|
||||
deviceSupport: { desktop: true, mobile: true, touch: true },
|
||||
status: 'published',
|
||||
currentVersion: {
|
||||
id: 'version-1',
|
||||
version: '1.0.0',
|
||||
entryUrl: 'https://play.example.test/releases/version-1/index.html',
|
||||
sha256: 'sha256',
|
||||
publishedAt: '2026-09-18T00:00:00.000Z',
|
||||
controls: ['键盘'],
|
||||
},
|
||||
playCount: 1,
|
||||
createdAt: '2026-09-18T00:00:00.000Z',
|
||||
forkCount: 0,
|
||||
lineage: {
|
||||
generation: 1,
|
||||
rootGameId: 'game-root',
|
||||
rootTitle: '母版',
|
||||
parentGameId: 'game-root',
|
||||
parentTitle: '母版',
|
||||
parentAuthorName: '原作者',
|
||||
},
|
||||
});
|
||||
const setSelectionStage = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="game-detail"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '查看创作族谱' }),
|
||||
);
|
||||
expect(setSelectionStage).toHaveBeenCalledWith('game-lineage', {
|
||||
path: '/games/lineage?id=game-root&from=game-1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user