html,php,ajax写的一个小说阅读器

纯阅读版本:

<?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
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容