小说阅读器 ebook-reader

小说阅读器 ebook-reader

介绍

采用GPT设计并实现的一个小说阅读器工具,支持在线阅读TXT格式的电子书,提供舒适的阅读体验。

功能特点

  • 📚 支持TXT格式电子书导入
  • 📖 在线阅读,自动记忆阅读进度
  • 🎨 自定义阅读界面
    • 字体大小调节
    • 多种背景颜色主题
    • 行距调整
  • 📑 章节目录导航
  • ⌨️ 键盘快捷键支持
  • 💾 本地数据存储,无需登录

技术栈

  • 前端界面:HTML5 + Tailwind CSS
  • 本地存储:IndexedDB
  • 无后端依赖,纯前端实现

使用说明

  1. 打开书架页面 code/bookshelf.html
  2. 点击"添加图书"按钮上传TXT格式电子书
  3. 点击书籍封面进入阅读界面
  4. 在阅读界面可以:
    • 使用左右方向键或点击按钮翻页
    • 点击设置图标调整阅读界面
    • 点击目录图标快速跳转
    • 阅读进度自动保存

快捷键

  • 上一页
  • 下一页

本地开发

  1. 克隆仓库到本地
  2. 使用浏览器直接打开 code/bookshelf.html 即可运行
  3. 无需安装任何依赖

浏览器支持

  • 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%'; // 初始进度为 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);
  });
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值