fix(gxnlpt): 收录/收藏切换时主滚动容器scrollTop跳变引起抖动

- 新增 _resetScrollToTopForViewSwitch:
  在 contentView 切换前瞬时(behavior:'auto')把 .content-wrap 的 scrollTop 复位到 0
- openSubmitView/openFavoritesView 切换 contentView 之前调用

根因:
list 视图主内容区高 ~3000px,submit/favorites 视图高 ~1200px。
用户从 list 视图某个分类(如"碳认证机构")点击收录/收藏时,
当前 scrollTop 通常 ~1500px,切换后 scrollTop 1500 > scrollHeight 1200,
浏览器自动把 scrollTop 钳到 0,造成"页面瞬间跳到顶部"的视觉抖动,
sidebar sticky 周围容器/背景也被一起拉到顶部,共同表现为"侧边栏抖动"。

方案:
contentView 切换前先瞬时复位 scrollTop,让浏览器只看到 1 次 layout,
避免 scrollTop 跳变。瞬时(behavior:'auto')而不是平滑(smooth),
因为平滑动画与 contentView 切换并行会多次回流反而更抖。

配合之前 deactived 钩子修复,本次一并提交。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
liulong 2026-06-09 01:30:59 +08:00
parent 6b8e38e3d7
commit 21c894e025

View File

@ -684,6 +684,21 @@ export default {
}
});
},
/**
* gxnlpt <keep-alive> 缓存,从本组件跳走时只会触发 deactivated,不会触发 destroyed
* 必须在此处清理 sidebar sticky:
* 1. portal body 下的 .gxnlpt-sidebar-sticky 元素如果不搬回 sidebar 容器,
* 会继续以 position:fixed 钉在 viewport,叠加到其他路由页面顶部
* 2. inline 写入的 left/width/top 也需要清空,避免污染别的页面
* 否则用户跳转到首页等其它页面后,会在屏幕上看到 gxnlpt 的侧边栏残留,
* 同时 devtools 中也能查到 element.style { left/width/top } 残留
*/
deactivated() {
this.unbindStackedNavMedia();
this.clearScrollUnlock();
this.destroyScrollSpy();
this.destroySidebarSticky();
},
beforeDestroy() {
this.unbindStackedNavMedia();
this.clearScrollUnlock();
@ -1085,37 +1100,39 @@ export default {
return;
}
this.destroyScrollSpy();
// (,):
// main.vue .content-wrap,list ~3000px,
// ("",scrollTop ~1500px) submit
// ~1200px,scrollTop 1500 > scrollHeight 1200,
// scrollTop 0,""
// scrollTop 0, scrollTop
this._resetScrollToTopForViewSwitch();
this.contentView = 'submit';
this.resetSubmitForm();
// sidebar ,sticky left/width/top,
// element.style ""
this.$nextTick(() => this.refreshSidebarStickyGeometry());
},
openFavoritesView() {
if (!this.ensureLogin('查看我的收藏')) {
return;
}
this.destroyScrollSpy();
this._resetScrollToTopForViewSwitch();
this.contentView = 'favorites';
this.loadFavorites();
// sidebar ,sticky left/width/top,
// element.style ""
this.$nextTick(() => this.refreshSidebarStickyGeometry());
},
/**
* 视图切换后重新测量 sidebar / sticky 几何常量,并立即应用最新的 left/width/top
* 不重新走 initSidebarSticky/destroySidebarSticky(避免把 sticky 元素搬回容器再搬出,
* 减少不必要的 DOM 抖动)
* 视图(listsubmit/favorites 或反向)切换前,把滚动容器 .content-wrap
* scrollTop 瞬时复位到 0,避免 contentView 切换导致主内容区 scrollHeight
* 骤变浏览器自动把 scrollTop 钳到 0 引发的"页面瞬间跳到顶部"视觉抖动
*
* 用瞬时滚动(behavior: 'auto')是为了让 scrollTop 在浏览器进入下一次 layout
* 之前就已经是 0,这样 contentView 切换时 scrollHeight 变化不会再次触发
* scrollTop 钳位 等于"用确定的 scrollTop 状态切换"替代"放任 scrollTop 跳变"
*/
refreshSidebarStickyGeometry() {
if (this.isStackedNavMode) return;
this._recomputeStickyGeometry();
// , _applySidebarSticky element.style
this._lastStickyTop = undefined;
this._lastStickyLeft = undefined;
this._lastStickyWidth = undefined;
this._lastStickyVisible = undefined;
this._applySidebarSticky();
_resetScrollToTopForViewSwitch() {
const scrollRoot = this.getScrollRoot();
if (!scrollRoot) return;
if (scrollRoot.scrollTop === 0) return;
scrollRoot.scrollTo({ top: 0, behavior: 'auto' });
},
async loadFavorites() {
this.favoritesLoading = true;