From eb70ca233b3c33fbf6a6c1f5f1c12e468ff8eb33 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 3 Jul 2025 08:48:44 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84DefaultLayout.vue=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/usePullDownGesture.ts | 255 ++++++++++++++++++++ src/composables/useScrollLock.ts | 159 +++++++++++++ src/layouts/components/DefaultLayout.vue | 283 +++-------------------- src/views/system/MessageView.vue | 8 +- 4 files changed, 441 insertions(+), 264 deletions(-) create mode 100644 src/composables/usePullDownGesture.ts create mode 100644 src/composables/useScrollLock.ts diff --git a/src/composables/usePullDownGesture.ts b/src/composables/usePullDownGesture.ts new file mode 100644 index 00000000..7438536a --- /dev/null +++ b/src/composables/usePullDownGesture.ts @@ -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 + // 检查是否可以使用下拉手势的函数 + 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, + } +} diff --git a/src/composables/useScrollLock.ts b/src/composables/useScrollLock.ts new file mode 100644 index 00000000..1867e61c --- /dev/null +++ b/src/composables/useScrollLock.ts @@ -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 = { + 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, + } +} diff --git a/src/layouts/components/DefaultLayout.vue b/src/layouts/components/DefaultLayout.vue index 7de233e8..a3cb3df2 100644 --- a/src/layouts/components/DefaultLayout.vue +++ b/src/layouts/components/DefaultLayout.vue @@ -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([]) // 插件快速访问相关状态 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) - } }) }) @@ -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, }" >
{ :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(() => {
- -