From 301f66da2d40de368c26276b00275be99fe7a9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 30 Sep 2026 11:44:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=B8=E7=8E=A9=E9=A1=B5=E6=94=B9=E6=88=90?= =?UTF-8?q?=E5=9B=BA=E5=AE=9A=E7=94=BB=E5=B8=83=EF=BC=8C=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF=E4=B8=8D=E5=86=8D=E6=95=B4=E9=A1=B5=E5=8F=AF=E6=BB=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlatformEntryActiveFlowShell 在 game-play 舞台给页签容器加 platform-tab-panel--game-play,不再挂 overflow-auto,也不再叠加双份内边距 - index.css 新增 .platform-tab-panel--game-play,面板 overflow:hidden 并去掉右侧留白 - gameDistribution.css 让 .game-play-page 改 height:100%/min-height:0、播放器 flex:1 1 auto/min-height:0,删除移动端把页面撑高的 calc(100dvh - 8rem) - 矮视口(max-height:26rem,横屏手机)压扁启动面板并隐藏封面,保证「开始游戏」可达,页面与面板本身仍不滚动 - PlatformEntryActiveFlowShell.test.tsx 双向锁定游玩舞台页签容器不滚动、游戏广场等舞台仍保持滚动 - check-game-distribution-web-e2e.mjs 移动端段新增「移动端游玩页不可滚动(面板与整页都不溢出)」断言 - 里程碑文档补上该修复的根因、口径与验证记录 --- ...‹碑】游戏分发目录详情与在线游玩-2026-09-18.md | 6 ++- scripts/check-game-distribution-web-e2e.mjs | 17 ++++++++ .../game-distribution/gameDistribution.css | 41 +++++++++++++++---- .../PlatformEntryActiveFlowShell.test.tsx | 29 +++++++++++++ .../PlatformEntryActiveFlowShell.tsx | 7 +++- src/index.css | 6 +++ 6 files changed, 96 insertions(+), 10 deletions(-) diff --git a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md index ca79346e7..015600f54 100644 --- a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md +++ b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md @@ -321,7 +321,9 @@ **生产环境**(`genarrative.world`)同一时刻 `/api/game-distribution/games` 返回 **0 条**:说明当前生产还没有任何已发布的游戏,条目 ④「真实环境整链路」需要在生产发布一个作品后再跑。也就是说此刻能拿到的真实环境证据是「dev 部署的公开读路径 + 发行隔离」,写路径与审核链路的真实环境验收仍需生产账号。 -## 网页侧交互修正(2026-09-30) +## 网页侧两处交互修正(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 条未取证口径)。 +- **游玩页不再可滚**:移动端此前是整页可滚的半屏播放器。原因是面板仍是 `overflow-auto`,而播放器用 `min-height: calc(100dvh - 8rem)` 撑高页面。现在 `PlatformEntryActiveFlowShell.tsx` 在 `game-play` 舞台给页签容器加 `platform-tab-panel--game-play`(不滚动、去掉多余内边距),`.game-play-page` 改为 `height: 100%; min-height: 0`,播放器 `flex: 1; min-height: 0` 撑满剩余高度;矮视口(`max-height: 26rem`,横屏手机)另给启动面板一套压扁布局,兜底只在极端高度下发生在启动卡片内部,页面与面板本身始终不滚动。回归用例 `PlatformEntryActiveFlowShell.test.tsx`「游玩舞台的页签容器不滚动」「游戏广场等舞台仍保持页签容器滚动」双向锁定。 +- 真机口径的守卫补在现役 E2E 里:`scripts/check-game-distribution-web-e2e.mjs` 的移动端段新增「移动端游玩页不可滚动(面板与整页都不溢出)」,断言 `.platform-tab-panel` 的 `overflow-y` 为 `hidden` 且面板与整页都不溢出(该脚本需真实本地栈,未在本轮执行)。 +- 验证:`npx vitest run src/components/game-distribution/GameDistributionPages.test.tsx`(21 passed)、`npx vitest run src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx`(20 passed)、`npm run typecheck`、`npx eslint`(四个改动文件)、`npm run check:encoding`、`git diff --check` 全部通过。视觉与真机横竖屏走查仍需人工/截图评审(沿用阶段 C 第 8 条未取证口径)。 diff --git a/scripts/check-game-distribution-web-e2e.mjs b/scripts/check-game-distribution-web-e2e.mjs index 60ba8f326..73a322544 100644 --- a/scripts/check-game-distribution-web-e2e.mjs +++ b/scripts/check-game-distribution-web-e2e.mjs @@ -522,6 +522,23 @@ async function main() { mobileStartDisabled && rotationHint, `startDisabled=${mobileStartDisabled} rotationHint=${rotationHint}`, ); + // 游玩页是固定画布:面板必须是 overflow:hidden 且不溢出,整页也不能出现滚动条。 + const playScroll = await mobilePage.evaluate(() => { + const panel = document.querySelector('.platform-tab-panel'); + return { + documentOverflow: + document.documentElement.scrollHeight - window.innerHeight, + panelOverflow: panel ? panel.scrollHeight - panel.clientHeight : 0, + panelOverflowY: panel ? getComputedStyle(panel).overflowY : 'missing', + }; + }); + check( + '移动端游玩页不可滚动(面板与整页都不溢出)', + playScroll.documentOverflow <= 1 && + playScroll.panelOverflow <= 1 && + playScroll.panelOverflowY === 'hidden', + JSON.stringify(playScroll), + ); await mobile.close(); await context.close(); } finally { diff --git a/src/components/game-distribution/gameDistribution.css b/src/components/game-distribution/gameDistribution.css index c24e193f5..dad457374 100644 --- a/src/components/game-distribution/gameDistribution.css +++ b/src/components/game-distribution/gameDistribution.css @@ -494,7 +494,9 @@ display: flex; flex-direction: column; gap: 0.9rem; - min-height: 100%; + /* 游玩页是固定画布:撑满面板而不是撑高面板,页面本身永不滚动。 */ + height: 100%; + min-height: 0; padding: clamp(0.7rem, 1.8vw, 1.5rem); } .game-play-toolbar { @@ -536,8 +538,8 @@ } .game-player-shell { position: relative; - flex: 1; - min-height: min(72vh, 48rem); + flex: 1 1 auto; + min-height: 0; overflow: hidden; border: 1px solid rgba(199, 101, 61, 0.24); border-radius: 1.5rem; @@ -581,7 +583,7 @@ display: block; width: 100%; height: 100%; - min-height: min(72vh, 48rem); + min-height: 0; border: 0; opacity: 0; transition: opacity 220ms ease; @@ -716,9 +718,34 @@ .game-play-toolbar__actions .game-play-device-hint { display: none; } - .game-player-shell, - .game-player-frame { - min-height: calc(100dvh - 8rem); +} + +/* + * 矮视口(横屏手机)里播放器只剩很小的高度,压扁启动面板保证 + * 「开始游戏」可达;页面依旧不滚动,只有这一块在极端情况下内部兜底。 + */ +@media (max-height: 26rem) { + .game-player-launch { + grid-template-columns: minmax(0, 1fr); + align-content: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + text-align: center; + overflow-y: auto; + } + .game-player-launch .game-cover { + display: none; + } + .game-player-launch__copy p { + display: -webkit-box; + margin: 0.25rem 0 0.6rem; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + .game-player-loading, + .game-player-timeout { + font-size: 0.8rem; } } diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx index 92c5cbd8f..68cec511c 100644 --- a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx @@ -898,6 +898,35 @@ describe('游戏发布灰度入口', () => { ); }); + it('游玩舞台的页签容器不滚动', async () => { + gameDistributionMock.getGame.mockResolvedValue(null); + + const { container } = render( + , + ); + + const panel = container.querySelector('.platform-tab-panel'); + expect(panel?.classList.contains('platform-tab-panel--game-play')).toBe( + true, + ); + expect(panel?.className).not.toContain('overflow-auto'); + }); + + it('游戏广场等舞台仍保持页签容器滚动', async () => { + gameDistributionMock.listGames.mockResolvedValue([]); + + const { container } = render( + , + ); + expect(await screen.findByText('游戏广场')).toBeTruthy(); + + const panel = container.querySelector('.platform-tab-panel'); + expect(panel?.classList.contains('platform-tab-panel--game-play')).toBe( + false, + ); + expect(panel?.className).toContain('overflow-auto'); + }); + it('灰度命中时广场展示发布入口', async () => { loginAsAuthor(); gameDistributionMock.listGames.mockResolvedValue([]); diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx index c5029d2e0..6a4f6ba7d 100644 --- a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx @@ -540,6 +540,11 @@ export function PlatformEntryFlowShellImpl({ selectionStage === 'game-play' || selectionStage === 'game-mine' || selectionStage === 'game-publish'; + /** + * 游玩页自己撑满剩余高度,面板不能再滚动,否则移动端会出现整页可滚的 + * 半屏播放器;其余舞台继续沿用面板滚动。 + */ + const isGamePlayStage = selectionStage === 'game-play'; const gameSearchParams = new URLSearchParams( typeof window === 'undefined' ? '' : window.location.search, ); @@ -717,7 +722,7 @@ export function PlatformEntryFlowShellImpl({
{isProfileStage ? (