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() {
const deferredPrompt = ref<BeforeInstallPromptEvent | null>(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()) {
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
window.addEventListener("beforeinstallprompt", (e: Event) => {
console.log("[PWA] beforeinstallprompt event fired");
e.preventDefault();
deferredPrompt.value = e as BeforeInstallPromptEvent;
showInstallButton.value = true;
canInstall.value = true;
});
// 监听安装成功事件
window.addEventListener("appinstalled", () => {
console.log("[PWA] App installed successfully");
deferredPrompt.value = null;
showInstallButton.value = false;
canInstall.value = false;
isInstalled.value = true;
});
// iOS 设备也显示安装按钮iOS 不支持 beforeinstallprompt
if (isIOSSafari()) {
console.log("[PWA] iOS Safari detected, showing install button");
showInstallButton.value = true;
}
}
canInstall.value = true;
}
});
async function promptInstall() {
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 };
}
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;
canInstall.value = false;
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 {
showInstallButton: computed(() => showInstallButton.value && !isInstalled.value),
canInstall,
isInstalled,
promptInstall,
install,
isIOS: isIOS(),
isIOSSafari: isIOSSafari(),
};

View File

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

View File

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

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

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