624 lines
24 KiB
HTML
624 lines
24 KiB
HTML
<!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>
|