Fix PWA icons and mobile reading overflow

Generate PNG icons from favicon.svg at build time so manifest icons resolve, replace legacy font tags with div wrappers, and tighten mobile layout so long text wraps instead of clipping.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-05 18:10:38 +08:00
parent a8907d6cc0
commit ecd4f25700
7 changed files with 90 additions and 16 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ import { generateSidebar } from './sidebar.mts'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
export default defineConfig({ export default defineConfig({
title: 'DAO DE JING', title: '道德经',
description: '传统文化典籍资料库', description: '传统文化典籍资料库',
lang: 'zh-CN', lang: 'zh-CN',
srcDir: '.', srcDir: '.',
+6
View File
@@ -27,6 +27,12 @@ export function createPwaPlugin() {
start_url: '/', start_url: '/',
categories: ['books', 'education'], categories: ['books', 'education'],
icons: [ icons: [
{
src: '/apple-touch-icon.png',
sizes: '180x180',
type: 'image/png',
purpose: 'any',
},
{ {
src: '/icon-192.png', src: '/icon-192.png',
sizes: '192x192', sizes: '192x192',
+37 -6
View File
@@ -3,15 +3,25 @@
'Noto Sans SC', 'Source Han Sans SC', sans-serif; 'Noto Sans SC', 'Source Han Sans SC', sans-serif;
} }
html {
overflow-x: clip;
}
/* 旧版 markdown 里的 <font size="4"> 在手机上过大 */ /* 旧版 markdown 里的 <font size="4"> 在手机上过大 */
.vp-doc font { .vp-doc font,
.vp-md-font {
display: block;
max-width: 100%;
font-size: inherit !important; font-size: inherit !important;
overflow-wrap: anywhere;
} }
.vp-doc { .vp-doc {
line-height: 1.85; line-height: 1.85;
letter-spacing: 0.02em; letter-spacing: 0.02em;
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere;
max-width: 100%;
} }
.vp-doc h1 { .vp-doc h1 {
@@ -32,13 +42,20 @@
.vp-doc p { .vp-doc p {
margin: 1em 0; margin: 1em 0;
overflow-wrap: anywhere;
} }
/* 道德经等用缩进写的“伪代码块”,改成正文排版 */ /* 道德经等用缩进写的“伪代码块”,改成正文排版 */
.vp-doc pre { .vp-doc pre,
white-space: pre-wrap; .vp-doc pre code {
white-space: pre-wrap !important;
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere;
}
.vp-doc pre {
overflow-x: auto; overflow-x: auto;
max-width: 100%;
font-family: var(--vp-font-family-base); font-family: var(--vp-font-family-base);
font-size: 1rem; font-size: 1rem;
line-height: 1.9; line-height: 1.9;
@@ -51,11 +68,13 @@
.vp-doc :not(pre) > code { .vp-doc :not(pre) > code {
font-size: 0.92em; font-size: 0.92em;
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere;
} }
.vp-doc img { .vp-doc img {
display: block; display: block;
max-width: 100% !important; max-width: 100% !important;
width: auto !important;
height: auto !important; height: auto !important;
margin: 1em auto; margin: 1em auto;
border-radius: 8px; border-radius: 8px;
@@ -66,6 +85,7 @@
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
font-size: 0.92rem; font-size: 0.92rem;
max-width: 100%;
} }
.vp-doc blockquote { .vp-doc blockquote {
@@ -91,6 +111,16 @@
padding-top: 0; padding-top: 0;
} }
.VPDoc .container,
.VPDoc .content,
.VPDoc .content-container,
.vp-doc,
.vp-md-font {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
.VPDoc .container { .VPDoc .container {
padding: 0 14px; padding: 0 14px;
} }
@@ -100,7 +130,7 @@
} }
.vp-doc { .vp-doc {
padding: 12px 0 88px; padding: 8px 0 88px;
font-size: 17px; font-size: 17px;
} }
@@ -109,8 +139,9 @@
} }
/* 手机隐藏右侧大纲,避免挤压正文 */ /* 手机隐藏右侧大纲,避免挤压正文 */
.VPDoc .aside { .VPDoc .aside,
display: none; .VPDoc .aside-container {
display: none !important;
} }
.VPDoc.has-aside .content-container { .VPDoc.has-aside .content-container {
+12 -1
View File
@@ -1,5 +1,5 @@
import DefaultTheme from 'vitepress/theme' import DefaultTheme from 'vitepress/theme'
import { h } from 'vue' import { h, defineComponent } from 'vue'
import InstallApp from './InstallApp.vue' import InstallApp from './InstallApp.vue'
import './custom.css' import './custom.css'
@@ -9,8 +9,19 @@ if (typeof window !== 'undefined') {
}) })
} }
/** 旧 markdown 的 <font> 标签改为 div,避免手机端布局溢出 */
const MdFont = defineComponent({
name: 'MdFont',
setup(_, { slots }) {
return () => h('div', { class: 'vp-md-font' }, slots.default?.())
},
})
export default { export default {
extends: DefaultTheme, extends: DefaultTheme,
enhanceApp({ app }) {
app.component('font', MdFont)
},
Layout: () => { Layout: () => {
return h(DefaultTheme.Layout, null, { return h(DefaultTheme.Layout, null, {
'layout-bottom': () => h(InstallApp), 'layout-bottom': () => h(InstallApp),
+4 -4
View File
@@ -1,11 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="S"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="">
<defs> <defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0f3460"/> <stop offset="0%" stop-color="#1b8a5a"/>
<stop offset="100%" stop-color="#1a1a2e"/> <stop offset="100%" stop-color="#0f3460"/>
</linearGradient> </linearGradient>
</defs> </defs>
<rect width="512" height="512" rx="112" fill="url(#bg)"/> <rect width="512" height="512" rx="112" fill="url(#bg)"/>
<circle cx="256" cy="256" r="196" fill="none" stroke="#d4af37" stroke-width="10" opacity="0.85"/> <circle cx="256" cy="256" r="196" fill="none" stroke="#d4af37" stroke-width="10" opacity="0.85"/>
<text x="256" y="292" text-anchor="middle" font-size="220" font-family="'KaiTi','STKaiti','SimSun',serif" fill="#f5f5f5">S</text> <text x="256" y="300" text-anchor="middle" font-size="220" font-family="'KaiTi','STKaiti','SimSun',serif" fill="#f5f5f5"></text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 590 B

After

Width:  |  Height:  |  Size: 594 B

+12 -4
View File
@@ -7,14 +7,22 @@ import toIco from 'to-ico'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const siteDir = path.join(root, 'assets', 'site') const siteDir = path.join(root, 'assets', 'site')
const sourcePng = path.join(siteDir, 'apple-touch-icon.png') const sourcePng = path.join(siteDir, 'apple-touch-icon.png')
const sourceSvg = path.join(siteDir, 'favicon.svg')
if (!fs.existsSync(sourcePng)) { function resolveSource() {
console.warn('[generate-icons] 跳过:未找到 apple-touch-icon.png') if (fs.existsSync(sourcePng)) return sourcePng
if (fs.existsSync(sourceSvg)) return sourceSvg
return null
}
const source = resolveSource()
if (!source) {
console.warn('[generate-icons] 跳过:未找到 apple-touch-icon.png 或 favicon.svg')
process.exit(0) process.exit(0)
} }
async function resizePng(size) { async function resizePng(size) {
return sharp(sourcePng).resize(size, size).png().toBuffer() return sharp(source, { density: 300 }).resize(size, size).png().toBuffer()
} }
const icoSizes = [16, 32, 48, 64, 128, 256] const icoSizes = [16, 32, 48, 64, 128, 256]
@@ -26,4 +34,4 @@ fs.writeFileSync(path.join(siteDir, 'icon-192.png'), await resizePng(192))
fs.writeFileSync(path.join(siteDir, 'icon-512.png'), await resizePng(512)) fs.writeFileSync(path.join(siteDir, 'icon-512.png'), await resizePng(512))
fs.writeFileSync(path.join(siteDir, 'favicon.png'), await resizePng(512)) fs.writeFileSync(path.join(siteDir, 'favicon.png'), await resizePng(512))
console.log('[generate-icons] 已生成 favicon.ico、icon-192.png、icon-512.png') console.log(`[generate-icons] 已${path.basename(source)} 生成 favicon.ico、icon-192.png、icon-512.png`)
+18
View File
@@ -162,6 +162,15 @@ app.get('/api/me', authGuard, (req, res) => {
app.use(authGuard) app.use(authGuard)
function sendDistFile(res, relativePath, contentType) {
const filePath = path.join(distPath, relativePath)
if (!fs.existsSync(filePath)) {
return res.status(404).end('Not Found')
}
if (contentType) res.type(contentType)
return res.sendFile(filePath)
}
if (!fs.existsSync(distPath)) { if (!fs.existsSync(distPath)) {
console.error( console.error(
'未找到构建产物 .vitepress/dist,请先运行: npm run build', '未找到构建产物 .vitepress/dist,请先运行: npm run build',
@@ -169,10 +178,19 @@ if (!fs.existsSync(distPath)) {
process.exit(1) process.exit(1)
} }
app.get('/site.webmanifest', (req, res) => {
sendDistFile(res, 'site.webmanifest', 'application/manifest+json')
})
app.use( app.use(
express.static(distPath, { express.static(distPath, {
index: false, index: false,
fallthrough: true, fallthrough: true,
setHeaders(res, filePath) {
if (filePath.endsWith('.webmanifest')) {
res.setHeader('Content-Type', 'application/manifest+json')
}
},
}), }),
) )