// ==UserScript== // @name [MT论坛]自动下一页 by:青春向上 // @namespace https://github.com/qcxs/mtbbs // @version 2025-11-22 // @description 集众多页面为一体,统一使用page参数控制分页。滚动加载下一页,手动加载上一页,页码跳转,地址栏无刷新更新。已适配:社区、导读、搜索、个人空间帖子/回复/留言、帖子评论、我的好友、积分明细、消息提醒。 // @author 青春向上 // @match *://bbs.binmt.cc/forum-*.html* // @match *://bbs.binmt.cc/forum.php?*fid=* // @match *://bbs.binmt.cc/forum.php?*mod=guide* // @match *://bbs.binmt.cc/search.php?*searchid=* // @match *://bbs.binmt.cc/home.php?*do=thread* // @match *://bbs.binmt.cc/forum.php?*tid=* // @match *://bbs.binmt.cc/*thread-*.html* // @match *://bbs.binmt.cc/home.php?*do=friend* // @match *://bbs.binmt.cc/home.php?*do=favorite* // @match *://bbs.binmt.cc/home.php?*do=following* // @match *://bbs.binmt.cc/home.php?*do=follower* // @match *://bbs.binmt.cc/home.php?*do=wall* // @match *://bbs.binmt.cc/home.php?*do=notice* // @match *://bbs.binmt.cc/home.php?*view=visitor* // @match *://bbs.binmt.cc/home.php?*view=trace* // @match *://bbs.binmt.cc/home.php?*ac=friend* // @match *://bbs.binmt.cc/home.php?*ac=credit* // @match *://bbs.binmt.cc/home.php?*view=blacklist* // @icon https://bbs.binmt.cc/favicon.ico // @grant none // @run-at document-end // ==/UserScript== (function () { 'use strict'; const ENUM = { //帖子列表 postListSelector: '.comiis_forumlist>ul', //评论列表 commentListSelector: '.comiis_postlist', //好友列表 friendsListSelector: '.comiis_userlist01', //留言列表 leaveMessageListSelector: '.comiis_plli', //积分明细 integralListSelector: '.comiis_credits_log>ul', //通知列表 noticeSelector: '.comiis_notice_list>ul', //我的收藏 favoriteSelector: '.comiis_mysclist>ul', //当无法从页码选择器中获取总页码时,取最大,会自动根据响应数据判断是否还有下一页 unknownPage: 999, //距离页面底部多少时,加载下一页,单位:像素 loadThreshold: 1500, //请求超时时间,3秒 requestTimeout: 3000, } const $ = window.jQuery; //页面没有jQuery,停止执行 if (!$) return; //模式,内容css选择器 const { mode, listSelector } = initFromUrl(); //当前页,总页数 let { currentPage, totalPage } = getPageInfo() let isLoading = false; let isFailed = false; // 节流函数+已监听元素缓存 const observedMarkers = new Set(); // 防重复监听 // 从url中分析页面 function initFromUrl() { let listSelector = ''; const url = new URL(window.location.href); const splitSegments = url.pathname.split('-'); let mode = ''; if (url.searchParams.get('fid') || splitSegments.length === 3 && splitSegments[0].endsWith('forum')) { // 情况1:社区,eg:forum-2-1.html mode = 'forum' listSelector = ENUM.postListSelector } else if (url.searchParams.get('mod') === 'guide') { // 情况2:导读 mode = 'guide'; listSelector = ENUM.postListSelector } else if (url.pathname === '/search.php') { // 情况3:搜索 mode = 'search'; listSelector = ENUM.postListSelector } else if (url.searchParams.get('do') === 'thread') { // 情况4:个人空间 mode = 'home'; listSelector = ENUM.postListSelector } else if (url.searchParams.get('tid') || splitSegments.length === 4 && splitSegments[0].endsWith('thread')) { // 情况5:帖子评论,eg:thread-156601-1-1.html mode = 'thread'; listSelector = ENUM.commentListSelector; } else if (url.searchParams.get('do') === 'friend' || url.searchParams.get('do') === 'following' || url.searchParams.get('do') === 'follower' || url.searchParams.get('view') === 'visitor' || url.searchParams.get('view') === 'trace' || url.searchParams.get('view') === 'blacklist' || url.searchParams.get('ac') === 'friend') { // 情况6:我的好友 mode = 'friends'; listSelector = ENUM.friendsListSelector } else if (url.searchParams.get('do') === 'wall') { // 情况7:个人空间-留言 mode = 'leaveMessage'; listSelector = ENUM.leaveMessageListSelector } else if (url.searchParams.get('ac') === 'credit') { // 情况8:积分明细 mode = 'integral'; listSelector = ENUM.integralListSelector } else if (url.searchParams.get('do') === 'notice') { // 情况9:消息提醒 mode = 'notice'; listSelector = ENUM.noticeSelector } else if (url.searchParams.get('do') === 'favorite') { // 情况10:我的收藏 mode = 'favorite'; listSelector = ENUM.favoriteSelector } console.log(`模式:${mode || '未找到'}`); return { mode, listSelector }; } function initBylistSelector() { if (listSelector == ENUM.postListSelector) { // 捕获阶段绑定全局click事件(关键:第三个参数true) document.addEventListener('click', function handleCaptureClick(e) { // 1. 判断点击元素是否有「class为forumlist_li的li父元素」(包括自身) const forumlistDiv = e.target.closest('div.mmlist_li_box'); if (!forumlistDiv) { return; // 无目标父元素,不拦截,执行原点击行为 } // 2. 找到该div下所有a标签,遍历匹配链接 const targetLinks = forumlistDiv.querySelectorAll('a'); let matchedUrl = null; // 正则表达式:匹配 thread-数字-任意字符.html 格式(支持 thread-xxxx-1-1.html 等变体) const threadReg = /thread-\d+.*\.html$/i; for (const link of targetLinks) { const href = link.getAttribute('href') || ''; // 3. 正则匹配目标链接格式 if (threadReg.test(href)) { matchedUrl = href; break; // 找到第一个匹配链接即可 } } // 4. 有匹配链接:新标签打开,拦截原行为;无匹配:不拦截 if (matchedUrl) { e.preventDefault(); // 阻止原跳转/默认行为 e.stopPropagation(); // 阻止事件传播(避免其他脚本触发) e.stopImmediatePropagation(); // 阻止同阶段其他监听者 window.open(matchedUrl, '_blank'); // 新标签打开页面 } }, true); console.log('新标签中打开网页') } else if (listSelector == ENUM.friendsListSelector || listSelector == ENUM.integralListSelector || listSelector == ENUM.noticeSelector || listSelector == ENUM.favoriteSelector) { //易出bug,限制在这些网页中 //阻止原局部刷新网页,避免编写初始化脚本 $(document).on('click', 'a', function (e) { //如果点击的是内容容器,则跳过 if (e.target.closest(listSelector)) return true; // 目标链接判断:可自定义条件(示例:href 不包含 "javascript:" 且需要正常跳转的链接) var href = $(this).attr('href'); if (href && href.indexOf('javascript:') === -1 && href.indexOf('#') === -1) { // 1. 阻止原事件冒泡(避免原代码的事件触发) e.stopImmediatePropagation(); // 2. 强制触发默认跳转(等同于正常点击链接) window.location.href = href; // 3. 阻止后续行为 e.preventDefault(); return false; } }); console.log('阻止默认局部刷新') } } // 解析响应内容 function parseResponse(root) { const htmlDom = $(root); //将其解析为网页 const $temp = $(`
${root}
`); //评论列表特殊,其余可通过选取返回的元素拼接 if (listSelector === ENUM.commentListSelector) { return htmlDom || ''; } else { return $temp.find(listSelector).html() || ''; } } // 构造跳转URL(统一修改page参数,保留所有原有参数) function buildJumpUrl(targetPage, inajax) { const url = new URL(window.location.href); url.searchParams.set('page', targetPage); if (inajax) { url.searchParams.set('inajax', '1'); } else { url.searchParams.delete('inajax'); } return url.toString(); } // 获取分页信息 function getPageInfo() { const $select = $('#dumppage'); //将unknownPage定义为无总页数 const totalPage = $select.length ? $select.find('option').length : ENUM.unknownPage; // 从URL参数获取当前page,优先级高于下拉框 const urlPage = new URL(window.location.href).searchParams.get('page'); //如果url中包含当前页,取其。否则取页码选择器,都没有则为第一页 let currentPage = parseInt(urlPage?.trim()) || ($select.length ? parseInt($select.find('option:selected').val()) : 1); //冗余设计,避免page属性异常(非整数、小于1) if (currentPage < 1) { currentPage = 1; } else if (!Number.isInteger(currentPage)) { currentPage = Math.floor(currentPage); } //打印日志 console.log(`分页:第${currentPage}/${totalPage}页`); return { currentPage, totalPage }; } // 添加分页标记(调整为li容器,适配帖子列表结构) function addPageMarker({ pageNum = currentPage, isLoadingState, errorObject, loadAll }) { const $postList = $(listSelector).first(); //只有当前页码为第一页时,不插入标签 if (!$postList.length || (currentPage == 1 && currentPage == pageNum)) return; // 移除加载中标记 $postList.find(`li:contains("加载中...")`).remove(); //有总页数则显示,否则不显示 const totalPageText = totalPage != ENUM.unknownPage ? `/共${totalPage}页` : ''; let prepend = pageNum <= currentPage; let marker = null; if (isLoadingState) { //加载中标记 marker = $(`
  • 第${pageNum}页 ${totalPageText} 加载中...
  • `) } else if (errorObject) { //加载失败标记 marker = $(`
  • 第${pageNum}页${totalPageText} 加载失败 点击重试

  • `) // 将 errorObject 转换为 JSON 字符串并截断 const errorMessage = typeof (errorObject == 'string') ? errorObject : JSON.stringify(errorObject); const truncatedErrorMessage = errorMessage.length > 200 ? + `${errorMessage.substring(0, 200)}......` : errorMessage; // 使用 .text() 将JSON字符串安全地插入到p标签中,避免因内容包含html而被错误解析 marker.find('p').text(truncatedErrorMessage); // 绑定重试事件 $(marker).on('click', '.retry-button', function () { isFailed = false; loadPage(pageNum); $(this).closest('.retry-marker').remove(); console.log(`尝试重新加载第${pageNum}页`) }); } else if (loadAll) { //全部加载标记 marker = $(`
  • 已全部 共${pageNum}页 加载 回到第1页
  • `) prepend = false; } else { //请求成功标记 marker = $(`
  • 第${pageNum}页${totalPageText} ${(prepend && pageNum != 1) ? `上一页` : ''}
  • `) // “上一页”点击事件 $(marker).on('click', '.loadPreNext', function () { if (isFailed) { alert('下一页加载发生错误,重试后才允许加载上一页。') return; } loadPage(pageNum - 1); $(this).remove(); console.log(`尝试加载第${pageNum - 1}页`) }); } // 是否为前插 if (prepend) { $postList.prepend(marker); } else { $postList.append(marker); } } // 全部加载完成事件 // 三种情况调用:页面加载时最后一页、请求了最后一页、请求结果表明没有最后一页 function allLoaded(pageNum = totalPage) { addPageMarker({ pageNum, loadAll: true }) //解绑滚动监听 $(window).off("scroll", handleScroll); console.log("所有页已全部加载,解除滚动监听。") } // 加载下一页(统一使用page参数请求) function loadPage(page) { if (isLoading || (page > totalPage || page < 1) || isFailed) return; addPageMarker({ pageNum: page, isLoadingState: true }); // 显示加载中标记 //格外添加inajax属性,返回root标签结果 const requestUrl = buildJumpUrl(page, true); isLoading = true; $.ajax({ type: 'GET', url: requestUrl, //返回数据自动按照xml进行解析,此时可直接获取root标签的内容 dataType: 'xml', timeout: ENUM.requestTimeout }).then(function (response) { // jquery版本过低,无法使用catch处理异常。使用 try...catch 包裹所有业务逻辑 try { const root = response.lastChild.firstChild.nodeValue if (typeof (root) == "undefined" || root == null) throw new Error('数据格式错误!'); const htmlContent = parseResponse(root); const $postList = $(listSelector).first(); // 导读无下一页时,会显示无主题。 // 个人空间无下一页时,解析返回为空或不包含下一页。但最后一页也不包含下一页,却仍有内容 // 但如果解析为空,也可能是解析失败 if (root.includes('本版块或指定的范围内尚无主题') || (!htmlContent && totalPage == ENUM.unknownPage)) { //下一页无数据,直接判定已全部加载 allLoaded(currentPage) return; } else if (!htmlContent) throw new Error('数据解析失败!') // 合并新页面帖子内容 if (page < currentPage) { $postList.prepend(htmlContent); addPageMarker({ pageNum: page }); } else { addPageMarker({ pageNum: page }); $postList.append(htmlContent); // 更新当前页 currentPage = page; } console.log(`加载成功:第${currentPage}/${totalPage}页`); // 为新增元素初始化点赞按钮,函数存在则调用,不存在则不执行 window.comiis_recommend_addkey?.(); // 为新增元素初始化关注按钮 window.comiis_user_gz_key?.(); //为新增元素初始化评论按钮 if (window.popup?.init) window.popup.init(); // 监听新增标签,当显示在网页时,代码上一页已阅读完毕 (function () { const $markers = $('.page-jump-link').closest('li'); // 有的是div,有的时li if (!$markers.length) return; $markers.each(function () { const dom = this; if (!observedMarkers.has(dom)) { //是当前页第一个元素,表示之前没有内容,故无已阅读之说,跳过 if ($(dom).is(':first-child')) return; observedMarkers.add(dom); observer.observe(dom); } }); })() if (page >= totalPage || !root.includes('下一页') && totalPage == ENUM.unknownPage) allLoaded(page); } catch (error) { return $.Deferred().reject(error); } }).then(null, function (e) { // 既能处理 AJAX 错误,也能处理上面手动 reject 的业务错误 try { console.error(`加载失败:第${page}页`, e); addPageMarker({ pageNum: page, errorObject: e }); // 显示失败重试标记 isFailed = true; } catch (error) { //当服务器返回的数据中包含script标签,此时jquery会尝试解析它,又有可能发生错误 //如果此时发生错误,always不会执行 console.log('处理异常时又发生异常', error) } }).always(function () { isLoading = false; }); } //滚动事件 function handleScroll() { if (isFailed || isLoading || currentPage >= totalPage) return; const scrollTop = $(window).scrollTop(); const windowHeight = $(window).height(); const docHeight = $(document).height(); // 滚动到阈值触发加载,预加载下一页 if (docHeight - (scrollTop + windowHeight) <= ENUM.loadThreshold) { loadPage(currentPage + 1); } } // 创建观察者实例 const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { const { target, isIntersecting } = entry; const jump = target.querySelector('.page-jump-link') if (jump == null) return; const markerText = jump.textContent?.trim(); const pageMatch = markerText.match(/第(\d+)页/); if (!pageMatch || !isIntersecting) return; //已阅读完上一页 const pageNum = parseInt(pageMatch[1]) - 1; const newUrl = buildJumpUrl(pageNum); if (window.location.href !== newUrl) { console.log('已阅读' + pageNum) window.history.replaceState({}, document.title, newUrl); } // 持续监听,当用户返回上一页时,可以更新为上一页,故不解除绑定 // observer.unobserve(target); }); }, { threshold: 0.8 // 显示80%触发,兼顾体验与准确性 }); // 初始化当前页标记(页面顶部显示当前页码) function initCurrentPage() { //前插标识 addPageMarker({ pageNum: currentPage }) // 隐藏原分页选择器 $('.comiis_page').css('display', 'none'); // 启动滚动加载监听(预加载下一页) $(window).scroll(handleScroll); console.log('滚动加载监听已启动,等待滚动加载下一页'); //检测是否全部加载 if (currentPage >= totalPage) allLoaded(totalPage); // 绑定页码跳转事件(事件委托,支持动态生成的标记) $(document).off('click', '.page-jump-link').on('click', '.page-jump-link', function () { const inputPage = prompt(`请输入要跳转的页码(1-${totalPage}):`, currentPage); if (!inputPage) return; // 取消输入 const targetPage = parseInt(inputPage.trim()); // 校验输入合法性 if (isNaN(targetPage) || targetPage < 1 || targetPage > totalPage) { alert(`请输入1-${totalPage}之间的有效数字!`); return; } // 跳转至目标页码 window.location.href = buildJumpUrl(targetPage); }); } // 脚本初始化入口 function init() { //根据不同网页进行不同格外操作 initBylistSelector() if (!$('.comiis_page').length) { console.log('无下一页,脚本终止') return; } if (!mode || !listSelector) { console.log('未适配的网页,脚本终止'); return; } // 为当前页进行初始化 initCurrentPage(); } // 启动脚本 init(); })();