diff --git a/public/offline.html b/public/offline.html deleted file mode 100644 index 126ceaa4..00000000 --- a/public/offline.html +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - - MoviePilot - 离线模式 - - - - -
- -
📡
-

网络连接已断开

-

- - MoviePilot 需要网络连接才能正常工作。请检查您的网络连接后重试。 -

- - - -
-

🔍 可能的解决方案

- -
- -
-

📱 关于MoviePilot

-

MoviePilot是一个智能媒体管理平台,需要稳定的网络连接来获取最新的电影、剧集信息以及执行各种管理操作。

-

感谢您的耐心等待,网络恢复后您将能够:

- -
-
- - - - - diff --git a/src/api/index.ts b/src/api/index.ts index 9197bc6c..c8e73da3 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,6 +2,7 @@ import axios from 'axios' import router from '@/router' import { useAuthStore } from '@/stores' import { initializeRequestOptimizer } from '@/utils/requestOptimizer' +import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus' // 创建axios实例 const api = axios.create({ @@ -32,15 +33,45 @@ api.interceptors.request.use(config => { return config }) +// 离线状态管理 +const globalOfflineStatus = useGlobalOfflineStatus() + // 添加响应拦截器 api.interceptors.response.use( response => { + // 成功响应时,清除应用离线状态 + globalOfflineStatus.setAppOffline(false) return response.data }, error => { if (!error.response) { - // 请求超时 - return Promise.reject(new Error(error)) + // 网络错误或请求超时 - 通知离线状态管理系统 + const isNetworkError = + error.code === 'NETWORK_ERROR' || + error.code === 'ERR_NETWORK' || + error.code === 'ECONNABORTED' || + error.name === 'NetworkError' + + if (isNetworkError) { + let reason = 'Network connection failed' + if (error.code === 'ECONNABORTED') { + reason = 'Request timeout' + } + globalOfflineStatus.setAppOffline(true, reason) + } + + if (error.code === 'NETWORK_ERROR' || error.code === 'ERR_NETWORK') { + // 网络连接问题 + return Promise.reject(new Error('Network connection failed, please check your network status')) + } else if (error.code === 'ECONNABORTED') { + // 请求超时 + return Promise.reject(new Error('Request timeout, please try again later')) + } else if (error.name === 'AbortError') { + // 请求被中止(路由切换等) + return Promise.reject(new Error('Request cancelled')) + } + // 其他网络错误 + return Promise.reject(new Error(error.message || 'Network error')) } else if (error.response.status === 403) { // 认证 Store const authStore = useAuthStore() diff --git a/src/composables/useOfflineStatus.ts b/src/composables/useOfflineStatus.ts new file mode 100644 index 00000000..6e589134 --- /dev/null +++ b/src/composables/useOfflineStatus.ts @@ -0,0 +1,61 @@ +import { ref, computed } from 'vue' +import { useOnline } from '@vueuse/core' + +// 全局状态 +const isAppOffline = ref(false) +const appOfflineReason = ref('') + +// 全局离线状态管理 +export function useGlobalOfflineStatus() { + const isOnline = useOnline() + + // 综合离线状态(网络离线 或 应用离线) + const isOffline = computed(() => !isOnline.value || isAppOffline.value) + + // 是否可以执行网络操作 + const canPerformNetworkAction = computed(() => isOnline.value && !isAppOffline.value) + + // 设置应用离线状态 + const setAppOffline = (offline: boolean, reason?: string) => { + isAppOffline.value = offline + appOfflineReason.value = reason || '' + } + + // 获取离线消息 + const getOfflineMessage = () => { + if (!isOnline.value) { + return appOfflineReason.value + } + if (isAppOffline.value) { + return appOfflineReason.value + } + return '' + } + + return { + isOnline, + isOffline, + canPerformNetworkAction, + setAppOffline, + getOfflineMessage, + } +} + +// 单个组件的离线状态 +export function useOfflineStatus(initialMessage?: string) { + const { isOnline, isOffline, canPerformNetworkAction, getOfflineMessage } = useGlobalOfflineStatus() + + const message = computed(() => { + if (initialMessage) { + return initialMessage + } + return getOfflineMessage() + }) + + return { + isOnline, + isOffline, + canPerformNetworkAction, + message, + } +} diff --git a/src/composables/usePullDownGesture.ts b/src/composables/usePullDownGesture.ts index ecaf5c1f..24eb23d4 100644 --- a/src/composables/usePullDownGesture.ts +++ b/src/composables/usePullDownGesture.ts @@ -1,6 +1,5 @@ import { ref, computed, onMounted, onBeforeUnmount, readonly, watch } from 'vue' import { useDisplay } from 'vuetify' -import { useRoute } from 'vue-router' import { usePWA } from './usePWA' // 下拉手势配置类型 diff --git a/src/layouts/components/DefaultLayout.vue b/src/layouts/components/DefaultLayout.vue index 0dea46b5..cd03844c 100644 --- a/src/layouts/components/DefaultLayout.vue +++ b/src/layouts/components/DefaultLayout.vue @@ -19,6 +19,8 @@ import { onUnreadMessage } from '@/utils/badge' import { usePullDownGesture } from '@/composables/usePullDownGesture' import { useScrollLockWithWatch } from '@/composables/useScrollLock' import { usePWA } from '@/composables/usePWA' +import OfflinePage from '@/layouts/components/OfflinePage.vue' +import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus' const display = useDisplay() // PWA模式检测 @@ -59,6 +61,20 @@ const systemMenus = ref([]) // 插件快速访问相关状态 const showPluginQuickAccess = ref(false) +// 离线状态管理 +const { setAppOffline, isOffline } = useGlobalOfflineStatus() + +// 监听Service Worker消息 +const handleServiceWorkerMessage = (event: MessageEvent) => { + if (event.data && event.data.type === 'OFFLINE_STATUS') { + if (event.data.offline) { + setAppOffline(true, t('common.serverConnectionFailed')) + } else { + setAppOffline(false) + } + } +} + // 使用滚动锁定 composable(自动监听showPluginQuickAccess的变化) useScrollLockWithWatch(showPluginQuickAccess) @@ -70,8 +86,10 @@ const canUsePullGesture = () => { const isAdmin = superUser.value // 检查插件快速访问面板是否已显示 const quickAccessOpen = showPluginQuickAccess.value + // 检查是否离线 + const offline = isOffline.value - return isDashboard && isAdmin && !quickAccessOpen + return isDashboard && isAdmin && !quickAccessOpen && !offline } // 使用下拉手势 composable @@ -138,14 +156,25 @@ onMounted(() => { // 监听全局未读消息事件 const unsubscribe = onUnreadMessage(handleUnreadMessage) + // 监听Service Worker消息 + if ('serviceWorker' in navigator) { + navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage) + } + // 组件卸载时清理监听 onBeforeUnmount(() => { unsubscribe() + if ('serviceWorker' in navigator) { + navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage) + } }) })