Greasy Fork

妖火回帖引用显示(他到底说了啥)

在妖火论坛的回帖楼层中显示被引用楼层的内容,并可直接跳转到引用楼层

// ==UserScript==
// @name         妖火回帖引用显示(他到底说了啥)
// @namespace    https://www.yaohuo.me/bbs/userinfo.aspx?touserid=20740
// @version      1.1.6
// @description  在妖火论坛的回帖楼层中显示被引用楼层的内容,并可直接跳转到引用楼层
// @author       SiXi
// @match        https://www.yaohuo.me/bbs-*
// @match        https://www.yaohuo.me/bbs/book_*
// @match        https://yaohuo.me/bbs-*
// @match        https://yaohuo.me/bbs/book_*
// @icon         https://www.yaohuo.me/css/favicon.ico
// @license      Apache 2
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 存储已加载的楼层内容
    const loadedFloors = new Map();
    // 记录正在加载的状态
    let isLoading = false;
    // 记录加载尝试次数
    let loadAttempts = 0;
    const MAX_ATTEMPTS = 5;

    // 添加日志函数
    function log(message, data = '') {
        console.log(`[引用显示] ${message}`, data);
    }

    // 节流函数
    function throttle(func, limit) {
        let inThrottle;
        return function(...args) {
            if (!inThrottle) {
                func.apply(this, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        };
    }

    // 获取楼层描述
    function getFloorNumber(text) {
        const floorMap = {
            "沙发": 1,
            "椅子": 2,
            "板凳": 3
        };
        return floorMap[text] || parseInt(text);
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function waitForNewContent() {
        const startCount = document.querySelectorAll('.forum-post, .list-reply').length;
        log('开始等待新内容加载,当前楼层数:', startCount);
        let attempts = 0;
        const maxAttempts = 5;

        while (attempts < maxAttempts) {
            await sleep(800);
            const currentCount = document.querySelectorAll('.forum-post, .list-reply').length;
            log(`第${attempts + 1}次检查,当前楼层数:`, currentCount);

            if (currentCount > startCount) {
                log('检测到新内容');
                return true;
            }
            attempts++;
        }
        log('等待超时,未检测到新内容');
        return false;
    }

    // 获取引用内容和发帖妖友信息
    async function getQuotedContent(floorNumber, retryCount = 0) {
        log('开始获取引用内容,目标楼层:', floorNumber);

        if (loadedFloors.has(floorNumber)) {
            log('从缓存获取楼层内容');
            return loadedFloors.get(floorNumber);
        }

        if (isLoading) {
            log('其他请求正在加载中,等待重试');
            if (retryCount < 3) {
                await sleep(1000);
                return getQuotedContent(floorNumber, retryCount + 1);
            }
            // 返回错误信息时也使用对象格式
            return {
                text: '加载超时',
                isError: true
            };
        }

        const findFloorContent = () => {
            const floors = document.querySelectorAll('.forum-post, .list-reply');
            log('当前页面楼层数:', floors.length);

            for (const floor of floors) {
                const floorInfo = floor.querySelector('.floor-info, .floornumber');
                if (!floorInfo) continue;

                const currentFloor = getFloorNumber(floorInfo.textContent.replace(/[楼\s]/g, ''));
                if (currentFloor === floorNumber) {
                    log('找到目标楼层:', currentFloor);

                    // 修改内容获取逻辑
                    let content = '';
                    // 获取内容,优先使用 .retext
                    const contentElement = floor.querySelector('.retext') || floor.querySelector('.post-content');
                    if (contentElement) {
                        content = contentElement.innerHTML;
                        // 如果内容中包含引用,移除引用部分
                        const tempDiv = document.createElement('div');
                        tempDiv.innerHTML = content;
                        const quoteElement = tempDiv.querySelector('.replay-other, .reother');
                        if (quoteElement) {
                            quoteElement.remove();
                        }
                        content = tempDiv.innerHTML.trim();
                    }

                    // 获取用户信息(通常不会获取失败,以防万一设置初始值)
                    const userNick = floor.querySelector('.user-nick a, .renick a')?.textContent || '未知用户';
                    const userId = floor.querySelector('.user-id a, .reidlink .renickid')?.textContent.replace(/[()]/g, '') || '未知ID';

                    const result = {
                        userNick,
                        userId,
                        floorNumber: currentFloor,
                        content,
                        text: `<strong>${userNick}(${userId}) ${currentFloor}楼:</strong>${content}`
                    };
                    log('找到内容:', result);
                    return result;
                }
            }
            log('未找到目标楼层');
            return null;
        };

        let content = findFloorContent();
        if (content) {
            loadedFloors.set(floorNumber, content);
            return content;
        }

        if (loadAttempts >= MAX_ATTEMPTS) {
            log('达到最大尝试次数');
            return {
                text: '加载失败:已达到最大尝试次数',
                isError: true
            };
        }

        const loadMoreBtn = document.querySelector('#KL_show_tip, #YH_show_tip');// KL适用于book_re.aspx
        if (!loadMoreBtn || loadMoreBtn.textContent.includes('没有了')) {
            log('没有更多内容可加载');
            return {
                text: '未找到该楼层内容',
                isError: true
            };
        }

        log('点击加载更多按钮');
        isLoading = true;
        loadAttempts++;

        try {
            loadMoreBtn.click();
            const hasNewContent = await waitForNewContent();
            isLoading = false;

            if (!hasNewContent && loadAttempts >= MAX_ATTEMPTS) {
                log('加载新内容失败');
                return {
                    text: '加载失败:未能加载新内容',
                    isError: true
                };
            }

            log('递归查找内容');
            return await getQuotedContent(floorNumber);
        } catch (error) {
            console.error('加载失败:', error);
            isLoading = false;
            return {
                text: '加载失败',
                isError: true
            };
        } finally {
            isLoading = false;
        }
    }

    // 添加一个函数来给所有楼层添加锚点(跳转时使用)
    function addAnchorsToFloors() {
        const floors = document.querySelectorAll('.forum-post, .list-reply');
        floors.forEach(floor => {
            const floorInfo = floor.querySelector('.floor-info, .floornumber');
            if (!floorInfo) return;

            const floorNumber = getFloorNumber(floorInfo.textContent.replace(/[楼\s]/g, ''));
            if (!floor.id) {
                floor.id = `floor-${floorNumber}`;
                log(`添加锚点: floor-${floorNumber}`);
            }
        });
    }

    // 修改 scrollToFloor 函数
    function scrollToFloor(floorNumber) {
        // 查找目标楼层
        const floors = document.querySelectorAll('.forum-post, .list-reply');
        let targetFloor = null;

        // 遍历所有楼层查找目标
        for (const floor of floors) {
            const floorInfo = floor.querySelector('.floor-info, .floornumber');
            if (!floorInfo) continue;

            const currentFloor = getFloorNumber(floorInfo.textContent.replace(/[楼\s]/g, ''));
            if (currentFloor === floorNumber) {
                targetFloor = floor;
                break;
            }
        }

        if (targetFloor) {
            // 滚动方法
            targetFloor.scrollIntoView({ behavior: 'smooth', block: 'start' });

            // 添加目标楼层高亮效果
            const originalBackground = targetFloor.style.backgroundColor;
            targetFloor.style.backgroundColor = '#fff3cd';
            targetFloor.style.transition = 'background-color 0.3s';

            // 2秒后恢复默认的背景色
            setTimeout(() => {
                targetFloor.style.backgroundColor = originalBackground;
            }, 2000);

            return true;
        }
        return false;
    }

    // 添加内容截断函数
    function truncateContent(content) {
        const maxLength = 70;
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = content;

        // 处理图片缩放
        const images = tempDiv.getElementsByTagName('img');
        Array.from(images).forEach(img => {
            img.style.maxWidth = '200px';
            img.style.maxHeight = '120px';
            img.style.objectFit = 'contain';
        });

        const text = tempDiv.textContent;

        if (text.length <= maxLength) {
            return {
                content: content,
                fullContent: content,
                isShortened: false
            };
        }

        // 保存完整内容
        const fullContent = tempDiv.innerHTML;

        // 创建截断内容
        let truncatedContent = '';
        let currentLength = 0;

        function processNode(node) {
            if (currentLength >= maxLength) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const remainingLength = maxLength - currentLength;
                const text = node.textContent;
                truncatedContent += text.slice(0, remainingLength);
                currentLength += text.length;
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                const clone = node.cloneNode(true);
                if (node.tagName === 'IMG') {
                    truncatedContent += clone.outerHTML;
                } else {
                    truncatedContent += `<${node.tagName.toLowerCase()}>`;
                    Array.from(node.childNodes).forEach(child => {
                        if (currentLength < maxLength) {
                            processNode(child);
                        }
                    });
                    truncatedContent += `</${node.tagName.toLowerCase()}>`;
                }
            }
        }

        Array.from(tempDiv.childNodes).forEach(node => {
            if (currentLength < maxLength) {
                processNode(node);
            }
        });

        return {
            content: truncatedContent,
            fullContent: fullContent,
            isShortened: true
        };
    }

    // 添加图片缩放函数
    function resizeImages(container) {
        const images = container.getElementsByTagName('img');
        for (let img of images) {
            img.onload = function() {
                const ratio = Math.min(
                    120 / this.naturalHeight,
                    200 / this.naturalWidth,
                    1
                );

                if (ratio < 1) {
                    this.style.width = Math.floor(this.naturalWidth * ratio) + 'px';
                    this.style.height = Math.floor(this.naturalHeight * ratio) + 'px';
                }
            };

            // 如果图片已经加载完成,直接调整大小
            if (img.complete) {
                img.onload();
            }
        }
    }

    // 修改 createQuoteBox 函数
    function createQuoteBox(quoteData) {
        const box = document.createElement('div');
        box.className = 'quoted-content';

        const contentDiv = document.createElement('div');
        contentDiv.style.flex = '1';

        // 处理内容
        let content = '';
        if (typeof quoteData === 'string') {
            content = quoteData;
        } else {
            content = quoteData.text;
        }

        // 处理内容截断
        const truncated = truncateContent(content);
        contentDiv.innerHTML = truncated.content;

        // 处理图片缩放
        resizeImages(contentDiv);

        // 如果内容被截断,添加展开按钮
        if (truncated.isShortened) {
            const expandWrapper = document.createElement('span');
            expandWrapper.className = 'expand-wrapper';
            expandWrapper.innerHTML = '... ';

            const expandButton = document.createElement('button');
            expandButton.className = 'bt4 quote-expand-btn';
            expandButton.setAttribute('data-action', 'expand');
            expandButton.type = 'button';
            expandButton.textContent = '展开';
            expandButton.style.cssText = `
                margin-left: 5px;
                cursor: pointer;
                width: 50px;
                vertical-align: middle;
                position: relative;
                z-index: 999;
                pointer-events: auto;
            `;

            // 使用委托方式处理点击事件
            const expandHandler = function(event) {
                if (event.target.classList.contains('quote-expand-btn')) {
                    event.preventDefault();
                    event.stopPropagation();
                    event.stopImmediatePropagation();

                    try {
                        const parentContent = event.target.closest('.quoted-content');
                        if (parentContent) {
                            const contentDiv = parentContent.querySelector('div');
                            if (contentDiv) {
                                contentDiv.innerHTML = truncated.fullContent;
                                resizeImages(contentDiv);
                                event.target.closest('.expand-wrapper').remove();

                                // 移除事件监听器
                                document.removeEventListener('click', expandHandler, true);
                            }
                        }
                    } catch (error) {
                        console.error('展开内容时出错:', error);
                    }

                    return false;
                }
            };

            // 使用事件委托,将事件监听器添加到父元素
            document.addEventListener('click', expandHandler, true);

            expandWrapper.appendChild(expandButton);
            contentDiv.appendChild(expandWrapper);
        }

        const jumpButton = document.createElement('a');
        jumpButton.textContent = '跳转';
        jumpButton.href = '#floor-' + quoteData.floorNumber;
        jumpButton.style.cssText = `
            margin-left: 10px;
            color: #4CAF50;
            cursor: pointer;
            white-space: nowrap;
            font-size: 12px;
            text-decoration: none;
        `;

        // 如果是错误信息,隐藏跳转按钮
        if (quoteData.isError) {
            jumpButton.style.display = 'none';
        }

        // 修改跳转逻辑,使用更强的事件处理
        jumpButton.addEventListener('click', async function(e) {
            e.preventDefault();
            e.stopPropagation(); // 阻止事件冒泡

            if (typeof quoteData === 'object' && quoteData.floorNumber) {
                log('尝试跳转到楼层:', quoteData.floorNumber);

                // 尝试滚动到目标楼层
                if (!scrollToFloor(quoteData.floorNumber)) {
                    // 如果找不到目标楼层,尝试加载更多内容
                    const loadMoreBtn = document.querySelector('#KL_show_tip, #YH_show_tip');
                    if (loadMoreBtn && !loadMoreBtn.textContent.includes('没有了')) {
                        // 使用原生 alert 而不是自定义提示
                        window.alert('正在加载更多内容,请稍候...');

                        // 模拟真实的点击事件
                        const clickEvent = new MouseEvent('click', {
                            bubbles: true,
                            cancelable: true,
                            view: window
                        });
                        loadMoreBtn.dispatchEvent(clickEvent);

                        // 等待新内容加载
                        const contentLoaded = await waitForNewContent();
                        if (!contentLoaded) {
                            window.alert('加载超时,请手动点击加载更多');
                            return;
                        }

                        // 再次尝试滚动
                        let retryCount = 0;
                        const maxRetries = 3;

                        const tryScroll = async () => {
                            if (scrollToFloor(quoteData.floorNumber)) {
                                return;
                            }

                            if (retryCount < maxRetries) {
                                retryCount++;
                                await sleep(800);
                                tryScroll();
                            } else {
                                window.alert('未找到目标楼层,可能在其他页面');
                            }
                        };

                        await tryScroll();
                    } else {
                        window.alert('未找到目标楼层,可能在其他页面');
                    }
                }
            }
            return false; // 阻止默认行为
        }, true); // 使用捕获阶段

        // 创建按钮容器
        const buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex';
        buttonContainer.style.alignItems = 'center';
        buttonContainer.appendChild(jumpButton);

        box.style.cssText = `
            margin: 5px 0;
            padding: 8px;
            background-color: #f5f5f5;
            border-left: 3px solid #4CAF50;
            border-radius: 3px;
            font-size: 14px;
            color: #666;
            display: flex;
            align-items: flex-start;
            justify-content: space-between;
            margin-bottom: 10px;
        `;

        box.appendChild(contentDiv);
        box.appendChild(buttonContainer);
        return box;
    }

    // 修改 handleQuoteReplies 函数,在处理引用时也添加锚点
    const handleQuoteReplies = throttle(async () => {
        log('开始处理引用回复');
        addAnchorsToFloors();

        const replies = document.querySelectorAll('.forum-post, .list-reply');
        for (const reply of replies) {
            if (reply.dataset.processed) continue;

            const quoteElement = reply.querySelector('.replay-other, .reother');
            if (!quoteElement) continue;

            const quoteText = quoteElement.textContent;
            const floorMatch = quoteText.match(/回复(\d+)楼|回复(沙发|椅子|板凳)/);
            if (!floorMatch) continue;

            reply.dataset.processed = 'true';
            const floorNumber = getFloorNumber(floorMatch[1] || floorMatch[2]);
            log('处理引用,目标楼层:', floorNumber);

            try {
                const content = await getQuotedContent(floorNumber);
                if (!reply.querySelector('.quoted-content')) {
                    const quoteBox = createQuoteBox(content);
                    // 修改插入位置:确保引用内容显示在楼层内容的顶部
                    const replyContent = reply.querySelector('.retext') || reply.querySelector('.post-content');
                    if (replyContent) {
                        replyContent.insertBefore(quoteBox, replyContent.firstChild);
                    }
                }
            } catch (error) {
                console.error('处理引用内容时出错:', error);
            }
        }
    }, 500);

    // 优化后的MutationObserver
    const observer = new MutationObserver((mutations) => {
        const hasNewContent = mutations.some(mutation =>
            Array.from(mutation.addedNodes).some(node =>
                node.classList && (node.classList.contains('forum-post') || node.classList.contains('list-reply'))
            )
        );

        if (hasNewContent) {
            log('检测到新内容变化');
            handleQuoteReplies();
        }
    });

    // 限制观察范围,否则偶尔出现过度无限加载
    const container = document.querySelector('.forum-container, .recontent');
    if (container) {
        observer.observe(container, {
            childList: true,
            subtree: true
        });
    }

    // 初始处理
    handleQuoteReplies();
})();