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:
@@ -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">
|
||||||
iPhone:Safari 底部分享 →「添加到主屏幕」,阅读更方便。
|
iPhone:Safari 底部分享 →「添加到主屏幕」,阅读更方便。
|
||||||
</p>
|
</p>
|
||||||
|
<p v-else-if="installMode === 'android-menu'" class="install-app__text">
|
||||||
|
Android:Chrome 右上角菜单 →「安装应用」或「添加到主屏幕」。
|
||||||
|
</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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user