feat(web-admin): force a refresh when a newer build is deployed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-10-16 13:53:15 +08:00
1 parent a1c00847e5
commit 020fbe1b0e
4 files changed
+61 -2

No files matched your search

+1
View File
@@ -63,3 +63,4 @@ declare module 'qrcode.vue' {
// 全局变量声明
declare const __APP_VERSION__: string // 版本号
declare const __APP_BUILD_ID__: string // 构建指纹:短 git hash + 构建时间戳
+3 -1
View File
@@ -16,6 +16,7 @@ import '@styles/index.scss' // 样式
import '@utils/sys/console.ts' // 控制台输出内容
import { setupGlobDirectives } from './directives'
import { setupErrorHandle } from './utils/sys/error-handle'
import { startAppUpdateCheck } from './utils/check-app-update'
function maskSecret(value?: string) {
if (!value) return '(empty)'
@@ -62,4 +63,5 @@ setupGlobDirectives(app)
setupErrorHandle(app)
app.use(language)
app.mount('#app')
app.mount('#app')
startAppUpdateCheck()
+40
View File
@@ -0,0 +1,40 @@
// web-admin/src/utils/check-app-update.ts
import { startAppUpdateChecker } from './app-update'
/** 生产环境检查 dist/version.json,发现新 buildId 后强制刷新。 */
export function startAppUpdateCheck(): () => void {
return startAppUpdateChecker({
isProd: import.meta.env.PROD,
localBuildId: __APP_BUILD_ID__,
localVersion: __APP_VERSION__,
baseUrl: import.meta.env.BASE_URL,
now: () => Date.now(),
fetchRemote: async (url) => {
const res = await fetch(url, { cache: 'no-store' })
if (!res.ok) throw new Error(String(res.status))
return res.json()
},
onReload: () => {
location.reload()
},
alertUpdate: async (message) => {
await ElMessageBox.alert(message, '发现新版本', {
confirmButtonText: '立即刷新',
showClose: false,
closeOnClickModal: false,
closeOnPressEscape: false,
showCancelButton: false,
dangerouslyUseHTMLString: true
})
},
setInterval: (fn, ms) => window.setInterval(fn, ms),
clearInterval: (id) => window.clearInterval(id),
subscribeVisible: (handler) => {
const listener = () => {
if (document.visibilityState === 'visible') handler()
}
document.addEventListener('visibilitychange', listener)
return () => document.removeEventListener('visibilitychange', listener)
}
})
}
+17 -1
View File
@@ -1,6 +1,8 @@
import { execSync } from 'node:child_process'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { createAppBuildMeta, readGitShort, writeVersionJson } from './scripts/app-build-meta'
import { fileURLToPath } from 'url'
import vueDevTools from 'vite-plugin-vue-devtools'
import viteCompression from 'vite-plugin-compression'
@@ -17,13 +19,20 @@ export default ({ mode }: { mode: string }) => {
const env = loadEnv(mode, root)
const { VITE_VERSION, VITE_PORT, VITE_BASE_URL, VITE_API_URL, VITE_API_PROXY_URL } = env
const gitShort = readGitShort(() =>
execSync('git rev-parse --short HEAD', {
stdio: ['ignore', 'pipe', 'ignore']
}).toString()
)
const appBuildMeta = createAppBuildMeta(VITE_VERSION || '', Date.now(), gitShort)
console.log(`🚀 API_URL = ${VITE_API_URL}`)
console.log(`🚀 VERSION = ${VITE_VERSION}`)
return defineConfig({
define: {
__APP_VERSION__: JSON.stringify(VITE_VERSION),
// html-to-docx 等依赖会读 Node 全局;浏览器映射到 globalThis
__APP_BUILD_ID__: JSON.stringify(appBuildMeta.buildId),
global: 'globalThis'
},
base: VITE_BASE_URL,
@@ -88,6 +97,13 @@ export default ({ mode }: { mode: string }) => {
}
},
plugins: [
{
name: 'emit-version-json',
apply: 'build',
closeBundle() {
writeVersionJson(path.resolve(root, 'dist'), appBuildMeta)
}
},
vue(),
tailwindcss(),
// 自动按需导入 API