小说阅读器 ebook-reader
小说阅读器 ebook-reader
介绍
采用GPT设计并实现的一个小说阅读器工具,支持在线阅读TXT格式的电子书,提供舒适的阅读体验。
功能特点
- 📚 支持TXT格式电子书导入
- 📖 在线阅读,自动记忆阅读进度
- 🎨 自定义阅读界面
- 字体大小调节
- 多种背景颜色主题
- 行距调整
- 📑 章节目录导航
- ⌨️ 键盘快捷键支持
- 💾 本地数据存储,无需登录
技术栈
- 前端界面:HTML5 + Tailwind CSS
- 本地存储:IndexedDB
- 无后端依赖,纯前端实现
使用说明
- 打开书架页面
code/bookshelf.html
- 点击"添加图书"按钮上传TXT格式电子书
- 点击书籍封面进入阅读界面
- 在阅读界面可以:
- 使用左右方向键或点击按钮翻页
- 点击设置图标调整阅读界面
- 点击目录图标快速跳转
- 阅读进度自动保存
快捷键
←
上一页→
下一页
本地开发
- 克隆仓库到本地
- 使用浏览器直接打开
code/bookshelf.html
即可运行 - 无需安装任何依赖
浏览器支持
- Chrome (推荐)
- Firefox
- Edge
- Safari
源码下载
小说阅读器 ebook-reader
附注
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%'; // 初始进度为 0const 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();});});// 保存设置到 localStoragefunction 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 `<buttonclass="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);});
}