From 58a3532c1bed8f2f34065c21af28fdcc593e458d Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 23 Dec 2025 12:53:07 +0800 Subject: [PATCH 01/44] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20package.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8eee3b2a..0a4509b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moviepilot", - "version": "2.8.8", + "version": "2.8.9", "private": true, "type": "module", "bin": "dist/service.js", From e77dcdd3d4e5272c5a40ebbe4c4f12839ff9cf5e Mon Sep 17 00:00:00 2001 From: PKC278 <52959804+PKC278@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:53:55 +0800 Subject: [PATCH 02/44] =?UTF-8?q?feat(passkey):=20=E6=B7=BB=E5=8A=A0PassKe?= =?UTF-8?q?y=E6=94=AF=E6=8C=81=E5=B9=B6=E4=BC=98=E5=8C=96=E5=8F=8C?= =?UTF-8?q?=E9=87=8D=E9=AA=8C=E8=AF=81=E7=99=BB=E5=BD=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/en-US.ts | 31 ++- src/locales/zh-CN.ts | 31 ++- src/locales/zh-TW.ts | 31 ++- src/pages/login.vue | 406 +++++++++++++++++++++++++++-- src/views/user/UserProfileView.vue | 400 +++++++++++++++++++++++++--- 5 files changed, 827 insertions(+), 72 deletions(-) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index f8550b56..25553b82 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -2560,9 +2560,38 @@ export default { vocechatUser: 'VoceChat User', synologychatUser: 'SynologyChat User', doubanUser: 'Douban User', - twoFactorAuthentication: 'Two-Factor Authentication', + setupAuthenticator: 'Setup Authenticator', + authenticatorManagement: 'Authenticator Management', + authenticatorEnabled: 'You have enabled authenticator two-factor authentication', + clearAuthenticatorTip: 'To set up a new authenticator, please clear the current configuration first.', + clearAuthenticator: 'Clear Authenticator', enableTwoFactor: 'Enable Two-Factor Authentication', disableTwoFactor: 'Disable Two-Factor Authentication', + setupMfa: 'Setup Two-Factor Authentication', + enableMfa: 'Enable Two-Factor Authentication', + useAuthenticator: 'Use Authenticator', + usePasskey: 'Use Passkey', + enabled: 'Enabled', + keysCount: '{count} keys', + passkeyManagement: 'Passkey Management', + registerNewPasskey: 'Register New Passkey', + passkeyDescription: 'Passkeys allow you to sign in quickly and securely without a password.', + passkeyName: 'Passkey Name', + passkeyNamePlaceholder: 'e.g.: iPhone, Windows Hello', + registerPasskey: 'Register Passkey', + registeredPasskeys: 'Registered Passkeys', + createdAt: 'Created At', + noPasskeys: 'You have not registered any passkeys yet', + passkeyNameRequired: 'Please enter a passkey name', + passkeyRegisterSuccess: 'Passkey registered successfully', + passkeyRegisterFailed: 'Registration failed', + passkeyRegisterCancelled: 'Registration cancelled', + passkeyDeleteSuccess: 'Passkey deleted', + passkeyDeleteFailed: 'Delete failed', + passkeyDomainWarning: 'The availability of PassKeys is closely related to the {domain}. In a public network environment, please make sure to configure the correct access domain name in "Basic Settings". Domain changes or configuration errors will cause the PassKey to be unusable.', + otpRequiredForPasskey: 'For security reasons, you must first enable {otp} before you can register a PassKey. This is to ensure that you can still log in to your account via OTP code if the PassKey becomes invalid due to domain configuration changes.', + accessDomain: 'access domain name', + otpAuthenticator: 'OTP Authenticator', otpGenerateFailed: 'Failed to get OTP URI: {message}!', otpDisableSuccess: 'Two-factor authentication disabled successfully!', otpDisableFailed: 'Failed to disable OTP: {message}!', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 670049a6..5ddbbf57 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -2529,9 +2529,38 @@ export default { vocechatUser: 'VoceChat用户', synologychatUser: 'SynologyChat用户', doubanUser: '豆瓣用户', - twoFactorAuthentication: '登录双重验证', + setupAuthenticator: '设置身份验证器', + authenticatorManagement: '身份验证器管理', + authenticatorEnabled: '您已启用身份验证器双重验证', + clearAuthenticatorTip: '如需设置新的身份验证器,请先清除当前配置。', + clearAuthenticator: '清除身份验证器', enableTwoFactor: '开启双重验证', disableTwoFactor: '关闭双重验证', + setupMfa: '设置双重验证', + enableMfa: '开启双重验证', + useAuthenticator: '使用身份验证器', + usePasskey: '使用通行密钥', + enabled: '已启用', + keysCount: '{count} 个密钥', + passkeyManagement: '通行密钥管理', + registerNewPasskey: '注册新通行密钥', + passkeyDescription: '通行密钥可以让您无需密码即可快速安全地登录。', + passkeyName: '通行密钥名称', + passkeyNamePlaceholder: '例如:iPhone、Windows Hello', + registerPasskey: '注册通行密钥', + registeredPasskeys: '已注册的通行密钥', + createdAt: '创建时间', + noPasskeys: '您还没有注册任何通行密钥', + passkeyNameRequired: '请输入通行密钥名称', + passkeyRegisterSuccess: '通行密钥注册成功', + passkeyRegisterFailed: '注册失败', + passkeyRegisterCancelled: '注册被取消', + passkeyDeleteSuccess: '通行密钥已删除', + passkeyDeleteFailed: '删除失败', + passkeyDomainWarning: '通行密钥(PassKey)的可用性与 {domain} 紧密相关。在公网环境下,请务必在“基础设置”中配置正确的访问域名。域名变更或配置错误将导致通行密钥无法使用。', + otpRequiredForPasskey: '为了安全起见,您必须先启用 {otp} 验证码,然后才能注册通行密钥。这是为了防止在域名配置变动导致 PassKey 失效时,您仍能通过 OTP 码登录账户。', + accessDomain: '访问域名', + otpAuthenticator: 'OTP 身份验证器', otpGenerateFailed: '获取otp uri失败:{message}!', otpDisableSuccess: '关闭登录双重验证成功!', otpDisableFailed: '关闭otp失败:{message}!', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 90cdfb70..efe3c3e4 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -2515,9 +2515,38 @@ export default { vocechatUser: 'VoceChat用戶', synologychatUser: 'SynologyChat用戶', doubanUser: '豆瓣用戶', - twoFactorAuthentication: '登錄雙重驗證', + setupAuthenticator: '設置身份驗證器', + authenticatorManagement: '身份驗證器管理', + authenticatorEnabled: '您已啟用身份驗證器雙重驗證', + clearAuthenticatorTip: '如需設置新的身份驗證器,請先清除當前配置。', + clearAuthenticator: '清除身份驗證器', enableTwoFactor: '開啟雙重驗證', disableTwoFactor: '關閉雙重驗證', + setupMfa: '設置雙重驗證', + enableMfa: '開啟雙重驗證', + useAuthenticator: '使用身份驗證器', + usePasskey: '使用通行密鑰', + enabled: '已啟用', + keysCount: '{count} 個密鑰', + passkeyManagement: '通行密鑰管理', + registerNewPasskey: '註冊新通行密鑰', + passkeyDescription: '通行密鑰可以讓您無需密碼即可快速安全地登入。', + passkeyName: '通行密鑰名稱', + passkeyNamePlaceholder: '例如:iPhone、Windows Hello', + registerPasskey: '註冊通行密鑰', + registeredPasskeys: '已註冊的通行密鑰', + createdAt: '建立時間', + noPasskeys: '您還沒有註冊任何通行密鑰', + passkeyNameRequired: '請輸入通行密鑰名稱', + passkeyRegisterSuccess: '通行密鑰註冊成功', + passkeyRegisterFailed: '註冊失敗', + passkeyRegisterCancelled: '註冊被取消', + passkeyDeleteSuccess: '通行密鑰已刪除', + passkeyDeleteFailed: '刪除失敗', + passkeyDomainWarning: '通行密鑰(PassKey)的可用性與 {domain} 緊密相關。在公網環境下,請務必在「基本設定」中配置正確的訪問域名。域名變更或配置錯誤將導致通行密鑰無法使用。', + otpRequiredForPasskey: '為了安全起見,您必須先啟用 {otp} 驗證碼,然後才能註冊通行密鑰。這是為了防止在網域配置變動導致 PassKey 失效時,您仍能通過 OTP 碼登入帳戶。', + accessDomain: '訪問域名', + otpAuthenticator: 'OTP 身份驗證器', otpGenerateFailed: '獲取otp uri失敗:{message}!', otpDisableSuccess: '關閉登錄雙重驗證成功!', otpDisableFailed: '關閉otp失敗:{message}!', diff --git a/src/pages/login.vue b/src/pages/login.vue index cba49a81..2b6a459b 100644 --- a/src/pages/login.vue +++ b/src/pages/login.vue @@ -1,5 +1,4 @@ + + 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 10/44] =?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 11/44] =?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 }} + + + + +