From 21c894e0252286916185f97e8726c8b639965875 Mon Sep 17 00:00:00 2001 From: liulong <18539103286> Date: Tue, 9 Jun 2026 01:30:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(gxnlpt):=20=E6=94=B6=E5=BD=95/=E6=94=B6?= =?UTF-8?q?=E8=97=8F=E5=88=87=E6=8D=A2=E6=97=B6=E4=B8=BB=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E5=AE=B9=E5=99=A8scrollTop=E8=B7=B3=E5=8F=98=E5=BC=95=E8=B5=B7?= =?UTF-8?q?=E6=8A=96=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 _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 --- .../src/pages/index/views/gxnlpt/index.vue | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue b/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue index 854891d..21e96be 100644 --- a/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue +++ b/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue @@ -684,6 +684,21 @@ export default { } }); }, + /** + * gxnlpt 被 缓存,从本组件跳走时只会触发 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 抖动)。 + * 视图(list→submit/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;