V2EX快捷查看回复

V2EX快捷查看回复对象

  1. // ==UserScript==
  2. // @name V2EX快捷查看回复
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2.3
  5. // @description V2EX快捷查看回复对象
  6. // @author xiyue
  7. // @license MIT
  8. // @match https://v2ex.com/t/*
  9. // @match https://www.v2ex.com/t/*
  10. // @icon https://www.google.com/s2/favicons?domain=v2ex.com
  11. // @require https://unpkg.com/axios@0.25.0/dist/axios.min.js
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. "use strict";
  17. // 重写回复函数,给@后面增加楼层数
  18. replyOne = function (username) {
  19. setReplyBoxSticky();
  20. const replyContent = document.getElementById("reply_content");
  21. const oldContent = replyContent.value;
  22. const prefix = "@" + username + " #" + event.target.offsetParent.querySelector(".no").innerText;
  23. let newContent = "";
  24. if (oldContent.length > 0) {
  25. if (oldContent != prefix) {
  26. newContent = oldContent + "\n" + prefix;
  27. }
  28. } else {
  29. newContent = prefix;
  30. }
  31. replyContent.focus();
  32. replyContent.value = newContent;
  33. moveEnd(replyContent);
  34. };
  35.  
  36. let style = document.querySelector("style").sheet,
  37. replyWidth = 642, // 悬浮窗口大小
  38. svgIcon = `<svg t="1637731023724" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7157" width="16" height="16"><path d="M438.43 536.09m-41.15 0a41.15 41.15 0 1 0 82.3 0 41.15 41.15 0 1 0-82.3 0Z" p-id="7158" fill="#778087"></path><path d="M258.58 536.09m-41.15 0a41.15 41.15 0 1 0 82.3 0 41.15 41.15 0 1 0-82.3 0Z" p-id="7159" fill="#778087"></path><path d="M618.28 536.09m-41.15 0a41.15 41.15 0 1 0 82.3 0 41.15 41.15 0 1 0-82.3 0Z" p-id="7160" fill="#778087"></path><path d="M747.58 742.48c-0.17 0-1.07 0.67-3.09 2.82l0.32-0.2c0.61-0.53 1.24-1.05 1.8-1.6 0.33-0.33 0.65-0.68 0.97-1.02zM132.38 327l-0.32 0.21c-0.61 0.53-1.25 1-1.8 1.59-0.34 0.33-0.66 0.68-1 1 0.2 0.03 1.09-0.66 3.12-2.8zM279.54 171.77l-0.32 0.2c-0.61 0.53-1.24 1-1.8 1.59-0.34 0.33-0.65 0.68-1 1 0.2 0.05 1.1-0.64 3.12-2.79z" p-id="7161" fill="#778087"></path><path d="M955.14 161.78c-11.46-31.23-38.45-50.89-71.43-53.59-7.3-0.6-14.78-0.18-22.09-0.18H293.89c-10.3 0-20.75 1.35-30.33 5.2-30.19 12.13-50.92 39.18-51.37 72.32-0.31 23.6 0 47.23 0 70.84v6.86H152.5c-10.46 0-20.87 0.21-31 3.33C88 276.85 65.12 307.11 65 342.2c-0.07 24.74 0 49.48 0 74.22V720c0 11.47-0.28 23 3.43 34.09 8 24.05 25.75 41.29 49.18 50.33 10.13 3.91 21.11 4.68 31.88 4.68h88.23c16.82 22.9 33.62 45.81 50.14 68.92l16.68 23.32c6.31 8.82 14.56 14.84 25.88 14.84s19.57-6 25.88-14.84c20.52-28.7 41.19-57.18 62.32-85.42 1.49-2 3-4 4.46-6l89.39-0.83c25.54-0.24 51.09 0 76.63 0h141.45a88 88 0 0 0 39.52-9.34c26.66-13.2 41.76-41.75 41.76-70.75v-75.17h39c9.5 0 19 0.11 28.51 0 28.56-0.31 57.57-15.7 70.67-41.8a84.73 84.73 0 0 0 9-38.66V189.52c-0.01-9.34-0.64-18.91-3.87-27.74zM745.55 744.62c-0.24 0.17-0.5 0.32-0.74 0.48a12.36 12.36 0 0 0-1.11 1l0.79-0.85c-1.22 0.77-2.47 1.48-3.75 2.14A54.14 54.14 0 0 1 735 749c-8 0.54-16.09 0.08-24 0.08H534.32c-22.78 0-45.55 0.25-68.32 0.46l-54.45 0.51h-3.31c-11.55 0.66-19.13 5.89-25.88 14.85l-0.17 0.22c-0.52 0.68-1 1.37-1.53 2.05l-15.22 20.33q-17.69 23.63-35 47.49-18.23-25.26-36.65-50.38L278.61 764q-1.32-1.82-2.7-3.45a30.17 30.17 0 0 0-7.09-6.58l-0.44-0.3-0.29-0.18a27.08 27.08 0 0 0-7.4-3.27c-0.51-0.14-1-0.26-1.56-0.37a29.81 29.81 0 0 0-6.4-0.7h-97.27c-4.56 0-9.17 0.14-13.73-0.11a57.12 57.12 0 0 1-5.63-1.55q-2.06-1.07-4-2.32c-0.62-0.54-1.26-1-1.83-1.62s-1.2-1.24-1.78-1.87c-0.8-1.3-1.56-2.6-2.28-3.92-0.43-1.34-0.81-2.68-1.13-4.05-0.19-3.74-0.09-7.52-0.09-11.26V387c0-15.33-0.15-30.66 0-46 0-0.79 0-1.57 0.08-2.35 0.31-1.38 0.7-2.73 1.13-4.07 0.71-1.32 1.47-2.6 2.24-3.91 0.28-0.3 0.57-0.6 0.85-0.91-0.23 0 0.73-1.27 2-2.14l0.75-0.47c0.39-0.34 0.77-0.69 1.11-1.05l-0.79 0.84c1.22-0.77 2.47-1.48 3.74-2.14a57.12 57.12 0 0 1 5.63-1.55c5.74-0.35 11.57-0.11 17.28-0.11h502.66c23.34 0 46.68-0.11 70 0 1.14 0 2.28 0.05 3.42 0.11a55.61 55.61 0 0 1 5.65 1.56c1.37 0.71 2.71 1.47 4 2.31 0.61 0.54 1.26 1.06 1.83 1.62s1.19 1.24 1.77 1.87c0.8 1.3 1.57 2.6 2.29 3.93 0.42 1.33 0.81 2.67 1.12 4 0.19 3.75 0.09 7.52 0.09 11.26v335.46c0 15.32 0.16 30.66 0 46 0 0.79 0 1.57-0.07 2.36-0.32 1.37-0.7 2.73-1.13 4.07-0.72 1.31-1.47 2.6-2.25 3.9-0.28 0.31-0.56 0.61-0.85 0.91 0.27 0.02-0.69 1.25-1.99 2.12zM892.72 589.4c-0.25 0.17-0.5 0.32-0.75 0.48a13.87 13.87 0 0 0-1.11 1l0.79-0.85c-1.22 0.77-2.47 1.48-3.74 2.14a55.68 55.68 0 0 1-5.95 1.63c-11.38 0.88-23.14 0-34.45 0h-35.68v-244c0-16.06-1.72-31.4-9.56-46-14-26.11-42.76-40.59-71.74-40.65H272.14v-68.41c0-3.77-0.09-7.57 0.1-11.34a50.4 50.4 0 0 1 1.11-4c0.72-1.32 1.47-2.6 2.25-3.91l0.85-0.91c-0.23 0 0.73-1.27 2-2.14l0.75-0.47a14 14 0 0 0 1.11-1.05l-0.79 0.85c1.22-0.77 2.47-1.49 3.75-2.15a57.12 57.12 0 0 1 5.63-1.55c5.73-0.35 11.57-0.11 17.28-0.11h502.66c23.34 0 46.68-0.11 70 0 1.14 0 2.28 0 3.41 0.11a55.08 55.08 0 0 1 5.66 1.56c1.37 0.71 2.71 1.47 4 2.31 0.62 0.54 1.26 1.06 1.83 1.62s1.2 1.24 1.78 1.88c0.8 1.29 1.56 2.59 2.28 3.92 0.43 1.33 0.81 2.68 1.13 4 0.19 3.75 0.09 7.52 0.09 11.26V530c0 15.32 0.15 30.66 0 46 0 0.79 0 1.58-0.08 2.36-0.31 1.37-0.7 2.73-1.13 4.07-0.71 1.31-1.47 2.6-2.24 3.9-0.28 0.31-0.57 0.61-0.85 0.91 0.28 0.06-0.72 1.29-2 2.16z" p-id="7162" fill="#778087"></path><path d="M894.75 587.26c-0.18 0-1.08 0.67-3.1 2.82l0.32-0.2c0.61-0.53 1.25-1 1.8-1.6 0.34-0.28 0.66-0.67 0.98-1.02z" p-id="7163" fill="#778087"></path></svg>`,
  39. bgColor = window.getComputedStyle(document.querySelector("#Main .box"), null).backgroundColor,
  40. borderColor = window.getComputedStyle(document.querySelector(".cell"), null).borderBottomColor;
  41. style.insertRule(
  42. `.fixed-reply {
  43. transition: all 300ms;
  44. transform: translateY(-10px);
  45. pointer-events: none;
  46. opacity: 0;
  47. padding: 12px 20px;
  48. width: ${replyWidth}px;
  49. box-sizing: border-box;
  50. position: absolute;
  51. bottom: 30px;
  52. left: -14px;
  53. background: ${bgColor};
  54. border-radius: 8px;
  55. box-shadow: 0 0 18px rgb(0 0 0 / 10%);
  56. border: solid 1px ${borderColor};
  57. user-select: auto;
  58. }`,
  59. 1
  60. );
  61. style.insertRule(
  62. `.show-reply {
  63. position: relative;
  64. display: inline-flex;
  65. align-items: center;
  66. justify-content: flex-start;
  67. }`,
  68. 1
  69. );
  70. style.insertRule(
  71. `
  72. .show-reply:hover>.fixed-reply{
  73. transition: all 300ms;
  74. transform: translateY(0);
  75. pointer-events: auto;
  76. opacity: 1;
  77. }`,
  78. 1
  79. );
  80. style.insertRule(
  81. `.show-reply:hover:before {
  82. content: "";
  83. position: absolute;
  84. width: 160px;
  85. height: 10px;
  86. left: 0;
  87. bottom: 100%;
  88. }`,
  89. 1
  90. );
  91. style.insertRule(
  92. `.cell {
  93. transition:all 300ms;
  94. }`,
  95. 1
  96. );
  97. style.insertRule(
  98. `.cell.highlight {
  99. background-color: #FFE97F;
  100. }`,
  101. 1
  102. );
  103.  
  104. let replyList = [];
  105. let lastNum = 0;
  106.  
  107. function linkReply() {
  108. replyList = document.querySelectorAll(".reply_content");
  109. // 遍历回复列表
  110. replyList.forEach((el, index) => {
  111. let texts = el.parentNode.parentNode.querySelector(".no").innerText * 1;
  112. if (lastNum > texts) {
  113. console.error("评论顺序加载错误!", lastNum, texts);
  114. } else {
  115. lastNum = texts;
  116. }
  117.  
  118. // 获取所有@
  119. el.querySelectorAll(".reply_content a").forEach((atEl) => {
  120. let quoteIndex = getIndex(el, atEl, index);
  121. let replyEl = getContent(atEl.innerText, quoteIndex, index);
  122. if (replyEl) {
  123. let tempNode = document.createElement("div");
  124. tempNode.className = "show-reply";
  125. tempNode.appendChild(replyEl);
  126. tempNode.appendChild(document.createRange().createContextualFragment(svgIcon));
  127. atEl.parentNode.insertBefore(tempNode, atEl);
  128. replyEl.parentNode.insertBefore(atEl, replyEl);
  129. }
  130. });
  131. });
  132.  
  133. // 添加监听事件,点击楼层号跳转到对应楼层
  134. document.querySelectorAll(".reply_content").forEach((el) => {
  135. el.parentNode.parentNode.querySelector(".no").addEventListener("click", function (event) {
  136. event.preventDefault();
  137. event.stopPropagation();
  138. gotoReply(this.innerText);
  139. });
  140. });
  141. }
  142.  
  143. // 搜索楼层
  144. function getContent(userId, quoteIndex, maxIndex) {
  145. let lastContent = null;
  146. // 先尝试搜索引用楼层
  147. if (
  148. replyList[quoteIndex - 1] &&
  149. replyList[quoteIndex - 1].parentNode.querySelector("strong a").innerText === userId
  150. ) {
  151. lastContent = replyList[quoteIndex - 1].parentNode.parentNode;
  152. }
  153. // 如果第一个范围没有找到则搜索第二范围
  154. if (!lastContent) {
  155. for (var i = 0; i < maxIndex; i++) {
  156. if (replyList[i].parentNode.querySelector("strong a").innerText === userId) {
  157. lastContent = replyList[i].parentNode.parentNode;
  158. }
  159. }
  160. }
  161. if (lastContent) {
  162. var tempNode = document.createElement("div");
  163. tempNode.className = "fixed-reply";
  164. tempNode.addEventListener("click", function (event) {
  165. event.preventDefault();
  166. });
  167. tempNode.appendChild(lastContent.cloneNode(true)).querySelector("td:last-child").width = replyWidth - 48 - 10;
  168. tempNode.querySelector(".no").addEventListener("click", function (event) {
  169. event.preventDefault();
  170. event.stopPropagation();
  171. gotoReply(this.innerText);
  172. });
  173. return tempNode;
  174. } else {
  175. return false;
  176. }
  177. }
  178.  
  179. // 判断是否含有楼层号
  180. function getIndex(el, atEl, index) {
  181. let elStr = el.innerHTML,
  182. atElStr = atEl.innerHTML,
  183. newReg = new RegExp(`@<a href="/member/${atElStr}">${atElStr}</a>\\s+#\\d+`),
  184. regMatch = newReg.exec(elStr);
  185. if (regMatch) {
  186. let nums = regMatch[0].split("#")[1] * 1;
  187. return nums;
  188. } else {
  189. // 避免有人@自己导致引用出错
  190. return index - 1 >= 0 ? index - 1 : 0;
  191. }
  192. }
  193.  
  194. // 跳转到对应楼层
  195. function gotoReply(index, page = 1) {
  196. for (var i = replyList.length - 1; i >= 0; i--) {
  197. let el = replyList[i];
  198. // 如果没有找到指定楼层则跳转到最后一楼
  199. if (el.parentNode.querySelector(".no").innerText == index || index === replyList.length - 1) {
  200. var replaceTop = el.parentNode.parentNode.parentNode.parentNode.parentNode.getBoundingClientRect().top,
  201. repId = el.parentNode.parentNode.parentNode.parentNode.parentNode.id;
  202. window.scrollTo({ top: window.scrollY + replaceTop, behavior: "smooth" });
  203. document.querySelectorAll("#Main .cell[id]")[index - 1].className += " highlight";
  204. (() => {
  205. setTimeout(() => {
  206. document.querySelectorAll("#Main .cell[id]")[index - 1].className = "cell";
  207. }, 1500);
  208. })();
  209. history.pushState(
  210. null,
  211. null,
  212. `${window.location.origin}${window.location.pathname}${page === "1" ? "" : `?p=${page}`}#${repId}`
  213. );
  214. break;
  215. }
  216. }
  217. }
  218.  
  219. // 处理评论翻页
  220. let switchPage = document.querySelector(".page_normal");
  221. if (switchPage) {
  222. autoLoadNextPage(switchPage.parentNode.querySelectorAll(".page_normal"));
  223. } else {
  224. linkReply();
  225. }
  226.  
  227. async function autoLoadNextPage(pages) {
  228. // 楼层跳转链接添加监听事件
  229. let pageLink = document.querySelectorAll(".page_normal"),
  230. pageCurrent = document.querySelectorAll(".page_current");
  231.  
  232. var allEl = [];
  233. allEl.push.apply(allEl, pageLink);
  234. allEl.push.apply(allEl, pageCurrent);
  235.  
  236. allEl.forEach((el) => {
  237. el.addEventListener("click", function (event) {
  238. event.preventDefault();
  239. let clickPageNum = event.target.innerText;
  240. document.querySelectorAll(".page_current").forEach((el) => {
  241. el.className = "page_normal";
  242. });
  243. document.querySelectorAll(".page_normal").forEach((el) => {
  244. if (el.innerText === clickPageNum) {
  245. el.className = "page_current";
  246. }
  247. });
  248. let noNum = (clickPageNum - 1) * 100 + 1;
  249. gotoReply(noNum, clickPageNum);
  250. console.log(noNum);
  251. });
  252. });
  253.  
  254. // 异步加载其他回复页面数据
  255. for (let el of pages) {
  256. let req = await axios.get(el.getAttribute("href")),
  257. domData = req.data,
  258. template = document.createElement("template"),
  259. mainReplyEl = document.querySelectorAll("#Main .box .cell:last-child")[0];
  260. template.innerHTML = domData;
  261. // 判断插入位置
  262.  
  263. const tempFirstId =
  264. template.content.querySelector("#Main .box .cell[id]").getAttribute("id").replace(/\D/g, "") * 1;
  265. const cellEl = document.querySelectorAll("#Main .box .cell[id]");
  266.  
  267. for (const el of cellEl) {
  268. let listId = el.getAttribute("id").replace(/\D/g, "") * 1;
  269. if (listId > tempFirstId) {
  270. mainReplyEl = el;
  271. break;
  272. }
  273. }
  274.  
  275. template.content.querySelectorAll("#Main .box .cell[id]").forEach((el, index) => {
  276. mainReplyEl.parentNode.insertBefore(el, mainReplyEl);
  277. });
  278. }
  279. linkReply();
  280. }
  281.  
  282. // 增加处理图床url的逻辑
  283. function replaceImageBed() {
  284. const imgBedList = ["https://imgur.com", "https://i.imgur.com"];
  285. const findImgBed = (item) => {
  286. for (let i = 0; i < imgBedList.length; i++) {
  287. if (item.href.startsWith(imgBedList[i])) {
  288. return true;
  289. }
  290. }
  291. return false;
  292. };
  293.  
  294. document.querySelectorAll("a").forEach((item) => {
  295. if (findImgBed(item) && item.href === item.innerText) {
  296. console.log(item);
  297. const a = document.createElement("a");
  298. a.href = item.href;
  299. const img = document.createElement("img");
  300. img.src = item.href + ".png";
  301. a.appendChild(img);
  302. item.replaceWith(a);
  303. }
  304. });
  305. }
  306. replaceImageBed();
  307.  
  308. // 增加每日自动签到
  309. function autoSign() {
  310. if (document.querySelector(`[href="/mission/daily"]`)) {
  311. fetch("/mission/daily").then(async (res) => {
  312. if (res.status === 200) {
  313. console.log("签到成功");
  314. const domparse = new DOMParser();
  315. const doc = domparse.parseFromString(await res.text(), "text/html");
  316. const url = doc.querySelector(`[value="领取 X 铜币"]`).getAttribute("onclick");
  317. const regex = /location\.href\s*=\s*['"]([^'"]+)['"]/;
  318. const match = url.match(regex);
  319. const extractedUrl = match ? match[1] : null;
  320. if (extractedUrl) {
  321. fetch(extractedUrl).then((res) => {
  322. document.querySelector(`[href="/mission/daily"]`).innerText = "已签到";
  323. });
  324. }
  325. }
  326. });
  327. }
  328. }
  329. autoSign();
  330. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址