V2EX 回复跳转工具

在 V2EX 上添加一键跳转到特定回复或楼层的功能,支持跨页面跳转和@用户跳转

// ==UserScript==
// @name         V2EX 回复跳转工具
// @namespace    http://tampermonkey.net/
// @version      0.7
// @description  在 V2EX 上添加一键跳转到特定回复或楼层的功能,支持跨页面跳转和@用户跳转
// @author       xiaomo1135
// @match        https://www.v2ex.com/t/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    
    // 存储要跳转的目标楼层
    function saveTargetFloor(floor) {
        localStorage.setItem('v2ex_target_floor', floor);
    }
    
    // 获取要跳转的目标楼层
    function getTargetFloor() {
        return localStorage.getItem('v2ex_target_floor');
    }
    
    // 清除目标楼层
    function clearTargetFloor() {
        localStorage.removeItem('v2ex_target_floor');
    }
    
    // 存储要跳转的目标用户名
    function saveTargetUsername(username) {
        localStorage.setItem('v2ex_target_username', username);
    }
    
    // 获取要跳转的目标用户名
    function getTargetUsername() {
        return localStorage.getItem('v2ex_target_username');
    }
    
    // 清除目标用户名
    function clearTargetUsername() {
        localStorage.removeItem('v2ex_target_username');
    }
    
    // 获取当前页面的回复元素
    function getReplies() {
        return document.querySelectorAll('#Main .box .cell[id^="r_"]');
    }
    
    // 获取总回复数
    function getTotalReplies() {
        let totalReplies = 0;
        
        // 方法1:从页面顶部文本获取
        const topicInfo = document.querySelector('.topic_buttons');
        if (topicInfo) {
            const prevElement = topicInfo.previousElementSibling;
            if (prevElement) {
                const match = prevElement.textContent.match(/(\d+)\s*[条個]回[复覆]/);
                if (match && match[1]) {
                    totalReplies = parseInt(match[1]);
                    console.log('从顶部信息获取到楼层数:', totalReplies);
                }
            }
        }
        
        // 方法2:从页面其他位置获取
        if (totalReplies === 0) {
            const allElements = document.querySelectorAll('div, span, h1, h2, h3, p');
            for (const element of allElements) {
                const match = element.textContent.match(/(\d+)\s*[条個]回[复覆]/);
                if (match && match[1]) {
                    totalReplies = parseInt(match[1]);
                    console.log('从页面元素获取到楼层数:', totalReplies, element);
                    break;
                }
            }
        }
        
        return totalReplies;
    }
    
    // 获取当前页码
    function getCurrentPage() {
        let currentPage = 1;
        const pageMatch = window.location.href.match(/\?p=(\d+)/);
        if (pageMatch && pageMatch[1]) {
            currentPage = parseInt(pageMatch[1]);
        }
        return currentPage;
    }
    
    // 获取每页回复数量
    function getRepliesPerPage() {
        return getReplies().length;
    }
    
    // 获取主题的总页数
    function getTotalPages() {
        // 从分页控件获取总页数
        const pagination = document.querySelector('.page_input');
        if (pagination) {
            return parseInt(pagination.getAttribute('max')) || 1;
        }
        
        // 如果没有分页控件,尝试从其他元素获取
        const pageLinks = document.querySelectorAll('.page_normal, .page_current');
        if (pageLinks.length > 0) {
            let maxPage = 1;
            pageLinks.forEach(link => {
                const pageNum = parseInt(link.textContent);
                if (!isNaN(pageNum) && pageNum > maxPage) {
                    maxPage = pageNum;
                }
            });
            return maxPage;
        }
        
        return 1; // 默认为1页
    }
    
    // 查找用户名对应的回复
    function findReplyByUsername(username) {
        const replies = getReplies();
        const currentPage = getCurrentPage();
        const repliesPerPage = replies.length;
        
        for (let i = 0; i < replies.length; i++) {
            const reply = replies[i];
            const usernameElement = reply.querySelector('strong a.dark');
            
            if (usernameElement && usernameElement.textContent.trim() === username) {
                // 计算楼层号
                const floorNum = (currentPage - 1) * repliesPerPage + i + 1;
                return { reply, floorNum };
            }
        }
        
        return null;
    }
    
    // 系统地搜索用户回复
    function searchUserReplySystematically(username) {
        // 获取总页数
        const totalPages = getTotalPages();
        const currentPage = getCurrentPage();
        
        // 已经搜索过的页面
        const searchedPages = JSON.parse(localStorage.getItem('v2ex_searched_pages') || '[]');
        
        // 将当前页添加到已搜索页面
        if (!searchedPages.includes(currentPage)) {
            searchedPages.push(currentPage);
            localStorage.setItem('v2ex_searched_pages', JSON.stringify(searchedPages));
        }
        
        // 查找下一个要搜索的页面
        let nextPage = null;
        for (let i = 1; i <= totalPages; i++) {
            if (!searchedPages.includes(i)) {
                nextPage = i;
                break;
            }
        }
        
        // 如果找到了下一个要搜索的页面,跳转到该页面
        if (nextPage !== null) {
            const baseUrl = window.location.href.split('?')[0];
            window.location.href = `${baseUrl}?p=${nextPage}`;
        } else {
            // 如果所有页面都已搜索,清除搜索状态
            clearTargetUsername();
            localStorage.removeItem('v2ex_searched_pages');
        }
    }
    
    // 页面加载完成后检查是否需要滚动到特定楼层或用户
    function checkAndScrollToTarget() {
        // 检查是否有目标楼层
        const targetFloor = getTargetFloor();
        if (targetFloor) {
            console.log('找到目标楼层:', targetFloor);
            
            // 清除存储的目标楼层,防止刷新页面后再次跳转
            clearTargetFloor();
            
            // 获取当前页面所有回复
            const allReplies = getReplies();
            
            // 获取当前页码和每页回复数
            const currentPage = getCurrentPage();
            const repliesPerPage = allReplies.length;
            
            // 计算目标楼层在当前页面的索引
            const floorNum = parseInt(targetFloor);
            
            // 修正:计算在当前页面的准确索引
            const firstFloorInCurrentPage = (currentPage - 1) * repliesPerPage + 1;
            const indexInCurrentPage = floorNum - firstFloorInCurrentPage;
            
            console.log('当前页码:', currentPage);
            console.log('每页回复数:', repliesPerPage);
            console.log('当前页第一个楼层:', firstFloorInCurrentPage);
            console.log('目标楼层在当前页的索引:', indexInCurrentPage);
            
            // 如果索引有效,滚动到目标楼层
            if (indexInCurrentPage >= 0 && indexInCurrentPage < allReplies.length) {
                const targetReply = allReplies[indexInCurrentPage];
                
                // 延迟执行滚动操作,确保页面完全加载
                setTimeout(() => {
                    console.log('滚动到目标楼层元素:', targetReply);
                    targetReply.scrollIntoView({ behavior: 'smooth' });
                    
                    // 高亮显示目标楼层
                    const originalBg = targetReply.style.backgroundColor;
                    targetReply.style.backgroundColor = '#fffbcc';
                    setTimeout(() => {
                        targetReply.style.backgroundColor = originalBg;
                    }, 2000);
                }, 1000);
            } else {
                console.error('无法在当前页找到目标楼层,索引超出范围');
            }
        }
        
        // 检查是否有目标用户名
        const targetUsername = getTargetUsername();
        if (targetUsername) {
            console.log('找到目标用户名:', targetUsername);
            
            // 查找用户名对应的回复
            const result = findReplyByUsername(targetUsername);
            
            if (result) {
                // 找到了用户回复,清除搜索状态
                clearTargetUsername();
                localStorage.removeItem('v2ex_searched_pages');
                
                const { reply, floorNum } = result;
                
                // 延迟执行滚动操作,确保页面完全加载
                setTimeout(() => {
                    console.log('滚动到目标用户回复:', reply);
                    reply.scrollIntoView({ behavior: 'smooth' });
                    
                    // 高亮显示目标楼层
                    const originalBg = reply.style.backgroundColor;
                    reply.style.backgroundColor = '#fffbcc';
                    setTimeout(() => {
                        reply.style.backgroundColor = originalBg;
                    }, 2000);
                }, 1000);
            } else {
                // 如果当前页面没有找到,继续系统地搜索其他页面
                console.log('当前页面未找到用户回复,继续搜索其他页面');
                searchUserReplySystematically(targetUsername);
            }
        }
    }
    
    // 添加@用户跳转功能
    function addAtUserJumpButtons() {
        // 查找所有回复内容
        const replyContents = document.querySelectorAll('.reply_content');
        
        replyContents.forEach(content => {
            // 查找所有@用户的文本
            const atMatches = content.innerHTML.match(/@<a href="\/member\/([^"]+)"[^>]*>([^<]+)<\/a>/g);
            
            if (atMatches) {
                // 为每个@用户添加跳转按钮
                atMatches.forEach(match => {
                    // 提取用户名
                    const usernameMatch = match.match(/@<a href="\/member\/([^"]+)"[^>]*>([^<]+)<\/a>/);
                    if (usernameMatch && usernameMatch[2]) {
                        const username = usernameMatch[2];
                        
                        // 创建一个新的HTML字符串,包含原始@用户链接和新的跳转按钮
                        const jumpButton = `<a href="javascript:void(0);" class="at-user-jump" data-username="${username}" style="margin-left:5px;font-size:12px;color:#778087;">[查看]</a>`;
                        
                        // 替换原始@用户文本
                        content.innerHTML = content.innerHTML.replace(match, match + jumpButton);
                    }
                });
                
                // 为新添加的跳转按钮绑定事件
                const jumpButtons = content.querySelectorAll('.at-user-jump');
                jumpButtons.forEach(button => {
                    button.addEventListener('click', function(e) {
                        e.preventDefault();
                        const username = this.getAttribute('data-username');
                        
                        // 查找用户名对应的回复
                        const result = findReplyByUsername(username);
                        
                        if (result) {
                            const { reply, floorNum } = result;
                            
                            // 滚动到目标回复
                            reply.scrollIntoView({ behavior: 'smooth' });
                            
                            // 高亮显示目标楼层
                            const originalBg = reply.style.backgroundColor;
                            reply.style.backgroundColor = '#fffbcc';
                            setTimeout(() => {
                                reply.style.backgroundColor = originalBg;
                            }, 2000);
                        } else {
                            // 如果当前页面没有找到,直接开始系统地搜索,不显示确认框
                            // 保存目标用户名
                            saveTargetUsername(username);
                            
                            // 初始化已搜索页面列表
                            localStorage.setItem('v2ex_searched_pages', JSON.stringify([getCurrentPage()]));
                            
                            // 从第1页开始系统地搜索
                            const baseUrl = window.location.href.split('?')[0];
                            window.location.href = `${baseUrl}?p=1`;
                        }
                    });
                });
            }
        });
    }
    
    // 添加跳转到指定楼层的功能 - 固定在右下角
    function addJumpToFloorFunction() {
        const totalReplies = getTotalReplies();
        
        // 创建固定在右下角的跳转控件
        const jumpDiv = document.createElement('div');
        jumpDiv.className = 'v2ex-floor-jump';
        
        // 设置固定定位样式
        jumpDiv.style.position = 'fixed';
        jumpDiv.style.bottom = '20px';
        jumpDiv.style.right = '20px';
        jumpDiv.style.zIndex = '1000';
        jumpDiv.style.padding = '10px';
        jumpDiv.style.backgroundColor = '#f9f9f9';
        jumpDiv.style.borderRadius = '5px';
        jumpDiv.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
        jumpDiv.style.textAlign = 'center';
        
        // 创建一个可折叠的容器
        const collapsibleDiv = document.createElement('div');
        collapsibleDiv.style.display = 'none'; // 默认折叠
        
        // 创建输入框
        const input = document.createElement('input');
        input.type = 'number';
        input.min = '1';
        input.max = totalReplies.toString();
        input.placeholder = '输入楼层数 (1-' + totalReplies + ')';
        input.style.width = '150px';
        input.style.marginRight = '10px';
        input.style.padding = '5px';
        input.style.marginBottom = '10px';
        
        // 创建跳转按钮
        const button = document.createElement('button');
        button.textContent = '跳转到楼层';
        button.style.padding = '5px 10px';
        button.style.display = 'block';
        button.style.margin = '0 auto';
        
        // 显示总楼层信息
        const infoSpan = document.createElement('span');
        infoSpan.style.display = 'block';
        infoSpan.style.marginTop = '5px';
        infoSpan.style.color = '#999';
        infoSpan.style.fontSize = '12px';
        infoSpan.textContent = totalReplies > 0 ? `共 ${totalReplies} 个楼层` : '无法获取楼层数';
        
        // 创建折叠/展开按钮
        const toggleButton = document.createElement('button');
        toggleButton.textContent = '楼层跳转 ▲';
        toggleButton.style.padding = '5px 10px';
        toggleButton.style.backgroundColor = '#e2e2e2';
        toggleButton.style.border = 'none';
        toggleButton.style.borderRadius = '3px';
        toggleButton.style.cursor = 'pointer';
        
        // 折叠/展开功能
        toggleButton.addEventListener('click', function() {
            if (collapsibleDiv.style.display === 'none') {
                collapsibleDiv.style.display = 'block';
                toggleButton.textContent = '楼层跳转 ▼';
            } else {
                collapsibleDiv.style.display = 'none';
                toggleButton.textContent = '楼层跳转 ▲';
            }
        });
        
        // 跳转按钮点击事件
        button.addEventListener('click', () => {
            const floor = parseInt(input.value);
            if (floor && floor > 0 && floor <= totalReplies) {
                // 计算目标楼层在哪一页
                const repliesPerPage = getRepliesPerPage();
                const targetPage = Math.ceil(floor / repliesPerPage);
                
                console.log('目标楼层:', floor);
                console.log('每页回复数:', repliesPerPage);
                console.log('目标页码:', targetPage);
                
                // 如果在当前页
                if (targetPage === getCurrentPage()) {
                    // 计算在当前页的位置
                    const firstFloorInCurrentPage = (targetPage - 1) * repliesPerPage + 1;
                    const indexInCurrentPage = floor - firstFloorInCurrentPage;
                    
                    console.log('当前页第一个楼层:', firstFloorInCurrentPage);
                    console.log('目标楼层在当前页的索引:', indexInCurrentPage);
                    
                    const allPosts = getReplies();
                    
                    if (indexInCurrentPage >= 0 && indexInCurrentPage < allPosts.length) {
                        const targetReply = allPosts[indexInCurrentPage];
                        if (targetReply) {
                            targetReply.scrollIntoView({ behavior: 'smooth' });
                            // 高亮显示目标楼层
                            const originalBg = targetReply.style.backgroundColor;
                            targetReply.style.backgroundColor = '#fffbcc';
                            setTimeout(() => {
                                targetReply.style.backgroundColor = originalBg;
                            }, 2000);
                        }
                    }
                } else {
                    // 需要跳转到其他页面
                    // 保存目标楼层,以便页面加载后滚动
                    saveTargetFloor(floor);
                    
                    // 跳转到目标页面
                    const baseUrl = window.location.href.split('?')[0];
                    window.location.href = `${baseUrl}?p=${targetPage}`;
                }
            }
        });
        
        // 组装DOM结构
        collapsibleDiv.appendChild(input);
        collapsibleDiv.appendChild(button);
        collapsibleDiv.appendChild(infoSpan);
        
        jumpDiv.appendChild(toggleButton);
        jumpDiv.appendChild(collapsibleDiv);
        
        // 添加到页面
        document.body.appendChild(jumpDiv);
    }
    
    // 为每个回复添加楼层标记和复制链接按钮
    function addFloorLabelsAndCopyButtons() {
        const replies = getReplies();
        const currentPage = getCurrentPage();
        const repliesPerPage = replies.length;
        
        replies.forEach((reply, index) => {
            // 计算当前回复的楼层号
            const floorNum = (currentPage - 1) * repliesPerPage + index + 1;
            
            // 添加楼层标记
            const floorLabel = document.createElement('div');
            floorLabel.textContent = `楼层: ${floorNum}`;
            floorLabel.style.color = '#999';
            floorLabel.style.fontSize = '12px';
            floorLabel.style.marginBottom = '5px';
            reply.insertBefore(floorLabel, reply.firstChild);
            
            // 创建复制链接按钮
            const jumpButton = document.createElement('a');
            jumpButton.textContent = '复制链接';
            jumpButton.href = 'javascript:void(0)';
            jumpButton.className = 'v2ex-jump-btn';
            jumpButton.style.marginLeft = '10px';
            jumpButton.style.fontSize = '12px';
            jumpButton.style.color = '#778087';
            
            // 获取回复ID
            if (reply.id) {
                const replyID = reply.id;
                
                // 点击事件 - 复制链接到剪贴板
                jumpButton.addEventListener('click', function(e) {
                    e.preventDefault();
                    const url = `${window.location.origin}${window.location.pathname}#${replyID}`;
                    navigator.clipboard.writeText(url).then(() => {
                        // 临时改变按钮文字提示已复制
                        const originalText = jumpButton.textContent;
                        jumpButton.textContent = '已复制!';
                        setTimeout(() => {
                            jumpButton.textContent = originalText;
                        }, 1000);
                    });
                });
                
                // 添加按钮到回复操作区域
                const replyActions = reply.querySelector('.fr');
                if (replyActions) {
                    replyActions.appendChild(jumpButton);
                }
            }
        });
    }
    
    // 页面加载完成后执行
    window.addEventListener('load', function() {
        console.log('V2EX 跳转工具已加载');
        
        // 检查是否需要滚动到特定楼层或用户
        checkAndScrollToTarget();
        
        // 为每个回复添加楼层标记和复制链接按钮
        addFloorLabelsAndCopyButtons();
        
        // 添加@用户跳转功能
        addAtUserJumpButtons();
        
        // 添加固定在右下角的跳转功能
        addJumpToFloorFunction();
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址