From e4e4d50ca87d6d2bbc519422afb60c339993f208 Mon Sep 17 00:00:00 2001 From: Seven Date: Tue, 6 Jan 2026 22:40:51 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20PWA=20=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0=20Servic?= =?UTF-8?q?e=20Worker=20=E6=B3=A8=E5=86=8C=E5=92=8C=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/usePWAInstall.ts | 119 ++++++++++++++++--------------- src/main.ts | 25 +++++++ src/views/pwa/download.vue | 47 +++++++----- src/vite-env.d.ts | 1 + 4 files changed, 115 insertions(+), 77 deletions(-) diff --git a/src/composables/usePWAInstall.ts b/src/composables/usePWAInstall.ts index f22702a..397b555 100644 --- a/src/composables/usePWAInstall.ts +++ b/src/composables/usePWAInstall.ts @@ -5,28 +5,9 @@ interface BeforeInstallPromptEvent extends Event { export function usePWAInstall() { const deferredPrompt = ref(null); - const showInstallButton = ref(false); + const canInstall = ref(false); const isInstalled = ref(false); - // 检查是否已安装 - function checkIfInstalled() { - // 检查是否在独立模式下运行(已安装) - if (window.matchMedia("(display-mode: standalone)").matches) { - isInstalled.value = true; - console.log("[PWA] Already installed (standalone mode)"); - return true; - } - - // 检查 iOS Safari 独立模式 - if ((window.navigator as any).standalone === true) { - isInstalled.value = true; - console.log("[PWA] Already installed (iOS standalone)"); - return true; - } - - return false; - } - // 检测是否是 iOS 设备 function isIOS() { return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream; @@ -37,64 +18,84 @@ export function usePWAInstall() { return isIOS() && !(window.navigator as any).standalone; } - // 在全局作用域设置监听器(不在 onMounted 中) - if (typeof window !== "undefined") { + // 使用 onMounted 确保在客户端环境执行 + onMounted(() => { + console.log("[PWA] usePWAInstall mounted"); + // 检查是否已安装 - if (!checkIfInstalled()) { - // 监听安装提示事件(仅 Android/Chrome) - window.addEventListener("beforeinstallprompt", (e: Event) => { - console.log("[PWA] beforeinstallprompt event fired"); - e.preventDefault(); - deferredPrompt.value = e as BeforeInstallPromptEvent; - showInstallButton.value = true; - }); - - // 监听安装成功事件 - window.addEventListener("appinstalled", () => { - console.log("[PWA] App installed successfully"); - deferredPrompt.value = null; - showInstallButton.value = false; - isInstalled.value = true; - }); - - // iOS 设备也显示安装按钮(iOS 不支持 beforeinstallprompt) - if (isIOSSafari()) { - console.log("[PWA] iOS Safari detected, showing install button"); - showInstallButton.value = true; - } + if (window.matchMedia("(display-mode: standalone)").matches) { + isInstalled.value = true; + console.log("[PWA] Already installed (standalone mode)"); + return; } - } - async function promptInstall() { + // 检查 iOS Safari 独立模式 + if ((window.navigator as any).standalone === true) { + isInstalled.value = true; + console.log("[PWA] Already installed (iOS standalone)"); + return; + } + + // 监听安装提示事件(仅 Android/Chrome) + window.addEventListener("beforeinstallprompt", (e: Event) => { + console.log("[PWA] beforeinstallprompt event fired"); + e.preventDefault(); + deferredPrompt.value = e as BeforeInstallPromptEvent; + canInstall.value = true; + }); + + // 监听安装成功事件 + window.addEventListener("appinstalled", () => { + console.log("[PWA] App installed successfully"); + deferredPrompt.value = null; + canInstall.value = false; + isInstalled.value = true; + }); + + // iOS 设备也显示安装按钮(iOS 不支持 beforeinstallprompt) + if (isIOSSafari()) { + console.log("[PWA] iOS Safari detected, showing install button"); + canInstall.value = true; + } + }); + + async function install() { // iOS 设备返回特殊标识,由组件处理 if (isIOSSafari()) { console.log("[PWA] iOS: Showing manual install guide"); - return { outcome: "ios-instruction" as const }; + return { outcome: "ios-instruction" as const, success: false }; } // Android/Chrome 设备 if (!deferredPrompt.value) { console.log("[PWA] No deferred prompt available"); - return { outcome: "not-available" as const }; + return { outcome: "not-available" as const, success: false }; } - console.log("[PWA] Showing install prompt"); - await deferredPrompt.value.prompt(); - const { outcome } = await deferredPrompt.value.userChoice; - console.log("[PWA] User choice:", outcome); + try { + console.log("[PWA] Showing install prompt"); + await deferredPrompt.value.prompt(); + const { outcome } = await deferredPrompt.value.userChoice; + console.log("[PWA] User choice:", outcome); - if (outcome === "accepted") { - showInstallButton.value = false; - deferredPrompt.value = null; + if (outcome === "accepted") { + canInstall.value = false; + deferredPrompt.value = null; + return { outcome, success: true }; + } + + return { outcome, success: false }; + } + catch (error) { + console.error("[PWA] Install failed:", error); + return { outcome: "error" as const, success: false }; } - - return { outcome }; } return { - showInstallButton: computed(() => showInstallButton.value && !isInstalled.value), + canInstall, isInstalled, - promptInstall, + install, isIOS: isIOS(), isIOSSafari: isIOSSafari(), }; diff --git a/src/main.ts b/src/main.ts index 44ba798..922bf48 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import { IonicVue } from "@ionic/vue"; import { createPinia } from "pinia"; +import { useRegisterSW } from "virtual:pwa-register/vue"; import { createApp } from "vue"; import uiComponents from "@/ui"; import App from "./App.vue"; @@ -41,6 +42,30 @@ import "./theme/ionic.css"; useTheme(); +// 注册 PWA Service Worker(使用 Vue 3 Composition API) +const { offlineReady, needRefresh, updateServiceWorker } = useRegisterSW({ + onRegistered(registration) { + console.log("[PWA] Service Worker registered:", registration); + }, + onRegisterError(error) { + console.error("[PWA] Service Worker registration failed:", error); + }, + immediate: true, +}); + +// 监听 PWA 状态 +watch(offlineReady, (ready) => { + if (ready) { + console.log("[PWA] App ready to work offline."); + } +}); + +watch(needRefresh, (refresh) => { + if (refresh) { + console.log("[PWA] New content available, please refresh."); + } +}); + function initTradingView() { const { VITE_TRADINGVIEW_LIBRARY_URL } = useEnv(); const promise1 = new Promise((resolve) => { diff --git a/src/views/pwa/download.vue b/src/views/pwa/download.vue index 62f778d..cb77733 100644 --- a/src/views/pwa/download.vue +++ b/src/views/pwa/download.vue @@ -6,46 +6,52 @@ import IconParkOutlineDownload from "~icons/icon-park-outline/download"; import IconParkOutlinePhone from "~icons/icon-park-outline/phone"; import IconParkOutlineShare from "~icons/icon-park-outline/share"; -const { showInstallButton, isInstalled, promptInstall, isIOS, isIOSSafari } = usePWAInstall(); +const { canInstall, isInstalled, install, isIOS, isIOSSafari } = usePWAInstall(); const platform = usePlatform(); const appName = "Riwa"; const isLoading = ref(false); -const showIOSGuide = ref(false); + +// 调试日志 +onMounted(() => { + console.log("[Download Page] Mounted"); + console.log("[Download Page] platform:", platform); + console.log("[Download Page] canInstall:", canInstall.value); + console.log("[Download Page] isInstalled:", isInstalled.value); + console.log("[Download Page] isIOS:", isIOS); + console.log("[Download Page] isIOSSafari:", isIOSSafari); +}); // 检测用户是否卸载过应用 const wasUninstalled = computed(() => { - // 如果localStorage中有标记表示曾经安装过,但当前未安装 const wasInstalled = localStorage.getItem("pwa-was-installed"); return wasInstalled === "true" && !isInstalled.value; }); -// 显示下载按钮的条件: -// 1. 浏览器环境 -// 2. 未安装或曾经卸载过 -// 3. Android 或有安装提示事件 +// 显示下载按钮的条件 const shouldShowDownloadButton = computed(() => { - return platform === "browser" && (!isInstalled.value || wasUninstalled.value) && (showInstallButton.value || wasUninstalled.value); + const result = platform === "browser" && !isInstalled.value && (canInstall.value || isIOSSafari); + console.log("[Download Page] shouldShowDownloadButton:", result); + return result; }); -// iOS 安装指引 -async function showIOSInstallGuide() { - showIOSGuide.value = true; -} - // 处理安装 async function handleInstall() { if (isIOSSafari) { - await showIOSInstallGuide(); + const alert = await alertController.create({ + header: "iOS 安装指引", + message: "请点击浏览器底部的分享按钮,然后选择\"添加到主屏幕\"", + buttons: ["知道了"], + }); + await alert.present(); return; } isLoading.value = true; try { - const result = await promptInstall(); + const result = await install(); - if (result.outcome === "accepted") { - // 记录已安装状态 + if (result.success && result.outcome === "accepted") { localStorage.setItem("pwa-was-installed", "true"); const alert = await alertController.create({ @@ -56,7 +62,12 @@ async function handleInstall() { await alert.present(); } else if (result.outcome === "ios-instruction") { - await showIOSInstallGuide(); + const alert = await alertController.create({ + header: "iOS 安装指引", + message: "请点击浏览器底部的分享按钮,然后选择\"添加到主屏幕\"", + buttons: ["知道了"], + }); + await alert.present(); } } catch (error) { diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 59a3251..779f7aa 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,4 +1,5 @@ /// +/// /// import type { MessageSchema } from "@/locales";