详情页差异改由「入口上下文」驱动:导航层按当前一侧写 from=co-creation,玩家侧(游戏 Tab/广场/搜索/收藏/我的游戏/默认深链)不看血缘与共创标签、开始共创弱化;登录态只决定能否共创
Project CI / Backend tests (pull_request) Failing after 34s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m48s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m1s
Project CI / Repository checks (pull_request) Failing after 25s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m10s
Project CI / Frontend tests (pull_request) Successful in 2m30s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m32s
Project CI / Native shell tests (pull_request) Successful in 7m24s
Project CI / Backend tests (pull_request) Failing after 34s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m48s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m1s
Project CI / Repository checks (pull_request) Failing after 25s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m10s
Project CI / Frontend tests (pull_request) Successful in 2m30s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m32s
Project CI / Native shell tests (pull_request) Successful in 7m24s
This commit is contained in:
@@ -61,7 +61,7 @@ import {
|
||||
VERSION_STATUS_LABEL,
|
||||
VERSION_STATUS_NEEDS_AUTHOR_ACTION,
|
||||
} from './gameVersionHistoryModel';
|
||||
import { isCoCreator } from './gameViewerRole';
|
||||
import { isCoCreator, resolveGameViewerContext } from './gameViewerRole';
|
||||
import {
|
||||
collectLineageAncestorChain,
|
||||
collectLineagePlayCountTotals,
|
||||
@@ -176,6 +176,18 @@ function GameDetailResolvedAsset({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情页的**入口来源**:导航层写在 URL 上的 `from`(`/games/detail?id=…&from=co-creation`)。
|
||||
*
|
||||
* 为什么用 query 而不是内存里的 stage:刷新 / 深链 / 分享链接都要稳定,
|
||||
* 而 stage 只活在内存里(`/games/detail` 刷新后 stage 仍是 `game-detail`,来源会丢)。
|
||||
* 非浏览器环境按「没有来源」处理 ⇒ 玩家侧。
|
||||
*/
|
||||
function readDetailEntrySource() {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return new URLSearchParams(window.location.search).get('from');
|
||||
}
|
||||
|
||||
export function GameDetailPage({
|
||||
gameId,
|
||||
onBack,
|
||||
@@ -540,11 +552,14 @@ function GameDetailContent({
|
||||
onOpenGameDetail?: (gameId: string) => void;
|
||||
}) {
|
||||
/**
|
||||
* 观看者角色(2026-10-07 产品口径:普通用户 = 游客 / 共创者 = 开发游戏的)。
|
||||
* **判据只有一处**(`isCoCreator`,当前口径 = 登录态,见该模块的「待产品确认」说明),
|
||||
* 页面上所有角色差异都用这一个布尔量驱动,不在 JSX 里散写 `isAuthenticated`。
|
||||
* 观看者所处的**入口上下文**(2026-10-07 产品口径更正:游客 = 点开游戏界面的玩家,
|
||||
* 不是「未登录访客」):由导航层写在 URL 上的来源决定,**与登录态无关**。
|
||||
* **判据只有一处**(`resolveGameViewerContext` + `isCoCreator`,见该模块的说明),
|
||||
* 页面上所有差异都用这一个布尔量驱动,不在 JSX 里散写来源判断。
|
||||
*/
|
||||
const coCreator = isCoCreator(isAuthenticated);
|
||||
const coCreator = isCoCreator(
|
||||
resolveGameViewerContext(readDetailEntrySource()),
|
||||
);
|
||||
/**
|
||||
* 只有已公开作品才有公开评价/评分投影;审核中、审核未通过、未公开、已下架
|
||||
* 一律不请求评价接口,封面位改为状态文案。
|
||||
@@ -878,8 +893,8 @@ function GameDetailContent({
|
||||
return (
|
||||
<div
|
||||
className="game-detail-page"
|
||||
// 角色差异的可观测面(探针/验收用):游客 / 共创者。
|
||||
data-viewer-role={coCreator ? 'co-creator' : 'guest'}
|
||||
// 入口上下文的可观测面(探针/验收用):玩家侧 / 共创者侧。
|
||||
data-viewer-context={coCreator ? 'co-creator' : 'player'}
|
||||
>
|
||||
<button type="button" className="game-back-button" onClick={onBack}>
|
||||
<ArrowLeft /> 返回
|
||||
@@ -1102,12 +1117,14 @@ function GameDetailContent({
|
||||
{/*
|
||||
「一键开始共创」从共创信息卡搬到主操作行(2026-10-07 用户口径):
|
||||
它是这部作品的主操作之一,和「立即玩 / 收藏」并列;共创卡内不再重复出现该按钮。
|
||||
**角色差异**(判据 `isCoCreator`):
|
||||
- 共创者:与改动前**完全一致**——实心强调 + lg 尺寸(48px / 0.94rem),文案「一键开始共创」,
|
||||
禁止共创时置灰 + 原因;
|
||||
- 游客:**不占用重点色**,缩成次要小按钮(描边 + 次要底色、32px / 0.78rem,见
|
||||
`.game-fork-remix-guest`),文案如实写「登录后开始共创」——不喊「一键共创」这种大口号。
|
||||
但**仍然可点可达**:点击走仓库既有的登录门禁(`onLogin`),既不禁用也不隐藏。
|
||||
**入口上下文差异**(判据 `isCoCreator`,来源写在 URL 的 `from` 上):
|
||||
- 共创者侧(共创 Tab / 族谱 / 对比页进来):与改动前**完全一致**——实心强调 + lg 尺寸
|
||||
(48px / 0.94rem),文案「一键开始共创」,禁止共创时置灰 + 原因;
|
||||
- 玩家侧(游戏 Tab / 广场 / 搜索 / 收藏 / 我的游戏 / 默认深链):**不占用重点色**,缩成
|
||||
次要小按钮(描边 + 次要底色、32px / 0.78rem,见 `.game-fork-remix-guest`),
|
||||
文案如实写「开始共创」/未登录时「登录后开始共创」——不喊「一键共创」这种大口号。
|
||||
但**仍然可点可达**:未登录走仓库既有的登录门禁(`onLogin`),既不禁用也不隐藏,
|
||||
登录态只决定「能不能真的开始共创」,不再决定「看不看得到血缘」。
|
||||
*/}
|
||||
{publiclyVisible ? (
|
||||
<>
|
||||
@@ -1122,7 +1139,7 @@ function GameDetailContent({
|
||||
}`
|
||||
: 'game-fork-remix-link game-fork-remix-guest'
|
||||
}
|
||||
data-viewer-role={coCreator ? 'co-creator' : 'guest'}
|
||||
data-viewer-context={coCreator ? 'co-creator' : 'player'}
|
||||
aria-disabled={forkAuthorization === 'forbidden'}
|
||||
aria-expanded={
|
||||
isAuthenticated && forkAuthorization !== 'forbidden'
|
||||
@@ -1157,7 +1174,9 @@ function GameDetailContent({
|
||||
? '一键开始共创'
|
||||
: forkAuthorization === 'forbidden'
|
||||
? '禁止共创'
|
||||
: '登录后开始共创'}
|
||||
: isAuthenticated
|
||||
? '开始共创'
|
||||
: '登录后开始共创'}
|
||||
</button>
|
||||
{isAuthenticated && isForkPanelOpen
|
||||
? createPortal(
|
||||
|
||||
@@ -220,6 +220,16 @@ afterEach(() => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
/*
|
||||
* 详情页的默认 fixture = **共创者侧**(`?from=co-creation`):这是 2026-10-07 之前
|
||||
* 唯一被实现/验收过的一侧,绝大多数用例(血缘图、共创标签、一键开始共创)讲的都是它。
|
||||
* 玩家侧的用例各自把 URL 显式设回 `/games/detail?id=…`(见「入口上下文」那两条)。
|
||||
*/
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/games/detail?id=game-1&from=co-creation',
|
||||
);
|
||||
authUiMock.value.user = { id: 'author-1' };
|
||||
authUiMock.value.canAccessProtectedData = true;
|
||||
authUiMock.value.openLoginModal.mockReset();
|
||||
@@ -2318,9 +2328,11 @@ describe('GameDetailPage 开始共创入口', () => {
|
||||
expect(screen.getByText(/当前设备无法直接唤起客户端/u)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('游客(未登录)入口是次要小按钮「登录后开始共创」,点击走仓库既有登录门禁而不是静默隐藏', async () => {
|
||||
it('玩家侧(默认入口,未登录)入口是次要小按钮「登录后开始共创」,点击走仓库既有登录门禁而不是静默隐藏', async () => {
|
||||
authUiMock.value.user = null;
|
||||
authUiMock.value.canAccessProtectedData = false;
|
||||
// 入口上下文 = 玩家侧:URL 上没有共创来源标记(默认深链也是玩家侧)。
|
||||
window.history.replaceState(null, '', '/games/detail?id=game-1');
|
||||
getGameMock.mockResolvedValueOnce(
|
||||
createGame({ forkAuthorization: 'full' }),
|
||||
);
|
||||
@@ -2331,7 +2343,7 @@ describe('GameDetailPage 开始共创入口', () => {
|
||||
const gate = await screen.findByRole('button', {
|
||||
name: '登录后开始共创',
|
||||
});
|
||||
// 文案如实:不给游客喊「一键开始共创」这种大口号。
|
||||
// 文案如实:不给玩家侧喊「一键开始共创」这种大口号。
|
||||
expect(screen.queryByRole('button', { name: '一键开始共创' })).toBeNull();
|
||||
expect(screen.queryByText(/从平台作品开始创作/u)).toBeNull();
|
||||
|
||||
@@ -2340,18 +2352,18 @@ describe('GameDetailPage 开始共创入口', () => {
|
||||
// 未登录既不唤起客户端,也不展开获取方式。
|
||||
expect(openForkProtocolMock).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText(/从平台作品开始创作/u)).toBeNull();
|
||||
// 角色差异可观测:根节点带 `data-viewer-role`,按钮走「游客」变体(不占重点色)。
|
||||
// 入口上下文可观测:根节点带 `data-viewer-context`,按钮走「玩家侧」变体(不占重点色)。
|
||||
expect(
|
||||
document
|
||||
.querySelector('.game-detail-page')
|
||||
?.getAttribute('data-viewer-role'),
|
||||
).toBe('guest');
|
||||
expect(gate.getAttribute('data-viewer-role')).toBe('guest');
|
||||
?.getAttribute('data-viewer-context'),
|
||||
).toBe('player');
|
||||
expect(gate.getAttribute('data-viewer-context')).toBe('player');
|
||||
expect(gate.className).toContain('game-fork-remix-guest');
|
||||
expect(gate.className).not.toContain('co-creation-fork-primary');
|
||||
});
|
||||
|
||||
it('游客变体的度量与配色:次要描边 + 32px/0.78rem,明显小于共创者的主操作(48px/0.94rem)', async () => {
|
||||
it('玩家侧变体的度量与配色:次要描边 + 32px/0.78rem,明显小于共创者侧的主操作(48px/0.94rem)', async () => {
|
||||
// jsdom 不算几何:用 CSS 源码契约把「主次差异」钉死(真机几何由 smoke 记录)。
|
||||
const css = readFileSync(
|
||||
'src/components/game-distribution/gameDistribution.css',
|
||||
@@ -3522,8 +3534,12 @@ describe('GameLineagePage 可视化与交互', () => {
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
);
|
||||
// ①'' 竖版画布有左侧代际轴 → 卡上不再重复「第 N 代」;玩数由卡面承载 → 角标里没有 ▶N。
|
||||
expect(cardButton.querySelector('.game-lineage-node__generation')).toBeNull();
|
||||
expect(cardButton.querySelector('.game-lineage-node__play-count')).toBeNull();
|
||||
expect(
|
||||
cardButton.querySelector('.game-lineage-node__generation'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
cardButton.querySelector('.game-lineage-node__play-count'),
|
||||
).toBeNull();
|
||||
// ② 卡内**没有任何动作按钮**(打开作品 / 开始共创 都已从卡面移除)。
|
||||
expect(within(cardButton).queryByRole('button')).toBeNull();
|
||||
expect(within(cardButton).queryByText('打开作品')).toBeNull();
|
||||
@@ -4468,22 +4484,22 @@ describe('GameLineagePage 筛选器 / 主干线 / 信息面板(文档对齐)
|
||||
expect(canvasButton.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(outlineButton.getAttribute('aria-pressed')).toBe('false');
|
||||
expect(
|
||||
document.querySelector('.game-lineage-filter__group')?.getAttribute(
|
||||
'aria-label',
|
||||
),
|
||||
document
|
||||
.querySelector('.game-lineage-filter__group')
|
||||
?.getAttribute('aria-label'),
|
||||
).toBe('视图形态');
|
||||
|
||||
fireEvent.click(outlineButton);
|
||||
expect(await screen.findByLabelText('创作族谱大纲')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('button', { name: '大纲视图' }).getAttribute(
|
||||
'aria-pressed',
|
||||
),
|
||||
screen
|
||||
.getByRole('button', { name: '大纲视图' })
|
||||
.getAttribute('aria-pressed'),
|
||||
).toBe('true');
|
||||
expect(
|
||||
screen.getByRole('button', { name: '图谱视图' }).getAttribute(
|
||||
'aria-pressed',
|
||||
),
|
||||
screen
|
||||
.getByRole('button', { name: '图谱视图' })
|
||||
.getAttribute('aria-pressed'),
|
||||
).toBe('false');
|
||||
});
|
||||
|
||||
@@ -4928,7 +4944,7 @@ describe('GameDetailPage 共创模块(文档对齐)', () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('游客与共创者(同一份数据):游客看不到血缘图/族谱入口/共创标签,共创者完全保留', async () => {
|
||||
it('入口上下文(同一登录态、同一份数据):玩家侧看不到血缘图/族谱入口/共创标签,共创者侧完全保留', async () => {
|
||||
const gameWithLineage = () =>
|
||||
createGame({
|
||||
id: 'game-current',
|
||||
@@ -4957,12 +4973,14 @@ describe('GameDetailPage 共创模块(文档对齐)', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// ① 游客(未登录):血缘相关内容与共创标签一律不渲染,但公开数据与主题入口保留。
|
||||
authUiMock.value.user = null;
|
||||
authUiMock.value.canAccessProtectedData = false;
|
||||
// ① 玩家侧(**已登录**,默认入口 URL):血缘相关内容与共创标签一律不渲染,
|
||||
// 但公开数据与主题入口保留。→ 这条同时证明「差异来自入口,不来自登录态」。
|
||||
authUiMock.value.user = { id: 'author-1' };
|
||||
authUiMock.value.canAccessProtectedData = true;
|
||||
window.history.replaceState(null, '', '/games/detail?id=game-current');
|
||||
getGameMock.mockResolvedValueOnce(gameWithLineage());
|
||||
getGameLineageMock.mockResolvedValueOnce(buildChainDataset());
|
||||
const guest = renderDetail();
|
||||
const player = renderDetail();
|
||||
await screen.findByText('累计衍生作品 3');
|
||||
expect(screen.queryByLabelText('创作血缘')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '全屏查看族谱' })).toBeNull();
|
||||
@@ -4978,15 +4996,22 @@ describe('GameDetailPage 共创模块(文档对齐)', () => {
|
||||
expect(
|
||||
document
|
||||
.querySelector('.game-detail-page')
|
||||
?.getAttribute('data-viewer-role'),
|
||||
).toBe('guest');
|
||||
?.getAttribute('data-viewer-context'),
|
||||
).toBe('player');
|
||||
// 玩家侧的「开始共创」是次要小按钮:已登录也不再喊「一键」。
|
||||
expect(
|
||||
screen.getByRole('button', { name: '开始共创' }).className,
|
||||
).toContain('game-fork-remix-guest');
|
||||
// 图谱页自己的「图下方父代/子代清单」也不会出现(整块不渲染)。
|
||||
expect(document.querySelector('.lineage-relation__grid')).toBeNull();
|
||||
guest.unmount();
|
||||
player.unmount();
|
||||
|
||||
// ② 共创者(已登录,默认 mock):与改动前一致 —— 图、入口、标签都在。
|
||||
authUiMock.value.user = { id: 'author-1' };
|
||||
authUiMock.value.canAccessProtectedData = true;
|
||||
// ② 共创者侧(**同一登录态**,只把入口来源换成共创侧):与改动前一致 —— 图、入口、标签都在。
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/games/detail?id=game-current&from=co-creation',
|
||||
);
|
||||
getGameMock.mockResolvedValueOnce(gameWithLineage());
|
||||
getGameLineageMock.mockResolvedValueOnce(buildChainDataset());
|
||||
renderDetail();
|
||||
@@ -5000,8 +5025,11 @@ describe('GameDetailPage 共创模块(文档对齐)', () => {
|
||||
expect(
|
||||
document
|
||||
.querySelector('.game-detail-page')
|
||||
?.getAttribute('data-viewer-role'),
|
||||
?.getAttribute('data-viewer-context'),
|
||||
).toBe('co-creator');
|
||||
expect(
|
||||
screen.getByRole('button', { name: '一键开始共创' }).className,
|
||||
).toContain('co-creation-fork-primary');
|
||||
});
|
||||
|
||||
it('「全屏查看族谱」= 真·全屏 overlay(portal 到 body / Esc 与关闭按钮 / 焦点与滚动恢复)', async () => {
|
||||
|
||||
@@ -1,18 +1,77 @@
|
||||
/**
|
||||
* 观看者角色(2026-10-07 产品口径):**普通用户 = 游客**(主要是玩游戏的),
|
||||
* **共创者 = 开发游戏的**;两者看到的**游戏详情页**有细微差异(见 `GameDetailPage`)。
|
||||
* 观看者所处的**入口上下文**(2026-10-07 产品口径更正):
|
||||
* **游客 = 来玩游戏的玩家侧**(不是「未登录访客」),**共创者 = 开发游戏的那一侧**。
|
||||
*
|
||||
* ⚠️ **待产品确认的口径**:仓库现有信号里没有能表达「共创者」的那一位 ——
|
||||
* - `capabilities`(`src/services/hostBridge.ts`)是 **AGC 宿主桥**的能力清单(剪贴板 / 分享 / 导航),
|
||||
* 与「这个人在平台上开不开发游戏」无关;
|
||||
* - `forkAuthorization` 是**作品级**的共创授权(禁止 / 非商用 / 全开放),是作品的属性而不是观看者的;
|
||||
* - 钱包 / 会员档位(`usePlatformWalletStore`)与「是否共创者」也没有既有的推导关系;
|
||||
* - 后端 DTO 里没有任何「账号能力位」字段(没有 `isCreator` / `creatorLevel` 之类)。
|
||||
* 口径来源:产品会上定的「普通用户是游客(主要是玩游戏的),共创者是开发游戏的」,
|
||||
* 并且明确「**游客是指点开游戏界面的用户**」——即差异取决于**从哪一侧点进来的**,
|
||||
* 与**登录态无关**(登录态只决定「能不能真的开始共创」,不再决定「看不看得到血缘」)。
|
||||
*
|
||||
* 所以这里**临时**用登录态近似:**未登录 = 游客,已登录 = 共创者**。
|
||||
* 不新增后端字段、不自己造权限系统:产品给出真正的判据(能力位 / 是否绑定客户端 / 是否发行过作品…)后,
|
||||
* **只需要改这一个函数**——详情页里所有「游客 / 共创者」差异都收敛在这一处判断上。
|
||||
* 因此这个模块的输入是**入口来源**,不是 `isAuthenticated`:
|
||||
* - 导航层(`PlatformEntryActiveFlowShell`)在打开详情页时按**当前所在的一侧**写入 URL 标记
|
||||
* `/games/detail?id=…&from=co-creation`;
|
||||
* - 详情页只读这个标记(见 `GameDetailPage`),所以**刷新 / 深链 / 分享链接都稳定**;
|
||||
* - 没有标记、或标记不是共创来源 ⇒ **玩家侧**(旧链接、直接把 URL 贴给别人的场景一律落玩家侧)。
|
||||
*
|
||||
* ⚠️ 若产品最终改为**账号角色 / 能力位**(例如后端下发 `isCreator` / `creatorLevel`),
|
||||
* **只需要替换这一个模块的实现**:`isCoCreator` 是所有差异的唯一判据,调用点不用改。
|
||||
*/
|
||||
export function isCoCreator(isAuthenticated: boolean): boolean {
|
||||
return isAuthenticated;
|
||||
|
||||
/** 详情页 URL 上的来源标记值:`/games/detail?id=…&from=co-creation`。 */
|
||||
export const CO_CREATION_DETAIL_SOURCE = 'co-creation';
|
||||
|
||||
/**
|
||||
* 「共创者侧」的导航阶段:从这些页面打开作品详情时算共创者侧。
|
||||
* - `game-co-creation`:共创 Tab(侧边栏顶层入口)
|
||||
* - `game-lineage`:创作族谱页
|
||||
* - `game-compare`:原版与二创对比页(玩家的详情页不渲染对比入口,玩家到不了这里)
|
||||
*
|
||||
* 不在表里的(游戏 Tab / 广场 / 搜索 / 收藏 / 我的游戏 / 共创主题 / 默认深链)都算**玩家侧**;
|
||||
* 其中「共创主题」页两边的用户都能到(详情页的主题入口对玩家也保留),
|
||||
* 按「玩家也能到 ⇒ 落玩家侧」处理,避免玩家从主题页点进去又看到共创视图。
|
||||
*/
|
||||
export const CO_CREATION_ORIGIN_STAGES: readonly string[] = [
|
||||
'game-co-creation',
|
||||
'game-lineage',
|
||||
'game-compare',
|
||||
];
|
||||
|
||||
export type GameViewerContext = 'player' | 'co-creator';
|
||||
|
||||
function isCoCreationSource(source: string | null | undefined) {
|
||||
return (source ?? '').trim().toLowerCase() === CO_CREATION_DETAIL_SOURCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源标记 → 观看者所处的一侧。**缺省 / 未知一律 = 玩家侧**。
|
||||
*/
|
||||
export function resolveGameViewerContext(
|
||||
source: string | null | undefined,
|
||||
): GameViewerContext {
|
||||
return isCoCreationSource(source) ? 'co-creator' : 'player';
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航层用:这一跳该不该带上「共创来源」标记?
|
||||
*
|
||||
* - 当前就在共创侧阶段(共创 Tab / 族谱 / 对比)→ 带上;
|
||||
* - 当前已经在**共创侧的详情页**里(血缘链、主题、相关作品之间跳)→ **保留**来源,别中途掉回玩家侧;
|
||||
* - 其余(游戏 Tab / 广场 / 我的游戏 / 默认深链)→ 不带。
|
||||
*
|
||||
* 只认 `co-creation` 这一个值:族谱页自己的 `from=<gameId>`(高亮当前作品)含义不同,不能被当成来源。
|
||||
*/
|
||||
export function shouldCarryCoCreationSource(input: {
|
||||
stage: string;
|
||||
currentSource?: string | null;
|
||||
}): boolean {
|
||||
if (CO_CREATION_ORIGIN_STAGES.includes(input.stage)) return true;
|
||||
return (
|
||||
input.stage === 'game-detail' && isCoCreationSource(input.currentSource)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是不是「共创者」这一侧。**详情页所有差异都由它驱动**(一个谓词,判断不散落在 JSX 各处)。
|
||||
*/
|
||||
export function isCoCreator(context: GameViewerContext): boolean {
|
||||
return context === 'co-creator';
|
||||
}
|
||||
|
||||
@@ -1447,21 +1447,20 @@ describe('创作族谱', () => {
|
||||
?.querySelector('.game-card');
|
||||
expect(treeNode).toBeTruthy();
|
||||
fireEvent.click(treeNode as HTMLElement);
|
||||
// 从族谱页(共创者侧)打开作品详情:来源标记**保留**下去,别中途掉回玩家侧。
|
||||
expect(setSelectionStage).toHaveBeenCalledWith('game-detail', {
|
||||
path: '/games/detail?id=game-root',
|
||||
path: '/games/detail?id=game-root&from=co-creation',
|
||||
});
|
||||
});
|
||||
|
||||
it('详情页的族谱入口带上根作品与当前作品', async () => {
|
||||
// 族谱入口只给**共创者**(2026-10-07 角色口径:游客只玩游戏,看不到血缘入口),
|
||||
// 所以这条用例先把观看者设成已登录(`isCoCreator` 当前口径 = 登录态)。
|
||||
authUiMock.value.user = {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
displayName: '测试用户',
|
||||
} as AuthUser;
|
||||
authUiMock.value.canAccessProtectedData = true;
|
||||
window.history.replaceState(null, '', '/games/detail?id=game-1');
|
||||
// 族谱入口只给**共创者侧**(2026-10-07 口径更正:看血缘与否取决于**入口上下文**,
|
||||
// 与登录态无关)——所以这里**不登录**,只把入口来源标成共创侧。
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/games/detail?id=game-1&from=co-creation',
|
||||
);
|
||||
gameDistributionMock.getGame.mockResolvedValue({
|
||||
id: 'game-1',
|
||||
title: '衍生作品',
|
||||
@@ -1558,6 +1557,66 @@ describe('创作族谱', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('打开详情页的来源标记:从共创 Tab 进 = 共创者侧,从游戏广场进 = 玩家侧(不带标记)', async () => {
|
||||
const oneGame = {
|
||||
id: 'game-1',
|
||||
title: '可以共创的作品',
|
||||
summary: 's',
|
||||
description: '',
|
||||
category: '动作',
|
||||
tags: [],
|
||||
coverColor: '#d77a51',
|
||||
icon: '✦',
|
||||
author: { id: 'author-1', name: '测试作者' },
|
||||
deviceSupport: { desktop: true, mobile: true, touch: true },
|
||||
status: 'published',
|
||||
currentVersion: null,
|
||||
playCount: 1,
|
||||
createdAt: '2026-09-18T00:00:00.000Z',
|
||||
coverObjectKey: null,
|
||||
screenshots: [],
|
||||
forkCount: 0,
|
||||
forkAuthorization: 'full',
|
||||
};
|
||||
gameDistributionMock.listGames.mockResolvedValue({
|
||||
games: [oneGame],
|
||||
nextCursor: null,
|
||||
});
|
||||
|
||||
// ① 共创 Tab(`/games/co-creation`)→ 带 `from=co-creation`
|
||||
window.history.replaceState(null, '', '/games/co-creation');
|
||||
const fromCoCreation = vi.fn();
|
||||
const first = render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="game-co-creation"
|
||||
setSelectionStage={fromCoCreation}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: /可以共创的作品/u }),
|
||||
);
|
||||
expect(fromCoCreation).toHaveBeenCalledWith('game-detail', {
|
||||
path: '/games/detail?id=game-1&from=co-creation',
|
||||
});
|
||||
first.unmount();
|
||||
|
||||
// ② 游戏广场(`/games`)→ **不带**标记(默认玩家侧)
|
||||
window.history.replaceState(null, '', '/games');
|
||||
const fromPlaza = vi.fn();
|
||||
render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="games"
|
||||
setSelectionStage={fromPlaza}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: /可以共创的作品/u }),
|
||||
);
|
||||
expect(fromPlaza).toHaveBeenCalledWith('game-detail', {
|
||||
path: '/games/detail?id=game-1',
|
||||
});
|
||||
});
|
||||
|
||||
describe('共创顶层入口(只列支持共创的作品)', () => {
|
||||
it('侧边栏「共创」直达 game-co-creation,游戏页不再有「游戏/共创」子页签', async () => {
|
||||
window.history.replaceState(null, '', '/games');
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
hasAppHistoryBackEntry,
|
||||
pushAppHistoryPath,
|
||||
replaceAppHistoryPath,
|
||||
resolveSelectionStageFromPath,
|
||||
} from '../../routing/activeAppPageRoutes';
|
||||
import {
|
||||
connectionsPath,
|
||||
@@ -44,6 +45,10 @@ import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { ClientDownloadEntry } from '../creation-home/ClientDownloadEntry';
|
||||
import { setCreatorViewer } from '../creator/creatorRelationshipState';
|
||||
import {
|
||||
CO_CREATION_DETAIL_SOURCE,
|
||||
shouldCarryCoCreationSource,
|
||||
} from '../game-distribution/gameViewerRole';
|
||||
import { PaymentCheckoutView } from './PaymentCheckoutView';
|
||||
import {
|
||||
PlatformActiveMobileWelcomeDialog,
|
||||
@@ -558,6 +563,27 @@ export function PlatformEntryFlowShellImpl({
|
||||
// 作者视角要一直带着 owner 作用域:刷新或直接打开该链接时详情页仍读
|
||||
// `GET /my-games/{gameId}`,否则作者点自己的未公开作品只会拿到公开接口的 404。
|
||||
if (options?.owner) params.set('owner', '1');
|
||||
/*
|
||||
* **入口上下文**(2026-10-07 产品口径更正:游客 = 点开游戏界面的玩家,不是「未登录访客」):
|
||||
* 从共创侧(共创 Tab / 族谱页 / 对比页)打开详情时带上 `from=co-creation`,
|
||||
* 详情页据此决定「看不看得到血缘图与共创标签」;其余来源 = 玩家侧(不带标记)。
|
||||
* 已经在共创侧详情页里继续跳(血缘链 / 主题 / 相关作品)时保留标记,不中途掉回玩家侧。
|
||||
* 用 URL 而不是内存 stage:刷新、深链、分享链接都稳定。
|
||||
*/
|
||||
const currentStage = resolveSelectionStageFromPath(
|
||||
window.location.pathname,
|
||||
);
|
||||
const currentSource = new URLSearchParams(window.location.search).get(
|
||||
'from',
|
||||
);
|
||||
if (
|
||||
shouldCarryCoCreationSource({
|
||||
stage: currentStage,
|
||||
currentSource,
|
||||
})
|
||||
) {
|
||||
params.set('from', CO_CREATION_DETAIL_SOURCE);
|
||||
}
|
||||
const path = `/games/detail?${params.toString()}`;
|
||||
setSelectionStage('game-detail', { path });
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user