From 6d2916dc9f1f1f55a8391d15ead3d6e8c6f1f9e9 Mon Sep 17 00:00:00 2001
From: PKC278 <52959804+PKC278@users.noreply.github.com>
Date: Fri, 2 Jan 2026 20:36:33 +0800
Subject: [PATCH 1/9] =?UTF-8?q?feat(pwa):=20=E9=87=8D=E6=9E=84=20Service?=
=?UTF-8?q?=20Worker=20=E5=8F=8A=E7=89=88=E6=9C=AC=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E6=9C=BA=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/App.vue | 7 +
src/components/dialog/AboutDialog.vue | 8 +-
src/components/toast/VersionUpdateToast.vue | 7 +-
src/composables/useVersionChecker.ts | 87 ++-
src/locales/en-US.ts | 1 +
src/locales/zh-CN.ts | 1 +
src/locales/zh-TW.ts | 1 +
src/service-worker.ts | 755 +++++++++-----------
src/types/global.d.ts | 3 +
vite.config.ts | 106 +--
10 files changed, 432 insertions(+), 544 deletions(-)
diff --git a/src/App.vue b/src/App.vue
index 588e571c..bb42aec2 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -237,6 +237,13 @@ async function loadBackgroundImages(retryCount = 0) {
}
onMounted(async () => {
+ // 移除URL中的时间戳参数
+ const url = new URL(window.location.href)
+ if (url.searchParams.has('t')) {
+ url.searchParams.delete('t')
+ window.history.replaceState({}, '', url.toString())
+ }
+
// 配置 ApexCharts
configureApexCharts()
diff --git a/src/components/dialog/AboutDialog.vue b/src/components/dialog/AboutDialog.vue
index 853ea2a7..64745d36 100644
--- a/src/components/dialog/AboutDialog.vue
+++ b/src/components/dialog/AboutDialog.vue
@@ -1,12 +1,10 @@
diff --git a/src/composables/useVersionChecker.ts b/src/composables/useVersionChecker.ts
index 2f2d1756..edc3d486 100644
--- a/src/composables/useVersionChecker.ts
+++ b/src/composables/useVersionChecker.ts
@@ -3,17 +3,25 @@ import { useToast } from 'vue-toastification'
import i18n from '@/plugins/i18n'
import VersionUpdateToast from '@/components/toast/VersionUpdateToast.vue'
-// 声明全局变量类型
-declare const __APP_VERSION__: string
-
// 全局状态
const currentVersion = ref(__APP_VERSION__)
+let isListenerAdded = false
+let notificationShowTime = 0
const serverVersion = ref(null)
const versionChecked = ref(false)
const needsUpdate = computed(() => {
return serverVersion.value !== null && serverVersion.value !== currentVersion.value
})
+/**
+ * 刷新页面并添加时间戳
+ */
+export const reloadWithTimestamp = (): void => {
+ const url = new URL(window.location.href)
+ url.searchParams.set('t', Date.now().toString())
+ window.location.href = url.toString()
+}
+
/**
* 清除所有缓存和 Service Worker
*/
@@ -56,6 +64,7 @@ export function useVersionChecker() {
const component = h(VersionUpdateToast, {
message: i18n.global.t('common.newVersionAvailable'),
refreshText: i18n.global.t('common.refresh'),
+ onRefresh: reloadWithTimestamp,
})
toast.info(component, {
@@ -79,16 +88,46 @@ export function useVersionChecker() {
// 更新服务端版本
serverVersion.value = latestVersion
- // 版本不同,且尚未显示通知
- if (needsUpdate.value) {
- versionChecked.value = true
- console.log(`[VersionChecker] 检测到版本更新: ${currentVersion.value} -> ${latestVersion}`)
+ // 执行版本不一致时的处理逻辑
+ const handleVersionMismatch = async () => {
+ if (needsUpdate.value) {
+ versionChecked.value = true
+ console.log(`[VersionChecker] 检测到版本更新: ${currentVersion.value} -> ${latestVersion}`)
- // 清除缓存和 Service Worker
- await clearCachesAndServiceWorker()
+ // 清除缓存和 Service Worker
+ await clearCachesAndServiceWorker()
- // 显示持久化通知
- showUpdateNotification()
+ // 显示持久化通知
+ showUpdateNotification()
+ }
+ }
+
+ // 优先尝试通过 Service Worker 检查更新
+ if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
+ console.log('[VersionChecker] Requesting Service Worker update check...')
+
+ const registration = await navigator.serviceWorker.getRegistration()
+
+ // 如果已经有等待中的更新,直接处理
+ if (registration?.waiting) {
+ console.log('[VersionChecker] New worker waiting, skipping manual check.')
+ handleVersionMismatch()
+ return
+ }
+
+ const messageChannel = new MessageChannel()
+
+ messageChannel.port1.onmessage = event => {
+ if (event.data && event.data.type === 'SW_NO_UPDATE_DETECTED') {
+ console.log('[VersionChecker] Service Worker reported no update, checking version manually...')
+ handleVersionMismatch()
+ }
+ }
+
+ navigator.serviceWorker.controller.postMessage({ type: 'CHECK_SW_UPDATE' }, [messageChannel.port2])
+ } else {
+ // 如果没有 Service Worker 控制,直接进行版本比较
+ await handleVersionMismatch()
}
}
@@ -100,6 +139,32 @@ export function useVersionChecker() {
serverVersion.value = null
}
+ // 监听 Service Worker 版本更新消息
+ if (!isListenerAdded && 'serviceWorker' in navigator) {
+ navigator.serviceWorker.addEventListener('message', event => {
+ // 1. 发现新版本 -> 弹出通知
+ if (event.data && event.data.type === 'SW_VERSION_DETECTED') {
+ console.log('[VersionChecker] Detected new version:', event.data.version)
+ notificationShowTime = Date.now()
+ toast.info(i18n.global.t('common.newVersionFound'), {
+ timeout: false,
+ hideProgressBar: true,
+ closeButton: false,
+ })
+ }
+ // 2. 安装完成 -> 刷新页面
+ else if (event.data && event.data.type === 'SW_RELOAD_PAGE') {
+ const elapsed = Date.now() - notificationShowTime
+ const delay = Math.max(0, 1000 - elapsed)
+ console.log(`[VersionChecker] Update installed, reloading in ${delay}ms...`)
+ setTimeout(() => {
+ reloadWithTimestamp()
+ }, delay)
+ }
+ })
+ isListenerAdded = true
+ }
+
return {
// 状态
currentVersion: computed(() => currentVersion.value),
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index 70bcf524..0700b691 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -69,6 +69,7 @@ export default {
preset: 'Preset',
refresh: 'Refresh',
newVersionAvailable: 'New version detected, please refresh the page to get the latest features',
+ newVersionFound: 'New version found, updating...',
},
mediaType: {
movie: 'Movie',
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 21bd8ad2..f95c75d2 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -69,6 +69,7 @@ export default {
preset: '预设',
refresh: '刷新',
newVersionAvailable: '检测到新版本,请刷新页面以获取最新功能',
+ newVersionFound: '发现新版本,正在更新...',
},
mediaType: {
movie: '电影',
diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts
index 9991a2ae..1b1b30c1 100644
--- a/src/locales/zh-TW.ts
+++ b/src/locales/zh-TW.ts
@@ -69,6 +69,7 @@ export default {
preset: '預設',
refresh: '刷新',
newVersionAvailable: '檢測到新版本,請刷新頁面以獲取最新功能',
+ newVersionFound: '發現新版本,正在更新...',
},
mediaType: {
movie: '電影',
diff --git a/src/service-worker.ts b/src/service-worker.ts
index 0c97bbd0..7217a4ce 100644
--- a/src/service-worker.ts
+++ b/src/service-worker.ts
@@ -1,4 +1,9 @@
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
+import { registerRoute, setCatchHandler } from 'workbox-routing'
+import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
+import { ExpirationPlugin } from 'workbox-expiration'
+import { CacheableResponsePlugin } from 'workbox-cacheable-response'
+import * as navigationPreload from 'workbox-navigation-preload'
// Service Worker 类型声明
declare let self: ServiceWorkerGlobalScope & {
@@ -6,27 +11,62 @@ declare let self: ServiceWorkerGlobalScope & {
}
// 缓存版本控制
-const CACHE_VERSION = 'v13'
-const CACHE_NAMES = {
- appShell: `app-shell-${CACHE_VERSION}`,
- static: `static-resources-${CACHE_VERSION}`,
- images: `image-cache-${CACHE_VERSION}`,
- fonts: `font-cache-${CACHE_VERSION}`,
- api: `api-cache-${CACHE_VERSION}`,
- tmdb: `tmdb-image-cache-${CACHE_VERSION}`,
- pages: `pages-cache-${CACHE_VERSION}`,
-}
+const CACHE_VERSION = `${__APP_VERSION__}-${__BUILD_TIME__}`
-// 缓存大小限制
-const CACHE_SIZE_LIMITS = {
- appShell: { maxEntries: 10, maxAgeSeconds: 7 * 24 * 60 * 60 }, // 7天
- static: { maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 }, // 30天
- images: { maxEntries: 200, maxAgeSeconds: 30 * 24 * 60 * 60 }, // 30天
- fonts: { maxEntries: 50, maxAgeSeconds: 365 * 24 * 60 * 60 }, // 1年
- api: { maxEntries: 500, maxAgeSeconds: 24 * 60 * 60 }, // 24小时
- tmdb: { maxEntries: 300, maxAgeSeconds: 7 * 24 * 60 * 60 }, // 7天
- pages: { maxEntries: 50, maxAgeSeconds: 7 * 24 * 60 * 60 }, // 7天
-}
+// 启用导航预载
+navigationPreload.enable()
+
+// 自动清理旧的预缓存
+cleanupOutdatedCaches()
+
+// 预缓存并路由
+precacheAndRoute(self.__WB_MANIFEST)
+
+// 变量记录是否为更新安装
+let isUpdate = false
+
+// 监听安装事件以检测更新
+self.addEventListener('install', () => {
+ // 强制等待中的 Service Worker 立即激活
+ self.skipWaiting()
+
+ // 检查是否是更新(即是否已经有激活的 Service Worker)
+ if (self.registration.active) {
+ isUpdate = true
+ // 通知客户端发现新版本
+ self.clients.matchAll({ includeUncontrolled: true, type: 'window' }).then(clients => {
+ clients.forEach(client => {
+ client.postMessage({
+ type: 'SW_VERSION_DETECTED',
+ version: CACHE_VERSION,
+ })
+ })
+ })
+ }
+})
+
+// 监听激活事件
+self.addEventListener('activate', event => {
+ // 让 Service Worker 立即接管页面
+ event.waitUntil(
+ (async () => {
+ await self.clients.claim()
+
+ // 清理旧版本的运行时缓存
+ await cleanupRuntimeCaches(true)
+
+ // 如果是更新,则通知客户端刷新页面
+ if (isUpdate) {
+ const clients = await self.clients.matchAll({ type: 'window' })
+ clients.forEach(client => {
+ client.postMessage({
+ type: 'SW_RELOAD_PAGE',
+ })
+ })
+ }
+ })(),
+ )
+})
// 通知选项
const options = {
@@ -38,100 +78,216 @@ const options = {
// 存储未读消息数量的键名
const UNREAD_COUNT_KEY = 'mp_unread_count'
-// 从IndexedDB获取未读消息数量
-async function getStoredUnreadCount(): Promise {
- try {
- const count = await get(UNREAD_COUNT_KEY)
- return count || 0
- } catch (error) {
- console.error('Failed to get stored unread count:', error)
- return 0
+// --- 缓存策略配置 ---
+
+// 导航请求与 App Shell - 优先网络
+registerRoute(
+ ({ request, url }) => request.mode === 'navigate' || url.pathname === '/' || url.pathname === '/index.html',
+ new NetworkFirst({
+ cacheName: `app-shell-${CACHE_VERSION}`,
+ plugins: [
+ new ExpirationPlugin({
+ maxEntries: 10,
+ maxAgeSeconds: 7 * 24 * 60 * 60, // 7天
+ }),
+ ],
+ }),
+)
+
+// 静态资源 (JS, CSS, HTML) - 优先缓存
+registerRoute(
+ ({ request }) => ['style', 'script', 'worker'].includes(request.destination),
+ new StaleWhileRevalidate({
+ cacheName: `static-resources-${CACHE_VERSION}`,
+ plugins: [
+ new CacheableResponsePlugin({
+ statuses: [0, 200],
+ }),
+ ],
+ }),
+)
+
+// 图片资源 - 优先缓存
+registerRoute(
+ ({ request }) => request.destination === 'image',
+ new CacheFirst({
+ cacheName: `image-cache-${CACHE_VERSION}`,
+ plugins: [
+ new CacheableResponsePlugin({
+ statuses: [0, 200],
+ }),
+ new ExpirationPlugin({
+ maxEntries: 200,
+ maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
+ }),
+ ],
+ }),
+)
+
+// 字体资源 - 优先缓存
+registerRoute(
+ ({ request }) => request.destination === 'font',
+ new CacheFirst({
+ cacheName: `font-cache-${CACHE_VERSION}`,
+ plugins: [
+ new CacheableResponsePlugin({
+ statuses: [0, 200],
+ }),
+ new ExpirationPlugin({
+ maxEntries: 50,
+ maxAgeSeconds: 365 * 24 * 60 * 60, // 1年
+ }),
+ ],
+ }),
+)
+
+// TMDB 图片 - 优先缓存
+registerRoute(
+ ({ url }) => url.hostname === 'image.tmdb.org',
+ new CacheFirst({
+ cacheName: `tmdb-image-cache-${CACHE_VERSION}`,
+ plugins: [
+ new CacheableResponsePlugin({
+ statuses: [0, 200],
+ }),
+ new ExpirationPlugin({
+ maxEntries: 300,
+ maxAgeSeconds: 7 * 24 * 60 * 60, // 7天
+ }),
+ ],
+ }),
+)
+
+// API GET 请求 - 优先网络
+registerRoute(
+ ({ url, request }) =>
+ url.pathname.includes('/api/v1/') &&
+ request.method === 'GET' &&
+ !url.pathname.includes('/api/v1/system/message') && // 排除 SSE 长连接
+ !url.pathname.includes('/api/v1/common/message') && // 排除通用消息
+ !url.pathname.includes('/api/v1/message/'), // 排除所有消息类接口
+ new NetworkFirst({
+ cacheName: `api-cache-${CACHE_VERSION}`,
+ networkTimeoutSeconds: 5,
+ plugins: [
+ new CacheableResponsePlugin({
+ statuses: [0, 200],
+ }),
+ new ExpirationPlugin({
+ maxEntries: 500,
+ maxAgeSeconds: 24 * 60 * 60, // 24小时
+ }),
+ ],
+ }),
+)
+
+// 设置默认离线页面
+setCatchHandler(async ({ request }) => {
+ if (request?.destination === 'document') {
+ return (await caches.match('/offline.html')) || Response.error()
}
+ return Response.error()
+})
+
+// --- 辅助函数 (通知与徽章) ---
+
+// 清理运行时缓存
+async function cleanupRuntimeCaches(onlyOld: boolean = false) {
+ const cacheNames = await caches.keys()
+ const runtimeCachePrefixes = [
+ 'app-shell',
+ 'static-resources',
+ 'image-cache',
+ 'font-cache',
+ 'api-cache',
+ 'tmdb-image-cache',
+ 'pages-cache',
+ ]
+
+ await Promise.all(
+ cacheNames.map(cacheName => {
+ const isRuntimeCache = runtimeCachePrefixes.some(prefix => cacheName.startsWith(prefix))
+ if (isRuntimeCache) {
+ if (!onlyOld || !cacheName.includes(CACHE_VERSION)) {
+ console.log('[SW] Deleting runtime cache:', cacheName)
+ return caches.delete(cacheName)
+ }
+ }
+ return Promise.resolve()
+ }),
+ )
}
-// 保存未读消息数量到IndexedDB
-async function setStoredUnreadCount(count: number): Promise {
- try {
- await set(UNREAD_COUNT_KEY, count)
- } catch (error) {
- console.error('Failed to set stored unread count:', error)
- }
-}
-
-// 简单的IndexedDB包装器
+// 简单的 IndexedDB 包装器 (用于未读计数)
async function openDB(): Promise {
- // Bump the version to add the new "sync" store while keeping existing data intact
return new Promise((resolve, reject) => {
const request = indexedDB.open('mp_badge_db', 2)
-
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
-
request.onupgradeneeded = event => {
const db = (event.target as IDBOpenDBRequest).result
-
- // Badge store (existing)
if (!db.objectStoreNames.contains('badge')) {
db.createObjectStore('badge')
}
-
- // Dedicated store for offline-sync items
- if (!db.objectStoreNames.contains('sync')) {
- db.createObjectStore('sync')
- }
}
})
}
-// 获取IndexedDB中的数据
async function get(key: string, storeName: string = 'badge'): Promise {
- const db = await openDB()
- return new Promise((resolve, reject) => {
- const tx = db.transaction([storeName], 'readonly')
- const store = tx.objectStore(storeName)
- const request = store.get(key)
-
- request.onerror = () => reject(request.error)
- request.onsuccess = () => resolve(request.result)
- })
+ try {
+ const db = await openDB()
+ return new Promise((resolve, reject) => {
+ if (!db.objectStoreNames.contains(storeName)) {
+ resolve(null)
+ return
+ }
+ const tx = db.transaction([storeName], 'readonly')
+ const store = tx.objectStore(storeName)
+ const request = store.get(key)
+ request.onerror = () => reject(request.error)
+ request.onsuccess = () => resolve(request.result)
+ })
+ } catch (e) {
+ return null
+ }
}
-// 保存数据到IndexedDB
async function set(key: string, value: any, storeName: string = 'badge'): Promise {
- const db = await openDB()
- return new Promise((resolve, reject) => {
- const tx = db.transaction([storeName], 'readwrite')
- const store = tx.objectStore(storeName)
-
- store.put(value, key)
-
- tx.oncomplete = () => resolve()
- tx.onerror = () => reject(tx.error)
- })
+ try {
+ const db = await openDB()
+ return new Promise((resolve, reject) => {
+ if (!db.objectStoreNames.contains(storeName)) {
+ console.warn(`Store ${storeName} not found`)
+ resolve()
+ return
+ }
+ const tx = db.transaction([storeName], 'readwrite')
+ const store = tx.objectStore(storeName)
+ store.put(value, key)
+ tx.oncomplete = () => resolve()
+ tx.onerror = () => reject(tx.error)
+ })
+ } catch (e) {
+ // 忽略错误
+ }
}
-// 删除IndexedDB中的数据(确保事务完成)
-async function del(key: string, storeName: string = 'badge'): Promise {
- const db = await openDB()
- return new Promise((resolve, reject) => {
- const tx = db.transaction([storeName], 'readwrite')
- const store = tx.objectStore(storeName)
-
- store.delete(key)
-
- tx.oncomplete = () => resolve()
- tx.onerror = () => reject(tx.error)
- })
+async function getStoredUnreadCount(): Promise {
+ const count = await get(UNREAD_COUNT_KEY)
+ return typeof count === 'number' ? count : 0
+}
+
+async function setStoredUnreadCount(count: number): Promise {
+ await set(UNREAD_COUNT_KEY, count)
}
-// 更新桌面图标徽章
async function updateBadge(count: number) {
- if ('setAppBadge' in navigator) {
+ if ('setAppBadge' in self.navigator) {
try {
if (count > 0) {
- await navigator.setAppBadge!(count)
+ await (self.navigator as any).setAppBadge(count)
} else {
- await navigator.clearAppBadge!()
+ await (self.navigator as any).clearAppBadge()
}
} catch (error) {
console.error('Failed to update app badge:', error)
@@ -139,11 +295,10 @@ async function updateBadge(count: number) {
}
}
-// 清除桌面图标徽章
async function clearBadge() {
- if ('clearAppBadge' in navigator) {
+ if ('clearAppBadge' in self.navigator) {
try {
- await navigator.clearAppBadge!()
+ await (self.navigator as any).clearAppBadge()
await setStoredUnreadCount(0)
} catch (error) {
console.error('Failed to clear app badge:', error)
@@ -151,352 +306,87 @@ async function clearBadge() {
}
}
-// 清理旧版本缓存
-async function deleteOldCaches() {
- const cacheWhitelist = Object.values(CACHE_NAMES)
- const cacheNames = await caches.keys()
-
- await Promise.all(
- cacheNames.map(async cacheName => {
- if (!cacheWhitelist.includes(cacheName)) {
- console.log('Deleting old cache:', cacheName)
- return caches.delete(cacheName)
- }
- }),
- )
-}
-
-// 获取缓存大小
-async function getCacheSize(cacheName: string): Promise {
- if (!('estimate' in navigator.storage)) {
- return 0
- }
-
- try {
- const cache = await caches.open(cacheName)
- const keys = await cache.keys()
- let totalSize = 0
-
- for (const request of keys) {
- const response = await cache.match(request)
- if (response) {
- const blob = await response.blob()
- totalSize += blob.size
- }
- }
-
- return totalSize
- } catch (error) {
- console.error('Failed to get cache size:', error)
- return 0
- }
-}
-
// 监控缓存大小
async function monitorCacheSize() {
const cacheSizes: Record = {}
let totalSize = 0
- for (const [key, cacheName] of Object.entries(CACHE_NAMES)) {
- const size = await getCacheSize(cacheName)
- cacheSizes[key] = size
- totalSize += size
- }
+ try {
+ const cacheNames = await caches.keys()
- // 发送缓存统计信息给客户端
- const clients = await self.clients.matchAll()
- clients.forEach(client => {
- client.postMessage({
- type: 'CACHE_SIZE_UPDATE',
- data: {
- cacheSizes,
- totalSize,
- totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
- },
- })
- })
+ // 并行处理所有缓存以提高性能
+ await Promise.all(
+ cacheNames.map(async cacheName => {
+ const cache = await caches.open(cacheName)
+ // 使用 matchAll 一次性获取所有响应,避免循环中多次 match
+ const responses = await cache.matchAll()
+ let cacheSize = 0
- return { cacheSizes, totalSize }
-}
-
-// 清理过期缓存条目
-async function cleanupExpiredCaches() {
- for (const [key, cacheName] of Object.entries(CACHE_NAMES)) {
- const limit = CACHE_SIZE_LIMITS[key as keyof typeof CACHE_SIZE_LIMITS]
- if (!limit) continue
-
- try {
- const cache = await caches.open(cacheName)
- const keys = await cache.keys()
-
- // 如果缓存条目超过限制,删除最老的条目
- if (keys.length > limit.maxEntries) {
- const deleteCount = keys.length - limit.maxEntries
- console.log(`Cleaning up ${deleteCount} entries from ${cacheName}`)
-
- // 删除最老的条目(假设数组开头是最老的)
- for (let i = 0; i < deleteCount; i++) {
- await cache.delete(keys[i])
+ for (const response of responses) {
+ // 仅通过 Content-Length 获取大小
+ const contentLength = response.headers.get('content-length')
+ if (contentLength) {
+ cacheSize += parseInt(contentLength, 10)
+ }
}
- }
- } catch (error) {
- console.error(`Failed to cleanup cache ${cacheName}:`, error)
+
+ cacheSizes[cacheName] = cacheSize
+ }),
+ )
+
+ // 计算总大小
+ totalSize = Object.values(cacheSizes).reduce((acc, size) => acc + size, 0)
+
+ // 获取存储估算
+ let quota = 0
+ let usage = 0
+ if (self.navigator.storage && self.navigator.storage.estimate) {
+ const estimate = await self.navigator.storage.estimate()
+ quota = estimate.quota || 0
+ usage = estimate.usage || 0
}
- }
-}
-// 安装事件
-self.addEventListener('install', () => {
- // 强制等待中的Service Worker立即成为活动的Service Worker
- self.skipWaiting()
-})
-
-// 激活事件
-self.addEventListener('activate', event => {
- event.waitUntil(
- (async () => {
- // 启用导航预载功能以提高性能
- if ('navigationPreload' in self.registration) {
- await self.registration.navigationPreload.enable()
- }
-
- // 清理旧版本的缓存
- await deleteOldCaches()
-
- // 清理过期的缓存条目
- await cleanupExpiredCaches()
-
- // 监控缓存大小
- await monitorCacheSize()
- })(),
- )
- // 告诉活动的Service Worker立即控制页面
- self.clients.claim()
-})
-
-// 处理API请求,当离线时发送消息到客户端
-self.addEventListener('fetch', event => {
- const url = new URL(event.request.url)
-
- // 处理API请求
- if (event.request.url.includes('/api/v1/')) {
- // GET请求:尝试从缓存返回
- if (event.request.method === 'GET') {
- event.respondWith(
- (async () => {
- try {
- // 尝试网络请求
- const networkResponse = await fetch(event.request)
- return networkResponse
- } catch (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_NAMES.api)
- const cachedResponse = await cache.match(event.request)
- if (cachedResponse) {
- return cachedResponse
- }
-
- // 如果没有缓存,抛出错误
- throw error
- }
- })(),
- )
+ const result = {
+ cacheSizes,
+ totalSize,
+ totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
+ quota,
+ usage,
+ quotaMB: (quota / 1024 / 1024).toFixed(2),
+ usageMB: (usage / 1024 / 1024).toFixed(2),
}
- // POST/PUT/DELETE请求:离线时加入同步队列
- else if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(event.request.method)) {
- event.respondWith(
- (async () => {
- try {
- // 尝试网络请求
- const networkResponse = await fetch(event.request)
- return networkResponse
- } catch (error) {
- // 网络错误时,加入同步队列
- await addToSyncQueue(event.request)
- // 通知客户端请求已加入队列
- if (self.clients) {
- self.clients.matchAll().then(clients => {
- clients.forEach(client => {
- client.postMessage({
- type: 'REQUEST_QUEUED',
- url: event.request.url,
- method: event.request.method,
- })
- })
- })
- }
-
- // 返回一个假的成功响应
- return new Response(
- JSON.stringify({
- success: true,
- queued: true,
- message: '请求已加入离线队列,将在网络恢复后自动同步',
- }),
- {
- status: 202,
- headers: { 'Content-Type': 'application/json' },
- },
- )
- }
- })(),
- )
- }
- return
- }
-})
-
-// 后台同步队列
-const syncQueue: Array<{
- id: string
- url: string
- method: string
- data?: any
- timestamp: number
-}> = []
-
-// 添加请求到同步队列
-async function addToSyncQueue(request: Request) {
- const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
- const url = request.url
- const method = request.method
-
- let data: any = null
- if (method !== 'GET' && method !== 'HEAD') {
- try {
- data = await request.clone().text()
- } catch (e) {
- console.error('Failed to read request body:', e)
- }
- }
-
- const syncItem = {
- id,
- url,
- method,
- data,
- timestamp: Date.now(),
- }
-
- // 保存到IndexedDB (使用专用的 "sync" store)
- await set(id, syncItem, 'sync')
- syncQueue.push(syncItem)
-
- // 注册后台同步
- if ('sync' in self.registration) {
- await self.registration.sync.register('sync-data')
- }
-}
-
-// 执行同步队列中的请求
-async function processSyncQueue() {
- const db = await openDB()
-
- // 先用只读事务获取所有同步项
- const items: Array = await new Promise((resolve, reject) => {
- const tx = db.transaction(['sync'], 'readonly')
- const store = tx.objectStore('sync')
- const req = store.getAll()
- req.onsuccess = () => resolve(req.result)
- req.onerror = () => reject(req.error)
- })
-
- // 收集需要删除的项目ID
- const itemsToDelete: string[] = []
- const itemsToDeleteExpired: string[] = []
-
- for (const syncItem of items) {
- const key = syncItem.id
- try {
- // 构建请求
- const init: RequestInit = {
- method: syncItem.method,
- headers: {
- 'Content-Type': 'application/json',
- },
- }
-
- if (syncItem.data) {
- init.body = syncItem.data
- }
-
- // 发送请求
- const response = await fetch(syncItem.url, init)
-
- if (response.ok) {
- // 成功后标记为需要删除
- itemsToDelete.push(key)
-
- // 通知客户端同步成功
- const clients = await self.clients.matchAll()
- clients.forEach(client => {
- client.postMessage({
- type: 'SYNC_SUCCESS',
- syncId: syncItem.id,
- url: syncItem.url,
- })
- })
- } else {
- throw new Error(`HTTP ${response.status}`)
- }
- } catch (error) {
- console.error('Sync failed for item:', key, error)
-
- // 如果该同步项已存在超过 24 小时,则标记为需要删除
- if (Date.now() - syncItem.timestamp > 24 * 60 * 60 * 1000) {
- itemsToDeleteExpired.push(key)
- }
- }
- }
-
- // 批量删除所有成功处理的项目和过期项目
- const allItemsToDelete = [...itemsToDelete, ...itemsToDeleteExpired]
- if (allItemsToDelete.length > 0) {
- await new Promise((resolve, reject) => {
- const tx = db.transaction(['sync'], 'readwrite')
- const store = tx.objectStore('sync')
-
- // 批量删除所有标记的项目
- allItemsToDelete.forEach(id => {
- store.delete(id)
+ // 发送缓存统计信息给客户端
+ const clients = await self.clients.matchAll()
+ clients.forEach(client => {
+ client.postMessage({
+ type: 'CACHE_SIZE_UPDATE',
+ data: result,
})
-
- tx.oncomplete = () => resolve()
- tx.onerror = () => reject(tx.error)
})
+
+ return result
+ } catch (error) {
+ console.error('Failed to monitor cache size:', error)
+ return {
+ cacheSizes: {},
+ totalSize: 0,
+ totalSizeMB: '0.00',
+ quota: 0,
+ usage: 0,
+ quotaMB: '0.00',
+ usageMB: '0.00',
+ }
}
}
-// 初始化 Workbox
-cleanupOutdatedCaches()
-precacheAndRoute(self.__WB_MANIFEST)
+// --- 事件监听 ---
-// 监听 sync 事件,处理后台同步
-self.addEventListener('sync', (event: SyncEvent) => {
- if (event.tag === 'sync-data') {
- event.waitUntil(processSyncQueue())
- }
-})
-
-// 监听 push 事件,显示通知
+// 监听 push 事件
self.addEventListener('push', function (event) {
if (!event.data) {
return
}
- // 解析获取推送消息
let payload
try {
payload = event.data?.json()
@@ -505,7 +395,7 @@ self.addEventListener('push', function (event) {
title: event.data?.text(),
}
}
- // 根据推送消息生成桌面通知并展现出来
+
try {
const content = {
body: payload.body || '',
@@ -515,7 +405,6 @@ self.addEventListener('push', function (event) {
actions: options.actions,
}
- // 增加未读消息计数并持久化存储
event.waitUntil(
(async () => {
const currentCount = await getStoredUnreadCount()
@@ -525,11 +414,11 @@ self.addEventListener('push', function (event) {
})(),
)
} catch (e) {
- // 静默处理错误
+ // 忽略错误
}
})
-// 监听通知点击事件
+// 监听通知点击
self.addEventListener('notificationclick', function (event) {
const info = event.notification
if (event.action === 'close') {
@@ -539,10 +428,9 @@ self.addEventListener('notificationclick', function (event) {
}
})
-// 监听来自主应用的消息,用于清除徽章或更新徽章数量
+// 监听消息
self.addEventListener('message', function (event) {
if (event.data && event.data.type === 'CLEAR_BADGE') {
- // 清除徽章
clearBadge()
.then(() => {
event.ports[0]?.postMessage({ success: true })
@@ -551,7 +439,6 @@ self.addEventListener('message', function (event) {
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
})
} else if (event.data && event.data.type === 'UPDATE_BADGE') {
- // 更新徽章数量
const count = event.data.count || 0
setStoredUnreadCount(count)
.then(() => updateBadge(count))
@@ -562,25 +449,27 @@ self.addEventListener('message', function (event) {
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
})
} else if (event.data && event.data.type === 'GET_UNREAD_COUNT') {
- // 获取未读消息数量
getStoredUnreadCount()
.then(count => {
event.ports[0]?.postMessage({ count })
})
- .catch(error => {
+ .catch(() => {
event.ports[0]?.postMessage({ count: 0 })
})
} else if (event.data && event.data.type === 'CLEANUP_CACHES') {
- // 手动触发缓存清理
- Promise.all([deleteOldCaches(), cleanupExpiredCaches(), monitorCacheSize()])
- .then(([, , cacheInfo]) => {
+ // 手动清理: 清理所有运行时缓存
+ const performCleanup = async () => {
+ await cleanupRuntimeCaches(false)
+ return await monitorCacheSize()
+ }
+ performCleanup()
+ .then(cacheInfo => {
event.ports[0]?.postMessage({ success: true, cacheInfo })
})
.catch(error => {
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
})
} else if (event.data && event.data.type === 'GET_CACHE_INFO') {
- // 获取缓存信息
monitorCacheSize()
.then(cacheInfo => {
event.ports[0]?.postMessage({ success: true, cacheInfo })
@@ -588,5 +477,21 @@ self.addEventListener('message', function (event) {
.catch(error => {
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
})
+ } else if (event.data && event.data.type === 'CHECK_SW_UPDATE') {
+ // 检查 Service Worker 更新
+ self.registration
+ .update()
+ .then(() => {
+ // 如果没有正在安装或等待的 worker,说明没有检测到更新
+ if (!self.registration.installing && !self.registration.waiting) {
+ event.ports[0]?.postMessage({ type: 'SW_NO_UPDATE_DETECTED' })
+ }
+ })
+ .catch(error => {
+ console.error('Failed to check for SW update:', error)
+ event.ports[0]?.postMessage({ type: 'SW_NO_UPDATE_DETECTED' })
+ })
+ } else if (event.data && event.data.type === 'SKIP_WAITING') {
+ self.skipWaiting()
}
})
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
index 01ebeff1..23650204 100644
--- a/src/types/global.d.ts
+++ b/src/types/global.d.ts
@@ -1,5 +1,8 @@
// PWA Badge API 类型定义
declare global {
+ const __APP_VERSION__: string
+ const __BUILD_TIME__: string
+
interface Navigator {
/**
* 设置应用徽章数量
diff --git a/vite.config.ts b/vite.config.ts
index 8e6dc8ac..4d99beae 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -14,6 +14,7 @@ import { readFileSync } from 'node:fs'
// 读取 package.json 获取版本号
const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8'))
+const buildTime = new Date().getTime().toString()
// https://vitejs.dev/config/
export default defineConfig({
@@ -56,104 +57,10 @@ export default defineConfig({
strategies: 'injectManifest',
srcDir: 'src',
filename: 'service-worker.ts',
- workbox: {
- globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg,webp,woff,woff2,ttf,otf,eot}'],
- // 确保关键资源被预缓存
- additionalManifestEntries: [
- {
- url: '/offline.html',
- revision: null,
- },
- // 预缓存App Shell关键资源
- {
- url: '/logo.png',
- revision: null,
- },
- ],
- // 启用导航预加载
- navigationPreload: true,
- runtimeCaching: [
- // App Shell缓存 - 优先缓存
- {
- urlPattern: /^\/$|\/index\.html$/,
- handler: 'CacheFirst',
- options: {
- cacheName: 'app-shell-cache',
- expiration: {
- maxEntries: 10,
- maxAgeSeconds: 7 * 24 * 60 * 60, // 7天
- },
- },
- },
- {
- urlPattern: /\.(?:js|css|html)$/,
- handler: 'StaleWhileRevalidate',
- options: {
- cacheName: 'static-resources',
- },
- },
- {
- urlPattern: /\.(?:png|jpg|jpeg|svg|ico|webp|avif|gif|bmp|tiff)$/,
- handler: 'CacheFirst',
- options: {
- cacheName: 'image-cache',
- expiration: {
- maxEntries: 200,
- maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
- },
- },
- },
- {
- urlPattern: /\.(?:woff|woff2|ttf|otf|eot)$/,
- handler: 'CacheFirst',
- options: {
- cacheName: 'font-cache',
- expiration: {
- maxEntries: 50,
- maxAgeSeconds: 365 * 24 * 60 * 60, // 1年
- },
- },
- },
- {
- urlPattern: /\/api\/v1\/.*$/,
- handler: 'NetworkFirst',
- options: {
- cacheName: 'api-cache',
- networkTimeoutSeconds: 10,
- expiration: {
- maxEntries: 500,
- maxAgeSeconds: 24 * 60 * 60, // 24小时
- },
- },
- },
- {
- urlPattern: /^https:\/\/image\.tmdb\.org\/.*$/,
- handler: 'CacheFirst',
- options: {
- cacheName: 'tmdb-image-cache',
- expiration: {
- maxEntries: 300,
- maxAgeSeconds: 7 * 24 * 60 * 60, // 7天
- },
- },
- },
- {
- urlPattern: ({ request }) => request.destination === 'document',
- handler: 'StaleWhileRevalidate',
- options: {
- cacheName: 'pages-cache',
- },
- },
- ],
- navigateFallback: '/offline.html',
- navigateFallbackDenylist: [/.*\/api\/.*/, /\/offline\.html$/],
- ignoreURLParametersMatching: [/^utm_/, /^fbclid$/, /^gclid$/],
- skipWaiting: true,
- clientsClaim: true,
- },
injectManifest: {
rollupFormat: 'iife',
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg,webp,woff,woff2,ttf,otf,eot}'],
},
devOptions: {
enabled: true,
@@ -283,7 +190,8 @@ export default defineConfig({
],
define: {
'process.env': {},
- '__APP_VERSION__': JSON.stringify(`v${packageJson.version}`)
+ '__APP_VERSION__': JSON.stringify(`v${packageJson.version}`),
+ '__BUILD_TIME__': JSON.stringify(buildTime),
},
resolve: {
alias: {
@@ -307,12 +215,6 @@ export default defineConfig({
},
chunkSizeWarningLimit: 5000,
cssCodeSplit: false,
- rollupOptions: {
- output: {
- entryFileNames: '[name].js',
- chunkFileNames: '[name].js',
- },
- },
},
optimizeDeps: {
exclude: ['vuetify'],
From 425bf808ed4fe280ee692be6693edd56e49bf269 Mon Sep 17 00:00:00 2001
From: PKC278 <52959804+PKC278@users.noreply.github.com>
Date: Fri, 2 Jan 2026 21:22:14 +0800
Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20i18n-menu=20?=
=?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0=E5=A4=96=E9=83=A8=E8=B0=83?=
=?UTF-8?q?=E7=94=A8=E5=AF=BC=E8=87=B4=E7=9A=84=E8=BF=90=E8=A1=8C=E6=97=B6?=
=?UTF-8?q?=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/dialog/SearchBarDialog.vue | 4 ++--
src/layouts/components/DefaultLayout.vue | 2 +-
src/layouts/components/Footer.vue | 2 +-
src/pages/appcenter.vue | 2 +-
src/pages/discover.vue | 2 +-
src/pages/login.vue | 4 ++--
src/pages/setting.vue | 3 ++-
src/pages/subscribe.vue | 4 ++--
src/pages/workflow.vue | 2 +-
src/router/i18n-menu.ts | 29 +++++++----------------
src/views/plugin/PluginCardListView.vue | 2 +-
11 files changed, 22 insertions(+), 34 deletions(-)
diff --git a/src/components/dialog/SearchBarDialog.vue b/src/components/dialog/SearchBarDialog.vue
index 300ccc84..4ef28ed0 100644
--- a/src/components/dialog/SearchBarDialog.vue
+++ b/src/components/dialog/SearchBarDialog.vue
@@ -122,7 +122,7 @@ function loadRecentSearches() {
function getMenus(): NavMenu[] {
let menus: NavMenu[] = []
// 导航菜单
- getNavMenus().forEach(
+ getNavMenus(t).forEach(
item =>
item &&
menus.push({
@@ -134,7 +134,7 @@ function getMenus(): NavMenu[] {
}),
)
// 设置标签页
- getSettingTabs().forEach(
+ getSettingTabs(t).forEach(
item =>
item &&
menus.push({
diff --git a/src/layouts/components/DefaultLayout.vue b/src/layouts/components/DefaultLayout.vue
index 38de86c6..0e8677d4 100644
--- a/src/layouts/components/DefaultLayout.vue
+++ b/src/layouts/components/DefaultLayout.vue
@@ -197,7 +197,7 @@ const {
// 根据分类获取菜单列表
const getMenuList = (header: string) => {
// 使用国际化菜单
- const menus = getNavMenus()
+ const menus = getNavMenus(t)
const filteredMenus = filterMenusByPermission(menus, userPermissions.value)
return filteredMenus.filter((item: NavMenu) => item.header === header)
}
diff --git a/src/layouts/components/Footer.vue b/src/layouts/components/Footer.vue
index dc3d5d34..683011a0 100644
--- a/src/layouts/components/Footer.vue
+++ b/src/layouts/components/Footer.vue
@@ -49,7 +49,7 @@ const userPermissions = computed(() => {
// 获取导航菜单
const navMenus = computed(() => {
- const allMenus = getNavMenus()
+ const allMenus = getNavMenus(t)
return filterMenusByPermission(allMenus, userPermissions.value)
})
diff --git a/src/pages/appcenter.vue b/src/pages/appcenter.vue
index 13b9cdcc..b7b2f031 100644
--- a/src/pages/appcenter.vue
+++ b/src/pages/appcenter.vue
@@ -23,7 +23,7 @@ const appGroups = ref>({})
// 根据header属性对应用进行分类
function categorizeApps() {
// 获取所有菜单并根据权限过滤
- const allMenus = getNavMenus()
+ const allMenus = getNavMenus(t)
const filteredMenus = filterMenusByPermission(allMenus, userPermissions.value)
const menus = filteredMenus.filter((item: NavMenu) => !item.footer)
diff --git a/src/pages/discover.vue b/src/pages/discover.vue
index 04992b39..192a4781 100644
--- a/src/pages/discover.vue
+++ b/src/pages/discover.vue
@@ -58,7 +58,7 @@ function initializeColors() {
// 初始化发现标签
function initDiscoverTabs() {
- const tabs = getDiscoverTabs()
+ const tabs = getDiscoverTabs(t)
for (const tab of tabs) {
discoverTabs.value.push({
name: tab.name,
diff --git a/src/pages/login.vue b/src/pages/login.vue
index 83af1a22..040c16d5 100644
--- a/src/pages/login.vue
+++ b/src/pages/login.vue
@@ -21,7 +21,7 @@ const authStore = useAuthStore()
const userStore = useUserStore()
// 获取有权限的菜单
-const navMenus = getNavMenus()
+const navMenus = computed(() => getNavMenus(t))
// 表单
const form = ref({
@@ -229,7 +229,7 @@ async function handleLoginSuccess(response: any) {
...userPayload.permissions,
}
- const filteredMenus = filterMenusByPermission(navMenus, userPermissions)
+ const filteredMenus = filterMenusByPermission(navMenus.value, userPermissions)
if (filteredMenus.length === 0) {
errorMessage.value = t('login.noPermission')
return
diff --git a/src/pages/setting.vue b/src/pages/setting.vue
index 738050b3..9bf58d67 100644
--- a/src/pages/setting.vue
+++ b/src/pages/setting.vue
@@ -12,10 +12,11 @@ import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import { getSettingTabs } from '@/router/i18n-menu'
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
+const { t } = useI18n()
const route = useRoute()
const activeTab = ref((route.query.tab as string) || '')
-const settingTabs = computed(() => getSettingTabs())
+const settingTabs = computed(() => getSettingTabs(t))
// 使用动态标签页
const { registerHeaderTab } = useDynamicHeaderTab()
diff --git a/src/pages/subscribe.vue b/src/pages/subscribe.vue
index 66be37fb..038fb520 100644
--- a/src/pages/subscribe.vue
+++ b/src/pages/subscribe.vue
@@ -22,9 +22,9 @@ const shareViewKey = ref(0)
// 获取标签页
const subscribeTabs = computed(() => {
if (subType === '电影') {
- return getSubscribeMovieTabs()
+ return getSubscribeMovieTabs(t)
} else {
- return getSubscribeTvTabs()
+ return getSubscribeTvTabs(t)
}
})
diff --git a/src/pages/workflow.vue b/src/pages/workflow.vue
index f7480d93..0be74387 100644
--- a/src/pages/workflow.vue
+++ b/src/pages/workflow.vue
@@ -17,7 +17,7 @@ const listViewKey = ref(0)
// 获取标签页
const workflowTabs = computed(() => {
- return getWorkflowTabs()
+ return getWorkflowTabs(t)
})
// 新增工作流对话框
diff --git a/src/router/i18n-menu.ts b/src/router/i18n-menu.ts
index 50976c88..9efa7737 100644
--- a/src/router/i18n-menu.ts
+++ b/src/router/i18n-menu.ts
@@ -1,9 +1,8 @@
-import { useI18n } from 'vue-i18n'
import { useGlobalSettingsStore } from '@/stores'
+import type { Composer } from 'vue-i18n'
// 构建路由菜单,每次调用时使用当前的语言环境
-export function getNavMenus() {
- const { t } = useI18n()
+export function getNavMenus(t: Composer['t']) {
const globalSettingsStore = useGlobalSettingsStore()
// 检查是否为高级模式
@@ -148,9 +147,7 @@ export function getNavMenus() {
}
// 获取设置标签页
-export function getSettingTabs() {
- const { t } = useI18n()
-
+export function getSettingTabs(t: Composer['t']) {
return [
{
title: t('settingTabs.system.title'),
@@ -204,9 +201,7 @@ export function getSettingTabs() {
}
// 获取电影订阅标签页
-export function getSubscribeMovieTabs() {
- const { t } = useI18n()
-
+export function getSubscribeMovieTabs(t: Composer['t']) {
return [
{
title: t('subscribeTabs.movie.mysub'),
@@ -222,9 +217,7 @@ export function getSubscribeMovieTabs() {
}
// 获取电视剧订阅标签页
-export function getSubscribeTvTabs() {
- const { t } = useI18n()
-
+export function getSubscribeTvTabs(t: Composer['t']) {
return [
{
title: t('subscribeTabs.tv.mysub'),
@@ -245,9 +238,7 @@ export function getSubscribeTvTabs() {
}
// 获取插件标签页
-export function getPluginTabs() {
- const { t } = useI18n()
-
+export function getPluginTabs(t: Composer['t']) {
return [
{
title: t('pluginTabs.installed'),
@@ -263,9 +254,7 @@ export function getPluginTabs() {
}
// 获取发现标签页
-export function getDiscoverTabs() {
- const { t } = useI18n()
-
+export function getDiscoverTabs(t: Composer['t']) {
return [
{
name: t('discoverTabs.themoviedb'),
@@ -286,9 +275,7 @@ export function getDiscoverTabs() {
}
// 获取工作流标签页
-export function getWorkflowTabs() {
- const { t } = useI18n()
-
+export function getWorkflowTabs(t: Composer['t']) {
return [
{
title: t('workflowTabs.list'),
diff --git a/src/views/plugin/PluginCardListView.vue b/src/views/plugin/PluginCardListView.vue
index 19b3aa6d..20726059 100644
--- a/src/views/plugin/PluginCardListView.vue
+++ b/src/views/plugin/PluginCardListView.vue
@@ -32,7 +32,7 @@ const { appMode } = usePWA()
const activeTab = ref('installed')
// 获取插件标签页
-const pluginTabs = computed(() => getPluginTabs())
+const pluginTabs = computed(() => getPluginTabs(t))
// 使用动态标签页
const { registerHeaderTab } = useDynamicHeaderTab()
From 78e2d05730952662ff49bd208d2f350e9f979dd2 Mon Sep 17 00:00:00 2001
From: PKC278 <52959804+PKC278@users.noreply.github.com>
Date: Fri, 2 Jan 2026 21:22:27 +0800
Subject: [PATCH 3/9] =?UTF-8?q?feat(index):=20=E6=B7=BB=E5=8A=A0=E9=A1=B5?=
=?UTF-8?q?=E9=9D=A2=E5=8A=A0=E8=BD=BD=E8=B6=85=E6=97=B6=E6=8F=90=E7=A4=BA?=
=?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=94=B9=E9=BB=98=E8=AE=A4=E4=B8=BB=E9=A2=98?=
=?UTF-8?q?=E8=AE=BE=E7=BD=AE=E4=B8=BA=E8=B7=9F=E9=9A=8F=E7=B3=BB=E7=BB=9F?=
=?UTF-8?q?=20fix(service-worker):=20=E4=BC=98=E5=8C=96=E6=B8=85=E7=90=86?=
=?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E7=BC=93=E5=AD=98=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.html | 67 ++++++++++++++++++-
src/App.vue | 6 +-
src/composables/useVersionChecker.ts | 4 +-
src/layouts/components/UserProfile.vue | 2 +-
src/service-worker.ts | 34 +++++++---
.../setting/AccountSettingNotification.vue | 2 +-
6 files changed, 96 insertions(+), 19 deletions(-)
diff --git a/index.html b/index.html
index 286dd6ea..f05de863 100644
--- a/index.html
+++ b/index.html
@@ -193,6 +193,35 @@
transform: rotate(1turn);
}
}
+
+ /* 超时通知样式 */
+ #loading-timeout {
+ position: absolute;
+ z-index: 2500;
+ display: none;
+ inset-block-end: 20px;
+ inset-inline-start: 50%;
+ transform: translateX(-50%);
+ background: rgba(0, 0, 0, 0.8);
+ color: #fff;
+ padding: 12px 24px;
+ border-radius: 12px;
+ font-size: 14px;
+ font-family: sans-serif;
+ text-align: center;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
+ white-space: nowrap;
+ backdrop-filter: blur(4px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ }
+
+ #timeout-btn {
+ color: var(--initial-loader-color, #9155FD);
+ text-decoration: none;
+ font-weight: bold;
+ margin-inline-start: 8px;
+ border-bottom: 1px solid var(--initial-loader-color, #9155FD);
+ }
@@ -257,6 +318,10 @@
+
+
diff --git a/src/App.vue b/src/App.vue
index bb42aec2..af9b50fa 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -15,7 +15,7 @@ import { themeManager } from '@/utils/themeManager'
// 生效主题
const { global: globalTheme } = useTheme()
-let themeValue = localStorage.getItem('theme') || 'light'
+let themeValue = localStorage.getItem('theme') || 'auto'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
@@ -239,8 +239,8 @@ async function loadBackgroundImages(retryCount = 0) {
onMounted(async () => {
// 移除URL中的时间戳参数
const url = new URL(window.location.href)
- if (url.searchParams.has('t')) {
- url.searchParams.delete('t')
+ if (url.searchParams.has('_t')) {
+ url.searchParams.delete('_t')
window.history.replaceState({}, '', url.toString())
}
diff --git a/src/composables/useVersionChecker.ts b/src/composables/useVersionChecker.ts
index edc3d486..80e448d6 100644
--- a/src/composables/useVersionChecker.ts
+++ b/src/composables/useVersionChecker.ts
@@ -18,8 +18,8 @@ const needsUpdate = computed(() => {
*/
export const reloadWithTimestamp = (): void => {
const url = new URL(window.location.href)
- url.searchParams.set('t', Date.now().toString())
- window.location.href = url.toString()
+ url.searchParams.set('_t', Date.now().toString())
+ window.location.replace(url.toString());
}
/**
diff --git a/src/layouts/components/UserProfile.vue b/src/layouts/components/UserProfile.vue
index 80ad008c..b52c6c30 100644
--- a/src/layouts/components/UserProfile.vue
+++ b/src/layouts/components/UserProfile.vue
@@ -272,7 +272,7 @@ const getUIModeIcon = computed(() => {
// 主题相关功能
const { name: themeName, global: globalTheme } = useTheme()
-const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
+const savedTheme = ref(localStorage.getItem('theme') ?? 'auto')
const currentThemeName = ref(savedTheme.value)
const themes: ThemeSwitcherTheme[] = [
diff --git a/src/service-worker.ts b/src/service-worker.ts
index 7217a4ce..d893411e 100644
--- a/src/service-worker.ts
+++ b/src/service-worker.ts
@@ -7,11 +7,13 @@ import * as navigationPreload from 'workbox-navigation-preload'
// Service Worker 类型声明
declare let self: ServiceWorkerGlobalScope & {
- __WB_MANIFEST: Array<{ url: string; revision?: string }>
+ readonly __WB_MANIFEST: Array<{ url: string; revision?: string }>
}
// 缓存版本控制
-const CACHE_VERSION = `${__APP_VERSION__}-${__BUILD_TIME__}`
+const RESOURCE_VERSION = 'V2'
+// 开发环境CACHE_VERSION回退到RESOURCE_VERSION
+const CACHE_VERSION = typeof __APP_VERSION__ !== 'undefined' ? `${__APP_VERSION__}-${__BUILD_TIME__}` : RESOURCE_VERSION
// 启用导航预载
navigationPreload.enable()
@@ -111,7 +113,7 @@ registerRoute(
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
- cacheName: `image-cache-${CACHE_VERSION}`,
+ cacheName: `image-cache-${RESOURCE_VERSION}`,
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
@@ -128,7 +130,7 @@ registerRoute(
registerRoute(
({ request }) => request.destination === 'font',
new CacheFirst({
- cacheName: `font-cache-${CACHE_VERSION}`,
+ cacheName: `font-cache-${RESOURCE_VERSION}`,
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
@@ -145,7 +147,7 @@ registerRoute(
registerRoute(
({ url }) => url.hostname === 'image.tmdb.org',
new CacheFirst({
- cacheName: `tmdb-image-cache-${CACHE_VERSION}`,
+ cacheName: `tmdb-image-cache-${RESOURCE_VERSION}`,
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
@@ -165,7 +167,8 @@ registerRoute(
request.method === 'GET' &&
!url.pathname.includes('/api/v1/system/message') && // 排除 SSE 长连接
!url.pathname.includes('/api/v1/common/message') && // 排除通用消息
- !url.pathname.includes('/api/v1/message/'), // 排除所有消息类接口
+ !url.pathname.includes('/api/v1/message/') && // 排除所有消息类接口
+ !url.pathname.includes('/api/v1/system/global'), // 排除global接口
new NetworkFirst({
cacheName: `api-cache-${CACHE_VERSION}`,
networkTimeoutSeconds: 5,
@@ -201,14 +204,23 @@ async function cleanupRuntimeCaches(onlyOld: boolean = false) {
'font-cache',
'api-cache',
'tmdb-image-cache',
- 'pages-cache',
+ ]
+
+ // 当前版本的缓存全名
+ const currentCacheNames = [
+ `app-shell-${CACHE_VERSION}`,
+ `static-resources-${CACHE_VERSION}`,
+ `image-cache-${RESOURCE_VERSION}`,
+ `font-cache-${RESOURCE_VERSION}`,
+ `tmdb-image-cache-${RESOURCE_VERSION}`,
+ `api-cache-${CACHE_VERSION}`,
]
await Promise.all(
cacheNames.map(cacheName => {
const isRuntimeCache = runtimeCachePrefixes.some(prefix => cacheName.startsWith(prefix))
if (isRuntimeCache) {
- if (!onlyOld || !cacheName.includes(CACHE_VERSION)) {
+ if (!onlyOld || !currentCacheNames.includes(cacheName)) {
console.log('[SW] Deleting runtime cache:', cacheName)
return caches.delete(cacheName)
}
@@ -285,9 +297,9 @@ async function updateBadge(count: number) {
if ('setAppBadge' in self.navigator) {
try {
if (count > 0) {
- await (self.navigator as any).setAppBadge(count)
+ await self.navigator.setAppBadge(count)
} else {
- await (self.navigator as any).clearAppBadge()
+ await self.navigator.clearAppBadge()
}
} catch (error) {
console.error('Failed to update app badge:', error)
@@ -298,7 +310,7 @@ async function updateBadge(count: number) {
async function clearBadge() {
if ('clearAppBadge' in self.navigator) {
try {
- await (self.navigator as any).clearAppBadge()
+ await self.navigator.clearAppBadge()
await setStoredUnreadCount(0)
} catch (error) {
console.error('Failed to clear app badge:', error)
diff --git a/src/views/setting/AccountSettingNotification.vue b/src/views/setting/AccountSettingNotification.vue
index b5de2856..26ba0eff 100644
--- a/src/views/setting/AccountSettingNotification.vue
+++ b/src/views/setting/AccountSettingNotification.vue
@@ -45,7 +45,7 @@ const templateTypes = ref([
// 编辑器主题
const { name: themeName, global: globalTheme } = useTheme()
-const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
+const savedTheme = ref(localStorage.getItem('theme') ?? 'auto')
const currentThemeName = ref(savedTheme.value)
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
From 43d2406ee9bc39005db6f6946054b3e4a61c7f1a Mon Sep 17 00:00:00 2001
From: PKC278 <52959804+PKC278@users.noreply.github.com>
Date: Sat, 3 Jan 2026 10:32:58 +0800
Subject: [PATCH 4/9] =?UTF-8?q?feat(timeout):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E9=A1=B5=E9=9D=A2=E5=8A=A0=E8=BD=BD?=
=?UTF-8?q?=E8=B6=85=E6=97=B6=E6=8F=90=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.html | 32 ++++++++++++++++++++++++---
src/components/dialog/AboutDialog.vue | 6 ++---
src/locales/en-US.ts | 2 +-
src/locales/zh-CN.ts | 2 +-
src/locales/zh-TW.ts | 2 +-
5 files changed, 35 insertions(+), 9 deletions(-)
diff --git a/index.html b/index.html
index f05de863..b94410ca 100644
--- a/index.html
+++ b/index.html
@@ -301,6 +301,34 @@
setTimeout(function() {
const timeoutEl = document.getElementById('loading-timeout');
if (timeoutEl) {
+ // 适配多语言
+ const lang = navigator.language || 'zh-CN';
+ const messages = {
+ 'zh-CN': {
+ text: '页面加载似乎遇到了阻碍,请尝试',
+ btn: '清除缓存'
+ },
+ 'zh-TW': {
+ text: '頁面載入似乎遇到了阻礙,請嘗試',
+ btn: '清除快取'
+ },
+ 'en-US': {
+ text: 'Page loading seems to be blocked, please try',
+ btn: 'Clear Cache'
+ }
+ };
+
+ // 默认匹配前缀,如 en-GB 匹配 en-US 的逻辑
+ let msg = messages['zh-CN'];
+ if (lang.startsWith('zh-TW') || lang.startsWith('zh-HK')) {
+ msg = messages['zh-TW'];
+ } else if (lang.startsWith('en')) {
+ msg = messages['en-US'];
+ } else if (lang.startsWith('zh')) {
+ msg = messages['zh-CN'];
+ }
+
+ timeoutEl.innerHTML = msg.text + ' ' + msg.btn + '';
timeoutEl.style.display = 'block';
}
}, 15000); // 15秒后显示超时提示
@@ -319,9 +347,7 @@
-
+
diff --git a/src/components/dialog/AboutDialog.vue b/src/components/dialog/AboutDialog.vue
index 64745d36..04ac0fdf 100644
--- a/src/components/dialog/AboutDialog.vue
+++ b/src/components/dialog/AboutDialog.vue
@@ -120,7 +120,7 @@ function releaseTime(releaseDate: string) {
}
// 强制清除缓存
-async function cleanCache() {
+async function clearCache() {
await clearCachesAndServiceWorker()
// 刷新页面,添加时间戳参数以强制更新
reloadWithTimestamp()
@@ -191,12 +191,12 @@ onMounted(() => {
size="x-small"
variant="tonal"
class="ms-2"
- @click="cleanCache"
+ @click="clearCache"
>
- {{ t('setting.about.cleanCache') }}
+ {{ t('setting.about.clearCache') }}
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index 0700b691..aaf49b12 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -1250,7 +1250,7 @@ export default {
dataDirectory: '/moviepilot',
expand: 'Expand',
collapse: 'Collapse',
- cleanCache: 'Clear Cache',
+ clearCache: 'Clear Cache',
},
system: {
custom: 'Custom',
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index f95c75d2..a8e9df96 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -1247,7 +1247,7 @@ export default {
dataDirectory: '/moviepilot',
expand: '展开',
collapse: '收起',
- cleanCache: '清除缓存',
+ clearCache: '清除缓存',
},
system: {
custom: '自定义',
diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts
index 1b1b30c1..abde346c 100644
--- a/src/locales/zh-TW.ts
+++ b/src/locales/zh-TW.ts
@@ -1235,7 +1235,7 @@ export default {
dataDirectory: '/moviepilot',
expand: '展開',
collapse: '收起',
- cleanCache: '清除緩存',
+ clearCache: '清除快取',
},
system: {
custom: '自定義',
From 0fcad02f3b4a43e3be07ef4d327b95e801f7cc26 Mon Sep 17 00:00:00 2001
From: PKC278 <52959804+PKC278@users.noreply.github.com>
Date: Sat, 3 Jan 2026 20:15:05 +0800
Subject: [PATCH 5/9] =?UTF-8?q?fix(VersionUpdateToast):=20=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E9=80=9A=E7=9F=A5=E6=A0=B7=E5=BC=8F=20fix(useVersionC?=
=?UTF-8?q?hecker):=20=E4=BC=98=E5=8C=96=E5=A4=84=E7=90=86=E9=80=BB?=
=?UTF-8?q?=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.html | 13 +++++++++--
src/App.vue | 3 ++-
src/components/toast/VersionUpdateToast.vue | 24 ++++++++++++++++++---
src/composables/useVersionChecker.ts | 23 +++++++++++++-------
src/styles/main.scss | 6 ++++++
5 files changed, 55 insertions(+), 14 deletions(-)
diff --git a/index.html b/index.html
index b94410ca..b343341c 100644
--- a/index.html
+++ b/index.html
@@ -294,7 +294,7 @@
// 3. 重载页面
const url = new URL(window.location.href)
url.searchParams.set('_t', Date.now().toString())
- window.location.replace(url.toString())
+ window.location.replace(url.pathname + url.search + url.hash)
}
};
@@ -328,7 +328,16 @@
msg = messages['zh-CN'];
}
- timeoutEl.innerHTML = msg.text + ' ' + msg.btn + '';
+ const textNode = document.createTextNode(msg.text + ' ');
+ const btnLink = document.createElement('a');
+ btnLink.href = 'javascript:void(0)';
+ btnLink.id = 'timeout-btn';
+ btnLink.onclick = window.clearAndReload;
+ btnLink.textContent = msg.btn;
+
+ timeoutEl.innerHTML = '';
+ timeoutEl.appendChild(textNode);
+ timeoutEl.appendChild(btnLink);
timeoutEl.style.display = 'block';
}
}, 15000); // 15秒后显示超时提示
diff --git a/src/App.vue b/src/App.vue
index af9b50fa..29da4893 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -241,7 +241,8 @@ onMounted(async () => {
const url = new URL(window.location.href)
if (url.searchParams.has('_t')) {
url.searchParams.delete('_t')
- window.history.replaceState({}, '', url.toString())
+ const newUrl = url.pathname + url.search + url.hash
+ window.history.replaceState(null, '', newUrl)
}
// 配置 ApexCharts
diff --git a/src/components/toast/VersionUpdateToast.vue b/src/components/toast/VersionUpdateToast.vue
index 568bef28..0f5bb784 100644
--- a/src/components/toast/VersionUpdateToast.vue
+++ b/src/components/toast/VersionUpdateToast.vue
@@ -1,9 +1,10 @@
{{ message }}
-
@@ -11,7 +12,7 @@
// 接收 props
interface Props {
message: string
- refreshText: string
+ refreshText?: string
onRefresh?: () => void
}
@@ -30,12 +31,12 @@ const handleRefresh = () => {
.version-update-toast {
display: flex;
align-items: center;
- justify-content: space-between;
gap: 12px;
}
.message {
flex: 1;
+ white-space: nowrap;
}
.refresh-button {
@@ -48,6 +49,7 @@ const handleRefresh = () => {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
+ flex-shrink: 0;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@@ -60,4 +62,20 @@ const handleRefresh = () => {
.refresh-button:active {
transform: scale(0.98);
}
+
+.spinner {
+ width: 16px;
+ height: 16px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-top-color: #fff;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ flex-shrink: 0;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/composables/useVersionChecker.ts b/src/composables/useVersionChecker.ts
index 80e448d6..6407744b 100644
--- a/src/composables/useVersionChecker.ts
+++ b/src/composables/useVersionChecker.ts
@@ -19,7 +19,7 @@ const needsUpdate = computed(() => {
export const reloadWithTimestamp = (): void => {
const url = new URL(window.location.href)
url.searchParams.set('_t', Date.now().toString())
- window.location.replace(url.toString());
+ window.location.replace(url.pathname + url.search + url.hash)
}
/**
@@ -72,6 +72,7 @@ export function useVersionChecker() {
closeButton: false,
closeOnClick: false,
draggable: false,
+ toastClassName: 'version-update-toast-container',
})
}
@@ -104,13 +105,13 @@ export function useVersionChecker() {
// 优先尝试通过 Service Worker 检查更新
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
- console.log('[VersionChecker] Requesting Service Worker update check...')
+ console.log('[VersionChecker] 正在请求 Service Worker 检查更新...')
const registration = await navigator.serviceWorker.getRegistration()
// 如果已经有等待中的更新,直接处理
if (registration?.waiting) {
- console.log('[VersionChecker] New worker waiting, skipping manual check.')
+ console.log('[VersionChecker] Service Worker 发现新版本,跳过版本号对比')
handleVersionMismatch()
return
}
@@ -119,7 +120,7 @@ export function useVersionChecker() {
messageChannel.port1.onmessage = event => {
if (event.data && event.data.type === 'SW_NO_UPDATE_DETECTED') {
- console.log('[VersionChecker] Service Worker reported no update, checking version manually...')
+ console.log('[VersionChecker] Service Worker 报告无更新, 进行版本号检查...')
handleVersionMismatch()
}
}
@@ -144,19 +145,25 @@ export function useVersionChecker() {
navigator.serviceWorker.addEventListener('message', event => {
// 1. 发现新版本 -> 弹出通知
if (event.data && event.data.type === 'SW_VERSION_DETECTED') {
- console.log('[VersionChecker] Detected new version:', event.data.version)
+ console.log('[VersionChecker] 发现新版本:', event.data.version)
notificationShowTime = Date.now()
- toast.info(i18n.global.t('common.newVersionFound'), {
+
+ const component = h(VersionUpdateToast, {
+ message: i18n.global.t('common.newVersionFound'),
+ })
+
+ toast.info(component, {
timeout: false,
hideProgressBar: true,
closeButton: false,
+ toastClassName: 'version-update-toast-container',
})
}
// 2. 安装完成 -> 刷新页面
else if (event.data && event.data.type === 'SW_RELOAD_PAGE') {
const elapsed = Date.now() - notificationShowTime
- const delay = Math.max(0, 1000 - elapsed)
- console.log(`[VersionChecker] Update installed, reloading in ${delay}ms...`)
+ const delay = Math.max(0, 1500 - elapsed)
+ console.log(`[VersionChecker] 更新已安装, 延迟 ${delay}ms 后刷新...`)
setTimeout(() => {
reloadWithTimestamp()
}, delay)
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 5f0be75f..c0fac356 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -11,3 +11,9 @@
@import 'vue-toastification/dist/index.css';
@import 'vue3-perfect-scrollbar/style.css';
@import '@vue-js-cron/vuetify/dist/vuetify.css';
+
+/* 版本更新通知专用样式 */
+.version-update-toast-container {
+ min-width: unset !important;
+ width: fit-content !important;
+}
From f342b08179e851dbd8a0a1f8345b9dc23dbd72a6 Mon Sep 17 00:00:00 2001
From: PKC278 <52959804+PKC278@users.noreply.github.com>
Date: Sat, 3 Jan 2026 21:53:04 +0800
Subject: [PATCH 6/9] =?UTF-8?q?fix(service-worker):=20=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E7=BC=93=E5=AD=98=E7=89=88=E6=9C=AC=E6=8E=A7=E5=88=B6=E5=92=8C?=
=?UTF-8?q?=E7=9B=91=E6=8E=A7=E7=BC=93=E5=AD=98=E5=A4=A7=E5=B0=8F=E9=80=BB?=
=?UTF-8?q?=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/composables/useVersionChecker.ts | 9 -------
src/service-worker.ts | 37 +++++++++++++++-------------
2 files changed, 20 insertions(+), 26 deletions(-)
diff --git a/src/composables/useVersionChecker.ts b/src/composables/useVersionChecker.ts
index 6407744b..b4574492 100644
--- a/src/composables/useVersionChecker.ts
+++ b/src/composables/useVersionChecker.ts
@@ -132,14 +132,6 @@ export function useVersionChecker() {
}
}
- /**
- * 重置版本检查状态(用于测试或特殊场景)
- */
- const resetVersionCheck = (): void => {
- versionChecked.value = false
- serverVersion.value = null
- }
-
// 监听 Service Worker 版本更新消息
if (!isListenerAdded && 'serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', event => {
@@ -180,6 +172,5 @@ export function useVersionChecker() {
versionChecked: computed(() => versionChecked.value),
// 方法
checkVersion,
- resetVersionCheck,
}
}
diff --git a/src/service-worker.ts b/src/service-worker.ts
index d893411e..22b87d1f 100644
--- a/src/service-worker.ts
+++ b/src/service-worker.ts
@@ -12,8 +12,7 @@ declare let self: ServiceWorkerGlobalScope & {
// 缓存版本控制
const RESOURCE_VERSION = 'V2'
-// 开发环境CACHE_VERSION回退到RESOURCE_VERSION
-const CACHE_VERSION = typeof __APP_VERSION__ !== 'undefined' ? `${__APP_VERSION__}-${__BUILD_TIME__}` : RESOURCE_VERSION
+const CACHE_VERSION = `${__APP_VERSION__}-${__BUILD_TIME__}` // 开发环境下无法使用此环境变量,生产环境正常
// 启用导航预载
navigationPreload.enable()
@@ -321,35 +320,35 @@ async function clearBadge() {
// 监控缓存大小
async function monitorCacheSize() {
const cacheSizes: Record = {}
- let totalSize = 0
+ let calculatedTotalSize = 0
try {
const cacheNames = await caches.keys()
- // 并行处理所有缓存以提高性能
+ // 并行处理所有缓存
await Promise.all(
cacheNames.map(async cacheName => {
const cache = await caches.open(cacheName)
- // 使用 matchAll 一次性获取所有响应,避免循环中多次 match
- const responses = await cache.matchAll()
+ const requests = await cache.keys()
let cacheSize = 0
- for (const response of responses) {
- // 仅通过 Content-Length 获取大小
- const contentLength = response.headers.get('content-length')
- if (contentLength) {
- cacheSize += parseInt(contentLength, 10)
+ // 遍历请求以获取响应头部,避免 matchAll 一次性加载大量响应对象到内存
+ for (const request of requests) {
+ const response = await cache.match(request)
+ if (response) {
+ const contentLength = response.headers.get('content-length')
+ if (contentLength) {
+ cacheSize += parseInt(contentLength, 10)
+ }
}
}
-
cacheSizes[cacheName] = cacheSize
}),
)
- // 计算总大小
- totalSize = Object.values(cacheSizes).reduce((acc, size) => acc + size, 0)
+ calculatedTotalSize = Object.values(cacheSizes).reduce((acc, size) => acc + size, 0)
- // 获取存储估算
+ // 获取系统级存储估算
let quota = 0
let usage = 0
if (self.navigator.storage && self.navigator.storage.estimate) {
@@ -358,14 +357,18 @@ async function monitorCacheSize() {
usage = estimate.usage || 0
}
+ // 构造结果:满足 useCacheManager.ts 的需求
const result = {
cacheSizes,
- totalSize,
- totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
+ // 优先使用准确的 usage (真实磁盘占用),如果不可用则退回到计算值
+ totalSize: usage || calculatedTotalSize,
+ totalSizeMB: ((usage || calculatedTotalSize) / 1024 / 1024).toFixed(2),
+ // 额外信息保留,供未来扩展
quota,
usage,
quotaMB: (quota / 1024 / 1024).toFixed(2),
usageMB: (usage / 1024 / 1024).toFixed(2),
+ calculatedTotalSize,
}
// 发送缓存统计信息给客户端
From e84fc5f4244a46a573d79922f46ab55a28f09192 Mon Sep 17 00:00:00 2001
From: PKC278 <52959804+PKC278@users.noreply.github.com>
Date: Sat, 3 Jan 2026 22:32:52 +0800
Subject: [PATCH 7/9] Update src/service-worker.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
---
src/service-worker.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/service-worker.ts b/src/service-worker.ts
index 22b87d1f..ca7579d5 100644
--- a/src/service-worker.ts
+++ b/src/service-worker.ts
@@ -279,7 +279,7 @@ async function set(key: string, value: any, storeName: string = 'badge'): Promis
tx.onerror = () => reject(tx.error)
})
} catch (e) {
- // 忽略错误
+ console.error(`[SW] Failed to set IndexedDB key "${key}" in store "${storeName}":`, e)
}
}
From 94aaf83107948c5956a45714d259f42d6399db7b Mon Sep 17 00:00:00 2001
From: PKC278 <52959804+PKC278@users.noreply.github.com>
Date: Sat, 3 Jan 2026 22:36:25 +0800
Subject: [PATCH 8/9] =?UTF-8?q?fix(index):=20=E7=A7=BB=E9=99=A4=E5=A4=9A?=
=?UTF-8?q?=E4=BD=99=E5=88=A4=E6=96=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.html | 2 --
1 file changed, 2 deletions(-)
diff --git a/index.html b/index.html
index b343341c..f05a5787 100644
--- a/index.html
+++ b/index.html
@@ -324,8 +324,6 @@
msg = messages['zh-TW'];
} else if (lang.startsWith('en')) {
msg = messages['en-US'];
- } else if (lang.startsWith('zh')) {
- msg = messages['zh-CN'];
}
const textNode = document.createTextNode(msg.text + ' ');
From 7dd10f9c96e2947af65aa52e674b179a8c999099 Mon Sep 17 00:00:00 2001
From: PKC278 <52959804+PKC278@users.noreply.github.com>
Date: Sat, 3 Jan 2026 23:28:29 +0800
Subject: [PATCH 9/9] =?UTF-8?q?fix(VersionUpdateToast):=20=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E6=B6=88=E6=81=AF=E6=A0=B7=E5=BC=8F=E5=92=8C=E7=A7=BB?=
=?UTF-8?q?=E5=8A=A8=E7=AB=AF=E9=80=82=E9=85=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/toast/VersionUpdateToast.vue | 3 ++-
src/styles/main.scss | 27 +++++++++++++++++++++
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/src/components/toast/VersionUpdateToast.vue b/src/components/toast/VersionUpdateToast.vue
index 0f5bb784..dc13b757 100644
--- a/src/components/toast/VersionUpdateToast.vue
+++ b/src/components/toast/VersionUpdateToast.vue
@@ -36,7 +36,8 @@ const handleRefresh = () => {
.message {
flex: 1;
- white-space: nowrap;
+ word-break: break-all;
+ line-height: 1.4;
}
.refresh-button {
diff --git a/src/styles/main.scss b/src/styles/main.scss
index c0fac356..b7692a03 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -16,4 +16,31 @@
.version-update-toast-container {
min-width: unset !important;
width: fit-content !important;
+
+ // 移动端适配:强制靠右并修正位置
+ @media only screen and (width <= 600px) {
+ max-width: calc(100vw - 1rem) !important;
+ margin-inline: 0 0.5rem !important;
+ border-radius: 8px !important;
+ position: relative !important;
+ top: calc(100vh - 12rem) !important;
+ }
+}
+
+// 使用 :has 选择器精准控制包含更新通知的容器
+.Vue-Toastification__container:has(.version-update-toast-container) {
+ @media only screen and (width <= 600px) {
+ top: auto !important;
+ bottom: 0 !important;
+ display: flex !important;
+ flex-direction: column !important;
+ align-items: flex-end !important;
+ padding-block-end: 4.5rem !important;
+ pointer-events: none;
+
+ .version-update-toast-container {
+ pointer-events: auto;
+ margin-inline-end: 0.5rem !important;
+ }
+ }
}