// ==UserScript== // @name [MT论坛]消息提醒预览 // @namespace https://github.com/qcxs/mtbbs // @version 2026-05-16 // @description 消息预览:避免查看回复频繁跳转网页,帖子回复查看:查看当前用户对某帖的回复 // @author 青春向上 // @match *://bbs.binmt.cc/home.php?mod=space&do=notice&view=mypost* // @match *://bbs.binmt.cc/home.php?*type=reply* // @icon https://bbs.binmt.cc/favicon.ico // @grant none // @run-at document-idline // ==/UserScript== (async function () { 'use strict'; // 全局配置 const CONFIG = { PROCESSED_MARK: 'mt-preview-processed', // 已处理标记 // 消息提醒 notice: { noticeSelector: '.comiis_notice_list>ul', // 消息列表外层容器选择器 selectContent: 'div.comiis_messages', // 消息内容DOM选择器 validCheck: 'a.lit[href*="goto=findpost"]', // li校验选择器 MAX_CACHE_COUNT: 100, // 本地缓存最大保存条数 CACHE_STORAGE_KEY: 'mt_bbs_preview_cache', // localStorage 缓存键名 REQUEST_DELAY: 100 // 消息请求队列间隔时间(毫秒) }, // 帖子回复查看 postReply: { threadSelector: '.comiis_forumlist>ul', // 帖子列表选择器 threadASelector: '.mmlist_li_box a', // 帖子标题链接选择器 validCheck: '.mmlist_li_box a', // li校验选择器 loadedMark: 'replyLoaded' // 加载状态标记(dataset驼峰命名) } }; // 页面判断 const urlObj = new URL(window.location.href); const searchParams = urlObj.searchParams; const phpFile = urlObj.pathname.split('/').pop(); let pageType = '未知页面' // 页面分流执行 if (phpFile === 'home.php' && searchParams.get('mod') === 'space' && searchParams.get('do') === 'notice') { await initNoticePreview(); pageType = '【消息提醒页】' } else if (phpFile === 'home.php' && searchParams.get('mod') === 'space' && searchParams.get('do') === 'thread' && searchParams.get('type') === 'reply') { initPostReply(); pageType = '【帖子回复页】' } console.log('当前页面类型:', pageType); // 通用工具函数 async function fetchReplyRoot(url, timeout = 3000) { try { const res = await fetch(url, { signal: AbortSignal.timeout(timeout) }); if (!res.ok) return ''; const xml = new DOMParser().parseFromString(await res.text(), 'text/xml'); return xml.querySelector('root')?.textContent || ''; } catch { return ''; } } /** * 通用列表动态监听 * @param containerSel 外层容器选择器 * @param itemSel 要监听新增的子项选择器 * @param callback 新增元素回调 */ function initCommonListObserver(containerSel, itemSel, validCheck, callback) { const container = document.querySelector(containerSel); if (!container) { console.log('监听失败,没有帖子!') return; } // 先初始化已有元素 document.querySelectorAll(itemSel).forEach(el => { if (validCheck(el)) callback(el) }); // 统一监听逻辑 const observer = new MutationObserver(muts => { muts.forEach(mut => { mut.addedNodes.forEach(node => { // 过滤:仅元素节点 + 匹配目标选择器 if (node.nodeType === 1 && node.matches(itemSel)) { if (validCheck(node)) callback(node); } }); }); }); observer.observe(container, { childList: true, subtree: false }); } // 功能一:消息预览 async function initNoticePreview() { const cfg = CONFIG.notice; // 只有发送fetch请求后,才写入list至localstorage,其余情况在内存中执行 let cacheList = []; try { const data = localStorage.getItem(cfg.CACHE_STORAGE_KEY); if (data) { const parsed = JSON.parse(data); // 必须是数组才使用,否则清空重建 if (Array.isArray(parsed)) { cacheList = parsed; } else { throw new Error('缓存格式有误,自动重置!') } } } catch (e) { console.log('发生错误:',e) // 旧版/损坏缓存:清空缓存 localStorage.removeItem(cfg.CACHE_STORAGE_KEY); cacheList = []; } const requestQueue = []; let isQueueRunning = false; initCommonListObserver(cfg.noticeSelector, `${cfg.noticeSelector}>li`, li => !li.hasAttribute(cfg.PROCESSED_MARK) && li.querySelector(cfg.validCheck), li => { if (!li.hasAttribute(CONFIG.PROCESSED_MARK)) { processNoticeItem(li); } }); function processNoticeItem(li) { li.setAttribute(CONFIG.PROCESSED_MARK, 'true'); const viewLink = li.querySelector(cfg.validCheck); if (!viewLink) return; const { tid, pid } = getTidPidFromUrl(viewLink.href); if (!tid || !pid) return; const cacheKey = `${tid}_${pid}`; const cacheData = cacheList.find(item => item.key === cacheKey); if (cacheData) { insertContentToLi(li, cacheData.content, cacheData.replyHref); } else { requestQueue.push({ li, tid, pid, cacheKey }); if (!isQueueRunning) runQueue(); } } // 请求队列,避免短时间内大量请求,容易封ip async function runQueue() { if (requestQueue.length === 0) { isQueueRunning = false; return; } isQueueRunning = true; const task = requestQueue.shift(); try { const url = `https://bbs.binmt.cc/forum.php?mod=viewthread&tid=${task.tid}&viewpid=${task.pid}&mobile=2&inajax=1`; const rootHtml = await fetchReplyRoot(url); const result = parseContentAndReplyHref(rootHtml); insertContentToLi(task.li, result.content, result.replyHref); if (result.content && !result.content.includes('失败') && !result.content.includes('[空内容]')) { cacheList = cacheList.filter(item => item.key !== task.cacheKey); cacheList.unshift({ key: task.cacheKey, ...result, time: Date.now() }); if (cacheList.length > cfg.MAX_CACHE_COUNT) cacheList = cacheList.slice(0, cfg.MAX_CACHE_COUNT); localStorage.setItem(cfg.CACHE_STORAGE_KEY, JSON.stringify(cacheList)); } } catch (e) { insertContentToLi(task.li, '[加载失败]', ''); } finally { setTimeout(runQueue, cfg.REQUEST_DELAY); } } function getTidPidFromUrl(url) { const p = new URLSearchParams(url); return { tid: p.get('ptid'), pid: p.get('pid') }; } // 解析回复内容、回复评论链接 function parseContentAndReplyHref(rootHtml) { try { if (!rootHtml) return { content: '[获取失败]', replyHref: '' }; const div = document.createElement('div'); div.innerHTML = rootHtml; const content = div.querySelector(cfg.selectContent)?.innerHTML?.trim() || '[空内容]'; const replyHref = div.querySelector('a[href*="action=reply"]')?.href || ''; div.remove(); return { content, replyHref }; } catch { return { content: '[解析失败]', replyHref: '' }; } } // 插入预览内容 function insertContentToLi(li, html, replyHref) { const box = document.createElement('div'); box.style.width = '100%'; li.appendChild(box); const root = box.attachShadow({ mode: 'open' }); root.innerHTML = `