From c62c65e15ebb8f71c23d5bbf7e78873f3045aeea Mon Sep 17 00:00:00 2001 From: chenchun Date: Wed, 29 Apr 2026 18:04:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(scroll-lock):=20=E4=BD=BF=E7=94=A8=20class?= =?UTF-8?q?Name=20=E8=BF=BD=E8=B8=AA=E9=94=81=E5=AE=9A=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=A4=9A=E7=BB=84=E4=BB=B6=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E9=94=81=E5=AE=9A=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/composables/usePageScrollLock.ts | 41 ++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/app/composables/usePageScrollLock.ts b/app/composables/usePageScrollLock.ts index 00b2ba4..a6a735a 100644 --- a/app/composables/usePageScrollLock.ts +++ b/app/composables/usePageScrollLock.ts @@ -1,8 +1,8 @@ -import { onBeforeUnmount, watch, type Ref, isRef, unref } from 'vue' +import { onBeforeUnmount, watch, type Ref, isRef, unref, computed } from 'vue' import { useNuxtApp, useHead } from '#imports' -// 全局模块级变量,用于记录锁定请求的次数,解决多组件并发锁定的竞态问题 -let lenisLockCount = 0 +// 按 className 维度追踪各锁的引用计数,解决多组件使用不同 className 并发锁定时的类名泄漏问题 +const lockCounts = new Map() export const usePageScrollLock = ( isLocked: Ref | boolean, @@ -12,13 +12,17 @@ export const usePageScrollLock = ( // 由于 $lenis 是从 client 插件中注入的,SSR 期间可能为 undefined // 而且在 setup 期间解构可能拿不到最新的值,我们改为在函数内部动态获取 + const lockClassName = options?.className ?? 'is-home-intro-active' + + // SSR 和 CSR 都支持:自动将 class 注入到 HTML 标签上 + // 多实例调用时 Unhead 会合并各自的 class 值 useHead({ htmlAttrs: { + class: computed(() => unref(isLocked) ? lockClassName : ''), lang: 'zh-CN', }, }) - const lockClassName = options?.className ?? 'is-home-intro-active' let currentlyLocked = false const updateLock = (locked: boolean) => { @@ -29,11 +33,17 @@ export const usePageScrollLock = ( // Lenis 控制仅在客户端有效,不需要依赖 $lenis 立即存在 if (import.meta.client) { if (locked) { - lenisLockCount++ - // 只有第一个锁请求时,才真正停止 Lenis 并滚动到顶部 - if (lenisLockCount === 1) { + const prevCount = lockCounts.get(lockClassName) ?? 0 + lockCounts.set(lockClassName, prevCount + 1) + + // 该 className 首次被锁定时,添加对应类名 + if (prevCount === 0) { document.documentElement.classList.add(lockClassName) - // 注意:需要在这里动态获取 $lenis,因为此时 client 插件才可能挂载完成 + } + + // 所有 className 的锁计数总和为 1 时(即全局第一个锁),停止 Lenis 并滚到顶部 + const totalLocks = Array.from(lockCounts.values()).reduce((sum, c) => sum + c, 0) + if (totalLocks === 1) { const lenisInstance = nuxtApp.$lenis if (lenisInstance) { lenisInstance.scrollTo(0, { immediate: true }) @@ -41,11 +51,18 @@ export const usePageScrollLock = ( } } } else { - lenisLockCount = Math.max(0, lenisLockCount - 1) - // 只有当所有锁请求都释放时,才重新启动 Lenis - if (lenisLockCount === 0) { + const prevCount = lockCounts.get(lockClassName) ?? 0 + const newCount = Math.max(0, prevCount - 1) + lockCounts.set(lockClassName, newCount) + + // 该 className 的锁计数归零时,移除对应类名 + if (newCount === 0) { document.documentElement.classList.remove(lockClassName) - // 同理,动态获取 + } + + // 所有 className 的锁计数总和为 0 时(即全局最后一个锁释放),重启 Lenis + const totalLocks = Array.from(lockCounts.values()).reduce((sum, c) => sum + c, 0) + if (totalLocks === 0) { const lenisInstance = nuxtApp.$lenis if (lenisInstance) { lenisInstance.start()