Files
DAO_DE_JING/.vitepress/theme/InstallApp.vue
T
dekun ee74d31dac Fix image loading and improve PWA install guidance.
Allow /images/ as public static assets and show install hints on all pages including mobile and HTTP deployments.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 08:56:14 +08:00

183 lines
4.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
type InstallMode = 'native' | 'ios' | 'android-menu' | 'desktop' | 'https-required'
const showInstall = ref(false)
const dismissed = ref(false)
const installMode = ref<InstallMode | null>(null)
const isMobile = ref(false)
let deferredPrompt: BeforeInstallPromptEvent | null = null
let fallbackTimer: ReturnType<typeof setTimeout> | null = null
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
}
const visible = computed(() => !dismissed.value && showInstall.value && installMode.value !== null)
function needsHttpsForInstall() {
return (
!window.isSecureContext &&
!['localhost', '127.0.0.1'].includes(location.hostname)
)
}
function onBeforeInstallPrompt(event: Event) {
event.preventDefault()
deferredPrompt = event as BeforeInstallPromptEvent
installMode.value = 'native'
showInstall.value = true
if (fallbackTimer) {
clearTimeout(fallbackTimer)
fallbackTimer = null
}
}
onMounted(() => {
isMobile.value = window.matchMedia('(max-width: 768px)').matches
if (sessionStorage.getItem('install-app-dismissed') === '1') {
dismissed.value = true
return
}
const isStandalone =
window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as Navigator & { standalone?: boolean }).standalone
if (isStandalone) return
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
if (isIOS) {
installMode.value = 'ios'
showInstall.value = true
return
}
if (needsHttpsForInstall()) {
installMode.value = 'https-required'
showInstall.value = true
return
}
window.addEventListener('beforeinstallprompt', onBeforeInstallPrompt)
fallbackTimer = setTimeout(() => {
if (deferredPrompt || installMode.value) return
installMode.value = isMobile.value ? 'android-menu' : 'desktop'
showInstall.value = true
}, 2500)
})
onUnmounted(() => {
window.removeEventListener('beforeinstallprompt', onBeforeInstallPrompt)
if (fallbackTimer) clearTimeout(fallbackTimer)
})
async function installApp() {
if (!deferredPrompt) return
await deferredPrompt.prompt()
await deferredPrompt.userChoice
deferredPrompt = null
showInstall.value = false
}
function dismiss() {
dismissed.value = true
sessionStorage.setItem('install-app-dismissed', '1')
}
</script>
<template>
<div v-if="visible" class="install-app">
<div class="install-app__content">
<p v-if="installMode === 'native'" class="install-app__text">
可将道德经安装到桌面 App 一样使用
</p>
<p v-else-if="installMode === 'ios'" class="install-app__text">
iPhoneSafari 底部分享 添加到主屏幕阅读更方便
</p>
<p v-else-if="installMode === 'android-menu'" class="install-app__text">
AndroidChrome 右上角菜单 安装应用添加到主屏幕
</p>
<p v-else-if="installMode === 'desktop'" class="install-app__text">
电脑地址栏右侧点击安装图标或浏览器菜单 安装道德经
</p>
<p v-else class="install-app__text">
安装应用需通过 HTTPS 域名访问内网 http://IP 地址只能添加快捷方式,无法真正安装)。
</p>
<div class="install-app__actions">
<button
v-if="installMode === 'native'"
type="button"
class="install-app__primary"
@click="installApp"
>
安装应用
</button>
<button type="button" class="install-app__ghost" @click="dismiss">知道了</button>
</div>
</div>
</div>
</template>
<style scoped>
.install-app {
position: fixed;
right: 16px;
bottom: calc(16px + env(safe-area-inset-bottom, 0));
z-index: 50;
max-width: min(92vw, 360px);
}
.install-app__content {
padding: 14px 16px;
border-radius: 12px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
box-shadow: var(--vp-shadow-3);
}
.install-app__text {
margin: 0 0 12px;
font-size: 14px;
line-height: 1.5;
color: var(--vp-c-text-1);
}
.install-app__actions {
display: flex;
gap: 8px;
}
.install-app__primary,
.install-app__ghost {
border-radius: 8px;
padding: 10px 14px;
font-size: 14px;
cursor: pointer;
}
.install-app__primary {
border: none;
background: var(--vp-c-brand-1);
color: var(--vp-c-bg);
}
.install-app__ghost {
border: 1px solid var(--vp-c-divider);
background: transparent;
color: var(--vp-c-text-2);
}
@media (max-width: 768px) {
.install-app {
left: 12px;
right: 12px;
max-width: none;
}
}
</style>