纯阅读版本:
<?php
// index.php
session_start();
$uploadDir = 'books/';
$booksFile = $uploadDir . 'books.json';
// 初始化书籍存储目录
if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true);
if (!file_exists($booksFile)) file_put_contents($booksFile, '[]');
// 处理文件上传
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
// 验证文件
if (!isset($_FILES['book']) || $_FILES['book']['error'] !== UPLOAD_ERR_OK) {
die(json_encode(['status' => 'error', 'message' => '文件上传失败']));
}
// 生成安全目录名
$bookName = preg_replace('/[^\w\x{4e00}-\x{9fa5}]/u', '', pathinfo($_FILES['book']['name'], PATHINFO_FILENAME));
$safeDir = $bookName . '_' . bin2hex(random_bytes(4));
$targetDir = $uploadDir . $safeDir . '/';
// 创建目录
if (!mkdir($targetDir, 0755, true)) {
die(json_encode(['status' => 'error', 'message' => '目录创建失败']));
}
// 移动文件
move_uploaded_file($_FILES['book']['tmp_name'], $targetDir . 'source.txt');
// 处理章节分割
$content = mb_convert_encoding(file_get_contents($targetDir . 'source.txt'), 'UTF-8', 'auto');
$chapters = preg_split('/(^(?:第[零一二三四五六七八九十百千0-9]+章|第\d+章|卷\d+).*$)/mu', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
$chapterList = [];
for ($i = 1; $i < count($chapters); $i += 2) {
if (empty(trim($chapters[$i]))) continue;
$chapterTitle = trim(strip_tags($chapters[$i]));
$chapterContent = nl2br(htmlspecialchars(trim($chapters[$i+1] ?? '')));
// $chapterContent = str_replace('<br />','<textarea></textarea>',$chapterContent);
$fileName = "chapter_" . (count($chapterList)+1) . ".html";
file_put_contents($targetDir . $fileName,
"<div class='chapter-content'>
<h3 class='chapter-title'>{$chapterTitle}</h3>
<div class='content-text'>{$chapterContent}</div>
</div>");
$chapterList[] = [
'title' => $chapterTitle,
'file' => $fileName
];
}
// 保存章节信息
file_put_contents($targetDir . 'chapters.json', json_encode($chapterList, JSON_UNESCAPED_UNICODE));
// 更新书籍列表
$books = json_decode(file_get_contents($booksFile), true);
$books[] = [
'id' => $safeDir,
'title' => $bookName,
'upload_time' => date('Y-m-d H:i:s'),
'chapter_count' => count($chapterList)
];
file_put_contents($booksFile, json_encode($books, JSON_UNESCAPED_UNICODE));
die(json_encode(['status' => 'success', 'book' => $safeDir]));
}
// 获取当前书籍
$currentBook = isset($_GET['book']) ? basename($_GET['book']) : '';
if ($currentBook) $_SESSION['current_book'] = $currentBook;
$currentBook = $_SESSION['current_book'] ?? '';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>多书阅读器</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #2c3e50;
--accent-color: #3498db;
}
body {
background: #f8f9fa;
min-height: 100vh;
}
#sidebar {
background: white;
height: 100vh;
box-shadow: 2px 0 8px rgba(0,0,0,0.1);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#chapterList {
overflow: auto;
height: 60vh;
}
textarea {
resize: vertical;
display: block;
width: 100%;
height: 2em;
}
.book-item {
padding: 12px;
margin: 8px 0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #eee;
}
.book-item:hover {
transform: translateX(3px);
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.book-item.active {
border-left: 4px solid var(--accent-color);
background: #f8f9fa;
}
.chapter-item {
padding: 10px 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s;
}
.chapter-item:hover {
background: #f8f9fa;
}
.chapter-item.active {
color: var(--accent-color);
font-weight: 500;
}
#contentArea {
padding: 2rem;
line-height: 1.8;
font-size: 1.1rem;
background: white;
height: 100vh;
overflow: auto;
}
/* 移动端样式 */
#mobileMenuBtn {
display: none;
position: fixed;
top: 10px;
left: 10px;
z-index: 1000;
padding: 8px 12px;
border: none;
border-radius: 5px;
background: var(--accent-color);
color: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
transition: transform 0.2s;
}
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 998;
}
@media (max-width: 768px) {
#mobileMenuBtn {
display: block;
}
#sidebar {
position: fixed;
top: 0;
left: 0;
width: 280px;
height: 100vh;
transform: translateX(-100%);
z-index: 999;
}
.sidebar-mobile-show {
transform: translateX(0) !important;
}
#contentArea {
width: 100%;
padding: 60px 15px 15px;
}
}
/* 保持原有样式,新增以下内容 */
.note-section {
margin: 1rem 0;
border-left: 3px solid #3498db;
padding-left: 1rem;
}
.note-input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
min-height: 60px;
}
#copyNotesBtn {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
</style>
</head>
<body>
<button id="mobileMenuBtn">☰ 菜单</button>
<div class="sidebar-overlay"></div>
<div class="container-fluid">
<div class="row">
<!-- 左侧边栏 -->
<div class="col-lg-3 col-md-4" id="sidebar">
<div class="p-3">
<!-- 上传表单 -->
<form id="uploadForm" class="mb-4">
<input type="file" id="bookFile" name="book" accept=".txt" hidden>
<label for="bookFile" class="btn btn-primary w-100">
<i class="bi bi-upload me-2"></i>上传小说
</label>
<div class="progress mt-2" style="height: 4px; display: none;">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</form>
<!-- 书籍列表 -->
<h5 class="text-muted mb-3">我的书架</h5>
<div id="bookList">
<?php foreach(json_decode(file_get_contents($booksFile), true) as $book): ?>
<div class="book-item <?= $book['id'] === $currentBook ? 'active' : '' ?>"
data-id="<?= htmlspecialchars($book['id']) ?>">
<div class="fw-bold"><?= htmlspecialchars($book['title']) ?></div>
<small class="text-muted">
共<?= $book['chapter_count'] ?>章 ·
<?= date('m/d H:i', strtotime($book['upload_time'])) ?>
</small>
</div>
<?php endforeach; ?>
</div>
<!-- 章节目录 -->
<h5 class="text-muted mt-4 mb-3">章节列表</h5>
<div id="chapterList">
<?php if ($currentBook && file_exists($uploadDir . $currentBook . '/chapters.json')): ?>
<?php $chapters = json_decode(file_get_contents($uploadDir . $currentBook . '/chapters.json'), true); ?>
<?php foreach ($chapters as $chapter): ?>
<div class="chapter-item" data-file="<?= htmlspecialchars($chapter['file']) ?>">
<?= htmlspecialchars($chapter['title']) ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<!-- 阅读区域 -->
<div class="col-lg-9 col-md-8">
<div id="contentArea">
<?php if ($currentBook): ?>
<div class="text-center text-muted mt-5">请选择章节开始阅读</div>
<?php else: ?>
<div class="text-center mt-5">
<h3 class="text-muted">欢迎使用阅读器</h3>
<p class="text-secondary">请上传或选择书籍开始阅读</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function() {
// 移动端侧边栏控制
const $sidebar = $('#sidebar');
const $overlay = $('.sidebar-overlay');
const $menuBtn = $('#mobileMenuBtn');
function isMobile() {
return window.innerWidth <= 768;
}
function initMobileLayout() {
if(isMobile()) {
$menuBtn.show();
$overlay.hide();
$sidebar.removeClass('sidebar-mobile-show');
} else {
$menuBtn.hide();
$overlay.hide();
$sidebar.removeClass('sidebar-mobile-show');
}
}
$menuBtn.on('click', function(e) {
e.stopPropagation();
$sidebar.addClass('sidebar-mobile-show');
$overlay.fadeIn(200);
$('body').css('overflow', 'hidden');
});
$overlay.on('click', function() {
$sidebar.removeClass('sidebar-mobile-show');
$overlay.fadeOut(200);
$('body').css('overflow', 'auto');
});
$(document).on('click', '.chapter-item', function() {
if(isMobile()) {
$sidebar.removeClass('sidebar-mobile-show');
$overlay.fadeOut(200);
$('body').css('overflow', 'auto');
}
});
$(window).on('resize', initMobileLayout);
initMobileLayout();
// 文件上传处理
$('#bookFile').on('change', function() {
const formData = new FormData();
formData.append('book', this.files[0]);
$('.progress').show().find('.progress-bar').css('width', '0%');
$.ajax({
url: '',
type: 'POST',
data: formData,
contentType: false,
processData: false,
xhr: function() {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if(e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
$('.progress-bar').css('width', percent + '%');
}
}, false);
return xhr;
},
success: function(res) {
if(res.status === 'success') {
location.href = '?book=' + encodeURIComponent(res.book);
} else {
alert(res.message || '上传失败');
}
},
complete: function() {
$('.progress').hide();
}
});
});
// 书籍和章节控制
$(document).on('click', '.book-item', function() {
window.location.href = '?book=' + encodeURIComponent($(this).data('id'));
});
// 修改章节点击事件处理代码
$(document).on('click', '.chapter-item', function() {
$('.chapter-item').removeClass('active');
$(this).addClass('active');
const bookDir = 'books/' + getCurrentBookId();
const chapterFile = $(this).data('file');
$('#contentArea').load(bookDir + '/' + chapterFile, function() {
// 新增以下两行代码
$('#contentArea').scrollTop(0); // 重置内容区域滚动条
$('html, body').animate({ scrollTop: 0 }, 0); // 重置页面滚动
if(isMobile()) {
$('html, body').animate({
scrollTop: $('#contentArea').offset().top
}, 500);
}
});
});
function getCurrentBookId() {
return new URLSearchParams(location.search).get('book') || '';
}
});
</script>
</body>
</html>
增加笔记版本。
<?php
include('login.php');
function deleteDirectory($dirPath) {
if (!is_dir($dirPath)) return false;
$files = array_diff(scandir($dirPath), ['.', '..']);
foreach ($files as $file) {
$path = "$dirPath/$file";
is_dir($path) ? deleteDirectory($path) : unlink($path);
}
return rmdir($dirPath);
}
// index.php
session_start();
$uploadDir = 'books/';
$booksFile = $uploadDir . 'books.json';
// 初始化书籍存储目录
if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true);
if (!file_exists($booksFile)) file_put_contents($booksFile, '[]');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'deleteBook') {
header('Content-Type: application/json');
session_start();
// 验证书籍ID
$bookId = basename($_POST['bookId'] ?? '');
if (!$bookId) {
die(json_encode(['status' => 'error', 'message' => '无效书籍ID']));
}
// 加载书籍列表
$books = json_decode(file_get_contents($booksFile), true);
$index = array_search($bookId, array_column($books, 'id'));
if ($index === false) {
die(json_encode(['status' => 'error', 'message' => '书籍不存在']));
}
// 删除目录
$bookDir = $uploadDir . $bookId;
if (is_dir($bookDir)) {
if (!deleteDirectory($bookDir)) {
die(json_encode(['status' => 'error', 'message' => '目录删除失败']));
}
}
// 更新书籍列表
array_splice($books, $index, 1);
file_put_contents($booksFile, json_encode($books, JSON_UNESCAPED_UNICODE));
die(json_encode(['status' => 'success']));
}
// 处理文件上传
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
// 验证文件
if (!isset($_FILES['book']) || $_FILES['book']['error'] !== UPLOAD_ERR_OK) {
die(json_encode(['status' => 'error', 'message' => '文件上传失败']));
}
// 生成安全目录名
$bookName = preg_replace('/[^\w\x{4e00}-\x{9fa5}]/u', '', pathinfo($_FILES['book']['name'], PATHINFO_FILENAME));
$safeDir = $bookName . '_' . bin2hex(random_bytes(4));
$targetDir = $uploadDir . $safeDir . '/';
// 创建目录
if (!mkdir($targetDir, 0755, true)) {
die(json_encode(['status' => 'error', 'message' => '目录创建失败']));
}
// 移动文件
move_uploaded_file($_FILES['book']['tmp_name'], $targetDir . 'source.txt');
// 处理章节分割
$content = mb_convert_encoding(file_get_contents($targetDir . 'source.txt'), 'UTF-8', 'auto');
$chapters = preg_split('/(^(?:第[零一二三四五六七八九十百千0-9]+章|第\d+章|卷\d+).*$)/mu', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
$chapterList = [];
for ($i = 1; $i < count($chapters); $i += 2) {
if (empty(trim($chapters[$i]))) continue;
$chapterTitle = trim(strip_tags($chapters[$i]));
// 修改章节内容处理部分(约在index.php第45行附近)
$chapterContent = nl2br(htmlspecialchars(trim($chapters[$i+1] ?? '')));
$index = 0;
// 修改章节内容处理部分
$chapterContent = preg_replace_callback('/<br\s*\/?>/i', function($matches) use (&$index) {
$index++;
return '</div>
<div class="paragraph-wrapper" data-index="'.$index.'">'.
'<div class="paragraph-content"></div>'.
'<textarea class="note-textarea" data-index="'.$index.'" style="display:none"></textarea>';
}, $chapterContent);
// 在文件开头添加样式(保持原有样式基础上新增):
$fileName = "chapter_" . (count($chapterList)+1) . ".html";
file_put_contents($targetDir . $fileName,
"<div class='chapter-content'>
<h3 class='chapter-title'>{$chapterTitle}</h3>
<div class='content-text'>{$chapterContent}</div>
</div>");
$chapterList[] = [
'title' => $chapterTitle,
'file' => $fileName
];
}
// 保存章节信息
file_put_contents($targetDir . 'chapters.json', json_encode($chapterList, JSON_UNESCAPED_UNICODE));
// 更新书籍列表
$books = json_decode(file_get_contents($booksFile), true);
$books[] = [
'id' => $safeDir,
'title' => $bookName,
'upload_time' => date('Y-m-d H:i:s'),
'chapter_count' => count($chapterList)
];
file_put_contents($booksFile, json_encode($books, JSON_UNESCAPED_UNICODE));
die(json_encode(['status' => 'success', 'book' => $safeDir]));
}
// 获取当前书籍
$currentBook = isset($_GET['book']) ? basename($_GET['book']) : '';
if ($currentBook) $_SESSION['current_book'] = $currentBook;
$currentBook = $_SESSION['current_book'] ?? '';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>多书阅读器</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #2c3e50;
--accent-color: #3498db;
}
body {
background: #f8f9fa;
min-height: 100vh;
}
#sidebar {
background: white;
height: 100vh;
box-shadow: 2px 0 8px rgba(0,0,0,0.1);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#chapterList {
overflow: auto;
height: 60vh;
}
textarea {
resize: vertical;
display: block;
width: 100%;
/*height: 2em;*/
}
.book-item {
padding: 12px;
margin: 8px 0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #eee;
}
.book-item:hover {
transform: translateX(3px);
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.book-item.active {
border-left: 4px solid var(--accent-color);
background: #f8f9fa;
}
.chapter-item {
padding: 10px 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s;
}
.chapter-item:hover {
background: #f8f9fa;
}
.chapter-item.active {
color: var(--accent-color);
font-weight: 500;
}
#contentArea {
padding: 2rem;
line-height: 1.8;
font-size: 1.1rem;
background: white;
height: 100vh;
overflow: auto;
}
/* 移动端样式 */
#mobileMenuBtn {
display: none;
position: fixed;
top: 10px;
left: 10px;
z-index: 1000;
padding: 8px 12px;
border: none;
border-radius: 5px;
background: var(--accent-color);
color: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
transition: transform 0.2s;
}
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 998;
}
@media (max-width: 768px) {
#mobileMenuBtn {
display: block;
}
#sidebar {
position: fixed;
top: 0;
left: 0;
width: 280px;
height: 100vh;
transform: translateX(-100%);
z-index: 999;
}
.sidebar-mobile-show {
transform: translateX(0) !important;
}
#contentArea {
width: 100%;
padding: 60px 15px 15px;
}
}
/* 保持原有样式,新增以下内容 */
.note-section {
margin: 1rem 0;
border-left: 3px solid #3498db;
padding-left: 1rem;
}
.note-input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
min-height: 60px;
}
#copyNotesBtn {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* 保存提示 */
.save-notification {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 150, 0, 0.9);
color: white;
padding: 12px 24px;
border-radius: 25px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
z-index: 9999;
pointer-events: none;
}
/* 段落交互样式 */
.paragraph-wrapper {
position: relative;
margin: 1rem 0;
}
.paragraph-content {
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background 0.2s;
}
.paragraph-content:hover {
background: #f8f9fa;
}
.note-textarea.active {
display: block !important;
margin-top: 8px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
.save-notification.show {
opacity: 1;
transform: translateX(-50%) translateY(-10px);
}
/* 字数统计面板 */
.stats-panel {
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 1000;
min-width: 200px;
}
.stats-item {
margin: 8px 0;
font-size: 0.9em;
}
.stats-label {
color: #666;
margin-right: 8px;
}
.stats-value {
color: #333;
font-weight: 500;
}
/* 复制按钮样式 */
.copy-btn {
width: 100%;
padding: 8px 12px;
background: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.2s;
margin-top: 10px;
}
.copy-btn:hover {
background: #2980b9;
transform: translateY(-1px);
}
.copy-btn:active {
transform: translateY(0);
}
/* ❌ 缺少has-note样式 */
.paragraph-content.has-note {
border-left: 2px solid #3498db;
padding-left: 10px;
background: #f8f9fa;
}
/* 添加CSS样式 */
.note-textarea {
min-height: 40px;
max-height: 400px;
overflow-y: hidden;
transition: height 0.2s ease;
}
/* 折叠按钮样式 */
.toggle-button {
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
border-radius: 50%;
background: #3498db;
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
font-size: 12px;
transition: transform 0.3s;
z-index: 1001;
}
/* 折叠动画 */
.stats-panel {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.stats-panel.collapsed {
transform: translateX(100%);
opacity: 0;
visibility: hidden;
}
.delete-btn {
padding: 2px 8px;
font-size: 12px;
transition: opacity 0.2s;
}
.book-item:hover .delete-btn {
opacity: 1;
}
.book-item:not(:hover) .delete-btn {
opacity: 0.6;
}
</style>
</head>
<body>
<!-- 保存提示 -->
<div id="saveNotification" class="save-notification"></div>
<button id="toggleStats" class="toggle-button">▼</button>
<!-- 字数统计 -->
<div class="stats-panel">
<div class="stats-item">
<span class="stats-label">字数状态</span>
<span class="stats-value" id="notesCount">0</span>/<span class="stats-value" id="originalCount">0</span>
</div>
<!-- 原有统计项 -->
<div class="stats-item">
<button id="copyAllNotes" class="copy-btn">📋 复制全部笔记</button>
</div>
</div>
<button id="mobileMenuBtn">☰ 菜单</button>
<div class="sidebar-overlay"></div>
<div class="container-fluid">
<div class="row">
<!-- 左侧边栏 -->
<div class="col-lg-3 col-md-4" id="sidebar">
<div class="p-3">
<!-- 上传表单 -->
<form id="uploadForm" class="mb-4">
<input type="file" id="bookFile" name="book" accept=".txt" hidden>
<label for="bookFile" class="btn btn-primary w-100">
<i class="bi bi-upload me-2"></i>上传小说
</label>
<div class="progress mt-2" style="height: 4px; display: none;">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</form>
<!-- 书籍列表 -->
<h5 class="text-muted mb-3">我的书架</h5>
<div id="bookList">
<?php foreach(json_decode(file_get_contents($booksFile), true) as $book): ?>
<div class="book-item <?= $book['id'] === $currentBook ? 'active' : '' ?>" data-id="<?= htmlspecialchars($book['id']) ?>">
<div class="d-flex justify-content-between align-items-center">
<div class="fw-bold"><?= htmlspecialchars($book['title']) ?></div>
<button class="btn btn-danger btn-sm delete-btn">删除</button>
</div>
<small class="text-muted">
共<?= $book['chapter_count'] ?>章 ·
<?= date('m/d H:i', strtotime($book['upload_time'])) ?>
</small>
</div>
<?php endforeach; ?>
</div>
<!-- 章节目录 -->
<h5 class="text-muted mt-4 mb-3">章节列表</h5>
<div id="chapterList">
<?php if ($currentBook && file_exists($uploadDir . $currentBook . '/chapters.json')): ?>
<?php $chapters = json_decode(file_get_contents($uploadDir . $currentBook . '/chapters.json'), true); ?>
<?php foreach ($chapters as $chapter): ?>
<div class="chapter-item" data-file="<?= htmlspecialchars($chapter['file']) ?>">
<?= htmlspecialchars($chapter['title']) ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<!-- 阅读区域 -->
<div class="col-lg-9 col-md-8">
<div id="contentArea">
<?php if ($currentBook): ?>
<div class="text-center text-muted mt-5">请选择章节开始阅读</div>
<?php else: ?>
<div class="text-center mt-5">
<h3 class="text-muted">欢迎使用阅读器</h3>
<p class="text-secondary">请上传或选择书籍开始阅读</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
// 删除书籍功能
$(document).on('click', '.delete-btn', function(e) {
e.stopPropagation();
const $bookItem = $(this).closest('.book-item');
const bookId = $bookItem.data('id');
if (!confirm(`确定要永久删除《${$bookItem.find('.fw-bold').text()}》吗?此操作不可恢复!`)) return;
$.post({
url: '',
data: {
action: 'deleteBook',
bookId: bookId
},
dataType: 'json'
}).done(function(res) {
if (res.status === 'success') {
$bookItem.slideUp(300, () => $(this).remove());
// 如果当前正在阅读被删除的书籍
if ('<?= $currentBook ?>' === bookId) {
window.location.href = './';
}
} else {
alert('删除失败:' + (res.message || '未知错误'));
}
}).fail(function() {
alert('网络请求失败,请检查连接');
});
});
// 新增折叠功能
document.getElementById('toggleStats').addEventListener('click', function() {
const panel = document.querySelector('.stats-panel');
const isCollapsed = panel.classList.toggle('collapsed');
// 更新按钮图标
this.textContent = isCollapsed ? '▶' : '▼';
// 保持按钮始终可见
if (isCollapsed) {
this.style.transform = 'translateX(-100%)';
} else {
this.style.transform = 'translateX(0)';
}
});
// 初始化时保持展开状态
document.querySelector('.stats-panel').classList.remove('collapsed');
$(function() {
// 移动端侧边栏控制
const $sidebar = $('#sidebar');
const $overlay = $('.sidebar-overlay');
const $menuBtn = $('#mobileMenuBtn');
// 自动调整高度函数
function autoResizeTextarea() {
const $this = $(this);
const currentVal = $this.val();
const lastContent = $this.data('lastContent') || '';
// 检测是否新增了换行符(包括回车和自动换行)
const hasNewLine = currentVal.includes('\n') &&
!lastContent.includes('\n') &&
currentVal !== lastContent;
// 检测是否需要自动换行调整
const needsAutoResize = this.scrollHeight > $this.height();
// 仅在需要调整时执行
if (hasNewLine || needsAutoResize) {
clearTimeout($this.data('resizeTimer'));
$this.data('resizeTimer', setTimeout(() => {
// 重置高度后重新计算
$this.css('height', '0').height(this.scrollHeight);
$this.data('lastContent', currentVal); // 更新最后内容
}, 50));
}
}
// 初始化已有textarea
$('.note-textarea').each(function() {
$(this).data('lastContent', $(this).val())
.trigger('input'); // 初始化时触发调整
});
// 绑定输入事件
$(document).on('input', '.note-textarea', autoResizeTextarea);
function isMobile() {
return window.innerWidth <= 768;
}
function initMobileLayout() {
if(isMobile()) {
$menuBtn.show();
$overlay.hide();
$sidebar.removeClass('sidebar-mobile-show');
} else {
$menuBtn.hide();
$overlay.hide();
$sidebar.removeClass('sidebar-mobile-show');
}
}
$menuBtn.on('click', function(e) {
e.stopPropagation();
$sidebar.addClass('sidebar-mobile-show');
$overlay.fadeIn(200);
$('body').css('overflow', 'hidden');
});
$overlay.on('click', function() {
$sidebar.removeClass('sidebar-mobile-show');
$overlay.fadeOut(200);
$('body').css('overflow', 'auto');
});
$(document).on('click', '.chapter-item', function() {
if(isMobile()) {
$sidebar.removeClass('sidebar-mobile-show');
$overlay.fadeOut(200);
$('body').css('overflow', 'auto');
}
});
$(window).on('resize', initMobileLayout);
initMobileLayout();
// 文件上传处理
$('#bookFile').on('change', function() {
const formData = new FormData();
formData.append('book', this.files[0]);
$('.progress').show().find('.progress-bar').css('width', '0%');
$.ajax({
url: '',
type: 'POST',
data: formData,
contentType: false,
processData: false,
xhr: function() {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if(e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
$('.progress-bar').css('width', percent + '%');
}
}, false);
return xhr;
},
success: function(res) {
if(res.status === 'success') {
location.href = '?book=' + encodeURIComponent(res.book);
} else {
alert(res.message || '上传失败');
}
},
complete: function() {
$('.progress').hide();
}
});
});
// 书籍和章节控制
$(document).on('click', '.book-item', function() {
window.location.href = '?book=' + encodeURIComponent($(this).data('id'));
});
// 添加去抖动函数
function debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// 在复制功能中添加兼容性处理
function fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
return true;
} catch (err) {
return false;
} finally {
document.body.removeChild(textarea);
}
}
// 修改后的复制功能代码
$('#copyAllNotes').click(async function() {
try {
// 收集所有笔记
let notes = [];
$('.note-textarea').each(function(index) {
const content = $(this).val().trim();
if (content) {
notes.push(`\n${content}`);
}
});
if (notes.length === 0) {
showSaveNotification('暂无笔记可复制');
return;
}
// 格式化文本(必须先定义)
const textToCopy = notes;
// 执行复制操作
if (navigator.clipboard) {
await navigator.clipboard.writeText(textToCopy);
} else {
if (!fallbackCopy(textToCopy)) {
throw new Error('Clipboard API not available');
}
}
showSaveNotification(`已复制${notes.length}条笔记`);
// 视觉反馈
$(this).css({
'background': '#2ecc71',
'color': 'white'
}).text('✓ 复制成功');
setTimeout(() => {
$(this).css({
'background': '#3498db',
'color': 'white'
}).text('📋 复制全部笔记');
}, 2000);
} catch (err) {
console.error('复制失败:', err);
showSaveNotification('复制失败,请手动选择文本');
$(this).css('background', '#e74c3c').text('⚠ 复制失败');
setTimeout(() => {
$(this).css('background', '#3498db').text('📋 复制全部笔记');
}, 2000);
}
});
// 增强保存功能
function bindNoteEvents() {
$('#contentArea').off('input', '.note-textarea').on('input', '.note-textarea', debounce(function() {
const $textarea = $(this);
const bookId = '<?= $currentBook ?>';
const chapterFile = $('.chapter-item.active').data('file');
const paragraphIndex = $textarea.data('index');
const content = $textarea.val().trim();
// 实时更新字数统计
calculateWordCount();
// 自动显示/隐藏逻辑
$textarea.toggleClass('active', content.length > 0)
.closest('.paragraph-wrapper')
.find('.paragraph-content')
.toggleClass('has-note', content.length > 0);
// 发送保存请求
$.post('api.php', {
action: 'saveNote',
bookId: bookId,
chapter: chapterFile,
index: paragraphIndex,
content: content
}).done(function(response) {
if (response.status === 'success') {
showSaveNotification(`保存成功 (${content.length}字)`);
} else {
showSaveNotification('保存失败: ' + (response.message || '未知错误'));
}
}).fail(function() {
showSaveNotification('网络连接失败');
});
}, 1000));
}
// 修改后的loadNotes函数
function loadNotes() {
const bookId = '<?= $currentBook ?>';
const chapterFile = $('.chapter-item.active').data('file');
if (!bookId || !chapterFile) {
return $.Deferred().resolve().promise(); // 返回空Promise
}
// 返回jQuery的ajax对象
return $.post('api.php', {
action: 'loadNotes',
bookId: bookId,
chapter: chapterFile
}, function(response) {
if (response.status === 'success') {
Object.entries(response.notes).forEach(([index, content]) => {
const $textarea = $(`textarea[data-index="${index}"]`);
$textarea.val(content);
autoResizeTextarea.call($textarea[0]); // 新增
if (content.trim().length > 0) {
$textarea.addClass('active')
.closest('.paragraph-wrapper')
.find('.paragraph-content')
.addClass('has-note');
}
});
}
});
}
// 在全局作用域添加这些函数
function showSaveNotification(message) {
const $note = $('#saveNotification');
$note.text(message).addClass('show');
setTimeout(() => $note.removeClass('show'), 2000);
}
// 优化字数统计
function calculateWordCount() {
let notesCount = 0;
$('.note-textarea').each(function() {
notesCount += $(this).val().trim().length;
});
$('#notesCount').text(notesCount);
}
function calculateOriginalCount() {
const originalText = $('#contentArea').clone().find('.note-textarea').remove().end().text();
const count = originalText.replace(/\s+/g, '').length;
$('#originalCount').text(count);
}
// 新增DOM处理函数
function processParagraphs() {
$('.content-text').each(function() {
const $container = $(this);
let html = $container.html();
// 转换原始结构
html = html.replace(/<\/div><div class="paragraph-wrapper/g, '<div class="paragraph-wrapper')
.replace(/^<div class="paragraph-wrapper/, '</div><div class="paragraph-wrapper');
$container.html(html);
// 提取原始文本到段落容器
$container.find('.paragraph-wrapper').each(function() {
const $wrapper = $(this);
const $content = $wrapper.find('.paragraph-content');
const $textarea = $wrapper.find('textarea');
// 将原始文本移动到段落容器
const rawHtml = $wrapper.contents().not($content).not($textarea).text();
$content.html(rawHtml);
$wrapper.contents().not($content).not($textarea).remove();
});
});
// 绑定双击事件(移动到这里)
$('.paragraph-content').off('dblclick').on('dblclick', function() {
const $wrapper = $(this).closest('.paragraph-wrapper');
const $textarea = $wrapper.find('.note-textarea');
$textarea.addClass('active')
.focus()
.off('blur')
.on('blur', function() {
if ($(this).val().trim() === '') {
$(this).removeClass('active');
}
});
});
}
// 修改章节点击事件处理代码
$(document).on('click', '.chapter-item', function() {
$('.chapter-item').removeClass('active');
$(this).addClass('active');
const bookDir = 'books/' + getCurrentBookId();
const chapterFile = $(this).data('file');
$('#contentArea').load(bookDir + '/' + chapterFile, function() {
processParagraphs();
// 修改后的调用方式
loadNotes()
.always(function() {
bindNoteEvents();
calculateWordCount();
calculateOriginalCount();
})
.fail(function() {
showSaveNotification('笔记加载失败');
});
});
});
function getCurrentBookId() {
return new URLSearchParams(location.search).get('book') || '';
}
});
</script>
</body>
</html>
后端api:
<?php
// load_chapters.php
if (!empty($_SESSION['current_book'])) {
$bookDir = 'books/' . basename($_SESSION['current_book']);
$jsonFile = $bookDir . '/chapters.json';
if (file_exists($jsonFile)) {
$chapters = json_decode(file_get_contents($jsonFile), true);
foreach ($chapters as $chapter) {
echo '<div class="chapter-item list-group-item-action" data-file="'.htmlspecialchars($chapter['file']).'">';
echo htmlspecialchars($chapter['title']);
echo '</div>';
}
}
}
?>
实时保存api:
<?php
// api.php
session_start();
header('Content-Type: application/json');
$uploadDir = 'books/';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$action = $_POST['action'] ?? '';
$bookId = $_POST['bookId'] ?? '';
$chapter = basename($_POST['chapter'] ?? '');
$index = (int)($_POST['index'] ?? 0);
$content = $_POST['content'] ?? '';
// 验证参数
if (empty($bookId) || !preg_match('/^[\w\x{4e00}-\x{9fa5}-]+$/u', $bookId)) {
throw new Exception('无效的书籍ID');
}
$bookPath = $uploadDir . $bookId;
$notesPath = $bookPath . '/notes';
// 确保目录存在
if (!file_exists($notesPath)) {
mkdir($notesPath, 0755, true);
}
$noteFile = $notesPath . '/' . pathinfo($chapter, PATHINFO_FILENAME) . '.json';
if ($action === 'saveNote') {
// 读取现有笔记
$notes = file_exists($noteFile) ? json_decode(file_get_contents($noteFile), true) : [];
// 更新笔记内容
if ($index > 0) {
$notes[$index] = $content;
file_put_contents($noteFile, json_encode($notes, JSON_UNESCAPED_UNICODE));
}
echo json_encode(['status' => 'success']);
}
elseif ($action === 'loadNotes') {
// 读取笔记文件
$notes = file_exists($noteFile) ? json_decode(file_get_contents($noteFile), true) : [];
echo json_encode(['status' => 'success', 'notes' => $notes]);
}
else {
throw new Exception('无效的操作');
}
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
} else {
echo json_encode(['status' => 'error', 'message' => '无效的请求方法']);
}
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
暂无评论内容