From 0aa9c21531840a7b2ed43bb208cf322570df0b89 Mon Sep 17 00:00:00 2001 From: biss Date: Sun, 23 Aug 2026 16:59:17 +0800 Subject: [PATCH] =?UTF-8?q?=E7=83=AD=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++--- web/src/services/appUpdates.ts | 44 +++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8ef72e8..17ffc3b 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,11 @@ ZIP 会生成到 `.artifacts/app-updates`,根目录直接包含 `index.html` `versions.props` 的 `JiaowuFrontendVersion`(传入 `--version` 时也必须一致)。使用 SuperAdmin 进入“运维与审计 → App 前端热更新”,上传 ZIP,填写目标平台、通道和 兼容的原生版本后先保存为草稿,再执行发布。当前 Android 工程的 `versionName` 为 -`1.0`,因此对应更新包的“兼容原生版本”应填写 `1.0`。 +`2.6.0`,因此对应更新包的“兼容原生版本”应填写 `2.6.0`。 -App 启动、以及从后台回到前台超过 5 分钟后,会向 `/api/app-updates/latest` 检查版本, -在后台下载并校验服务端提供的 SHA-256;切到后台或下次启动时切换,并保留当前页面地址。 +App 每次启动、网络恢复以及从后台回到前台时,都会向 `/api/app-updates/latest` 检查版本, +在后台下载并校验服务端提供的 SHA-256。下载完成后,用户可在常驻提示中确认立即重新加载; +若选择稍后,切到后台或下次启动时自动切换,并保留当前页面地址。 新资源若未能成功启动,原生更新器会自动回滚。再次发布 已归档版本即可回滚正式通道;不同原生版本、Android/iOS、测试/正式通道彼此隔离。 更新版本元数据和 ZIP 保存在数据库中,部署新服务端版本前必须先执行 diff --git a/web/src/services/appUpdates.ts b/web/src/services/appUpdates.ts index a739366..5059d8b 100644 --- a/web/src/services/appUpdates.ts +++ b/web/src/services/appUpdates.ts @@ -1,5 +1,6 @@ +import { App } from '@capacitor/app' import { Capacitor } from '@capacitor/core' -import { CapacitorUpdater } from '@capgo/capacitor-updater' +import { CapacitorUpdater, type BundleInfo } from '@capgo/capacitor-updater' import { ElNotification } from 'element-plus' import http from '../api/http' @@ -20,6 +21,7 @@ const updateChannel = import.meta.env.VITE_APP_UPDATE_CHANNEL?.trim() || 'production' let checkInProgress: Promise | undefined +let updatePromptVisible = false function absoluteApiUrl(relativePath: string) { const configuredBase = String(http.defaults.baseURL ?? '/api') @@ -87,9 +89,12 @@ async function checkForAppUpdate() { ElNotification({ title: `新版本 ${data.version} 已就绪`, message: - data.releaseNotes || '切换到后台或下次启动应用时将自动启用更新。', + (data.releaseNotes + ? `${data.releaseNotes} 点击此提示可立即启用。` + : '切换到后台或下次启动应用时将自动启用更新;点击此提示可立即启用。'), type: 'success', - duration: 6000, + duration: 0, + onClick: () => void applyDownloadedUpdate(downloaded), }) } catch (error) { // OTA failure must never stop users from entering the bundled application. @@ -102,6 +107,33 @@ async function checkForAppUpdate() { return checkInProgress } +async function applyDownloadedUpdate(bundle: BundleInfo) { + if (updatePromptVisible) return + updatePromptVisible = true + try { + await ElNotification.closeAll() + // `set` replaces the WebView bundle and immediately reloads, so ask for a + // deliberate confirmation instead of interrupting unsaved form input. + await import('element-plus').then(({ ElMessageBox }) => + ElMessageBox.confirm( + '将重新加载应用以启用已下载的新版本。未保存的内容请先提交。', + '启用更新', + { + confirmButtonText: '立即启用', + cancelButtonText: '稍后', + type: 'info', + }, + ), + ) + await CapacitorUpdater.set({ id: bundle.id }) + } catch { + // Choosing "later" is safe: next() has already queued this bundle for + // activation when the app backgrounds or is launched again. + } finally { + updatePromptVisible = false + } +} + export async function initializeAppUpdates() { if (!Capacitor.isNativePlatform()) return @@ -117,5 +149,11 @@ export async function initializeAppUpdates() { // Every cold start checks the latest public release immediately. A bundle // downloaded here is activated when the app is next launched. void checkForAppUpdate() + // Keep the native app aligned with the PWA behavior: returning to the app + // is another opportunity to discover a newly deployed web bundle. The + // single-flight guard prevents duplicate cold-start/foreground requests. + void App.addListener('appStateChange', ({ isActive }) => { + if (isActive) void checkForAppUpdate() + }) window.addEventListener('online', () => void checkForAppUpdate()) }