|
|
@@ -0,0 +1,114 @@
|
|
|
+<template>
|
|
|
+ <button
|
|
|
+ v-if="canInstall"
|
|
|
+ type="button"
|
|
|
+ class="pwa-install-button"
|
|
|
+ aria-label="安装 RunForge 应用"
|
|
|
+ @click="installApp"
|
|
|
+ >
|
|
|
+ <icon-download class="install-icon" />
|
|
|
+ <span>安装应用</span>
|
|
|
+ </button>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
+import { Message, Modal } from '@arco-design/web-vue'
|
|
|
+import { isElectron } from '@/utils/electron'
|
|
|
+
|
|
|
+const installPrompt = ref(null)
|
|
|
+const isInstalled = ref(false)
|
|
|
+const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent)
|
|
|
+
|
|
|
+const canInstall = computed(() => {
|
|
|
+ return !isElectron() && !isInstalled.value && (Boolean(installPrompt.value) || isIos)
|
|
|
+})
|
|
|
+
|
|
|
+function handleBeforeInstallPrompt(event) {
|
|
|
+ event.preventDefault()
|
|
|
+ installPrompt.value = event
|
|
|
+}
|
|
|
+
|
|
|
+function handleAppInstalled() {
|
|
|
+ installPrompt.value = null
|
|
|
+ isInstalled.value = true
|
|
|
+ Message.success('RunForge 已安装')
|
|
|
+}
|
|
|
+
|
|
|
+async function installApp() {
|
|
|
+ if (isIos && !installPrompt.value) {
|
|
|
+ Modal.info({
|
|
|
+ title: '安装 RunForge',
|
|
|
+ content: '请点击浏览器底部的“分享”按钮,然后选择“添加到主屏幕”。'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const promptEvent = installPrompt.value
|
|
|
+ if (!promptEvent) return
|
|
|
+
|
|
|
+ await promptEvent.prompt()
|
|
|
+ const { outcome } = await promptEvent.userChoice
|
|
|
+ installPrompt.value = null
|
|
|
+
|
|
|
+ if (outcome === 'accepted') {
|
|
|
+ Message.success('正在安装 RunForge')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ isInstalled.value = window.matchMedia('(display-mode: standalone)').matches
|
|
|
+ || window.navigator.standalone === true
|
|
|
+ window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
|
|
+ window.addEventListener('appinstalled', handleAppInstalled)
|
|
|
+})
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
|
|
+ window.removeEventListener('appinstalled', handleAppInstalled)
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.pwa-install-button {
|
|
|
+ position: fixed;
|
|
|
+ right: max(20px, env(safe-area-inset-right));
|
|
|
+ bottom: max(20px, env(safe-area-inset-bottom));
|
|
|
+ z-index: 1000;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 7px;
|
|
|
+ padding: 10px 16px;
|
|
|
+ border: 0;
|
|
|
+ border-radius: 999px;
|
|
|
+ color: #fff;
|
|
|
+ background: rgb(var(--primary-6, 22, 93, 255));
|
|
|
+ box-shadow: 0 8px 24px rgba(22, 93, 255, 0.28);
|
|
|
+ font: inherit;
|
|
|
+ font-weight: 500;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.pwa-install-button:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 10px 28px rgba(22, 93, 255, 0.36);
|
|
|
+}
|
|
|
+
|
|
|
+.pwa-install-button:focus-visible {
|
|
|
+ outline: 3px solid rgba(22, 93, 255, 0.3);
|
|
|
+ outline-offset: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.install-icon {
|
|
|
+ font-size: 17px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .pwa-install-button {
|
|
|
+ right: max(14px, env(safe-area-inset-right));
|
|
|
+ bottom: max(14px, env(safe-area-inset-bottom));
|
|
|
+ padding: 9px 14px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|