您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
CC98 tools for previewing topic.
// ==UserScript== // @name CC98 Tools - Topic Preview // @version 1.0.1 // @description CC98 tools for previewing topic. // @icon https://www.cc98.org/static/98icon.ico // @author ml98 // @namespace https://www.cc98.org/user/name/ml98 // @license MIT // @match https://www.cc98.org/* // @match https://www-cc98-org-s.webvpn.zju.edu.cn:8001/* // @grant none // ==/UserScript== /* eslint-env jquery */ (async function () { if (typeof $ === 'undefined') { return; } const boardsInfo = JSON.parse(localStorage.boardsInfo?.slice(4) || "[]"); const boards = Object.fromEntries( boardsInfo .map((i) => i.boards) .flat() .map((i) => [i.id, i.name]) ); init(); function init() { const fragment = html(` <div id="topic-preview-container" class="hide"> <style></style> <header id="topic-preview-header"> <a id="topic-preview-title" target="_blank"></a> <a id="topic-preview-board" target="_blank"></a> </header> <div id="topic-preview-body"></div> <footer id="topic-preview-footer"> <button id="topic-preview-more" class="ant-btn ant-btn-primary">more</button> </footer> </div> `); document.documentElement.append(fragment); const container = document.querySelector("#topic-preview-container"); const button = container.querySelector("#topic-preview-more"); $(button).on("click", more); let timer1_id = 0; let timer2_id = 0; let timer3_id = 0; $(document.body) .on("mouseenter", "a", function (e) { if (this.href?.match(/topic\/\d+/)) { clearTimeout(timer2_id); clearTimeout(timer3_id); timer1_id = setTimeout(() => { container.classList.remove("hide"); const topicId = this.href.match(/topic\/(\d+)/)[1]; preview(topicId); }, 1000); } }) .on("mouseleave", "a", function (e) { if (this.href?.match(/topic\/\d+/)) { clearTimeout(timer1_id); timer2_id = setTimeout(() => { container.classList.add("hide"); }, 1500); } }); $(container) .on("mouseenter", function (e) { clearTimeout(timer2_id); clearTimeout(timer3_id); }) .on("mouseleave", function (e) { timer3_id = setTimeout(() => { container.classList.add("hide"); }, 1500); }); if(true) { container.querySelector("style").innerHTML = ` .focus-topic-title { width: fit-content; min-width: 1em; } /* container */ #topic-preview-container { /* left: 20%; right: 20%; top: 5%; bottom: 15%; border-radius: 12px; transform: translateY(0%); transition: 0.25s ease; */ left: 55%; right: 0%; top: 0%; bottom: 0%; border-radius: 12px 0 0 12px; transform: translateX(0%); transition: 0.25s ease; position: fixed; z-index: 10000000; background: white; padding: 20px; box-shadow: 0px 0px 12px 2px #0008; display: flex; flex-direction: column; } #topic-preview-container.hide { /* transform: translateY(-110%); */ transform: translateX(110%); } /* header */ #topic-preview-header { display: flex; margin-bottom: 10px; font-size: 1.25rem; } #topic-preview-title { flex: 1; } #topic-preview-board { margin-left: 10px; display: flex; align-items: center; } /* body */ #topic-preview-body { margin-bottom: 10px; flex: 1; overflow: auto; overscroll-behavior: none; } .topic-preview-post { margin: 10px; border-bottom: 3px dashed #0004; } .topic-preview-postInfo { display: flex; font-size: large; margin-bottom: 10px; } .topic-preview-userName { flex: 1; margin-left: 0.5em; } .topic-preview-content { display: block; line-height: normal; white-space: pre-wrap; overflow-wrap: break-word; } .topic-preview-content * { max-width: 100%; } .topic-preview-content img { margin-top: 10px; margin-bottom: 10px; border-radius: 4px; box-shadow: 0 0 5px 0px #0008; } .topic-preview-content img.topic-preview-emoji { display: inline-block; box-shadow: none; } .topic-preview-content blockquote { padding-left: 1em; max-height: 20em; overflow: auto; } .topic-preview-content>blockquote>blockquote>blockquote>blockquote { visibility: hidden; height: 2rem; } .topic-preview-content>blockquote>blockquote>blockquote>blockquote:before { visibility: visible; content: '...'; } .topic-preview-content iframe { border: none; } .topic-preview-awards { font-size: 0.5rem; text-align: center; } .topic-preview-like { display: flex; justify-content: flex-end; } .topic-preview-like > div { margin: 10px; } /* footer */ #topic-preview-footer { margin: auto; } /* webkit-scrollbar */ #topic-preview-body::-webkit-scrollbar { width: auto; height: auto; } #topic-preview-body::-webkit-scrollbar-track { background-color: #0001; } #topic-preview-body::-webkit-scrollbar-thumb { background-color: #8888; border-radius: 100vw; border: 5px solid #0000; background-clip: content-box; } #topic-preview-body::-webkit-scrollbar-thumb:hover { background-color: #888; } #topic-preview-body::-webkit-scrollbar-thumb:active { background-color: #666; } `; } } async function preview(topicId) { const container = document.querySelector("#topic-preview-container"); if (topicId == container.topicId) { return; } container.topicId = topicId; container.page = 0; const postContainer = container.querySelector("#topic-preview-body"); const title = container.querySelector("#topic-preview-title"); const board = container.querySelector("#topic-preview-board"); postContainer.innerHTML = ""; title.textContent = ""; board.textContent = ""; const topic = await getTopic(topicId); const posts = await getTopic(topicId, 0); title.href = "/topic/" + topicId; title.textContent = topic.title; board.href = "/board/" + topic.boardId; board.textContent = boards[topic.boardId]; // console.log(posts.length); if (posts.length == 10) { container.page++; } for (const post of posts) { postContainer.append(parsePost(post)); } } async function more() { const container = document.querySelector("#topic-preview-container"); const postContainer = container.querySelector("#topic-preview-body"); const posts = await getTopic(container.topicId, container.page); // console.log(container.page, posts.length); if (posts.length == 10) { container.page++; } while (postContainer.children.length % 10) { postContainer.removeChild(postContainer.lastChild); } for (const post of posts) { postContainer.append(parsePost(post)); } } function parsePost(post) { const userName = (post.isAnonymous ? "匿名" + post.userName.toUpperCase() : post.userName) + (post.isLZ ? " (LZ)" : ""); const page = Math.floor((post.floor - 1) / 10) + 1, floor = post.floor % 10; const content = parseUbb(post.content) + parseAwards(post.awards); const firstTime = parseTime(post.time); const lastTime = parseTime(post.lastUpdateTime); const time = firstTime + (lastTime ? " | " + lastTime : ""); return html(` <div class="topic-preview-post"> <div class="topic-preview-postInfo"> <div class="topic-preview-floor"> <a href="/topic/${post.topicId}/${page}#${floor}" target="_blank">#${post.floor}</a> </div> <div class="topic-preview-userName"> ${post.isAnonymous ? `${userName}` : `<a href="/user/id/${post.userId}" target="_blank">${userName}</a>` } </div> <div class="topic-preview-time">${time}</div> </div> <article class="topic-preview-content">${content}</article> <div class="topic-preview-like"> <div><i title="赞" class="fa fa-thumbs-o-up"></i> ${post.likeCount}</div> <div><i title="踩" class="fa fa-thumbs-o-down"></i> ${post.dislikeCount}</div> </div> </div> `); } function parseTime(time) { if (!time) { return ""; } const t = new Date(time), now = new Date(); return t.toLocaleDateString() == now.toLocaleDateString() ? t.toLocaleTimeString() : t.toLocaleString(); } function parseUbb(text) { if (!text) { return ""; } const emoji_base = '<img class="topic-preview-emoji" src="/static/images'; return text .replace(/\[ac(\d+)\]/gi, emoji_base + '/ac-dark/$1.png">') .replace(/\[a:(\d+)\]/gi, emoji_base + '/mahjong/animal2017/$1.png">') .replace(/\[c:(018|049|096)\]/gi, emoji_base + '/mahjong/carton2017/$1.gif">') .replace(/\[c:(\d+)\]/gi, emoji_base + '/mahjong/carton2017/$1.png">') .replace(/\[f:(004|009|056|061|062|087|115|120|137|168|169|175|206)\]/gi, emoji_base + '/mahjong/face2017/$1.gif">') .replace(/\[f:(\d+)\]/gi, emoji_base + '/mahjong/face2017/$1.png">') .replace(/\[(ms|tb)(\d+)\]/gi, emoji_base + '/$1/$1$2.png">') .replace(/\[cc98(1[5-9]|2\d|3[067])\]/gi, emoji_base + '/cc98/cc98$1.png">') .replace(/\[(em|cc98)(\d+)\]/gi, emoji_base + '/$1/$1$2.gif">') .replace(/\[img(=\d)?\](.+?)\[\/img\]/gi, '<img src="$2">') .replace(/\[url\](.+?)\[\/url\]/gi, '<a href="$1" target="_blank">$1</a>') .replace(/\[url=([^\]]+?)\]\[\/url\]/gi, '<a href="$1" target="_blank">$1</a>') .replace(/\[url=([^\]]+?)\](.+?)\[\/url\]/gi, '<a href="$1" target="_blank">$2</a>') .replace(/\[video\](.+?)\[\/video\]/gi, '<video controls src="$1"></video>') .replace(/\[audio\](.+?)\[\/audio\]/gi, '<audio controls src="$1"></audio>') .replace(/\[upload(=[^\]]+?)?\](.+?)\[\/upload\]/gi, '<a href="$2" target="_blank">$2</a>') .replace(/\[bili(=\d+)?\](https:\/\/www.bilibili.com\/video\/)?(BV.+?)\[\/bili\]/gi, '<iframe width="640" height="480" allowfullscreen ' + 'src="https://player.bilibili.com/player.html?bvid=$3&page$1"></iframe>') .replace(/\[size=(\d)\]/gi, '<span style="font-size:calc($1rem/3);">') .replace(/\[color=([^\]]+?)\]/gi, '<span style="color:$1;">') .replace(/\[font=([^\]]+?)\]/gi, '<span style=\'font-family:$1;\'>') .replace(/\[align=(left|center|right)\]/gi, '<span style="text-align:$1;display:block;">') .replace(/\[(left|center|right)\]/gi, '<span style="text-align:$1;display:block;">') .replace(/\[\/(size|color|font|align|left|center|right)\]/gi, '</span>') .replace(/\[(\/?)(u|b|i|del|code|table|thead|tbody|th|tr|td)\]/gi, '<$1$2>') .replace(/\[line\]/gi, '<br>') .replace(/\[(\/?)noubb\]/gi, '<$1code>') .replace(/\[(\/?)quotex?\]/gi, '<$1blockquote>'); } function parseAwards(awards) { if (!awards?.length) { return ""; } return `<br> <table class="topic-preview-awards"> <thead> <tr> <th>用户</th> <th>时间</th> <th>操作</th> <th>理由</th> </tr> </thead> <tbody>${awards.map(award=>` <tr> <td>${award.operatorName}</td> <td>${award.time.replace('T', ' ').split('.')[0]}</td> <td>${award.content}</td> <td>${award.reason}</td> </tr>`).join('')} </tbody> </table>`; } async function cc98fetch(url, data) { await sleep(500); try { const resp = await fetch("https://api-v2.cc98.org" + url, { ...data, headers: { authorization: localStorage.accessToken?.slice(4) || "", }, }); const json = await resp.json(); return json; } catch { return {}; } } async function getTopic(topicId, page) { if (page === undefined) { return await cc98fetch(`/topic/${topicId}`); } return await cc98fetch(`/topic/${topicId}/post?from=${page * 10}&size=10`); } async function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function html(s) { const t = document.createElement("template"); t.innerHTML = s.trim(); return sanitize(t.content); } function sanitize(fragment) { fragment.querySelectorAll("script").forEach((node) => node.remove()); fragment.querySelectorAll("*").forEach(function (node) { node.getAttributeNames() .filter((attr) => attr.startsWith("on")) .forEach((attr) => node.removeAttribute(attr)); }); return fragment; } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址