diff --git a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md index d9b9fa6a4..ca79346e7 100644 --- a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md +++ b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md @@ -320,3 +320,8 @@ | 同上路径但带 `Cookie` | **403 Forbidden** + `application/json`(`x-api-version: 2026-06-16`、`x-response-time-ms`)——发行读取**拒绝携带账号凭证**的请求,与本地用例断言一致 | **生产环境**(`genarrative.world`)同一时刻 `/api/game-distribution/games` 返回 **0 条**:说明当前生产还没有任何已发布的游戏,条目 ④「真实环境整链路」需要在生产发布一个作品后再跑。也就是说此刻能拿到的真实环境证据是「dev 部署的公开读路径 + 发行隔离」,写路径与审核链路的真实环境验收仍需生产账号。 + +## 网页侧交互修正(2026-09-30) + +- **广场首屏「立即试玩」不再替用户选中作品**:`src/components/game-distribution/GameGalleryPage.tsx` 原来是 `onClick={() => featuredGame && openDetail(featuredGame.id)}`,点一下等于直接打开第一款游戏的详情。现在按钮只把列表滚进视野(按平台页签面板计算相对偏移,`scrollTo({ behavior: 'smooth' })`,无 `scrollTo` 时退化成直接赋值),不再触发 `onOpenDetail`;`featuredGame` 派生随之退役。回归用例 `GameDistributionPages.test.tsx`「首屏「立即试玩」滚动到列表,而不是打开第一款游戏详情」断言 `onOpenDetail` 未被调用且滚动落在页签面板上。 +- 验证:`npx vitest run src/components/game-distribution/GameDistributionPages.test.tsx`(21 passed)、`npm run typecheck`、`npx eslint`、`npm run check:encoding`、`git diff --check` 通过。视觉与真机走查仍需人工/截图评审(沿用阶段 C 第 8 条未取证口径)。 diff --git a/src/components/game-distribution/GameDistributionPages.test.tsx b/src/components/game-distribution/GameDistributionPages.test.tsx index 15b3b8720..f550408c7 100644 --- a/src/components/game-distribution/GameDistributionPages.test.tsx +++ b/src/components/game-distribution/GameDistributionPages.test.tsx @@ -375,6 +375,50 @@ describe('GameGalleryPage 筛选与返回上下文', () => { ).toBe('180'), ); }); + + it('首屏「立即试玩」滚动到列表,而不是打开第一款游戏详情', async () => { + resetGalleryContext(); + const onOpenDetail = vi.fn(); + listGamesMock.mockResolvedValue([ + createGame({ id: 'game-1', title: '星轨防线' }), + createGame({ id: 'game-2', title: '第二款' }), + ]); + const scrollTo = vi.fn(); + const originalScrollTo = Object.getOwnPropertyDescriptor( + Element.prototype, + 'scrollTo', + ); + Object.defineProperty(Element.prototype, 'scrollTo', { + configurable: true, + writable: true, + value: scrollTo, + }); + try { + const { container } = render( +
+ +
, + ); + expect(await screen.findByText('星轨防线')).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: /立即试玩/u })); + + expect(onOpenDetail).not.toHaveBeenCalled(); + expect(scrollTo).toHaveBeenCalledTimes(1); + expect(scrollTo.mock.instances[0]).toBe( + container.querySelector('.platform-tab-panel'), + ); + expect(scrollTo.mock.calls[0]?.[0]).toMatchObject({ + behavior: 'smooth', + }); + } finally { + if (originalScrollTo) { + Object.defineProperty(Element.prototype, 'scrollTo', originalScrollTo); + } else { + delete (Element.prototype as { scrollTo?: unknown }).scrollTo; + } + } + }); }); describe('GamePlayPage 启动超时与旋转提示', () => { diff --git a/src/components/game-distribution/GameGalleryPage.tsx b/src/components/game-distribution/GameGalleryPage.tsx index 11636ccfc..36d888e05 100644 --- a/src/components/game-distribution/GameGalleryPage.tsx +++ b/src/components/game-distribution/GameGalleryPage.tsx @@ -145,6 +145,7 @@ export function GameGalleryPage({ const [error, setError] = useState(''); const requestSerialRef = useRef(0); const scrollContainerRef = useRef(null); + const gameListRef = useRef(null); const hasRestoredScrollRef = useRef(false); /** * 顶栏搜索词只在「真的变了」时覆盖目录筛选。 @@ -167,6 +168,31 @@ export function GameGalleryPage({ container instanceof HTMLElement ? container : null; }, []); + /** + * 首屏「立即试玩」只把列表滚进视野,不替用户选定某一款游戏。 + * + * 滚动容器是平台页签面板而不是窗口,所以按容器相对位置计算,缺少 + * `scrollTo` 时退化成直接赋值,保证无动画环境也能生效。 + */ + const scrollToGameList = useCallback(() => { + const anchor = gameListRef.current; + if (!anchor) return; + const container = scrollContainerRef.current; + if (!container) { + anchor.scrollIntoView?.({ behavior: 'smooth', block: 'start' }); + return; + } + const nextScrollTop = + container.scrollTop + + anchor.getBoundingClientRect().top - + container.getBoundingClientRect().top; + if (typeof container.scrollTo === 'function') { + container.scrollTo({ top: nextScrollTop, behavior: 'smooth' }); + return; + } + container.scrollTop = nextScrollTop; + }, []); + const loadGames = useCallback(() => { const requestSerial = ++requestSerialRef.current; setIsLoading(true); @@ -231,7 +257,6 @@ export function GameGalleryPage({ () => games.filter((game) => matchesGameDeviceFilter(game, device)), [device, games], ); - const featuredGame = useMemo(() => visibleGames[0] ?? null, [visibleGames]); const hasActiveFilters = activeKeyword.trim().length > 0 || category !== '全部' || device !== '全部'; const resetFilters = () => { @@ -270,7 +295,7 @@ export function GameGalleryPage({ featuredGame && openDetail(featuredGame.id)} + onClick={scrollToGameList} > @@ -282,7 +307,7 @@ export function GameGalleryPage({ -
+
发现好玩的

游戏广场