diff --git a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md index d9b9fa6a4..7d22dd8c7 100644 --- a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md +++ b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md @@ -320,3 +320,16 @@ | 同上路径但带 `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` 未被调用且滚动落在页签面板上。 +- **游玩页不再可滚**:移动端此前是整页可滚的半屏播放器。原因是面板仍是 `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`,横屏手机)另给启动面板一套压扁布局,兜底只在极端高度下发生在启动卡片内部,页面与面板本身始终不滚动。启动面板纵向对齐用 `safe center`(`center` 作旧浏览器回退)并把 `overflow-y: auto` 放在基础规则:居中溢出会把顶部标题裁到 `scrollTop = 0` 也够不到的位置,只加内部滚动并不能修掉。回归用例 `PlatformEntryActiveFlowShell.test.tsx`「游玩舞台的页签容器不滚动」「游戏广场等舞台仍保持页签容器滚动」双向锁定。 +- **补齐上面这条的空守卫**:`.platform-tab-panel--game-play { overflow: hidden }` 一开始只有单类名 (0,1,0),被文件更靠后的 `.platform-desktop-shell--workbench .platform-tab-panel { overflow: auto }`((0,2,0))压掉,真实 Chromium 里 `overflow-y` 仍是 `auto`——只是当时内容恰好没溢出,测试与肉眼都没发现。现在覆盖规则带上 workbench 祖先一起写,实测 390x844/360x640/844x390 三档 `overflowY=hidden`、面板与整页横竖都不溢出,广场舞台仍是 `overflow: auto` 且内容可滚。详情见 pitfalls 同日条目。 +- **「我的游戏」列表与画廊同口径**:`.my-game-list` 的单列 `auto` 轨道会被 `white-space: nowrap` 的简介顶到内容最小宽度(实测 592px),加上 `.my-game-card` 缺 `min-width: 0`,390/360 宽的手机上卡片超出视口、面板横向可滚且看着不居中。现在改成画廊那套口径(`grid-template-columns: minmax(0, 1fr)`、`.my-game-card { min-width: 0; overflow: hidden }`、标题 `min-width: 0; overflow-wrap: anywhere`);真实 Chromium 夹具在 390/360 下量到轨道=卡片=可视宽、面板横向溢出 0(改前 592px 卡片、横向可滚 230/260px)。 +- **页签面板左右留白对齐**:`.platform-tab-panel` 原本写死 `padding-right`(并在移动端与 workbench 两处再压成 0),而它是不分层规则、优先级高于 Tailwind 的 `padding-inline`,导致面板左 12/24px、右 0,列表左右留白 28/16。三处 `padding-right` 删除后左右统一由使用处的 `px-3`/`sm:px-6` 决定:390 下 12/12(列表 28/28)、820 下 24/24(列表 44/44),无 px 类的舞台仍是 0/0;Chromium 夹具实测「我的游戏」与画廊两张页面同时恢复对称,且横向溢出保持 0。 +- **广场的「发布游戏」「我的游戏」上移到 hero 右上角**:两个入口原本混在 hero 下方的 `.game-toolbar` 动作区(和搜索框同排),现在提到 `.game-page-actions` 一行、右对齐放在 hero 上方,并留 1.1rem 底部间距;工具栏只保留标题与搜索。未登录或灰度未命中时两者都不渲染,该行随之消失。Chromium 夹具实测 390/360/820 三档都是 `aboveHero=true`、与 hero 间距 18px、右边缘与 hero 对齐。 +- 真机口径的守卫补在现役 E2E 里:`scripts/check-game-distribution-web-e2e.mjs` 的移动端段新增「移动端游玩页不可滚动(面板与整页都不溢出)」,断言 `.platform-tab-panel` 的 `overflow-y` 为 `hidden`,且面板与整页**横竖两轴**都不溢出(面板自身 `overflow-x: hidden`,横向溢出只会被静默裁掉,只量 `scrollHeight` 会漏掉「内容被裁一半」)。(该脚本需要管理员账号发布一款真实游戏,未在本轮执行;同一批数值用 Chromium 直连 dev 栈的 `/games/play?id=…` 量过,见 pitfalls 同日条目。) +- **详情页返回按来源回到上一页**:`GameDetailPage` 左上角原来是写死的「返回游戏广场」,并且直接 `setSelectionStage('games')`(等于 push 一条新的 `/games`)。从「我的游戏」点进详情再返回就落到广场,从广场进详情返回也会多压一条重复历史。现在文案简化为「返回」,行为改成优先 `window.history.back()`,由 `ActiveApp` 已有的 `popstate` 同步把舞台还原成真正的来源页(我的游戏 / 广场);游玩页的返回同样处理,避免详情↔游玩互相 push。协议侧给应用写入的历史条目补了 `__genarrativeAppHistoryDepth`,新增 `hasAppHistoryBackEntry()` 判断「当前条目是应用内导航写入的且存在上一页」;直接打开详情深链、或原生壳里没有可回退条目时,才 `replaceAppHistoryPath` 兜底到广场(游玩页兜底到自己的详情)。回归用例 `activeAppPageRoutes.test.ts` 锁定深度标记语义,`PlatformEntryActiveFlowShell.test.tsx`「游戏详情返回」两条锁定原生返回与深链兜底。 +- **游戏分发的返回文案统一成「返回」**:`GameDetailPage`、`GamePlayPage`(含超时面板里的按钮)、`MyGamesPage`、`GamePublishPage` 四处原本是「返回游戏广场 / 返回详情」,同一套流程里出现三种写法;现在统一成「返回」,目标页仍由各自的 `onBack`(广场 / 我的游戏 / 详情)决定,按钮不再承诺一个可能不对的目的地。`GameDistributionPages.test.tsx` 的断言同步改为 `/^返回$/u`。游玩页工具栏上那个按钮展示的是游戏名(退出开始面板后就靠它认游戏),不属于「返回 xx」文案,保持不动。 +- 验证:`npx vitest run src/components/game-distribution/GameDistributionPages.test.tsx`(21 passed)、`npx vitest run src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx`(22 passed)、`npx vitest run src/routing/activeAppPageRoutes.test.ts`、`npm run typecheck`、`npx eslint`(五个改动文件)、`npm run check:encoding`、`git diff --check` 全部通过;启动面板的溢出可达性、「我的游戏」移动端横向溢出都用真实 Chromium 静态夹具量过(见 pitfalls 同日条目),详情返回链在真实 Chromium(dev 栈 390x844)实测 `/games`→详情→返回=`/games`、深链直开详情→返回=`/games`、详情→立即玩→返回详情=`/games/detail?id=…`。视觉与真机横竖屏走查仍需人工/截图评审(沿用阶段 C 第 8 条未取证口径)。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index c947dc7d0..1a301b3ba 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6108,6 +6108,39 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **做法**:`npm run check:agc-direct-execution-fixture -- --agc-exe "<已安装渠道包的主程序绝对路径>" --cases completed`。夹具在临时项目和 loopback Provider 上跑 CLI 执行链路,可直接核对已安装包的随包 Codex。 - **判读**:若报 `Codex app-server JSON-RPC 失败`,sidecar 已启动并返回协议错误,应先检查请求体及已安装包的源码版本;若根本无法启动,再查安装目录 `coding-agent/win-x64/manifest.json` 的组件哈希、`codex-package.json` 的版本和缺失组件。夹具模式中的 `remote_control disabled reason=provider-proxy-auth` 是预期诊断行。 +## 2026-09-30 居中溢出叠加内部滚动,会把顶部内容裁到滚不到的地方(游玩页启动面板) + +- **现象**:极矮视口(横屏手机,游玩页 `max-height: 26rem` 那一档)里启动面板内容比容器高时,标题与简介被裁在容器上方,`scrollTop` 已经到 0 也够不到,看起来像「标题凭空消失」。 +- **根因**:`.game-player-launch` 同时有 `align-content: center`(两列布局下是 `align-items: center`)与 `overflow-y: auto`。溢出量被居中分配成上下各一半,负方向那一半落在滚动范围之外——滚动只能从 0 开始,所以顶部永远不可达。只给 `overflow-y: auto` 或只改居中都修不掉。 +- **处理**:纵向对齐改用 `safe center`(先写 `align-content: center` / `align-items: center` 作旧浏览器回退,再写 `safe center`),溢出时退回 start;并把 `overflow-y: auto` 提到基础规则,让启动卡片成为唯一的内部滚动兜底,页面与 `.platform-tab-panel` 本身仍保持 `overflow: hidden`。 +- **判据**:真实 Chromium 夹具(`tmp/launch-overflow-check`,把 `gameDistribution.css` 直接链进静态页面)在 `844x200` 下量:改回 `center` 时 `titleTopAtScrollZero = -20`(不可达),改成 `safe center` 后为 `+12`(可达),滚到底按钮同样可达;`390x844` 与 `1280x800` 正常视口不溢出,位置与改前一致。 +- **关联**:`src/components/game-distribution/gameDistribution.css` 的 `.game-player-launch` 与两处媒体查询、`src/components/game-distribution/GamePlayPage.tsx`。 + +## 2026-09-30 单列 grid 的 auto 轨道被 nowrap 文本撑开,移动端面板只剩横向滚动 + +- **现象**:「我的游戏」页在 390/360 宽的手机上可以横向滑动,卡片比可视区宽(实测卡片 592px、可视 390px、面板横向可滚 230px),看起来也不居中;同一页面的画廊卡片却正常。 +- **根因**:三件事叠在一起。① `.my-game-list` 是单列 grid,隐式列宽是 `auto`,而 auto 轨道的最小尺寸是**内容最小宽度**,不会被容器宽度压回去;② `.my-game-card`(flex 行)没有 `min-width: 0`,其最小内容宽度 = 封面 `8.5rem` + gap + 正文最小内容;③ 正文里的 `.my-game-card__summary` 是 `white-space: nowrap`,一条长且不可断行的简介就把最小内容宽度顶到 592px。画廊之所以正常,是因为 `.game-grid` 用 `repeat(n, minmax(0, 1fr))`,`.game-card` 又有 `min-width: 0; overflow: hidden`。 +- **处理**:让「我的游戏」与画廊同口径——`.my-game-list` 改 `grid-template-columns: minmax(0, 1fr)`,`.my-game-card` 加 `min-width: 0; overflow: hidden`,标题加 `min-width: 0; overflow-wrap: anywhere`。改后 390/360 实测轨道 = 列表 = 卡片 = 可视宽,面板横向溢出 0。 +- **同批修掉左右留白不对称**:面板左留白 12/24px(Tailwind `px-3`/`sm:px-6`),右留白却是 0,列表左右留白因此是 28/16。根因是 `src/index.css` 里 `.platform-tab-panel` 写死了 `padding-right: 0.25rem`——它属于**不分层(unlayered)规则,优先级高于 Tailwind 放进 `@layer utilities` 的 `padding-inline`**,把右内边距钉死在 0;`@media (max-width: 639px) .platform-tab-panel` 与 `.platform-desktop-shell--workbench .platform-tab-panel` 又各写了一次 `padding-right: 0`。三处声明全部删掉,左右统一交给使用处的 `padding-inline`;没有 px 类的舞台(创作主页 / 我的 / 游玩页)维持 0/0。改后实测 390 下 12/12、820 下 24/24,列表左右留白 28/28 与 44/44。 +- **判据**:真实 Chromium 静态夹具 `tmp/mygames-check/`(`tmp/` 已 gitignore,由 dev 栈 `http://127.0.0.1:10000/tmp/mygames-check/` 托管)同时渲染「我的游戏」和画廊两组卡片,量面板横向溢出与网格轨道宽度:改前 390/360 下轨道 591.891px、卡片 592px、面板横向可滚 230/260px;改后轨道 = 列表 = 卡片 = 可视宽、溢出 0。布局问题 jsdom 量不出来,这类回归目前靠这个夹具手工复核。 +- **关联**:`src/components/game-distribution/gameDistribution.css`、`src/components/game-distribution/MyGamesPage.tsx`、`.game-grid`/`.game-card`。 + +## 2026-09-30 SPA 的「返回」写死目标页,会丢掉用户真实来源 + +- **现象**:游戏详情页左上角写死「返回游戏广场」并把舞台设成 `games`。从「我的游戏」点进详情,返回后落在广场而不是我的游戏;从广场进详情,返回还会 push 一条重复的 `/games`,此时浏览器原生后退反而回到刚离开的详情页。 +- **根因**:`setSelectionStage(stage, { path })` 一律走 `pushAppHistoryPath`,而返回按钮复用它,语义变成「前进到广场」;来源页信息从未被记录,返回只能靠猜。 +- **处理**:`activeAppPageRoutes` 给应用写入的历史条目补内部深度标记(`push` 时 +1、`replace` 保持),新增 `hasAppHistoryBackEntry()`;详情/游玩页的返回改为「有应用内历史就 `window.history.back()`(`popstate` 已由 `ActiveApp` 同步舞台),否则 `replaceAppHistoryPath` 兜底到广场/详情」。深度标记而不是只看「有没有 state」是必要的:直接打开深链、或原生壳通过 host bridge 补写首条目时,`history.back()` 会直接退出应用。 +- **判据**:真实 Chromium(dev 栈 390x844)实测 `/games` → 详情 → 返回 = `/games`;深链直开 `/games/detail?id=…` → 返回 = `/games`;详情 → 立即玩 → 返回详情 = `/games/detail?id=…`。jsdom 侧 `activeAppPageRoutes.test.ts` 锁深度标记语义,`PlatformEntryActiveFlowShell.test.tsx`「游戏详情返回」两条锁原生返回与深链兜底。 +- **关联**:`src/routing/activeAppPageRoutes.ts`、`src/ActiveApp.tsx`、`src/components/platform-entry/PlatformEntryActiveFlowShell.tsx`、`src/components/game-distribution/GameDetailPage.tsx`。 + +## 2026-09-30 单类名覆盖被 workbench 祖先规则压掉,游玩页的「不滚动」守卫实际是空的 + +- **现象**:给游玩页面板加的 `.platform-tab-panel--game-play { overflow: hidden }` 看起来生效(类挂上了、单测断言类名也过),但真实 Chromium 里 `getComputedStyle(panel).overflowY` 是 `auto`——守卫没生效,只是当时内容恰好没溢出(`panelOverflowY = 0`),所以肉眼和 jsdom 都看不出来。 +- **根因**:`.platform-desktop-shell--workbench .platform-tab-panel { overflow: auto }`(`src/index.css` 文件末尾附近)特异性是 (0,2,0),而且它在文件里更靠后,把单类名 (0,1,0) 的覆盖压掉了。面板在所有舞台都渲染在这层 workbench 容器里,所以这个覆盖永远轮不到生效。 +- **处理**:覆盖规则带上同样的祖先作用域,`overflow: hidden` 用两个选择器(单类名 + `.platform-desktop-shell--workbench .platform-tab-panel.platform-tab-panel--game-play`)一起写,既压得过 workbench 规则也不依赖文件顺序。 +- **判据**:真实 Chromium(dev 栈 390x844 / 360x640 / 844x390 横屏矮视口)实测 `overflowY=hidden`、面板与整页横竖都不溢出,同时广场舞台仍是 `overflowY=auto` 且内容可滚(689px)。同类问题排查姿势:改 `.platform-tab-panel` 或任何 workbench 内元素的样式,先在浏览器里读 `getComputedStyle`,别只看类名/源码顺序。 +- **关联**:`src/index.css`(`.platform-tab-panel--game-play` 与 `.platform-desktop-shell--workbench .platform-tab-panel`)、`scripts/check-game-distribution-web-e2e.mjs`、`src/components/game-distribution/gameDistribution.css`。 + ## 2026-09-24 移动端固定底部菜单会压住「内联」弹窗:`z-index` 高也点不到 - **现象**:手机端主站从头像入口上传头像,裁剪弹窗的「取消 / 上传」被底部「游戏 / 我的」菜单盖住,点按钮命中底部菜单;弹窗遮罩、面板本身都正常渲染,只有底部一条区域失去交互。 diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 99e4c004a..8e98e71bf 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -72,7 +72,7 @@ - 游戏目录 `/games`、详情 `/games/detail?id=`、游玩 `/games/play?id=`;作者管理 `/games/mine` 只列当前账号的游戏和发行状态。平台壳及既有账号页提供入口,不建立第二套账号或作品系统。 - 目录使用重点推荐位和紧凑卡片网格,首版推荐位只消费后台明确设置且已公开的游戏;其余列表按发布时间排列。支持关键词、分类与设备筛选,筛选写入 URL,返回列表恢复筛选和滚动位置;没有内容时展示真实空态。 - 详情的主动作是“立即玩”,展示封面、标题、作者、短简介、设备标识、截图和简短操作方式;不虚构评分、玩家数、排名和收藏状态。游客浏览详情及启动游戏均无需登录;上传、更新、撤回和作者管理需要当前平台登录态。 -- 游玩页先展示封面与开始按钮,用户点击后加载游戏,避免列表自动加载多个 iframe 或自动播音;提供加载、超时、重试、返回详情和全屏。iframe 的 `load` 事件只表示文档已加载,不宣称已经通过运行验证;首版不把 iframe 消息当平台业务状态。 +- 游玩页先展示封面与开始按钮,用户点击后加载游戏,避免列表自动加载多个 iframe 或自动播音;提供加载、超时、重试、返回和全屏。iframe 的 `load` 事件只表示文档已加载,不宣称已经通过运行验证;首版不把 iframe 消息当平台业务状态。 - 移动端优先采用全宽内容、底部主要动作、至少 44px 点击区域,覆盖安全区、横竖屏和键盘焦点。游戏声明仅支持桌面时,手机详情保留可浏览内容并禁用开始,明确显示“请在电脑上游玩”;横屏游戏提供旋转提示,但不能把系统方向锁定成功作为启动前提。 - 视觉延续当前暖色主题 token、米白/浅暖背景、橙色主动作与现有字体;通过封面占比、留白、圆角和清晰状态层级现代化呈现,不另建配色系统。通用卡片动作、弹窗、上传、状态反馈、空态与媒体预览优先复用共享组件。发布打开独立弹窗/抽屉,不在当前页面末尾追加表单;技术信息仅在失败诊断或确需用户决策时展示。 diff --git a/scripts/check-game-distribution-web-e2e.mjs b/scripts/check-game-distribution-web-e2e.mjs index 60ba8f326..d79fe2231 100644 --- a/scripts/check-game-distribution-web-e2e.mjs +++ b/scripts/check-game-distribution-web-e2e.mjs @@ -522,6 +522,32 @@ async function main() { mobileStartDisabled && rotationHint, `startDisabled=${mobileStartDisabled} rotationHint=${rotationHint}`, ); + // 游玩页是固定画布:面板必须是 overflow:hidden 且不溢出,整页也不能出现滚动条。 + // 面板自身 `overflow-x: hidden`,横向溢出只会被静默裁掉、不会出滚动条,所以两个方向都要量: + // 只量 scrollHeight 会漏掉「卡片被裁一半」这类横向回归。 + const playScroll = await mobilePage.evaluate(() => { + const panel = document.querySelector('.platform-tab-panel'); + return { + documentOverflowY: + document.documentElement.scrollHeight - window.innerHeight, + documentOverflowX: + document.documentElement.scrollWidth - window.innerWidth, + panelOverflowY: panel ? panel.scrollHeight - panel.clientHeight : 0, + panelOverflowX: panel ? panel.scrollWidth - panel.clientWidth : 0, + panelOverflowStyle: panel + ? getComputedStyle(panel).overflowY + : 'missing', + }; + }); + check( + '移动端游玩页不可滚动(面板与整页都不溢出)', + playScroll.documentOverflowY <= 1 && + playScroll.documentOverflowX <= 1 && + playScroll.panelOverflowY <= 1 && + playScroll.panelOverflowX <= 1 && + playScroll.panelOverflowStyle === 'hidden', + JSON.stringify(playScroll), + ); await mobile.close(); await context.close(); } finally { diff --git a/src/components/game-distribution/GameDetailPage.tsx b/src/components/game-distribution/GameDetailPage.tsx index 243c052e9..d66abbcb7 100644 --- a/src/components/game-distribution/GameDetailPage.tsx +++ b/src/components/game-distribution/GameDetailPage.tsx @@ -193,7 +193,7 @@ export function GameDetailPage({ return (
diff --git a/src/components/game-distribution/GameDistributionPages.test.tsx b/src/components/game-distribution/GameDistributionPages.test.tsx index 15b3b8720..8c2c4a430 100644 --- a/src/components/game-distribution/GameDistributionPages.test.tsx +++ b/src/components/game-distribution/GameDistributionPages.test.tsx @@ -375,10 +375,54 @@ 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 启动超时与旋转提示', () => { - it('启动超时后给出重试与返回详情,重试可恢复可玩', async () => { + it('启动超时后给出重试与返回,重试可恢复可玩', async () => { vi.useFakeTimers(); try { const onBack = vi.fn(); @@ -395,7 +439,7 @@ describe('GamePlayPage 启动超时与旋转提示', () => { vi.advanceTimersByTime(GAME_PLAY_STARTUP_TIMEOUT_MS); }); expect(screen.getByText(/启动超时/u)).toBeTruthy(); - expect(screen.getByRole('button', { name: /返回详情/u })).toBeTruthy(); + expect(screen.getByRole('button', { name: /^返回$/u })).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: /^重试$/u })); const retriedFrame = screen.getByTitle('星轨防线 在线游玩'); diff --git a/src/components/game-distribution/GameGalleryPage.tsx b/src/components/game-distribution/GameGalleryPage.tsx index 11636ccfc..6df822fc1 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 = () => { @@ -250,44 +275,12 @@ export function GameGalleryPage({ [onOpenDetail, scrollKey], ); + const hasPageActions = Boolean(onOpenPublish || onOpenMyGames); + return (
-
-
- - -

- 把灵感做成游戏, -
- 现在就开始玩。 -

-

- 来自 AGC - 创作者的轻量游戏集合。打开网页即可游玩,每一次尝试都值得被看见。 -

-
- featuredGame && openDetail(featuredGame.id)} - > - - 支持桌面端与移动端 -
-
- -
- -
-
- 发现好玩的 -

游戏广场

-
-
+ {hasPageActions ? ( +
{onOpenPublish ? ( ) : null} +
+ ) : null} +
+
+ + +

+ 把灵感做成游戏, +
+ 现在就开始玩。 +

+

+ 来自 AGC + 创作者的轻量游戏集合。打开网页即可游玩,每一次尝试都值得被看见。 +

+
+ + + 支持桌面端与移动端 +
+
+ +
+ +
+
+ 发现好玩的 +

游戏广场

+
+