PHPWind Reddit Style Refactor

将 PHPWind 论坛回复重构为 Reddit 嵌套风格

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         PHPWind Reddit Style Refactor
// @namespace    http://tampermonkey.net/
// @version      1.4.6
// @description  将 PHPWind 论坛回复重构为 Reddit 嵌套风格
// @match        *://*.blue-plus.net/read.php?*
// @match        *://*.east-plus.net/read.php?*
// @match        *://*.imoutolove.me/read.php?*
// @match        *://*.level-plus.net/read.php?*
// @match        *://*.north-plus.net/read.php?*
// @match        *://*.snow-plus.net/read.php?*
// @match        *://*.soul-plus.net/read.php?*
// @match        *://*.south-plus.net/read.php?*
// @match        *://*.south-plus.org/read.php?*
// @match        *://*.spring-plus.net/read.php?*
// @match        *://*.summer-plus.net/read.php?*
// @match        *://*.white-plus.net/read.php?*
// @run-at       document-start
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';

    // 防止脚本在 iframe 中重复运行(如帖子内容嵌入了同域 read.php 的 iframe)
    if (window.top !== window.self) return;

    // 第一优先级:立即隐藏页面,消除从论坛列表点进帖子时的原始排版闪烁。
    // 直接操作 documentElement + 注入内联 style 标签,比 GM_addStyle 更快,
    // 因为在 document-start 时机 head 元素可能尚未解析。
    const docEl = document.documentElement;
    docEl.style.visibility = 'hidden';
    const antiFlashStyle = document.createElement('style');
    antiFlashStyle.textContent = 'html,body{visibility:hidden!important}';
    docEl.appendChild(antiFlashStyle);

    function showPage() {
        docEl.style.visibility = '';
        if (antiFlashStyle.parentNode) antiFlashStyle.remove();
        document.body.classList.add('reddit-ready');
    }

    // --- 配置与常量 ---
    const CONFIG = {
        DEBUG: true,
        NESTING_LIMIT: 10,
        FETCH_CONCURRENCY: 4, // 并行抓取并发数
    };
    const LAYOUT_STORAGE_KEY = 'phpwind-reddit-layout-mode';

    // --- CSS 样式 ---
    const styles = `
        /* 初始完全隐藏 body,消除原样式闪烁,reddit 就绪后再显示 */
        body { visibility: hidden !important; }
        body.reddit-ready { visibility: visible !important; }
        /* 原始排版模式下隐藏原始元素 */
        .original-hidden { display: none !important; }

        /* Reddit 容器 */
        #reddit-container {
            max-width: 800px;
            margin: 20px auto;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            background: #fff;
            padding: 20px;
            color: #1c1c1c;
        }

        .reddit-post-wrapper {
            position: relative;
            margin-top: 12px;
        }

        .reddit-comment {
            display: flex;
            flex-direction: row;
            min-height: 40px; /* 确保即使内容很少,也有足够高度容纳头像和基础信息 */
        }

        .reddit-comment-left {
            display: flex;
            flex-direction: column;
            align-items: center;
            width: 32px;
            margin-right: 12px;
            position: relative;
            flex-shrink: 0;
        }

        .reddit-avatar-wrapper {
            position: relative;
            width: 32px;
            height: 32px;
            z-index: 3;
        }

        .reddit-avatar {
            width: 100%;
            height: 100%;
            border-radius: 50%;
            object-fit: cover;
            background-color: #f6f7f8;
            border: 2px solid #fff; /* 给头像加个白边,防止被线穿过 */
        }

        .reddit-threadline {
            position: absolute;
            top: 32px; /* 从头像底部开始 */
            bottom: -12px; /* 延伸到容器底部 */
            width: 2px;
            background-color: #edeff1;
            cursor: pointer;
            transition: background-color 0.2s;
            z-index: 2;
            left: 15px; /* 居中 */
        }

        .reddit-threadline:hover {
            background-color: #0079d3;
        }

        .reddit-comment-right {
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            min-width: 0;
        }

        .reddit-comment-header {
            display: flex;
            align-items: center;
            font-size: 13px;
            margin-bottom: 11px;
            line-height: 16px;
            min-height: 18px;
        }

        .reddit-author {
            font-weight: 600;
            color: #1c1c1c;
            text-decoration: none;
            margin-right: 4px;
        }

        .reddit-author:hover {
            text-decoration: underline;
        }

        .reddit-time {
            color: #787c7e;
        }

        .reddit-comment-body {
            font-size: 14px;
            line-height: 1.5;
            word-wrap: break-word;
            margin-bottom: 4px;
            color: #1c1c1c;
        }

        /* 隐藏某些冗余的原始元素 */
        .reddit-comment-body .f12,
        .reddit-comment-body h1[id^="subject_"] {
            display: none !important;
        }

        /* 引用框样式重构,不再隐藏,而是改为 Reddit 风格的引用 */
        .reddit-comment-body .quote:not(.jumbotron),
        .reddit-comment-body .blockquote:not(.jumbotron),
        .reddit-comment-body .blockquote2,
        .reddit-comment-body .blockquote3 {
            display: block !important;
            margin: 8px 0 !important;
            padding: 2px 0 2px 12px !important;
            border-left: 3px solid #edeff1 !important;
            color: #576f76 !important;
            background: transparent !important;
            font-size: 13px !important;
        }

        /* 隐藏引用中的头部标题(如“引用”、“回复X楼”),因为嵌套结构已经表达了关系 */
        .reddit-comment-body .quote h6,
        .reddit-comment-body .blockquote h6,
        .reddit-comment-body .blockquote2 h6,
        .reddit-comment-body .blockquote3 h6 {
             display: none !important;
        }

        /* 冗余引用消除:当引用内容仅仅是重复父楼层内容时隐藏 */
        .reddit-comment-body .redundant-quote {
            display: none !important;
        }
        /* 隐藏冗余引用后的换行,防止留白 */
        .reddit-comment-body .redundant-quote + br {
            display: none !important;
        }

        /* 购买框样式微调 */
        .reddit-comment-body .jumbotron {
            display: block !important;
            margin: 10px 0;
            padding: 12px;
            background-color: #fff5f5;
            border: 1px solid #feb2b2;
            border-radius: 6px;
        }
        /* 购买框内的所有子元素保持可见,不被上述隐藏规则影响 */
        .reddit-comment-body .jumbotron .blockquote,
        .reddit-comment-body .jumbotron .blockquote2,
        .reddit-comment-body .jumbotron .blockquote3,
        .reddit-comment-body .jumbotron .f12,
        .reddit-comment-body .jumbotron .quote {
            display: revert !important;
        }

        .reddit-comment-actions {
            display: flex;
            align-items: center;
            gap: 4px;
            margin-bottom: 8px;
        }

        .reddit-action-btn {
            display: flex;
            align-items: center;
            gap: 6px;
            padding: 6px 8px;
            border-radius: 9999px;
            border: none;
            background: transparent;
            color: #576f76;
            font-size: 12px;
            font-weight: 600;
            cursor: pointer;
            text-decoration: none;
            line-height: 16px;
        }

        .reddit-action-btn:hover {
            background-color: #eaedef;
        }

        .reddit-action-icon {
            width: 16px;
            height: 16px;
            fill: currentColor;
        }

        /* 嵌套样式与曲线设计 */
        .reddit-children {
            position: relative;
            margin-left: 36px; /* 增加缩进,为大弧度留出空间 */
            padding-left: 0;
        }

        /* 曲线连接线:连接父级垂直线到子级头像 */
        .reddit-children > .reddit-post-wrapper::before {
            content: "";
            position: absolute;
            left: -21px; /* 36(margin) - 15(parent threadline) = 21 */
            top: -12px;
            width: 37px; /* 延伸到子头像中心 (36 - 15 + 16 = 37) */
            height: 28px; /* 到达头像中心高度 */
            border-left: 2px solid #edeff1;
            border-bottom: 2px solid #edeff1;
            border-bottom-left-radius: 24px; /* 更大、更平缓的弧度 */
            pointer-events: none;
            z-index: 1;
        }

        /* 垂直补充线:如果当前评论之后还有兄弟评论,则垂直线继续向下延伸 */
        .reddit-children > .reddit-post-wrapper:not(:last-child)::after {
            content: "";
            position: absolute;
            left: -21px;
            top: 16px;
            bottom: -12px;
            border-left: 2px solid #edeff1;
            pointer-events: none;
            z-index: 1;
        }

        /* 折叠按钮 */
        .reddit-collapse-btn {
            position: absolute;
            left: 6px; /* 居中对齐到 threadline (15px) */
            top: 36px;
            width: 20px;
            height: 20px;
            background: #fff;
            border: 1px solid #edeff1;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            z-index: 4;
            color: #878a8c;
            opacity: 1; /* 常显,更符合直觉 */
            transition: all 0.2s;
            padding: 0;
        }

        .reddit-comment:hover .reddit-collapse-btn {
            border-color: #d7d9db;
            color: #576f76;
        }

        .reddit-collapse-btn:hover {
            background-color: #eaedef;
            border-color: #878a8c;
            color: #1c1c1c;
        }

        .reddit-collapse-btn svg {
            width: 12px;
            height: 12px;
        }

        /* 针对折叠状态 */
        .reddit-post-wrapper.collapsed > .reddit-comment > .reddit-comment-right > .reddit-comment-body,
        .reddit-post-wrapper.collapsed > .reddit-comment > .reddit-comment-right > .reddit-comment-actions,
        .reddit-post-wrapper.collapsed > .reddit-children,
        .reddit-post-wrapper.collapsed > .reddit-comment > .reddit-comment-left > .reddit-threadline,
        .reddit-post-wrapper.collapsed > .reddit-comment > .reddit-comment-left > .reddit-collapse-btn {
            display: none !important;
        }

        .reddit-post-wrapper.collapsed > .reddit-comment > .reddit-comment-left > .reddit-avatar-wrapper {
            opacity: 0.6;
        }

        /* 头部折叠/展开按钮 */
        .reddit-expand-inline,
        .reddit-collapse-inline {
            display: none;
            margin-left: 8px;
            width: 16px;
            height: 16px;
            cursor: pointer;
            vertical-align: middle;
            fill: #787c7e;
            transition: fill 0.2s;
        }

        .reddit-expand-inline {
            fill: #0079d3;
        }

        .reddit-comment-header:hover .reddit-collapse-inline {
            display: inline-block;
        }

        .reddit-post-wrapper.collapsed > .reddit-comment > .reddit-comment-right > .reddit-comment-header .reddit-expand-inline {
            display: inline-block;
        }

        .reddit-post-wrapper.collapsed > .reddit-comment > .reddit-comment-right > .reddit-comment-header .reddit-collapse-inline {
            display: none !important;
        }

        /* 切换按钮 */
        #layout-toggle-btn {
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 9999;
            padding: 10px 15px;
            background: #0079d3;
            color: white;
            border: none;
            border-radius: 20px;
            cursor: pointer;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            font-weight: bold;
        }

        #layout-toggle-btn:hover { background: #005fa3; }

        /* Loading 状态 */
        #reddit-loader {
            text-align: center;
            padding: 50px;
            font-size: 18px;
            color: #666;
        }

        /* 回复框样式 */
        .reddit-reply-box {
            margin-top: 10px;
            margin-bottom: 15px;
            padding: 12px;
            background: #f6f7f8;
            border-radius: 4px;
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .reddit-reply-textarea {
            width: 100%;
            min-height: 100px;
            padding: 8px;
            border: 1px solid #edeff1;
            border-radius: 4px;
            font-family: inherit;
            resize: vertical;
        }

        .reddit-reply-footer {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
        }

        .reddit-reply-footer-actions {
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .reddit-reply-footer-submit {
            display: flex;
            align-items: center;
            justify-content: flex-end;
            gap: 8px;
        }

        .reddit-reply-btn {
            padding: 4px 16px;
            border-radius: 999px;
            font-weight: 700;
            cursor: pointer;
            border: none;
            font-size: 12px;
        }

        .reddit-reply-submit {
            background-color: #0079d3;
            color: white;
        }

        .reddit-reply-submit:hover { background-color: #005fa3; }

        .reddit-reply-cancel {
            background-color: transparent;
            color: #576f76;
        }

        .reddit-reply-cancel:hover { background-color: #eaedef; }

        /* 表情相关 */
        .reddit-reply-actions {
            display: flex;
            align-items: center;
            gap: 12px;
            margin-bottom: 4px;
        }

        .reddit-emoji-btn {
            background: transparent;
            border: none;
            cursor: pointer;
            padding: 4px;
            display: flex;
            align-items: center;
            color: #576f76;
            border-radius: 4px;
            transition: background-color 0.2s;
        }

        .reddit-emoji-btn:hover {
            background-color: #eaedef;
        }

        .reddit-emoji-container {
            display: none;
            margin: 6px 0 8px;
        }

        .reddit-emoji-container.active {
            display: block;
        }

        #reddit-smilie-storage {
            display: none !important;
        }

        #smiliebox {
            display: block !important;
            border: 1px solid #edeff1 !important;
            padding: 10px !important;
            background: #fff !important;
            max-width: 100%;
            margin: 8px 0 !important;
            border-radius: 4px;
        }

        #menu_face, #menu_generalface {
            z-index: 10001 !important;
        }

        /* 投票容器样式 */
        .reddit-vote-container {
            margin: 15px 0;
            padding: 10px;
            border: 1px solid #edeff1;
            border-radius: 8px;
            background: #f8f9fa;
        }
        .reddit-vote-container form {
            display: block !important;
            margin: 0 !important;
        }
        .reddit-vote-container .tt2 {
            display: block !important;
            border: 1px solid #edeff1 !important;
            border-radius: 6px;
            overflow: hidden;
            background: #fff;
        }
        .reddit-vote-container table {
            width: 100% !important;
            border-collapse: collapse;
            background: #fff !important;
        }
        .reddit-vote-container th,
        .reddit-vote-container td {
            padding: 8px !important;
            border-bottom: 1px solid #edeff1;
            font-size: 13px;
            text-align: left !important;
            font-weight: normal !important;
            line-height: 1.6 !important;
        }
        .reddit-vote-container tr:last-child th,
        .reddit-vote-container tr:last-child td {
            border-bottom: none !important;
        }
        .reddit-vote-container .h {
            background: #f1f3f5 !important;
            color: #1c1c1c !important;
            font-weight: 600 !important;
        }
        .reddit-vote-container input[type="radio"],
        .reddit-vote-container input[type="checkbox"] {
            appearance: auto !important;
            -webkit-appearance: auto !important;
            width: 16px !important;
            height: 16px !important;
            margin: 0 8px 0 0 !important;
            cursor: pointer;
            vertical-align: middle;
            display: inline-block !important;
            opacity: 1 !important;
            position: static !important;
        }
        .reddit-vote-container input[type="submit"],
        .reddit-vote-container input[type="button"] {
            padding: 6px 16px;
            background: #0079d3;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            margin-top: 10px;
        }
        .reddit-vote-container input[type="submit"]:hover {
            background: #005fa3;
        }
        .reddit-vote-container img {
            vertical-align: middle;
            max-width: 100%;
        }

        .reddit-title-row {
            display: flex;
            align-items: flex-start;
            justify-content: space-between;
            gap: 16px;
            margin-bottom: 20px;
        }

        .reddit-title-text {
            font-size: 24px;
            font-weight: bold;
            color: #1c1c1c;
            min-width: 0;
        }

        .reddit-topic-actions {
            display: flex;
            align-items: center;
            justify-content: flex-end;
            gap: 8px;
            flex-shrink: 0;
        }

        .reddit-topic-btn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-height: 32px;
            padding: 0 12px;
            border-radius: 999px;
            background: #0079d3;
            color: #fff !important;
            font-size: 13px;
            font-weight: 700;
            text-decoration: none !important;
            white-space: nowrap;
        }

        .reddit-topic-btn:hover {
            background: #005fa3;
        }

        /* Apple 风格悬浮导航栏 */
        #reddit-sidebar {
            position: fixed;
            left: 24px;
            top: 50%;
            transform: translateY(-50%);
            z-index: 10002;
            display: flex;
            flex-direction: column;
            gap: 16px;
            transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        }

        #reddit-sidebar.hidden {
            opacity: 0;
            pointer-events: none;
            transform: translateY(-50%) translateX(-30px);
        }

        .sidebar-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            width: 64px;
            min-height: 64px;
            padding: 8px 4px;
            box-sizing: border-box;
            background: rgba(255, 255, 255, 0.72);
            backdrop-filter: blur(20px) saturate(180%);
            -webkit-backdrop-filter: blur(20px) saturate(180%);
            border-radius: 18px;
            box-shadow: 0 4px 24px rgba(0, 0, 0, 0.04);
            text-decoration: none;
            color: #1d1d1f;
            transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
            border: 1px solid rgba(255, 255, 255, 0.4);
            overflow: visible;
        }

        .sidebar-item:hover {
            background: rgba(255, 255, 255, 0.92);
            transform: scale(1.08) translateX(4px);
            box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08);
            border-color: rgba(255, 255, 255, 0.6);
            color: #0071e3; /* Apple Blue */
        }

        .sidebar-icon {
            width: 22px;
            height: 22px;
            margin-bottom: 4px;
            fill: none;
            stroke: currentColor;
            stroke-width: 1.5;
            stroke-linecap: round;
            stroke-linejoin: round;
        }

        .sidebar-text {
            font-size: 11px;
            font-weight: 600;
            letter-spacing: 0.02em;
            text-align: center;
            word-break: break-all;
            line-height: 1.3;
            max-width: 100%;
        }

        /* 发帖按钮下拉菜单 */
        .reddit-post-dropdown {
            position: relative;
            display: inline-block;
        }

        .reddit-post-menu {
            display: none;
            position: absolute;
            top: 100%;
            right: 0;
            background: rgba(255, 255, 255, 0.98);
            backdrop-filter: blur(20px) saturate(180%);
            -webkit-backdrop-filter: blur(20px) saturate(180%);
            min-width: 100px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.1);
            border-radius: 12px;
            z-index: 10005;
            margin-top: 8px;
            border: 1px solid rgba(255, 255, 255, 0.4);
            padding: 6px;
            overflow: visible; /* 改为 visible 以允许伪元素超出 */
            animation: redditFadeIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
        }

        .reddit-post-menu::before {
            content: '';
            position: absolute;
            top: -12px;
            left: 0;
            right: 0;
            height: 12px;
            z-index: -1;
        }

        @keyframes redditFadeIn {
            from { opacity: 0; transform: translateY(-8px) scale(0.95); }
            to { opacity: 1; transform: translateY(0) scale(1); }
        }

        .reddit-post-dropdown:hover .reddit-post-menu {
            display: block;
        }

        .reddit-post-menu-item {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 8px 12px;
            color: #1c1c1c;
            text-decoration: none;
            font-size: 13px;
            font-weight: 600;
            border-radius: 8px;
            transition: all 0.2s;
            white-space: nowrap;
        }

        .reddit-post-menu-item:hover {
            background: rgba(0, 113, 227, 0.1);
            color: #0071e3;
        }

        /* === 响应式布局:窄屏时自动收起侧栏和切换按钮 === */
        /* 左侧悬浮触发区域(窄屏时出现) */
        #reddit-sidebar-trigger {
            display: none;
            position: fixed;
            left: 0;
            top: 0;
            width: 28px;
            height: 100vh;
            z-index: 10001;
            background: transparent;
        }

        /* 窄屏下的侧栏 + 按钮响应式 */
        @media (max-width: 1100px) {
            /* 触发区域可见 */
            #reddit-sidebar-trigger {
                display: block;
            }

            /* 侧栏默认隐藏 */
            #reddit-sidebar {
                opacity: 0;
                pointer-events: none;
                transform: translateY(-50%) translateX(-30px);
            }

            /* 鼠标悬浮触发区域或侧栏自身时,显示侧栏 */
            #reddit-sidebar-trigger:hover ~ #reddit-sidebar,
            #reddit-sidebar:hover {
                opacity: 1 !important;
                pointer-events: auto !important;
                transform: translateY(-50%) translateX(0) !important;
            }

            /* 切换按钮移到右下角,避免遮挡标题区的"回帖/发新帖"按钮 */
            #layout-toggle-btn {
                top: auto;
                bottom: 20px;
                right: 20px;
            }
        }
    `;
    GM_addStyle(styles);

    // --- 全局状态 ---
    let redditContainer = null;
    let isRedditMode = getStoredLayoutMode() !== 'original';
    let allPosts = new Map(); // floorNumber -> PostObject
    let rootPosts = [];
    let originalElements = [];
    let activeTextarea = null;
    let originalSmilieParent = null;
    let originalSmilieNextSibling = null;

    function getStoredLayoutMode() {
        try {
            return localStorage.getItem(LAYOUT_STORAGE_KEY) || 'reddit';
        } catch (e) {
            return 'reddit';
        }
    }

    function setStoredLayoutMode(mode) {
        try {
            localStorage.setItem(LAYOUT_STORAGE_KEY, mode);
        } catch (e) { }
    }

    let isUpdating = false;
    async function refreshDataNoReload(targetFloor = null) {
        if (isUpdating) return;
        isUpdating = true;

        if (CONFIG.DEBUG) console.log('Refreshing data...', targetFloor);

        // 1. 记录状态
        const savedScrollY = window.scrollY;
        const containerHeight = redditContainer.offsetHeight;
        const collapsedFloors = new Set();
        document.querySelectorAll('.reddit-post-wrapper.collapsed').forEach(el => {
            const floor = el.dataset.floor;
            if (floor) collapsedFloors.add(floor);
        });

        try {
            // 2. 抓取最新数据 (带缓存穿透)
            const currentPage = getCurrentPageNumber();
            const totalPagesBefore = getTotalPages();
            const pageUrls = buildPageUrls(totalPagesBefore);
            const currentPageEntry = pageUrls.find(p => p.page === currentPage);
            if (!currentPageEntry) return;

            // 给服务端一点处理时间,防止数据库主从延迟等
            await new Promise(r => setTimeout(r, 200));

            // 抓取当前页
            const cacheBuster = (url) => url + (url.includes('?') ? '&' : '?') + '_t=' + Date.now();
            const doc = await fetchPage(cacheBuster(currentPageEntry.url));

            const totalPagesAfter = getTotalPages(doc);
            parsePosts(doc);

            // 抓取新增页
            if (totalPagesAfter > totalPagesBefore) {
                const newEntries = buildPageUrls(totalPagesAfter).filter(p => p.page > totalPagesBefore);
                for (const entry of newEntries) {
                    const newDoc = await fetchPage(cacheBuster(entry.url));
                    parsePosts(newDoc);
                }
            }

            // 3. 渲染
            buildTree();
            redditContainer.style.minHeight = containerHeight + 'px';
            renderReddit();
            redditContainer.style.minHeight = '';

            // 4. 恢复状态
            document.querySelectorAll('.reddit-post-wrapper').forEach(el => {
                const floor = el.dataset.floor;
                if (collapsedFloors.has(floor)) el.classList.add('collapsed');
            });

            // 5. 定位
            if (targetFloor === 'latest') {
                const floors = Array.from(allPosts.keys()).sort((a, b) => b - a);
                if (floors.length > 0) {
                    const el = document.querySelector(`.reddit-post-wrapper[data-floor="${floors[0]}"]`);
                    if (el) {
                        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
                        el.style.backgroundColor = 'rgba(0, 121, 211, 0.1)';
                        setTimeout(() => el.style.backgroundColor = '', 2000);
                    }
                }
            } else if (targetFloor !== null) {
                const el = document.querySelector(`.reddit-post-wrapper[data-floor="${targetFloor}"]`);
                if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
            } else {
                window.scrollTo(0, savedScrollY);
            }
        } catch (e) {
            console.error('Refresh failed:', e);
            alert('刷新失败: ' + e.message);
        } finally {
            isUpdating = false;
        }
    }

    // --- 初始化 ---
    async function init() {
        // 1. 识别并隐藏原始所有主要容器,防止它们在 Reddit 模式下露出来
        const selectors = ['#toptool', '#wrapA', '#main', '#footer', '#breadcrumbs', '.bdbA', '.t3', 'audio#music'];
        selectors.forEach(s => {
            const el = document.querySelector(s);
            if (el) {
                if (isRedditMode) el.classList.add('original-hidden');
                originalElements.push(el);
            }
        });

        if (originalElements.length === 0) {
            showPage();
            return;
        }

        const originalSmilieBox = document.getElementById('smiliebox');
        if (originalSmilieBox) {
            originalSmilieParent = originalSmilieBox.parentNode;
            originalSmilieNextSibling = originalSmilieBox.nextSibling;
        }

        // 覆盖原生的 addsmile 函数,使其支持我们的动态回复框
        const originalAddSmile = window.addsmile;
        window.addsmile = function (code) {
            if (insertSmileCode(code)) {
                return;
            } else if (originalAddSmile) {
                originalAddSmile(code);
            }
        };

        // 2. 创建 Reddit 容器
        redditContainer = document.createElement('div');
        redditContainer.id = 'reddit-container';
        // 3. 监听全局点击事件
        document.addEventListener('click', handlePurchaseClick, true);
        document.body.appendChild(redditContainer);

        // 3. 创建左侧悬浮触发区域(窄屏幕时使用,必须在 toggleBtn 和 sidebar 之前插入以使 CSS ~ 选择器生效)
        const sidebarTrigger = document.createElement('div');
        sidebarTrigger.id = 'reddit-sidebar-trigger';
        if (!isRedditMode) sidebarTrigger.style.display = 'none';
        document.body.appendChild(sidebarTrigger);

        // 4. 创建切换按钮
        const toggleBtn = document.createElement('button');
        toggleBtn.type = 'button';
        toggleBtn.id = 'layout-toggle-btn';
        toggleBtn.innerText = isRedditMode ? '返回原始排版' : '切换到 Reddit 样式';
        toggleBtn.onclick = toggleLayout;
        document.body.appendChild(toggleBtn);

        // 5. 创建悬浮导航栏
        createFloatingSidebar();

        // 5. 开始抓取数据
        try {
            parsePosts(document);
            buildTree();
            renderReddit();
            if (!isRedditMode) redditContainer.style.display = 'none';
            // 首次渲染完成后显示页面,消除原样式闪烁
            showPage();

            await startFetching();
            if (!isRedditMode) redditContainer.style.display = 'none';
        } catch (e) {
            console.error('Reddit Refactor Error:', e);
            redditContainer.innerHTML = '<div style="text-align:center;padding:20px;">加载失败: ' + e.message + '</div>';
            showPage();
        }
    }

    function toggleLayout() {
        isRedditMode = !isRedditMode;
        const sidebar = document.getElementById('reddit-sidebar');
        const sidebarTrigger = document.getElementById('reddit-sidebar-trigger');
        if (isRedditMode) {
            originalElements.forEach(el => el.classList.add('original-hidden'));
            redditContainer.style.display = 'block';
            if (sidebar) sidebar.classList.remove('hidden');
            if (sidebarTrigger) sidebarTrigger.style.display = '';
            document.getElementById('layout-toggle-btn').innerText = '返回原始排版';
            setStoredLayoutMode('reddit');
        } else {
            restoreOriginalSmilieBox();
            originalElements.forEach(el => el.classList.remove('original-hidden'));
            redditContainer.style.display = 'none';
            if (sidebar) sidebar.classList.add('hidden');
            if (sidebarTrigger) sidebarTrigger.style.display = 'none';
            document.getElementById('layout-toggle-btn').innerText = '切换到 Reddit 样式';
            setStoredLayoutMode('original');
        }
    }

    function createFloatingSidebar() {
        if (document.getElementById('reddit-sidebar')) return;

        const sidebar = document.createElement('div');
        sidebar.id = 'reddit-sidebar';
        if (!isRedditMode) sidebar.classList.add('hidden');

        // 提取当前版块链接
        const forumLink = document.querySelector('.bdbA #breadcrumbs a[href*="fid-"]') ||
            document.querySelector('#breadcrumbs a[href*="fid-"]') ||
            { href: 'thread.php?fid=9', innerText: '茶馆' };

        const forumName = (forumLink.innerText || forumLink.textContent || '茶馆').trim().replace(/<i>.*<\/i>/g, '');

        sidebar.innerHTML = `
            <a href="index.php" class="sidebar-item" title="首页">
                <svg class="sidebar-icon" viewBox="0 0 24 24"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
                <span class="sidebar-text">首页</span>
            </a>
            <a href="${forumLink.href}" class="sidebar-item" title="${forumName}">
                <svg class="sidebar-icon" viewBox="0 0 24 24"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg>
                <span class="sidebar-text">${forumName}</span>
            </a>
            <a href="search.php" class="sidebar-item" title="搜索">
                <svg class="sidebar-icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
                <span class="sidebar-text">搜索</span>
            </a>
            <a href="javascript:void(0)" class="sidebar-item" title="收藏" id="reddit-sidebar-favor">
                <svg class="sidebar-icon" viewBox="0 0 24 24"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>
                <span class="sidebar-text">收藏</span>
            </a>
        `;
        document.body.appendChild(sidebar);

        // 绑定收藏功能
        const favorBtn = sidebar.querySelector('#reddit-sidebar-favor');
        if (favorBtn) {
            favorBtn.onclick = (e) => {
                e.preventDefault();

                // 优先尝试点击页面原有的收藏按钮,这能触发论坛自带的弹窗提示
                const originalFavorBtn = document.getElementById('favor');
                if (originalFavorBtn && (originalFavorBtn.getAttribute('onclick') || '').includes('favor')) {
                    originalFavorBtn.click();
                    return;
                }

                // 降级方案:自己发送 AJAX 请求,防止跳转导致空白页
                const tid = getTopicId();
                if (!tid) {
                    alert('无法获取帖子ID,收藏失败。');
                    return;
                }

                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `pw_ajax.php?action=favor&tid=${tid}`,
                    onload: function (res) {
                        const text = res.responseText || '';
                        if (text.includes('已经收藏')) {
                            alert('您已经收藏过此帖了!');
                        } else if (text.includes('成功') || text.includes('success')) {
                            alert('收藏成功!');
                        } else if (text.includes('登录')) {
                            alert('收藏失败:请先登录。');
                        } else {
                            // PHPWind 返回的信息通常是用 \t 分隔的
                            const parts = text.split('\t');
                            const msg = parts.length > 1 ? parts[1] : text;
                            alert('收藏提示:\n' + msg.replace(/<[^>]+>/g, '').trim());
                        }
                    },
                    onerror: function () {
                        alert('网络请求失败,收藏失败。');
                    }
                });
            };
        }
    }

    // --- 数据抓取逻辑 ---
    function getTotalPages(doc = document) {
        let maxPage = 1;

        // 方法1:通过分页链接的数字来判断最大页数
        const pageElements = doc.querySelectorAll('.pages a, .pages b, .pages li');
        pageElements.forEach(el => {
            const text = (el.innerText || el.textContent || '').trim();
            if (/^\d+$/.test(text)) {
                const pageNum = parseInt(text, 10);
                if (pageNum > maxPage) maxPage = pageNum;
            }
        });

        // 方法2:通过分页文本提取 "Pages: 1/4" 或 "1 / 4"
        const pagesContainers = doc.querySelectorAll('.pages');
        pagesContainers.forEach(container => {
            const text = container.innerText || '';
            const match = text.match(/\/\s*(\d+)/);
            if (match) {
                const pageNum = parseInt(match[1], 10);
                if (pageNum > maxPage) maxPage = pageNum;
            }
        });

        return maxPage;
    }

    function getCurrentPageNumber() {
        const pageMatch = window.location.href.match(/[?&]page=(\d+)|-page-(\d+)/);
        if (!pageMatch) return 1;
        return parseInt(pageMatch[1] || pageMatch[2], 10) || 1;
    }

    function buildPageUrls(totalPages) {
        let baseUrl = window.location.href;
        baseUrl = baseUrl.replace(/[&?]page=\d+/, '').replace(/-page-\d+/, '');

        const urls = [];
        for (let i = 1; i <= totalPages; i++) {
            let url = baseUrl;
            if (url.includes('.html')) {
                url = url.replace('.html', `-page-${i}.html`);
            } else {
                url += (url.includes('?') ? '&' : '?') + `page=${i}`;
            }
            urls.push({ page: i, url });
        }
        return urls;
    }

    // --- 并行抓取工具 ---
    async function fetchPagesParallel(urls, concurrency = CONFIG.FETCH_CONCURRENCY) {
        const results = new Array(urls.length);
        let idx = 0;

        async function worker() {
            while (idx < urls.length) {
                const i = idx++;
                try {
                    results[i] = await fetchPage(urls[i].url);
                } catch (e) {
                    if (CONFIG.DEBUG) console.warn('Fetch page failed:', urls[i].page, e.message);
                    results[i] = null;
                }
            }
        }

        const workers = Array.from({ length: Math.min(concurrency, urls.length) }, () => worker());
        await Promise.all(workers);
        return results;
    }

    async function startFetching() {
        const totalPages = getTotalPages();
        if (totalPages <= 1) return;

        const currentPage = getCurrentPageNumber();
        const pageEntries = buildPageUrls(totalPages);
        const pagesToFetch = pageEntries.filter(entry => entry.page !== currentPage);

        if (CONFIG.DEBUG) console.time('parallelFetch');

        // 并行抓取所有页面,限制并发数
        const docs = await fetchPagesParallel(pagesToFetch);

        // 一次性解析所有结果
        for (const doc of docs) {
            if (doc) parsePosts(doc);
        }

        if (CONFIG.DEBUG) console.timeEnd('parallelFetch');

        // 全部完成后一次性渲染
        buildTree();
        renderReddit();
    }

    async function fetchPage(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                timeout: 10000,
                onload: (res) => {
                    if (res.status !== 200) {
                        reject(new Error(`HTTP ${res.status}`));
                        return;
                    }
                    const parser = new DOMParser();
                    resolve(parser.parseFromString(res.responseText, 'text/html'));
                },
                onerror: () => reject(new Error('网络请求错误')),
                ontimeout: () => reject(new Error('请求超时'))
            });
        });
    }

    function getNodeText(node) {
        return (node?.innerText || node?.textContent || '').replace(/\u00a0/g, ' ').trim();
    }

    function decodeCloudflareEmail(encoded) {
        if (!encoded || encoded.length < 4) return '';
        try {
            const key = parseInt(encoded.slice(0, 2), 16);
            let email = '';
            for (let i = 2; i < encoded.length; i += 2) {
                email += String.fromCharCode(parseInt(encoded.slice(i, i + 2), 16) ^ key);
            }
            return email;
        } catch (e) {
            return '';
        }
    }

    function getDisplayText(node) {
        const protectedEmail = node?.querySelector?.('.__cf_email__[data-cfemail]');
        if (protectedEmail) {
            const decoded = decodeCloudflareEmail(protectedEmail.getAttribute('data-cfemail'));
            if (decoded) return decoded;
        }
        return getNodeText(node);
    }

    function insertSmileCode(code, target = activeTextarea) {
        target = target || document.querySelector('form[name="FORM"] textarea[name="atc_content"]');
        if (!target || code === undefined || code === null || code === '') return false;

        const insert = ' [s:' + code + '] ';
        const start = typeof target.selectionStart === 'number' ? target.selectionStart : target.value.length;
        const end = typeof target.selectionEnd === 'number' ? target.selectionEnd : start;
        const val = target.value || '';
        target.value = val.substring(0, start) + insert + val.substring(end);
        target.focus();
        target.selectionStart = target.selectionEnd = start + insert.length;
        target.dispatchEvent(new Event('input', { bubbles: true }));
        target.dispatchEvent(new Event('change', { bubbles: true }));
        return true;
    }

    function bindSmilieBoxClicks(smilieBox, textarea) {
        if (!smilieBox || smilieBox.dataset.redditSmileBound === '1') return;
        smilieBox.dataset.redditSmileBound = '1';
        smilieBox.addEventListener('click', event => {
            const smile = event.target?.closest?.('img[onclick*="addsmile"]');
            if (!smile || !smilieBox.contains(smile)) return;

            const onclick = smile.getAttribute('onclick') || '';
            const match = onclick.match(/addsmile\(([^)]+)\)/i);
            if (!match) return;

            event.preventDefault();
            event.stopPropagation();
            event.stopImmediatePropagation();
            const code = match[1].replace(/['"\s]/g, '');
            insertSmileCode(code, activeTextarea || textarea);
        }, true);
    }

    function getSmilieStorage() {
        let storage = document.getElementById('reddit-smilie-storage');
        if (!storage) {
            storage = document.createElement('div');
            storage.id = 'reddit-smilie-storage';
            document.body.appendChild(storage);
        }
        return storage;
    }

    function hideSmilieBox() {
        const smilieBox = document.getElementById('smiliebox');
        if (smilieBox) getSmilieStorage().appendChild(smilieBox);
        document.querySelectorAll('.reddit-emoji-container.active').forEach(container => {
            container.classList.remove('active');
        });
    }

    function restoreOriginalSmilieBox() {
        const smilieBox = document.getElementById('smiliebox');
        document.querySelectorAll('.reddit-emoji-container.active').forEach(container => {
            container.classList.remove('active');
        });
        if (!smilieBox || !originalSmilieParent) return;

        if (smilieBox.parentNode !== originalSmilieParent) {
            if (originalSmilieNextSibling && originalSmilieNextSibling.parentNode === originalSmilieParent) {
                originalSmilieParent.insertBefore(smilieBox, originalSmilieNextSibling);
            } else {
                originalSmilieParent.appendChild(smilieBox);
            }
        }
    }

    function showSmilieBoxFor(textarea, emojiContainer) {
        const smilieBox = document.getElementById('smiliebox');
        if (!smilieBox || !emojiContainer) return;

        if (emojiContainer.contains(smilieBox)) {
            hideSmilieBox();
            return;
        }

        activeTextarea = textarea;
        hideSmilieBox();
        emojiContainer.classList.add('active');
        emojiContainer.appendChild(smilieBox);
        bindSmilieBoxClicks(smilieBox, textarea);
        if (window.showDefault && !document.getElementById('menu_show')?.innerHTML) {
            window.showDefault();
        }
        textarea?.focus();
    }

    function bindReplyBox(replyBox, textarea, submitBtn) {
        const emojiBtn = replyBox.querySelector('.reddit-emoji-btn');
        const emojiContainer = replyBox.querySelector('.reddit-emoji-container');
        if (!emojiBtn || !emojiContainer) return;

        textarea.onfocus = () => { activeTextarea = textarea; };
        textarea.onclick = () => { activeTextarea = textarea; };
        emojiBtn.onmousedown = () => { activeTextarea = textarea; };
        emojiBtn.onclick = () => showSmilieBoxFor(textarea, emojiContainer);

        // 快捷键支持: Ctrl + Enter 提交
        textarea.addEventListener('keydown', (e) => {
            if (e.ctrlKey && e.key === 'Enter') {
                e.preventDefault();
                submitBtn.click();
            }
        });
    }

    function getOriginalTopicActionLinks() {
        const actionArea = document.querySelector('.t3');
        const scope = actionArea || document;
        const newTopicLink = scope.querySelector('a[href^="post.php?fid"]') || document.querySelector('#menu_post a[href^="post.php?fid"]');
        const replyLink = scope.querySelector('a[href*="action-reply"][href*="tid"]') || document.querySelector('a[href*="action-reply"][href*="tid"]');

        // 查找投票链接
        let pollLink = scope.querySelector('a[href*="special=1"], a[href*="special-1"]');
        if (!pollLink) {
            const menuPost = document.getElementById('menu_post');
            if (menuPost) {
                pollLink = Array.from(menuPost.querySelectorAll('a')).find(a => /投票/.test(a.innerText) || /special[=-]1/.test(a.href));
            }
        }

        return {
            newTopicHref: newTopicLink ? newTopicLink.href : '',
            replyHref: replyLink ? replyLink.href : '',
            pollHref: pollLink ? pollLink.href : ''
        };
    }

    function extractParentFloorFromText(text) {
        const normalized = (text || '').replace(/\u00a0/g, ' ');
        const match = normalized.match(/(?:回|回复|引用第?)\s*(楼主|\d+)\s*楼?/);
        if (!match) return null;
        return match[1] === '楼主' ? 0 : parseInt(match[1], 10);
    }

    function extractParentFloor(table) {
        // 优先从内容中的引用块提取,因为引用块包含最精确的回复对象信息
        const contentElement = table.querySelector('.tpc_content');
        if (contentElement) {
            const quoteElements = contentElement.querySelectorAll('.quote, blockquote, .blockquote2, .blockquote3');
            for (const quote of quoteElements) {
                const parentFloor = extractParentFloorFromText(getNodeText(quote));
                if (parentFloor !== null) return parentFloor;
            }
        }

        // 降级方案:从标题中提取(往往包含“回 楼主”等信息,但不够精确)
        const subject = table.querySelector('h1[id^="subject_"]');
        if (subject) {
            let parentFloor = extractParentFloorFromText(getNodeText(subject));
            if (parentFloor !== null) return parentFloor;
        }

        return null;
    }

    function findPostActionLink(table, postId, textPattern, hrefPattern) {
        const doc = table.ownerDocument;
        const areas = [table];
        if (postId) {
            const menu = doc.getElementById(`menu_read_${postId}`);
            if (menu) areas.push(menu);
        }

        for (const area of areas) {
            const links = area.querySelectorAll('a');
            for (const link of links) {
                const text = getNodeText(link);
                const title = link.getAttribute('title') || '';
                const href = link.getAttribute('href') || '';
                if (textPattern.test(text) || textPattern.test(title) || hrefPattern.test(href)) {
                    return {
                        href,
                        onclick: link.getAttribute('onclick') || ''
                    };
                }
            }
        }
        return null;
    }

    function getVoteMultiLimit(doc) {
        const scripts = Array.from(doc.querySelectorAll('script'));
        for (const script of scripts) {
            const text = script.textContent || '';
            const match = text.match(/var\s+multi\s*=\s*['"]?(\d+)['"]?/);
            if (match) return parseInt(match[1], 10) || 0;
        }
        return 0;
    }

    function getVoteHtml(doc) {
        const voteForm = doc.querySelector('form[name="vote"], form[action*="action=vote"]');
        const voteContent = voteForm?.querySelector('.tt2') || Array.from(doc.querySelectorAll('.tt2')).find(el => /投票|票数|人参与|本次投票/.test(getNodeText(el)));
        const source = voteForm && voteContent ? voteForm : voteContent;
        if (!source) return '';

        const clone = source.cloneNode(true);
        clone.querySelectorAll('script, style').forEach(el => el.remove());

        if (clone.tagName === 'FORM') {
            clone.classList.add('reddit-vote-form');
            clone.dataset.voteMulti = String(getVoteMultiLimit(doc));
            if (!clone.getAttribute('method')) clone.setAttribute('method', 'post');
        }

        return `<div class="reddit-vote-container">${clone.outerHTML}</div>`;
    }

    // --- 购买按钮点击处理 ---
    function handlePurchaseClick(event) {
        // 1. 寻找最接近的可能是按钮的元素
        const target = event.target.closest('input, button, a, span, div');
        if (!target) return;

        // 2. 向上寻找带有购买特征的元素
        const purchaseEl = target.closest('[data-reddit-onclick]') ||
            target.closest('[onclick*="buytopic"], [onclick*="sellcontent"], [onclick*="sendmsg"], [onclick*="location.href"]') ||
            target;
        const onclick = purchaseEl.getAttribute('data-reddit-onclick') || purchaseEl.getAttribute('onclick') || '';
        const text = (purchaseEl.value || purchaseEl.textContent || '').trim();

        // 精确匹配购买意图
        const isPurchaseBtn = (/购买|付钱|支付|buy|完成/i.test(text) && (['INPUT', 'BUTTON', 'A', 'SPAN', 'DIV'].includes(purchaseEl.tagName))) ||
            /sellcontent|buytopic|action=sell|buy_me/i.test(onclick) ||
            purchaseEl.hasAttribute('data-reddit-onclick');
        if (!isPurchaseBtn) return;

        // 不再强制要求在 .jumbotron 内,只要识别到购买意图即可拦截

        let requestUrl = '';
        let requestData = '';

        // 情况1: location.href 或 location.assign 跳转 (通常是免费购买)
        const locationMatch = onclick.match(/(?:window\.)?location(?:\.href)?\s*=\s*['"]([^'"]+)['"]/) ||
            onclick.match(/(?:window\.)?location\.assign\(['"]([^'"]+)['"]\)/);
        if (locationMatch) {
            requestUrl = locationMatch[1] || locationMatch[2];
        } else {
            const sendmsgMatch = onclick.match(/sendmsg\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]*)['"]/);
            if (sendmsgMatch) {
                requestUrl = sendmsgMatch[1];
                requestData = sendmsgMatch[2];
            }
        }

        if (!requestUrl) return;

        // 解码 HTML 实体 (例如 &amp; -> &)
        requestUrl = requestUrl.replace(/&amp;/g, '&');

        const postWrapper = target.closest('.reddit-post-wrapper');
        const floorNum = postWrapper ? postWrapper.dataset.floor : null;

        event.preventDefault();
        event.stopPropagation();
        event.stopImmediatePropagation();

        if (CONFIG.DEBUG) console.log('Purchase intercepted:', requestUrl, 'Floor:', floorNum);

        // 按钮状态反馈
        if (purchaseEl.tagName === 'INPUT') {
            purchaseEl.value = '处理中...';
            purchaseEl.disabled = true;
        }

        executeAsyncPurchase(requestUrl, requestData, floorNum, purchaseEl);
    }

    function executeAsyncPurchase(requestUrl, requestData, targetFloorNum = null, purchaseBtn = null) {
        // 确保URL是绝对路径
        if (requestUrl && !requestUrl.startsWith('http')) {
            requestUrl = new URL(requestUrl, window.location.href).href;
        }

        const requestMethod = requestData ? 'POST' : 'GET';

        const resetBtn = () => {
            if (purchaseBtn && purchaseBtn.tagName === 'INPUT') {
                purchaseBtn.value = '愿意购买,我买,我付钱';
                purchaseBtn.disabled = false;
            }
        };

        GM_xmlhttpRequest({
            method: requestMethod,
            url: requestUrl,
            data: requestData || undefined,
            headers: requestData ? {
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                'Referer': window.location.href
            } : {},
            timeout: 15000,
            onload: async function (res) {
                if (res.status < 200 || res.status >= 400) {
                    resetBtn();
                    alert('操作失败: ' + res.status);
                    return;
                }
                const responseText = res.responseText || '';
                if (/success|成功|完成|操作/.test(responseText) || responseText.trim().length === 0 || /ok/i.test(responseText)) {
                    setTimeout(async () => {
                        await refreshDataNoReload();
                    }, 600);
                } else if (/不足|不够|余额|SP币/.test(responseText)) {
                    resetBtn();
                    alert('购买失败:余额不足');
                } else {
                    setTimeout(() => refreshDataNoReload(), 600);
                }
            },
            onerror: () => { resetBtn(); alert('网络错误'); },
            ontimeout: () => { resetBtn(); alert('请求超时'); }
        });
    }

    function initVoteForms(container = redditContainer) {
        if (!container) return;

        container.querySelectorAll('.reddit-vote-form:not([data-reddit-vote-ready])').forEach(form => {
            form.dataset.redditVoteReady = '1';
            const limit = parseInt(form.dataset.voteMulti || '0', 10);
            if (limit <= 0) return;

            form.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(input => {
                input.addEventListener('change', () => {
                    if (input.type === 'radio') return;

                    const checked = Array.from(form.querySelectorAll('input[type="checkbox"]:checked'));
                    if (checked.length > limit) {
                        input.checked = false;
                        alert(`本投票最多可选 ${limit} 项`);
                    }
                });
            });
        });
    }

    // --- 数据解析逻辑 ---
    function parsePosts(doc) {
        const tables = doc.querySelectorAll('.t5, .read_t');
        tables.forEach(table => {
            const tiptop = table.querySelector('.tiptop');
            if (!tiptop) return;

            const floorText = getNodeText(tiptop);
            const floorMatch = floorText.match(/(楼主)|(\d+)楼|GF|B(\d+)F/);
            if (!floorMatch) return;

            let floorNum;
            if (floorMatch[1] || floorMatch[0] === 'GF') {
                floorNum = 0;
            } else if (floorMatch[3]) {
                floorNum = parseInt(floorMatch[3], 10);
            } else if (floorMatch[2]) {
                floorNum = parseInt(floorMatch[2], 10);
            } else {
                return;
            }

            const existingPost = allPosts.get(floorNum);

            const postIdElement = table.querySelector('th[id^="td_"], td[id^="td_"]');
            const postId = postIdElement ? postIdElement.id.replace(/^td_/, '') : '';

            const authorTags = table.querySelectorAll('a[href^="u.php?action-show"]');
            let author = '匿名';
            let authorHref = '#';
            let avatarUrl = '';
            for (const a of authorTags) {
                const img = a.querySelector('img');
                if (img && !avatarUrl) {
                    avatarUrl = img.src;
                }
                const text = getDisplayText(a);
                if (text && text !== '作者资料' && author === '匿名') {
                    author = text;
                    authorHref = a.href;
                }
            }
            if (!avatarUrl) avatarUrl = 'https://www.redditstatic.com/avatars/defaults/v2/avatar_default_1.png';

            // 提取主内容
            const contentElement = table.querySelector('.tpc_content');
            let content = contentElement ? contentElement.innerHTML : '';
            if (content.includes('buytopic') || content.includes('sellcontent') || content.includes('action=sell')) {
                // 安全改写:改名属性,防止原生跳转
                content = content.replace(/\bonclick\s*=/gi, 'data-reddit-onclick=');
            }

            // 提取附件和签名 (位于 tpc_content 之后,tipad 之前)
            if (contentElement) {
                let next = contentElement.nextElementSibling;
                while (next && !next.classList.contains('tipad') && !next.classList.contains('tiptop')) {
                    // 排除一些已知的干扰元素,但保留购买框 (jumbotron)
                    if (!next.classList.contains('gray') && (!next.tagName.startsWith('H') || next.classList.contains('jumbotron') || next.innerHTML.includes('buytopic'))) {
                        let html = next.outerHTML;
                        if (html.includes('buytopic') || html.includes('sellcontent') || html.includes('action=sell')) {
                            html = html.replace(/\bonclick\s*=/gi, 'data-reddit-onclick=');
                            if (!next.classList.contains('jumbotron')) {
                                html = `<div class="jumbotron">${html}</div>`;
                            }
                        }
                        content += html;
                    }
                    next = next.nextElementSibling;
                }
            }

            // 如果是楼主,提取投票表单
            if (floorNum === 0) {
                content += getVoteHtml(doc);
            }

            let time = '未知时间';
            const timeSpan = tiptop.querySelector('.gray[title]');
            if (timeSpan) {
                time = getNodeText(timeSpan);
            } else {
                const timeMatch = floorText.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/);
                if (timeMatch) time = timeMatch[0];
            }

            const parentFloor = extractParentFloor(table);

            // 识别冗余引用:如果引用块指向的是父楼层,则标记为冗余并隐藏
            // 使用临时 DOM 解析,确保能读取到引用块的完整文本内容
            if (parentFloor !== null && content) {
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = content;
                const quoteEls = tempDiv.querySelectorAll('.quote, .blockquote, .blockquote2, .blockquote3, blockquote');
                let modified = false;
                quoteEls.forEach(el => {
                    // 跳过购买框内的引用(jumbotron)
                    if (el.closest('.jumbotron')) return;
                    const textContent = (el.textContent || el.innerText || '').replace(/\u00a0/g, ' ').trim();
                    if (extractParentFloorFromText(textContent) === parentFloor) {
                        el.classList.add('redundant-quote');
                        modified = true;
                    }
                });
                if (modified) content = tempDiv.innerHTML;
            }

            const tipad = table.querySelector('.tipad');
            const replyBtn = tipad ? tipad.querySelector('.huifu a') : null;
            const replyOnclick = replyBtn ? replyBtn.getAttribute('onclick') : '';

            const quoteBtn = tipad ? tipad.querySelector('.yinyong a') : null;
            const quoteHref = quoteBtn ? quoteBtn.getAttribute('href') : '#';
            const quoteOnclick = quoteBtn ? quoteBtn.getAttribute('onclick') : '';
            const editLink = findPostActionLink(table, postId, /编辑/, /action[-=](?:modify|edit)/i);

            if (existingPost) {
                // 更新已有楼层的可变内容(处理购买后内容变化、编辑等)
                existingPost.content = content;
                existingPost.time = time;
            } else {
                allPosts.set(floorNum, {
                    id: floorNum,
                    postId,
                    author,
                    authorHref,
                    avatarUrl,
                    content,
                    time,
                    replyOnclick,
                    quoteHref,
                    quoteOnclick,
                    editHref: editLink ? editLink.href : '',
                    editOnclick: editLink ? editLink.onclick : '',
                    parentFloor,
                    children: []
                });
            }
        });
    }

    // --- 树结构构建 ---
    function buildTree() {
        rootPosts = [];
        // 使用 for...of 替代 forEach 减少闭包开销
        for (const post of allPosts.values()) {
            post.children = [];
        }

        const sortedFloors = Array.from(allPosts.keys()).sort((a, b) => a - b);
        for (let i = 0; i < sortedFloors.length; i++) {
            const floorNum = sortedFloors[i];
            const post = allPosts.get(floorNum);
            if (post.parentFloor !== null && allPosts.has(post.parentFloor) && post.parentFloor < floorNum) {
                allPosts.get(post.parentFloor).children.push(post);
            } else {
                rootPosts.push(post);
            }
        }
    }

    // --- 回复提交逻辑 ---
    function getTopicId() {
        const formTid = document.querySelector('form[name="FORM"] input[name="tid"]')?.value;
        if (formTid) return formTid;

        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('tid') || window.location.href.match(/tid-(\d+)/)?.[1] || '';
    }

    function buildReplyFormData(replyText) {
        const form = document.querySelector('form[name="FORM"]');
        const tid = getTopicId();
        if (!tid) throw new Error('无法识别的主题 ID (tid)');

        const formData = new URLSearchParams();
        if (form) {
            Array.from(form.elements).forEach(element => {
                if (!element.name || element.disabled) return;
                if (['file', 'submit', 'button', 'image', 'reset'].includes(element.type)) return;
                if (['checkbox', 'radio'].includes(element.type) && !element.checked) return;
                formData.append(element.name, element.value);
            });
        }

        formData.set('step', '2');
        formData.set('action', 'reply');
        formData.set('tid', tid);
        formData.set('atc_content', replyText);
        if (!formData.get('atc_title')) {
            formData.set('atc_title', document.querySelector('input[name="atc_title"]')?.value || 'Re:');
        }
        if (!formData.get('atc_autourl')) formData.set('atc_autourl', '1');
        formData.set('Submit', ' 提 交 ');

        return formData;
    }

    function getReplySubmitUrl() {
        const form = document.querySelector('form[name="FORM"]');
        return new URL(form?.getAttribute('action') || 'post.php?', window.location.href).href;
    }

    function getPostErrorMessage(responseText) {
        const text = (responseText || '').replace(/\s+/g, ' ');
        const titleError = text.match(/<title>\s*([^<]*(?:错误|失败)[^<]*)<\/title>/i);
        if (titleError) return titleError[1].trim();

        const alertError = text.match(/alert\(['"]([^'"]*(?:错误|失败|登录|权限|验证码|内容|标题|灌水)[^'"]*)['"]\)/i);
        if (alertError) return alertError[1].trim();

        const keywordError = text.match(/(请先登录|您还没有登录|权限不足|非法请求|验证码(?:错误|不正确)?|内容少于|标题不能为空|发帖间隔|灌水预防)/);
        return keywordError ? keywordError[1] : '';
    }

    function isReplySuccess(res) {
        const responseText = res.responseText || '';
        const responseUrl = res.finalUrl || res.responseURL || '';
        if (/read\.php.*(?:tid[=-])\d+/i.test(responseUrl)) return true;
        return /(?:发表成功|回复成功|发帖成功|操作完成|如果您的浏览器没有自动跳转)/.test(responseText);
    }

    function getFloorLabel(floorId) {
        return floorId === 0 ? '楼主' : `${floorId}楼`;
    }

    async function submitReply(post, text) {
        // 如果有post信息则构造引用,否则直接回复
        const replyText = post ? `[quote]回 ${getFloorLabel(post.id)}(${post.author}) 的帖子[/quote]\n${text}` : text;

        const formData = buildReplyFormData(replyText);

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: getReplySubmitUrl(),
                data: formData.toString(),
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                    'Referer': window.location.href
                },
                onload: (res) => {
                    if (res.status < 200 || res.status >= 400) {
                        reject(new Error(`服务器返回 HTTP ${res.status}`));
                        return;
                    }

                    const errorMessage = getPostErrorMessage(res.responseText);
                    if (errorMessage) {
                        reject(new Error(errorMessage));
                        return;
                    }

                    if (isReplySuccess(res)) {
                        resolve();
                    } else {
                        reject(new Error('服务器未返回明确的回复成功信息,请切回原始排版确认后再重试'));
                    }
                },
                onerror: () => reject(new Error('网络请求失败'))
            });
        });
    }

    // --- 渲染逻辑 ---
    function renderReddit() {
        if (isRedditMode) {
            hideSmilieBox();
        } else {
            restoreOriginalSmilieBox();
        }
        redditContainer.innerHTML = '';
        const fragment = document.createDocumentFragment();

        if (rootPosts.length === 0) {
            redditContainer.innerHTML = '<div style="text-align:center;padding:20px;">未发现可解析的内容。</div>';
            return;
        }

        // 渲染发帖标题与原始发帖/回帖入口
        const titleElement = document.querySelector('#subject_tpc');
        const titleText = titleElement ? (titleElement.innerText || titleElement.textContent || '').trim() : '';
        const topicLinks = getOriginalTopicActionLinks();
        if (titleText || topicLinks.replyHref || topicLinks.newTopicHref) {
            const titleRow = document.createElement('div');
            titleRow.className = 'reddit-title-row';

            const titleContainer = document.createElement('div');
            titleContainer.className = 'reddit-title-text';
            titleContainer.innerText = titleText;
            titleRow.appendChild(titleContainer);

            const actions = document.createElement('div');
            actions.className = 'reddit-topic-actions';
            if (topicLinks.replyHref) {
                const replyLink = document.createElement('a');
                replyLink.className = 'reddit-topic-btn';
                replyLink.href = topicLinks.replyHref;
                replyLink.innerText = '回帖';
                actions.appendChild(replyLink);
            }
            if (topicLinks.newTopicHref) {
                const dropdown = document.createElement('div');
                dropdown.className = 'reddit-post-dropdown';

                const mainBtn = document.createElement('a');
                mainBtn.className = 'reddit-topic-btn';
                mainBtn.href = topicLinks.newTopicHref;
                mainBtn.innerText = '发新帖';
                dropdown.appendChild(mainBtn);

                if (topicLinks.pollHref) {
                    const menu = document.createElement('div');
                    menu.className = 'reddit-post-menu';

                    const itemNew = document.createElement('a');
                    itemNew.className = 'reddit-post-menu-item';
                    itemNew.href = topicLinks.newTopicHref;
                    itemNew.innerText = '新 帖';
                    menu.appendChild(itemNew);

                    const itemPoll = document.createElement('a');
                    itemPoll.className = 'reddit-post-menu-item';
                    itemPoll.href = topicLinks.pollHref;
                    itemPoll.innerText = '投 票';
                    menu.appendChild(itemPoll);

                    dropdown.appendChild(menu);
                }

                actions.appendChild(dropdown);
            }
            titleRow.appendChild(actions);
            fragment.appendChild(titleRow);
        }

        rootPosts.forEach(post => {
            fragment.appendChild(createPostElement(post, 0));
        });

        // 渲染底部回复框
        const replyBox = document.createElement('div');
        replyBox.className = 'reddit-reply-box';
        replyBox.style.marginTop = '40px';
        replyBox.innerHTML = `
            <textarea class="reddit-reply-textarea" placeholder="写下你的回复..."></textarea>
            <div class="reddit-emoji-container"></div>
            <div class="reddit-reply-footer">
                <div class="reddit-reply-footer-actions">
                    <button type="button" class="reddit-emoji-btn" title="选择表情">
                        <svg style="width:20px;height:20px" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zM6.5 9a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm7 0a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm-7 4c1.11 0 2.06.67 2.48 1.61.1.2.33.39.52.39s.42-.19.52-.39A2.75 2.75 0 0113.5 13h-7z"></path></svg>
                    </button>
                </div>
                <div class="reddit-reply-footer-submit">
                    <button type="button" class="reddit-reply-btn reddit-reply-submit">回复</button>
                </div>
            </div>
        `;

        const textarea = replyBox.querySelector('textarea');
        const submitBtn = replyBox.querySelector('.reddit-reply-submit');
        bindReplyBox(replyBox, textarea, submitBtn);

        submitBtn.onclick = async () => {
            const text = textarea.value.trim();
            if (!text) return;

            submitBtn.disabled = true;
            submitBtn.innerText = '正在提交...';
            try {
                await submitReply(null, text);
                textarea.value = '';
                submitBtn.disabled = false;
                submitBtn.innerText = '回复';
                await refreshDataNoReload(); // 不传 'latest',保持当前滚动位置
            } catch (e) {
                alert('回复失败: ' + e.message);
                submitBtn.disabled = false;
                submitBtn.innerText = '回复';
            }
        };
        fragment.appendChild(replyBox);

        redditContainer.appendChild(fragment);
        initVoteForms();
    }

    function createPostElement(post, depth) {
        const wrapper = document.createElement('div');
        wrapper.className = 'reddit-post-wrapper';
        wrapper.dataset.floor = String(post.id);

        wrapper.innerHTML = `
            <div class="reddit-comment">
                <div class="reddit-comment-left">
                    <div class="reddit-avatar-wrapper">
                        <img src="${post.avatarUrl}" class="reddit-avatar" />
                    </div>
                    ${post.children.length > 0 ? `
                        <div class="reddit-threadline" title="折叠线程"></div>
                        <button type="button" class="reddit-collapse-btn" title="折叠评论">
                            <svg viewBox="0 0 20 20" fill="currentColor"><path d="M15 9H5v2h10V9z"></path></svg>
                        </button>
                    ` : ''}
                </div>
                <div class="reddit-comment-right">
                    <div class="reddit-comment-header">
                        <a href="${post.authorHref}" class="reddit-author" target="_blank">${post.author}</a>
                        <span class="reddit-time">• ${post.time}</span>
                        ${post.children.length > 0 ? `
                            <svg class="reddit-collapse-inline" viewBox="0 0 20 20" fill="currentColor"><path d="M15 9H5v2h10V9z"></path></svg>
                            <svg class="reddit-expand-inline" viewBox="0 0 20 20" fill="currentColor"><path d="M11 9h4v2h-4v4H9v-4H5V9h4V5h2v4z"></path></svg>
                        ` : ''}
                    </div>
                    <div class="reddit-comment-body">${post.content}</div>
                    <div class="reddit-comment-actions">
                        <button type="button" class="reddit-action-btn reddit-reply-toggle-btn">
                            <svg class="reddit-action-icon" viewBox="0 0 20 20"><path d="M15.429 16.353A8.44 8.44 0 0 1 9.07 18.5a8.775 8.775 0 0 1-4.704-1.36 1.488 1.488 0 0 1-.582-1.688l.685-2.228-1.579-1.365A6.99 6.99 0 0 1 1.5 6.5C1.5 2.915 5.314.5 10 .5s8.5 2.415 8.5 6c0 3.824-3.528 7.306-3.071 9.853Zm-1.89-1.257c-.198-1.128.535-4.227.535-4.227 1.636-1.536 3.176-3.14 3.176-4.869 0-2.824-3.142-4.75-7.25-4.75-4.108 0-7.25 1.926-7.25 4.75 0 2.224 1.691 3.99 4.316 4.542l.216.045-.065.212-.862 2.802a7.513 7.513 0 0 0 3.655-.918l.21-.112.213.109a7.195 7.195 0 0 0 5.106 2.416Z"></path></svg>
                            回复
                        </button>
                        ${post.editHref ? `
                            <a class="reddit-action-btn reddit-edit-btn" href="${post.editHref}">
                                <svg class="reddit-action-icon" viewBox="0 0 20 20"><path d="M15.9 2.7a2.1 2.1 0 0 0-3 0L4 11.6 3 17l5.4-1 8.9-8.9a2.1 2.1 0 0 0 0-3l-1.4-1.4ZM7.7 14.6l-2.8.5.5-2.8 6.6-6.6 2.3 2.3-6.6 6.6Zm7.7-7.7-2.3-2.3 1-1a.6.6 0 0 1 .8 0L16.3 5a.6.6 0 0 1 0 .8l-.9 1.1Z"></path></svg>
                                编辑
                            </a>
                        ` : ''}
                    </div>
                    <div class="reddit-reply-container"></div>
                </div>
            </div>
        `;

        const toggleCollapse = () => {
            wrapper.classList.toggle('collapsed');
        };
        const threadline = wrapper.querySelector('.reddit-threadline');
        if (threadline) threadline.onclick = toggleCollapse;

        const collapseBtn = wrapper.querySelector('.reddit-collapse-btn');
        if (collapseBtn) collapseBtn.onclick = toggleCollapse;

        const inlineCollapse = wrapper.querySelector('.reddit-collapse-inline');
        if (inlineCollapse) inlineCollapse.onclick = toggleCollapse;

        const inlineExpand = wrapper.querySelector('.reddit-expand-inline');
        if (inlineExpand) inlineExpand.onclick = toggleCollapse;

        // 回复功能逻辑
        const replyBtn = wrapper.querySelector('.reddit-reply-toggle-btn');
        const replyContainer = wrapper.querySelector('.reddit-reply-container');

        replyBtn.onclick = () => {
            if (replyContainer.innerHTML === '') {
                const replyBox = document.createElement('div');
                replyBox.className = 'reddit-reply-box';
                replyBox.innerHTML = `
                    <textarea class="reddit-reply-textarea" placeholder="写下你的回复..."></textarea>
                    <div class="reddit-emoji-container"></div>
                    <div class="reddit-reply-footer">
                        <div class="reddit-reply-footer-actions">
                            <button type="button" class="reddit-emoji-btn" title="选择表情">
                                <svg style="width:20px;height:20px" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zM6.5 9a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm7 0a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm-7 4c1.11 0 2.06.67 2.48 1.61.1.2.33.39.52.39s.42-.19.52-.39A2.75 2.75 0 0113.5 13h-7z"></path></svg>
                            </button>
                        </div>
                        <div class="reddit-reply-footer-submit">
                            <button type="button" class="reddit-reply-btn reddit-reply-cancel">取消</button>
                            <button type="button" class="reddit-reply-btn reddit-reply-submit">回复</button>
                        </div>
                    </div>
                `;

                const textarea = replyBox.querySelector('textarea');
                const submitBtn = replyBox.querySelector('.reddit-reply-submit');
                const cancelBtn = replyBox.querySelector('.reddit-reply-cancel');
                bindReplyBox(replyBox, textarea, submitBtn);

                // 自动聚焦
                setTimeout(() => textarea.focus(), 10);

                cancelBtn.onclick = () => {
                    hideSmilieBox();
                    replyContainer.innerHTML = '';
                };

                submitBtn.onclick = async () => {
                    const text = textarea.value.trim();
                    if (!text) return;

                    submitBtn.disabled = true;
                    submitBtn.innerText = '正在提交...';

                    try {
                        await submitReply(post, text);
                        hideSmilieBox();
                        replyContainer.innerHTML = '';
                        await refreshDataNoReload(); // 不传 'latest',保持当前滚动位置
                    } catch (e) {
                        alert('回复失败: ' + e.message);
                        submitBtn.disabled = false;
                        submitBtn.innerText = '回复';
                    }
                };

                replyContainer.appendChild(replyBox);
            } else {
                if (replyContainer.querySelector('#smiliebox')) hideSmilieBox();
                replyContainer.innerHTML = '';
            }
        };

        if (post.children.length > 0 && depth < CONFIG.NESTING_LIMIT) {
            const childrenDiv = document.createElement('div');
            childrenDiv.className = 'reddit-children';
            post.children.forEach(child => {
                childrenDiv.appendChild(createPostElement(child, depth + 1));
            });
            wrapper.appendChild(childrenDiv);
        }

        return wrapper;
    }

    // 启动
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();