您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
有时候用手机的浏览器打开百度贴吧,只想看一眼就走,并不想打开APP,这个脚本用于帮助用户自动展开楼层。注意:只支持手机浏览器,测试环境为Iceraven+Tampermonkey
当前为
// ==UserScript== // @name 手机百度贴吧自动展开楼层 // @namespace http://tampermonkey.net/ // @homepage http://greasyfork.icu/scripts/445657 // @version 2.0 // @description 有时候用手机的浏览器打开百度贴吧,只想看一眼就走,并不想打开APP,这个脚本用于帮助用户自动展开楼层。注意:只支持手机浏览器,测试环境为Iceraven+Tampermonkey // @author voeoc // @match https://tieba.baidu.com/p/* // @match https://jump2.bdimg.com/p/* // @match https://tiebac.baidu.com/p/* // @connect https://tieba.baidu.com/mg/o/getFloorData // @connect https://jump2.bdimg.com/mg/o/getFloorData // @connect https://tiebac.baidu.com/mg/o/getFloorData // @icon https://tieba.baidu.com/favicon.ico // @grant unsafeWindow // @grant GM_addStyle // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function () { 'use strict'; const DEBUGLOG_SWITCH = false; // 调试信息开关 const isFixedTitle = false; // 是否将标题置顶 const STR_NEWOPENLZLTEXT = "展开评论"; // 打开楼中楼按钮的文本 const STR_VOEOCMARK = "VOEOCMARK"; // 临时标记 const REG_URL_POSTPAGE = RegExp(`postPage\?(?=.*tid\=)(?=.*postAuthorId\=)(?=.*forumId\=)`, 'i'); // 评论页url正则 const REG_URL_FLOORREPLAYPAGE = RegExp(`lzlPage\?(?=.*floor\=)(?=.*pid\=)`, 'i'); // 楼中楼页url正则 const REG_URL_JSON_FLOORDATA = RegExp(`getFloorData\?(?=.*pn\=)(?=.*rn\=)(?=.*tid\=)(?=.*pid\=)`, 'i'); // 楼中楼json数据 const REG_URL_PBDATA = RegExp(`/getPbData\?(?=.*pn\=)(?=.*rn\=)(?=.*only_post\=)(?=.*kz\=)`, 'i'); // 评论页数据url正则 const PAGE_TYPE = { // 页面类型 UNKNOW: -1, // 未知 MAINPAGE: 0, // 主页 POSTPAGE: 1, // 评论页 FLOORREPLAYPAGE: 2, // 楼中楼页 }; let tie = undefined; // 一楼 let tiebaName = undefined; // 吧名 let lzId = ""; // 楼主id let currentHash = unsafeWindow.location.hash; // 存储当前页的Hash let oldHash = currentHash; // 存储上一链接的Hash let scrollPos = undefined; // 存储当前滚动位置 let floorDataList = {} // 存储所有楼层数据以便搜索,索引为楼层数字符串,值为抓取的json。主要信息为pid,获取路径floorDataList[floor].id let currentUrlInfo = { // 截取当前url host: "tieba.baidu.com", tid: "", postAuthorId: "", forumId: "", } function DEBUGLOG(msg, label = "") { if (!DEBUGLOG_SWITCH) { return; } let outputFunc = console.log; if (label == "error") { outputFunc = console.error; } outputFunc(`voeoc(DEBUG)<${label}>: ${msg}`); } function waitElementLoaded(selector, func, TIME_OUT = 30) { //const TIME_OUT = 30; // 找n次没有找到就放弃 let findTimeNum = 0; // 记录查找的次数 let timer = setInterval(() => { let element = document.querySelector(selector); DEBUGLOG(`${selector}=${element}`, "waitElementLoaded"); if (element != null) { // 清除定时器 clearInterval(timer); func(element); } else { findTimeNum++; if (TIME_OUT < findTimeNum) { // 清除定时器 clearInterval(timer); } } }, 200); } // 获取url参数 function getUrlAttr(url, attrName) { return RegExp(`${attrName}=([^&]*)&?`, 'i').exec(url)[1].trim(); } // 简单判断当前页面的类型 function getPageType(hash = unsafeWindow.location.hash) { if (hash === "" || hash === "#/") { return PAGE_TYPE.MAINPAGE; } else if (REG_URL_POSTPAGE.test(hash)) { return PAGE_TYPE.POSTPAGE; } else if (REG_URL_FLOORREPLAYPAGE.test(hash)) { return PAGE_TYPE.FLOORREPLAYPAGE; } return PAGE_TYPE.UNKNOW; } // 展开对应楼中楼 function openLzlPage(floorNum, floorElement, expandBtn) { // 另一种打开楼中楼的方法,但是需要页面切换 //let newHash = `#/lzlPage?tid=${currentUrlInfo.tid}&pid=${pid}&floor=${floorNum}&postAuthorId=${currentUrlInfo.postAuthorId}&forumId=${currentUrlInfo.forumId}`; //DEBUGLOG(newHash, "newHash"); //unsafeWindow.location.hash = newHash; const page_size = 20; // 单个页面评论数量,至少为10 let pid = floorDataList[floorNum].id; // 楼层id let current_page = parseInt(expandBtn.getAttribute("current_page")); // 当前页数 // 按钮动画 expandBtn.disabled = true; expandBtn.classList.add("loading"); function recoverExpandBtn() { expandBtn.disabled = false; expandBtn.classList.remove("loading"); } let url = `${unsafeWindow.origin}/mg/o/getFloorData?pn=${current_page}&rn=${page_size}&tid=${currentUrlInfo.tid}&pid=${pid}`; DEBUGLOG(url, "GM_xmlhttpRequest"); // 使用GM_xmlhttpRequest前需要添加对应url的@connect标记 GM_xmlhttpRequest({ method: "get", url: url, onload: function(details){ try { // 爬取解析楼中楼评论数据 let floorData = JSON.parse(details.responseText); let pageinfo = floorData.data.page; // 楼中楼信息,包括楼层数、页面大小、页面数量 let pageSelector = undefined; let subpostlist = floorData.data.sub_post_list; // 评论列表 // 加载楼中楼 let lzlParent = floorElement.querySelector("div.lzl-post"); let originItemList = lzlParent.getElementsByClassName("lzl-post-item"); let sampleItem = originItemList[0].cloneNode(true); // 获取dataset数据 let dvlist = [] for(let dv in originItemList[0].querySelector(".thread-text").dataset){ dvlist.push(`data-${dv}`); } const CONTENT_TYPE = { // 页面类型 TEXT: 0, // 文本 EMOJI: 2, // 表情 USERNAME: 4, // 用户名,一般用作回复 }; //lzlParent.innerHTML = ""; // 删除原有 // 去掉前两个评论 if(current_page == 1) { subpostlist.shift(); subpostlist.shift(); } subpostlist.forEach(function(subpost){ let allContent = ""; let userid = subpost.author.id; subpost.content.forEach(function(content){ let item = ""; switch(content.type) { case CONTENT_TYPE.EMOJI: item = `<img ${dvlist[0]}="" src="${content.src}" alt="${content.text}"class="emotion-img">` break; case CONTENT_TYPE.USERNAME: item = `<span ${dvlist[0]}="" class="user rich-link-disabled"> ${content.text} </span>` break; case CONTENT_TYPE.TEXT: default: // 如有其他的类型暂时用文本代替 item = `<span ${dvlist[0]}="" class="text-content">${content.text}</span>`; break; } allContent += item; }) let newItem = sampleItem.cloneNode(true); newItem.querySelector(".username").innerHTML = `${subpost.author.show_nickname} ${lzId == subpost.author.id ? `<svg ${dvlist[1]}="" class="landlord"><use xlink:href="#icon_landlord"></use></svg>`: ""}:`; newItem.querySelector(".thread-text").innerHTML = allContent; lzlParent.insertBefore(newItem, expandBtn.parentNode); if(parseInt(pageinfo.total_page) > current_page) { // 仍有剩余评论未展开 expandBtn.setAttribute("current_page", current_page + 1); } else { // 所有评论已展开 expandBtn.parentNode.style.display = "none"; } }); } finally { recoverExpandBtn(); } }, onerror: function(details) { recoverExpandBtn(); console.error(`无法加载评论区,爬取的url为${url}`); }, onabort: onerror, ontimeout: onerror, }); } // 检测URL Hash变化 function checkUrlHashChange() { let newHash = unsafeWindow.location.hash; if(currentHash != newHash) { currentHash = newHash; } else { return false; } let pageType = getPageType(); if (pageType == PAGE_TYPE.POSTPAGE) { // 收集url数据 currentUrlInfo = { host: unsafeWindow.location.hostname, tid: getUrlAttr(currentHash, "tid"), postAuthorId: getUrlAttr(currentHash, "postAuthorId"), forumId: getUrlAttr(currentHash, "forumId"), } // 当页面变动时,刷新展开楼层的按钮 waitElementLoaded(".post-page-list", (postpagelist) => { // 等待页面加载完成 // 监听页面改变事件的回调 function onNewFloorAdded(newFloor){ if(newFloor.classList.contains(STR_VOEOCMARK)) { // 已被打上标记 return; } newFloor.classList.add(STR_VOEOCMARK); let expandBtnParent = newFloor.querySelector(".open-app-guide"); // 楼中楼展开按钮 if(!expandBtnParent) { // 原来的展开楼中楼在App查看按钮不存在 return; } let floorinfo = newFloor.querySelector(".floor-info"); // 楼层数 if(!floorinfo) { // 楼层数获取失败 return; } // 爬取当前的楼层数 let floorNum = RegExp(`第([0-9]+)楼`, 'i').exec(floorinfo.innerHTML)[1].trim(); // 创建新按钮 let expandBtn = document.createElement( "span"); expandBtn.className = "open-app-text-real"; expandBtn.innerHTML = STR_NEWOPENLZLTEXT; expandBtn.setAttribute("current_page", 1); expandBtn.onclick = function() { openLzlPage(floorNum, newFloor, expandBtn); }; // 替换新按钮 expandBtnParent.insertBefore(expandBtn, expandBtnParent.children[0]); }; // 使用新按钮刷新楼层 function searchAndUpdatePostPage() { // 遍历所有楼层元素 let dlist = postpagelist.querySelectorAll(`div.post-item:not(.${STR_VOEOCMARK})`); dlist.forEach(onNewFloorAdded); } // 注册楼层元素添加事件 let config = { attributes: false, childList: true, characterData: false, subtree: false, }; let observer = new MutationObserver(function(mutationList) { searchAndUpdatePostPage(); }); observer.observe(postpagelist, config); searchAndUpdatePostPage(); }, 10); // 恢复一楼显示 restore(); // 页面切换后恢复滚动位置 scrollTo(scrollPos); } else if(pageType == PAGE_TYPE.MAINPAGE) { if(tie) { // 将剪切走的一楼复制回来 waitElementLoaded("#replySwitch", (splitline) => { // 等待页面加载完成 splitline.parentNode.insertBefore(tie, splitline); }, 10); scrollTo(0); } } return true; } // 滚动到指定y坐标(如果当前楼层数比较大,只能滚动到贴末尾的最大加载位置) function scrollTo(yPos) { waitElementLoaded(".post-page", (postpage) => { // 等待页面加载完成 document.documentElement.scrollTop = yPos; // 在一定时间内维持滚动位置 setTimeout(function () { document.documentElement.scrollTop = yPos; DEBUGLOG(yPos, "scrollTo"); }, 200); }); } // 显示一楼的内容 function restore() { DEBUGLOG("restore") waitElementLoaded(".text", (titletext) => { // 等待标题位置加载 // 显示贴吧名 try { let tiebaNameclone = tiebaName.cloneNode(true); titletext.parentNode.replaceChild(tiebaNameclone, titletext); // 关联点击贴吧名的事件 tiebaNameclone.onclick = function () { tiebaName.click(); } } catch (e) { console.error(e); } // 显示楼主发帖层 try { // 复原样式丢失 tie.style.cssText = ` margin-left: 0.12rem; margin-right: 0.12rem; margin-bottom: 0.25rem; ` // 尝试找回楼主丢失的头像 try { let lzavatar = tie.querySelector(".avatar"); lzavatar.style.backgroundImage = `url("${lzavatar.getAttribute("data-src")}")` } catch (e) { console.error(e); } // 尝试复原发帖内容的字体样式 try { let textContent = tie.querySelector(".thread-text"); textContent.style.cssText = ` margin-top: 0.18rem; font-size: 0.16rem; line-height: 0.28rem; ` } catch (e) { console.error(e); } let replySwitch = document.querySelector("#replySwitch"); // 标题下方的分割 replySwitch.parentNode.insertBefore(tie, replySwitch); } catch (e) { console.error(e); } // 尝试复原标题样式 try { let threadtitle = document.querySelector(".thread-title"); threadtitle.style.cssText = ` margin-bottom: 0.13rem; font-size: 0.22rem; font-weight: 700; line-height: 0.33rem; ` // 置顶标题显示 if (isFixedTitle) { let threadtitleclone = threadtitle.cloneNode(true) threadtitle.style.visibility = "hidden"; threadtitleclone.style.cssText += ` position: fixed !important; z-index: 99 !important; opacity: 0.8 !important; background-color: #FFFFFF !important; ` threadtitle.parentNode.insertBefore(threadtitleclone, threadtitle) } } catch (e) { console.error(e); } }) } (function main() { // 删减多余的打开APP的按钮,减少遮挡 GM_addStyle(` .comment-box, .only-lz, .nav-bar-bottom, .open-app, .more-image-desc { display: none !important; } `); // 隐藏原有的打开楼中楼App按钮 GM_addStyle(` .open-app-text { display: none !important; } .open-app-text-real { display: block !important; -webkit-box-flex: 0; -webkit-flex: none; -ms-flex: none; flex: none; font-size: .13rem; color: #614ec2; } @keyframes rotate { 0%{-webkit-transform:rotate3d(1, 0, 0, 0deg);} 25%{-webkit-transform:rotate3d(1, 0, 0, 90deg);} 50%{-webkit-transform:rotate3d(1, 0, 0, 180deg);} 75%{-webkit-transform:rotate3d(1, 0, 0, 270deg);} 100%{-webkit-transform:rotate3d(1, 0, 0, 360deg);} } .open-app-text-real.loading { animation: rotate 0.5s linear infinite; } `); (function() { let oldXHR = unsafeWindow.XMLHttpRequest; // 监听楼层加载的网络事件 unsafeWindow.XMLHttpRequest = function() { let realXHR = new oldXHR(); realXHR.addEventListener('readystatechange', function() { if(REG_URL_PBDATA.test(realXHR.responseURL) && realXHR.response != "") { let data = JSON.parse(realXHR.response).data; let post_list = data.post_list; // 获取楼主id try { lzId = post_list[0].author.id; } catch(e){console.error(e)} // 获取楼层信息 for (let i = 0; i < post_list.length; i++) { let d = post_list[i]; floorDataList[d.floor] = d; } } }, false); return realXHR; } })(); unsafeWindow.onscroll = function () { checkUrlHashChange(); if (getPageType() == PAGE_TYPE.POSTPAGE) { if(unsafeWindow.pageYOffset != 0) { // 记录当前滚动位置 scrollPos = unsafeWindow.pageYOffset; //DEBUGLOG(scrollPos, "scrollPos"); } } } unsafeWindow.onhashchange = function(e) { checkUrlHashChange(); } switch (getPageType()) { case PAGE_TYPE.MAINPAGE: waitElementLoaded(".post-page-entry-btn", (postbtn) => { tiebaName = document.querySelector(".forum-block"); // 获取吧名 tie = document.querySelector(".main-thread-content"); // 获取楼主发帖内容 // 点击展开按钮 postbtn.click(); // 手动触发页面刷新检测 checkUrlHashChange(); }) break; case PAGE_TYPE.POSTPAGE: // 如果当前刷新加载的是评论页,则需要先打开主页面获取数据 unsafeWindow.location.hash = ""; unsafeWindow.location.reload(); break; case PAGE_TYPE.FLOORREPLAYPAGE: break; default: break; } })() })();