From ecd4f25700e14b9bbfbfa7828bdbdbc4f43566a8 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 5 Jun 2026 18:10:38 +0800 Subject: [PATCH] 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 --- .vitepress/config.mts | 2 +- .vitepress/pwa.mts | 6 ++++++ .vitepress/theme/custom.css | 43 +++++++++++++++++++++++++++++++------ .vitepress/theme/index.ts | 13 ++++++++++- assets/site/favicon.svg | 8 +++---- scripts/generate-icons.mjs | 16 ++++++++++---- server/index.js | 18 ++++++++++++++++ 7 files changed, 90 insertions(+), 16 deletions(-) diff --git a/.vitepress/config.mts b/.vitepress/config.mts index 75e57bc..2e1112b 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -9,7 +9,7 @@ import { generateSidebar } from './sidebar.mts' const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') export default defineConfig({ - title: 'DAO DE JING', + title: '道德经', description: '传统文化典籍资料库', lang: 'zh-CN', srcDir: '.', diff --git a/.vitepress/pwa.mts b/.vitepress/pwa.mts index 2f8d99a..5997086 100644 --- a/.vitepress/pwa.mts +++ b/.vitepress/pwa.mts @@ -27,6 +27,12 @@ export function createPwaPlugin() { start_url: '/', categories: ['books', 'education'], icons: [ + { + src: '/apple-touch-icon.png', + sizes: '180x180', + type: 'image/png', + purpose: 'any', + }, { src: '/icon-192.png', sizes: '192x192', diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css index 33b4712..d67a086 100644 --- a/.vitepress/theme/custom.css +++ b/.vitepress/theme/custom.css @@ -3,15 +3,25 @@ 'Noto Sans SC', 'Source Han Sans SC', sans-serif; } +html { + overflow-x: clip; +} + /* 旧版 markdown 里的 在手机上过大 */ -.vp-doc font { +.vp-doc font, +.vp-md-font { + display: block; + max-width: 100%; font-size: inherit !important; + overflow-wrap: anywhere; } .vp-doc { line-height: 1.85; letter-spacing: 0.02em; word-break: break-word; + overflow-wrap: anywhere; + max-width: 100%; } .vp-doc h1 { @@ -32,13 +42,20 @@ .vp-doc p { margin: 1em 0; + overflow-wrap: anywhere; } /* 道德经等用缩进写的“伪代码块”,改成正文排版 */ -.vp-doc pre { - white-space: pre-wrap; +.vp-doc pre, +.vp-doc pre code { + white-space: pre-wrap !important; word-break: break-word; + overflow-wrap: anywhere; +} + +.vp-doc pre { overflow-x: auto; + max-width: 100%; font-family: var(--vp-font-family-base); font-size: 1rem; line-height: 1.9; @@ -51,11 +68,13 @@ .vp-doc :not(pre) > code { font-size: 0.92em; word-break: break-word; + overflow-wrap: anywhere; } .vp-doc img { display: block; max-width: 100% !important; + width: auto !important; height: auto !important; margin: 1em auto; border-radius: 8px; @@ -66,6 +85,7 @@ overflow-x: auto; -webkit-overflow-scrolling: touch; font-size: 0.92rem; + max-width: 100%; } .vp-doc blockquote { @@ -91,6 +111,16 @@ 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 { padding: 0 14px; } @@ -100,7 +130,7 @@ } .vp-doc { - padding: 12px 0 88px; + padding: 8px 0 88px; font-size: 17px; } @@ -109,8 +139,9 @@ } /* 手机隐藏右侧大纲,避免挤压正文 */ - .VPDoc .aside { - display: none; + .VPDoc .aside, + .VPDoc .aside-container { + display: none !important; } .VPDoc.has-aside .content-container { diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index 0c99811..eda76b3 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -1,5 +1,5 @@ import DefaultTheme from 'vitepress/theme' -import { h } from 'vue' +import { h, defineComponent } from 'vue' import InstallApp from './InstallApp.vue' import './custom.css' @@ -9,8 +9,19 @@ if (typeof window !== 'undefined') { }) } +/** 旧 markdown 的 标签改为 div,避免手机端布局溢出 */ +const MdFont = defineComponent({ + name: 'MdFont', + setup(_, { slots }) { + return () => h('div', { class: 'vp-md-font' }, slots.default?.()) + }, +}) + export default { extends: DefaultTheme, + enhanceApp({ app }) { + app.component('font', MdFont) + }, Layout: () => { return h(DefaultTheme.Layout, null, { 'layout-bottom': () => h(InstallApp), diff --git a/assets/site/favicon.svg b/assets/site/favicon.svg index 9ab23b2..f378fb4 100644 --- a/assets/site/favicon.svg +++ b/assets/site/favicon.svg @@ -1,11 +1,11 @@ - + - - + + - S + diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs index 81f1830..2d9b900 100644 --- a/scripts/generate-icons.mjs +++ b/scripts/generate-icons.mjs @@ -7,14 +7,22 @@ import toIco from 'to-ico' const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const siteDir = path.join(root, 'assets', 'site') const sourcePng = path.join(siteDir, 'apple-touch-icon.png') +const sourceSvg = path.join(siteDir, 'favicon.svg') -if (!fs.existsSync(sourcePng)) { - console.warn('[generate-icons] 跳过:未找到 apple-touch-icon.png') +function resolveSource() { + 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) } 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] @@ -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, '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`) diff --git a/server/index.js b/server/index.js index 86323ac..c47d23c 100644 --- a/server/index.js +++ b/server/index.js @@ -162,6 +162,15 @@ app.get('/api/me', authGuard, (req, res) => { 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)) { console.error( '未找到构建产物 .vitepress/dist,请先运行: npm run build', @@ -169,10 +178,19 @@ if (!fs.existsSync(distPath)) { process.exit(1) } +app.get('/site.webmanifest', (req, res) => { + sendDistFile(res, 'site.webmanifest', 'application/manifest+json') +}) + app.use( express.static(distPath, { index: false, fallthrough: true, + setHeaders(res, filePath) { + if (filePath.endsWith('.webmanifest')) { + res.setHeader('Content-Type', 'application/manifest+json') + } + }, }), )