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>
This commit is contained in:
dekun
2026-06-15 08:56:14 +08:00
parent ecd4f25700
commit ee74d31dac
2 changed files with 58 additions and 21 deletions
+57 -21
View File
@@ -1,26 +1,39 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vitepress'
import { computed, onMounted, onUnmounted, ref } from 'vue'
const route = useRoute()
const showAndroidInstall = ref(false)
const showIOSHint = ref(false)
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 isDocPage = computed(() => route.path !== '/')
const visible = computed(() => !dismissed.value && showInstall.value && installMode.value !== null)
const visible = computed(() => {
if (dismissed.value) return false
if (isMobile.value && isDocPage.value) return false
return showAndroidInstall.value || showIOSHint.value
})
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
@@ -38,15 +51,29 @@ onMounted(() => {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
if (isIOS) {
showIOSHint.value = true
installMode.value = 'ios'
showInstall.value = true
return
}
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault()
deferredPrompt = event as BeforeInstallPromptEvent
showAndroidInstall.value = true
})
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() {
@@ -54,7 +81,7 @@ async function installApp() {
await deferredPrompt.prompt()
await deferredPrompt.userChoice
deferredPrompt = null
showAndroidInstall.value = false
showInstall.value = false
}
function dismiss() {
@@ -66,15 +93,24 @@ function dismiss() {
<template>
<div v-if="visible" class="install-app">
<div class="install-app__content">
<p v-if="showAndroidInstall" class="install-app__text">
<p v-if="installMode === 'native'" class="install-app__text">
可将道德经安装到桌面 App 一样使用
</p>
<p v-else class="install-app__text">
<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="showAndroidInstall"
v-if="installMode === 'native'"
type="button"
class="install-app__primary"
@click="installApp"
+1
View File
@@ -104,6 +104,7 @@ const PUBLIC_PATHS = new Set([
function isPublicPath(reqPath) {
if (PUBLIC_PATHS.has(reqPath)) return true
if (reqPath.startsWith('/assets/')) return true
if (reqPath.startsWith('/images/')) return true
if (reqPath === '/sw.js' || reqPath.endsWith('.webmanifest')) return true
if (/^\/workbox-[\w-]+\.js$/.test(reqPath)) return true
return false