feat: 添加应用版本更新功能;实现版本检查、更新提示及国际化支持

This commit is contained in:
2025-12-30 18:07:14 +07:00
parent 7ce60b860c
commit e7e2b1bd85
6 changed files with 484 additions and 70 deletions

242
app.update.md Normal file
View File

@@ -0,0 +1,242 @@
# 应用版本更新使用示例
## 功能说明
`useAppUpdate` 提供了完整的应用版本检查和更新功能,支持:
- ✅ 自动获取当前应用版本(原生/Web
- ✅ 从后端 API 检查最新版本
- ✅ 版本号比较(语义化版本)
- ✅ 强制更新和可选更新
- ✅ 打开应用商店更新页面
- ✅ 国际化支持(中英文)
- ✅ Service Worker 更新PWA
## 基础使用
### 1. 在应用启动时检查更新
`App.vue` 或主布局组件中:
```vue
<script setup lang="ts">
import { onMounted } from 'vue'
const { checkAndPromptUpdate } = useAppUpdate()
onMounted(async () => {
// 应用启动 3 秒后检查更新
setTimeout(() => {
checkAndPromptUpdate()
}, 3000)
})
</script>
```
### 2. 手动触发检查更新
在设置页面添加"检查更新"按钮:
```vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>系统设置</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item button @click="handleCheckUpdate">
<ion-label>
<h2>检查更新</h2>
<p>当前版本: {{ currentVersion }}</p>
</ion-label>
<ion-spinner v-if="isChecking" slot="end" />
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
const {
isChecking,
currentVersion,
checkAndPromptUpdate,
} = useAppUpdate()
async function handleCheckUpdate() {
await checkAndPromptUpdate()
}
</script>
```
### 3. 自定义更新提示
如果不想使用默认对话框,可以自己处理:
```vue
<script setup lang="ts">
const {
hasUpdate,
latestVersion,
forceUpdate,
updateMessage,
checkForUpdate,
openStoreUpdate,
} = useAppUpdate()
async function checkUpdate() {
const result = await checkForUpdate()
if (result.hasUpdate) {
// 自定义提示逻辑
if (result.forceUpdate) {
// 强制更新 - 阻止用户继续使用
showForceUpdateModal()
} else {
// 可选更新 - 显示提示但允许跳过
showOptionalUpdateToast()
}
}
}
</script>
```
### 4. 显示版本信息
```vue
<template>
<ion-item>
<ion-label>
<h2>应用版本</h2>
<p>{{ currentVersion || '获取中...' }}</p>
</ion-label>
<ion-badge v-if="hasUpdate" color="danger">有更新</ion-badge>
</ion-item>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
const { currentVersion, hasUpdate, getCurrentVersion, checkForUpdate } = useAppUpdate()
onMounted(async () => {
await getCurrentVersion()
await checkForUpdate()
})
</script>
```
## API 参考
### 返回值
```typescript
{
// 状态
isChecking: Ref<boolean> // 是否正在检查更新
hasUpdate: Ref<boolean> // 是否有新版本
currentVersion: Ref<string> // 当前版本号
latestVersion: Ref<string> // 最新版本号
forceUpdate: Ref<boolean> // 是否强制更新
updateMessage: Ref<string> // 更新提示信息
updateUrl: Ref<string> // 应用商店链接
// 方法
getCurrentVersion(): Promise<string> // 获取当前版本
checkForUpdate(): Promise<UpdateResult> // 检查更新
checkAndPromptUpdate(): Promise<void> // 检查并显示提示
showUpdateDialog(): Promise<void> // 显示更新对话框
openStoreUpdate(): Promise<void> // 打开应用商店
applyUpdate(): Promise<void> // 应用更新
forceReload(): Promise<void> // 强制刷新
}
```
### 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`

View File

@@ -65,3 +65,25 @@ export type UserDepositOrderBody = TreatyQuery<typeof client.api.deposit.orders.
export type UserWithdrawOrderData = Treaty.Data<typeof client.api.withdraw.get>["data"][number]; export type UserWithdrawOrderData = Treaty.Data<typeof client.api.withdraw.get>["data"][number];
export type UserWithdrawOrderBody = TreatyQuery<typeof client.api.withdraw.get>; export type UserWithdrawOrderBody = TreatyQuery<typeof client.api.withdraw.get>;
/**
* 应用版本信息
*/
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[];
}

View File

@@ -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 isChecking = ref(false);
const hasUpdate = ref(false); const hasUpdate = ref(false);
const latestVersion = ref(""); 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<string> {
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; hasUpdate: boolean;
currentVersion: string; currentVersion: string;
latestVersion?: string; latestVersion?: string;
forceUpdate?: boolean;
updateMessage?: string;
updateUrl?: string;
}> { }> {
isChecking.value = true; isChecking.value = true;
try { try {
// 方案1: 从服务器检查版本(需要后端 API // 1. 获取当前版本
// const response = await fetch('/api/version'); const current = await getCurrentVersion();
// const { version } = await response.json(); currentVersion.value = current;
// latestVersion.value = version;
// 方案2: 检查 Service Worker 更新PWA 应用) // 2. 从服务器检查最新版本
if ("serviceWorker" in navigator) { // TODO: 后端接口实现后替换为真实 API 调用
const registration = await navigator.serviceWorker.getRegistration(); // const response = await client.api.app.version.get({
if (registration) { // query: {
await registration.update(); // platform: platform === 'ios' ? 'ios' : platform === 'android' ? 'android' : 'web',
const hasNewWorker = registration.waiting !== null || registration.installing !== null; // currentVersion: current,
hasUpdate.value = hasNewWorker; // },
// })
if (hasNewWorker) { // 模拟后端返回数据(开发阶段)
return { await new Promise(resolve => setTimeout(resolve, 500));
hasUpdate: true, const versionInfo: VersionInfo = {
currentVersion: currentVersion.value, version: "0.0.2", // 模拟有新版本
latestVersion: "新版本可用", 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 { return {
hasUpdate: false, hasUpdate: isNewVersion,
currentVersion: currentVersion.value, currentVersion: current,
latestVersion: versionInfo.version,
forceUpdate: forceUpdate.value,
updateMessage: updateMessage.value,
updateUrl: updateUrl.value,
}; };
} }
catch (error) { catch (error) {
console.error("检查更新失败:", error); console.error("检查更新失败:", error);
throw error; const current = currentVersion.value || await getCurrentVersion();
return {
hasUpdate: false,
currentVersion: current,
};
} }
finally { finally {
isChecking.value = false; isChecking.value = false;
@@ -59,9 +152,71 @@ export function useAppUpdate() {
} }
/** /**
* 应用更新(重新加载应用) * 打开应用商店更新页面
*/
async function openStoreUpdate(): Promise<void> {
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<void> {
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<void> { async function applyUpdate(): Promise<void> {
// 如果是原生应用且有更新链接,打开应用商店
if (platform !== "browser" && updateUrl.value) {
await openStoreUpdate();
return;
}
// Web 应用检查 Service Worker 更新
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
const registration = await navigator.serviceWorker.getRegistration(); const registration = await navigator.serviceWorker.getRegistration();
if (registration?.waiting) { if (registration?.waiting) {
@@ -91,13 +246,31 @@ export function useAppUpdate() {
window.location.reload(); window.location.reload();
} }
/**
* 自动检查更新并显示提示
*/
async function checkAndPromptUpdate(): Promise<void> {
const result = await checkForUpdate();
if (result.hasUpdate) {
await showUpdateDialog();
}
}
return { return {
isChecking, isChecking,
hasUpdate, hasUpdate,
currentVersion, currentVersion,
latestVersion, latestVersion,
forceUpdate,
updateMessage,
updateUrl,
checkForUpdate, checkForUpdate,
applyUpdate, applyUpdate,
forceReload, forceReload,
openStoreUpdate,
showUpdateDialog,
checkAndPromptUpdate,
getCurrentVersion,
}; };
} }

View File

@@ -6,6 +6,15 @@
"transfer": "Transfer", "transfer": "Transfer",
"balance": "Balance" "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": { "scanner": {
"title": "Scan QR Code", "title": "Scan QR Code",
"hint": "Align QR code within frame to scan", "hint": "Align QR code within frame to scan",

View File

@@ -6,6 +6,15 @@
"transfer": "转账", "transfer": "转账",
"balance": "余额" "balance": "余额"
}, },
"app": {
"update": {
"title": "发现新版本",
"message": "有新版本可用,是否立即更新?",
"now": "立即更新",
"later": "稍后再说",
"forceUpdate": "发现新版本,需要更新后才能继续使用"
}
},
"scanner": { "scanner": {
"title": "扫描二维码", "title": "扫描二维码",
"hint": "将二维码对准扫描框进行扫描", "hint": "将二维码对准扫描框进行扫描",

View File

@@ -5,7 +5,7 @@ import { checkbox, close, contrastOutline, information, languageOutline, refresh
const { t } = useI18n(); const { t } = useI18n();
const router = useIonRouter(); const router = useIonRouter();
const { cacheSize, calculateCacheSize, clearCache } = useCacheSize(); const { cacheSize, calculateCacheSize, clearCache } = useCacheSize();
const { isChecking, checkForUpdate } = useAppUpdate(); const { isChecking, checkAndPromptUpdate } = useAppUpdate();
const { currentLanguage } = useLanguage(); const { currentLanguage } = useLanguage();
const { theme } = useTheme(); const { theme } = useTheme();
const themeNames = { const themeNames = {
@@ -27,48 +27,7 @@ function handleClearCache() {
} }
async function handleCheckUpdate() { async function handleCheckUpdate() {
try { checkAndPromptUpdate();
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();
}
} }
onMounted(() => { onMounted(() => {