feat: 更新 PWA 安装逻辑,添加 Service Worker 注册和安装提示处理

This commit is contained in:
2026-01-06 22:40:51 +07:00
parent db3a2327c8
commit e4e4d50ca8
4 changed files with 115 additions and 77 deletions

View File

@@ -5,28 +5,9 @@ interface BeforeInstallPromptEvent extends Event {
export function usePWAInstall() { export function usePWAInstall() {
const deferredPrompt = ref<BeforeInstallPromptEvent | null>(null); const deferredPrompt = ref<BeforeInstallPromptEvent | null>(null);
const showInstallButton = ref(false); const canInstall = ref(false);
const isInstalled = 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 设备 // 检测是否是 iOS 设备
function isIOS() { function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream; 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; return isIOS() && !(window.navigator as any).standalone;
} }
// 在全局作用域设置监听器(不在 onMounted 中) // 使用 onMounted 确保在客户端环境执行
if (typeof window !== "undefined") { onMounted(() => {
console.log("[PWA] usePWAInstall mounted");
// 检查是否已安装 // 检查是否已安装
if (!checkIfInstalled()) { if (window.matchMedia("(display-mode: standalone)").matches) {
isInstalled.value = true;
console.log("[PWA] Already installed (standalone mode)");
return;
}
// 检查 iOS Safari 独立模式
if ((window.navigator as any).standalone === true) {
isInstalled.value = true;
console.log("[PWA] Already installed (iOS standalone)");
return;
}
// 监听安装提示事件(仅 Android/Chrome // 监听安装提示事件(仅 Android/Chrome
window.addEventListener("beforeinstallprompt", (e: Event) => { window.addEventListener("beforeinstallprompt", (e: Event) => {
console.log("[PWA] beforeinstallprompt event fired"); console.log("[PWA] beforeinstallprompt event fired");
e.preventDefault(); e.preventDefault();
deferredPrompt.value = e as BeforeInstallPromptEvent; deferredPrompt.value = e as BeforeInstallPromptEvent;
showInstallButton.value = true; canInstall.value = true;
}); });
// 监听安装成功事件 // 监听安装成功事件
window.addEventListener("appinstalled", () => { window.addEventListener("appinstalled", () => {
console.log("[PWA] App installed successfully"); console.log("[PWA] App installed successfully");
deferredPrompt.value = null; deferredPrompt.value = null;
showInstallButton.value = false; canInstall.value = false;
isInstalled.value = true; isInstalled.value = true;
}); });
// iOS 设备也显示安装按钮iOS 不支持 beforeinstallprompt // iOS 设备也显示安装按钮iOS 不支持 beforeinstallprompt
if (isIOSSafari()) { if (isIOSSafari()) {
console.log("[PWA] iOS Safari detected, showing install button"); console.log("[PWA] iOS Safari detected, showing install button");
showInstallButton.value = true; canInstall.value = true;
}
}
} }
});
async function promptInstall() { async function install() {
// iOS 设备返回特殊标识,由组件处理 // iOS 设备返回特殊标识,由组件处理
if (isIOSSafari()) { if (isIOSSafari()) {
console.log("[PWA] iOS: Showing manual install guide"); console.log("[PWA] iOS: Showing manual install guide");
return { outcome: "ios-instruction" as const }; return { outcome: "ios-instruction" as const, success: false };
} }
// Android/Chrome 设备 // Android/Chrome 设备
if (!deferredPrompt.value) { if (!deferredPrompt.value) {
console.log("[PWA] No deferred prompt available"); console.log("[PWA] No deferred prompt available");
return { outcome: "not-available" as const }; return { outcome: "not-available" as const, success: false };
} }
try {
console.log("[PWA] Showing install prompt"); console.log("[PWA] Showing install prompt");
await deferredPrompt.value.prompt(); await deferredPrompt.value.prompt();
const { outcome } = await deferredPrompt.value.userChoice; const { outcome } = await deferredPrompt.value.userChoice;
console.log("[PWA] User choice:", outcome); console.log("[PWA] User choice:", outcome);
if (outcome === "accepted") { if (outcome === "accepted") {
showInstallButton.value = false; canInstall.value = false;
deferredPrompt.value = null; deferredPrompt.value = null;
return { outcome, success: true };
} }
return { outcome }; return { outcome, success: false };
}
catch (error) {
console.error("[PWA] Install failed:", error);
return { outcome: "error" as const, success: false };
}
} }
return { return {
showInstallButton: computed(() => showInstallButton.value && !isInstalled.value), canInstall,
isInstalled, isInstalled,
promptInstall, install,
isIOS: isIOS(), isIOS: isIOS(),
isIOSSafari: isIOSSafari(), isIOSSafari: isIOSSafari(),
}; };

View File

@@ -1,5 +1,6 @@
import { IonicVue } from "@ionic/vue"; import { IonicVue } from "@ionic/vue";
import { createPinia } from "pinia"; import { createPinia } from "pinia";
import { useRegisterSW } from "virtual:pwa-register/vue";
import { createApp } from "vue"; import { createApp } from "vue";
import uiComponents from "@/ui"; import uiComponents from "@/ui";
import App from "./App.vue"; import App from "./App.vue";
@@ -41,6 +42,30 @@ import "./theme/ionic.css";
useTheme(); 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() { function initTradingView() {
const { VITE_TRADINGVIEW_LIBRARY_URL } = useEnv(); const { VITE_TRADINGVIEW_LIBRARY_URL } = useEnv();
const promise1 = new Promise((resolve) => { const promise1 = new Promise((resolve) => {

View File

@@ -6,46 +6,52 @@ import IconParkOutlineDownload from "~icons/icon-park-outline/download";
import IconParkOutlinePhone from "~icons/icon-park-outline/phone"; import IconParkOutlinePhone from "~icons/icon-park-outline/phone";
import IconParkOutlineShare from "~icons/icon-park-outline/share"; 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 platform = usePlatform();
const appName = "Riwa"; const appName = "Riwa";
const isLoading = ref(false); 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(() => { const wasUninstalled = computed(() => {
// 如果localStorage中有标记表示曾经安装过但当前未安装
const wasInstalled = localStorage.getItem("pwa-was-installed"); const wasInstalled = localStorage.getItem("pwa-was-installed");
return wasInstalled === "true" && !isInstalled.value; return wasInstalled === "true" && !isInstalled.value;
}); });
// 显示下载按钮的条件 // 显示下载按钮的条件
// 1. 浏览器环境
// 2. 未安装或曾经卸载过
// 3. Android 或有安装提示事件
const shouldShowDownloadButton = computed(() => { 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() { async function handleInstall() {
if (isIOSSafari) { if (isIOSSafari) {
await showIOSInstallGuide(); const alert = await alertController.create({
header: "iOS 安装指引",
message: "请点击浏览器底部的分享按钮,然后选择\"添加到主屏幕\"",
buttons: ["知道了"],
});
await alert.present();
return; return;
} }
isLoading.value = true; isLoading.value = true;
try { 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"); localStorage.setItem("pwa-was-installed", "true");
const alert = await alertController.create({ const alert = await alertController.create({
@@ -56,7 +62,12 @@ async function handleInstall() {
await alert.present(); await alert.present();
} }
else if (result.outcome === "ios-instruction") { else if (result.outcome === "ios-instruction") {
await showIOSInstallGuide(); const alert = await alertController.create({
header: "iOS 安装指引",
message: "请点击浏览器底部的分享按钮,然后选择\"添加到主屏幕\"",
buttons: ["知道了"],
});
await alert.present();
} }
} }
catch (error) { catch (error) {

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

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