diff --git a/functions/api/version.ts b/functions/api/version.ts new file mode 100644 index 0000000..da4a3ff --- /dev/null +++ b/functions/api/version.ts @@ -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 = 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 = async () => { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }); +}; diff --git a/functions/tsconfig.json b/functions/tsconfig.json new file mode 100644 index 0000000..62df338 --- /dev/null +++ b/functions/tsconfig.json @@ -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"] +} diff --git a/package.json b/package.json index 2a810d2..3363cb3 100644 --- a/package.json +++ b/package.json @@ -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:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 302f560..32a3fa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 52f3cd8..1a7a325 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 diff --git a/src/composables/useAppUpdate.ts b/src/composables/useAppUpdate.ts index 65bf604..3e5c844 100644 --- a/src/composables/useAppUpdate.ts +++ b/src/composables/useAppUpdate.ts @@ -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}¤tVersion=${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(); } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 779f7aa..0b29c2b 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,6 +1,7 @@ /// /// /// +/// import type { MessageSchema } from "@/locales"; import { diff --git a/tsconfig.json b/tsconfig.json index dcd0f73..97e3ba3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ESNext", "jsx": "preserve", - "lib": ["ESNext", "DOM"], + "lib": ["ESNext", "DOM", "WebWorker"], "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "bundler",