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

811 lines
27 KiB
JavaScript
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.

// ==================== 章节数据管理 ====================
// 自动从 data/chapter-*.json 文件加载章节数据
let chaptersData = [];
let isDataLoaded = false;
// 版本号,每次更新时修改
const CACHE_VERSION = '20260327-1742';
// 加载所有章节数据
async function loadChaptersData() {
if (isDataLoaded) return chaptersData;
const chapters = [];
let chapterNum = 1;
// 循环加载章节,直到找不到文件
while (true) {
try {
// 格式化章节号(带前导零)
const chapterId = chapterNum.toString().padStart(2, '0');
const response = await fetch(`data/chapter-${chapterId}.json?v=${CACHE_VERSION}`);
if (!response.ok) {
// 尝试不带前导零的格式
const response2 = await fetch(`data/chapter-${chapterNum}.json?v=${CACHE_VERSION}`);
if (!response2.ok) break;
const data = await response2.json();
chapters.push(normalizeChapterData(data, chapterNum));
} else {
const data = await response.json();
chapters.push(normalizeChapterData(data, chapterNum));
}
chapterNum++;
} catch (error) {
// 没有更多章节了
break;
}
}
// 额外检查小数章节如107.5
for (let i = 1; i <= chapterNum + 10; i++) {
try {
const decimalId = i + 0.5;
const response = await fetch(`data/chapter-${decimalId}.json?v=${CACHE_VERSION}`);
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));
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();
} else if (currentPage === 'reader.html') {
initReader();
} else if (currentPage.startsWith('chapter-')) {
// 独立章节页面初始化
initChapterReader();
}
// 初始化回到顶部按钮
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();
}
// 获取章节参数(支持小数章节)
const urlParams = new URLSearchParams(window.location.search);
const chapterId = parseFloat(urlParams.get('id')) || 1;
// 加载章节内容
await loadChapter(chapterId);
// 初始化阅读器设置
initReaderSettings();
// 初始化导航
initReaderNav(chapterId);
// 初始化侧边栏
initSidebar();
// 记录阅读进度
recordReadingProgress(chapterId);
// 自动隐藏头部和底部
initAutoHide();
}
async function loadChapter(chapterId) {
// 支持小数章节使用parseFloat比较
const chapter = chaptersData.find(c => parseFloat(c.id) === parseFloat(chapterId));
if (!chapter) {
// 尝试从JSON文件直接加载
try {
const chapterIdStr = chapterId.toString();
const response = await fetch(`data/chapter-${chapterIdStr}.json?v=${CACHE_VERSION}`);
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;
// 找到当前章节在数组中的索引
const currentIndex = data.findIndex(ch => parseFloat(ch.id) === parseFloat(currentId));
if (prevBtn) {
if (currentIndex > 0) {
const prevChapter = data[currentIndex - 1];
prevBtn.disabled = false;
prevBtn.onclick = () => {
window.location.href = `reader.html?id=${prevChapter.id}`;
};
} else {
prevBtn.disabled = true;
}
}
if (nextBtn) {
if (currentIndex < data.length - 1) {
const nextChapter = data[currentIndex + 1];
nextBtn.disabled = false;
nextBtn.onclick = () => {
window.location.href = `reader.html?id=${nextChapter.id}`;
};
} 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);
const id = urlParams.get('id');
return id ? parseFloat(id) : 1;
}
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;
}