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