纯阅读版本:
<?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
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">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #6b8e6b;
--accent-color: #8db596;
--background-color: #f8fbf8;
--sidebar-bg: #ffffff;
--text-primary: #2c3e2c;
--text-secondary: #5a735a;
--text-muted: #7a8c7a;
--border-color: #e1eae1;
--hover-color: #f0f7f0;
--success-color: #7bb17b;
/* 新增护眼模式变量 */
--reading-bg: #f8fbf8;
--reading-text: #2c3e2c;
--reading-font-size: 1.1rem;
--reading-line-height: 1.8;
--reading-font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
}
/* 夜间模式 */
.night-mode {
--primary-color: #8db596;
--accent-color: #a8c6a8;
--background-color: #1a2f1a;
--sidebar-bg: #243824;
--text-primary: #e8f5e8;
--text-secondary: #c8dcc8;
--text-muted: #9ab39a;
--border-color: #2d4a2d;
--hover-color: #2d4a2d;
--success-color: #8db596;
--reading-bg: #1a2f1a;
--reading-text: #e8f5e8;
}
/* 纸张模式 */
.paper-mode {
--reading-bg: #fefcf0;
--reading-text: #5c4b37;
--background-color: #fefcf0;
}
/* 羊皮纸模式 */
.parchment-mode {
--reading-bg: #f5f0e6;
--reading-text: #4a3c2a;
--background-color: #f5f0e6;
}
/* 深色护眼模式 */
.dark-eye-mode {
--reading-bg: #1e3a1e;
--reading-text: #c8e6c8;
--background-color: #1e3a1e;
}
* {
box-sizing: border-box;
}
body {
background: var(--background-color);
min-height: 100vh;
font-family: var(--reading-font-family);
color: var(--text-primary);
line-height: 1.6;
transition: all 0.3s ease;
}
#sidebar {
background: var(--sidebar-bg);
height: 100vh;
box-shadow: 2px 0 12px rgba(107, 142, 107, 0.08);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-right: 1px solid var(--border-color);
}
#chapterList {
overflow: auto;
height: 60vh;
}
textarea {
resize: vertical;
display: block;
width: 100%;
font-family: var(--reading-font-family);
}
.btn-primary {
background: var(--primary-color);
border: none;
border-radius: 8px;
padding: 10px 16px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: #5a7a5a;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(107, 142, 107, 0.2);
}
.book-item {
padding: 14px;
margin: 10px 0;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
background: var(--sidebar-bg);
position: relative;
overflow: hidden;
}
.book-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--accent-color);
opacity: 0;
transition: opacity 0.3s;
}
.book-item:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(107, 142, 107, 0.1);
border-color: var(--accent-color);
}
.book-item:hover::before {
opacity: 1;
}
.book-item.active {
border-left: 4px solid var(--accent-color);
background: var(--hover-color);
}
.chapter-item {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.2s;
border-radius: 6px;
margin: 4px 0;
}
.chapter-item:hover {
background: var(--hover-color);
transform: translateX(4px);
}
.chapter-item.active {
color: var(--primary-color);
font-weight: 600;
background: var(--hover-color);
border-left: 3px solid var(--accent-color);
}
#contentArea {
padding: 2.5rem;
line-height: var(--reading-line-height);
font-size: var(--reading-font-size);
background: var(--reading-bg);
color: var(--reading-text);
height: 100vh;
overflow: auto;
border-radius: 12px;
margin: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
}
.chapter-title {
color: var(--primary-color);
font-weight: 600;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--accent-color);
text-align: center;
font-size: 1.5rem;
}
/* 移动端样式 */
#mobileMenuBtn {
display: none;
position: fixed;
top: 15px;
left: 15px;
z-index: 1000;
padding: 10px 14px;
border: none;
border-radius: 8px;
background: var(--primary-color);
color: white;
box-shadow: 0 4px 12px rgba(107, 142, 107, 0.3);
transition: all 0.3s;
font-weight: 500;
}
#mobileMenuBtn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(107, 142, 107, 0.4);
}
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 998;
backdrop-filter: blur(2px);
}
@media (max-width: 768px) {
#mobileMenuBtn {
display: block;
}
#sidebar {
position: fixed;
top: 0;
left: 0;
width: 300px;
height: 100vh;
transform: translateX(-100%);
z-index: 999;
border-radius: 0 12px 12px 0;
}
.sidebar-mobile-show {
transform: translateX(0) !important;
}
#contentArea {
width: 100%;
padding: 70px 20px 20px;
margin: 0;
border-radius: 0;
}
}
/* 笔记相关样式 */
.note-section {
margin: 1.5rem 0;
border-left: 4px solid var(--accent-color);
padding-left: 1.2rem;
background: color-mix(in srgb, var(--reading-bg) 95%, var(--accent-color));
border-radius: 0 8px 8px 0;
padding: 1rem 1.2rem;
}
.note-input {
width: 100%;
padding: 0.8rem;
border: 1px solid var(--border-color);
border-radius: 8px;
resize: vertical;
min-height: 80px;
background: color-mix(in srgb, var(--reading-bg) 98%, var(--accent-color));
transition: all 0.3s;
}
.note-input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(141, 181, 150, 0.1);
outline: none;
}
#copyNotesBtn {
position: fixed;
bottom: 25px;
right: 25px;
z-index: 1000;
box-shadow: 0 4px 16px rgba(107, 142, 107, 0.3);
border-radius: 50px;
padding: 12px 20px;
background: var(--primary-color);
border: none;
color: white;
font-weight: 500;
transition: all 0.3s;
}
#copyNotesBtn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(107, 142, 107, 0.4);
background: #5a7a5a;
}
/* 保存提示 */
.save-notification {
position: fixed;
bottom: 25px;
left: 50%;
transform: translateX(-50%);
background: var(--success-color);
color: white;
padding: 14px 28px;
border-radius: 30px;
box-shadow: 0 6px 20px rgba(123, 177, 123, 0.4);
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
z-index: 9999;
pointer-events: none;
font-weight: 500;
}
/* 段落交互样式 */
.paragraph-wrapper {
position: relative;
margin: 1.2rem 0;
padding: 8px;
border-radius: 6px;
transition: background 0.2s;
}
.paragraph-content {
cursor: pointer;
padding: 10px;
border-radius: 6px;
transition: all 0.2s;
line-height: var(--reading-line-height);
}
.paragraph-content:hover {
background: var(--hover-color);
}
.note-textarea.active {
display: block !important;
margin-top: 10px;
animation: fadeIn 0.3s ease;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px;
background: color-mix(in srgb, var(--reading-bg) 98%, var(--accent-color));
min-height: 80px;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.save-notification.show {
opacity: 1;
transform: translateX(-50%) translateY(-10px);
}
/* 字数统计面板 */
.stats-panel {
position: fixed;
top: 25px;
right: 25px;
background: var(--sidebar-bg);
padding: 20px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
z-index: 1000;
min-width: 220px;
border: 1px solid var(--border-color);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.stats-item {
margin: 10px 0;
font-size: 0.95em;
}
.stats-label {
color: var(--text-muted);
margin-right: 10px;
}
.stats-value {
color: var(--primary-color);
font-weight: 600;
}
/* 复制按钮样式 */
.copy-btn {
width: 100%;
padding: 10px 14px;
background: var(--accent-color);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
margin-top: 12px;
font-weight: 500;
}
.copy-btn:hover {
background: #7da585;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(141, 181, 150, 0.3);
}
.copy-btn:active {
transform: translateY(0);
}
.paragraph-content.has-note {
border-left: 3px solid var(--accent-color);
padding-left: 12px;
background: color-mix(in srgb, var(--reading-bg) 95%, var(--accent-color));
}
/* 笔记文本框样式 */
.note-textarea {
min-height: 50px;
overflow-y: hidden;
transition: height 0.2s ease;
font-family: var(--reading-font-family);
line-height: 1.5;
}
/* 折叠按钮样式 */
.toggle-button {
position: absolute;
top: 15px;
right: 15px;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--accent-color);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
font-size: 12px;
transition: transform 0.3s, background 0.3s;
z-index: 1001;
}
.toggle-button:hover {
background: #7da585;
transform: scale(1.1);
}
/* 折叠动画 */
.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: 4px 10px;
font-size: 12px;
transition: all 0.3s;
border-radius: 6px;
background: color-mix(in srgb, var(--sidebar-bg) 90%, #ff4444);
color: #a55;
border: 1px solid color-mix(in srgb, var(--border-color) 80%, #ff4444);
}
.delete-btn:hover {
background: color-mix(in srgb, var(--sidebar-bg) 80%, #ff4444);
color: #933;
transform: scale(1.05);
}
.book-item:hover .delete-btn {
opacity: 1;
}
.book-item:not(:hover) .delete-btn {
opacity: 0.7;
}
/* 进度条样式 */
.progress {
height: 6px;
border-radius: 3px;
background: var(--border-color);
overflow: hidden;
}
.progress-bar {
background: var(--accent-color);
transition: width 0.3s ease;
}
/* 标题样式优化 */
h5.text-muted {
color: var(--text-secondary) !important;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: color-mix(in srgb, var(--background-color) 90%, #000);
}
::-webkit-scrollbar-thumb {
background: var(--accent-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #7da585;
}
/* 新增护眼控制面板 */
.eye-care-panel {
position: fixed;
top: 25px;
left: 25px;
background: var(--sidebar-bg);
padding: 20px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
z-index: 1000;
min-width: 200px;
border: 1px solid var(--border-color);
transition: all 0.3s;
}
.eye-care-panel.collapsed {
transform: translateX(-100%);
opacity: 0;
visibility: hidden;
}
.eye-care-control {
margin: 12px 0;
}
.eye-care-label {
display: block;
color: var(--text-secondary);
font-size: 0.9em;
margin-bottom: 6px;
font-weight: 500;
}
.mode-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 12px;
}
.mode-btn {
padding: 8px 12px;
border: 1px solid var(--border-color);
background: var(--sidebar-bg);
color: var(--text-primary);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.85em;
text-align: center;
}
.mode-btn:hover {
background: var(--hover-color);
}
.mode-btn.active {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.font-controls {
display: flex;
align-items: center;
gap: 10px;
margin: 12px 0;
}
.font-btn {
width: 36px;
height: 36px;
border: 1px solid var(--border-color);
background: var(--sidebar-bg);
color: var(--text-primary);
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.font-btn:hover {
background: var(--hover-color);
}
.font-btn.active {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.line-height-control {
display: flex;
align-items: center;
gap: 10px;
margin: 12px 0;
}
.line-height-btn {
flex: 1;
padding: 8px;
border: 1px solid var(--border-color);
background: var(--sidebar-bg);
color: var(--text-primary);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
font-size: 0.85em;
}
.line-height-btn:hover {
background: var(--hover-color);
}
.line-height-btn.active {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.toggle-eye-care {
position: fixed;
top: 15px;
left: 15px;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--accent-color);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
font-size: 14px;
transition: transform 0.3s, background 0.3s;
z-index: 1001;
}
.toggle-eye-care:hover {
background: #7da585;
transform: scale(1.1);
}
/* 专注阅读模式 */
.focus-mode .chapter-title {
font-size: 1.8rem;
margin-bottom: 2rem;
}
.focus-mode .paragraph-content {
font-size: 1.2rem;
line-height: 2;
margin: 1.5rem 0;
}
/* 阅读进度指示器 */
.reading-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: var(--border-color);
z-index: 1002;
}
.reading-progress-bar {
height: 100%;
background: var(--accent-color);
width: 0%;
transition: width 0.1s ease;
}
/* 呼吸动画效果 */
@keyframes breathe {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.breathe-mode .paragraph-content {
animation: breathe 6s ease-in-out infinite;
}
</style>
</head>
<body>
<!-- 阅读进度条 -->
<div class="reading-progress">
<div class="reading-progress-bar" id="readingProgress"></div>
</div>
<!-- 保存提示 -->
<div id="saveNotification" class="save-notification"></div>
<!-- 护眼控制面板 -->
<button id="toggleEyeCare" class="toggle-eye-care">👁️</button>
<div class="eye-care-panel" id="eyeCarePanel">
<h6 style="color: var(--text-primary); margin-bottom: 15px;">护眼设置</h6>
<div class="eye-care-control">
<span class="eye-care-label">阅读模式</span>
<div class="mode-selector">
<button class="mode-btn active" data-mode="default">默认</button>
<button class="mode-btn" data-mode="paper">纸张</button>
<button class="mode-btn" data-mode="parchment">羊皮纸</button>
<button class="mode-btn" data-mode="dark-eye">深色护眼</button>
<button class="mode-btn" data-mode="night">夜间</button>
<button class="mode-btn" data-mode="focus">专注模式</button>
</div>
</div>
<div class="eye-care-control">
<span class="eye-care-label">字体大小</span>
<div class="font-controls">
<button class="font-btn" data-size="0.9">小</button>
<button class="font-btn active" data-size="1.1">中</button>
<button class="font-btn" data-size="1.3">大</button>
<button class="font-btn" data-size="1.5">特大</button>
</div>
</div>
<div class="eye-care-control">
<span class="eye-care-label">行高</span>
<div class="line-height-control">
<button class="line-height-btn" data-height="1.5">紧凑</button>
<button class="line-height-btn active" data-height="1.8">舒适</button>
<button class="line-height-btn" data-height="2.2">宽松</button>
</div>
</div>
<div class="eye-care-control">
<span class="eye-care-label">特殊效果</span>
<div style="display: flex; gap: 8px;">
<button class="mode-btn" id="toggleBreathe">呼吸模式</button>
<button class="mode-btn" id="toggleBlur">背景模糊</button>
</div>
</div>
</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"><i class="fas fa-copy me-2"></i>复制全部笔记</button>
</div>
</div>
<button id="mobileMenuBtn"><i class="fas fa-bars me-2"></i>菜单</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-4">
<!-- 上传表单 -->
<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="fas fa-upload me-2"></i>上传小说
</label>
<div class="progress mt-3" style="display: none;">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</form>
<!-- 书籍列表 -->
<h5 class="mb-3"><i class="fas fa-book me-2"></i>我的书架</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-sm delete-btn"><i class="fas fa-trash"></i></button>
</div>
<small class="text-muted">
<i class="fas fa-list me-1"></i>共<?= $book['chapter_count'] ?>章 ·
<i class="far fa-clock me-1"></i><?= date('m/d H:i', strtotime($book['upload_time'])) ?>
</small>
</div>
<?php endforeach; ?>
</div>
<!-- 章节目录 -->
<h5 class="mt-4 mb-3"><i class="fas fa-list-ul me-2"></i>章节列表</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']) ?>">
<i class="far fa-file-alt me-2"></i><?= 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 py-5">
<i class="fas fa-book-open fa-3x mb-3" style="color: var(--accent-color);"></i>
<h4>请选择章节开始阅读</h4>
</div>
<?php else: ?>
<div class="text-center mt-5 py-5">
<i class="fas fa-book-reader fa-4x mb-4" style="color: var(--accent-color);"></i>
<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('网络请求失败,请检查连接');
});
});
// 护眼功能管理器
class EyeCareManager {
constructor() {
this.settings = {
mode: 'default',
fontSize: '1.1',
lineHeight: '1.8',
breatheMode: false,
blurMode: false,
focusMode: false
};
this.init();
}
init() {
this.loadSettings();
this.bindEvents();
this.applySettings();
}
loadSettings() {
const saved = localStorage.getItem('eyeCareSettings');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
}
saveSettings() {
localStorage.setItem('eyeCareSettings', JSON.stringify(this.settings));
}
bindEvents() {
// 模式切换
$('.mode-btn').on('click', (e) => {
const mode = $(e.target).data('mode');
if (mode) {
this.setMode(mode);
}
});
// 字体大小
$('.font-btn').on('click', (e) => {
const size = $(e.target).data('size');
this.setFontSize(size);
});
// 行高
$('.line-height-btn').on('click', (e) => {
const height = $(e.target).data('height');
this.setLineHeight(height);
});
// 特殊效果
$('#toggleBreathe').on('click', () => {
this.toggleBreatheMode();
});
$('#toggleBlur').on('click', () => {
this.toggleBlurMode();
});
// 阅读进度
$(window).on('scroll', this.updateReadingProgress.bind(this));
}
setMode(mode) {
$('body').removeClass('night-mode paper-mode parchment-mode dark-eye-mode focus-mode');
if (mode !== 'default') {
$('body').addClass(`${mode}-mode`);
}
if (mode === 'focus') {
this.settings.focusMode = true;
this.setFontSize('1.2');
this.setLineHeight('2');
} else {
this.settings.focusMode = false;
}
this.settings.mode = mode;
this.updateActiveButtons('.mode-btn', mode);
this.saveSettings();
}
setFontSize(size) {
document.documentElement.style.setProperty('--reading-font-size', size + 'rem');
this.settings.fontSize = size;
this.updateActiveButtons('.font-btn', size);
this.saveSettings();
}
setLineHeight(height) {
document.documentElement.style.setProperty('--reading-line-height', height);
this.settings.lineHeight = height;
this.updateActiveButtons('.line-height-btn', height);
this.saveSettings();
}
toggleBreatheMode() {
this.settings.breatheMode = !this.settings.breatheMode;
$('body').toggleClass('breathe-mode', this.settings.breatheMode);
$('#toggleBreathe').toggleClass('active', this.settings.breatheMode);
this.saveSettings();
}
toggleBlurMode() {
this.settings.blurMode = !this.settings.blurMode;
$('#contentArea').css('backdrop-filter', this.settings.blurMode ? 'blur(2px)' : 'none');
$('#toggleBlur').toggleClass('active', this.settings.blurMode);
this.saveSettings();
}
updateActiveButtons(selector, value) {
$(selector).removeClass('active');
$(`${selector}[data-${selector.includes('font') ? 'size' : selector.includes('line-height') ? 'height' : 'mode'}="${value}"]`).addClass('active');
}
applySettings() {
this.setMode(this.settings.mode);
this.setFontSize(this.settings.fontSize);
this.setLineHeight(this.settings.lineHeight);
if (this.settings.breatheMode) this.toggleBreatheMode();
if (this.settings.blurMode) this.toggleBlurMode();
}
updateReadingProgress() {
const winHeight = $(window).height();
const docHeight = $(document).height();
const scrollTop = $(window).scrollTop();
const progress = (scrollTop / (docHeight - winHeight)) * 100;
$('#readingProgress').css('width', progress + '%');
}
}
// 初始化护眼管理器
let eyeCareManager;
$(function() {
eyeCareManager = new EyeCareManager();
// 护眼面板控制
$('#toggleEyeCare').on('click', function() {
const panel = $('#eyeCarePanel');
const isCollapsed = panel.hasClass('collapsed');
panel.toggleClass('collapsed', !isCollapsed);
$(this).text(isCollapsed ? '👁️' : '⚙️');
if (isCollapsed) {
$(this).css('transform', 'translateX(220px)');
} else {
$(this).css('transform', 'translateX(0)');
}
});
// 移动端侧边栏控制
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).off('input', '.note-textarea').on('keydown', '.note-textarea', function(e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
const textarea = this;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = textarea.value;
textarea.value = value.substring(0, start) + '\n' + value.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + 1;
autoResizeTextarea.call(textarea);
$(this).trigger('input');
}
});
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'));
});
// 字数统计面板折叠
$('#toggleStats').on('click', function() {
const panel = $('.stats-panel');
const isCollapsed = panel.hasClass('collapsed');
panel.toggleClass('collapsed', !isCollapsed);
this.textContent = isCollapsed ? '▼' : '▶';
if (isCollapsed) {
$(this).css('transform', 'translateX(0)');
} else {
$(this).css('transform', 'translateX(-100%)');
}
});
// 添加去抖动函数
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.join('');
// 执行复制操作
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': 'var(--success-color)',
'color': 'white'
}).html('<i class="fas fa-check me-2"></i>复制成功');
setTimeout(() => {
$(this).css({
'background': 'var(--accent-color)',
'color': 'white'
}).html('<i class="fas fa-copy me-2"></i>复制全部笔记');
}, 2000);
} catch (err) {
console.error('复制失败:', err);
showSaveNotification('复制失败,请手动选择文本');
$(this).css('background', '#e74c3c').html('<i class="fas fa-exclamation-triangle me-2"></i>复制失败');
setTimeout(() => {
$(this).css('background', 'var(--accent-color)').html('<i class="fas fa-copy me-2"></i>复制全部笔记');
}, 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 total = 0;
$('.note-textarea').each(function() {
const text = $(this).val();
total += professionalWordCount(text);
});
$('#notesCount').text(total);
}
function calculateOriginalCount() {
const originalText = $('#contentArea').clone().find('.note-textarea').remove().end().text();
$('#originalCount').text(professionalWordCount(originalText));
}
// 行业标准字数统计
function professionalWordCount(text) {
// 中文及中文标点计数(每个字符计1字)
const chineseRegex = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/g;
const chineseMatches = text.match(chineseRegex) || [];
const chineseCount = chineseMatches.length;
// 处理非中文部分
const nonChinese = text
.replace(chineseRegex, '') // 移除已统计的中文
.replace(/\s+/g, ' ') // 合并连续空格
.trim();
// 统计英文单词和数据单词(连续字母数字序列计1字)
const westernWords = nonChinese ? nonChinese.split(/\s+/) : [];
return chineseCount + westernWords.length;
}
// 新增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') || '';
}
});
// 初始化时保持展开状态
document.querySelector('.stats-panel').classList.remove('collapsed');
</script>
</body>
</html>
后端api:
实时保存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


![表情[aini]-红穆笔记](https://www.4s5.cn/wp-content/themes/zibll/img/smilies/aini.gif)
![表情[ciya]-红穆笔记](https://www.4s5.cn/wp-content/themes/zibll/img/smilies/ciya.gif)
![表情[xia]-红穆笔记](https://www.4s5.cn/wp-content/themes/zibll/img/smilies/xia.gif)


暂无评论内容