diff --git a/index.html b/index.html index a527550e..286dd6ea 100644 --- a/index.html +++ b/index.html @@ -250,108 +250,7 @@
diff --git a/shims.d.ts b/shims.d.ts index 35041540..c044f34a 100644 --- a/shims.d.ts +++ b/shims.d.ts @@ -12,3 +12,4 @@ declare module 'vue-prism-component' { export default component } declare module 'vue-shepherd'; +declare module 'colorthief'; 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/src/@validators/index.ts b/src/@validators/index.ts index 58cef60b..0ce5a144 100644 --- a/src/@validators/index.ts +++ b/src/@validators/index.ts @@ -1,4 +1,4 @@ -import type { ValidationRule } from 'vuetify/types/services/validation' +type ValidationRule = (value: any) => string | boolean // 必输校验 export const requiredValidator: ValidationRule = (value: any) => { diff --git a/src/components/FileBrowser.vue b/src/components/FileBrowser.vue index c8f1817c..eec6b6a5 100644 --- a/src/components/FileBrowser.vue +++ b/src/components/FileBrowser.vue @@ -4,6 +4,7 @@ import FileToolbar from './filebrowser/FileToolbar.vue' import FileNavigator from './filebrowser/FileNavigator.vue' import type { EndPoints, FileItem, StorageConf } from '@/api/types' import { storageIconDict } from '@/api/constants' +import type { AxiosInstance } from 'axios' // LocalStorage keys const SORT_KEY = 'fileBrowser.sort' @@ -16,7 +17,7 @@ const props = defineProps({ tree: Boolean, endpoints: Object as PropType, axios: { - type: Function, + type: Object as PropType, required: true, }, axiosconfig: Object, diff --git a/src/components/dialog/AboutDialog.vue b/src/components/dialog/AboutDialog.vue index a1a80c37..853ea2a7 100644 --- a/src/components/dialog/AboutDialog.vue +++ b/src/components/dialog/AboutDialog.vue @@ -1,12 +1,18 @@ + + 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/composables/useVersionChecker.ts b/src/composables/useVersionChecker.ts new file mode 100644 index 00000000..2f2d1756 --- /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 +}) + +/** + * 清除所有缓存和 Service Worker + */ +export 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) + } +} + +/** + * 版本检查 Composable + * + * 功能: + * - 检查前端版本与服务端版本是否一致 + * - 检测到版本更新时清除缓存和 Service Worker + * - 显示持久化更新通知 + */ +export function useVersionChecker() { + const toast = useToast() + + /** + * 显示版本更新通知(带刷新按钮) + */ + 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/layouts/components/Footer.vue b/src/layouts/components/Footer.vue index 7d54be75..2009389b 100644 --- a/src/layouts/components/Footer.vue +++ b/src/layouts/components/Footer.vue @@ -328,6 +328,7 @@ const showDynamicButton = computed(() => { block-size: auto; inline-size: auto; min-block-size: 0; + max-width: 60px; // 限制最大宽度以便动画 .footer-card-content { padding: 3px; @@ -358,14 +359,12 @@ const showDynamicButton = computed(() => { transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1); } -.fade-slide-enter-from { - opacity: 0; - transform: translateX(20px); -} - +.fade-slide-enter-from, .fade-slide-leave-to { opacity: 0; transform: translateX(20px); + max-width: 0; + margin-inline-start: 0 !important; } @keyframes fade-in { diff --git a/src/layouts/components/UserProfile.vue b/src/layouts/components/UserProfile.vue index 233626d4..80ad008c 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,37 @@ 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 +} + +// 获取当前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 +583,41 @@ onUnmounted(() => { {{ t('user.siteAuth') }} + + + + + + + {{ mode.title }} + + + + +