mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 10:00:08 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01835c0ac5 | ||
|
|
e5749bd6ef | ||
|
|
689e58737b | ||
|
|
38da061cf1 | ||
|
|
e79940e52e |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "2.9.1",
|
"version": "2.9.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { ref, computed, h } from 'vue'
|
import { ref, h } from 'vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { Workbox } from 'workbox-window'
|
||||||
import i18n from '@/plugins/i18n'
|
import i18n from '@/plugins/i18n'
|
||||||
import VersionUpdateToast from '@/components/toast/VersionUpdateToast.vue'
|
import VersionUpdateToast from '@/components/toast/VersionUpdateToast.vue'
|
||||||
|
|
||||||
// 全局状态
|
// 全局状态
|
||||||
const currentVersion = ref(__APP_VERSION__)
|
const currentVersion = ref(__APP_VERSION__)
|
||||||
let isListenerAdded = false
|
let isUpdateToastShown = false
|
||||||
let notificationShowTime = 0
|
let wb: Workbox | null = null
|
||||||
const serverVersion = ref<string | null>(null)
|
|
||||||
const versionChecked = ref(false)
|
/**
|
||||||
const needsUpdate = computed(() => {
|
* 普通刷新页面
|
||||||
return serverVersion.value !== null && serverVersion.value !== currentVersion.value
|
*/
|
||||||
})
|
export const reloadPage = (): void => {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新页面并添加时间戳
|
* 刷新页面并添加时间戳
|
||||||
@@ -45,26 +48,38 @@ export const clearCachesAndServiceWorker = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存并刷新
|
||||||
|
*/
|
||||||
|
const clearCacheAndReload = async (): Promise<void> => {
|
||||||
|
await clearCachesAndServiceWorker()
|
||||||
|
reloadWithTimestamp()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 版本检查 Composable
|
* 版本检查 Composable
|
||||||
*
|
*
|
||||||
* 功能:
|
* 功能:
|
||||||
* - 检查前端版本与服务端版本是否一致
|
* - 使用 Workbox 监听 Service Worker 更新
|
||||||
* - 检测到版本更新时清除缓存和 Service Worker
|
* - 检查浏览器版本与服务端版本是否一致
|
||||||
* - 显示持久化更新通知
|
* - 显示持久化更新通知
|
||||||
*/
|
*/
|
||||||
export function useVersionChecker() {
|
export function useVersionChecker() {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示版本更新通知(带刷新按钮)
|
* 显示版本更新通知
|
||||||
|
* @param message 通知消息文本
|
||||||
|
* @param refreshText 按钮文本,不传则不显示按钮
|
||||||
|
* @param onRefresh 按钮点击事件
|
||||||
*/
|
*/
|
||||||
const showUpdateNotification = (): void => {
|
const showUpdateNotification = (message: string, refreshText?: string, onRefresh?: () => void): void => {
|
||||||
// 使用自定义 Vue 组件作为 toast 内容,传递翻译后的文本作为 props
|
if (isUpdateToastShown) return
|
||||||
|
isUpdateToastShown = true
|
||||||
const component = h(VersionUpdateToast, {
|
const component = h(VersionUpdateToast, {
|
||||||
message: i18n.global.t('common.newVersionAvailable'),
|
message,
|
||||||
refreshText: i18n.global.t('common.refresh'),
|
refreshText,
|
||||||
onRefresh: reloadWithTimestamp,
|
onRefresh,
|
||||||
})
|
})
|
||||||
|
|
||||||
toast.info(component, {
|
toast.info(component, {
|
||||||
@@ -72,105 +87,88 @@ export function useVersionChecker() {
|
|||||||
closeButton: false,
|
closeButton: false,
|
||||||
closeOnClick: false,
|
closeOnClick: false,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
toastClassName: 'version-update-toast-container',
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化 Workbox
|
||||||
|
if (!wb && 'serviceWorker' in navigator) {
|
||||||
|
wb = new Workbox('/service-worker.js')
|
||||||
|
|
||||||
|
// Service Worker 激活事件 (install -> activate)
|
||||||
|
wb.addEventListener('activated', event => {
|
||||||
|
// 只有在更新时才显示通知
|
||||||
|
if (event.isUpdate) {
|
||||||
|
console.log('[VersionChecker] Service Worker 更新已就绪,等待用户刷新')
|
||||||
|
|
||||||
|
showUpdateNotification(i18n.global.t('common.swUpdateReady'), i18n.global.t('common.refresh'), reloadPage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注册 Service Worker
|
||||||
|
wb.register()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查版本并在需要时显示更新通知
|
* 检查版本并在需要时显示更新通知
|
||||||
* @param latestVersion 服务端返回的最新版本号
|
* @param latestVersion 服务端返回的最新版本号
|
||||||
*/
|
*/
|
||||||
const checkVersion = async (latestVersion: string): Promise<void> => {
|
const checkVersion = async (latestVersion: string): Promise<void> => {
|
||||||
// 如果已经检查过,则跳过
|
// 如果已经显示过通知,说明已经检查过了
|
||||||
if (versionChecked.value) {
|
if (isUpdateToastShown) return
|
||||||
|
|
||||||
|
// 版本一致,无需操作
|
||||||
|
if (latestVersion === currentVersion.value) {
|
||||||
|
console.log('[VersionChecker] 版本号一致,无需操作')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新服务端版本
|
console.log(`[VersionChecker] 检测到版本不一致: ${currentVersion.value} -> ${latestVersion}`)
|
||||||
serverVersion.value = latestVersion
|
|
||||||
|
|
||||||
// 执行版本不一致时的处理逻辑
|
// 尝试触发 Service Worker 更新检查
|
||||||
const handleVersionMismatch = async () => {
|
|
||||||
if (needsUpdate.value) {
|
|
||||||
versionChecked.value = true
|
|
||||||
console.log(`[VersionChecker] 检测到版本更新: ${currentVersion.value} -> ${latestVersion}`)
|
|
||||||
|
|
||||||
// 清除缓存和 Service Worker
|
|
||||||
await clearCachesAndServiceWorker()
|
|
||||||
|
|
||||||
// 显示持久化通知
|
|
||||||
showUpdateNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先尝试通过 Service Worker 检查更新
|
|
||||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||||
console.log('[VersionChecker] 正在请求 Service Worker 检查更新...')
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.getRegistration()
|
||||||
|
if (registration) {
|
||||||
|
console.log('[VersionChecker] 触发 Service Worker 更新检查...')
|
||||||
|
|
||||||
const registration = await navigator.serviceWorker.getRegistration()
|
// 标记是否发现更新
|
||||||
|
let updateFound = false
|
||||||
|
const onUpdateFound = () => {
|
||||||
|
updateFound = true
|
||||||
|
}
|
||||||
|
|
||||||
// 如果已经有等待中的更新,直接处理
|
// 监听 updatefound 事件
|
||||||
if (registration?.waiting) {
|
registration.addEventListener('updatefound', onUpdateFound, { once: true })
|
||||||
console.log('[VersionChecker] Service Worker 发现新版本,跳过版本号对比')
|
|
||||||
handleVersionMismatch()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageChannel = new MessageChannel()
|
// 等待检查完成
|
||||||
|
await registration.update()
|
||||||
|
|
||||||
messageChannel.port1.onmessage = event => {
|
// 检查是否有更新正在进行
|
||||||
if (event.data && event.data.type === 'SW_NO_UPDATE_DETECTED') {
|
// 如果发现更新,或者正在安装/等待中,则直接返回(交由 SW activated 事件处理)
|
||||||
console.log('[VersionChecker] Service Worker 报告无更新, 进行版本号检查...')
|
if (updateFound || registration.installing || registration.waiting) {
|
||||||
handleVersionMismatch()
|
console.log('[VersionChecker] Service Worker 更新中...')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[VersionChecker] SW 无更新,但版本号不一致,可能是缓存问题')
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[VersionChecker] Service Worker 更新检查失败:', error)
|
||||||
|
// 失败继续向下执行,显示通知
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.serviceWorker.controller.postMessage({ type: 'CHECK_SW_UPDATE' }, [messageChannel.port2])
|
|
||||||
} else {
|
} else {
|
||||||
// 如果没有 Service Worker 控制,直接进行版本比较
|
console.log('[VersionChecker] 无 Service Worker, 直接显示通知')
|
||||||
await handleVersionMismatch()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 监听 Service Worker 版本更新消息
|
// 最终兜底:显示版本不一致通知(清除缓存)
|
||||||
if (!isListenerAdded && 'serviceWorker' in navigator) {
|
showUpdateNotification(
|
||||||
navigator.serviceWorker.addEventListener('message', event => {
|
i18n.global.t('common.versionMismatch'),
|
||||||
// 1. 发现新版本 -> 弹出通知
|
i18n.global.t('common.clearCache'),
|
||||||
if (event.data && event.data.type === 'SW_VERSION_DETECTED') {
|
clearCacheAndReload,
|
||||||
console.log('[VersionChecker] 发现新版本:', event.data.version)
|
)
|
||||||
notificationShowTime = Date.now()
|
|
||||||
|
|
||||||
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, 1500 - elapsed)
|
|
||||||
console.log(`[VersionChecker] 更新已安装, 延迟 ${delay}ms 后刷新...`)
|
|
||||||
setTimeout(() => {
|
|
||||||
reloadWithTimestamp()
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
isListenerAdded = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
|
||||||
currentVersion: computed(() => currentVersion.value),
|
|
||||||
serverVersion: computed(() => serverVersion.value),
|
|
||||||
needsUpdate,
|
|
||||||
versionChecked: computed(() => versionChecked.value),
|
|
||||||
// 方法
|
|
||||||
checkVersion,
|
checkVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ export default {
|
|||||||
status: 'Status',
|
status: 'Status',
|
||||||
preset: 'Preset',
|
preset: 'Preset',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
newVersionAvailable: 'New version detected, please refresh the page to get the latest features',
|
swUpdateReady: 'New version is ready, please refresh the page to get the latest features',
|
||||||
newVersionFound: 'New version found, updating...',
|
versionMismatch: 'Browser cache version does not match server version, please try clearing cache',
|
||||||
|
clearCache: 'Clear Cache',
|
||||||
},
|
},
|
||||||
mediaType: {
|
mediaType: {
|
||||||
movie: 'Movie',
|
movie: 'Movie',
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ export default {
|
|||||||
status: '状态',
|
status: '状态',
|
||||||
preset: '预设',
|
preset: '预设',
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
newVersionAvailable: '检测到新版本,请刷新页面以获取最新功能',
|
swUpdateReady: '新版本已就绪,请刷新页面以获取最新功能',
|
||||||
newVersionFound: '发现新版本,正在更新...',
|
versionMismatch: '浏览器缓存版本与服务端版本不一致,请尝试清除缓存',
|
||||||
|
clearCache: '清除缓存',
|
||||||
},
|
},
|
||||||
mediaType: {
|
mediaType: {
|
||||||
movie: '电影',
|
movie: '电影',
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ export default {
|
|||||||
status: '狀態',
|
status: '狀態',
|
||||||
preset: '預設',
|
preset: '預設',
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
newVersionAvailable: '檢測到新版本,請刷新頁面以獲取最新功能',
|
swUpdateReady: '新版本已就緒,請刷新頁面以獲取最新功能',
|
||||||
newVersionFound: '發現新版本,正在更新...',
|
versionMismatch: '瀏覽器快取版本與伺服器版本不一致,請嘗試清除快取',
|
||||||
|
clearCache: '清除快取',
|
||||||
},
|
},
|
||||||
mediaType: {
|
mediaType: {
|
||||||
movie: '電影',
|
movie: '電影',
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ cleanupOutdatedCaches()
|
|||||||
// 预缓存并路由
|
// 预缓存并路由
|
||||||
precacheAndRoute(self.__WB_MANIFEST)
|
precacheAndRoute(self.__WB_MANIFEST)
|
||||||
|
|
||||||
// 变量记录是否为更新安装
|
// 变量记录是否为更新安装(兼容旧版前端监听逻辑)
|
||||||
let isUpdate = false
|
let isUpdate = false
|
||||||
|
|
||||||
// 监听安装事件以检测更新
|
// 监听安装事件
|
||||||
self.addEventListener('install', () => {
|
self.addEventListener('install', () => {
|
||||||
// 强制等待中的 Service Worker 立即激活
|
// 强制等待中的 Service Worker 立即激活
|
||||||
self.skipWaiting()
|
self.skipWaiting()
|
||||||
|
|
||||||
// 检查是否是更新(即是否已经有激活的 Service Worker)
|
// 检查是否是更新(兼容旧版前端监听逻辑)
|
||||||
if (self.registration.active) {
|
if (self.registration.active) {
|
||||||
isUpdate = true
|
isUpdate = true
|
||||||
// 通知客户端发现新版本
|
// 通知客户端发现新版本
|
||||||
@@ -56,7 +56,7 @@ self.addEventListener('activate', event => {
|
|||||||
// 清理旧版本的运行时缓存
|
// 清理旧版本的运行时缓存
|
||||||
await cleanupRuntimeCaches(true)
|
await cleanupRuntimeCaches(true)
|
||||||
|
|
||||||
// 如果是更新,则通知客户端刷新页面
|
// 如果是更新,则通知客户端刷新页面(兼容旧版前端监听逻辑)
|
||||||
if (isUpdate) {
|
if (isUpdate) {
|
||||||
const clients = await self.clients.matchAll({ type: 'window' })
|
const clients = await self.clients.matchAll({ type: 'window' })
|
||||||
clients.forEach(client => {
|
clients.forEach(client => {
|
||||||
@@ -492,20 +492,6 @@ self.addEventListener('message', function (event) {
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(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') {
|
} else if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
self.skipWaiting()
|
self.skipWaiting()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,36 +11,3 @@
|
|||||||
@import 'vue-toastification/dist/index.css';
|
@import 'vue-toastification/dist/index.css';
|
||||||
@import 'vue3-perfect-scrollbar/style.css';
|
@import 'vue3-perfect-scrollbar/style.css';
|
||||||
@import '@vue-js-cron/vuetify/dist/vuetify.css';
|
@import '@vue-js-cron/vuetify/dist/vuetify.css';
|
||||||
|
|
||||||
/* 版本更新通知专用样式 */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user