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 @@ @@ -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; + } + } }