2026-03-27 16:02:13 +08:00
|
|
|
|
// ==================== 章节数据管理 ====================
|
|
|
|
|
|
// 自动从 data/chapter-*.json 文件加载章节数据
|
|
|
|
|
|
let chaptersData = [];
|
|
|
|
|
|
let isDataLoaded = false;
|
|
|
|
|
|
|
2026-03-27 16:25:31 +08:00
|
|
|
|
// 版本号,每次更新时修改
|
2026-03-28 10:38:19 +08:00
|
|
|
|
const CACHE_VERSION = '20260328-1035';
|
2026-03-27 16:25:31 +08:00
|
|
|
|
|
2026-03-27 16:02:13 +08:00
|
|
|
|
// 加载所有章节数据
|
|
|
|
|
|
async function loadChaptersData() {
|
|
|
|
|
|
if (isDataLoaded) return chaptersData;
|
|
|
|
|
|
|
|
|
|
|
|
const chapters = [];
|
2026-03-27 16:14:34 +08:00
|
|
|
|
let chapterNum = 1;
|
2026-03-27 16:02:13 +08:00
|
|
|
|
|
2026-03-27 16:14:34 +08:00
|
|
|
|
// 循环加载章节,直到找不到文件
|
|
|
|
|
|
while (true) {
|
2026-03-27 16:02:13 +08:00
|
|
|
|
try {
|
2026-03-27 16:14:34 +08:00
|
|
|
|
// 格式化章节号(带前导零)
|
|
|
|
|
|
const chapterId = chapterNum.toString().padStart(2, '0');
|
2026-03-27 16:25:31 +08:00
|
|
|
|
const response = await fetch(`data/chapter-${chapterId}.json?v=${CACHE_VERSION}`);
|
2026-03-27 16:02:13 +08:00
|
|
|
|
|
2026-03-27 16:14:34 +08:00
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
// 尝试不带前导零的格式
|
2026-03-27 16:25:31 +08:00
|
|
|
|
const response2 = await fetch(`data/chapter-${chapterNum}.json?v=${CACHE_VERSION}`);
|
2026-03-27 16:14:34 +08:00
|
|
|
|
if (!response2.ok) break;
|
|
|
|
|
|
const data = await response2.json();
|
|
|
|
|
|
chapters.push(normalizeChapterData(data, chapterNum));
|
|
|
|
|
|
} else {
|
2026-03-27 16:02:13 +08:00
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
chapters.push(normalizeChapterData(data, chapterNum));
|
|
|
|
|
|
}
|
2026-03-27 16:14:34 +08:00
|
|
|
|
|
|
|
|
|
|
chapterNum++;
|
2026-03-27 16:02:13 +08:00
|
|
|
|
} catch (error) {
|
2026-03-27 16:14:34 +08:00
|
|
|
|
// 没有更多章节了
|
|
|
|
|
|
break;
|
2026-03-27 16:02:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 16:17:01 +08:00
|
|
|
|
// 额外检查小数章节(如107.5)
|
|
|
|
|
|
for (let i = 1; i <= chapterNum + 10; i++) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const decimalId = i + 0.5;
|
2026-03-27 16:25:31 +08:00
|
|
|
|
const response = await fetch(`data/chapter-${decimalId}.json?v=${CACHE_VERSION}`);
|
2026-03-27 16:17:01 +08:00
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
chapters.push(normalizeChapterData(data, decimalId));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 忽略错误
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按id排序
|
|
|
|
|
|
chapters.sort((a, b) => parseFloat(a.id) - parseFloat(b.id));
|
|
|
|
|
|
|
2026-03-27 16:02:13 +08:00
|
|
|
|
chaptersData = chapters;
|
|
|
|
|
|
isDataLoaded = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 将数据挂载到 window 对象,供其他函数使用
|
|
|
|
|
|
window.chaptersData = chaptersData;
|
|
|
|
|
|
|
|
|
|
|
|
return chaptersData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标准化章节数据
|
|
|
|
|
|
function normalizeChapterData(data, defaultId) {
|
|
|
|
|
|
// 从内容中提取描述(前100个字符)
|
|
|
|
|
|
const extractDesc = (content) => {
|
|
|
|
|
|
if (!content) return '';
|
|
|
|
|
|
// 去除换行和多余空格,取前100字符
|
|
|
|
|
|
const cleanContent = content.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
|
|
|
|
return cleanContent.substring(0, 100) + (cleanContent.length > 100 ? '...' : '');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: data.id || defaultId,
|
|
|
|
|
|
title: data.title || `第${defaultId}章`,
|
|
|
|
|
|
subtitle: data.subtitle || '',
|
|
|
|
|
|
desc: data.desc || extractDesc(data.content),
|
|
|
|
|
|
content: data.content || '',
|
|
|
|
|
|
status: data.status || '已完结',
|
|
|
|
|
|
date: data.date || '2026-03-26'
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== DOM加载完成后初始化 ====================
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
|
|
|
|
// 先加载章节数据
|
|
|
|
|
|
await loadChaptersData();
|
|
|
|
|
|
|
|
|
|
|
|
initTheme();
|
|
|
|
|
|
initNavigation();
|
|
|
|
|
|
initParticles();
|
|
|
|
|
|
|
|
|
|
|
|
// 根据页面类型初始化不同功能
|
|
|
|
|
|
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
|
|
|
|
|
|
|
|
|
|
|
|
if (currentPage === 'index.html' || currentPage === '') {
|
|
|
|
|
|
renderLatestChapters();
|
|
|
|
|
|
} else if (currentPage === 'chapters.html') {
|
|
|
|
|
|
renderChaptersList();
|
|
|
|
|
|
setupFilters();
|
|
|
|
|
|
setupSearch();
|
|
|
|
|
|
updateReadingProgress();
|
|
|
|
|
|
}
|
2026-03-27 17:54:18 +08:00
|
|
|
|
// reader.html 由页面内联脚本处理,不在此处初始化
|
|
|
|
|
|
// 避免冲突导致内容闪烁
|
2026-03-27 16:02:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化回到顶部按钮
|
|
|
|
|
|
initScrollTop();
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化阅读器设置
|
|
|
|
|
|
initReaderSettings();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 主题切换 ====================
|
|
|
|
|
|
function initTheme() {
|
|
|
|
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
|
|
|
|
if (!themeToggle) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查本地存储的主题
|
|
|
|
|
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
|
|
|
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
|
|
|
|
updateThemeIcon(savedTheme);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
updateThemeIcon(newTheme);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateThemeIcon(theme) {
|
|
|
|
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
|
|
|
|
if (!themeToggle) return;
|
|
|
|
|
|
|
|
|
|
|
|
const icon = themeToggle.querySelector('.theme-icon');
|
|
|
|
|
|
if (icon) {
|
|
|
|
|
|
icon.textContent = theme === 'dark' ? '🌙' : '☀️';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 导航栏 ====================
|
|
|
|
|
|
function initNavigation() {
|
|
|
|
|
|
const menuToggle = document.getElementById('menuToggle');
|
|
|
|
|
|
const navLinks = document.querySelector('.nav-links');
|
|
|
|
|
|
|
|
|
|
|
|
if (menuToggle && navLinks) {
|
|
|
|
|
|
menuToggle.addEventListener('click', () => {
|
|
|
|
|
|
navLinks.classList.toggle('active');
|
|
|
|
|
|
menuToggle.classList.toggle('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 导航栏滚动效果
|
|
|
|
|
|
let lastScroll = 0;
|
|
|
|
|
|
const navbar = document.querySelector('.navbar');
|
|
|
|
|
|
|
|
|
|
|
|
if (navbar) {
|
|
|
|
|
|
window.addEventListener('scroll', () => {
|
|
|
|
|
|
const currentScroll = window.pageYOffset;
|
|
|
|
|
|
|
|
|
|
|
|
if (currentScroll > 100) {
|
|
|
|
|
|
navbar.style.background = 'rgba(15, 15, 26, 0.95)';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
navbar.style.background = 'rgba(15, 15, 26, 0.8)';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastScroll = currentScroll;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 粒子效果 ====================
|
|
|
|
|
|
function initParticles() {
|
|
|
|
|
|
const particlesContainer = document.getElementById('particles');
|
|
|
|
|
|
if (!particlesContainer) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 创建粒子
|
|
|
|
|
|
for (let i = 0; i < 30; i++) {
|
|
|
|
|
|
const particle = document.createElement('div');
|
|
|
|
|
|
particle.className = 'particle';
|
|
|
|
|
|
particle.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
width: ${Math.random() * 4 + 2}px;
|
|
|
|
|
|
height: ${Math.random() * 4 + 2}px;
|
|
|
|
|
|
background: rgba(99, 102, 241, ${Math.random() * 0.3 + 0.1});
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
left: ${Math.random() * 100}%;
|
|
|
|
|
|
top: ${Math.random() * 100}%;
|
|
|
|
|
|
animation: particleFloat ${Math.random() * 10 + 10}s ease-in-out infinite;
|
|
|
|
|
|
animation-delay: ${Math.random() * 5}s;
|
|
|
|
|
|
`;
|
|
|
|
|
|
particlesContainer.appendChild(particle);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 首页最新章节 ====================
|
|
|
|
|
|
function renderLatestChapters() {
|
|
|
|
|
|
const container = document.getElementById('latestChapters');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取最新的6章
|
|
|
|
|
|
const latestChapters = chaptersData.slice(-6).reverse();
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = latestChapters.map(chapter => `
|
|
|
|
|
|
<a href="reader.html?id=${chapter.id}" class="chapter-card">
|
|
|
|
|
|
<div class="chapter-number">${chapter.id}</div>
|
|
|
|
|
|
<div class="chapter-info">
|
|
|
|
|
|
<h3 class="chapter-title">${chapter.title}</h3>
|
|
|
|
|
|
<p class="chapter-desc">${chapter.desc}</p>
|
|
|
|
|
|
<div class="chapter-meta">
|
|
|
|
|
|
<span>${chapter.date}</span>
|
|
|
|
|
|
<span class="chapter-status">${chapter.status}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 章节列表页 ====================
|
|
|
|
|
|
function renderChaptersList() {
|
|
|
|
|
|
const container = document.getElementById('chaptersList');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用加载的章节数据
|
|
|
|
|
|
const data = window.chaptersData || chaptersData;
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = data.map(chapter => `
|
|
|
|
|
|
<div class="timeline-item" data-chapter="${chapter.id}">
|
|
|
|
|
|
<div class="timeline-marker"></div>
|
|
|
|
|
|
<a href="reader.html?id=${chapter.id}" class="timeline-content">
|
|
|
|
|
|
<div class="timeline-header">
|
|
|
|
|
|
<h3>第${chapter.id}章 ${chapter.title}</h3>
|
|
|
|
|
|
<span class="timeline-date">${chapter.date}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="timeline-subtitle">${chapter.subtitle}</p>
|
|
|
|
|
|
<p class="timeline-desc">${chapter.desc}</p>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setupFilters() {
|
|
|
|
|
|
const filterTabs = document.querySelectorAll('.filter-tab');
|
|
|
|
|
|
|
|
|
|
|
|
filterTabs.forEach(tab => {
|
|
|
|
|
|
tab.addEventListener('click', () => {
|
|
|
|
|
|
filterTabs.forEach(t => t.classList.remove('active'));
|
|
|
|
|
|
tab.classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
const filter = tab.dataset.filter;
|
|
|
|
|
|
filterChapters(filter);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function filterChapters(filter) {
|
|
|
|
|
|
const items = document.querySelectorAll('.timeline-item');
|
|
|
|
|
|
const data = window.chaptersData || chaptersData;
|
|
|
|
|
|
|
|
|
|
|
|
items.forEach((item, index) => {
|
|
|
|
|
|
let show = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (filter === 'latest') {
|
|
|
|
|
|
show = index >= data.length - 5;
|
|
|
|
|
|
} else if (filter === 'unread') {
|
|
|
|
|
|
const chapterId = parseInt(item.dataset.chapter);
|
|
|
|
|
|
const readChapters = JSON.parse(localStorage.getItem('readChapters') || '[]');
|
|
|
|
|
|
show = !readChapters.includes(chapterId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (show) {
|
|
|
|
|
|
item.classList.remove('hidden');
|
|
|
|
|
|
item.style.display = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
item.classList.add('hidden');
|
|
|
|
|
|
item.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setupSearch() {
|
|
|
|
|
|
const searchInput = document.getElementById('searchInput');
|
|
|
|
|
|
if (!searchInput) return;
|
|
|
|
|
|
|
|
|
|
|
|
searchInput.addEventListener('input', (e) => {
|
|
|
|
|
|
const query = e.target.value.toLowerCase();
|
|
|
|
|
|
const items = document.querySelectorAll('.timeline-item');
|
|
|
|
|
|
|
|
|
|
|
|
items.forEach(item => {
|
|
|
|
|
|
const title = item.querySelector('h3').textContent.toLowerCase();
|
|
|
|
|
|
const desc = item.querySelector('.timeline-desc').textContent.toLowerCase();
|
|
|
|
|
|
const match = title.includes(query) || desc.includes(query);
|
|
|
|
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
item.classList.remove('hidden');
|
|
|
|
|
|
item.style.display = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
item.classList.add('hidden');
|
|
|
|
|
|
item.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateReadingProgress() {
|
|
|
|
|
|
const readChapters = JSON.parse(localStorage.getItem('readChapters') || '[]');
|
|
|
|
|
|
const data = window.chaptersData || chaptersData;
|
|
|
|
|
|
const progress = Math.round((readChapters.length / data.length) * 100);
|
|
|
|
|
|
|
|
|
|
|
|
const progressFill = document.getElementById('progressFill');
|
|
|
|
|
|
const progressText = document.getElementById('progressText');
|
|
|
|
|
|
|
|
|
|
|
|
if (progressFill) progressFill.style.width = `${progress}%`;
|
|
|
|
|
|
if (progressText) progressText.textContent = `阅读进度 ${progress}%`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 阅读器 ====================
|
|
|
|
|
|
async function initReader() {
|
|
|
|
|
|
// 确保数据已加载
|
|
|
|
|
|
if (!isDataLoaded) {
|
|
|
|
|
|
await loadChaptersData();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 16:48:42 +08:00
|
|
|
|
// 获取章节参数(支持小数章节)
|
2026-03-27 16:02:13 +08:00
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
2026-03-27 16:48:42 +08:00
|
|
|
|
const chapterId = parseFloat(urlParams.get('id')) || 1;
|
2026-03-27 16:02:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 加载章节内容
|
|
|
|
|
|
await loadChapter(chapterId);
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化阅读器设置
|
|
|
|
|
|
initReaderSettings();
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化导航
|
|
|
|
|
|
initReaderNav(chapterId);
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化侧边栏
|
|
|
|
|
|
initSidebar();
|
|
|
|
|
|
|
|
|
|
|
|
// 记录阅读进度
|
|
|
|
|
|
recordReadingProgress(chapterId);
|
|
|
|
|
|
|
|
|
|
|
|
// 自动隐藏头部和底部
|
|
|
|
|
|
initAutoHide();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadChapter(chapterId) {
|
2026-03-27 16:28:29 +08:00
|
|
|
|
// 支持小数章节,使用parseFloat比较
|
|
|
|
|
|
const chapter = chaptersData.find(c => parseFloat(c.id) === parseFloat(chapterId));
|
2026-03-27 16:02:13 +08:00
|
|
|
|
if (!chapter) {
|
|
|
|
|
|
// 尝试从JSON文件直接加载
|
|
|
|
|
|
try {
|
2026-03-27 16:28:29 +08:00
|
|
|
|
const chapterIdStr = chapterId.toString();
|
|
|
|
|
|
const response = await fetch(`data/chapter-${chapterIdStr}.json?v=${CACHE_VERSION}`);
|
2026-03-27 16:02:13 +08:00
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
const normalizedData = normalizeChapterData(data, chapterId);
|
|
|
|
|
|
renderChapterContent(normalizedData);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载章节失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderChapterContent(chapter);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderChapterContent(chapter) {
|
|
|
|
|
|
// 更新标题
|
|
|
|
|
|
const chapterTitleEl = document.getElementById('chapterTitle');
|
|
|
|
|
|
if (chapterTitleEl) {
|
|
|
|
|
|
chapterTitleEl.textContent = chapter.title;
|
|
|
|
|
|
}
|
|
|
|
|
|
document.title = `${chapter.title} - 阿拉德:剑之回响`;
|
|
|
|
|
|
|
|
|
|
|
|
// 加载内容
|
|
|
|
|
|
const contentEl = document.getElementById('chapterContent');
|
|
|
|
|
|
if (contentEl) {
|
|
|
|
|
|
// 将内容中的换行转换为段落
|
|
|
|
|
|
const paragraphs = chapter.content.split('\n\n').filter(p => p.trim());
|
|
|
|
|
|
const contentHtml = paragraphs.map(p => {
|
|
|
|
|
|
// 处理分隔线
|
|
|
|
|
|
if (p.trim() === '---') {
|
|
|
|
|
|
return '<hr class="chapter-divider">';
|
|
|
|
|
|
}
|
|
|
|
|
|
// 处理普通段落
|
|
|
|
|
|
return `<p>${p.replace(/\n/g, '<br>')}</p>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
contentEl.innerHTML = `
|
|
|
|
|
|
<div class="chapter-header">
|
|
|
|
|
|
<h1>${chapter.title}</h1>
|
|
|
|
|
|
<p class="chapter-subtitle">${chapter.subtitle}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chapter-body">
|
|
|
|
|
|
${contentHtml}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chapter-footer">
|
|
|
|
|
|
<p>本章完</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function initReaderNav(currentId) {
|
|
|
|
|
|
const prevBtn = document.getElementById('prevChapter');
|
|
|
|
|
|
const nextBtn = document.getElementById('nextChapter');
|
|
|
|
|
|
|
|
|
|
|
|
const data = window.chaptersData || chaptersData;
|
|
|
|
|
|
|
2026-03-27 16:25:31 +08:00
|
|
|
|
// 找到当前章节在数组中的索引
|
|
|
|
|
|
const currentIndex = data.findIndex(ch => parseFloat(ch.id) === parseFloat(currentId));
|
|
|
|
|
|
|
2026-03-27 16:02:13 +08:00
|
|
|
|
if (prevBtn) {
|
2026-03-27 16:25:31 +08:00
|
|
|
|
if (currentIndex > 0) {
|
|
|
|
|
|
const prevChapter = data[currentIndex - 1];
|
2026-03-27 16:02:13 +08:00
|
|
|
|
prevBtn.disabled = false;
|
|
|
|
|
|
prevBtn.onclick = () => {
|
2026-03-27 16:25:31 +08:00
|
|
|
|
window.location.href = `reader.html?id=${prevChapter.id}`;
|
2026-03-27 16:02:13 +08:00
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
prevBtn.disabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (nextBtn) {
|
2026-03-27 16:25:31 +08:00
|
|
|
|
if (currentIndex < data.length - 1) {
|
|
|
|
|
|
const nextChapter = data[currentIndex + 1];
|
2026-03-27 16:02:13 +08:00
|
|
|
|
nextBtn.disabled = false;
|
|
|
|
|
|
nextBtn.onclick = () => {
|
2026-03-27 16:25:31 +08:00
|
|
|
|
window.location.href = `reader.html?id=${nextChapter.id}`;
|
2026-03-27 16:02:13 +08:00
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
nextBtn.disabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function initSidebar() {
|
|
|
|
|
|
const openSidebar = document.getElementById('openSidebar');
|
|
|
|
|
|
const sidebarClose = document.getElementById('sidebarClose');
|
|
|
|
|
|
const readerSidebar = document.getElementById('readerSidebar');
|
|
|
|
|
|
const overlay = document.getElementById('overlay');
|
|
|
|
|
|
|
|
|
|
|
|
// 生成侧边栏章节列表
|
|
|
|
|
|
const sidebarChapters = document.getElementById('sidebarChapters');
|
|
|
|
|
|
if (sidebarChapters) {
|
|
|
|
|
|
const data = window.chaptersData || chaptersData;
|
|
|
|
|
|
const currentId = getCurrentChapterId();
|
|
|
|
|
|
|
|
|
|
|
|
sidebarChapters.innerHTML = data.map(chapter => `
|
|
|
|
|
|
<a href="reader.html?id=${chapter.id}" class="sidebar-chapter ${chapter.id === currentId ? 'active' : ''}">
|
|
|
|
|
|
<span class="chapter-num">${chapter.id}</span>
|
|
|
|
|
|
<span class="chapter-name">${chapter.title}</span>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (openSidebar && readerSidebar && overlay) {
|
|
|
|
|
|
openSidebar.addEventListener('click', () => {
|
|
|
|
|
|
readerSidebar.classList.add('active');
|
|
|
|
|
|
overlay.classList.add('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (sidebarClose && readerSidebar && overlay) {
|
|
|
|
|
|
sidebarClose.addEventListener('click', () => {
|
|
|
|
|
|
readerSidebar.classList.remove('active');
|
|
|
|
|
|
overlay.classList.remove('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (overlay && readerSidebar) {
|
|
|
|
|
|
overlay.addEventListener('click', () => {
|
|
|
|
|
|
readerSidebar.classList.remove('active');
|
|
|
|
|
|
overlay.classList.remove('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getCurrentChapterId() {
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
2026-03-27 16:32:35 +08:00
|
|
|
|
const id = urlParams.get('id');
|
|
|
|
|
|
return id ? parseFloat(id) : 1;
|
2026-03-27 16:02:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function recordReadingProgress(chapterId) {
|
|
|
|
|
|
let readChapters = JSON.parse(localStorage.getItem('readChapters') || '[]');
|
|
|
|
|
|
if (!readChapters.includes(chapterId)) {
|
|
|
|
|
|
readChapters.push(chapterId);
|
|
|
|
|
|
localStorage.setItem('readChapters', JSON.stringify(readChapters));
|
|
|
|
|
|
}
|
|
|
|
|
|
localStorage.setItem('lastReadChapter', chapterId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function initAutoHide() {
|
|
|
|
|
|
const header = document.getElementById('readerHeader');
|
|
|
|
|
|
const footer = document.getElementById('readerFooter');
|
|
|
|
|
|
let lastScrollY = window.scrollY;
|
|
|
|
|
|
let ticking = false;
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('scroll', () => {
|
|
|
|
|
|
if (!ticking) {
|
|
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
|
|
const currentScrollY = window.scrollY;
|
|
|
|
|
|
|
|
|
|
|
|
if (currentScrollY > lastScrollY && currentScrollY > 100) {
|
|
|
|
|
|
// 向下滚动,隐藏
|
|
|
|
|
|
header?.classList.add('hidden');
|
|
|
|
|
|
footer?.classList.add('hidden');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 向上滚动,显示
|
|
|
|
|
|
header?.classList.remove('hidden');
|
|
|
|
|
|
footer?.classList.remove('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastScrollY = currentScrollY;
|
|
|
|
|
|
ticking = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
ticking = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 工具函数 ====================
|
|
|
|
|
|
function debounce(func, wait) {
|
|
|
|
|
|
let timeout;
|
|
|
|
|
|
return function executedFunction(...args) {
|
|
|
|
|
|
const later = () => {
|
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
func(...args);
|
|
|
|
|
|
};
|
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
timeout = setTimeout(later, wait);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function throttle(func, limit) {
|
|
|
|
|
|
let inThrottle;
|
|
|
|
|
|
return function(...args) {
|
|
|
|
|
|
if (!inThrottle) {
|
|
|
|
|
|
func.apply(this, args);
|
|
|
|
|
|
inThrottle = true;
|
|
|
|
|
|
setTimeout(() => inThrottle = false, limit);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 回到顶部按钮 ====================
|
|
|
|
|
|
function initScrollTop() {
|
|
|
|
|
|
const scrollTopBtn = document.getElementById('scrollTop');
|
|
|
|
|
|
if (!scrollTopBtn) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 点击回到顶部
|
|
|
|
|
|
scrollTopBtn.addEventListener('click', () => {
|
|
|
|
|
|
window.scrollTo({
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
behavior: 'smooth'
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动时显示/隐藏按钮
|
|
|
|
|
|
const toggleVisibility = () => {
|
|
|
|
|
|
if (window.pageYOffset > 300) {
|
|
|
|
|
|
scrollTopBtn.style.opacity = '1';
|
|
|
|
|
|
scrollTopBtn.style.visibility = 'visible';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
scrollTopBtn.style.opacity = '0';
|
|
|
|
|
|
scrollTopBtn.style.visibility = 'hidden';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 初始状态隐藏
|
|
|
|
|
|
scrollTopBtn.style.opacity = '0';
|
|
|
|
|
|
scrollTopBtn.style.visibility = 'hidden';
|
|
|
|
|
|
scrollTopBtn.style.transition = 'opacity 0.3s ease, visibility 0.3s ease';
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('scroll', throttle(toggleVisibility, 100));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 独立章节页面初始化 ====================
|
|
|
|
|
|
async function initChapterReader() {
|
|
|
|
|
|
// 确保数据已加载
|
|
|
|
|
|
if (!isDataLoaded) {
|
|
|
|
|
|
await loadChaptersData();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openSidebar = document.getElementById('openSidebar');
|
|
|
|
|
|
const sidebarClose = document.getElementById('sidebarClose');
|
|
|
|
|
|
const readerSidebar = document.getElementById('readerSidebar');
|
|
|
|
|
|
const overlay = document.getElementById('overlay');
|
|
|
|
|
|
const progressFill = document.getElementById('progressFill');
|
|
|
|
|
|
|
|
|
|
|
|
const data = window.chaptersData || chaptersData;
|
|
|
|
|
|
|
|
|
|
|
|
// 动态计算进度条(基于总章节数)
|
|
|
|
|
|
if (progressFill) {
|
|
|
|
|
|
const totalChapters = data.length;
|
|
|
|
|
|
const currentChapterMatch = window.location.pathname.match(/chapter-(\d+)\.html/);
|
|
|
|
|
|
const currentChapter = currentChapterMatch ? parseInt(currentChapterMatch[1]) : 1;
|
|
|
|
|
|
const progress = Math.round((currentChapter / totalChapters) * 100);
|
|
|
|
|
|
progressFill.style.width = progress + '%';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成侧边栏章节列表
|
|
|
|
|
|
if (readerSidebar) {
|
|
|
|
|
|
const sidebarChapters = document.getElementById('sidebarChapters');
|
|
|
|
|
|
if (sidebarChapters) {
|
|
|
|
|
|
const currentChapterMatch = window.location.pathname.match(/chapter-(\d+)\.html/);
|
|
|
|
|
|
const currentChapter = currentChapterMatch ? parseInt(currentChapterMatch[1]) : 1;
|
|
|
|
|
|
|
|
|
|
|
|
sidebarChapters.innerHTML = data.map(chapter => `
|
|
|
|
|
|
<a href="chapter-${chapter.id.toString().padStart(2, '0')}.html" class="sidebar-chapter ${chapter.id === currentChapter ? 'active' : ''}">
|
|
|
|
|
|
<span class="chapter-num">${chapter.id}</span>
|
|
|
|
|
|
<span class="chapter-name">${chapter.title}</span>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (openSidebar && readerSidebar && overlay) {
|
|
|
|
|
|
openSidebar.addEventListener('click', () => {
|
|
|
|
|
|
readerSidebar.classList.add('active');
|
|
|
|
|
|
overlay.classList.add('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (sidebarClose && readerSidebar && overlay) {
|
|
|
|
|
|
sidebarClose.addEventListener('click', () => {
|
|
|
|
|
|
readerSidebar.classList.remove('active');
|
|
|
|
|
|
overlay.classList.remove('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (overlay && readerSidebar) {
|
|
|
|
|
|
overlay.addEventListener('click', () => {
|
|
|
|
|
|
readerSidebar.classList.remove('active');
|
|
|
|
|
|
overlay.classList.remove('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 阅读器设置 ====================
|
|
|
|
|
|
function initReaderSettings() {
|
|
|
|
|
|
const settingsBtn = document.getElementById('readerSettings');
|
|
|
|
|
|
const settingsPanel = document.getElementById('settingsPanel');
|
|
|
|
|
|
|
|
|
|
|
|
if (!settingsBtn || !settingsPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 切换设置面板显示
|
|
|
|
|
|
settingsBtn.addEventListener('click', (e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
settingsPanel.classList.toggle('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 点击外部关闭面板
|
|
|
|
|
|
document.addEventListener('click', (e) => {
|
|
|
|
|
|
if (!settingsPanel.contains(e.target) && e.target !== settingsBtn) {
|
|
|
|
|
|
settingsPanel.classList.remove('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化主题
|
|
|
|
|
|
initThemeSettings();
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化字体大小
|
|
|
|
|
|
initFontSize();
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化行间距
|
|
|
|
|
|
initLineHeight();
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化阅读宽度
|
|
|
|
|
|
initReadWidth();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 主题设置
|
|
|
|
|
|
function initThemeSettings() {
|
|
|
|
|
|
const themeOptions = document.querySelectorAll('.theme-option');
|
|
|
|
|
|
const savedTheme = localStorage.getItem('readerTheme') || 'dark';
|
|
|
|
|
|
|
|
|
|
|
|
// 应用保存的主题
|
|
|
|
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置激活状态
|
|
|
|
|
|
themeOptions.forEach(option => {
|
|
|
|
|
|
if (option.dataset.theme === savedTheme) {
|
|
|
|
|
|
option.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
option.addEventListener('click', () => {
|
|
|
|
|
|
const theme = option.dataset.theme;
|
|
|
|
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
|
|
|
|
localStorage.setItem('readerTheme', theme);
|
|
|
|
|
|
|
|
|
|
|
|
themeOptions.forEach(o => o.classList.remove('active'));
|
|
|
|
|
|
option.classList.add('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 字体大小设置
|
|
|
|
|
|
function initFontSize() {
|
|
|
|
|
|
const decreaseBtn = document.getElementById('fontDecrease');
|
|
|
|
|
|
const increaseBtn = document.getElementById('fontIncrease');
|
|
|
|
|
|
const fontValue = document.getElementById('fontValue');
|
|
|
|
|
|
const content = document.querySelector('.chapter-article') || document.getElementById('chapterContent');
|
|
|
|
|
|
|
|
|
|
|
|
if (!content) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sizes = ['14px', '16px', '18px', '20px', '22px', '24px'];
|
|
|
|
|
|
let currentSize = localStorage.getItem('readerFontSize') || '18px';
|
|
|
|
|
|
|
|
|
|
|
|
// 应用保存的字体大小
|
|
|
|
|
|
content.style.fontSize = currentSize;
|
|
|
|
|
|
if (fontValue) fontValue.textContent = currentSize;
|
|
|
|
|
|
|
|
|
|
|
|
decreaseBtn?.addEventListener('click', () => {
|
|
|
|
|
|
const index = sizes.indexOf(currentSize);
|
|
|
|
|
|
if (index > 0) {
|
|
|
|
|
|
currentSize = sizes[index - 1];
|
|
|
|
|
|
content.style.fontSize = currentSize;
|
|
|
|
|
|
if (fontValue) fontValue.textContent = currentSize;
|
|
|
|
|
|
localStorage.setItem('readerFontSize', currentSize);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
increaseBtn?.addEventListener('click', () => {
|
|
|
|
|
|
const index = sizes.indexOf(currentSize);
|
|
|
|
|
|
if (index < sizes.length - 1) {
|
|
|
|
|
|
currentSize = sizes[index + 1];
|
|
|
|
|
|
content.style.fontSize = currentSize;
|
|
|
|
|
|
if (fontValue) fontValue.textContent = currentSize;
|
|
|
|
|
|
localStorage.setItem('readerFontSize', currentSize);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 行间距设置
|
|
|
|
|
|
function initLineHeight() {
|
|
|
|
|
|
const lineHeightBtns = document.querySelectorAll('.line-height-btn');
|
|
|
|
|
|
const content = document.querySelector('.chapter-article') || document.getElementById('chapterContent');
|
|
|
|
|
|
|
|
|
|
|
|
if (!content) return;
|
|
|
|
|
|
|
|
|
|
|
|
const savedLineHeight = localStorage.getItem('readerLineHeight') || '1.8';
|
|
|
|
|
|
content.style.lineHeight = savedLineHeight;
|
|
|
|
|
|
|
|
|
|
|
|
lineHeightBtns.forEach(btn => {
|
|
|
|
|
|
if (btn.dataset.value === savedLineHeight) {
|
|
|
|
|
|
btn.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
|
const value = btn.dataset.value;
|
|
|
|
|
|
content.style.lineHeight = value;
|
|
|
|
|
|
localStorage.setItem('readerLineHeight', value);
|
|
|
|
|
|
|
|
|
|
|
|
lineHeightBtns.forEach(b => b.classList.remove('active'));
|
|
|
|
|
|
btn.classList.add('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 阅读宽度设置
|
|
|
|
|
|
function initReadWidth() {
|
|
|
|
|
|
const widthBtns = document.querySelectorAll('.width-btn');
|
|
|
|
|
|
const content = document.querySelector('.reader-content');
|
|
|
|
|
|
|
|
|
|
|
|
if (!content) return;
|
|
|
|
|
|
|
|
|
|
|
|
const savedWidth = localStorage.getItem('readerWidth') || 'medium';
|
|
|
|
|
|
applyWidth(content, savedWidth);
|
|
|
|
|
|
|
|
|
|
|
|
widthBtns.forEach(btn => {
|
|
|
|
|
|
if (btn.dataset.width === savedWidth) {
|
|
|
|
|
|
btn.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
|
const width = btn.dataset.width;
|
|
|
|
|
|
applyWidth(content, width);
|
|
|
|
|
|
localStorage.setItem('readerWidth', width);
|
|
|
|
|
|
|
|
|
|
|
|
widthBtns.forEach(b => b.classList.remove('active'));
|
|
|
|
|
|
btn.classList.add('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyWidth(content, width) {
|
|
|
|
|
|
const widths = {
|
|
|
|
|
|
narrow: '680px',
|
|
|
|
|
|
medium: '800px',
|
|
|
|
|
|
wide: '100%'
|
|
|
|
|
|
};
|
|
|
|
|
|
content.style.maxWidth = widths[width] || widths.medium;
|
|
|
|
|
|
}
|