您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 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或关注我们的公众号极客氢云获取最新地址