game/story.html

624 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>剧情 · 宇森 Yusen</title>
<link rel="stylesheet" href="assets/fonts/yusen-fonts.css" />
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<style>
/* ===== 三栏主容器 ===== */
.story-layout{
display:grid;
grid-template-columns:280px 1fr 300px;
grid-template-rows:1fr;
height:100%;min-height:0;max-width:100%;margin:0 auto;width:100%;
}
/* ===== 左栏:目录(固定满高,内部滚动) ===== */
.story-toc{
display:flex;flex-direction:column;border-right:1px solid rgba(139,92,246,.12);
background:linear-gradient(180deg, rgba(10,14,26,.96) 0%, rgba(8,10,22,.98) 100%);
overflow:hidden;min-height:0;
position:relative;
}
.story-toc::after{
content:'';position:absolute;right:0;top:0;bottom:0;width:1px;
background:linear-gradient(180deg, transparent, var(--violet), transparent);
opacity:.4;
}
.toc-brand{
padding:12px 20px 10px;border-bottom:1px solid rgba(255,255,255,.06);
position:relative;
}
.toc-brand h2{
font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:26px;
letter-spacing:4px;color:#fff;margin:0;
background:linear-gradient(135deg,#e0c3fc,#8ec5fc);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
}
.toc-brand small{
display:block;font-family:"Press Start 2P",monospace;
font-size:9px;color:var(--magenta);letter-spacing:3px;margin-top:6px;
text-transform:uppercase;
}
.toc-stat b{display:block;font-size:15px;color:var(--cyan);font-weight:700;}
.toc-scroll{
flex:1;overflow-y:auto;padding:8px 10px 24px;scrollbar-width:none;
}
/* 全局隐藏滚动条 */
.story-reader::-webkit-scrollbar,
.story-side::-webkit-scrollbar,
.toc-scroll::-webkit-scrollbar{display:none;width:0;}
.story-reader,.story-side,.toc-scroll{scrollbar-width:none;-ms-overflow-style:none;}
.toc-vol{margin-top:4px;}
.toc-vol:first-child{margin-top:0;}
.vol-header{
display:flex;align-items:center;gap:8px;
padding:12px 10px 6px;cursor:pointer;user-select:none;
transition:.2s;
}
.vol-header:hover{background:rgba(139,92,246,.06);border-radius:8px;}
.vol-toggle{
width:18px;height:18px;line-height:18px;text-align:center;
font-size:10px;color:var(--magenta);transition:transform .25s;
flex-shrink:0;
}
.vol-header.open .vol-toggle{transform:rotate(90deg);}
.vol-name{font-size:16px;color:var(--gold);letter-spacing:1.5px;font-weight:600;}
.toc-ch{
display:block;width:100%;text-align:left;
font-size:15.5px;color:rgba(210,220,235,.88);
background:transparent;border:none;
border-left:2px solid transparent;
padding:10px 14px 10px 18px;cursor:pointer;transition:.18s;
border-radius:0 8px 8px 0;line-height:1.5;text-decoration:none;
position:relative;
}
.toc-ch:hover{color:#fff;background:rgba(139,92,246,.08);}
.toc-ch.active{
color:#fff;background:linear-gradient(90deg, rgba(139,92,246,.18), transparent);
border-left-color:var(--magenta);
font-weight:500;
}
.toc-ch.active::before{
content:'▸';position:absolute;left:5px;top:50%;transform:translateY(-50%);
font-size:9px;color:var(--magenta);
}
/* ===== 页面容器:禁止滚动,交给各栏独立控制 ===== */
.page-content{overflow:hidden !important;}
/* ===== 中间:阅读区 ===== */
.story-reader{
overflow-y:auto;min-width:0;min-height:0;position:relative;
background:
radial-gradient(ellipse at 20% 0%, rgba(139,92,246,.06) 0%, transparent 60%),
linear-gradient(180deg, rgba(10,14,26,0) 0%, rgba(10,14,26,.35) 100%);
}
.reader-inner{max-width:720px;margin:0 auto;padding:48px 48px 32px;}
/* 章节切换:只 fade 正文区,骨架不动 */
.reader-body{transition:opacity .15s ease;}
.chapter-loading{
text-align:center;padding:48px 0;color:var(--text-dim);font-size:14px;
}
.reader-breadcrumb{
font-size:11px;color:var(--magenta);letter-spacing:2px;
margin-bottom:14px;text-transform:uppercase;
}
.reader-inner h1{
font-family:"ZCOOL QingKe HuangYou",sans-serif;
font-size:34px;letter-spacing:3px;color:#fff;
margin-bottom:8px;line-height:1.35;
background:linear-gradient(135deg,#fff 30%, #c4b5fd);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
}
.reader-meta{
display:flex;align-items:center;gap:20px;
padding:14px 0 24px;border-bottom:1px solid rgba(255,255,255,.06);
margin-bottom:32px;font-size:12px;color:var(--text-dim);
}
.reader-meta span{display:flex;align-items:center;gap:5px;}
.reader-body{
font-size:22px;color:#d4dce8;line-height:2.15;
text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;
}
.reader-body p{margin-bottom:22px;text-indent:2em;}
/* 引用块(系统提示 / 神明语):纯色背景 + 左侧色条,视觉独立 */
.reader-body blockquote{
margin:24px 0;padding:16px 22px 16px 24px;
background:rgba(139,92,246,.14);
border-left:4px solid var(--violet);
border-radius:0 10px 10px 0;
font-family:"Press Start 2P",monospace;
color:#d8c8ff;
text-indent:0;
}
.reader-body blockquote p{
text-indent:0;margin-bottom:8px;font-size:13px;line-height:1.95;letter-spacing:.5px;
}
.reader-body blockquote p:last-child{margin-bottom:0;}
.reader-body p:first-child{text-indent:0;font-size:24px;color:#eef2fb;}
.reader-body h2,.reader-body h3{color:#fff;margin:28px 0 12px;font-size:22px;
letter-spacing:1px;}
.reader-nav-row{
display:flex;justify-content:space-between;gap:16px;
margin-top:32px;padding-top:20px;border-top:1px solid rgba(255,255,255,.06);
}
.reader-nav-btn{
flex:1;padding:14px 20px;border-radius:10px;font-size:13px;
color:var(--text-dim);text-align:center;text-decoration:none;
border:1px solid rgba(255,255,255,.08);transition:.2s;
cursor:pointer;background:transparent;
}
.reader-nav-btn:hover:not(:disabled){
color:#fff;border-color:var(--violet);background:rgba(139,92,246,.1);
}
.reader-nav-btn:disabled{opacity:.3;cursor:default;}
/* ===== 右栏:信息面板 ===== */
/* ===== 右栏:信息面板(固定满高,内部滚动) ===== */
.story-side{
border-left:1px solid rgba(139,92,246,.12);
background:linear-gradient(180deg, rgba(10,14,26,.95), rgba(8,10,22,.98));
display:flex;flex-direction:column;overflow-y:auto;min-height:0;
min-width:0;
}
.side-section{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.04);}
.side-section:first-child{padding-top:18px;}
.side-title{
font-size:11px;color:var(--magenta);letter-spacing:2.5px;
text-transform:uppercase;margin-bottom:14px;font-weight:600;
}
/* 章节信息卡 */
.info-card{
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);
border-radius:12px;padding:16px;
}
.info-row{display:flex;justify-content:space-between;align-items:center;
padding:8px 0;border-bottom:1px solid rgba(255,255,255,.04);}
.info-row:last-child{border-bottom:none;}
.info-label{font-size:12px;color:var(--text-dim);}
.info-val{font-size:13px;color:#fff;font-weight:500;}
/* 阅读进度 */
.progress-bar{height:4px;background:rgba(255,255,255,.06);border-radius:2px;
margin-top:12px;overflow:hidden;position:relative;}
.progress-fill{
height:100%;background:linear-gradient(90deg, var(--violet), var(--cyan));
border-radius:2px;transition:width .4s ease;width:0%;
}
.progress-pct{text-align:right;font-size:11px;color:var(--text-dim);margin-top:6px;}
/* 章节跳转(左右大按钮) */
.chapter-arrows{display:flex;gap:12px;}
.chap-arrow{
flex:1;height:60px;border-radius:14px;cursor:pointer;border:1px solid rgba(255,255,255,.12);
background:rgba(255,255,255,.04);color:#e6f0fb;font-size:32px;font-weight:700;
display:flex;align-items:center;justify-content:center;transition:.2s;
font-family:"Noto Sans SC",sans-serif;
}
.chap-arrow:hover:not(:disabled){
background:rgba(139,92,246,.18);border-color:rgba(139,92,246,.5);
box-shadow:0 0 18px rgba(139,92,246,.25);transform:translateY(-2px);
}
.chap-arrow:disabled{opacity:.3;cursor:not-allowed;}
/* 阅读设置 */
.setting-row{display:flex;align-items:center;justify-content:space-between;
padding:8px 0;}
.setting-row-stack{flex-direction:column;align-items:stretch;gap:14px;}
.setting-label{font-size:16px;color:#cdd8e6;font-weight:600;text-align:center;}
.setting-btns{display:flex;gap:6px;justify-content:center;}
.setting-btn{
width:40px;height:40px;border-radius:8px;border:1px solid rgba(255,255,255,.14);
background:transparent;color:var(--text-dim);font-size:16px;font-weight:600;cursor:pointer;
transition:.15s;display:flex;align-items:center;justify-content:center;
}
.setting-btn:hover,.setting-btn.active{
border-color:var(--violet);color:#fff;background:rgba(139,92,246,.18);
}
/* 字号按钮组3小 + 3大 */
.size-btns{
display:flex;gap:6px;justify-content:space-between;
padding:4px 0;
}
.size-btn{
flex:1;height:36px;border-radius:8px;
background:rgba(255,255,255,.04);
border:1px solid rgba(255,255,255,.1);
color:var(--text-dim);font-size:13px;font-weight:600;
cursor:pointer;transition:.15s;
display:flex;align-items:center;justify-content:center;
}
.size-btn:hover{
color:#fff;background:rgba(139,92,246,.1);
border-color:rgba(139,92,246,.3);
}
.size-btn.active{
color:#fff;background:rgba(139,92,246,.22);
border-color:var(--violet);
box-shadow:0 0 10px rgba(139,92,246,.2);
}
/* 按钮自身字号 = 该档位真实阅读字号,所见即所得 */
.size-btn:nth-child(1){font-size:14px;} /* 小(xs:17) */
.size-btn:nth-child(2){font-size:16px;} /* 小(sm:20) */
.size-btn:nth-child(3){font-size:19px;} /* 小(md:24) */
.size-btn:nth-child(4){font-size:22px;} /* 大(lg:28) */
.size-btn:nth-child(5){font-size:26px;} /* 大(xl:34) */
.size-btn:nth-child(6){font-size:30px;} /* 大(xxl:40) */
/* 面板标题统一加大 */
.side-title{
font-size:14px !important;color:#fff !important;letter-spacing:2px;
margin-bottom:18px;font-weight:700;
}
.side-section{padding:20px 22px;}
/* 占位块(待开发 / 待接入) */
.todo-block{
display:flex;flex-direction:column;align-items:center;gap:6px;
padding:24px 16px;border-radius:10px;
background:rgba(255,255,255,.02);
border:1px dashed rgba(139,92,246,.3);
text-align:center;
}
.todo-block .todo-icon{font-size:28px;opacity:.7;}
.todo-block .todo-title{
font-size:14px;font-weight:700;color:#cdd8e6;letter-spacing:1px;
}
.todo-block .todo-tag{
display:inline-block;font-size:10px;color:var(--gold);letter-spacing:1.5px;
padding:2px 9px;border:1px solid rgba(231,184,92,.4);border-radius:99px;
background:rgba(231,184,92,.08);
}
.todo-block .todo-desc{
font-size:11.5px;color:#7b8ea8;line-height:1.6;margin-top:2px;
}
/* ===== 响应式 ===== */
@media (max-width:1100px){
.story-layout{grid-template-columns:240px 1fr 260px;}
}
@media (max-width:900px){
.story-layout{grid-template-columns:1fr;display:block;overflow-y:auto;}
.story-toc{border-right:none;border-bottom:1px solid rgba(139,92,246,.12);
max-height:260px;order:1;}
.story-reader{order:2;}
.story-side{display:none;} /* 移动端隐藏右侧 */
}
@media (max-width:768px){
.reader-inner{padding:28px 20px 80px;}
.reader-inner h1{font-size:24px;}
.reader-body{font-size:16px;}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
<a href="worlddata.html" class="auth-only">世界数据</a>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="page-content" style="padding:0;">
<div class="story-layout">
<!-- 左栏:目录 -->
<aside class="story-toc">
<div class="toc-brand">
<h2>剧情</h2>
<small>Chronicle</small>
</div>
<div class="toc-scroll" id="tocList">
<div style="padding:30px 16px;color:var(--text-dim);font-size:13px;">加载中...</div>
</div>
</aside>
<!-- 中间:阅读区(静态占位直接匹配 JS 骨架,避免刷新闪烁) -->
<article class="story-reader" id="storyReader">
<div class="reader-inner" id="readerContent">
<div class="reader-breadcrumb">宇森创世正史</div>
<h1>欢迎阅读</h1>
<div class="reader-meta">
<span>📄 <span id="metaWords"></span></span>
<span class="meta-chpos"></span>
<span>📚 —</span>
</div>
<div class="reader-body" id="readerBody">
<div class="chapter-loading">
<div class="spinner" style="width:30px;height:30px;border:3px solid rgba(139,92,246,.2);border-top-color:var(--violet);border-radius:50%;animation:spin .7s linear infinite;margin:0 auto 14px;"></div>
<span>加载剧情…</span>
</div>
</div>
<div class="reader-nav-row">
<button class="reader-nav-btn" disabled>← 上一章</button>
<button class="reader-nav-btn" disabled>下一章 →</button>
</div>
</div>
</article>
<!-- 右栏:信息面板 -->
<aside class="story-side" id="storySide">
<!-- 当前章节隐藏JS 仍需其 id -->
<div class="side-section" style="display:none;">
<div class="side-title">当前章节</div>
<div class="info-card">
<div class="info-row"><span class="info-label">标题</span><span class="info-val" id="infoTitle"></span></div>
<div class="info-row"><span class="info-label">所属卷</span><span class="info-val" id="infoVol"></span></div>
<div class="info-row"><span class="info-label">字数</span><span class="info-val" id="infoWords"></span></div>
<div class="info-row"><span class="info-label">状态</span><span class="info-val" id="infoStatus" style="color:var(--gold)">待阅读</span></div>
</div>
</div>
<!-- 阅读进度 -->
<div class="side-section">
<div class="side-title">阅读进度</div>
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<div class="progress-pct" id="progressPct">0%</div>
</div>
<!-- 阅读设置 -->
<div class="side-section">
<div class="side-title">阅读设置</div>
<div class="setting-row setting-row-stack">
<span class="setting-label">字号</span>
<div class="size-btns" id="sizeBtns">
<button class="size-btn" data-sz="xs"></button>
<button class="size-btn" data-sz="sm"></button>
<button class="size-btn" data-sz="md"></button>
<button class="size-btn" data-sz="lg"></button>
<button class="size-btn active" data-sz="xl"></button>
<button class="size-btn" data-sz="xxl"></button>
</div>
</div>
</div>
<!-- 章节评论(待开发) -->
<div class="side-section">
<div class="side-title">评论</div>
<div class="todo-block">
<div class="todo-icon">💬</div>
<div class="todo-title">章节评论</div>
<div class="todo-tag">待开发</div>
<div class="todo-desc">登录后可对本章发表评论、点赞、回复。后续接入。</div>
</div>
</div>
</aside>
</div>
</main>
</div>
<script src="assets/game-api.js"></script>
<script src="assets/common.js"></script>
<script>
// ========== 数据(来自后端 API ==========
let STORY = { volumes: [] }; // [{id, name, intro, chapters:[{id,name}]}]
let curVol = -1, curCh = -1;
// ========== 初始化:拉目录 ==========
async function initStory(){
try {
const d = await API.story();
STORY.volumes = (d && d.volumes) || [];
} catch(e){
STORY.volumes = [];
}
renderTOC();
}
// ========== 目录渲染 ==========
function renderTOC(){
const el = document.getElementById('tocList');
if (!STORY.volumes.length){
el.innerHTML = '<div style="padding:30px 16px;color:var(--text-dim);font-size:13px;">暂无剧情内容</div>';
return;
}
let html = '';
STORY.volumes.forEach((vol, vi) => {
html += `<div class="toc-vol">
<div class="vol-header open" data-vi="${vi}">
<span class="vol-toggle">▸</span>
<span class="vol-name">${vol.name}</span>
</div>`;
vol.chapters.forEach((ch, ci) => {
html += `<a class="toc-ch" data-vol="${vi}" data-ci="${ci}" data-id="${ch.id}">${ch.name}</a>`;
});
html += '</div>';
});
el.innerHTML = html;
// 卷折叠
el.querySelectorAll('.vol-header').forEach(h => h.addEventListener('click', function(){
this.classList.toggle('open');
const items = this.parentElement.querySelectorAll('.toc-ch');
items.forEach(item => item.style.display = this.classList.contains('open') ? '' : 'none');
}));
// 章节点击
el.querySelectorAll('.toc-ch').forEach(a => a.addEventListener('click', function(e){
e.preventDefault();
el.querySelectorAll('.toc-ch').forEach(x=>x.classList.remove('active'));
this.classList.add('active');
loadChapter(+this.dataset.vol, +this.dataset.ci);
}));
// 默认打开第一卷第一章
const first = el.querySelector('.toc-ch');
if (first) first.click();
}
// ========== 加载章节 ==========
async function loadChapter(vi, ci){
const vol = STORY.volumes[vi];
if (!vol || !vol.chapters[ci]) return;
const ch = vol.chapters[ci];
curVol = vi; curCh = ci;
const reader = document.getElementById('storyReader');
// 立即更新右侧面板的标题/卷(无需等 API
document.getElementById('infoTitle').textContent = ch.name;
document.getElementById('infoVol').textContent = vol.name;
document.getElementById('infoStatus').textContent = '加载中…';
document.getElementById('infoStatus').style.color = 'var(--gold)';
// 找到或构建正文结构(保持骨架,复用避免闪烁)
let content = document.getElementById('readerContent');
if (!content) {
// 首次加载:构建完整骨架
reader.innerHTML = `<div class="reader-inner" id="readerContent">
<div class="reader-breadcrumb">${vol.name}</div>
<h1>${ch.name}</h1>
<div class="reader-meta">
<span>📄 <span id="metaWords">…</span></span>
<span class="meta-chpos">第 ${ci+1}/${vol.chapters.length} 章</span>
<span>📚 ${vol.name}</span>
</div>
<div class="reader-body" id="readerBody"></div>
<div class="reader-nav-row">
<button class="reader-nav-btn" onclick="goPrev()" ${vi===0&&ci===0?'disabled':''}>← 上一章</button>
<button class="reader-nav-btn" onclick="goNext()" ${vi===STORY.volumes.length-1&&ci===vol.chapters.length-1?'disabled':''}>下一章 →</button>
</div>
</div>`;
} else {
// 已有骨架:仅更新动态字段
const bread = content.querySelector('.reader-breadcrumb');
if (bread) bread.textContent = vol.name;
const h1 = content.querySelector('h1');
if (h1) h1.textContent = ch.name;
const mc = content.querySelector('.meta-chpos');
if (mc) mc.textContent = `${ci+1}/${vol.chapters.length}`;
// 更新导航按钮状态
const btns = content.querySelectorAll('.reader-nav-btn');
if (btns.length>=2) {
btns[0].disabled = (vi===0 && ci===0);
btns[0].onclick = goPrev;
btns[1].disabled = (vi===STORY.volumes.length-1 && ci===vol.chapters.length-1);
btns[1].onclick = goNext;
}
}
// 仅对正文区做淡出(结构、背景不动 → 无闪烁)
const bodyEl = document.getElementById('readerBody');
bodyEl.style.opacity = '0';
await new Promise(r => setTimeout(r, 140));
bodyEl.innerHTML = '<div class="chapter-loading"><div class="spinner" style="width:30px;height:30px;border:3px solid rgba(139,92,246,.2);border-top-color:var(--violet);border-radius:50%;animation:spin .7s linear infinite;margin:0 auto 14px;"></div>正在加载...</div>';
bodyEl.style.opacity = '1';
reader.scrollTop = 0;
setupProgress();
try {
const d = await API.storyChapter(ch.id);
const body = d.body_html || '';
const wordCount = body.replace(/<[^>]+>/g,'').replace(/\s/g,'').length;
// 淡出旧 loading → 写入正文 → 淡入
bodyEl.style.opacity = '0';
await new Promise(r => setTimeout(r, 100));
bodyEl.innerHTML = body;
bodyEl.style.opacity = '1';
const mw = document.getElementById('metaWords');
if (mw) mw.textContent = wordCount + ' 字';
document.getElementById('infoWords').textContent = wordCount >= 1000 ? Math.round(wordCount/100)/10 + 'k' : wordCount;
document.getElementById('infoStatus').textContent = '已加载';
document.getElementById('infoStatus').style.color = 'var(--cyan)';
updateNavBtns();
} catch(e) {
bodyEl.innerHTML = '<p style="color:var(--text-dim);padding:40px;text-align:center;">本章内容加载失败,请稍后重试...</p>';
document.getElementById('infoStatus').textContent = '加载失败';
document.getElementById('infoStatus').style.color = 'var(--gold)';
}
}
// ========== 导航 ==========
function goPrev(){
if(curVol<0)return;
if(curCh > 0){ curCh--; } else if(curVol > 0){ curVol--; curCh = STORY.volumes[curVol].chapters.length-1; } else return;
jumpTo(curVol,curCh);
}
function goNext(){
if(curVol<0)return;
if(curCh < STORY.volumes[curVol].chapters.length-1){ curCh++; } else if(curVol < STORY.volumes.length-1){ curVol++; curCh=0; } else return;
jumpTo(curVol,curCh);
}
function jumpTo(vi,ci){
const vol = STORY.volumes[vi];
if(!vol || !vol.chapters[ci])return;
// 更新目录高亮
document.querySelectorAll('.toc-ch').forEach((a)=>{
const av=+a.dataset.vol, ac=+a.dataset.ci;
a.classList.toggle('active', av===vi && ac===ci);
});
loadChapter(vi,ci);
}
function updateNavBtns(){
const prev=document.getElementById('sidePrev'), next=document.getElementById('sideNext');
if(prev){prev.disabled=(curVol===0&&curCh===0);}
if(next){next.disabled=(curVol>=STORY.volumes.length-1 && curCh>=STORY.volumes[curVol?curVol:0].chapters.length-1);}
}
// 侧栏快捷跳转按钮
(function(){
const sp=document.getElementById('sidePrev'),sn=document.getElementById('sideNext');
if(sp)sp.onclick=goPrev;
if(sn)sn.onclick=goNext;
})();
// ========== 进度条 ==========
function setupProgress(){
const reader = document.getElementById('storyReader');
const fill = document.getElementById('progressFill');
const pct = document.getElementById('progressPct');
if(!reader||!fill) return;
reader.addEventListener('scroll', () => {
const st = reader.scrollTop;
const wh = reader.clientHeight;
const sh = reader.scrollHeight - wh;
const p = sh > 0 ? Math.min(100, Math.round(st/sh*100)) : 0;
fill.style.width = p+'%';
pct.textContent = p+'%';
}, {passive:true});
}
// ========== 字号设置 ==========
const sizeMap={xs:17,sm:20,md:24,lg:28,xl:34,xxl:40};
const sizeOrder=['xs','sm','md','lg','xl','xxl'];
function applyFontSize(sz){
const px=sizeMap[sz];
const body=document.getElementById('readerBody');
if(body)body.style.fontSize=px+'px';
document.querySelectorAll('.size-btn[data-sz]').forEach(b=>b.classList.toggle('active',b.dataset.sz===sz));
try{ localStorage.setItem('storyFontSize', sz); }catch(e){}
}
document.querySelectorAll('.size-btn[data-sz]').forEach(btn=>{
btn.addEventListener('click', ()=>applyFontSize(btn.dataset.sz));
});
// 应用上次字号,默认 xl最大
let savedSz='xl';
try{ savedSz = localStorage.getItem('storyFontSize') || 'xl'; }catch(e){}
applyFontSize(savedSz);
// ========== 初始化 ==========
initStory();
// spin 动画
const style=document.createElement('style');
style.textContent='@keyframes spin{to{transform:rotate(360deg)}}';
document.head.appendChild(style);
</script>
</body>
</html>