jianzhihuixiang/alacarte-novel-website/reader.html
2026-03-29 14:28:43 +08:00

1001 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="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: 10px;
}
.chapter-info {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 20px;
font-family: 'Noto Sans SC', sans-serif;
}
.chapter-info span {
margin: 0 10px;
}
.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-around;
align-items: center;
gap: 10px;
}
.nav-btn {
padding: 10px 16px;
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;
-webkit-appearance: none;
}
.nav-btn:hover { background: var(--btn-hover); }
.nav-btn.disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
.nav-btn.active-tts { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-color: #667eea; }
/* 右侧滚动按钮 */
.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; }
/* TTS弹出面板 */
.tts-modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8);
z-index: 2001;
justify-content: center;
align-items: center;
padding: 20px;
}
.tts-modal.active { display: flex; }
.tts-panel {
background: rgba(30,30,50,0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid var(--border-color);
width: 100%;
max-width: 400px;
padding: 24px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
[data-theme="light"] .tts-panel {
background: rgba(255,255,255,0.95);
}
.tts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border-color);
}
.tts-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
font-family: 'Noto Sans SC', sans-serif;
}
.tts-close {
width: 32px; height: 32px;
border-radius: 50%;
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;
}
.tts-close:hover { background: var(--btn-hover); }
.tts-controls {
display: flex;
justify-content: center;
gap: 16px;
margin-bottom: 20px;
}
.tts-btn {
width: 56px; height: 56px;
border-radius: 50%;
background: var(--btn-bg);
border: 2px solid var(--border-color);
color: var(--text-primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.3s ease;
font-family: 'Noto Sans SC', sans-serif;
}
.tts-btn:hover { background: var(--btn-hover); transform: scale(1.1); }
.tts-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
transform: scale(1.1);
}
.tts-progress { margin-bottom: 20px; }
.tts-progress-bar {
height: 6px;
background: var(--btn-bg);
border-radius: 3px;
overflow: hidden;
margin-bottom: 8px;
}
.tts-progress-fill {
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
width: 0%;
transition: width 0.1s ease;
}
.tts-time {
font-size: 13px;
color: var(--text-secondary);
font-family: 'Noto Sans SC', sans-serif;
text-align: center;
}
.tts-speed {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.tts-speed-label {
font-size: 14px;
color: var(--text-secondary);
font-family: 'Noto Sans SC', sans-serif;
}
.tts-speed-select {
padding: 8px 16px;
background: var(--btn-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
font-family: 'Noto Sans SC', sans-serif;
}
.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>
<div class="chapter-info">
<span id="chapterWordCount">计算中...</span>
</div>
</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>
<button class="nav-btn" id="ttsBtn">朗读</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>
<!-- TTS朗读弹窗 -->
<div class="tts-modal" id="ttsModal">
<div class="tts-panel">
<div class="tts-header">
<span class="tts-title">🔊 语音朗读</span>
<button class="tts-close" id="ttsClose">×</button>
</div>
<div class="tts-controls">
<button class="tts-btn" id="ttsPlay" title="播放"></button>
<button class="tts-btn" id="ttsPause" title="暂停"></button>
<button class="tts-btn" id="ttsStop" title="停止"></button>
</div>
<div class="tts-progress">
<div class="tts-progress-bar">
<div class="tts-progress-fill" id="ttsProgressFill"></div>
</div>
<div class="tts-time" id="ttsTime">0 / 0 句</div>
<div style="text-align: center; color: var(--text-secondary); font-size: 12px; margin-top: 15px; font-family: 'Noto Sans SC', sans-serif;">
点击 ▶ 按钮开始朗读
</div>
</div>
<div class="tts-speed">
<span class="tts-speed-label">朗读速度:</span>
<select class="tts-speed-select" id="ttsSpeed">
<option value="0.5">慢速 (0.5x)</option>
<option value="0.75">较慢 (0.75x)</option>
<option value="1" selected>正常 (1x)</option>
<option value="1.25">较快 (1.25x)</option>
<option value="1.5">快速 (1.5x)</option>
<option value="2">极速 (2x)</option>
</select>
</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"></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;
// 计算并显示字数(中文按字符计算,排除空格和换行)
const wordCount = chapter.content.replace(/[\s\n]/g, '').length;
document.getElementById('chapterWordCount').textContent = `字数:${wordCount}`;
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 });
// ========== TTS语音朗读功能 ==========
const ttsModal = document.getElementById('ttsModal');
const ttsBtn = document.getElementById('ttsBtn');
const ttsClose = document.getElementById('ttsClose');
// 检测浏览器是否支持TTS
function checkTTSSupport() {
if (!('speechSynthesis' in window)) {
alert('您的浏览器不支持语音朗读功能请使用Chrome、Edge或Safari浏览器。');
return false;
}
return true;
}
let ttsSynth = window.speechSynthesis;
let ttsUtterance = null;
let ttsSentences = [];
let ttsCurrentIndex = 0;
let ttsIsPlaying = false;
let ttsIsPaused = false;
let ttsSpeed = 1;
// 打开TTS面板
ttsBtn.addEventListener('click', () => {
if (!checkTTSSupport()) return;
initTTS();
ttsModal.classList.add('active');
});
// 关闭TTS面板
ttsClose.addEventListener('click', () => {
ttsModal.classList.remove('active');
});
// 点击遮罩关闭
ttsModal.addEventListener('click', (e) => {
if (e.target === ttsModal) {
ttsModal.classList.remove('active');
}
});
// 初始化TTS
function initTTS() {
const contentEl = document.getElementById('chapterContent');
if (!contentEl) return;
const text = contentEl.innerText || contentEl.textContent;
ttsSentences = text.match(/[^。!?\n]+[。!?\n]+|[^。!?\n]+$/g) || [text];
ttsSentences = ttsSentences.filter(s => s.trim().length > 0);
updateTTSProgress();
}
// 更新进度
function updateTTSProgress() {
const total = ttsSentences.length;
const current = ttsCurrentIndex;
document.getElementById('ttsTime').textContent = `${current} / ${total}`;
const percent = total > 0 ? (current / total * 100) : 0;
document.getElementById('ttsProgressFill').style.width = `${percent}%`;
}
// 播放当前句子
function playCurrentSentence() {
if (ttsCurrentIndex >= ttsSentences.length) {
stopTTS();
return;
}
const text = ttsSentences[ttsCurrentIndex].trim();
ttsUtterance = new SpeechSynthesisUtterance(text);
ttsUtterance.lang = 'zh-CN';
ttsUtterance.rate = ttsSpeed;
const voices = ttsSynth.getVoices();
const zhVoice = voices.find(v => v.lang.includes('zh') || v.lang.includes('CN'));
if (zhVoice) ttsUtterance.voice = zhVoice;
ttsUtterance.onend = () => {
if (ttsIsPlaying && !ttsIsPaused) {
ttsCurrentIndex++;
updateTTSProgress();
playCurrentSentence();
}
};
ttsUtterance.onerror = (e) => {
console.error('TTS error:', e);
if (ttsIsPlaying) {
ttsCurrentIndex++;
updateTTSProgress();
playCurrentSentence();
}
};
ttsSynth.speak(ttsUtterance);
}
// 播放
function playTTS() {
if (ttsSentences.length === 0) initTTS();
if (ttsIsPaused) {
ttsSynth.resume();
ttsIsPaused = false;
} else {
ttsIsPlaying = true;
playCurrentSentence();
}
document.getElementById('ttsPlay').classList.add('active');
document.getElementById('ttsPause').classList.remove('active');
ttsBtn.classList.add('active-tts');
}
// 暂停
function pauseTTS() {
if (ttsIsPlaying) {
ttsSynth.pause();
ttsIsPaused = true;
document.getElementById('ttsPlay').classList.remove('active');
document.getElementById('ttsPause').classList.add('active');
ttsBtn.classList.remove('active-tts');
}
}
// 停止
function stopTTS() {
ttsSynth.cancel();
ttsIsPlaying = false;
ttsIsPaused = false;
ttsCurrentIndex = 0;
updateTTSProgress();
document.getElementById('ttsPlay').classList.remove('active');
document.getElementById('ttsPause').classList.remove('active');
ttsBtn.classList.remove('active-tts');
}
// 设置速度
function setTTSSpeed(speed) {
ttsSpeed = parseFloat(speed);
if (ttsIsPlaying && !ttsIsPaused) {
ttsSynth.cancel();
playCurrentSentence();
}
}
// 绑定TTS事件
document.getElementById('ttsPlay').addEventListener('click', playTTS);
document.getElementById('ttsPause').addEventListener('click', pauseTTS);
document.getElementById('ttsStop').addEventListener('click', stopTTS);
document.getElementById('ttsSpeed').addEventListener('change', (e) => setTTSSpeed(e.target.value));
// 页面离开时停止
window.addEventListener('beforeunload', stopTTS);
// 初始化 - 等待app.js加载章节数据
document.addEventListener('DOMContentLoaded', async function() {
// 等待章节数据加载完成
await loadChaptersData();
// 渲染侧边栏
renderReaderSidebar();
// 渲染移动端目录
renderMobileTOC();
// 加载当前章节
loadChapterContent(currentChapter);
});
</script>
</body>
</html>