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"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vitepress'
const route = useRoute() type InstallMode = 'native' | 'ios' | 'android-menu' | 'desktop' | 'https-required'
const showAndroidInstall = ref(false)
const showIOSHint = ref(false) const showInstall = ref(false)
const dismissed = ref(false) const dismissed = ref(false)
const installMode = ref<InstallMode | null>(null)
const isMobile = ref(false) const isMobile = ref(false)
let deferredPrompt: BeforeInstallPromptEvent | null = null let deferredPrompt: BeforeInstallPromptEvent | null = null
let fallbackTimer: ReturnType<typeof setTimeout> | null = null
interface BeforeInstallPromptEvent extends Event { interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void> prompt: () => Promise<void>
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }> userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
} }
const isDocPage = computed(() => route.path !== '/') const visible = computed(() => !dismissed.value && showInstall.value && installMode.value !== null)
const visible = computed(() => { function needsHttpsForInstall() {
if (dismissed.value) return false return (
if (isMobile.value && isDocPage.value) return false !window.isSecureContext &&
return showAndroidInstall.value || showIOSHint.value !['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(() => { onMounted(() => {
isMobile.value = window.matchMedia('(max-width: 768px)').matches isMobile.value = window.matchMedia('(max-width: 768px)').matches
@@ -38,15 +51,29 @@ onMounted(() => {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
if (isIOS) { if (isIOS) {
showIOSHint.value = true installMode.value = 'ios'
showInstall.value = true
return return
} }
window.addEventListener('beforeinstallprompt', (event) => { if (needsHttpsForInstall()) {
event.preventDefault() installMode.value = 'https-required'
deferredPrompt = event as BeforeInstallPromptEvent showInstall.value = true
showAndroidInstall.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() { async function installApp() {
@@ -54,7 +81,7 @@ async function installApp() {
await deferredPrompt.prompt() await deferredPrompt.prompt()
await deferredPrompt.userChoice await deferredPrompt.userChoice
deferredPrompt = null deferredPrompt = null
showAndroidInstall.value = false showInstall.value = false
} }
function dismiss() { function dismiss() {
@@ -66,15 +93,24 @@ function dismiss() {
<template> <template>
<div v-if="visible" class="install-app"> <div v-if="visible" class="install-app">
<div class="install-app__content"> <div class="install-app__content">
<p v-if="showAndroidInstall" class="install-app__text"> <p v-if="installMode === 'native'" class="install-app__text">
可将道德经安装到桌面 App 一样使用 可将道德经安装到桌面 App 一样使用
</p> </p>
<p v-else class="install-app__text"> <p v-else-if="installMode === 'ios'" class="install-app__text">
iPhoneSafari 底部分享 添加到主屏幕阅读更方便 iPhoneSafari 底部分享 添加到主屏幕阅读更方便
</p> </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"> <div class="install-app__actions">
<button <button
v-if="showAndroidInstall" v-if="installMode === 'native'"
type="button" type="button"
class="install-app__primary" class="install-app__primary"
@click="installApp" @click="installApp"
+1
View File
@@ -104,6 +104,7 @@ const PUBLIC_PATHS = new Set([
function isPublicPath(reqPath) { function isPublicPath(reqPath) {
if (PUBLIC_PATHS.has(reqPath)) return true if (PUBLIC_PATHS.has(reqPath)) return true
if (reqPath.startsWith('/assets/')) return true if (reqPath.startsWith('/assets/')) return true
if (reqPath.startsWith('/images/')) return true
if (reqPath === '/sw.js' || reqPath.endsWith('.webmanifest')) return true if (reqPath === '/sw.js' || reqPath.endsWith('.webmanifest')) return true
if (/^\/workbox-[\w-]+\.js$/.test(reqPath)) return true if (/^\/workbox-[\w-]+\.js$/.test(reqPath)) return true
return false return false