小说阅读器 ebook-reader
介绍
采用GPT设计并实现的一个小说阅读器工具,支持在线阅读TXT格式的电子书,提供舒适的阅读体验。
功能特点
- 📚 支持TXT格式电子书导入
- 📖 在线阅读,自动记忆阅读进度
- 🎨 自定义阅读界面
- 字体大小调节
- 多种背景颜色主题
- 行距调整
- 📑 章节目录导航
- ⌨️ 键盘快捷键支持
- 💾 本地数据存储,无需登录
技术栈
- 前端界面:HTML5 + Tailwind CSS
- 本地存储:IndexedDB
- 无后端依赖,纯前端实现
使用说明
- 打开书架页面
code/bookshelf.html
- 点击"添加图书"按钮上传TXT格式电子书
- 点击书籍封面进入阅读界面
- 在阅读界面可以:
- 使用左右方向键或点击按钮翻页
- 点击设置图标调整阅读界面
- 点击目录图标快速跳转
- 阅读进度自动保存
快捷键
←
上一页→
下一页
本地开发
- 克隆仓库到本地
- 使用浏览器直接打开
code/bookshelf.html
即可运行 - 无需安装任何依赖
浏览器支持
- Chrome (推荐)
- Firefox
- Edge
- Safari
源码下载
附注
1.代码和界面由mastergo和claude3.5设计实现
2.可以通过HbuilderX的5+App封装为apk,安装在手机使用
演示截图
1.书架页面
2.添加图书
3.阅读页面
4.翻页操作
5.页码列表页码
6.设置页面
核心源码
code/bookshelf.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的书架</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4A90E2',
secondary: '#F5F5F5'
}
}
}
}
</script>
<script src="./js/db.js"></script>
</head>
<body class="bg-gray-50">
<header class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
<h1 class="text-xl font-medium">我的书架</h1>
<button class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors flex items-center space-x-2" onclick="document.getElementById('uploadModal').classList.remove('hidden')">
<i class="fas fa-plus"></i>
<span>添加图书</span>
</button>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 py-6">
<!-- 图书列表 -->
<div id="bookList" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<!-- 图书卡片将通过 JavaScript 动态生成 -->
</div>
</main>
<!-- 上传图书弹窗 -->
<div id="uploadModal" class="fixed inset-0 bg-black/50 hidden">
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl p-6 w-[30rem] max-w-[90%]">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-medium">添加图书</h3>
<button class="text-gray-400 hover:text-gray-600" onclick="document.getElementById('uploadModal').classList.add('hidden')">
<i class="fas fa-times"></i>
</button>
</div>
<label id="dropZone" class="block w-full p-8 border-2 border-dashed border-gray-300 rounded-lg text-center cursor-pointer hover:border-primary hover:text-primary transition-colors mb-6">
<input type="file" accept=".txt,.epub" class="hidden" id="fileInput" />
<i class="fas fa-cloud-upload-alt text-4xl mb-4"></i>
<p class="text-lg">点击或拖拽文件到此处</p>
<p class="text-sm text-gray-500 mt-2">支持 TXT、EPUB 格式</p>
</label>
<div class="flex justify-end space-x-4">
<button class="px-4 py-2 text-gray-600 hover:text-gray-900" onclick="closeUploadModal()">取消</button>
<button class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90" onclick="handleUpload()">确认导入</button>
</div>
</div>
</div>
<!-- 导入进度弹窗 -->
<div id="progressModal" class="fixed inset-0 bg-black/50 hidden">
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl p-6 w-80">
<h3 class="text-lg font-medium mb-4">导入图书中</h3>
<div class="mb-4">
<div class="h-2 bg-gray-200 rounded-full">
<div class="h-full w-2/3 bg-primary rounded-full"></div>
</div>
<div class="text-sm text-gray-500 mt-2">正在导入:时间移民.txt</div>
</div>
<button class="w-full px-4 py-2 bg-primary text-white rounded-md">取消导入</button>
</div>
</div>
<script>
// 页面加载完成后加载图书列表
document.addEventListener('DOMContentLoaded', loadBooks);
// 加载图书列表
async function loadBooks() {
try {
const books = await getAllBooks();
const bookList = document.getElementById('bookList');
bookList.innerHTML = books.map(book => createBookCard(book)).join('');
} catch (error) {
console.error('加载图书列表失败:', error);
}
}
// 创建图书卡片
function createBookCard(book) {
return `
<a href="index.html?id=${book.id}" class="block bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div class="aspect-[3/4] bg-gray-100 rounded-t-lg flex items-center justify-center overflow-hidden">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='160' viewBox='0 0 120 160'%3E%3Crect width='120' height='160' fill='%23f3f4f6'/%3E%3Cpath d='M30 40h60v10H30zM30 60h60v5H30zM30 70h40v5H30z' fill='%23d1d5db'/%3E%3C/svg%3E" alt="书籍封面" class="w-full h-full object-cover">
</div>
<div class="p-4">
<h3 class="font-medium text-gray-900 truncate">${book.title}</h3>
<p class="text-sm text-gray-500 mt-1">${book.author || '未知作者'}</p>
<div class="flex items-center text-xs text-gray-400 mt-2">
<i class="fas fa-book-open mr-1"></i>
<span>${book.progress ? `已读 ${Math.floor(book.progress * 100)}%` : '未读'}</span>
</div>
</div>
</a>
`;
}
// 文件拖拽处理
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-primary', 'text-primary');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-primary', 'text-primary');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-primary', 'text-primary');
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
}
});
// 处理文件上传
async function handleUpload() {
const file = fileInput.files[0];
if (!file) return;
const progressModal = document.getElementById('progressModal');
const progressBar = progressModal.querySelector('.bg-primary');
const progressText = progressModal.querySelector('.text-sm');
try {
// 显示进度弹窗
progressModal.classList.remove('hidden');
progressText.textContent = `正在导入:${file.name}`;
progressBar.style.width = '0%'; // 初始进度为 0
const content = await readFileWithProgress(file, (progress) => {
progressBar.style.width = `${progress}%`;
});
const book = {
title: file.name.replace(/\.[^/.]+$/, ''),
content: content,
uploadTime: new Date().toISOString(),
progress: 0,
currentPage: 1,
totalPages: Math.ceil(content.length / 1000)
};
await addBook(book);
await loadBooks();
closeUploadModal();
progressModal.classList.add('hidden');
} catch (error) {
console.error('导入图书失败:', error);
// 添加错误提示
progressText.textContent = '导入失败,请重试';
progressBar.style.backgroundColor = '#EF4444'; // 设置为红色表示失败
}
}
// 读取文件内容(带进度)
function readFileWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
// 添加进度监听
reader.onprogress = (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
onProgress(progress);
}
};
reader.onload = () => {
onProgress(100); // 确保最终显示 100%
resolve(reader.result);
};
reader.onerror = () => {
reject(reader.error);
};
reader.readAsText(file, 'GBK');
});
}
// 关闭上传弹窗
function closeUploadModal() {
document.getElementById('uploadModal').classList.add('hidden');
fileInput.value = '';
}
</script>
</body>
</html>
code/index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>阅读器</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4A90E2',
secondary: '#F5F5F5'
},
borderRadius: {
'none': '0px',
'sm': '2px',
DEFAULT: '4px',
'md': '8px',
'lg': '12px',
'xl': '16px',
'2xl': '20px',
'3xl': '24px',
'full': '9999px',
'button': '4px'
}
}
}
}
</script>
<style>
body {
background-color: #F5F5F5;
transition: background-color 0.3s;
}
.reading-content {
font-size: 18px;
line-height: 1.8;
color: #333333;
overflow: auto;
transition: font-size 0.3s, line-height 0.3s, color 0.3s;
}
.reading-content::-webkit-scrollbar {
width: 8px;
}
.reading-content::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
.reading-content::-webkit-scrollbar-thumb {
background: #6366f1;
border-radius: 4px;
}
.reading-content::-webkit-scrollbar-thumb:hover {
background: #4f46e5;
}
input[type="range"] {
--webkit-appearance: none;
width: 100%;
height: 4px;
background: #E0E0E0;
border-radius: 2px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
--webkit-appearance: none;
width: 16px;
height: 16px;
background: #4A90E2;
border-radius: 50%;
cursor: pointer;
}
/* 添加行数限制样式 */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 添加滚动条样式 */
.page-list-container::-webkit-scrollbar {
width: 8px;
}
.page-list-container::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
.page-list-container::-webkit-scrollbar-thumb {
background: #6366f1;
border-radius: 4px;
}
.page-list-container::-webkit-scrollbar-thumb:hover {
background: #4f46e5;
}
</style>
<script src="./js/db.js"></script>
</head>
<body class="flex flex-col h-screen">
<header class="bg-white/90 backdrop-blur-sm fixed w-full top-0 z-50 border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
<div class="flex items-center space-x-4">
<!-- 在 header 中添加返回按钮的链接 -->
<button class="text-gray-600 hover:text-primary !rounded-button" onclick="window.location.href='bookshelf.html'">
<i class="fas fa-arrow-left text-xl"></i>
</button>
<h1 class="text-xl font-medium">三体:黑暗森林</h1>
</div>
<div class="flex items-center space-x-4">
<button class="text-gray-600 hover:text-primary !rounded-button">
<i class="fas fa-bookmark text-xl"></i>
</button>
<button class="text-gray-600 hover:text-primary !rounded-button">
<i class="fas fa-cog text-xl"></i>
</button>
</div>
</div>
</header>
<main class="flex-1 mt-16 mb-16 max-w-3xl mx-auto px-6 py-8 reading-content" id="content">
<!-- 内容将通过 JavaScript 动态加载 -->
<div id="bookContent"></div>
<!-- 翻页按钮放在内容区域内部的底部 -->
<div class="mt-8 flex justify-center space-x-4">
<button id="prevPage" class="px-6 py-2 bg-white shadow-md rounded-full text-gray-600 hover:text-primary hover:border-primary transition-colors flex items-center space-x-2">
<i class="fas fa-chevron-left"></i>
<span>上一页</span>
</button>
<button id="nextPage" class="px-6 py-2 bg-white shadow-md rounded-full text-gray-600 hover:text-primary hover:border-primary transition-colors flex items-center space-x-2">
<span>下一页</span>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</main>
<div class="fixed bottom-0 w-full bg-white/90 backdrop-blur-sm border-t border-gray-200">
<div class="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
<div class="flex items-center space-x-4">
<button class="text-gray-600 hover:text-primary !rounded-button">
<i class="fas fa-list text-xl"></i>
</button>
<button class="text-gray-600 hover:text-primary !rounded-button">
<i class="fas fa-font text-xl"></i>
</button>
</div>
<div class="flex-1 mx-4">
<div class="h-1 bg-gray-200 rounded-full cursor-pointer">
<div id="progressBar" class="h-full bg-primary rounded-full"></div>
</div>
<div class="flex justify-between text-sm text-gray-500 mt-1">
<span>已读 <span id="progressText">0</span>%</span>
<span>第 <span id="currentPage">0</span>/<span id="totalPages">0</span> 页</span>
</div>
</div>
<div class="flex items-center space-x-4">
<button class="text-gray-600 hover:text-primary !rounded-button">
<i class="fas fa-moon text-xl"></i>
</button>
<button class="text-gray-600 hover:text-primary !rounded-button">
<i class="fas fa-share-alt text-xl"></i>
</button>
</div>
</div>
</div>
<div class="fixed inset-0 bg-black/50 hidden" id="settingsPanel">
<div class="absolute bottom-0 w-full bg-white rounded-t-xl p-6">
<div class="mb-6">
<h3 class="text-lg font-medium mb-4">字体大小</h3>
<input type="range" min="1" max="6" value="3" class="w-full">
</div>
<div class="mb-6">
<h3 class="text-lg font-medium mb-4">背景颜色</h3>
<div class="flex space-x-4">
<button class="w-10 h-10 rounded-full bg-white border border-gray-200 !rounded-button setting-color"></button>
<button class="w-10 h-10 rounded-full bg-[#F8F3E9] border border-gray-200 !rounded-button setting-color"></button>
<button class="w-10 h-10 rounded-full bg-[#E6F3FF] border border-gray-200 !rounded-button setting-color"></button>
<button class="w-10 h-10 rounded-full bg-[#222222] border border-gray-200 !rounded-button setting-color"></button>
</div>
</div>
<div class="mb-6">
<h3 class="text-lg font-medium mb-4">行距</h3>
<div class="flex space-x-4">
<button class="px-4 py-2 border border-gray-200 rounded-md text-sm !rounded-button">紧凑</button>
<button class="px-4 py-2 border border-gray-200 rounded-md text-sm !rounded-button">标准</button>
<button class="px-4 py-2 border border-gray-200 rounded-md text-sm !rounded-button">宽松</button>
</div>
</div>
</div>
</div>
<div class="fixed inset-0 bg-black/50 hidden" id="pageListPanel" style="z-index: 999">
<div class="absolute right-0 top-0 bottom-0 w-80 bg-white p-6 overflow-y-auto page-list-container">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-medium">目录</h3>
<button class="text-gray-400 hover:text-gray-600" onclick="document.getElementById('pageListPanel').classList.add('hidden')">
<i class="fas fa-times"></i>
</button>
</div>
<div id="pageList" class="space-y-2">
<!-- 页码列表将通过 JavaScript 动态生成 -->
</div>
</div>
</div>
</body>
</html>
<script>
// 页面加载完成后加载图书内容
document.addEventListener('DOMContentLoaded', loadBook);
let currentBook = null;
let bookPages = []; // 存储所有页面内容
async function loadBook() {
try {
const urlParams = new URLSearchParams(window.location.search);
const bookId = parseInt(urlParams.get('id'));
if (!bookId) return;
currentBook = await getBook(bookId);
if (!currentBook) return;
// 更新标题
document.querySelector('h1').textContent = currentBook.title;
// 计算分页
const paragraphs = currentBook.content.split('\n').filter(p => p.trim());
let currentPage = [];
let currentLength = 0;
for (const paragraph of paragraphs) {
if (currentLength + paragraph.length > 1000 && currentPage.length > 0) {
bookPages.push(currentPage);
currentPage = [];
currentLength = 0;
}
currentPage.push(paragraph);
currentLength += paragraph.length;
}
if (currentPage.length > 0) {
bookPages.push(currentPage);
}
// 显示当前页内容
showPage(currentBook.currentPage || 1);
} catch (error) {
console.error('加载图书失败:', error);
}
}
function showPage(pageNum) {
if (!currentBook || !bookPages.length) return;
const totalPages = bookPages.length;
pageNum = Math.min(Math.max(1, pageNum), totalPages);
// 更新内容到专门的内容容器
const bookContent = document.getElementById('bookContent');
bookContent.innerHTML = bookPages[pageNum - 1]
.map(para => `<p class="mb-6">${para.trim()}</p>`)
.join('');
// 滚动到顶部
document.getElementById('content').scrollTop = 0;
// 更新进度条和进度文本
const progress = Math.floor((pageNum / totalPages) * 100);
document.getElementById('progressBar').style.width = `${progress}%`;
document.getElementById('progressText').textContent = progress + "";
document.getElementById('currentPage').textContent = pageNum;
document.getElementById('totalPages').textContent = totalPages + "";
// 保存当前页码
currentBook.currentPage = pageNum;
updateBookProgress(currentBook.id, pageNum, totalPages);
}
// 添加翻页按钮事件监听
document.getElementById('prevPage').addEventListener('click', () => {
if (currentBook?.currentPage > 1) {
showPage((currentBook.currentPage || 1) - 1);
}
});
document.getElementById('nextPage').addEventListener('click', () => {
if (currentBook && bookPages.length) {
if (currentBook.currentPage < bookPages.length) {
showPage((currentBook.currentPage || 1) + 1);
}
}
});
// 添加键盘事件支持
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
showPage((currentBook?.currentPage || 1) - 1);
} else if (e.key === 'ArrowRight') {
showPage((currentBook?.currentPage || 1) + 1);
}
});
// 设置面板相关
const settingsPanel = document.getElementById('settingsPanel');
const fontSizeSlider = settingsPanel.querySelector('input[type="range"]');
const bgColorButtons = settingsPanel.querySelectorAll('.setting-color');
const lineHeightButtons = settingsPanel.querySelectorAll('.flex.space-x-4 button');
// 打开设置面板
document.querySelector('.fa-font').parentElement.addEventListener('click', () => {
settingsPanel.classList.remove('hidden');
});
// 打开设置面板
document.querySelector('.fa-cog').parentElement.addEventListener('click', () => {
settingsPanel.classList.remove('hidden');
});
// 关闭设置面板(点击遮罩层)
settingsPanel.addEventListener('click', (e) => {
if (e.target === settingsPanel) {
settingsPanel.classList.add('hidden');
}
});
// 字体大小调整
fontSizeSlider.addEventListener('input', (e) => {
const sizes = ['14px', '16px', '18px', '20px', '22px', '24px'];
const content = document.querySelector('.reading-content');
content.style.fontSize = sizes[e.target.value - 1];
saveSettings();
});
// 背景颜色切换
const bgColors = {
'bg-white': { bg: '#FFFFFF', text: '#333333' },
'bg-[#F8F3E9]': { bg: '#F8F3E9', text: '#333333' },
'bg-[#E6F3FF]': { bg: '#E6F3FF', text: '#333333' },
'bg-[#222222]': { bg: '#222222', text: '#CCCCCC' }
};
bgColorButtons.forEach(button => {
button.addEventListener('click', () => {
const content = document.querySelector('.reading-content');
const body = document.body;
// 获取按钮的背景色类名
const bgClass = Array.from(button.classList).find(cls => cls.startsWith('bg-'));
const colors = bgColors[bgClass];
body.style.backgroundColor = colors.bg;
content.style.color = colors.text;
// 移除其他按钮的选中状态
bgColorButtons.forEach(btn => btn.classList.remove('ring-2', 'ring-primary'));
// 添加当前按钮的选中状态
button.classList.add('ring-2', 'ring-primary');
saveSettings();
});
});
// 行距调整
const lineHeights = {
'紧凑': '1.5',
'标准': '1.8',
'宽松': '2.2'
};
lineHeightButtons.forEach(button => {
button.addEventListener('click', () => {
const content = document.querySelector('.reading-content');
content.style.lineHeight = lineHeights[button.textContent];
// 移除其他按钮的选中状态
lineHeightButtons.forEach(btn => btn.classList.remove('border-primary', 'text-primary'));
// 添加当前按钮的选中状态
button.classList.add('border-primary', 'text-primary');
saveSettings();
});
});
// 保存设置到 localStorage
function saveSettings() {
const content = document.querySelector('.reading-content');
const settings = {
fontSize: content.style.fontSize,
bgColor: document.body.style.backgroundColor,
textColor: content.style.color,
lineHeight: content.style.lineHeight
};
localStorage.setItem('readerSettings', JSON.stringify(settings));
}
// 从 localStorage 加载设置
function loadSettings() {
const settings = JSON.parse(localStorage.getItem('readerSettings')) || {
fontSize: '18px',
bgColor: '#FFFFFF',
textColor: '#333333',
lineHeight: '1.8'
};
// 应用字体大小
const content = document.querySelector('.reading-content');
content.style.fontSize = settings.fontSize;
const sizeIndex = ['14px', '16px', '18px', '20px', '22px', '24px'].indexOf(settings.fontSize);
if (sizeIndex !== -1) {
fontSizeSlider.value = sizeIndex + 1;
}
// 应用背景颜色
document.body.style.backgroundColor = settings.bgColor;
content.style.color = settings.textColor;
bgColorButtons.forEach(button => {
const bgClass = Array.from(button.classList).find(cls => cls.startsWith('bg-'));
const colors = bgColors[bgClass];
if (colors.bg === settings.bgColor) {
button.classList.add('ring-2', 'ring-primary');
}
});
// 应用行高
content.style.lineHeight = settings.lineHeight;
lineHeightButtons.forEach(button => {
if (lineHeights[button.textContent] === settings.lineHeight) {
button.classList.add('border-primary', 'text-primary');
}
});
}
// 在页面加载完成后加载设置
document.addEventListener('DOMContentLoaded', loadSettings);
// 添加页码列表相关功能
const pageListPanel = document.getElementById('pageListPanel');
const pageList = document.getElementById('pageList');
// 打开页码列表面板
document.querySelector('.fa-list').parentElement.addEventListener('click', () => {
pageListPanel.classList.remove('hidden');
updatePageList();
// 添加延时以确保列表已经渲染完成
setTimeout(() => {
const currentPageElement = pageList.querySelector('.bg-primary\\/10');
if (currentPageElement) {
currentPageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
});
// 更新页码列表
function updatePageList() {
if (!currentBook || !bookPages.length) return;
const currentPageNum = currentBook.currentPage || 1;
pageList.innerHTML = bookPages.map((page, index) => {
const pageNum = index + 1;
const isCurrentPage = pageNum === currentPageNum;
const preview = page[0].substring(0, 50).trim() + '...';
return `
<button
class="w-full text-left px-4 py-2 rounded-md transition-colors ${
isCurrentPage ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100'
}"
onclick="handlePageClick(${pageNum})"
>
<div class="flex items-center justify-between">
<span class="text-sm font-medium">第 ${pageNum} 页</span>
${isCurrentPage ? '<i class="fas fa-bookmark text-primary"></i>' : ''}
</div>
<p class="text-xs text-gray-500 mt-1 line-clamp-2">${preview}</p>
</button>
`;
}).join('');
}
// 处理页码点击
function handlePageClick(pageNum) {
showPage(pageNum);
pageListPanel.classList.add('hidden');
}
// 添加点击遮罩层关闭列表的功能
pageListPanel.addEventListener('click', (e) => {
if (e.target === pageListPanel) {
pageListPanel.classList.add('hidden');
}
});
</script>
code/js/db.js
// IndexDB 数据库操作
const DB_NAME = 'ebook_reader';
const DB_VERSION = 1;
const STORE_NAME = 'books';
// 初始化数据库
async function initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
store.createIndex('title', 'title', { unique: false });
store.createIndex('author', 'author', { unique: false });
}
};
});
}
// 添加图书
async function addBook(book) {
const db = await initDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.add(book);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 获取所有图书
async function getAllBooks() {
const db = await initDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 获取单本图书
async function getBook(id) {
const db = await initDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 更新图书进度
async function updateBookProgress(id, currentPage, totalPages) {
const db = await initDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(id);
request.onsuccess = () => {
const book = request.result;
if (book) {
book.currentPage = currentPage;
book.progress = currentPage / totalPages;
store.put(book);
resolve(book);
}
};
request.onerror = () => reject(request.error);
});
}