From eb098ca77523963b7248bc0f5313fbb779831e36 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 13 Jul 2025 09:46:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=BB=9A=E5=8A=A8=E9=94=81?= =?UTF-8?q?=E5=AE=9A=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useScrollLock.ts | 137 ++++++++++++++++++----- src/layouts/components/DefaultLayout.vue | 10 +- src/layouts/components/QuickAccess.vue | 18 ++- 3 files changed, 137 insertions(+), 28 deletions(-) diff --git a/src/composables/useScrollLock.ts b/src/composables/useScrollLock.ts index 9a848079..8d870977 100644 --- a/src/composables/useScrollLock.ts +++ b/src/composables/useScrollLock.ts @@ -1,5 +1,35 @@ import { ref, watch, onBeforeUnmount, readonly } from 'vue' +/** + * 滚动锁定 Composable + * + * 使用示例: + * + * // 基本用法 + * const { isLocked, lockScroll, restoreScroll } = useScrollLock() + * + * // 带配置的用法 + * const { isLocked, lockScroll, restoreScroll } = useScrollLock({ + * preventTouchScroll: true, + * preserveScrollPosition: true, + * allowScrollSelectors: ['.my-modal', '.scrollable-content'], + * allowScrollContainerSelectors: ['.modal-content'], + * customScrollCheck: (element) => { + * // 自定义逻辑 + * return element.classList.contains('allow-scroll') + * } + * }) + * + * // 自动监听版本 + * const { isLocked, lockScroll, restoreScroll } = useScrollLockWithWatch( + * showModal, // 响应式布尔值 + * { + * allowScrollSelectors: ['.modal-content'], + * allowScrollContainerSelectors: ['.scrollable-area'] + * } + * ) + */ + // 滚动锁定配置 export interface ScrollLockOptions { // 是否在组件卸载时自动恢复滚动 @@ -14,10 +44,22 @@ export interface ScrollLockOptions { position?: string width?: string } + // 允许滚动的选择器列表(CSS选择器) + // 例如:['.my-modal', '.scrollable-content'] + allowScrollSelectors?: string[] + // 允许滚动的容器选择器列表(CSS选择器) + // 这些容器内的可滚动元素将被允许滚动 + // 例如:['.modal-content', '.scroll-container'] + allowScrollContainerSelectors?: string[] + // 自定义滚动检查函数 + // 返回 true 表示允许滚动,false 表示阻止滚动 + customScrollCheck?: (element: Element) => boolean } // 默认配置 -const DEFAULT_OPTIONS: Required = { +const DEFAULT_OPTIONS: Required< + Omit +> = { autoRestore: true, preserveScrollPosition: true, preventTouchScroll: true, @@ -29,7 +71,13 @@ const DEFAULT_OPTIONS: Required = { } export function useScrollLock(options: ScrollLockOptions = {}) { - const config = { ...DEFAULT_OPTIONS, ...options } + const config = { + ...DEFAULT_OPTIONS, + allowScrollSelectors: options.allowScrollSelectors || [], + allowScrollContainerSelectors: options.allowScrollContainerSelectors || [], + customScrollCheck: options.customScrollCheck, + ...options, + } // 状态管理 const isLocked = ref(false) @@ -67,37 +115,74 @@ export function useScrollLock(options: ScrollLockOptions = {}) { } } + // 检查元素是否应该允许滚动 + const shouldAllowScroll = (element: Element): boolean => { + // 1. 检查是否匹配允许滚动的选择器 + for (const selector of config.allowScrollSelectors) { + if (element.matches(selector) || element.closest(selector)) { + return true + } + } + + // 2. 检查是否在允许滚动的容器内 + for (const selector of config.allowScrollContainerSelectors) { + const container = element.closest(selector) + if (container) { + // 检查容器是否可滚动 + const style = getComputedStyle(container) + const isScrollable = + container.scrollHeight > container.clientHeight && + style.overflow !== 'hidden' && + (style.overflow === 'auto' || + style.overflow === 'scroll' || + style.overflowY === 'auto' || + style.overflowY === 'scroll') + if (isScrollable) { + return true + } + } + } + + // 3. 检查是否在弹窗、菜单或其他覆盖层内 + const isInDialog = element.closest( + '.v-dialog, .v-menu, .v-bottom-sheet, .v-snackbar, [role="dialog"], .v-overlay__content', + ) + + // 4. 检查是否是可滚动的内容区域 + const isScrollableContent = element.closest( + '.v-card-text, .v-list, .v-table__wrapper, .v-data-table__wrapper, .v-sheet, .v-card__content, .v-data-table, .v-table', + ) + + // 5. 检查是否在可滚动的容器内 + const scrollableContainer = element.closest('[style*="overflow"], [class*="overflow"]') + const isInScrollableContainer = + scrollableContainer && + (scrollableContainer.scrollHeight > scrollableContainer.clientHeight || + getComputedStyle(scrollableContainer).overflow !== 'hidden') + + // 6. 使用自定义检查函数 + if (config.customScrollCheck && config.customScrollCheck(element)) { + return true + } + + // 如果不在弹窗内且不是可滚动内容且不在可滚动容器内,则不允许滚动 + return !!(isInDialog || isScrollableContent || isInScrollableContainer) + } + // 阻止触摸滚动事件 const preventTouchScroll = (event: TouchEvent) => { if (isLocked.value && config.preventTouchScroll) { - // 检查触摸事件的目标元素是否在弹窗内 + // 检查触摸事件的目标元素 const target = event.target as Element if (target) { - // 检查目标元素是否在弹窗、菜单或其他覆盖层内 - const isInDialog = target.closest( - '.v-dialog, .v-menu, .v-bottom-sheet, .v-snackbar, [role="dialog"], .v-overlay__content', - ) - - // 检查目标元素是否是可滚动的内容区域 - const isScrollableContent = target.closest( - '.v-card-text, .v-list, .v-table__wrapper, .v-data-table__wrapper, .v-sheet, .v-card__content, .v-data-table, .v-table', - ) - - // 检查目标元素是否在可滚动的容器内 - const scrollableContainer = target.closest('[style*="overflow"], [class*="overflow"]') - const isInScrollableContainer = - scrollableContainer && - (scrollableContainer.scrollHeight > scrollableContainer.clientHeight || - getComputedStyle(scrollableContainer).overflow !== 'hidden') - - // 如果不在弹窗内且不是可滚动内容且不在可滚动容器内,则阻止滚动 - if (!isInDialog && !isScrollableContent && !isInScrollableContainer) { - event.preventDefault() + // 如果元素应该允许滚动,则不阻止事件 + if (shouldAllowScroll(target)) { + return } - } else { - // 如果无法确定目标元素,则阻止滚动以确保安全 - event.preventDefault() } + + // 否则阻止滚动 + event.preventDefault() } } diff --git a/src/layouts/components/DefaultLayout.vue b/src/layouts/components/DefaultLayout.vue index 5d29a996..f3b1b738 100644 --- a/src/layouts/components/DefaultLayout.vue +++ b/src/layouts/components/DefaultLayout.vue @@ -164,7 +164,15 @@ const handleServiceWorkerMessage = (event: MessageEvent) => { } // 使用滚动锁定 composable(自动监听showPluginQuickAccess的变化) -useScrollLockWithWatch(showPluginQuickAccess) +useScrollLockWithWatch(showPluginQuickAccess, { + preventTouchScroll: true, + preserveScrollPosition: true, + autoRestore: true, + // 允许快速访问面板内的滚动 + allowScrollSelectors: ['.plugin-quick-access'], + // 允许快速访问面板内的可滚动容器 + allowScrollContainerSelectors: ['.plugin-grid'], +}) // 检查是否可以使用下拉手势 const canUsePullGesture = () => { diff --git a/src/layouts/components/QuickAccess.vue b/src/layouts/components/QuickAccess.vue index 4514adc4..2b51a511 100644 --- a/src/layouts/components/QuickAccess.vue +++ b/src/layouts/components/QuickAccess.vue @@ -141,7 +141,9 @@ function getPluginIcon(plugin: Plugin): string { // 如果是网络图片则使用代理后返回 if (plugin?.plugin_icon?.startsWith('http')) - return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(plugin?.plugin_icon)}&cache=true` + return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent( + plugin?.plugin_icon, + )}&cache=true` return `./plugin_icon/${plugin?.plugin_icon}` } @@ -233,6 +235,12 @@ function handleTouchStart(event: TouchEvent) { const target = event.target as HTMLElement startedFromBottomArea.value = !!target.closest('.bottom-drag-area') + // 如果触摸发生在插件网格内,不处理拖拽关闭 + if (target.closest('.plugin-grid')) { + startedFromBottomArea.value = false + return + } + startY.value = touch.clientY lastY.value = touch.clientY lastTime.value = Date.now() @@ -253,6 +261,12 @@ function handleTouchMove(event: TouchEvent) { // 只有从 bottom-drag-area 开始的触摸才处理上滑关闭 if (!startedFromBottomArea.value) return + // 检查当前触摸是否在插件网格内,如果是则不处理拖拽关闭 + const target = event.target as HTMLElement + if (target.closest('.plugin-grid')) { + return + } + const currentY = touch.clientY const currentTime = Date.now() const deltaY = startY.value - currentY // 向上为正值 @@ -561,6 +575,7 @@ function handleBackdropClick(event: MouseEvent) { flex: 1; flex-direction: column; gap: 16px; + max-block-size: calc(100vh - 200px); // 确保有最大高度限制 min-block-size: 0; -webkit-overflow-scrolling: touch; -ms-overflow-style: none; // IE/Edge @@ -571,6 +586,7 @@ function handleBackdropClick(event: MouseEvent) { // 隐藏滚动条 scrollbar-width: none; // Firefox touch-action: pan-y; + will-change: scroll-position; &::-webkit-scrollbar { display: none; // WebKit 浏览器