From ec4500dcef9b871b709d1fa3e5bcb80506350199 Mon Sep 17 00:00:00 2001 From: PKC278 <52959804+PKC278@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:00:37 +0800 Subject: [PATCH 01/10] =?UTF-8?q?refactor(versionChecker):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E7=89=88=E6=9C=AC=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E9=80=9A=E7=9F=A5=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/toast/VersionUpdateToast.vue | 58 ++++++++++ src/composables/useVersionChecker.ts | 113 ++++++++++++++++++++ src/locales/en-US.ts | 1 + src/locales/zh-CN.ts | 1 + src/locales/zh-TW.ts | 1 + src/stores/global.ts | 7 ++ src/utils/globalSetting.ts | 60 +---------- 7 files changed, 182 insertions(+), 59 deletions(-) create mode 100644 src/components/toast/VersionUpdateToast.vue create mode 100644 src/composables/useVersionChecker.ts diff --git a/src/components/toast/VersionUpdateToast.vue b/src/components/toast/VersionUpdateToast.vue new file mode 100644 index 00000000..4498dd40 --- /dev/null +++ b/src/components/toast/VersionUpdateToast.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/src/composables/useVersionChecker.ts b/src/composables/useVersionChecker.ts new file mode 100644 index 00000000..e3b3535c --- /dev/null +++ b/src/composables/useVersionChecker.ts @@ -0,0 +1,113 @@ +import { ref, computed, h } from 'vue' +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__) +const serverVersion = ref(null) +const versionChecked = ref(false) +const needsUpdate = computed(() => { + return serverVersion.value !== null && serverVersion.value !== currentVersion.value +}) + +/** + * 版本检查 Composable + * + * 功能: + * - 检查前端版本与服务端版本是否一致 + * - 检测到版本更新时清除缓存和 Service Worker + * - 显示持久化更新通知 + */ +export function useVersionChecker() { + const toast = useToast() + + /** + * 清除所有缓存和 Service Worker + */ + const clearCachesAndServiceWorker = async (): Promise => { + try { + // 1. 清除所有缓存 + if ('caches' in window) { + const cacheNames = await caches.keys() + await Promise.all(cacheNames.map(name => caches.delete(name))) + console.log('[VersionChecker] 已清除所有缓存') + } + + // 2. 注销 Service Worker + if ('serviceWorker' in navigator) { + const registrations = await navigator.serviceWorker.getRegistrations() + await Promise.all(registrations.map(registration => registration.unregister())) + console.log('[VersionChecker] 已注销所有 Service Worker') + } + } catch (error) { + console.error('[VersionChecker] 清除缓存失败:', error) + } + } + + /** + * 显示版本更新通知(带刷新按钮) + */ + const showUpdateNotification = (): void => { + // 使用自定义 Vue 组件作为 toast 内容,传递翻译后的文本作为 props + const component = h(VersionUpdateToast, { + message: i18n.global.t('common.newVersionAvailable'), + refreshText: i18n.global.t('common.refresh'), + }) + + toast.info(component, { + timeout: false, // 不自动消失 + closeButton: false, + closeOnClick: false, + draggable: false, + }) + } + + /** + * 检查版本并在需要时显示更新通知 + * @param latestVersion 服务端返回的最新版本号 + */ + const checkVersion = async (latestVersion: string): Promise => { + // 如果已经检查过,则跳过 + if (versionChecked.value) { + return + } + + // 更新服务端版本 + serverVersion.value = latestVersion + + // 版本不同,且尚未显示通知 + if (needsUpdate.value) { + versionChecked.value = true + console.log(`[VersionChecker] 检测到版本更新: ${currentVersion.value} -> ${latestVersion}`) + + // 清除缓存和 Service Worker + await clearCachesAndServiceWorker() + + // 显示持久化通知 + showUpdateNotification() + } + } + + /** + * 重置版本检查状态(用于测试或特殊场景) + */ + const resetVersionCheck = (): void => { + versionChecked.value = false + serverVersion.value = null + } + + return { + // 状态 + currentVersion: computed(() => currentVersion.value), + serverVersion: computed(() => serverVersion.value), + needsUpdate, + versionChecked: computed(() => versionChecked.value), + // 方法 + checkVersion, + resetVersionCheck, + } +} diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 73660dbb..916e2d8e 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -66,6 +66,7 @@ export default { serviceUnavailable: 'Service Unavailable', status: 'Status', preset: 'Preset', + refresh: 'Refresh', newVersionAvailable: 'New version detected, please refresh the page to get the latest features', }, mediaType: { diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 114010f1..5d4de0a3 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -66,6 +66,7 @@ export default { serviceUnavailable: '服务不可用', status: '状态', preset: '预设', + refresh: '刷新', newVersionAvailable: '检测到新版本,请刷新页面以获取最新功能', }, mediaType: { diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index da49cc34..60206381 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -66,6 +66,7 @@ export default { serviceUnavailable: '服務不可用', status: '狀態', preset: '預設', + refresh: '刷新', newVersionAvailable: '檢測到新版本,請刷新頁面以獲取最新功能', }, mediaType: { diff --git a/src/stores/global.ts b/src/stores/global.ts index 13eac221..50cce208 100644 --- a/src/stores/global.ts +++ b/src/stores/global.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia' import type { globalSettingsState } from '@/stores/types' import { fetchGlobalSettings } from '@/utils/globalSetting' +import { useVersionChecker } from '@/composables/useVersionChecker' export const useGlobalSettingsStore = defineStore('globalSettings', { state: (): globalSettingsState => ({ @@ -18,6 +19,12 @@ export const useGlobalSettingsStore = defineStore('globalSettings', { const result = await fetchGlobalSettings() this.data = result || {} this.initialized = true + + // 检查版本更新 + if (result.FRONTEND_VERSION) { + const { checkVersion } = useVersionChecker() + await checkVersion(result.FRONTEND_VERSION) + } } catch (error) { console.error('Failed to initialize global settings', error) } finally { diff --git a/src/utils/globalSetting.ts b/src/utils/globalSetting.ts index bcde0e96..f1586b02 100644 --- a/src/utils/globalSetting.ts +++ b/src/utils/globalSetting.ts @@ -1,58 +1,8 @@ import api from '@/api' -import { useToast } from 'vue-toastification' -import i18n from '@/plugins/i18n' // 创建一个专用的AbortController,用于globalSetting请求 const globalSettingController = new AbortController() -// 声明全局变量类型 -declare const __APP_VERSION__: string - -// 当前前端版本号(由 Vite 在编译时注入) -const CURRENT_FRONTEND_VERSION = __APP_VERSION__ -console.log(`[VersionChecker] 当前前端版本: ${CURRENT_FRONTEND_VERSION}`) - -// 标记是否已经显示过版本更新通知 -let versionNotificationShown = false - -/** - * 检查版本并在需要时显示更新通知 - */ -async function checkVersionAndNotify(serverVersion: string): Promise { - // 版本不同,且尚未显示通知 - if (serverVersion !== CURRENT_FRONTEND_VERSION && !versionNotificationShown) { - versionNotificationShown = true - console.log(`[VersionChecker] 检测到版本更新: ${CURRENT_FRONTEND_VERSION} -> ${serverVersion}`) - - try { - // 1. 清除所有缓存 - if ('caches' in window) { - const cacheNames = await caches.keys() - await Promise.all(cacheNames.map(name => caches.delete(name))) - console.log('[VersionChecker] 已清除所有缓存') - } - - // 2. 注销Service Worker - if ('serviceWorker' in navigator) { - const registrations = await navigator.serviceWorker.getRegistrations() - await Promise.all(registrations.map(registration => registration.unregister())) - console.log('[VersionChecker] 已注销所有 Service Worker') - } - } catch (error) { - console.error('[VersionChecker] 清除缓存失败:', error) - } - - // 3. 显示持久化通知,提示用户刷新 - const toast = useToast() - toast.info(i18n.global.t('common.newVersionAvailable'), { - timeout: false, // 不自动消失 - closeButton: false, - closeOnClick: false, - draggable: false, - }) - } -} - export async function fetchGlobalSettings() { try { const result: { [key: string]: any } = await api.get('system/global', { @@ -62,15 +12,7 @@ export async function fetchGlobalSettings() { // 手动设置signal,防止reqestOptimizer添加可中断的controller signal: globalSettingController.signal, }) - - const data = result.data || {} - - // 检查版本更新 - if (data.FRONTEND_VERSION) { - await checkVersionAndNotify(data.FRONTEND_VERSION) - } - - return data + return result.data || {} } catch (error) { console.error('Failed to fetch global settings', error) throw error From 6b8ed8d527e7bece5cc2b599501a2053a27788d1 Mon Sep 17 00:00:00 2001 From: PKC278 <52959804+PKC278@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:54:13 +0800 Subject: [PATCH 02/10] =?UTF-8?q?fix(vite):=20=E6=B6=88=E9=99=A4=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@core/scss/_mixins.scss | 2 +- vite.config.ts | 1 + yarn.lock | 8 +++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/@core/scss/_mixins.scss b/src/@core/scss/_mixins.scss index bdf641e9..301ed153 100644 --- a/src/@core/scss/_mixins.scss +++ b/src/@core/scss/_mixins.scss @@ -1,5 +1,5 @@ @use "sass:map"; -@use "vuetify/lib/styles/settings" as vuetify_settings; +@use "vuetify/lib/styles/settings/_index.sass" as vuetify_settings; @use "@styles/variables/_vuetify.scss" as vuetify; @mixin themed($property, $light-value, $dark-value) { diff --git a/vite.config.ts b/vite.config.ts index b3eaba14..8e6dc8ac 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -331,6 +331,7 @@ export default defineConfig({ css: { preprocessorOptions: { scss: { + api: 'modern-compiler', quietDeps: true, }, }, diff --git a/yarn.lock b/yarn.lock index fb893731..a8a89b83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3017,9 +3017,9 @@ camelcase-css@^2.0.1: integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702: - version "1.0.30001715" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz" - integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw== + version "1.0.30001761" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz" + integrity sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g== chalk@^4.0.0, chalk@^4.0.2: version "4.1.2" @@ -6856,6 +6856,7 @@ std-env@^3.9.0: integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6941,6 +6942,7 @@ stringify-object@^3.3.0: is-regexp "^1.0.0" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== From b98512789fcd7ec2c272328deb3985a3fb0301ad Mon Sep 17 00:00:00 2001 From: PKC278 <52959804+PKC278@users.noreply.github.com> Date: Tue, 30 Dec 2025 01:42:43 +0800 Subject: [PATCH 03/10] =?UTF-8?q?feat(uiMode):=20=E6=B7=BB=E5=8A=A0UI?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=89=8B=E5=8A=A8=E5=88=87=E6=8D=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/usePWA.ts | 14 +++++ src/composables/usePullDownGesture.ts | 9 ++- src/layouts/components/UserProfile.vue | 81 +++++++++++++++++++++++++- src/locales/en-US.ts | 2 + src/locales/zh-CN.ts | 2 + src/locales/zh-TW.ts | 2 + 6 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/composables/usePWA.ts b/src/composables/usePWA.ts index 22077622..7007cd7e 100644 --- a/src/composables/usePWA.ts +++ b/src/composables/usePWA.ts @@ -12,6 +12,16 @@ const globalPwaStatus = ref<{ const globalLoading = ref(false) let initPromise: Promise | null = null +// UI模式设置 +export type UIMode = 'auto' | 'desktop' | 'app' +const uiMode = ref((localStorage.getItem('ui-mode') as UIMode) || 'auto') + +// 设置UI模式 +function setUIMode(mode: UIMode) { + uiMode.value = mode + localStorage.setItem('ui-mode', mode) +} + // 全局初始化函数 async function initializePWAGlobally() { if (initPromise) return initPromise @@ -50,6 +60,8 @@ export function usePWA() { }) const appMode = computed(() => { + if (uiMode.value === 'app') return true + if (uiMode.value === 'desktop') return false return pwaMode.value && display.mdAndDown.value }) @@ -70,6 +82,8 @@ export function usePWA() { pwaMode, appMode, pwaStatus, + uiMode, + setUIMode, loading: globalLoading, initializePWA: initializePWAGlobally, } diff --git a/src/composables/usePullDownGesture.ts b/src/composables/usePullDownGesture.ts index 787329bb..439b3947 100644 --- a/src/composables/usePullDownGesture.ts +++ b/src/composables/usePullDownGesture.ts @@ -236,16 +236,15 @@ export function usePullDownGesture(options: PullDownOptions = {}) { } } - // PWA状态确定后,一次性决定是否添加事件监听器 + // 监听 appMode 变化动态添加/移除事件监听器 onMounted(() => { - // 等待PWA检测完成后添加事件监听器 - const stopWatcher = watch( + watch( appMode, newValue => { if (newValue) { addEventListeners() - // PWA状态确定后停止监听 - stopWatcher() + } else { + removeEventListeners() } }, { immediate: true }, diff --git a/src/layouts/components/UserProfile.vue b/src/layouts/components/UserProfile.vue index 233626d4..2f764178 100644 --- a/src/layouts/components/UserProfile.vue +++ b/src/layouts/components/UserProfile.vue @@ -16,6 +16,7 @@ import { saveLocalTheme } from '@/@core/utils/theme' import type { ThemeSwitcherTheme } from '@layouts/types' import { useConfirm } from '@/composables/useConfirm' import { themeManager } from '@/utils/themeManager' +import { usePWA, type UIMode } from '@/composables/usePWA' // 认证 Store const authStore = useAuthStore() @@ -27,6 +28,8 @@ const globalSettingsStore = useGlobalSettingsStore() const { t } = useI18n() // 显示器 const display = useDisplay() +// PWA +const { uiMode, setUIMode } = usePWA() // 提示框 const $toast = useToast() @@ -40,6 +43,9 @@ const siteAuthDialog = ref(false) // 自定义CSS弹窗 const cssDialog = ref(false) +// UI模式菜单是否显示 +const showUIModeMenu = ref(false) + // 主题菜单是否显示 const showThemeMenu = ref(false) @@ -233,6 +239,39 @@ const isAdvancedMode = computed(() => { return globalSettingsStore.get('ADVANCED_MODE') !== false }) +// UI模式相关 +const uiModes = computed(() => [ + { + name: 'auto', + title: t('theme.autoUI'), + icon: 'mdi-devices', + }, + { + name: 'desktop', + title: t('pwa.platforms.desktop'), + icon: 'mdi-monitor', + }, + { + name: 'app', + title: t('pwa.platforms.mobile'), + icon: 'mdi-cellphone', + }, +]) + +// 切换UI模式 +function changeUIMode(mode: UIMode) { + setUIMode(mode) + showUIModeMenu.value = false + // 刷新页面以应用更改 + window.location.reload() +} + +// 获取当前UI模式图标 +const getUIModeIcon = computed(() => { + const mode = uiModes.value.find(m => m.name === uiMode.value) + return mode?.icon || 'mdi-devices' +}) + // 主题相关功能 const { name: themeName, global: globalTheme } = useTheme() const savedTheme = ref(localStorage.getItem('theme') ?? themeName) @@ -546,6 +585,41 @@ onUnmounted(() => { {{ t('user.siteAuth') }} + + + + + + + {{ mode.title }} + + + + +