diff --git a/src/App.vue b/src/App.vue index bc6d8b0f..87cb5475 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,6 +15,7 @@ import { useAppStore, useSettingsStore } from "@/stores"; import { appConfig } from "@/settings"; import { ThemeMode, ComponentSize } from "@/enums"; +import { useVersionCheck } from "@/composables/useVersionCheck"; const appStore = useAppStore(); const settingsStore = useSettingsStore(); @@ -30,4 +31,9 @@ const fontColor = computed(() => { ? "rgba(255, 255, 255, .15)" : "rgba(0, 0, 0, .15)"; }); + +// 初始化版本检查 +if (!import.meta.env.DEV) { + useVersionCheck(5 * 60_000); +} diff --git a/src/composables/useVersionCheck.ts b/src/composables/useVersionCheck.ts new file mode 100644 index 00000000..e6bfef93 --- /dev/null +++ b/src/composables/useVersionCheck.ts @@ -0,0 +1,66 @@ +import { ElNotification } from "element-plus"; +import { onMounted, onUnmounted, ref } from "vue"; + +/** 版本检查 */ +export function useVersionCheck(interval = 60_000) { + const hasNewVersion = ref(false); + let notified = false; + const safeInterval = Math.max(10_000, interval); + + const checkVersion = async () => { + try { + const base = import.meta.env.BASE_URL; + const baseUrl = base.endsWith("/") ? base : `${base}/`; + // 时间戳防 CDN/代理缓存,no-cache 防浏览器磁盘缓存 + const res = await fetch(`${baseUrl}version.json?t=${Date.now()}`, { + cache: "no-cache", + }); + if (!res.ok) return; // 非核心功能,网络异常不影响正常使用 + + const data = await res.json(); + if (data.version && data.version !== __APP_INFO__.buildTimestamp) { + hasNewVersion.value = true; + showUpdateNotice(); + } + } catch (e) { + console.warn("版本检测失败", e); + } + }; + + const showUpdateNotice = () => { + if (notified) return; + notified = true; + + // 使用 Notification 而非 Modal,不打断用户操作 + ElNotification({ + title: "🎉 发现新版本", + message: "系统已更新,点击刷新以获取最新功能", + duration: 0, // 不自动关闭 + position: "bottom-right", + onClick: () => window.location.reload(), + onClose: () => { + notified = false; + }, + customClass: "version-update-notice", + }); + }; + + let timer: ReturnType; + // 用户从其他标签页切回时立即检测,无需等待下一次轮询 + const handleVisibilityChange = () => { + if (!document.hidden) checkVersion(); + }; + + onMounted(() => { + checkVersion(); + timer = setInterval(checkVersion, safeInterval); + document.addEventListener("visibilitychange", handleVisibilityChange); + }); + + onUnmounted(() => { + clearInterval(timer); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }); + + return { hasNewVersion }; +} diff --git a/vite.config.ts b/vite.config.ts index da77f8bc..8cc21b11 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,8 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; import { mockDevServerPlugin } from "vite-plugin-mock-dev-server"; import UnoCSS from "unocss/vite"; -import { resolve } from "path"; +import { join, resolve } from "path"; +import { existsSync, mkdirSync, writeFileSync } from "fs"; import { name, version } from "./package.json" with { type: "json" }; // 平台名称、版本信息 @@ -85,6 +86,20 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { dts: false, //dts: "types/components.d.ts", }), + // 生成版本号文件 + { + name: "dist-version", + closeBundle() { + const distDir = resolve(__dirname, "dist"); + if (!existsSync(distDir)) { + mkdirSync(distDir, { recursive: true }); + } + writeFileSync( + join(distDir, "version.json"), + JSON.stringify({ version: __APP_INFO__.buildTimestamp }) + ); + }, + }, ] as PluginOption[], // 预加载项目必需的依赖 optimizeDeps: {