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

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 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 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<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;
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<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> {
// 如果是原生应用且有更新链接,打开应用商店
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<void> {
const result = await checkForUpdate();
if (result.hasUpdate) {
await showUpdateDialog();
}
}
return {
isChecking,
hasUpdate,
currentVersion,
latestVersion,
forceUpdate,
updateMessage,
updateUrl,
checkForUpdate,
applyUpdate,
forceReload,
openStoreUpdate,
showUpdateDialog,
checkAndPromptUpdate,
getCurrentVersion,
};
}

View File

@@ -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",

View File

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

View File

@@ -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(() => {