Add PWA install support for mobile and desktop
Register service worker, install prompt UI, iOS meta tags, and document install steps in DEPLOY.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { createPwaPlugin } from './pwa.mts'
|
||||
import { missingAssetsPlugin } from './missing-assets-plugin.mjs'
|
||||
import { generateRewrites } from './rewrites.mts'
|
||||
import { generateSidebar } from './sidebar.mts'
|
||||
@@ -37,6 +38,9 @@ export default defineConfig({
|
||||
['meta', { name: 'msapplication-TileImage', content: '/apple-touch-icon.png' }],
|
||||
['meta', { name: 'apple-mobile-web-app-title', content: '道德经' }],
|
||||
['meta', { name: 'application-name', content: '道德经' }],
|
||||
['meta', { name: 'mobile-web-app-capable', content: 'yes' }],
|
||||
['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
|
||||
['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' }],
|
||||
],
|
||||
themeConfig: {
|
||||
logo: { src: '/favicon.ico', width: 24, height: 24 },
|
||||
@@ -54,7 +58,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
vite: {
|
||||
plugins: [missingAssetsPlugin()],
|
||||
plugins: [missingAssetsPlugin(), createPwaPlugin()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
export function createPwaPlugin() {
|
||||
return VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
injectRegister: 'auto',
|
||||
manifestFilename: 'site.webmanifest',
|
||||
includeAssets: [
|
||||
'favicon.ico',
|
||||
'favicon.svg',
|
||||
'favicon.png',
|
||||
'apple-touch-icon.png',
|
||||
'icon-192.png',
|
||||
'icon-512.png',
|
||||
],
|
||||
manifest: {
|
||||
name: '道德经 · 传统文化典籍',
|
||||
short_name: '道德经',
|
||||
description: '传统文化典籍资料库',
|
||||
lang: 'zh-CN',
|
||||
theme_color: '#0f3460',
|
||||
background_color: '#1a1a2e',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait-primary',
|
||||
scope: '/',
|
||||
start_url: '/',
|
||||
categories: ['books', 'education'],
|
||||
icons: [
|
||||
{
|
||||
src: '/icon-192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
{
|
||||
src: '/icon-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
{
|
||||
src: '/icon-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: [
|
||||
'assets/**/*.{js,css,woff2}',
|
||||
'favicon.ico',
|
||||
'favicon.svg',
|
||||
'favicon.png',
|
||||
'icon-192.png',
|
||||
'icon-512.png',
|
||||
'apple-touch-icon.png',
|
||||
'vp-icons.css',
|
||||
'site.webmanifest',
|
||||
],
|
||||
globIgnores: ['**/*.html', 'images/**', 'assets/**/*.png', 'assets/**/*.jpg', 'assets/**/*.webp'],
|
||||
navigateFallback: null,
|
||||
cleanupOutdatedCaches: true,
|
||||
},
|
||||
devOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const showAndroidInstall = ref(false)
|
||||
const showIOSHint = ref(false)
|
||||
const dismissed = ref(false)
|
||||
let deferredPrompt: BeforeInstallPromptEvent | null = null
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt: () => Promise<void>
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
|
||||
}
|
||||
|
||||
const visible = computed(
|
||||
() => !dismissed.value && (showAndroidInstall.value || showIOSHint.value),
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
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) {
|
||||
showIOSHint.value = true
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault()
|
||||
deferredPrompt = event as BeforeInstallPromptEvent
|
||||
showAndroidInstall.value = true
|
||||
})
|
||||
})
|
||||
|
||||
async function installApp() {
|
||||
if (!deferredPrompt) return
|
||||
await deferredPrompt.prompt()
|
||||
await deferredPrompt.userChoice
|
||||
deferredPrompt = null
|
||||
showAndroidInstall.value = false
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
dismissed.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="install-app">
|
||||
<div class="install-app__content">
|
||||
<p v-if="showAndroidInstall" class="install-app__text">
|
||||
可将「道德经」安装到桌面,像 App 一样使用。
|
||||
</p>
|
||||
<p v-else class="install-app__text">
|
||||
iPhone:点击 Safari 底部分享按钮,选择「添加到主屏幕」。
|
||||
</p>
|
||||
<div class="install-app__actions">
|
||||
<button
|
||||
v-if="showAndroidInstall"
|
||||
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: 16px;
|
||||
z-index: 100;
|
||||
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: 8px 12px;
|
||||
font-size: 13px;
|
||||
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);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { h } from 'vue'
|
||||
import InstallApp from './InstallApp.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout: () => {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
'layout-bottom': () => h(InstallApp),
|
||||
})
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user