mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 07:28:37 +08:00
增强滚动锁定功能
This commit is contained in:
@@ -1,5 +1,35 @@
|
|||||||
import { ref, watch, onBeforeUnmount, readonly } from 'vue'
|
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 {
|
export interface ScrollLockOptions {
|
||||||
// 是否在组件卸载时自动恢复滚动
|
// 是否在组件卸载时自动恢复滚动
|
||||||
@@ -14,10 +44,22 @@ export interface ScrollLockOptions {
|
|||||||
position?: string
|
position?: string
|
||||||
width?: 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<ScrollLockOptions> = {
|
const DEFAULT_OPTIONS: Required<
|
||||||
|
Omit<ScrollLockOptions, 'allowScrollSelectors' | 'allowScrollContainerSelectors' | 'customScrollCheck'>
|
||||||
|
> = {
|
||||||
autoRestore: true,
|
autoRestore: true,
|
||||||
preserveScrollPosition: true,
|
preserveScrollPosition: true,
|
||||||
preventTouchScroll: true,
|
preventTouchScroll: true,
|
||||||
@@ -29,7 +71,13 @@ const DEFAULT_OPTIONS: Required<ScrollLockOptions> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useScrollLock(options: ScrollLockOptions = {}) {
|
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)
|
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) => {
|
const preventTouchScroll = (event: TouchEvent) => {
|
||||||
if (isLocked.value && config.preventTouchScroll) {
|
if (isLocked.value && config.preventTouchScroll) {
|
||||||
// 检查触摸事件的目标元素是否在弹窗内
|
// 检查触摸事件的目标元素
|
||||||
const target = event.target as Element
|
const target = event.target as Element
|
||||||
if (target) {
|
if (target) {
|
||||||
// 检查目标元素是否在弹窗、菜单或其他覆盖层内
|
// 如果元素应该允许滚动,则不阻止事件
|
||||||
const isInDialog = target.closest(
|
if (shouldAllowScroll(target)) {
|
||||||
'.v-dialog, .v-menu, .v-bottom-sheet, .v-snackbar, [role="dialog"], .v-overlay__content',
|
return
|
||||||
)
|
|
||||||
|
|
||||||
// 检查目标元素是否是可滚动的内容区域
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 如果无法确定目标元素,则阻止滚动以确保安全
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 否则阻止滚动
|
||||||
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,15 @@ const handleServiceWorkerMessage = (event: MessageEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用滚动锁定 composable(自动监听showPluginQuickAccess的变化)
|
// 使用滚动锁定 composable(自动监听showPluginQuickAccess的变化)
|
||||||
useScrollLockWithWatch(showPluginQuickAccess)
|
useScrollLockWithWatch(showPluginQuickAccess, {
|
||||||
|
preventTouchScroll: true,
|
||||||
|
preserveScrollPosition: true,
|
||||||
|
autoRestore: true,
|
||||||
|
// 允许快速访问面板内的滚动
|
||||||
|
allowScrollSelectors: ['.plugin-quick-access'],
|
||||||
|
// 允许快速访问面板内的可滚动容器
|
||||||
|
allowScrollContainerSelectors: ['.plugin-grid'],
|
||||||
|
})
|
||||||
|
|
||||||
// 检查是否可以使用下拉手势
|
// 检查是否可以使用下拉手势
|
||||||
const canUsePullGesture = () => {
|
const canUsePullGesture = () => {
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ function getPluginIcon(plugin: Plugin): string {
|
|||||||
|
|
||||||
// 如果是网络图片则使用代理后返回
|
// 如果是网络图片则使用代理后返回
|
||||||
if (plugin?.plugin_icon?.startsWith('http'))
|
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}`
|
return `./plugin_icon/${plugin?.plugin_icon}`
|
||||||
}
|
}
|
||||||
@@ -233,6 +235,12 @@ function handleTouchStart(event: TouchEvent) {
|
|||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
startedFromBottomArea.value = !!target.closest('.bottom-drag-area')
|
startedFromBottomArea.value = !!target.closest('.bottom-drag-area')
|
||||||
|
|
||||||
|
// 如果触摸发生在插件网格内,不处理拖拽关闭
|
||||||
|
if (target.closest('.plugin-grid')) {
|
||||||
|
startedFromBottomArea.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
startY.value = touch.clientY
|
startY.value = touch.clientY
|
||||||
lastY.value = touch.clientY
|
lastY.value = touch.clientY
|
||||||
lastTime.value = Date.now()
|
lastTime.value = Date.now()
|
||||||
@@ -253,6 +261,12 @@ function handleTouchMove(event: TouchEvent) {
|
|||||||
// 只有从 bottom-drag-area 开始的触摸才处理上滑关闭
|
// 只有从 bottom-drag-area 开始的触摸才处理上滑关闭
|
||||||
if (!startedFromBottomArea.value) return
|
if (!startedFromBottomArea.value) return
|
||||||
|
|
||||||
|
// 检查当前触摸是否在插件网格内,如果是则不处理拖拽关闭
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (target.closest('.plugin-grid')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const currentY = touch.clientY
|
const currentY = touch.clientY
|
||||||
const currentTime = Date.now()
|
const currentTime = Date.now()
|
||||||
const deltaY = startY.value - currentY // 向上为正值
|
const deltaY = startY.value - currentY // 向上为正值
|
||||||
@@ -561,6 +575,7 @@ function handleBackdropClick(event: MouseEvent) {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
max-block-size: calc(100vh - 200px); // 确保有最大高度限制
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
-ms-overflow-style: none; // IE/Edge
|
-ms-overflow-style: none; // IE/Edge
|
||||||
@@ -571,6 +586,7 @@ function handleBackdropClick(event: MouseEvent) {
|
|||||||
// 隐藏滚动条
|
// 隐藏滚动条
|
||||||
scrollbar-width: none; // Firefox
|
scrollbar-width: none; // Firefox
|
||||||
touch-action: pan-y;
|
touch-action: pan-y;
|
||||||
|
will-change: scroll-position;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none; // WebKit 浏览器
|
display: none; // WebKit 浏览器
|
||||||
|
|||||||
Reference in New Issue
Block a user