mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-12 03:01:54 +08:00
添加离线状态管理和网络请求处理
This commit is contained in:
@@ -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()
|
||||
|
||||
61
src/composables/useOfflineStatus.ts
Normal file
61
src/composables/useOfflineStatus.ts
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
// 下拉手势配置类型
|
||||
|
||||
@@ -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<NavMenu[]>([])
|
||||
// 插件快速访问相关状态
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 👉 Offline Page -->
|
||||
<OfflinePage />
|
||||
|
||||
<!-- 👉 Pull Down Indicator -->
|
||||
<div
|
||||
v-if="appMode && showPullIndicator"
|
||||
|
||||
266
src/layouts/components/OfflinePage.vue
Normal file
266
src/layouts/components/OfflinePage.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script setup lang="ts">
|
||||
import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
|
||||
|
||||
interface Props {
|
||||
type?: 'offline' | 'online'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'offline',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const { isOnline, canPerformNetworkAction, getOfflineMessage } = useGlobalOfflineStatus()
|
||||
|
||||
// 重试连接
|
||||
const retrying = ref(false)
|
||||
const handleRetry = async () => {
|
||||
if (retrying.value) return
|
||||
|
||||
retrying.value = true
|
||||
|
||||
try {
|
||||
// 尝试发送一个简单的请求来检测网络
|
||||
await fetch('/favicon.ico?' + new Date().getTime(), {
|
||||
method: 'HEAD',
|
||||
cache: 'no-cache',
|
||||
})
|
||||
|
||||
// 如果成功,等待一下让状态更新
|
||||
setTimeout(() => {
|
||||
retrying.value = false
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
retrying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 当网络恢复时自动隐藏页面
|
||||
const shouldShow = computed(() => {
|
||||
return !canPerformNetworkAction.value
|
||||
})
|
||||
|
||||
// 状态文本
|
||||
const statusText = computed(() => {
|
||||
if (props.type === 'online') {
|
||||
return t('app.onlineMessage')
|
||||
}
|
||||
return getOfflineMessage()
|
||||
})
|
||||
|
||||
// 图标
|
||||
const statusIcon = computed(() => {
|
||||
return props.type === 'online' ? 'mdi-wifi' : 'mdi-wifi-off'
|
||||
})
|
||||
|
||||
// 颜色主题
|
||||
const colorTheme = computed(() => {
|
||||
return props.type === 'online' ? 'success' : 'error'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-500"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition-all duration-300"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div v-if="shouldShow" class="offline-page">
|
||||
<div class="offline-container">
|
||||
<!-- 状态图标 -->
|
||||
<div class="status-icon-wrapper">
|
||||
<div class="status-icon-bg">
|
||||
<VIcon :icon="statusIcon" size="64" :color="colorTheme" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要信息 -->
|
||||
<div class="content-section">
|
||||
<h1 class="offline-title">
|
||||
{{ props.type === 'online' ? t('app.online') : t('app.offline') }}
|
||||
</h1>
|
||||
|
||||
<p class="offline-message">
|
||||
{{ statusText }}
|
||||
</p>
|
||||
|
||||
<!-- 重试按钮 -->
|
||||
<div class="action-section">
|
||||
<VBtn
|
||||
v-if="props.type === 'offline'"
|
||||
:loading="retrying"
|
||||
:color="colorTheme"
|
||||
size="large"
|
||||
variant="flat"
|
||||
@click="handleRetry"
|
||||
>
|
||||
<VIcon icon="mdi-refresh" class="me-2" />
|
||||
{{ retrying ? t('common.checking') : t('common.retry') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- 状态指示器 -->
|
||||
<div class="status-indicators">
|
||||
<VChip
|
||||
:color="isOnline ? 'success' : 'error'"
|
||||
:prepend-icon="isOnline ? 'mdi-wifi' : 'mdi-wifi-off'"
|
||||
variant="tonal"
|
||||
class="me-2"
|
||||
>
|
||||
{{ isOnline ? t('common.networkOnline') : t('common.networkOffline') }}
|
||||
</VChip>
|
||||
|
||||
<VChip
|
||||
:color="canPerformNetworkAction ? 'success' : 'warning'"
|
||||
:prepend-icon="canPerformNetworkAction ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ canPerformNetworkAction ? t('common.serviceAvailable') : t('common.serviceUnavailable') }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="footer-section">
|
||||
<p class="app-info">{{ t('app.moviepilot') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.offline-page {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
background: linear-gradient(135deg, rgb(var(--v-theme-surface)) 0%, rgb(var(--v-theme-surface-variant)) 100%);
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.offline-container {
|
||||
padding: 40px;
|
||||
border-radius: 24px;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 10%), 0 0 0 1px rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
inline-size: 100%;
|
||||
max-inline-size: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-icon-wrapper {
|
||||
margin-block-end: 32px;
|
||||
}
|
||||
|
||||
.status-icon-bg {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--v-theme-surface-variant), 0.5);
|
||||
block-size: 120px;
|
||||
inline-size: 120px;
|
||||
margin-block: 0;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.status-icon-bg::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, rgb(var(--v-theme-primary)), rgb(var(--v-theme-secondary)));
|
||||
content: '';
|
||||
inset: -4px;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-block-end: 32px;
|
||||
}
|
||||
|
||||
.offline-title {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-block-end: 16px;
|
||||
}
|
||||
|
||||
.offline-message {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin-block-end: 32px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
margin-block-end: 32px;
|
||||
}
|
||||
|
||||
.status-indicators {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
margin-block-end: 32px;
|
||||
}
|
||||
|
||||
.help-panels {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.app-info {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (width <= 600px) {
|
||||
.offline-container {
|
||||
padding: 24px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.offline-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.offline-message {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.status-icon-bg {
|
||||
block-size: 100px;
|
||||
inline-size: 100px;
|
||||
}
|
||||
|
||||
.status-indicators {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗黑模式优化 */
|
||||
.v-theme--dark .offline-page {
|
||||
background: linear-gradient(135deg, rgb(var(--v-theme-surface)) 0%, rgba(var(--v-theme-surface-variant), 0.8) 100%);
|
||||
}
|
||||
|
||||
.v-theme--dark .offline-container {
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 30%), 0 0 0 1px rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
</style>
|
||||
@@ -49,6 +49,17 @@ export default {
|
||||
pageText: '{0}-{1} of {2}',
|
||||
noDataText: 'No data',
|
||||
loadingText: 'Loading...',
|
||||
networkRequired: 'This feature requires network connection',
|
||||
networkDisconnected: 'Network connection lost',
|
||||
featuresLimited: 'Some features may be limited',
|
||||
serverConnectionFailed: 'Server connection failed',
|
||||
troubleshooting: 'Troubleshooting',
|
||||
checking: 'Checking',
|
||||
retry: 'Retry',
|
||||
networkOnline: 'Network Online',
|
||||
networkOffline: 'Network Offline',
|
||||
serviceAvailable: 'Service Available',
|
||||
serviceUnavailable: 'Service Unavailable',
|
||||
},
|
||||
mediaType: {
|
||||
movie: 'Movie',
|
||||
@@ -120,6 +131,7 @@ export default {
|
||||
},
|
||||
app: {
|
||||
moviepilot: 'MoviePilot',
|
||||
slogan: 'Intelligent Movie & TV Media Library Management Tool',
|
||||
recommend: 'Recommend',
|
||||
subscribeMovie: 'Movie Subscription',
|
||||
subscribeTv: 'TV Subscription',
|
||||
@@ -131,6 +143,10 @@ export default {
|
||||
restartTip: 'After restart, you will be logged out and need to log in again.',
|
||||
restartTimeout: 'Restart timeout, the system may need more time to recover, please refresh the page manually later',
|
||||
restartFailed: 'Restart failed, please check system status',
|
||||
offline: 'Offline Mode',
|
||||
offlineMessage: 'Network connection lost, some features may be limited',
|
||||
online: 'Online Mode',
|
||||
onlineMessage: 'Network connection restored',
|
||||
},
|
||||
login: {
|
||||
wallpapers: 'Wallpapers',
|
||||
|
||||
@@ -49,6 +49,17 @@ export default {
|
||||
pageText: '{0}-{1} 共 {2} 条',
|
||||
noDataText: '没有数据',
|
||||
loadingText: '加载中...',
|
||||
networkRequired: '此功能需要网络连接',
|
||||
networkDisconnected: '网络连接已断开',
|
||||
featuresLimited: '部分功能可能受限',
|
||||
serverConnectionFailed: '服务器连接失败',
|
||||
troubleshooting: '疑难解答',
|
||||
checking: '检查中',
|
||||
retry: '重试',
|
||||
networkOnline: '网络在线',
|
||||
networkOffline: '网络离线',
|
||||
serviceAvailable: '服务可用',
|
||||
serviceUnavailable: '服务不可用',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '电影',
|
||||
@@ -120,6 +131,7 @@ export default {
|
||||
},
|
||||
app: {
|
||||
moviepilot: 'MoviePilot',
|
||||
slogan: '智能影视媒体库管理工具',
|
||||
recommend: '推荐',
|
||||
subscribeMovie: '电影订阅',
|
||||
subscribeTv: '电视剧订阅',
|
||||
@@ -131,6 +143,10 @@ export default {
|
||||
restartTip: '重启后,您将被注销并需要重新登录。',
|
||||
restartTimeout: '重启超时,系统可能需要更长时间恢复,请稍后手动刷新页面',
|
||||
restartFailed: '重启失败,请检查系统状态',
|
||||
offline: '离线模式',
|
||||
offlineMessage: '网络连接已断开,部分功能可能受限',
|
||||
online: '在线模式',
|
||||
onlineMessage: '网络连接已恢复',
|
||||
},
|
||||
login: {
|
||||
wallpapers: '壁纸',
|
||||
|
||||
@@ -49,6 +49,17 @@ export default {
|
||||
pageText: '{0}-{1} 共 {2} 條',
|
||||
noDataText: '沒有數據',
|
||||
loadingText: '加載中...',
|
||||
networkRequired: '此功能需要網絡連接',
|
||||
networkDisconnected: '網絡連接已斷開',
|
||||
featuresLimited: '部分功能可能受限',
|
||||
serverConnectionFailed: '服務器連接失敗',
|
||||
troubleshooting: '疑難排解',
|
||||
checking: '檢查中',
|
||||
retry: '重試',
|
||||
networkOnline: '網絡在線',
|
||||
networkOffline: '網絡離線',
|
||||
serviceAvailable: '服務可用',
|
||||
serviceUnavailable: '服務不可用',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '電影',
|
||||
@@ -120,6 +131,7 @@ export default {
|
||||
},
|
||||
app: {
|
||||
moviepilot: 'MoviePilot',
|
||||
slogan: '智能影視媒體庫管理工具',
|
||||
recommend: '推薦',
|
||||
subscribeMovie: '電影訂閱',
|
||||
subscribeTv: '電視劇訂閱',
|
||||
@@ -132,6 +144,10 @@ export default {
|
||||
restartTip: '重啟後,您將被註銷並需要重新登錄。',
|
||||
restartTimeout: '重啟超時,系統可能需要更長時間恢復,請稍後手動刷新頁面',
|
||||
restartFailed: '重啟失敗,請檢查系統狀態',
|
||||
offline: '離線模式',
|
||||
offlineMessage: '網絡連接已斷開,部分功能可能受限',
|
||||
online: '在線模式',
|
||||
onlineMessage: '網絡連接已恢復',
|
||||
},
|
||||
login: {
|
||||
wallpapers: '壁紙',
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
||||
|
||||
// Service Worker 类型声明
|
||||
declare let self: ServiceWorkerGlobalScope
|
||||
|
||||
cleanupOutdatedCaches()
|
||||
|
||||
// self.__WB_MANIFEST is default injection point
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
|
||||
// 离线版本控制 - 递增此版本号将触发install事件并强制更新缓存资源
|
||||
const OFFLINE_VERSION = 1
|
||||
const CACHE_NAME = 'mp-offline-cache-v1'
|
||||
const OFFLINE_URL = '/offline.html'
|
||||
|
||||
// 通知选项
|
||||
const options = {
|
||||
icon: '/logo.png',
|
||||
@@ -57,6 +48,7 @@ async function openDB(): Promise<IDBDatabase> {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取IndexedDB中的数据
|
||||
async function get(key: string): Promise<any> {
|
||||
const db = await openDB()
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -68,6 +60,7 @@ async function get(key: string): Promise<any> {
|
||||
})
|
||||
}
|
||||
|
||||
// 保存数据到IndexedDB
|
||||
async function set(key: string, value: any): Promise<void> {
|
||||
const db = await openDB()
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -106,17 +99,9 @@ async function clearBadge() {
|
||||
}
|
||||
}
|
||||
|
||||
// 安装事件 - 缓存离线页面
|
||||
// 安装事件
|
||||
self.addEventListener('install', event => {
|
||||
console.log('Service Worker install, version:', OFFLINE_VERSION)
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const cache = await caches.open(CACHE_NAME)
|
||||
// 使用 {cache: 'reload'} 确保从网络获取最新的离线页面
|
||||
// 而不是从HTTP缓存中获取
|
||||
await cache.add(new Request(OFFLINE_URL, { cache: 'reload' }))
|
||||
})(),
|
||||
)
|
||||
console.log('Service Worker install')
|
||||
// 强制等待中的Service Worker立即成为活动的Service Worker
|
||||
self.skipWaiting()
|
||||
})
|
||||
@@ -136,36 +121,48 @@ self.addEventListener('activate', event => {
|
||||
self.clients.claim()
|
||||
})
|
||||
|
||||
// Fetch事件 - 处理网络请求和离线回退
|
||||
// 处理API请求,当离线时发送消息到客户端
|
||||
self.addEventListener('fetch', event => {
|
||||
// 只处理HTML页面的导航请求
|
||||
if (event.request.mode === 'navigate') {
|
||||
if (event.request.url.includes('/api/v1/') && event.request.method === 'GET') {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
try {
|
||||
// 首先尝试使用navigationPreload响应(如果支持)
|
||||
const preloadResponse = await event.preloadResponse
|
||||
if (preloadResponse) {
|
||||
return preloadResponse
|
||||
}
|
||||
|
||||
// 总是优先尝试网络请求
|
||||
// 尝试网络请求
|
||||
const networkResponse = await fetch(event.request)
|
||||
return networkResponse
|
||||
} catch (error) {
|
||||
// 只有在抛出异常时才会触发catch,通常是由于网络错误
|
||||
// 如果fetch()返回4xx或5xx响应码,不会触发catch
|
||||
console.log('网络请求失败,返回离线页面:', error)
|
||||
// 网络错误时,通知客户端当前处于离线状态
|
||||
if (self.clients) {
|
||||
self.clients.matchAll().then(clients => {
|
||||
clients.forEach(client => {
|
||||
client.postMessage({
|
||||
type: 'OFFLINE_STATUS',
|
||||
offline: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const cache = await caches.open(CACHE_NAME)
|
||||
const cachedResponse = await cache.match(OFFLINE_URL)
|
||||
return cachedResponse || new Response('离线页面不可用', { status: 503 })
|
||||
// 尝试返回缓存的响应
|
||||
const cache = await caches.open('api-cache')
|
||||
const cachedResponse = await cache.match(event.request)
|
||||
if (cachedResponse) {
|
||||
return cachedResponse
|
||||
}
|
||||
|
||||
// 如果没有缓存,抛出错误
|
||||
throw error
|
||||
}
|
||||
})(),
|
||||
)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化 Workbox
|
||||
cleanupOutdatedCaches()
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
|
||||
// 监听 push 事件,显示通知
|
||||
self.addEventListener('push', function (event) {
|
||||
console.log('notification push')
|
||||
|
||||
Reference in New Issue
Block a user