添加离线状态管理和网络请求处理

This commit is contained in:
jxxghp
2025-07-05 08:23:06 +08:00
parent 6bd7274c9c
commit 2650bc6068
11 changed files with 472 additions and 347 deletions

View File

@@ -1,307 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MoviePilot - 离线模式</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.offline-container {
background: white;
border-radius: 16px;
padding: 40px;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
max-width: 500px;
width: 100%;
}
.offline-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
background: #f8f9fa;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
color: #6c757d;
}
h1 {
color: #2d3748;
margin-bottom: 16px;
font-size: 24px;
font-weight: 600;
}
.message {
color: #718096;
margin-bottom: 32px;
line-height: 1.6;
font-size: 16px;
}
.retry-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 24px;
min-width: 120px;
}
.retry-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3);
}
.retry-button:active {
transform: translateY(0);
}
.tips {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-top: 24px;
text-align: left;
}
.tips h3 {
color: #2d3748;
margin-bottom: 12px;
font-size: 16px;
}
.tips ul {
list-style: none;
padding: 0;
}
.tips li {
color: #718096;
margin-bottom: 8px;
padding-left: 20px;
position: relative;
font-size: 14px;
}
.tips li::before {
content: "•";
color: #667eea;
position: absolute;
left: 0;
font-weight: bold;
}
.info-box {
background: #e8f4f8;
border-radius: 8px;
padding: 20px;
margin-top: 16px;
text-align: left;
border-left: 4px solid #667eea;
}
.info-box h3 {
color: #2d3748;
margin-bottom: 12px;
font-size: 16px;
}
.info-box p {
color: #4a5568;
margin-bottom: 12px;
font-size: 14px;
line-height: 1.5;
}
.feature-list {
list-style: none;
padding: 0;
margin-top: 8px;
}
.feature-list li {
color: #4a5568;
margin-bottom: 6px;
padding-left: 0;
font-size: 14px;
}
.logo {
width: 48px;
height: 48px;
margin: 0 auto 16px;
background: url('/icon.png') no-repeat center;
background-size: contain;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
background: #dc3545;
border-radius: 50%;
margin-right: 8px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.online .status-indicator {
background: #28a745;
animation: none;
}
@media (max-width: 480px) {
.offline-container {
padding: 24px;
}
h1 {
font-size: 20px;
}
.message {
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="offline-container">
<div class="logo"></div>
<div class="offline-icon">📡</div>
<h1>网络连接已断开</h1>
<p class="message">
<span class="status-indicator"></span>
MoviePilot 需要网络连接才能正常工作。请检查您的网络连接后重试。
</p>
<button class="retry-button" onclick="checkConnection()">
重新连接
</button>
<div class="tips">
<h3>🔍 可能的解决方案</h3>
<ul>
<li>检查WiFi或移动数据连接是否正常</li>
<li>尝试刷新页面或重启浏览器</li>
<li>检查路由器或网络设备是否工作正常</li>
<li>联系网络服务提供商确认网络状态</li>
</ul>
</div>
<div class="info-box">
<h3>📱 关于MoviePilot</h3>
<p>MoviePilot是一个智能媒体管理平台需要稳定的网络连接来获取最新的电影、剧集信息以及执行各种管理操作。</p>
<p>感谢您的耐心等待,网络恢复后您将能够:</p>
<ul class="feature-list">
<li>🎬 浏览最新的电影和剧集</li>
<li>📥 管理下载任务</li>
<li>🔍 搜索和发现新内容</li>
<li>⚙️ 配置系统设置</li>
<li>📊 查看系统状态</li>
</ul>
</div>
</div>
<script>
let isOnline = false;
function updateStatus() {
const container = document.querySelector('.offline-container');
const message = document.querySelector('.message');
const button = document.querySelector('.retry-button');
if (navigator.onLine) {
container.classList.add('online');
message.innerHTML = '<span class="status-indicator"></span>网络连接已恢复!正在为您重新加载...';
button.textContent = '返回应用';
button.onclick = () => window.location.reload();
// 延迟2秒后自动重新加载
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
container.classList.remove('online');
message.innerHTML = '<span class="status-indicator"></span>当前网络连接不可用,您正在使用离线模式。部分功能可能受限,请检查您的网络连接。';
button.textContent = '重新连接';
button.onclick = checkConnection;
}
}
function checkConnection() {
const button = document.querySelector('.retry-button');
button.textContent = '检查中...';
button.disabled = true;
// 尝试获取一个小的资源来检测网络
fetch('/favicon.ico?' + new Date().getTime(), {
method: 'HEAD',
cache: 'no-cache'
})
.then(() => {
updateStatus();
})
.catch(() => {
button.textContent = '重新连接';
button.disabled = false;
});
}
// 监听网络状态变化
window.addEventListener('online', updateStatus);
window.addEventListener('offline', updateStatus);
// 初始化状态
updateStatus();
// 每30秒检查一次网络状态
setInterval(() => {
if (!navigator.onLine) {
checkConnection();
}
}, 30000);
</script>
</body>
</html>

View File

@@ -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()

View 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,
}
}

View File

@@ -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'
// 下拉手势配置类型

View File

@@ -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"

View 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>

View File

@@ -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',

View File

@@ -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: '壁纸',

View File

@@ -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: '壁紙',

View File

@@ -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')

View File

@@ -124,6 +124,7 @@ export default defineConfig({
],
navigateFallback: null,
navigateFallbackDenylist: [/.*\/api\/v\d+\/system\/logging.*/, /\/offline\.html$/],
ignoreURLParametersMatching: [/^utm_/, /^fbclid$/, /^gclid$/],
skipWaiting: true,
clientsClaim: true,
},