From e7e2b1bd85a69edddef412bc87796450ad6326c3 Mon Sep 17 00:00:00 2001 From: Seven Date: Tue, 30 Dec 2025 18:07:14 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD=EF=BC=9B?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=89=88=E6=9C=AC=E6=A3=80=E6=9F=A5=E3=80=81?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8F=90=E7=A4=BA=E5=8F=8A=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=8C=96=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.update.md | 242 ++++++++++++++++++++++++++++ src/api/types.ts | 22 +++ src/composables/useAppUpdate.ts | 227 ++++++++++++++++++++++---- src/locales/en-US.json | 9 ++ src/locales/zh-CN.json | 9 ++ src/views/system-settings/index.vue | 45 +----- 6 files changed, 484 insertions(+), 70 deletions(-) create mode 100644 app.update.md diff --git a/app.update.md b/app.update.md new file mode 100644 index 0000000..9249bee --- /dev/null +++ b/app.update.md @@ -0,0 +1,242 @@ +# 应用版本更新使用示例 + +## 功能说明 + +`useAppUpdate` 提供了完整的应用版本检查和更新功能,支持: + +- ✅ 自动获取当前应用版本(原生/Web) +- ✅ 从后端 API 检查最新版本 +- ✅ 版本号比较(语义化版本) +- ✅ 强制更新和可选更新 +- ✅ 打开应用商店更新页面 +- ✅ 国际化支持(中英文) +- ✅ Service Worker 更新(PWA) + +## 基础使用 + +### 1. 在应用启动时检查更新 + +在 `App.vue` 或主布局组件中: + +```vue + +``` + +### 2. 手动触发检查更新 + +在设置页面添加"检查更新"按钮: + +```vue + + + +``` + +### 3. 自定义更新提示 + +如果不想使用默认对话框,可以自己处理: + +```vue + +``` + +### 4. 显示版本信息 + +```vue + + + +``` + +## API 参考 + +### 返回值 + +```typescript +{ + // 状态 + isChecking: Ref // 是否正在检查更新 + hasUpdate: Ref // 是否有新版本 + currentVersion: Ref // 当前版本号 + latestVersion: Ref // 最新版本号 + forceUpdate: Ref // 是否强制更新 + updateMessage: Ref // 更新提示信息 + updateUrl: Ref // 应用商店链接 + + // 方法 + getCurrentVersion(): Promise // 获取当前版本 + checkForUpdate(): Promise // 检查更新 + checkAndPromptUpdate(): Promise // 检查并显示提示 + showUpdateDialog(): Promise // 显示更新对话框 + openStoreUpdate(): Promise // 打开应用商店 + applyUpdate(): Promise // 应用更新 + forceReload(): Promise // 强制刷新 +} +``` + +### checkForUpdate 返回类型 + +```typescript +{ + hasUpdate: boolean // 是否有更新 + currentVersion: string // 当前版本 + latestVersion?: string // 最新版本 + forceUpdate?: boolean // 是否强制更新 + updateMessage?: string // 更新说明 + updateUrl?: string // 更新链接 +} +``` + +## 后端 API 接口 + +需要后端实现以下接口: + +```typescript +// GET /api/app/version +// Query Parameters: +{ + platform: 'ios' | 'android' | 'web' // 平台类型 + currentVersion: string // 当前版本号 +} + +// Response: +{ + version: string // 最新版本号 + buildNumber?: number // 构建号 + forceUpdate: boolean // 是否强制更新 + updateMessage?: string // 更新说明(多语言) + updateUrl?: string // 下载链接 + minSupportVersion?: string // 最低支持版本 + releaseDate?: string // 发布时间 + releaseNotes?: string[] // 更新日志 +} +``` + +## 环境变量配置 + +在 `.env` 文件中添加: + +```bash +# 应用版本(可选,如果不设置会从 Capacitor 读取) +VITE_APP_VERSION=1.0.0 +``` + +## 版本号规范 + +遵循语义化版本规范(Semantic Versioning): + +- **主版本号**:不兼容的 API 修改 +- **次版本号**:向下兼容的功能性新增 +- **修订号**:向下兼容的问题修正 + +示例:`1.2.3` + +## 注意事项 + +1. **原生应用更新**:iOS 和 Android 原生应用必须通过应用商店更新,不能绕过 +2. **强制更新策略**:谨慎使用强制更新,避免影响用户体验 +3. **版本检查频率**:建议应用启动时检查,避免频繁请求 +4. **降级处理**:如果后端接口失败,应用仍能正常使用 +5. **测试环境**:开发阶段接口返回模拟数据,生产环境需替换为真实 API + +## 国际化文本 + +已添加的翻译键: + +```json +{ + "app.update.title": "发现新版本 / New Version Available", + "app.update.message": "有新版本可用... / A new version is available...", + "app.update.now": "立即更新 / Update Now", + "app.update.later": "稍后再说 / Later", + "app.update.forceUpdate": "发现新版本,需要更新后才能继续使用" +} +``` + +## 生产环境配置 + +1. **替换模拟数据**:在 [useAppUpdate.ts](../composables/useAppUpdate.ts#L80-L100) 中取消注释真实 API 调用 +2. **配置应用商店链接**:后端返回正确的 App Store / Google Play 链接 +3. **设置最低支持版本**:根据实际情况配置 `minSupportVersion` diff --git a/src/api/types.ts b/src/api/types.ts index 8654062..07bb381 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -65,3 +65,25 @@ export type UserDepositOrderBody = TreatyQuery["data"][number]; export type UserWithdrawOrderBody = TreatyQuery; + +/** + * 应用版本信息 + */ +export interface VersionInfo { + /** 最新版本号,如 "1.2.3" */ + version: string; + /** 构建号 */ + buildNumber?: number; + /** 是否强制更新 */ + forceUpdate: boolean; + /** 更新提示信息 */ + updateMessage?: string; + /** 更新下载链接(App Store / Google Play) */ + updateUrl?: string; + /** 最低支持版本,低于此版本必须更新 */ + minSupportVersion?: string; + /** 发布时间 */ + releaseDate?: string; + /** 更新内容 */ + releaseNotes?: string[]; +} diff --git a/src/composables/useAppUpdate.ts b/src/composables/useAppUpdate.ts index 40a092a..93817b8 100644 --- a/src/composables/useAppUpdate.ts +++ b/src/composables/useAppUpdate.ts @@ -1,3 +1,33 @@ +import type { VersionInfo } from "@/api/types"; +import { App } from "@capacitor/app"; +import { alertController, modalController } from "@ionic/vue"; +import { client } from "@/api"; +import { i18n } from "@/locales"; + +/** + * 版本比较函数 + * @param version1 版本号1,如 "1.2.3" + * @param version2 版本号2,如 "1.2.4" + * @returns 如果 version1 < version2 返回 -1,相等返回 0,大于返回 1 + */ +function compareVersion(version1: string, version2: string): number { + const v1Parts = version1.split(".").map(Number); + const v2Parts = version2.split(".").map(Number); + const maxLength = Math.max(v1Parts.length, v2Parts.length); + + for (let i = 0; i < maxLength; i++) { + const v1Part = v1Parts[i] || 0; + const v2Part = v2Parts[i] || 0; + + if (v1Part < v2Part) + return -1; + if (v1Part > v2Part) + return 1; + } + + return 0; +} + /** * 应用更新检查组合式函数 */ @@ -5,7 +35,31 @@ export function useAppUpdate() { const isChecking = ref(false); const hasUpdate = ref(false); const latestVersion = ref(""); - const currentVersion = ref("1.0.0"); // 从 package.json 或环境变量读取 + const currentVersion = ref(""); + const forceUpdate = ref(false); + const updateMessage = ref(""); + const updateUrl = ref(""); + const platform = usePlatform(); + + /** + * 获取当前应用版本 + */ + async function getCurrentVersion(): Promise { + try { + // 在原生平台使用 Capacitor App API + if (platform !== "browser") { + const appInfo = await App.getInfo(); + return appInfo.version; + } + + // Web 平台从环境变量或 package.json 获取 + return import.meta.env.VITE_APP_VERSION || "0.0.1"; + } + catch (error) { + console.error("获取当前版本失败:", error); + return "0.0.1"; + } + } /** * 检查是否有新版本 @@ -14,44 +68,83 @@ export function useAppUpdate() { hasUpdate: boolean; currentVersion: string; latestVersion?: string; + forceUpdate?: boolean; + updateMessage?: string; + updateUrl?: string; }> { isChecking.value = true; try { - // 方案1: 从服务器检查版本(需要后端 API) - // const response = await fetch('/api/version'); - // const { version } = await response.json(); - // latestVersion.value = version; + // 1. 获取当前版本 + const current = await getCurrentVersion(); + currentVersion.value = current; - // 方案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; + // 2. 从服务器检查最新版本 + // TODO: 后端接口实现后替换为真实 API 调用 + // const response = await client.api.app.version.get({ + // query: { + // platform: platform === 'ios' ? 'ios' : platform === 'android' ? 'android' : 'web', + // currentVersion: current, + // }, + // }) - if (hasNewWorker) { - return { - hasUpdate: true, - currentVersion: currentVersion.value, - latestVersion: "新版本可用", - }; - } + // 模拟后端返回数据(开发阶段) + await new Promise(resolve => setTimeout(resolve, 500)); + const versionInfo: VersionInfo = { + version: "0.0.2", // 模拟有新版本 + forceUpdate: false, + updateMessage: "修复了一些问题并优化了性能", + updateUrl: platform === "ios" + ? "https://apps.apple.com/app/id123456789" + : platform === "android" + ? "https://play.google.com/store/apps/details?id=riwa.ionic.app" + : "", + minSupportVersion: "0.0.1", + }; + + // 真实 API 调用后的逻辑 + // if (response.error) { + // console.error('检查更新失败:', response.error) + // return { + // hasUpdate: false, + // currentVersion: current, + // } + // } + // const versionInfo = response.data as VersionInfo + + // 3. 版本比较 + const isNewVersion = compareVersion(current, versionInfo.version) < 0; + + // 更新状态 + latestVersion.value = versionInfo.version; + hasUpdate.value = isNewVersion; + forceUpdate.value = versionInfo.forceUpdate || false; + updateMessage.value = versionInfo.updateMessage || ""; + updateUrl.value = versionInfo.updateUrl || ""; + + // 4. 检查是否低于最低支持版本(强制更新) + if (versionInfo.minSupportVersion) { + const isBelowMinVersion = compareVersion(current, versionInfo.minSupportVersion) < 0; + if (isBelowMinVersion) { + forceUpdate.value = true; } } - // 模拟检查(实际应用中需要替换为真实的 API 调用) - await new Promise(resolve => setTimeout(resolve, 1000)); - - // 暂时返回无更新 return { - hasUpdate: false, - currentVersion: currentVersion.value, + hasUpdate: isNewVersion, + currentVersion: current, + latestVersion: versionInfo.version, + forceUpdate: forceUpdate.value, + updateMessage: updateMessage.value, + updateUrl: updateUrl.value, }; } catch (error) { console.error("检查更新失败:", error); - throw error; + const current = currentVersion.value || await getCurrentVersion(); + return { + hasUpdate: false, + currentVersion: current, + }; } finally { isChecking.value = false; @@ -59,9 +152,71 @@ export function useAppUpdate() { } /** - * 应用更新(重新加载应用) + * 打开应用商店更新页面 + */ + async function openStoreUpdate(): Promise { + if (!updateUrl.value) { + console.warn("没有提供更新链接"); + return; + } + + // 在原生平台打开外部链接 + if (platform !== "browser") { + window.open(updateUrl.value, "_system"); + } + else { + // Web 平台直接打开链接 + window.open(updateUrl.value, "_blank"); + } + } + + /** + * 显示更新提示对话框 + */ + async function showUpdateDialog(): Promise { + const alert = await alertController.create({ + header: i18n.global.t("app.update.title"), + message: updateMessage.value || i18n.global.t("app.update.message"), + backdropDismiss: !forceUpdate.value, // 强制更新时不允许关闭 + buttons: forceUpdate.value + ? [ + { + text: i18n.global.t("app.update.now"), + role: "confirm", + handler: () => { + openStoreUpdate(); + }, + }, + ] + : [ + { + text: i18n.global.t("app.update.later"), + role: "cancel", + }, + { + text: i18n.global.t("app.update.now"), + role: "confirm", + handler: () => { + openStoreUpdate(); + }, + }, + ], + }); + + await alert.present(); + } + + /** + * 应用更新(重新加载应用或打开商店) */ async function applyUpdate(): Promise { + // 如果是原生应用且有更新链接,打开应用商店 + if (platform !== "browser" && updateUrl.value) { + await openStoreUpdate(); + return; + } + + // Web 应用检查 Service Worker 更新 if ("serviceWorker" in navigator) { const registration = await navigator.serviceWorker.getRegistration(); if (registration?.waiting) { @@ -91,13 +246,31 @@ export function useAppUpdate() { window.location.reload(); } + /** + * 自动检查更新并显示提示 + */ + async function checkAndPromptUpdate(): Promise { + const result = await checkForUpdate(); + + if (result.hasUpdate) { + await showUpdateDialog(); + } + } + return { isChecking, hasUpdate, currentVersion, latestVersion, + forceUpdate, + updateMessage, + updateUrl, checkForUpdate, applyUpdate, forceReload, + openStoreUpdate, + showUpdateDialog, + checkAndPromptUpdate, + getCurrentVersion, }; } diff --git a/src/locales/en-US.json b/src/locales/en-US.json index c88f1a4..f12254a 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -6,6 +6,15 @@ "transfer": "Transfer", "balance": "Balance" }, + "app": { + "update": { + "title": "New Version Available", + "message": "A new version is available. Update now?", + "now": "Update Now", + "later": "Later", + "forceUpdate": "A new version is required to continue using the app" + } + }, "scanner": { "title": "Scan QR Code", "hint": "Align QR code within frame to scan", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 1fa6f8c..d54e2ea 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -6,6 +6,15 @@ "transfer": "转账", "balance": "余额" }, + "app": { + "update": { + "title": "发现新版本", + "message": "有新版本可用,是否立即更新?", + "now": "立即更新", + "later": "稍后再说", + "forceUpdate": "发现新版本,需要更新后才能继续使用" + } + }, "scanner": { "title": "扫描二维码", "hint": "将二维码对准扫描框进行扫描", diff --git a/src/views/system-settings/index.vue b/src/views/system-settings/index.vue index 5321fbe..524d345 100644 --- a/src/views/system-settings/index.vue +++ b/src/views/system-settings/index.vue @@ -5,7 +5,7 @@ import { checkbox, close, contrastOutline, information, languageOutline, refresh const { t } = useI18n(); const router = useIonRouter(); const { cacheSize, calculateCacheSize, clearCache } = useCacheSize(); -const { isChecking, checkForUpdate } = useAppUpdate(); +const { isChecking, checkAndPromptUpdate } = useAppUpdate(); const { currentLanguage } = useLanguage(); const { theme } = useTheme(); const themeNames = { @@ -27,48 +27,7 @@ function handleClearCache() { } async function handleCheckUpdate() { - try { - const result = await checkForUpdate(); - - if (result.hasUpdate) { - const alert = await alertController.create({ - header: t("settings.updateAvailable"), - message: `${t("settings.currentVersion")}: ${result.currentVersion}\n${t("settings.latestVersion")}: ${result.latestVersion || t("settings.newVersion")}`, - buttons: [ - { - text: t("settings.cancel"), - role: "cancel", - }, - { - text: t("settings.updateNow"), - handler: () => { - window.location.reload(); - }, - }, - ], - }); - await alert.present(); - } - else { - const toast = await toastController.create({ - message: t("settings.alreadyLatest"), - duration: 2000, - icon: checkbox, - position: "bottom", - color: "success", - }); - await toast.present(); - } - } - catch (error) { - const toast = await toastController.create({ - message: t("settings.checkUpdateFailed"), - duration: 2000, - position: "bottom", - color: "danger", - }); - await toast.present(); - } + checkAndPromptUpdate(); } onMounted(() => {