Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
6.5 KiB
6.5 KiB
任务书 · iPhone 上键盘收起 / 刷新后整页上移,顶栏点不到(2026-09-17)
0. 基线
- 基线 commit:
73d3c99c(staging 已部署)。分支: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 行为推断,执行方需真机确认)
globals.css:322html, body { height: 100%; overflow: hidden; },应用壳靠.chat-app/.chat-panel的height: 100%撑满;没有任何代码处理window.scrollY:git grepvisualViewport/scrollRestoration/window.scrollTo在frontend/src0 命中(use-billing-panel.ts:195的scrollIntoView是容器内滚动)。- iOS Safari 不支持
viewport的interactive-widget=resizes-content(layout.tsx:26只对 Chrome Android 生效)。iOS 上键盘弹出时布局视口不变、Safari 直接滚动 window 让聚焦的输入框露出来;overflow: hidden挡不住这次程序滚动,却挡住了用户手指把页面拉回来。键盘收起后window.scrollY留在原地 → 顶栏在屏幕上方、底部空白。 - 刷新不能自愈:Safari 对同 URL 的 reload 默认恢复滚动位置(
history.scrollRestoration === "auto"),scrollY被原样还原,所以「刷新想测试」看到的还是上移后的页面。 - 与今天上线的两单无关:BUG-919 只改了
.message-list底部留白和.conversation的scroll-padding-block-end,都在滚动容器内部;BUG-918 只改时间轴内边距。这条是既有缺陷,今天因为在手机上连续输入才被看到。
3. 决策记录
- 产品:手机上任何时候顶栏都必须能点到;不接受「用户自己想办法滚回去」。
- 不改
html, body的overflow: hidden(桌面与滚动锚定依赖它),只补 window 级复位。
4. 任务分解
T1 window 级滚动复位(BUG-920)
- 新客户端组件
frontend/src/components/viewport-scroll-lock.tsx,在layout.tsx与StaleClientRecovery并列挂载,只在pointer: coarse或visualViewport存在时启用:- 挂载时
history.scrollRestoration = "manual",并window.scrollTo(0, 0); - 监听
window.visualViewport的resize与scroll、window的pageshow/orientationchange、document的focusout:当visualViewport.height回到 ≥window.innerHeight - 1(键盘已收)且window.scrollY > 0时window.scrollTo(0, 0);键盘打开期间(visualViewport.height < innerHeight - 100)不动,让 Safari 把输入框露出来。 - 卸载时移除监听。不用
setInterval。
- 挂载时
- 验收:源码契约测试(挂载点、
scrollRestoration = "manual"、监听事件集合、键盘打开期间不复位的判定);jsdom 下模拟visualViewport对象触发 resize 断言scrollTo(0,0)被调用一次;tsc/ lint /npm test失败清单同基线;/Static;gzip ±2%。
T3 顶栏右侧两枚芯片同一套尺寸(BUG-921,产品 2026-09-17「积分块有点扁」)
- 实证:手机顶栏右侧「当前盘面」与积分「✦ 557」并排,积分块看起来又宽又矮。规则出处:
globals.css:885.credit-buttonmin-height: 44px; min-width: 64px; padding: 0 var(--space-3); font-size: var(--type-caption)(13px);:2964-2973顶栏里的.rectification-board-peek同为min-height: 44px但padding: var(--space-2) var(--space-4)、strong用--type-body-sm(14px)、圆角与边框另一套;两枚都塞在.chat-panel的 46px 顶栏行里(: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-button无min-width;chat-headercompact 高度断言;触屏热区契约测试(BUG-695 那条)仍绿;真机清单加一条「顶栏两枚芯片等高、不扁」。
T2 记录
- BUG-921 同上记录。
- BUG-920(状态按真机结果:本机无 iOS 只能写
resolved附「真机待核」或investigating,不得编)、CHANGELOG、frontend/DESIGN.md移动端一节加一句「window 级滚动由 ViewportScrollLock 复位」、docs/testing/mobile-viewport-scroll-lock-20260917.md:iPhone Safari 上 (a) 点输入框弹键盘→收起→顶栏仍可点;(b) 弹键盘状态下刷新→顶栏在顶;(c) 横竖屏切换后无空白带;(d) Android Chrome 不受影响。
5. 让步顺序
- 若
focusout时机早于键盘收起动画导致复位被 Safari 再次覆盖,改为在visualViewport.resize上做,focusout只做兜底延时 300ms 一次。 - 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),需关掉标签页重新打开(不是刷新)才能复位。