feat: 添加版本检查 API,支持从 Cloudflare Function 获取应用版本信息;更新相关配置文件

This commit is contained in:
2026-01-14 02:38:02 +07:00
parent a64bd9b470
commit 26ba611095
8 changed files with 285 additions and 103 deletions

101
functions/api/version.ts Normal file
View File

@@ -0,0 +1,101 @@
/**
* Cloudflare Pages Function - 版本检查 API
* 访问路径: /api/version
*/
interface VersionInfo {
version: string;
forceUpdate: boolean;
updateMessage: string;
updateUrl: string;
minSupportVersion: string;
}
interface Env {
// 可以在这里添加环境变量类型
}
interface VersionConfig {
ios: VersionInfo;
android: VersionInfo;
web: VersionInfo;
}
// 版本配置 - 直接在这里管理版本信息
const versionConfig: VersionConfig = {
ios: {
version: "0.0.1",
forceUpdate: false,
updateMessage: "修复了一些问题并优化了性能",
updateUrl: "https://apps.apple.com/app/id123456789",
minSupportVersion: "0.9.0",
},
android: {
version: "0.0.1",
forceUpdate: false,
updateMessage: "修复了一些问题并优化了性能",
updateUrl: "https://play.google.com/store/apps/details?id=riwa.ionic.app",
minSupportVersion: "0.9.0",
},
web: {
version: "0.0.1",
forceUpdate: false,
updateMessage: "修复了一些问题并优化了性能",
updateUrl: "",
minSupportVersion: "0.9.0",
},
};
export const onRequestGet: PagesFunction<Env> = async (context) => {
try {
const { searchParams } = new URL(context.request.url);
const platform = searchParams.get("platform") || "web";
const currentVersion = searchParams.get("currentVersion") || "0.0.1";
// 验证平台参数
if (!["ios", "android", "web"].includes(platform)) {
return new Response(
JSON.stringify({ error: "Invalid platform parameter" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
// 获取对应平台的版本信息
const versionInfo = versionConfig[platform as keyof VersionConfig];
// 返回版本信息
return new Response(JSON.stringify(versionInfo), {
status: 200,
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300", // 缓存5分钟
"Access-Control-Allow-Origin": "*",
},
});
}
catch (error) {
console.error("Version check error:", error);
return new Response(
JSON.stringify({ error: "Internal server error" }),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
);
}
};
// 支持 CORS 预检请求
export const onRequestOptions: PagesFunction<Env> = async () => {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
};

17
functions/tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext"],
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"types": ["@cloudflare/workers-types"],
"strict": true,
"noEmit": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -64,6 +64,7 @@
"devDependencies": {
"@antfu/eslint-config": "catalog:",
"@capacitor/cli": "catalog:",
"@cloudflare/workers-types": "catalog:",
"@iconify-json/bx": "catalog:",
"@iconify-json/circle-flags": "catalog:",
"@iconify-json/cryptocurrency-color": "catalog:",

16
pnpm-lock.yaml generated
View File

@@ -54,6 +54,9 @@ catalogs:
'@capacitor/status-bar':
specifier: 8.0.0
version: 8.0.0
'@cloudflare/workers-types':
specifier: ^4.20260113.0
version: 4.20260113.0
'@elysiajs/eden':
specifier: ^1.4.5
version: 1.4.5
@@ -399,6 +402,9 @@ importers:
'@capacitor/cli':
specifier: 'catalog:'
version: 8.0.0
'@cloudflare/workers-types':
specifier: 'catalog:'
version: 4.20260113.0
'@iconify-json/bx':
specifier: 'catalog:'
version: 1.2.2
@@ -521,7 +527,7 @@ importers:
version: 7.4.0
wrangler:
specifier: 'catalog:'
version: 4.54.0
version: 4.54.0(@cloudflare/workers-types@4.20260113.0)
packages/distribute:
dependencies:
@@ -1323,6 +1329,9 @@ packages:
cpu: [x64]
os: [win32]
'@cloudflare/workers-types@4.20260113.0':
resolution: {integrity: sha512-CS2tUdGn1EMAV5GoFYYUfsZ4vwwXiYxwrUiI8ZRkxrJGqkHNGily/5Zf+vt/wh1HSoiCIChNYiuLEoCA/XUybw==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -10499,6 +10508,8 @@ snapshots:
'@cloudflare/workerd-windows-64@1.20251210.0':
optional: true
'@cloudflare/workers-types@4.20260113.0': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -19654,7 +19665,7 @@ snapshots:
'@cloudflare/workerd-linux-arm64': 1.20251210.0
'@cloudflare/workerd-windows-64': 1.20251210.0
wrangler@4.54.0:
wrangler@4.54.0(@cloudflare/workers-types@4.20260113.0):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.1
'@cloudflare/unenv-preset': 2.7.13(unenv@2.0.0-rc.24)(workerd@1.20251210.0)
@@ -19665,6 +19676,7 @@ snapshots:
unenv: 2.0.0-rc.24
workerd: 1.20251210.0
optionalDependencies:
'@cloudflare/workers-types': 4.20260113.0
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil

View File

@@ -1,17 +1,14 @@
catalogMode: prefer
shellEmulator: true
trustPolicy: no-downgrade
packages:
- packages/*
catalog:
'@riwa/api-types': http://192.168.1.7:9528/api/riwa-eden-0.0.138.tgz
'@antfu/eslint-config': ^6.6.1
'@capacitor-community/barcode-scanner': ^4.0.1
'@capacitor-mlkit/barcode-scanning': ^8.0.0
'@capacitor/app': 8.0.0
'@capacitor/barcode-scanner': ^3.0.0
'@capacitor/camera': ^8.0.0
'@capacitor/cli': 8.0.0
'@capacitor/clipboard': ^8.0.0
'@capacitor/core': 8.0.0
'@capacitor/file-transfer': ^2.0.0
@@ -21,35 +18,8 @@ catalog:
'@capacitor/keyboard': 8.0.0
'@capacitor/share': ^8.0.0
'@capacitor/status-bar': 8.0.0
'@cloudflare/workers-types': ^4.20260113.0
'@elysiajs/eden': ^1.4.5
'@ionic/vue': ^8.7.11
'@ionic/vue-router': ^8.7.11
'@tailwindcss/vite': ^4.1.18
'@vee-validate/zod': ^4.15.1
'@vueuse/core': ^14.1.0
'@vueuse/integrations': ^14.1.0
'@vueuse/router': ^14.1.0
'better-auth': ^1.4.6
'dayjs': ^1.11.19
'ethers': ^6.16.0
'html2canvas': ^1.4.1
'ionicons': ^8.0.13
'jsqr': ^1.4.0
'lightweight-charts': ^5.1.0
'lodash-es': ^4.17.21
'markdown-it': ^14.1.0
'pinia': ^3.0.4
'qr-scanner-wechat': ^0.1.3
'qrcode': ^1.5.4
'tailwindcss': ^4.1.18
'vconsole': ^3.15.1
'vee-validate': ^4.15.1
'vue': ^3.5.25
'vue-i18n': ^11.2.2
'vue-router': ^4.6.3
'zod': ^3.24.1
'@antfu/eslint-config': ^6.6.1
'@capacitor/cli': 8.0.0
'@iconify-json/bx': ^1.2.2
'@iconify-json/circle-flags': ^1.2.10
'@iconify-json/cryptocurrency-color': ^1.2.4
@@ -62,32 +32,65 @@ catalog:
'@iconify-json/tdesign': ^1.2.11
'@iconify/vue': ^5.0.0
'@ionic/cli': ^7.2.1
'@ionic/vue': ^8.7.11
'@ionic/vue-router': ^8.7.11
'@riwa/api-types': http://192.168.1.7:9528/api/riwa-eden-0.0.138.tgz
'@tailwindcss/vite': ^4.1.18
'@types/lodash-es': ^4.17.12
'@types/node': ^24.10.2
'@types/qrcode': ^1.5.6
'@vee-validate/zod': ^4.15.1
'@vitejs/plugin-basic-ssl': ^2.1.3
'@vitejs/plugin-legacy': ^7.2.1
'@vitejs/plugin-vue': ^6.0.2
'@vitejs/plugin-vue-jsx': ^5.1.2
'@vue/eslint-config-typescript': ^14.6.0
'@vue/test-utils': ^2.4.6
'cypress': ^15.7.1
'dotenv': ^17.2.3
'eslint': ^9.39.1
'eslint-plugin-format': ^1.1.0
'eslint-plugin-vue': ^10.6.2
'jiti': ^2.6.1
'jsdom': ^27.3.0
'lint-staged': ^16.2.7
'simple-git-hooks': ^2.13.1
'terser': ^5.44.1
'typescript': ~5.9.3
'unplugin-auto-import': ^20.3.0
'unplugin-icons': ^22.5.0
'unplugin-vue-components': ^30.0.0
'vite': ^7.2.7
'vite-plugin-pwa': ^1.2.0
'vitest': ^4.0.15
'vue-tsc': ^3.1.8
'workbox-window': ^7.4.0
'wrangler': ^4.54.0
'@vueuse/core': ^14.1.0
'@vueuse/integrations': ^14.1.0
'@vueuse/router': ^14.1.0
better-auth: ^1.4.6
cypress: ^15.7.1
dayjs: ^1.11.19
dotenv: ^17.2.3
eslint: ^9.39.1
eslint-plugin-format: ^1.1.0
eslint-plugin-vue: ^10.6.2
ethers: ^6.16.0
html2canvas: ^1.4.1
ionicons: ^8.0.13
jiti: ^2.6.1
jsdom: ^27.3.0
jsqr: ^1.4.0
lightweight-charts: ^5.1.0
lint-staged: ^16.2.7
lodash-es: ^4.17.21
markdown-it: ^14.1.0
pinia: ^3.0.4
qr-scanner-wechat: ^0.1.3
qrcode: ^1.5.4
simple-git-hooks: ^2.13.1
tailwindcss: ^4.1.18
terser: ^5.44.1
typescript: ~5.9.3
unplugin-auto-import: ^20.3.0
unplugin-icons: ^22.5.0
unplugin-vue-components: ^30.0.0
vconsole: ^3.15.1
vee-validate: ^4.15.1
vite: ^7.2.7
vite-plugin-pwa: ^1.2.0
vitest: ^4.0.15
vue: ^3.5.25
vue-i18n: ^11.2.2
vue-router: ^4.6.3
vue-tsc: ^3.1.8
workbox-window: ^7.4.0
wrangler: ^4.54.0
zod: ^3.24.1
catalogMode: prefer
shellEmulator: true
trustPolicy: off

View File

@@ -1,7 +1,6 @@
import type { VersionInfo } from "@/api/types";
import { App } from "@capacitor/app";
import { alertController } from "@ionic/vue";
import { client } from "@/api";
import { i18n } from "@/locales";
/**
@@ -28,12 +27,6 @@ function compareVersion(version1: string, version2: string): number {
return 0;
}
const updateUrls = {
ios: "https://apps.apple.com/app/id123456789",
android: "https://play.google.com/store/apps/details?id=riwa.ionic.app",
browser: "",
} as const;
/**
* 应用更新检查
*/
@@ -81,40 +74,53 @@ export function useAppUpdate() {
updateUrl?: string;
}> {
state.isChecking = true;
if (import.meta.env.DEV) {
const current = await getCurrentVersion();
// 开发环境下不检查更新
state.currentVersion = current;
state.hasUpdate = false;
state.latestVersion = current;
state.forceUpdate = false;
state.updateMessage = "";
state.updateUrl = "";
state.isChecking = false;
return {
hasUpdate: false,
currentVersion: current,
};
}
try {
// 1. 获取当前版本
const current = await getCurrentVersion();
state.currentVersion = current;
// 2. 从服务器检查最新版本
// TODO: 后端接口实现后替换为真实 API 调用
// const response = await client.api.app.version.get({
// query: {
// platform: platform === 'ios' ? 'ios' : platform === 'android' ? 'android' : 'web',
// currentVersion: current,
// },
// })
// 模拟后端返回数据(开发阶段)
await new Promise(resolve => setTimeout(resolve, 500));
const versionInfo: VersionInfo = {
version: "0.0.1", // 模拟有新版本
forceUpdate: false,
updateMessage: "修复了一些问题并优化了性能",
updateUrl: updateUrls[platform] || "",
minSupportVersion: "0.0.1",
};
// 2. PWA 应用检查 Service Worker 更新
if (platform === "browser" && "serviceWorker" in navigator) {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
// 触发 Service Worker 更新检查
await registration.update();
}
}
// 真实 API 调用后的逻辑
// if (response.error) {
// console.error('检查更新失败:', response.error)
// return {
// hasUpdate: false,
// currentVersion: current,
// }
// }
// const versionInfo = response.data as VersionInfo
// 3. 从 Cloudflare Function 检查最新版本
const apiUrl = window.location.origin;
const response = await fetch(
`${apiUrl}/api/version?platform=${platform}&currentVersion=${current}`,
{ cache: "no-cache" }, // 不使用缓存,确保获取最新版本信息
);
// 3. 版本比较
if (!response.ok) {
console.error("检查更新失败:", response.statusText);
return {
hasUpdate: false,
currentVersion: current,
};
}
const versionInfo: VersionInfo = await response.json();
// 4. 版本比较
const isNewVersion = compareVersion(current, versionInfo.version) < 0;
// 更新状态
@@ -186,7 +192,7 @@ export function useAppUpdate() {
text: i18n.global.t("app.update.now"),
role: "confirm",
handler: () => {
openStoreUpdate();
applyUpdate();
},
},
]
@@ -199,7 +205,7 @@ export function useAppUpdate() {
text: i18n.global.t("app.update.now"),
role: "confirm",
handler: () => {
openStoreUpdate();
applyUpdate();
},
},
],
@@ -218,22 +224,63 @@ export function useAppUpdate() {
return;
}
// Web 应用检查 Service Worker 更新
if ("serviceWorker" in navigator) {
// PWA 应用更新流程
if (platform === "browser" && "serviceWorker" in navigator) {
const registration = await navigator.serviceWorker.getRegistration();
if (registration?.waiting) {
// 通知 Service Worker 跳过等待,立即激活
registration.waiting.postMessage({ type: "SKIP_WAITING" });
// 等待 Service Worker 激活后重新加载页面
navigator.serviceWorker.addEventListener("controllerchange", () => {
window.location.reload();
});
return;
if (registration) {
// 检查是否有等待激活的 Service Worker
const waitingWorker = registration.waiting;
if (waitingWorker) {
// 通知 Service Worker 跳过等待,立即激活
(waitingWorker as ServiceWorker).postMessage({ type: "SKIP_WAITING" });
// 等待控制器变更后重新加载
navigator.serviceWorker.addEventListener("controllerchange", () => {
window.location.reload();
}, { once: true });
return;
}
// 检查是否有正在安装的 Service Worker
if (registration.installing) {
const installingWorker = registration.installing;
installingWorker.addEventListener("statechange", () => {
if (installingWorker.state === "installed" && navigator.serviceWorker.controller) {
// 新的 Service Worker 已安装,通知它跳过等待
(installingWorker as ServiceWorker).postMessage({ type: "SKIP_WAITING" });
navigator.serviceWorker.addEventListener("controllerchange", () => {
window.location.reload();
}, { once: true });
}
});
return;
}
// 手动触发更新检查
try {
await registration.update();
// 等待一小段时间让更新完成
await new Promise(resolve => setTimeout(resolve, 1000));
// 如果有等待的 Service Worker激活它
const waitingWorker = registration.waiting;
if (waitingWorker) {
(waitingWorker as ServiceWorker).postMessage({ type: "SKIP_WAITING" });
navigator.serviceWorker.addEventListener("controllerchange", () => {
window.location.reload();
}, { once: true });
return;
}
}
catch (error) {
console.error("Service Worker 更新失败:", error);
}
}
}
// 如果没有 Service Worker,直接重新加载
// 如果以上都没有执行,直接重新加载页面
window.location.reload();
}

1
src/vite-env.d.ts vendored
View File

@@ -1,6 +1,7 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/client" />
/// <reference types="unplugin-icons/types/vue" />
/// <reference types="@cloudflare/workers-types" />
import type { MessageSchema } from "@/locales";
import {

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["ESNext", "DOM"],
"lib": ["ESNext", "DOM", "WebWorker"],
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "bundler",