jianzhihuixiang/alacarte-novel-website/reader.html
小虾米 f8894dd3f3 fix: 修复上一章/下一章导航对小数章节(如107.5)的支持
- updateNavButtons: 改用数组索引查找,而非简单的+1/-1
- updateSidebarHighlight: 使用parseFloat比较章节ID
- updateMobileTOCHighlight: 使用parseFloat比较章节ID
- 更新版本号强制刷新缓存
2026-03-27 17:42:41 +08:00

662 lines
24 KiB
HTML
Raw 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">
<title>阿拉德:剑之回响</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
--text-primary: #e0e0e0;
--text-secondary: #888;
--accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--border-color: rgba(255,255,255,0.1);
--btn-bg: rgba(255,255,255,0.1);
--btn-hover: rgba(255,255,255,0.2);
}
[data-theme="light"] {
--bg-primary: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
--text-primary: #333;
--text-secondary: #666;
--border-color: rgba(0,0,0,0.1);
--btn-bg: rgba(0,0,0,0.05);
--btn-hover: rgba(0,0,0,0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Serif SC', serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.8;
min-height: 100vh;
transition: all 0.3s ease;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
padding-bottom: 120px;
}
/* 顶部导航 */
.top-nav {
position: fixed;
top: 0; left: 0; right: 0;
background: rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-color);
z-index: 1000;
padding: 10px 20px;
}
.top-nav-content {
max-width: 800px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-link {
color: var(--text-primary);
text-decoration: none;
font-family: 'Noto Sans SC', sans-serif;
font-size: 14px;
padding: 8px 16px;
background: var(--btn-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
transition: all 0.3s ease;
}
.nav-link:hover { background: var(--btn-hover); }
.chapter-header {
text-align: center;
padding: 80px 0 40px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 40px;
}
.chapter-number {
font-size: 14px;
color: var(--text-secondary);
letter-spacing: 4px;
text-transform: uppercase;
margin-bottom: 10px;
}
.chapter-title {
font-size: 32px;
font-weight: 700;
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 20px;
}
.chapter-content {
font-size: 18px;
line-height: 2;
text-align: justify;
}
.chapter-content p {
margin-bottom: 1.5em;
text-indent: 2em;
}
.chapter-content p:first-of-type::first-letter {
font-size: 3em;
float: left;
line-height: 1;
margin-right: 8px;
margin-top: -5px;
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
}
/* 固定底部导航 */
.fixed-nav {
position: fixed;
bottom: 0; left: 0; right: 0;
background: rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
border-top: 1px solid var(--border-color);
z-index: 1000;
padding: 15px 20px;
}
.fixed-nav-content {
max-width: 800px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-btn {
padding: 12px 24px;
background: var(--btn-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
text-decoration: none;
transition: all 0.3s ease;
font-family: 'Noto Sans SC', sans-serif;
font-size: 14px;
cursor: pointer;
}
.nav-btn:hover { background: var(--btn-hover); transform: translateY(-2px); }
.nav-btn.disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
/* 右侧滚动按钮 */
.scroll-buttons {
position: fixed;
right: 20px;
bottom: 90px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1001;
}
.scroll-btn {
width: 40px; height: 40px;
border-radius: 8px;
background: var(--btn-bg);
border: 1px solid var(--border-color);
color: var(--text-primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.3s ease;
font-family: 'Noto Sans SC', sans-serif;
}
.scroll-btn:hover { background: var(--btn-hover); }
/* 侧边栏 */
.sidebar {
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: rgba(0,0,0,0.5);
backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid var(--border-color);
width: 200px;
max-height: 70vh;
z-index: 999;
display: flex;
flex-direction: column;
}
/* 移动端目录弹窗 */
.toc-modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8);
z-index: 2000;
justify-content: center;
align-items: center;
padding: 20px;
}
.toc-modal.active { display: flex; }
.toc-content {
background: var(--bg-primary);
border-radius: 12px;
border: 1px solid var(--border-color);
width: 100%;
max-width: 400px;
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.toc-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}
.toc-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.toc-close {
width: 32px; height: 32px;
border-radius: 8px;
background: var(--btn-bg);
border: 1px solid var(--border-color);
color: var(--text-primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.3s ease;
}
.toc-close:hover { background: var(--btn-hover); }
.toc-list {
overflow-y: auto;
padding: 10px 20px 20px;
flex: 1;
}
.toc-item {
display: block;
padding: 12px 0;
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
border-bottom: 1px solid var(--border-color);
transition: all 0.3s ease;
line-height: 1.5;
}
.toc-item:hover { color: #667eea; }
.toc-item.current { color: #667eea; font-weight: 600; }
/* 目录按钮样式与nav-btn统一 */
#tocBtn {
padding: 12px 24px;
background: var(--btn-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-family: 'Noto Sans SC', sans-serif;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
#tocBtn:hover { background: var(--btn-hover); transform: translateY(-2px); }
.sidebar-title {
font-size: 14px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 2px;
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.sidebar-content {
overflow-y: auto;
padding: 10px 20px 20px;
flex: 1;
scrollbar-width: thin;
scrollbar-color: transparent transparent;
transition: scrollbar-color 0.3s ease;
}
.sidebar-content:hover { scrollbar-color: rgba(255,255,255,0.3) transparent; }
.sidebar-content::-webkit-scrollbar { width: 6px; }
.sidebar-content::-webkit-scrollbar-track { background: transparent; }
.sidebar-content::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 3px;
transition: background 0.3s ease;
}
.sidebar-content:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.3); }
.sidebar-content:hover::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.5); }
[data-theme="light"] .sidebar-content:hover { scrollbar-color: rgba(0,0,0,0.3) transparent; }
[data-theme="light"] .sidebar-content:hover::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.3); }
[data-theme="light"] .sidebar-content:hover::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.5); }
.sidebar-chapter {
display: block;
padding: 8px 0;
color: #aaa;
text-decoration: none;
font-size: 13px;
border-bottom: 1px solid rgba(255,255,255,0.05);
transition: all 0.3s ease;
line-height: 1.5;
}
.sidebar-chapter:hover { color: #667eea; }
.sidebar-chapter.current { color: #667eea; font-weight: 600; }
.loading { text-align: center; padding: 100px 0; color: var(--text-secondary); }
.error { text-align: center; padding: 100px 0; color: #ff6b6b; }
@media (max-width: 1200px) { .sidebar { display: none; } }
@media (max-width: 768px) {
.sidebar { display: none; }
#tocBtn { display: block; }
}
@media (min-width: 769px) {
#tocBtn { display: none; }
}
@media (max-width: 600px) {
.chapter-title { font-size: 24px; }
.chapter-content { font-size: 16px; }
.nav-btn { padding: 10px 15px; font-size: 12px; }
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<nav class="top-nav">
<div class="top-nav-content">
<a href="index.html" class="nav-link">返回首页</a>
<button class="nav-link" id="themeToggle">切换主题</button>
</div>
</nav>
<div class="container">
<header class="chapter-header">
<div class="chapter-number" id="chapterNum">加载中...</div>
<h1 class="chapter-title" id="chapterTitle">加载中...</h1>
</header>
<article class="chapter-content" id="chapterContent">
<div class="loading">正在加载章节内容...</div>
</article>
</div>
<!-- 固定底部导航 -->
<nav class="fixed-nav">
<div class="fixed-nav-content">
<a href="#" class="nav-btn" id="prevBtn">上一章</a>
<button class="nav-btn" id="tocBtn">目录</button>
<a href="#" class="nav-btn" id="nextBtn">下一章</a>
</div>
</nav>
<!-- 移动端目录弹窗 -->
<div class="toc-modal" id="tocModal">
<div class="toc-content">
<div class="toc-header">
<span class="toc-title">章节导航</span>
<button class="toc-close" id="tocClose">×</button>
</div>
<div class="toc-list" id="tocList"></div>
</div>
</div>
<!-- 右侧滚动按钮 -->
<div class="scroll-buttons">
<button class="scroll-btn" id="scrollTop"></button>
<button class="scroll-btn" id="scrollBottom"></button>
</div>
<!-- 侧边栏章节导航 -->
<aside class="sidebar">
<div class="sidebar-title">章节导航</div>
<div class="sidebar-content" id="sidebarContent"></div>
</aside>
<script src="js/app.js?v=6"></script>
<script>
// 获取URL参数中的章节ID支持小数章节
const urlParams = new URLSearchParams(window.location.search);
let currentChapter = parseFloat(urlParams.get('id')) || 1;
// 获取带缓存禁用的时间戳
function getCacheBuster() {
return `?t=${Date.now()}`;
}
// 加载具体章节内容
async function loadChapterContent(id) {
const data = window.chaptersData || chaptersData;
if (id < 1 || id > data.length) {
document.getElementById('chapterContent').innerHTML =
'<div class="error">章节不存在</div>';
return;
}
currentChapter = id;
try {
const response = await fetch(`data/chapter-${String(id).padStart(2, '0')}.json` + getCacheBuster());
const chapter = await response.json();
// 将纯文本转换为HTML每段包裹<p>标签)
// 支持单换行或双换行作为段落分隔
const htmlContent = chapter.content
.split(/\n\n|\n/)
.filter(p => p.trim())
.map(p => `<p>${p.trim()}</p>`)
.join('');
// 更新页面内容
document.getElementById('chapterNum').textContent = `Chapter ${id}`;
document.getElementById('chapterTitle').textContent = chapter.title;
document.getElementById('chapterContent').innerHTML = htmlContent;
document.title = `${chapter.title} - 阿拉德:剑之回响`;
// 更新导航按钮
updateNavButtons();
// 更新侧边栏高亮
updateSidebarHighlight();
// 更新移动端目录高亮
updateMobileTOCHighlight();
// 记录阅读进度
let readChapters = JSON.parse(localStorage.getItem('readChapters') || '[]');
if (!readChapters.includes(id)) {
readChapters.push(id);
localStorage.setItem('readChapters', JSON.stringify(readChapters));
}
// 滚动到顶部
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (error) {
console.error('Failed to load chapter:', error);
document.getElementById('chapterContent').innerHTML =
'<div class="error">章节加载失败</div>';
}
}
// 渲染侧边栏
function renderReaderSidebar() {
const data = window.chaptersData || chaptersData;
const sidebarContent = document.getElementById('sidebarContent');
let html = '';
data.forEach(ch => {
html += `<a href="?id=${ch.id}" class="sidebar-chapter" data-id="${ch.id}">第${ch.id}章:${ch.title}</a>`;
});
sidebarContent.innerHTML = html;
updateSidebarHighlight();
// 自动滚动到当前章节
setTimeout(() => {
const current = sidebarContent.querySelector('.current');
if (current) {
current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
}
// 更新侧边栏高亮(支持小数章节)
function updateSidebarHighlight() {
document.querySelectorAll('.sidebar-chapter').forEach(link => {
link.classList.remove('current');
if (parseFloat(link.dataset.id) === parseFloat(currentChapter)) {
link.classList.add('current');
}
});
}
// 更新导航按钮状态(支持小数章节)
function updateNavButtons() {
const data = window.chaptersData || chaptersData;
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
// 找到当前章节在数组中的索引使用parseFloat比较
const currentIndex = data.findIndex(ch => parseFloat(ch.id) === parseFloat(currentChapter));
if (currentIndex <= 0) {
prevBtn.classList.add('disabled');
prevBtn.href = '#';
} else {
const prevChapter = data[currentIndex - 1];
prevBtn.classList.remove('disabled');
prevBtn.href = `?id=${prevChapter.id}`;
}
if (currentIndex < 0 || currentIndex >= data.length - 1) {
nextBtn.classList.add('disabled');
nextBtn.href = '#';
} else {
const nextChapter = data[currentIndex + 1];
nextBtn.classList.remove('disabled');
nextBtn.href = `?id=${nextChapter.id}`;
}
}
// 主题切换
const themeToggle = document.getElementById('themeToggle');
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
themeToggle.textContent = savedTheme === 'dark' ? '浅色' : '深色';
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeToggle.textContent = newTheme === 'dark' ? '浅色' : '深色';
});
// 滚动按钮
document.getElementById('scrollTop').addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
document.getElementById('scrollBottom').addEventListener('click', () => {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
});
// 渲染移动端目录
function renderMobileTOC() {
const data = window.chaptersData || chaptersData;
const tocList = document.getElementById('tocList');
let html = '';
data.forEach(ch => {
const isCurrent = ch.id === currentChapter ? 'current' : '';
html += `<a href="?id=${ch.id}" class="toc-item ${isCurrent}" data-id="${ch.id}">第${ch.id}章:${ch.title}</a>`;
});
tocList.innerHTML = html;
// 自动定位到当前章节(无动画)
setTimeout(() => {
const currentItem = tocList.querySelector('.current');
if (currentItem) {
currentItem.scrollIntoView({ block: 'center' });
}
}, 50);
}
// 更新移动端目录高亮(支持小数章节)
function updateMobileTOCHighlight() {
document.querySelectorAll('.toc-item').forEach(item => {
item.classList.remove('current');
if (parseFloat(item.dataset.id) === parseFloat(currentChapter)) {
item.classList.add('current');
}
});
}
// 目录弹窗控制
const tocModal = document.getElementById('tocModal');
const tocBtn = document.getElementById('tocBtn');
const tocClose = document.getElementById('tocClose');
tocBtn.addEventListener('click', () => {
renderMobileTOC();
tocModal.classList.add('active');
});
tocClose.addEventListener('click', () => {
tocModal.classList.remove('active');
});
tocModal.addEventListener('click', (e) => {
if (e.target === tocModal) {
tocModal.classList.remove('active');
}
});
// 点击目录项后关闭弹窗
document.getElementById('tocList').addEventListener('click', (e) => {
if (e.target.classList.contains('toc-item')) {
tocModal.classList.remove('active');
}
});
// 阻止目录弹窗的滚动事件冒泡到页面
const tocList = document.getElementById('tocList');
tocList.addEventListener('wheel', (e) => {
e.stopPropagation();
}, { passive: true });
tocList.addEventListener('touchmove', (e) => {
e.stopPropagation();
}, { passive: true });
// 阻止弹窗内容区域的滚动冒泡
document.querySelector('.toc-content').addEventListener('wheel', (e) => {
e.stopPropagation();
}, { passive: true });
document.querySelector('.toc-content').addEventListener('touchmove', (e) => {
e.stopPropagation();
}, { passive: true });
// 初始化 - 等待app.js加载章节数据
document.addEventListener('DOMContentLoaded', async function() {
// 等待章节数据加载完成
await loadChaptersData();
// 渲染侧边栏
renderReaderSidebar();
// 渲染移动端目录
renderMobileTOC();
// 加载当前章节
loadChapterContent(currentChapter);
});
</script>
</body>
</html>