Files
Jyotisha/docs/tasks/TASK-mobile-viewport-scroll-lock-20260917.md
T

6.5 KiB
Raw Blame History

任务书 · iPhone 上键盘收起 / 刷新后整页上移,顶栏点不到(2026-09-17)

0. 基线

  • 基线 commit73d3c99cstaging 已部署)。分支:codex/mobile-viewport-scroll-lock-20260917
  • 纯前端;不动 page.tsx(新逻辑放 frontend/src/components/ 的一个客户端组件,从 layout.tsx 挂载)。
  • BUG 段:BUG-920 起(基线最大 BUG-919)。

1. 事故实证(产品负责人 2026-09-17 09:34 iPhone Safari 截图,staging 73d3c99c

  • 刷新校正页后,顶栏(侧栏按钮 / 「9月17日 · 生时校正」/ 当前盘面 / 点数)和时间轴都不在屏幕里,第一条可见内容是消息正文;输入框下面空出约 260px 的白底,浏览器工具栏之上什么都没有。产品原话:「点不到 tab 栏,没找到入口,整体上移了」。
  • 空白高度 ≈ 整页上移的距离:整个应用壳被 window 级滚动推上去了,不是壳内某个容器的问题。

2. 根因(代码可证部分 + iOS 行为推断,执行方需真机确认)

  1. globals.css:322 html, body { height: 100%; overflow: hidden; },应用壳靠 .chat-app / .chat-panelheight: 100% 撑满;没有任何代码处理 window.scrollYgit grep visualViewport / scrollRestoration / window.scrollTofrontend/src 0 命中(use-billing-panel.ts:195scrollIntoView 是容器内滚动)。
  2. iOS Safari 不支持 viewportinteractive-widget=resizes-contentlayout.tsx:26 只对 Chrome Android 生效)。iOS 上键盘弹出时布局视口不变、Safari 直接滚动 window 让聚焦的输入框露出来;overflow: hidden 挡不住这次程序滚动,却挡住了用户手指把页面拉回来。键盘收起后 window.scrollY 留在原地 → 顶栏在屏幕上方、底部空白。
  3. 刷新不能自愈:Safari 对同 URL 的 reload 默认恢复滚动位置(history.scrollRestoration === "auto"),scrollY 被原样还原,所以「刷新想测试」看到的还是上移后的页面。
  4. 与今天上线的两单无关:BUG-919 只改了 .message-list 底部留白和 .conversationscroll-padding-block-end,都在滚动容器内部;BUG-918 只改时间轴内边距。这条是既有缺陷,今天因为在手机上连续输入才被看到。

3. 决策记录

  • 产品:手机上任何时候顶栏都必须能点到;不接受「用户自己想办法滚回去」。
  • 不改 html, bodyoverflow: hidden(桌面与滚动锚定依赖它),只补 window 级复位。

4. 任务分解

T1 window 级滚动复位(BUG-920

  • 新客户端组件 frontend/src/components/viewport-scroll-lock.tsx,在 layout.tsxStaleClientRecovery 并列挂载,只在 pointer: coarsevisualViewport 存在时启用:
    • 挂载时 history.scrollRestoration = "manual",并 window.scrollTo(0, 0)
    • 监听 window.visualViewportresizescrollwindowpageshow / orientationchangedocumentfocusout:当 visualViewport.height 回到 ≥ window.innerHeight - 1(键盘已收)且 window.scrollY > 0window.scrollTo(0, 0);键盘打开期间(visualViewport.height < innerHeight - 100)不动,让 Safari 把输入框露出来。
    • 卸载时移除监听。不用 setInterval
  • 验收:源码契约测试(挂载点、scrollRestoration = "manual"、监听事件集合、键盘打开期间不复位的判定);jsdom 下模拟 visualViewport 对象触发 resize 断言 scrollTo(0,0) 被调用一次;tsc / lint / npm test 失败清单同基线;/ Staticgzip ±2%。

T3 顶栏右侧两枚芯片同一套尺寸(BUG-921,产品 2026-09-17「积分块有点扁」)

  • 实证:手机顶栏右侧「当前盘面」与积分「✦ 557」并排,积分块看起来又宽又矮。规则出处:globals.css:885 .credit-button min-height: 44px; min-width: 64px; padding: 0 var(--space-3); font-size: var(--type-caption)13px);:2964-2973 顶栏里的 .rectification-board-peek 同为 min-height: 44pxpadding: var(--space-2) var(--space-4)strong--type-body-sm(14px)、圆角与边框另一套;两枚都塞在 .chat-panel46px 顶栏行里(:855-856),上下各剩 1px,芯片没有呼吸空间,小字 + 强制 64px 最小宽度就显得扁。
  • 修法:顶栏芯片抽一个共用类(或让 .credit-button 与顶栏里的 .rectification-board-peek 共用同一组声明):同高、同圆角、同边框、同字号(14px,数字 tabular)、同内边距,min-width 去掉;手机(.is-compact / ≤767px)顶栏行从 46px 放到 52px,芯片视觉高 40px、点击热区仍 ≥44px(用透明 ::before 扩热区或外层 padding,BUG-695 的触屏命中区规则不得退)。桌面若同样受益可一并做,写进 DESIGN.md 顶栏一节。
  • 验收:CSS 声明断言两枚芯片高度/字号/圆角一致、.credit-buttonmin-widthchat-header compact 高度断言;触屏热区契约测试(BUG-695 那条)仍绿;真机清单加一条「顶栏两枚芯片等高、不扁」。

T2 记录

  • BUG-921 同上记录。
  • BUG-920(状态按真机结果:本机无 iOS 只能写 resolved 附「真机待核」或 investigating,不得编)、CHANGELOG、frontend/DESIGN.md 移动端一节加一句「window 级滚动由 ViewportScrollLock 复位」、docs/testing/mobile-viewport-scroll-lock-20260917.mdiPhone Safari 上 (a) 点输入框弹键盘→收起→顶栏仍可点;(b) 弹键盘状态下刷新→顶栏在顶;(c) 横竖屏切换后无空白带;(d) Android Chrome 不受影响。

5. 让步顺序

  1. focusout 时机早于键盘收起动画导致复位被 Safari 再次覆盖,改为在 visualViewport.resize 上做,focusout 只做兜底延时 300ms 一次。
  2. T2 不可让步。

6. 开工前置命令

git fetch origin --prune
git worktree add -b codex/mobile-viewport-scroll-lock-20260917 .worktrees/mobile-viewport-scroll-lock-20260917 origin/staging
cd .worktrees/mobile-viewport-scroll-lock-20260917/frontend && ./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -20

7. 环境缺口

  • 本机无 iOS / Chrome;根因第 2、3 条是按 iOS Safari 已知行为推断,执行方与产品在真机上按 §4 T2 清单确认。产品临时绕过:键盘收起后把页面往下拉一下不会有效(overflow hidden),需关掉标签页重新打开(不是刷新)才能复位。