feat: 添加 PWA 安装支持,包含安装提示和 iOS 指导页面

This commit is contained in:
2026-01-04 11:54:48 +07:00
parent c3f4c2709d
commit 03c9ddac66
5 changed files with 329 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { alertController, modalController } from "@ionic/vue";
import IOSInstallGuide from "./ios-install-guide.vue";
const { t } = useI18n();
const {
showInstallButton,
promptInstall,
isIOS,
} = usePWAInstall();
async function handleInstall() {
const result = await promptInstall();
if (result.outcome === "accepted") {
const alert = await alertController.create({
header: t("pwa.install.success"),
message: t("pwa.install.successMessage"),
buttons: [t("common.ok")],
});
await alert.present();
}
else if (result.outcome === "ios-instruction") {
const modal = await modalController.create({
component: IOSInstallGuide,
});
await modal.present();
}
}
</script>
<template>
<IonButton
v-if="showInstallButton"
expand="block"
@click="handleInstall"
>
<IIcBaselineDownload class="mr-2" />
{{ isIOS ? t('pwa.install.addToHomeScreen') : t('pwa.install.installApp') }}
</IonButton>
</template>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import { modalController } from "@ionic/vue";
const { t } = useI18n();
function dismiss() {
modalController.dismiss();
}
</script>
<template>
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>{{ t('pwa.install.howToInstall') }}</IonTitle>
<IonButtons slot="end">
<IonButton @click="dismiss">
关闭
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent class="ion-padding">
<div class="space-y-4">
<div class="text-center">
<IIcBaselineIosShare class="text-6xl text-primary mx-auto mb-4" />
<h2 class="text-lg font-semibold mb-2">
{{ t('pwa.install.iosTitle') }}
</h2>
</div>
<IonCard>
<IonCardContent>
<div class="space-y-3">
<p>{{ t('pwa.install.iosStep1') }}</p>
<p>{{ t('pwa.install.iosStep2') }}</p>
<p>{{ t('pwa.install.iosStep3') }}</p>
</div>
</IonCardContent>
</IonCard>
<div class="text-center text-sm text-gray-500">
{{ t('pwa.install.iosNote') }}
</div>
</div>
</IonContent>
</IonPage>
</template>

View File

@@ -0,0 +1,101 @@
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
}
export function usePWAInstall() {
const deferredPrompt = ref<BeforeInstallPromptEvent | null>(null);
const showInstallButton = 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;
}
// 检测是否是 iOS Safari未安装
function isIOSSafari() {
return isIOS() && !(window.navigator as any).standalone;
}
// 在全局作用域设置监听器(不在 onMounted 中)
if (typeof window !== "undefined") {
// 检查是否已安装
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;
}
}
}
async function promptInstall() {
// iOS 设备返回特殊标识,由组件处理
if (isIOSSafari()) {
console.log("[PWA] iOS: Showing manual install guide");
return { outcome: "ios-instruction" as const };
}
// Android/Chrome 设备
if (!deferredPrompt.value) {
console.log("[PWA] No deferred prompt available");
return { outcome: "not-available" as const };
}
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;
}
return { outcome };
}
return {
showInstallButton: computed(() => showInstallButton.value && !isInstalled.value),
isInstalled,
promptInstall,
isIOS: isIOS(),
isIOSSafari: isIOSSafari(),
};
}