From b8dff560f03f974bc410e588ba18e2a7e6e35d15 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 2 Jul 2025 14:18:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8F=92=E4=BB=B6=E5=BF=AB?= =?UTF-8?q?=E9=80=9F=E8=AE=BF=E9=97=AE=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=8B=E6=8B=89=E6=89=8B=E5=8A=BF=E8=A7=A6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/misc/PluginQuickAccess.vue | 591 ++++++++++++++++++++++ src/composables/useRecentPlugins.ts | 113 +++++ src/layouts/components/DefaultLayout.vue | 254 +++++++++- src/layouts/components/HeaderTab.vue | 4 + src/locales/en-US.ts | 8 + src/locales/zh-CN.ts | 8 + src/locales/zh-TW.ts | 8 + src/main.ts | 2 + src/pages/[...all].vue | 16 +- src/styles/custom.scss | 5 + 10 files changed, 993 insertions(+), 16 deletions(-) create mode 100644 src/components/misc/PluginQuickAccess.vue create mode 100644 src/composables/useRecentPlugins.ts diff --git a/src/components/misc/PluginQuickAccess.vue b/src/components/misc/PluginQuickAccess.vue new file mode 100644 index 00000000..951138c3 --- /dev/null +++ b/src/components/misc/PluginQuickAccess.vue @@ -0,0 +1,591 @@ + + + + + diff --git a/src/composables/useRecentPlugins.ts b/src/composables/useRecentPlugins.ts new file mode 100644 index 00000000..072bd5f3 --- /dev/null +++ b/src/composables/useRecentPlugins.ts @@ -0,0 +1,113 @@ +import type { Plugin } from '@/api/types' + +const RECENT_PLUGINS_KEY = 'moviepilot_recent_plugins' +const MAX_RECENT_PLUGINS = 5 + +interface RecentPlugin { + id: string + plugin_name: string + plugin_icon?: string + has_page: boolean + state: boolean + plugin_id: string + access_time: number +} + +// 将Plugin转换为RecentPlugin +function pluginToRecentPlugin(plugin: Plugin): RecentPlugin { + return { + id: plugin.id || '', + plugin_name: plugin.plugin_name || '', + plugin_icon: plugin.plugin_icon, + has_page: plugin.has_page || false, + state: plugin.state || false, + plugin_id: plugin.plugin_id || '', + access_time: Date.now(), + } +} + +// 将RecentPlugin转换为Plugin +function recentPluginToPlugin(recentPlugin: RecentPlugin): Plugin { + return { + id: recentPlugin.id, + plugin_name: recentPlugin.plugin_name, + plugin_icon: recentPlugin.plugin_icon, + has_page: recentPlugin.has_page, + state: recentPlugin.state, + plugin_id: recentPlugin.plugin_id, + } as Plugin +} + +export function useRecentPlugins() { + // 获取最近访问的插件 + function getRecentPlugins(): Plugin[] { + try { + const stored = localStorage.getItem(RECENT_PLUGINS_KEY) + if (!stored) return [] + + const recentPlugins: RecentPlugin[] = JSON.parse(stored) + + // 按访问时间倒序排列 + return recentPlugins.sort((a, b) => b.access_time - a.access_time).map(recentPluginToPlugin) + } catch (error) { + console.error('获取最近访问插件失败:', error) + return [] + } + } + + // 添加插件到最近访问 + function addRecentPlugin(plugin: Plugin) { + try { + if (!plugin.id || !plugin.has_page) return + + const stored = localStorage.getItem(RECENT_PLUGINS_KEY) + let recentPlugins: RecentPlugin[] = stored ? JSON.parse(stored) : [] + + // 移除已存在的相同插件(如果有的话) + recentPlugins = recentPlugins.filter(p => p.id !== plugin.id) + + // 添加新的插件到开头 + recentPlugins.unshift(pluginToRecentPlugin(plugin)) + + // 限制最大数量 + if (recentPlugins.length > MAX_RECENT_PLUGINS) { + recentPlugins = recentPlugins.slice(0, MAX_RECENT_PLUGINS) + } + + localStorage.setItem(RECENT_PLUGINS_KEY, JSON.stringify(recentPlugins)) + } catch (error) { + console.error('保存最近访问插件失败:', error) + } + } + + // 清除所有最近访问记录 + function clearRecentPlugins() { + try { + localStorage.removeItem(RECENT_PLUGINS_KEY) + } catch (error) { + console.error('清除最近访问插件失败:', error) + } + } + + // 移除特定插件 + function removeRecentPlugin(pluginId: string) { + try { + const stored = localStorage.getItem(RECENT_PLUGINS_KEY) + if (!stored) return + + let recentPlugins: RecentPlugin[] = JSON.parse(stored) + recentPlugins = recentPlugins.filter(p => p.id !== pluginId) + + localStorage.setItem(RECENT_PLUGINS_KEY, JSON.stringify(recentPlugins)) + } catch (error) { + console.error('移除最近访问插件失败:', error) + } + } + + return { + getRecentPlugins, + addRecentPlugin, + clearRecentPlugins, + removeRecentPlugin, + } +} diff --git a/src/layouts/components/DefaultLayout.vue b/src/layouts/components/DefaultLayout.vue index 43d2c78d..c6c38571 100644 --- a/src/layouts/components/DefaultLayout.vue +++ b/src/layouts/components/DefaultLayout.vue @@ -7,6 +7,7 @@ import UserNofification from '@/layouts/components/UserNotification.vue' import SearchBar from '@/layouts/components/SearchBar.vue' import ShortcutBar from '@/layouts/components/ShortcutBar.vue' import UserProfile from '@/layouts/components/UserProfile.vue' +import PluginQuickAccess from '@/components/misc/PluginQuickAccess.vue' import { useUserStore } from '@/stores' import { getNavMenus } from '@/router/i18n-menu' import { NavMenu } from '@/@layouts/types' @@ -49,6 +50,46 @@ const organizeMenus = ref([]) // 系统菜单项 const systemMenus = ref([]) +// 插件快速访问相关状态 +const showPluginQuickAccess = ref(false) + +// 下拉检测相关状态 +const isPulling = ref(false) +const startY = ref(0) +const pullDistance = ref(0) + +// 计算页面内容的transform +const contentTransform = computed(() => { + if (!isPulling.value || pullDistance.value <= 0) return 'translateY(0)' + // 页面内容的移动距离是下拉距离的30%,提供更自然的阻尼感 + const moveDistance = pullDistance.value * 0.3 + 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 isPulling.value && pullDistance.value > 20 +}) + +// 计算下拉指示器的旋转角度 +const indicatorRotation = computed(() => { + if (!isPulling.value) return 0 + const progress = Math.min(pullDistance.value / 120, 1) + return progress * 180 // 0到180度的旋转 +}) + +// 计算下拉指示器的透明度 +const indicatorOpacity = computed(() => { + if (!isPulling.value) return 0 + return Math.min(pullDistance.value / 60, 1) +}) + // 根据分类获取菜单列表 const getMenuList = (header: string) => { // 使用国际化菜单 @@ -74,6 +115,96 @@ function handleUnreadMessage(count: number) { } } +// 检查是否在页面顶部 +function isAtTop(): boolean { + return window.scrollY <= 5 +} + +// 处理触摸开始 +function handleTouchStart(event: TouchEvent) { + if (!appMode || !display.mdAndDown.value || !isAtTop()) return + + const touch = event.touches[0] + startY.value = touch.clientY + isPulling.value = false + pullDistance.value = 0 +} + +// 处理触摸移动 +function handleTouchMove(event: TouchEvent) { + if (!appMode || !display.mdAndDown.value || !isAtTop()) return + + const touch = event.touches[0] + const deltaY = touch.clientY - startY.value + + if (deltaY > 0 && isAtTop()) { + // 向下拖拽且在页面顶部 + isPulling.value = true + pullDistance.value = Math.min(deltaY * 0.6, 150) // 增加最大距离到150px + + // 阻止默认滚动 + event.preventDefault() + } +} + +// 处理触摸结束 +function handleTouchEnd() { + if (!appMode || !display.mdAndDown.value) return + + if (isPulling.value && pullDistance.value > 120) { + // 增加触发阈值到120px + // 触发插件快速访问 + showPluginQuickAccess.value = true + } + + // 先停止拖拽状态,触发回弹动画 + isPulling.value = false + + // 延迟重置其他状态,让动画完成 + setTimeout(() => { + pullDistance.value = 0 + startY.value = 0 + }, 300) // 与transition时间匹配 +} + +// 关闭插件快速访问 +function handleClosePluginQuickAccess() { + showPluginQuickAccess.value = false +} + +// 点击插件后关闭 +function handlePluginClick() { + showPluginQuickAccess.value = false +} + +// 阻止滚动的函数 +function preventScroll(e: TouchEvent) { + e.preventDefault() +} + +// 监听插件快速访问的显示状态,控制背景滚动 +watch(showPluginQuickAccess, visible => { + if (visible) { + // 显示时锁定背景滚动 - 使用更强的锁定方式 + document.body.style.overflow = 'hidden' + document.body.style.position = 'fixed' + document.body.style.width = '100%' + document.body.style.height = '100%' + document.documentElement.style.overflow = 'hidden' + // 禁用触摸滚动 + document.addEventListener('touchmove', preventScroll, { passive: false }) + } else { + // 隐藏时恢复滚动 + document.body.style.overflow = '' + document.body.style.position = '' + document.body.style.width = '' + document.body.style.height = '' + document.documentElement.style.overflow = '' + // 恢复触摸滚动 + document.removeEventListener('touchmove', preventScroll) + } +}) + onMounted(() => { // 获取菜单列表 startMenus.value = getMenuList(t('menu.start')) @@ -85,14 +216,54 @@ 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.width = '' + document.body.style.height = '' + document.documentElement.style.overflow = '' + document.removeEventListener('touchmove', preventScroll) + if (appMode && display.mdAndDown.value) { + document.removeEventListener('touchstart', handleTouchStart) + document.removeEventListener('touchmove', handleTouchMove) + document.removeEventListener('touchend', handleTouchEnd) + } }) })