diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 9e7c7cc..530d8f8 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -134,6 +134,7 @@ declare global { const until: typeof import('@vueuse/core').until const useActiveElement: typeof import('@vueuse/core').useActiveElement const useAnimate: typeof import('@vueuse/core').useAnimate + const useAppUpdate: typeof import('./src/composables/useAppUpdate').useAppUpdate const useArrayDifference: typeof import('@vueuse/core').useArrayDifference const useArrayEvery: typeof import('@vueuse/core').useArrayEvery const useArrayFilter: typeof import('@vueuse/core').useArrayFilter @@ -156,6 +157,7 @@ declare global { const useBreakpoints: typeof import('@vueuse/core').useBreakpoints const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation + const useCacheSize: typeof import('./src/composables/useCacheSize').useCacheSize const useCached: typeof import('@vueuse/core').useCached const useClipboard: typeof import('@vueuse/core').useClipboard const useClipboardItems: typeof import('@vueuse/core').useClipboardItems @@ -476,6 +478,7 @@ declare module 'vue' { readonly until: UnwrapRef readonly useActiveElement: UnwrapRef readonly useAnimate: UnwrapRef + readonly useAppUpdate: UnwrapRef readonly useArrayDifference: UnwrapRef readonly useArrayEvery: UnwrapRef readonly useArrayFilter: UnwrapRef @@ -498,6 +501,7 @@ declare module 'vue' { readonly useBreakpoints: UnwrapRef readonly useBroadcastChannel: UnwrapRef readonly useBrowserLocation: UnwrapRef + readonly useCacheSize: UnwrapRef readonly useCached: UnwrapRef readonly useClipboard: UnwrapRef readonly useClipboardItems: UnwrapRef diff --git a/components.d.ts b/components.d.ts index da2196b..031172c 100644 --- a/components.d.ts +++ b/components.d.ts @@ -40,6 +40,7 @@ declare module 'vue' { IonItem: typeof import('@ionic/vue')['IonItem'] IonLabel: typeof import('@ionic/vue')['IonLabel'] IonList: typeof import('@ionic/vue')['IonList'] + IonListHeader: typeof import('@ionic/vue')['IonListHeader'] IonModal: typeof import('@ionic/vue')['IonModal'] IonNote: typeof import('@ionic/vue')['IonNote'] IonPage: typeof import('@ionic/vue')['IonPage'] @@ -106,6 +107,7 @@ declare global { const IonItem: typeof import('@ionic/vue')['IonItem'] const IonLabel: typeof import('@ionic/vue')['IonLabel'] const IonList: typeof import('@ionic/vue')['IonList'] + const IonListHeader: typeof import('@ionic/vue')['IonListHeader'] const IonModal: typeof import('@ionic/vue')['IonModal'] const IonNote: typeof import('@ionic/vue')['IonNote'] const IonPage: typeof import('@ionic/vue')['IonPage'] diff --git a/src/composables/useAppUpdate.ts b/src/composables/useAppUpdate.ts new file mode 100644 index 0000000..40a092a --- /dev/null +++ b/src/composables/useAppUpdate.ts @@ -0,0 +1,103 @@ +/** + * 应用更新检查组合式函数 + */ +export function useAppUpdate() { + const isChecking = ref(false); + const hasUpdate = ref(false); + const latestVersion = ref(""); + const currentVersion = ref("1.0.0"); // 从 package.json 或环境变量读取 + + /** + * 检查是否有新版本 + */ + async function checkForUpdate(): Promise<{ + hasUpdate: boolean; + currentVersion: string; + latestVersion?: string; + }> { + isChecking.value = true; + try { + // 方案1: 从服务器检查版本(需要后端 API) + // const response = await fetch('/api/version'); + // const { version } = await response.json(); + // latestVersion.value = version; + + // 方案2: 检查 Service Worker 更新(PWA 应用) + if ("serviceWorker" in navigator) { + const registration = await navigator.serviceWorker.getRegistration(); + if (registration) { + await registration.update(); + const hasNewWorker = registration.waiting !== null || registration.installing !== null; + hasUpdate.value = hasNewWorker; + + if (hasNewWorker) { + return { + hasUpdate: true, + currentVersion: currentVersion.value, + latestVersion: "新版本可用", + }; + } + } + } + + // 模拟检查(实际应用中需要替换为真实的 API 调用) + await new Promise(resolve => setTimeout(resolve, 1000)); + + // 暂时返回无更新 + return { + hasUpdate: false, + currentVersion: currentVersion.value, + }; + } + catch (error) { + console.error("检查更新失败:", error); + throw error; + } + finally { + isChecking.value = false; + } + } + + /** + * 应用更新(重新加载应用) + */ + async function applyUpdate(): Promise { + if ("serviceWorker" in navigator) { + const registration = await navigator.serviceWorker.getRegistration(); + if (registration?.waiting) { + // 通知 Service Worker 跳过等待,立即激活 + registration.waiting.postMessage({ type: "SKIP_WAITING" }); + + // 等待 Service Worker 激活后重新加载页面 + navigator.serviceWorker.addEventListener("controllerchange", () => { + window.location.reload(); + }); + return; + } + } + + // 如果没有 Service Worker,直接重新加载 + window.location.reload(); + } + + /** + * 强制刷新应用(清除缓存后重新加载) + */ + async function forceReload(): Promise { + if ("caches" in window) { + const cacheNames = await caches.keys(); + await Promise.all(cacheNames.map(name => caches.delete(name))); + } + window.location.reload(); + } + + return { + isChecking, + hasUpdate, + currentVersion, + latestVersion, + checkForUpdate, + applyUpdate, + forceReload, + }; +} diff --git a/src/composables/useCacheSize.ts b/src/composables/useCacheSize.ts new file mode 100644 index 0000000..e9318c2 --- /dev/null +++ b/src/composables/useCacheSize.ts @@ -0,0 +1,122 @@ +/** + * 获取应用缓存大小的组合式函数 + */ +export function useCacheSize() { + const cacheSize = ref("计算中..."); + const isLoading = ref(false); + + /** + * 计算当前应用使用的缓存大小 + */ + async function calculateCacheSize() { + isLoading.value = true; + try { + if ("storage" in navigator && "estimate" in navigator.storage) { + // 使用 Storage API 获取精确的存储使用量 + const estimate = await navigator.storage.estimate(); + const usageInMB = ((estimate.usage || 0) / (1024 * 1024)).toFixed(2); + cacheSize.value = `${usageInMB} MB`; + } + else { + // 降级方案:计算 localStorage 大小 + let totalSize = 0; + for (const key in localStorage) { + if (Object.prototype.hasOwnProperty.call(localStorage, key)) { + totalSize += localStorage[key].length + key.length; + } + } + const sizeInKB = (totalSize / 1024).toFixed(2); + cacheSize.value = `${sizeInKB} KB (仅 localStorage)`; + } + } + catch (error) { + console.error("计算缓存大小失败:", error); + cacheSize.value = "未知"; + } + finally { + isLoading.value = false; + } + } + + /** + * 获取缓存大小的原始字节数 + */ + async function getCacheSizeInBytes(): Promise { + try { + if ("storage" in navigator && "estimate" in navigator.storage) { + const estimate = await navigator.storage.estimate(); + return estimate.usage || 0; + } + else { + let totalSize = 0; + for (const key in localStorage) { + if (Object.prototype.hasOwnProperty.call(localStorage, key)) { + totalSize += localStorage[key].length + key.length; + } + } + return totalSize; + } + } + catch (error) { + console.error("获取缓存大小失败:", error); + return 0; + } + } + + /** + * 清除应用缓存 + */ + async function clearCache(): Promise { + isLoading.value = true; + try { + // 清除 localStorage + localStorage.clear(); + + // 清除 sessionStorage + sessionStorage.clear(); + + // 清除 Cache API 缓存 + if ("caches" in window) { + const cacheNames = await caches.keys(); + await Promise.all( + cacheNames.map(cacheName => caches.delete(cacheName)), + ); + } + + // 清除 IndexedDB(如果需要) + if ("indexedDB" in window) { + const databases = await indexedDB.databases(); + await Promise.all( + databases.map((db) => { + if (db.name) { + return new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(db.name!); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + return Promise.resolve(); + }), + ); + } + + // 重新计算缓存大小 + await calculateCacheSize(); + } + catch (error) { + console.error("清除缓存失败:", error); + throw error; + } + finally { + isLoading.value = false; + } + } + + return { + cacheSize, + isLoading, + calculateCacheSize, + getCacheSizeInBytes, + clearCache, + }; +} diff --git a/src/router/index.ts b/src/router/index.ts index 2f274a8..7e9e712 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -53,6 +53,10 @@ const routes: Array = [ path: "/user/settings", component: () => import("@/views/user/settings.vue"), }, + { + path: "/system-settings", + component: () => import("@/views/system-settings/index.vue"), + }, { path: "/issue/issuing-apply", props: ({ query, params }) => ({ query, params }), diff --git a/src/views/system-settings/index.vue b/src/views/system-settings/index.vue new file mode 100644 index 0000000..1801baf --- /dev/null +++ b/src/views/system-settings/index.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/src/views/user/components/user-info.vue b/src/views/user/components/user-info.vue index 6897007..01b816f 100644 --- a/src/views/user/components/user-info.vue +++ b/src/views/user/components/user-info.vue @@ -6,14 +6,14 @@ const { user } = useAuth();