mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-27 03:09:45 +08:00
重构DefaultLayout.vue组件
This commit is contained in:
255
src/composables/usePullDownGesture.ts
Normal file
255
src/composables/usePullDownGesture.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { ref, computed, onMounted, onBeforeUnmount, inject, readonly } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
// 下拉手势配置类型
|
||||
export interface PullDownConfig {
|
||||
START_THRESHOLD: number // 开始下拉的最小距离
|
||||
SHOW_INDICATOR: number // 显示指示器的距离
|
||||
TRIGGER_THRESHOLD: number // 触发回调的距离
|
||||
MAX_PULL_DISTANCE: number // 最大下拉距离
|
||||
PULL_RESISTANCE: number // 下拉阻力系数
|
||||
CONTENT_FOLLOW_RATIO: number // 页面内容跟随比例
|
||||
TOLERANCE: number // 手指抖动容忍度
|
||||
}
|
||||
|
||||
// 下拉手势选项
|
||||
export interface PullDownOptions {
|
||||
config?: Partial<PullDownConfig>
|
||||
// 检查是否可以使用下拉手势的函数
|
||||
canUsePullGesture?: () => boolean
|
||||
// 触发回调
|
||||
onTrigger?: () => void
|
||||
// 是否启用(默认true)
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG: PullDownConfig = {
|
||||
START_THRESHOLD: 20,
|
||||
SHOW_INDICATOR: 60,
|
||||
TRIGGER_THRESHOLD: 100,
|
||||
MAX_PULL_DISTANCE: 200,
|
||||
PULL_RESISTANCE: 0.75,
|
||||
CONTENT_FOLLOW_RATIO: 0.4,
|
||||
TOLERANCE: 80,
|
||||
}
|
||||
|
||||
export function usePullDownGesture(options: PullDownOptions = {}) {
|
||||
const display = useDisplay()
|
||||
const route = useRoute()
|
||||
const appMode = inject('pwaMode')
|
||||
|
||||
// 合并配置
|
||||
const config = { ...DEFAULT_CONFIG, ...options.config }
|
||||
|
||||
// 状态管理
|
||||
const isPulling = ref(false)
|
||||
const startY = ref(0)
|
||||
const pullDistance = ref(0)
|
||||
const initialScrollTop = ref(0)
|
||||
const hasDialogOpen = ref(false)
|
||||
const lastDialogCheckTime = ref(0)
|
||||
const DIALOG_CHECK_INTERVAL = 500
|
||||
|
||||
// 计算属性
|
||||
const contentTransform = computed(() => {
|
||||
if (!isPulling.value || pullDistance.value <= 0) return 'translateY(0)'
|
||||
const moveDistance = pullDistance.value * config.CONTENT_FOLLOW_RATIO
|
||||
return `translateY(${moveDistance}px)`
|
||||
})
|
||||
|
||||
const contentTransition = computed(() => {
|
||||
return isPulling.value ? 'none' : 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||
})
|
||||
|
||||
const showPullIndicator = computed(() => {
|
||||
return isPulling.value && pullDistance.value >= config.SHOW_INDICATOR
|
||||
})
|
||||
|
||||
const indicatorRotation = computed(() => {
|
||||
if (!isPulling.value) return 0
|
||||
const progress = Math.min(
|
||||
(pullDistance.value - config.SHOW_INDICATOR) / (config.TRIGGER_THRESHOLD - config.SHOW_INDICATOR),
|
||||
1,
|
||||
)
|
||||
return progress * 180
|
||||
})
|
||||
|
||||
const indicatorOpacity = computed(() => {
|
||||
if (!isPulling.value) return 0
|
||||
const progress = Math.min(
|
||||
(pullDistance.value - config.SHOW_INDICATOR) / (config.TRIGGER_THRESHOLD - config.SHOW_INDICATOR),
|
||||
1,
|
||||
)
|
||||
return 0.7 + progress * 0.3
|
||||
})
|
||||
|
||||
const indicatorTransform = computed(() => {
|
||||
return `translate(-50%, ${Math.min(20 + pullDistance.value - config.SHOW_INDICATOR, 50)}px)`
|
||||
})
|
||||
|
||||
// 弹窗检测函数
|
||||
const hasOpenDialog = (excludeSelector?: string) => {
|
||||
try {
|
||||
const dialogSelectors = [
|
||||
'.v-overlay--active:not(.v-overlay--scroll-blocked)',
|
||||
'.v-dialog--active',
|
||||
'.v-menu--active',
|
||||
'.v-bottom-sheet--active',
|
||||
'.v-snackbar--active',
|
||||
'[role="dialog"]:not([style*="display: none"])',
|
||||
'.modal:not(.d-none):not([style*="display: none"])',
|
||||
'[aria-modal="true"]:not([style*="display: none"])',
|
||||
]
|
||||
|
||||
for (const selector of dialogSelectors) {
|
||||
const elements = document.querySelectorAll(selector)
|
||||
if (elements.length > 0) {
|
||||
// 如果需要排除特定元素(如QuickAccess面板)
|
||||
if (excludeSelector && elements.length === 1) {
|
||||
const element = elements[0]
|
||||
if (element.closest(excludeSelector)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.warn('检测弹窗状态时出错:', error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 事件处理函数
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
if (!appMode || !display.mdAndDown.value || !options.enabled) return
|
||||
|
||||
// 检查是否可以使用下拉手势
|
||||
if (options.canUsePullGesture && !options.canUsePullGesture()) return
|
||||
|
||||
// 检查是否有弹窗打开
|
||||
hasDialogOpen.value = hasOpenDialog('.quick-access-panel')
|
||||
lastDialogCheckTime.value = Date.now()
|
||||
|
||||
if (hasDialogOpen.value) return
|
||||
|
||||
const touch = event.touches[0]
|
||||
startY.value = touch.clientY
|
||||
|
||||
// 重置下拉状态
|
||||
isPulling.value = false
|
||||
pullDistance.value = 0
|
||||
|
||||
// 记录开始时的滚动位置
|
||||
initialScrollTop.value = window.scrollY || document.documentElement.scrollTop || 0
|
||||
}
|
||||
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
if (!appMode || !display.mdAndDown.value || !options.enabled) return
|
||||
|
||||
// 检查是否可以使用下拉手势
|
||||
if (options.canUsePullGesture && !options.canUsePullGesture()) return
|
||||
|
||||
// 只在必要时重新检测弹窗
|
||||
const currentTime = Date.now()
|
||||
if (currentTime - lastDialogCheckTime.value > DIALOG_CHECK_INTERVAL) {
|
||||
hasDialogOpen.value = hasOpenDialog('.quick-access-panel')
|
||||
lastDialogCheckTime.value = currentTime
|
||||
}
|
||||
|
||||
if (hasDialogOpen.value) {
|
||||
isPulling.value = false
|
||||
pullDistance.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
const touch = event.touches[0]
|
||||
const deltaY = touch.clientY - startY.value
|
||||
|
||||
if (isPulling.value) {
|
||||
if (deltaY > -config.TOLERANCE) {
|
||||
pullDistance.value = Math.max(0, Math.min(deltaY * config.PULL_RESISTANCE, config.MAX_PULL_DISTANCE))
|
||||
event.preventDefault()
|
||||
} else {
|
||||
isPulling.value = false
|
||||
pullDistance.value = 0
|
||||
}
|
||||
} else {
|
||||
if (deltaY > config.START_THRESHOLD) {
|
||||
const currentScrollTop = window.scrollY || document.documentElement.scrollTop || 0
|
||||
|
||||
if (currentScrollTop <= 100 && initialScrollTop.value <= 100) {
|
||||
isPulling.value = true
|
||||
pullDistance.value = Math.min(deltaY * config.PULL_RESISTANCE, config.MAX_PULL_DISTANCE)
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!appMode || !display.mdAndDown.value || !options.enabled) return
|
||||
|
||||
// 检查是否可以使用下拉手势
|
||||
if (options.canUsePullGesture && !options.canUsePullGesture()) return
|
||||
|
||||
// 重置弹窗检测标志
|
||||
hasDialogOpen.value = false
|
||||
lastDialogCheckTime.value = 0
|
||||
|
||||
if (isPulling.value && pullDistance.value >= config.TRIGGER_THRESHOLD) {
|
||||
// 达到触发阈值,执行回调
|
||||
options.onTrigger?.()
|
||||
}
|
||||
|
||||
// 停止拖拽状态
|
||||
isPulling.value = false
|
||||
|
||||
// 延迟重置其他状态
|
||||
setTimeout(() => {
|
||||
pullDistance.value = 0
|
||||
startY.value = 0
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 生命周期管理
|
||||
onMounted(() => {
|
||||
if (appMode && display.mdAndDown.value) {
|
||||
document.addEventListener('touchstart', handleTouchStart, { passive: false })
|
||||
document.addEventListener('touchmove', handleTouchMove, { passive: false })
|
||||
document.addEventListener('touchend', handleTouchEnd, { passive: true })
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (appMode && display.mdAndDown.value) {
|
||||
document.removeEventListener('touchstart', handleTouchStart)
|
||||
document.removeEventListener('touchmove', handleTouchMove)
|
||||
document.removeEventListener('touchend', handleTouchEnd)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isPulling: readonly(isPulling),
|
||||
pullDistance: readonly(pullDistance),
|
||||
|
||||
// 计算属性
|
||||
contentTransform,
|
||||
contentTransition,
|
||||
showPullIndicator,
|
||||
indicatorRotation,
|
||||
indicatorOpacity,
|
||||
indicatorTransform,
|
||||
|
||||
// 配置
|
||||
config,
|
||||
|
||||
// 工具函数
|
||||
hasOpenDialog,
|
||||
}
|
||||
}
|
||||
159
src/composables/useScrollLock.ts
Normal file
159
src/composables/useScrollLock.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { ref, watch, onBeforeUnmount, readonly } from 'vue'
|
||||
|
||||
// 滚动锁定配置选项
|
||||
export interface ScrollLockOptions {
|
||||
// 是否在组件卸载时自动恢复滚动(默认true)
|
||||
autoRestore?: boolean
|
||||
// 是否保存和恢复滚动位置(默认true)
|
||||
preserveScrollPosition?: boolean
|
||||
// 自定义锁定时的样式
|
||||
lockStyles?: {
|
||||
overflow?: string
|
||||
position?: string
|
||||
width?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_OPTIONS: Required<ScrollLockOptions> = {
|
||||
autoRestore: true,
|
||||
preserveScrollPosition: true,
|
||||
lockStyles: {
|
||||
overflow: 'hidden',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
},
|
||||
}
|
||||
|
||||
export function useScrollLock(options: ScrollLockOptions = {}) {
|
||||
const config = { ...DEFAULT_OPTIONS, ...options }
|
||||
|
||||
// 状态管理
|
||||
const isLocked = ref(false)
|
||||
const savedScrollPosition = ref(0)
|
||||
const originalBodyStyles = ref<{ [key: string]: string }>({})
|
||||
const originalDocumentStyles = ref<{ [key: string]: string }>({})
|
||||
|
||||
// 保存当前滚动位置
|
||||
const saveScrollPosition = () => {
|
||||
if (config.preserveScrollPosition) {
|
||||
savedScrollPosition.value =
|
||||
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 保存原始样式
|
||||
const saveOriginalStyles = () => {
|
||||
// 保存 body 样式
|
||||
originalBodyStyles.value = {
|
||||
overflow: document.body.style.overflow,
|
||||
position: document.body.style.position,
|
||||
top: document.body.style.top,
|
||||
width: document.body.style.width,
|
||||
}
|
||||
|
||||
// 保存 documentElement 样式
|
||||
originalDocumentStyles.value = {
|
||||
overflow: document.documentElement.style.overflow,
|
||||
}
|
||||
}
|
||||
|
||||
// 锁定滚动
|
||||
const lockScroll = () => {
|
||||
if (isLocked.value) return
|
||||
|
||||
// 保存当前状态
|
||||
saveScrollPosition()
|
||||
saveOriginalStyles()
|
||||
|
||||
// 应用锁定样式
|
||||
document.body.style.overflow = config.lockStyles.overflow || 'hidden'
|
||||
document.body.style.position = config.lockStyles.position || 'fixed'
|
||||
document.body.style.width = config.lockStyles.width || '100%'
|
||||
document.documentElement.style.overflow = config.lockStyles.overflow || 'hidden'
|
||||
|
||||
// 如果需要保持滚动位置,设置top偏移
|
||||
if (config.preserveScrollPosition) {
|
||||
document.body.style.top = `-${savedScrollPosition.value}px`
|
||||
}
|
||||
|
||||
isLocked.value = true
|
||||
}
|
||||
|
||||
// 恢复滚动
|
||||
const restoreScroll = () => {
|
||||
if (!isLocked.value) return
|
||||
|
||||
// 恢复原始样式
|
||||
document.body.style.overflow = originalBodyStyles.value.overflow || ''
|
||||
document.body.style.position = originalBodyStyles.value.position || ''
|
||||
document.body.style.top = originalBodyStyles.value.top || ''
|
||||
document.body.style.width = originalBodyStyles.value.width || ''
|
||||
document.documentElement.style.overflow = originalDocumentStyles.value.overflow || ''
|
||||
|
||||
// 恢复滚动位置
|
||||
if (config.preserveScrollPosition) {
|
||||
window.scrollTo(0, savedScrollPosition.value)
|
||||
}
|
||||
|
||||
isLocked.value = false
|
||||
}
|
||||
|
||||
// 切换滚动锁定状态
|
||||
const toggleScrollLock = (lock?: boolean) => {
|
||||
const shouldLock = lock !== undefined ? lock : !isLocked.value
|
||||
|
||||
if (shouldLock) {
|
||||
lockScroll()
|
||||
} else {
|
||||
restoreScroll()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听响应式值的变化
|
||||
const watchTarget = (target: any) => {
|
||||
return watch(
|
||||
target,
|
||||
newValue => {
|
||||
toggleScrollLock(!!newValue)
|
||||
},
|
||||
{ immediate: false },
|
||||
)
|
||||
}
|
||||
|
||||
// 生命周期清理
|
||||
onBeforeUnmount(() => {
|
||||
if (config.autoRestore && isLocked.value) {
|
||||
restoreScroll()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isLocked: readonly(isLocked),
|
||||
savedScrollPosition: readonly(savedScrollPosition),
|
||||
|
||||
// 方法
|
||||
lockScroll,
|
||||
restoreScroll,
|
||||
toggleScrollLock,
|
||||
watchTarget,
|
||||
|
||||
// 工具方法
|
||||
saveScrollPosition,
|
||||
}
|
||||
}
|
||||
|
||||
// 便捷的自动监听版本
|
||||
export function useScrollLockWithWatch(target: any, options: ScrollLockOptions = {}) {
|
||||
const scrollLock = useScrollLock(options)
|
||||
|
||||
// 自动监听目标值的变化
|
||||
const stopWatcher = scrollLock.watchTarget(target)
|
||||
|
||||
// 返回所有功能 + 停止监听的方法
|
||||
return {
|
||||
...scrollLock,
|
||||
stopWatcher,
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { filterMenusByPermission } from '@/utils/permission'
|
||||
import { onUnreadMessage } from '@/utils/badge'
|
||||
import { usePullDownGesture } from '@/composables/usePullDownGesture'
|
||||
import { useScrollLockWithWatch } from '@/composables/useScrollLock'
|
||||
|
||||
const display = useDisplay()
|
||||
const appMode = inject('pwaMode')
|
||||
@@ -55,114 +57,37 @@ const systemMenus = ref<NavMenu[]>([])
|
||||
// 插件快速访问相关状态
|
||||
const showPluginQuickAccess = ref(false)
|
||||
|
||||
// 下拉手势配置常量 (iOS风格)
|
||||
const PULL_CONFIG = {
|
||||
START_THRESHOLD: 20, // 开始下拉的最小距离
|
||||
SHOW_INDICATOR: 60, // 显示指示器的距离
|
||||
TRIGGER_THRESHOLD: 100, // 触发快速访问的距离
|
||||
MAX_PULL_DISTANCE: 200, // 最大下拉距离
|
||||
PULL_RESISTANCE: 0.75, // 下拉阻力系数
|
||||
CONTENT_FOLLOW_RATIO: 0.4, // 页面内容跟随比例
|
||||
TOLERANCE: 80, // 手指抖动容忍度
|
||||
}
|
||||
|
||||
// 下拉检测相关状态
|
||||
const isPulling = ref(false)
|
||||
const startY = ref(0)
|
||||
const pullDistance = ref(0)
|
||||
const initialScrollTop = ref(0)
|
||||
|
||||
// 检查是否有弹窗打开的函数
|
||||
const hasOpenDialog = () => {
|
||||
try {
|
||||
// 检查 Vuetify 的各种弹窗组件
|
||||
const vuetifyOverlays = document.querySelectorAll('.v-overlay--active:not(.v-overlay--scroll-blocked)')
|
||||
const dialogs = document.querySelectorAll('.v-dialog--active')
|
||||
const menus = document.querySelectorAll('.v-menu--active')
|
||||
const bottomSheets = document.querySelectorAll('.v-bottom-sheet--active')
|
||||
const snackbars = document.querySelectorAll('.v-snackbar--active')
|
||||
|
||||
// 检查自定义弹窗元素
|
||||
const customDialogs = document.querySelectorAll('[role="dialog"]:not([style*="display: none"])')
|
||||
const modalElements = document.querySelectorAll('.modal:not(.d-none):not([style*="display: none"])')
|
||||
|
||||
// 检查具有弹窗特征的元素
|
||||
const dialogElements = document.querySelectorAll('[aria-modal="true"]:not([style*="display: none"])')
|
||||
|
||||
// 计算有效的弹窗数量
|
||||
let totalDialogs =
|
||||
vuetifyOverlays.length +
|
||||
dialogs.length +
|
||||
menus.length +
|
||||
bottomSheets.length +
|
||||
snackbars.length +
|
||||
customDialogs.length +
|
||||
modalElements.length +
|
||||
dialogElements.length
|
||||
|
||||
// 如果 QuickAccess 面板打开,不算作阻止下拉的弹窗
|
||||
if (showPluginQuickAccess.value) {
|
||||
totalDialogs = Math.max(0, totalDialogs - 1)
|
||||
}
|
||||
|
||||
return totalDialogs > 0
|
||||
} catch (error) {
|
||||
console.warn('检测弹窗状态时出错:', error)
|
||||
// 出错时保守处理,认为有弹窗打开
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 使用滚动锁定 composable(自动监听showPluginQuickAccess的变化)
|
||||
useScrollLockWithWatch(showPluginQuickAccess)
|
||||
|
||||
// 检查是否可以使用下拉手势
|
||||
const canUsePullGesture = computed(() => {
|
||||
const canUsePullGesture = () => {
|
||||
// 检查是否在dashboard页面
|
||||
const isDashboard = route.name === 'dashboard' || route.path === '/dashboard'
|
||||
|
||||
// 检查是否是管理员
|
||||
const isAdmin = superUser.value
|
||||
// 检查插件快速访问面板是否已显示
|
||||
const quickAccessOpen = showPluginQuickAccess.value
|
||||
|
||||
return isDashboard && isAdmin
|
||||
})
|
||||
return isDashboard && isAdmin && !quickAccessOpen
|
||||
}
|
||||
|
||||
// 计算页面内容的transform
|
||||
const contentTransform = computed(() => {
|
||||
if (!isPulling.value || pullDistance.value <= 0) return 'translateY(0)'
|
||||
// 页面内容跟随下拉距离,使用配置的跟随比例
|
||||
const moveDistance = pullDistance.value * PULL_CONFIG.CONTENT_FOLLOW_RATIO
|
||||
return `translateY(${moveDistance}px)`
|
||||
})
|
||||
|
||||
// 计算页面内容的transition
|
||||
const contentTransition = computed(() => {
|
||||
// 拖拽时不使用transition,松手后使用transition回弹
|
||||
return isPulling.value ? 'none' : 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||
})
|
||||
|
||||
// 计算下拉指示器的显示状态
|
||||
const showPullIndicator = computed(() => {
|
||||
return canUsePullGesture.value && isPulling.value && pullDistance.value >= PULL_CONFIG.SHOW_INDICATOR
|
||||
})
|
||||
|
||||
// 计算下拉指示器的旋转角度
|
||||
const indicatorRotation = computed(() => {
|
||||
if (!isPulling.value) return 0
|
||||
// 从显示指示器开始计算旋转,到触发阈值时旋转180度
|
||||
const progress = Math.min(
|
||||
(pullDistance.value - PULL_CONFIG.SHOW_INDICATOR) / (PULL_CONFIG.TRIGGER_THRESHOLD - PULL_CONFIG.SHOW_INDICATOR),
|
||||
1,
|
||||
)
|
||||
return progress * 180 // 0到180度的旋转
|
||||
})
|
||||
|
||||
// 计算下拉指示器的透明度
|
||||
const indicatorOpacity = computed(() => {
|
||||
if (!isPulling.value) return 0
|
||||
// 从显示指示器开始计算透明度
|
||||
const progress = Math.min(
|
||||
(pullDistance.value - PULL_CONFIG.SHOW_INDICATOR) / (PULL_CONFIG.TRIGGER_THRESHOLD - PULL_CONFIG.SHOW_INDICATOR),
|
||||
1,
|
||||
)
|
||||
return 0.7 + progress * 0.3 // 0.7到1.0的透明度
|
||||
// 使用下拉手势 composable
|
||||
const {
|
||||
pullDistance,
|
||||
contentTransform,
|
||||
contentTransition,
|
||||
showPullIndicator,
|
||||
indicatorRotation,
|
||||
indicatorOpacity,
|
||||
indicatorTransform,
|
||||
config: PULL_CONFIG,
|
||||
} = usePullDownGesture({
|
||||
enabled: true,
|
||||
canUsePullGesture,
|
||||
onTrigger: () => {
|
||||
showPluginQuickAccess.value = true
|
||||
},
|
||||
})
|
||||
|
||||
// 根据分类获取菜单列表
|
||||
@@ -190,116 +115,6 @@ function handleUnreadMessage(count: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理触摸开始
|
||||
function handleTouchStart(event: TouchEvent) {
|
||||
if (!appMode || !display.mdAndDown.value) return
|
||||
|
||||
// 检查是否满足下拉手势的条件
|
||||
if (!canUsePullGesture.value) return
|
||||
|
||||
// 实时检查是否有弹窗打开
|
||||
if (hasOpenDialog()) return
|
||||
|
||||
// 如果插件快速访问面板已显示,不处理下拉手势
|
||||
if (showPluginQuickAccess.value) return
|
||||
|
||||
const touch = event.touches[0]
|
||||
startY.value = touch.clientY
|
||||
|
||||
// 重置下拉状态,但不立即阻止滚动
|
||||
isPulling.value = false
|
||||
pullDistance.value = 0
|
||||
|
||||
// 记录开始时的滚动位置,用于更准确的判断
|
||||
initialScrollTop.value = window.scrollY || document.documentElement.scrollTop || 0
|
||||
}
|
||||
|
||||
// 处理触摸移动
|
||||
function handleTouchMove(event: TouchEvent) {
|
||||
if (!appMode || !display.mdAndDown.value) return
|
||||
|
||||
// 检查是否满足下拉手势的条件
|
||||
if (!canUsePullGesture.value) return
|
||||
|
||||
// 实时检查是否有弹窗打开
|
||||
if (hasOpenDialog()) {
|
||||
// 如果检测到弹窗打开,立即停止下拉
|
||||
isPulling.value = false
|
||||
pullDistance.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
// 如果插件快速访问面板已显示,不处理下拉手势
|
||||
if (showPluginQuickAccess.value) return
|
||||
|
||||
const touch = event.touches[0]
|
||||
const deltaY = touch.clientY - startY.value
|
||||
|
||||
// 如果已经开始下拉,继续保持下拉状态,避免中途中断
|
||||
if (isPulling.value) {
|
||||
// 继续下拉,但要确保是向下移动
|
||||
if (deltaY > -PULL_CONFIG.TOLERANCE) {
|
||||
// 允许轻微的向上偏移,避免手指抖动导致中断
|
||||
pullDistance.value = Math.max(0, Math.min(deltaY * PULL_CONFIG.PULL_RESISTANCE, PULL_CONFIG.MAX_PULL_DISTANCE))
|
||||
// 阻止默认滚动行为
|
||||
event.preventDefault()
|
||||
} else {
|
||||
// 如果向上移动超过容忍度,停止下拉
|
||||
isPulling.value = false
|
||||
pullDistance.value = 0
|
||||
}
|
||||
} else {
|
||||
// 还没开始下拉,检查是否应该开始
|
||||
if (deltaY > PULL_CONFIG.START_THRESHOLD) {
|
||||
// 检查当前的滚动位置
|
||||
const currentScrollTop = window.scrollY || document.documentElement.scrollTop || 0
|
||||
|
||||
// 必须同时满足:1. 向下拖拽超过阈值 2. 当前在页面顶部 3. 从顶部开始拖拽
|
||||
if (currentScrollTop <= 100 && initialScrollTop.value <= 100) {
|
||||
// 向下拖拽且在页面顶部附近,开始下拉
|
||||
isPulling.value = true
|
||||
pullDistance.value = Math.min(deltaY * PULL_CONFIG.PULL_RESISTANCE, PULL_CONFIG.MAX_PULL_DISTANCE)
|
||||
// 阻止默认滚动
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理触摸结束
|
||||
function handleTouchEnd() {
|
||||
if (!appMode || !display.mdAndDown.value) return
|
||||
|
||||
// 检查是否满足下拉手势的条件
|
||||
if (!canUsePullGesture.value) return
|
||||
|
||||
// 实时检查是否有弹窗打开
|
||||
if (hasOpenDialog()) {
|
||||
// 如果检测到弹窗打开,立即停止下拉并重置状态
|
||||
isPulling.value = false
|
||||
pullDistance.value = 0
|
||||
startY.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
// 如果插件快速访问面板已显示,不处理下拉手势
|
||||
if (showPluginQuickAccess.value) return
|
||||
|
||||
if (isPulling.value && pullDistance.value >= PULL_CONFIG.TRIGGER_THRESHOLD) {
|
||||
// 达到触发阈值,触发插件快速访问
|
||||
showPluginQuickAccess.value = true
|
||||
}
|
||||
|
||||
// 先停止拖拽状态,触发回弹动画
|
||||
isPulling.value = false
|
||||
|
||||
// 延迟重置其他状态,让动画完成
|
||||
setTimeout(() => {
|
||||
pullDistance.value = 0
|
||||
startY.value = 0
|
||||
}, 300) // 与transition时间匹配
|
||||
}
|
||||
|
||||
// 关闭插件快速访问
|
||||
function handleClosePluginQuickAccess() {
|
||||
showPluginQuickAccess.value = false
|
||||
@@ -310,34 +125,6 @@ function handlePluginClick() {
|
||||
showPluginQuickAccess.value = false
|
||||
}
|
||||
|
||||
// 保存页面滚动位置
|
||||
let scrollPosition = 0
|
||||
|
||||
// 监听插件快速访问的显示状态,控制背景滚动
|
||||
watch(showPluginQuickAccess, visible => {
|
||||
if (visible) {
|
||||
// 保存当前滚动位置
|
||||
scrollPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||
|
||||
// 显示时锁定背景滚动
|
||||
document.body.style.overflow = 'hidden'
|
||||
document.body.style.position = 'fixed'
|
||||
document.body.style.top = `-${scrollPosition}px`
|
||||
document.body.style.width = '100%'
|
||||
document.documentElement.style.overflow = 'hidden'
|
||||
} else {
|
||||
// 隐藏时恢复滚动
|
||||
document.body.style.overflow = ''
|
||||
document.body.style.position = ''
|
||||
document.body.style.top = ''
|
||||
document.body.style.width = ''
|
||||
document.documentElement.style.overflow = ''
|
||||
|
||||
// 恢复滚动位置
|
||||
window.scrollTo(0, scrollPosition)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 获取菜单列表
|
||||
startMenus.value = getMenuList(t('menu.start'))
|
||||
@@ -349,27 +136,9 @@ onMounted(() => {
|
||||
// 监听全局未读消息事件
|
||||
const unsubscribe = onUnreadMessage(handleUnreadMessage)
|
||||
|
||||
// 只在appMode下添加触摸事件监听
|
||||
if (appMode && display.mdAndDown.value) {
|
||||
document.addEventListener('touchstart', handleTouchStart, { passive: false })
|
||||
document.addEventListener('touchmove', handleTouchMove, { passive: false })
|
||||
document.addEventListener('touchend', handleTouchEnd, { passive: true })
|
||||
}
|
||||
|
||||
// 组件卸载时清理监听
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribe()
|
||||
// 恢复body滚动样式
|
||||
document.body.style.overflow = ''
|
||||
document.body.style.position = ''
|
||||
document.body.style.top = ''
|
||||
document.body.style.width = ''
|
||||
document.documentElement.style.overflow = ''
|
||||
if (appMode && display.mdAndDown.value) {
|
||||
document.removeEventListener('touchstart', handleTouchStart)
|
||||
document.removeEventListener('touchmove', handleTouchMove)
|
||||
document.removeEventListener('touchend', handleTouchEnd)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -381,7 +150,7 @@ onMounted(() => {
|
||||
class="pull-indicator"
|
||||
:style="{
|
||||
opacity: indicatorOpacity,
|
||||
transform: `translate(-50%, ${Math.min(20 + pullDistance - PULL_CONFIG.SHOW_INDICATOR, 50)}px)`,
|
||||
transform: indicatorTransform,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -121,7 +121,7 @@ onBeforeUnmount(() => {
|
||||
:mode="!isLoaded ? 'intersect' : 'manual'"
|
||||
side="start"
|
||||
:items="messages"
|
||||
class="overflow-visible message-scroll h-full"
|
||||
class="overflow-auto h-full"
|
||||
@load="loadMessages"
|
||||
:load-more-text="t('message.loadMore') + ' ...'"
|
||||
>
|
||||
@@ -143,9 +143,3 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.message-scroll {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user