From af604d0c5cb954cff3086627100f54b6ce5fa884 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 2 Jul 2026 11:56:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E7=A6=BB=E7=BA=BF=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=8E=A2=E6=B5=8B=E5=8A=9F=E8=83=BD=E5=92=8C=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E7=8A=B6=E6=80=81=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 152 +++++++++++-- src/api/index.ts | 44 ++-- src/components/dialog/OfflineStatusDialog.vue | 171 --------------- src/composables/useOfflineStatus.ts | 120 +++++------ .../default/components/DefaultLayout.vue | 23 +- .../default/components/OfflinePage.vue | 199 +++++++++++++----- src/locales/en-US.ts | 7 + src/locales/zh-CN.ts | 7 + src/locales/zh-TW.ts | 7 + 9 files changed, 385 insertions(+), 345 deletions(-) delete mode 100644 src/components/dialog/OfflineStatusDialog.vue diff --git a/src/App.vue b/src/App.vue index d162ae22..d711eaa5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,7 @@ - - - - diff --git a/src/composables/useOfflineStatus.ts b/src/composables/useOfflineStatus.ts index 3eaa152e..e6c0f696 100644 --- a/src/composables/useOfflineStatus.ts +++ b/src/composables/useOfflineStatus.ts @@ -1,86 +1,82 @@ -import { ref, computed } from 'vue' +import { computed, ref } from 'vue' import { useOnline } from '@vueuse/core' -// 全局状态 -const isAppOffline = ref(false) -const appOfflineReason = ref('') -const consecutiveNetworkErrors = ref(0) -const MAX_CONSECUTIVE_ERRORS = 3 +export type ConnectionStatus = 'online' | 'checking' | 'offline' +export type ConnectionFailureReason = 'browser-offline' | 'network-error' | 'timeout' | 'server-unreachable' -// 全局离线状态管理 +const browserOnline = useOnline() +const connectionStatus = ref('online') +const connectionReason = ref(null) +const connectionCheckRequestId = ref(0) +const serverSuccessSequence = ref(0) + +/** 管理 MoviePilot 服务的全局连接状态。 */ export function useGlobalOfflineStatus() { - const isOnline = useOnline() + const isOnline = computed(() => connectionStatus.value === 'online') + const isChecking = computed(() => connectionStatus.value === 'checking') + const isOffline = computed(() => connectionStatus.value === 'offline') + const canPerformNetworkAction = computed(() => connectionStatus.value !== 'offline') - // 综合离线状态(网络离线 或 应用离线) - 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 || '' - - // 如果设置为在线状态,重置连续错误计数 - if (!offline) { - consecutiveNetworkErrors.value = 0 - } + /** 记录任意 MoviePilot API 成功响应并恢复在线状态。 */ + function markServerOnline() { + connectionStatus.value = 'online' + connectionReason.value = null + serverSuccessSequence.value += 1 } - // 记录网络错误 - const recordNetworkError = (reason?: string) => { - consecutiveNetworkErrors.value++ - - // 只有连续出现三次网络错误时才设置为离线模式 - if (consecutiveNetworkErrors.value >= MAX_CONSECUTIVE_ERRORS) { - setAppOffline(true, reason || `连续${MAX_CONSECUTIVE_ERRORS}次网络错误`) - } + /** 将连接状态标记为待确认,但不直接阻断页面操作。 */ + function markConnectionChecking(reason?: ConnectionFailureReason) { + connectionStatus.value = 'checking' + if (reason) connectionReason.value = reason } - // 重置连续错误计数 - const resetConsecutiveErrors = () => { - consecutiveNetworkErrors.value = 0 + /** 在权威探测失败后标记 MoviePilot 服务不可达。 */ + function markServerOffline(reason: ConnectionFailureReason = 'server-unreachable') { + connectionStatus.value = 'offline' + connectionReason.value = reason } - // 获取离线消息 - const getOfflineMessage = () => { - if (!isOnline.value) { - return appOfflineReason.value - } - if (isAppOffline.value) { - return appOfflineReason.value - } - return '' + /** 将普通请求的网络错误降级为待确认状态,并请求一次去重后的服务探测。 */ + function reportNetworkError(reason: ConnectionFailureReason = 'network-error') { + markConnectionChecking(reason) + connectionCheckRequestId.value += 1 + } + + /** 请求立即检查 MoviePilot 服务连接。 */ + function requestConnectionCheck(reason?: ConnectionFailureReason) { + markConnectionChecking(reason) + connectionCheckRequestId.value += 1 } return { + browserOnline, + connectionStatus, + connectionReason, + connectionCheckRequestId, + serverSuccessSequence, isOnline, + isChecking, isOffline, canPerformNetworkAction, - setAppOffline, - recordNetworkError, - resetConsecutiveErrors, - getOfflineMessage, - consecutiveNetworkErrors: computed(() => consecutiveNetworkErrors.value), + markServerOnline, + markConnectionChecking, + markServerOffline, + reportNetworkError, + requestConnectionCheck, } } -// 单个组件的离线状态 -export function useOfflineStatus(initialMessage?: string) { - const { isOnline, isOffline, canPerformNetworkAction, getOfflineMessage } = useGlobalOfflineStatus() - - const message = computed(() => { - if (initialMessage) { - return initialMessage - } - return getOfflineMessage() - }) +/** 为单个组件提供 MoviePilot 服务连接状态。 */ +export function useOfflineStatus() { + const status = useGlobalOfflineStatus() return { - isOnline, - isOffline, - canPerformNetworkAction, - message, + browserOnline: status.browserOnline, + isOnline: status.isOnline, + isChecking: status.isChecking, + isOffline: status.isOffline, + canPerformNetworkAction: status.canPerformNetworkAction, + connectionReason: status.connectionReason, + requestConnectionCheck: status.requestConnectionCheck, } } diff --git a/src/layouts/default/components/DefaultLayout.vue b/src/layouts/default/components/DefaultLayout.vue index 353c445b..ae265f20 100644 --- a/src/layouts/default/components/DefaultLayout.vue +++ b/src/layouts/default/components/DefaultLayout.vue @@ -107,7 +107,7 @@ const mainContentPaddingTop = computed(() => { const showPluginQuickAccess = ref(false) // 离线状态管理 -const { setAppOffline, isOffline } = useGlobalOfflineStatus() +const { isOffline } = useGlobalOfflineStatus() // 动态标签页相关 // 定义动态标签页类型 @@ -227,17 +227,6 @@ onUnmounted(() => { } }) -/** 处理 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) - } - } -} - /** 判断当前页面状态是否允许使用主界面下拉快捷入口手势。 */ const canUsePullGesture = () => { // 检查是否在dashboard页面 @@ -468,25 +457,17 @@ onMounted(async () => { await pluginSidebarNavStore.ensureSidebarNav() appendPluginSidebarMenus() - // 监听Service Worker消息 - if ('serviceWorker' in navigator) { - navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage) - } - // 组件卸载时清理监听 onBeforeUnmount(() => { window.removeEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange) window.removeEventListener(THEME_CUSTOMIZER_OPEN_EVENT, handleThemeCustomizerOpen) - if ('serviceWorker' in navigator) { - navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage) - } }) })