feat: 更新 PWA 安装逻辑,添加 Service Worker 注册和安装提示处理
This commit is contained in:
@@ -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(),
|
||||
};
|
||||
|
||||
25
src/main.ts
25
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) => {
|
||||
|
||||
@@ -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
1
src/vite-env.d.ts
vendored
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user