diff --git a/.memory/worklog.json b/.memory/worklog.json index aac9648..919a131 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -489,6 +489,13 @@ "message": "auto-save 2026-04-18 15:40 (~1)", "hash": "d46152c", "files_changed": 1 + }, + { + "ts": "2026-04-18T15:48:56+08:00", + "type": "commit", + "message": "auto-save 2026-04-18 15:48 (~2)", + "hash": "15b2812", + "files_changed": 2 } ] } diff --git a/web/Dockerfile b/web/Dockerfile index fbfc09e..5c8cfa3 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -2,24 +2,7 @@ FROM nginx:alpine COPY . /usr/share/nginx/html/ -RUN printf 'types {\n\ - text/html html htm;\n\ - text/css css;\n\ - text/plain txt;\n\ - text/markdown md;\n\ - application/javascript js;\n\ - application/json json;\n\ - image/jpeg jpg jpeg;\n\ - image/png png;\n\ - image/webp webp;\n\ - image/svg+xml svg;\n\ - image/x-icon ico;\n\ - font/woff woff;\n\ - font/woff2 woff2;\n\ -}\n\ -default_type application/octet-stream;\n\ -\n\ -server {\n\ +RUN printf 'server {\n\ listen 80;\n\ server_name _;\n\ root /usr/share/nginx/html;\n\ @@ -33,7 +16,13 @@ server {\n\ access_log off;\n\ }\n\ \n\ - # html / md: always revalidate\n\ + # novel.md specifically — force text/markdown so gzip triggers\n\ + location = /novel.md {\n\ + default_type "text/markdown; charset=utf-8";\n\ + add_header Cache-Control "public, max-age=0, must-revalidate";\n\ + }\n\ +\n\ + # html / md catch-all: always revalidate\n\ location ~* \\.(html|md)$ {\n\ add_header Cache-Control "public, max-age=0, must-revalidate";\n\ }\n\ diff --git a/web/index.html b/web/index.html index 6ce8093..e6b7eab 100644 --- a/web/index.html +++ b/web/index.html @@ -922,13 +922,21 @@ function closeLightbox() { } async function loadNovel() { + const container = document.getElementById('reader-content'); try { - const resp = await fetch('./novel.md'); + container.innerHTML = '
正在下载全本 …
'; + const resp = await fetch('./novel.md', { cache: 'no-cache' }); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + container.innerHTML = '
正在渲染章节 …
'; const text = await resp.text(); + // Yield to the browser so the loading message paints before heavy work + await new Promise(r => setTimeout(r, 0)); renderNovel(text); } catch (e) { - document.getElementById('reader-content').innerHTML = - '
小说载入失败:' + e.message + '
'; + console.error('loadNovel failed:', e); + container.innerHTML = + '
小说载入失败:' + e.message + + '。直接打开 novel.md
'; } } @@ -944,24 +952,28 @@ function renderNovel(md) { let m = heading.match(/^(第(\d+)章|终章)\s+(.+)$/); let isPart2 = false; let num, title; + const cnMap = { '一': '01', '二': '02', '三': '03', '四': '04', '五': '05', '六': '06', '七': '07', '八': '08', '九': '09', '十': '10' }; + const cnToInt = { '一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10 }; + let kicker, chN; if (m) { const isEpilogue = !m[2]; num = isEpilogue ? '终' : m[2].padStart(2, '0'); title = m[3]; - // kicker assigned below - var kicker = isEpilogue ? 'EPILOGUE' : 'CHAPTER ' + num; + kicker = isEpilogue ? 'EPILOGUE' : 'CHAPTER ' + num; + chN = isEpilogue ? 0 : parseInt(m[2]); } else { const m2 = heading.match(/^第二部\s*·\s*第(.+?)章\s*·\s*(.+)$/); if (!m2) return; isPart2 = true; - const cnMap = { '一': '01', '二': '02', '三': '03', '四': '04', '五': '05', '六': '06', '七': '07', '八': '08', '九': '09', '十': '10' }; const arabic = cnMap[m2[1]] || m2[1]; num = 'II · ' + arabic; title = m2[2]; - var kicker = 'PART II · CHAPTER ' + arabic; + kicker = 'PART II · CHAPTER ' + arabic; + chN = -(cnToInt[m2[1]] || parseInt(m2[1])); // -1 = Part II Ch1, -2 = Part II Ch2, ... } - const ch = CHAPTERS[idx]; - const imgPath = ch ? `./images/${ch.img}` : ''; + // Match chapter by semantic n (immune to section index drift caused by non-chapter ## headings) + const ch = CHAPTERS.find(c => c.n === chN); + const imgPath = (ch && ch.img) ? `./images/${ch.img}` : ''; const paragraphs = body.split(/\n\s*\n/).map(p => { const line = p.trim(); if (!line) return '';