一、业务背景
项目是一个 uni-app 多端应用中的支付流程子页面,展示「支付提示」类富文本列表。接口返回多条时,列表采用手风琴:同一时间只展开一条详情,展开某条时其余条收起。
交互上还有自定义导航栏(含状态栏高度),列表在导航占位下方滚动,整页是 页面级滚动(非 scroll-view 包一层的那种)。
二、现象(产品经理视角)
当用户 展开第二条(或从第一条切换到第二条)时:
- 第一条会收起,第二条会展开,逻辑正确;
- 但视觉上 第二条的开头(标题/内容起点)常常不在舒适可视区,像是用户手动把页面上滑了一大截,第二条「从半截」才开始出现。
期望:展开哪一条的时候,视口就要对齐到那一条内容的可见起点(至少标题区域不要被「卡在屏幕上方外」)。
三、原因分析(为什么不是简单的 bug)
这里并不是「数据错了」或「v-if 写反了」,而是 布局变化与滚动位置 的经典组合问题:
- 手风琴导致文档高度突变
上一条从「高」变「矮」,下面各块在文档流里 整体向上移动。浏览器/小程序内核不会自动帮你改scrollTop,所以用户眼睛看到的视口还停留在「旧的滚动偏移量」上。 - 固定顶栏占用可视区域
自定义导航是fixed,真正留给内容的「安全区」要从 导航总高度(状态栏 + 44px 等)算起。若只做scrollTop = 0,有时是 整页顶,不一定等于「当前卡片标题刚好贴在导航下面」。 - CSS 过渡让量测时机很关键
若用max-height+transition做展开动画,布局在 300ms 内仍在变化。若在activeIndex一赋值就立刻量节点位置,rect 可能还是过渡中间态,算出来的滚动目标会偏。
所以:需要在布局稳定后,用「当前滚动 + 节点相对视口位置」算出新的 scrollTop,并减去导航高度,再 pageScrollTo。
四、方案对比(简要)
| 思路 | 优点 | 注意点 |
|---|---|---|
仅 pageScrollTo(0) | 实现简单 | 容易忽略固定导航;且不一定是「当前条起点」 |
scroll-into-view(若用 scroll-view) | 声明式 | 当前页是页面滚动时需改结构或不用这条 |
| SelectorQuery + scrollOffset + pageScrollTo | 不改页面结构,对齐灵活 | 要处理 导航偏移 和 过渡延迟 |
最终采用 锚点 id + boundingClientRect + selectViewport().scrollOffset() + uni.pageScrollTo,与现有自定义导航高度字段对齐。
五、实现要点(可复用的套路)
-
给每一条可展开卡片包一层带唯一 id 的节点(如
tip-card-0、tip-card-1),量测「这一条内容的起点」——我们用的是 整张卡片顶部(含标题),与产品说的「从头展示」一致。 -
仅在「从展开 A 切换到展开 B」时滚动;用户点击「收起当前条」时不必滚动,避免多余跳动。
-
时机:
this.$nextTick后setTimeout略大于 max-height 动画时长(例如样式是 0.3s,则用 ~320ms),再执行查询与滚动,减少过渡中途量测的误差。 -
计算公式(核心)
- 设当前页面
scrollTop为s,卡片相对视口顶部的top为rect.top(可为负,表示已在屏外上方)。 - 希望卡片顶出现在 导航栏下沿 对应的位置,则:
targetScrollTop = s + rect.top - navbarHeight- 再
Math.max(0, targetScrollTop)避免负数。
- 设当前页面
-
uni.pageScrollTo的 duration 设一个较短值(如 200ms),过渡柔和;若仍觉得「滚早了/滚晚了」,优先调延迟毫秒数,而不是改公式。
六、可调参数与扩展
- 延迟:不同机型、不同富文本渲染耗时不同,可在 280~400ms 间微调。
- 对齐点:若产品希望对齐「正文」而非「标题」,把 id 挂到标题下方的容器即可,公式不变。
- 快速连点:若极端场景下连续切换多条,可考虑用递增 token 取消过期的
setTimeout,避免旧回调覆盖新状态(当时页面未做,可视需要加)。
七、小结
- 本质:折叠/展开改变了文档流高度,滚动偏移未随内容重排自动修正,造成「展开项像被滑到屏外」。
- 做法:在 动画结束后的下一帧附近 量测目标卡片位置,用
scrollTop + rect.top - 导航高度得到目标滚动值,再pageScrollTo。 - 体验:展开哪条,视口就跟到哪条的起点,和固定导航对齐,产品反馈会明显好于单纯滚到页面顶部。