V2EX_Good

V2EX Good,增强使用体验。

  1. // ==UserScript==
  2. // @name V2EX_Good
  3. // @description V2EX Good,增强使用体验。
  4. // @homepage https://greasyfork.org/zh-CN/scripts/3452
  5. // @namespace yfmx746qpx8vhhmrgzt9s4cijmejj3tn
  6. // @icon https://favicon.yandex.net/favicon/www.v2ex.com
  7. // @author ejin@v2ex
  8. // @match https://*.v2ex.com/*
  9. // @match https://v2ex.com/*
  10. // @version 2025.06.04
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. //版权申明:使用此脚本的片段,请标注来源,作者:ejin@v2ex
  15.  
  16. // 2025.05.07 帖子中的图片添加背景,解决偶尔看不到加载失败图片
  17. // 2025.05.03 按关键词屏蔽帖子中的垃圾回复。
  18. // 2025.04.03 签到页显示上次签到铜币数,余额页面显示签到页链接
  19. // 2025.04.03 各功能均改为异步执行,缩短脚本运行时间。
  20. // 2025.03.30 原生代码实现签到功能,去除对jQuery库的依赖
  21. // 2024.03.08 新消息界面,回复提醒对比感谢提醒更加醒目
  22. // 2024.01.16 新消息界面,显示消息序号,页码链接显示序号范围
  23. // 2023.12.27 避免链接转图片的大小超出布局
  24. // 2019.05.12 新浪的图片反防盗链
  25. // 2017.05.16 由于存储数据出错,改变存储数据的方式
  26. // 2016.05.25 链接自动转图片
  27. // 2016.05.21 新增召唤/呼叫管理员
  28. // 2016.04.12 在回复时可@所有人
  29. // 2015.10.16 新增在回复中标记楼主
  30. // 2014.01.24 初版修改版
  31.  
  32.  
  33. setTimeout(function(){
  34.  
  35. // 签到
  36. setTimeout(() => {
  37. if (document.querySelector('a.balance_area') && document.querySelector('a[href="/settings"]')) {//已登陆
  38. var username = document.querySelector('a[href^="/member/"]').innerHTML;
  39. var today=new Date().toISOString().split("T")[0];
  40. var infobar = document.querySelector('#search');
  41. if(localStorage.signdate==today && localStorage.signuser==username && infobar){
  42. return;//已签到就结束
  43. }
  44. var days=0;//连续登陆天数
  45. //开始签到流程
  46. fetch("/").then(()=>{
  47. //document.querySelector("#search").style.fontSize="14px";
  48. infobar.value = "正在检测每日签到状态...";
  49. return fetch("/mission/daily");//继续继续,前往领取页面
  50. })
  51. .then(rsp => rsp.text()).then(data=>{
  52. var parser = new DOMParser();
  53. var doc = parser.parseFromString(data, "text/html");
  54. if(doc.querySelector('input[value^="领取"]')){//领取按钮存在,尝试领取
  55. infobar.value = "正在领取签到奖励..."
  56. var url=doc.querySelector('input[value^="领取"]').getAttribute('onclick').split("'")[1];
  57. //<input type="button" class="xxx" value="领取 X 铜币" onclick="location.href = '/mission/daily/redeem?once=12345';">
  58. return fetch(url)//继续继续,提交领取动作
  59. } else {//按钮不存在
  60. if (data.indexOf("已领取") != -1) {
  61. localStorage.signdate=today;
  62. localStorage.signuser=username;
  63. throw new Error(infobar.value = "今天已经签到了。");
  64. } else {
  65. throw new Error(infobar.value = "无法识别领取奖励按钮 :( ");
  66. }
  67. }
  68. })
  69. .then(rsp => rsp.text()).then(data=>{
  70. days=data.split("已连续登")[1].split(" ")[1];//连续登陆天数
  71. localStorage.signdate=today;
  72. localStorage.signuser=username;
  73. //若是首页,签到入口隐藏
  74. if(document.querySelector('a[href="/mission/daily"]')){
  75. document.querySelector('a[href="/mission/daily"]').parentElement.parentElement.style.display="none";
  76. }
  77. return fetch("/balance");//继续继续,查看领取数量
  78. })
  79. .then(rsp => rsp.text()).then(data=>{
  80. if (data.indexOf("每日登录奖励")!== -1){
  81. var money=data.match(/每日登录奖励 \d+ 铜币/)[0].match(/\d+/)[0];
  82. localStorage.signmoney=money;
  83. console.log(infobar.value = "连续签到" + days + "天,本次" + money + "铜币");
  84. } else {
  85. console.log(infobar.value = "未能识别到领取");
  86. }
  87. })
  88. .catch(error => {
  89. console.error("Sign info:", error);
  90. if(typeof error=="string" && error.indexOf("已经签到") == -1) {
  91. infobar.value = "请手动领取今日的登录奖励!";
  92. }
  93. });//end fetch
  94. }//end 判断登陆状态
  95. }, 0);// end 签到
  96.  
  97. // 按关键词屏蔽帖子、个人资料中的垃圾回复
  98. setTimeout(() => {
  99. if(location.pathname.indexOf("/t/") === 0 || location.pathname.indexOf("/member/") === 0){
  100. var lowkeys = ["已 block", "已经 block", '龟男', '龟龟', '🐢'].map(key=>key.toLowerCase());
  101. var replys_html = document.body.innerHTML.toLowerCase();
  102. var check = lowkeys.some(key => replys_html.indexOf(key) != -1)
  103. var lowcount = 0;
  104. } else {
  105. return;
  106. }
  107. if (check) {
  108. // 帖子页面
  109. document.querySelectorAll('div[id^=r_]').forEach(ele => {
  110. var reply_html = ele.innerHTML.toLowerCase();
  111. lowkeys.some(key => {
  112. if (reply_html.indexOf(key) != -1) {
  113. ele.style.display = "none";
  114. lowcount++;
  115. return true;
  116. }
  117. });
  118. })
  119. if (lowcount > 0) {
  120. document.querySelector('div[class="fr topic_stats"]').innerHTML += "隐藏回复 " + lowcount + "&nbsp;";
  121. }
  122. // 个人资料页面
  123. document.querySelectorAll(".reply_content").forEach(ele=>{
  124. lowkeys.some(key => {
  125. if (ele.innerText.toLowerCase().indexOf(key) != -1) {
  126. ele.innerText="(含敏感词被屏蔽)";
  127. return true;
  128. }
  129. });
  130.  
  131. })
  132.  
  133. }
  134. }, 0);//end 按关键词屏蔽帖子中的垃圾回复
  135.  
  136. //帖子标记个别没有自动标记的管理员,回复所有人
  137. if (location.href.indexOf("/t/") != -1) {
  138. setTimeout(() => {
  139. var modarr=["Livid","Kai","Olivia","GordianZ","sparanoid","Tink","ano"];
  140. var modlist="@"+modarr.join(" @");//生成@所有管理员的列表
  141. var uname=document.getElementById("Rightbar").getElementsByTagName("a")[0].href.split("/member/")[1];//自己用户名
  142. //标记管理员,预存回复用户名列表
  143. var lzname=document.getElementById("Main").getElementsByClassName("avatar")[0].parentNode.href.split("/member/")[1];
  144. var allname='@'+lzname+' ';
  145. var all_elem = document.querySelectorAll('a[href^="/member"].dark');
  146. for(var i=0; i<all_elem.length; i++) {
  147. if (modlist.indexOf(all_elem[i].innerHTML)!= -1){
  148. if (document.getElementsByClassName("badges")[i].innerHTML.indexOf("mod") == -1){
  149. document.getElementsByClassName("badges")[i].innerHTML+='<div class="badge mod">MOD</div>';
  150. }
  151. }
  152. //为回复所有人做准备
  153. if ( uname != all_elem[i].innerHTML && all_elem[i].href.indexOf("/member/") != -1
  154. && all_elem[i].innerText == all_elem[i].innerHTML && allname.indexOf('@'+all_elem[i].innerHTML+' ') == -1 ) {
  155. allname+='@'+ all_elem[i].innerHTML+' ';
  156. }
  157. }
  158. if ( document.getElementById("reply_content") ) {
  159. document.getElementById("reply_content").parentNode.innerHTML
  160. +="&nbsp;&nbsp;&nbsp;&nbsp;<a href='javascript:;' onclick='if ( document.getElementById(\"reply_content\").value.indexOf(\""
  161. +allname+"\") == -1 ) {document.getElementById(\"reply_content\").value+=\"\\r\\n"+allname+"\"}'>@所有人</a>";
  162. if ( document.body.style.WebkitBoxShadow !== undefined ) {
  163. //允许调整回复框高度
  164. document.getElementById("reply_content").style.resize="vertical";
  165. }
  166. document.getElementById("reply_content").style.overflow="auto";
  167. document.getElementById("reply_content").parentNode.innerHTML
  168. +="&nbsp;&nbsp;&nbsp;&nbsp;<a href='javascript:;' onclick='if ( document.getElementById(\"reply_content\").value.indexOf(\""
  169. +modlist+"\") == -1 ) {document.getElementById(\"reply_content\").value+=\"\\r\\n"+modlist+"\"}'>@管理员</a>";
  170. }
  171. }, 0);// end setTimeout
  172. }// end 回复所有人,@管理员
  173.  
  174. // 帖子回复框增加快捷回复,提示广告贴应发在推广节点
  175. if (location.href.indexOf("/t/") != -1) {
  176. (function(){
  177. document.getElementById("reply_content").parentNode.innerHTML
  178. +="&nbsp;&nbsp;&nbsp;&nbsp;<a href='javascript:;' onclick='document.getElementById(\"reply_content\").value+=\"\\r\\n"+"@Livid 这贴明显是推广贴,却没有发在推广节点。"+"\"'>报告广告贴</a>";
  179. })()
  180. }// end 举报广告贴链接
  181.  
  182. // 图片链接自动转换成图片
  183. setTimeout(() => {
  184. document.querySelectorAll(".topic_content a, .reply_content a").forEach(a=>{
  185. var link=a.pathname;
  186. if( link.indexOf("v2ex.com/")==-1 && a.querySelector("img") !== null){
  187. if(a.querySelector("img")){
  188. a.querySelector("img").setAttribute("title",a.hostname);
  189. }
  190. return;
  191. }
  192. if (/\.(?:bmp|gif|jpg|jpeg|png|webp)$/i.test(link)){
  193. a.innerHTML = `<img src="${a.href}" title="图片来自 ${a.hostname}" style="max-width:98%" decoding="async" loading="lazy" />`;
  194. // decoding='async'异步解析图像,加快显示其他内容。loading='lazy'懒加载。
  195. // 排除v2ex链接,避免误判 例子(非图片) https://v2ex.com/i/Ve5X51qb.png
  196. }
  197. })
  198. }, 0);// end 图片链接自动转换成图片
  199.  
  200. //新浪图床的图片反防盗链
  201. setTimeout(() => {
  202. Array.from(document.images).forEach(ele=>{
  203. if (ele.src.indexOf(".sinaimg.cn")!=-1) {
  204. ele.setAttribute("referrerPolicy","no-referrer");
  205. ele.src="https://image.baidu.com/search/down?thumburl=https://baidu.com&url="+ele.src;
  206. }
  207. })
  208. }, 0);// end 新浪图床的图片反防盗链
  209.  
  210. // 在账户余额界面/明细界面的上方增加签到页面链接
  211. if ( location.pathname == '/balance') {
  212. setTimeout(() => {
  213. document.querySelectorAll('span[class="gray"]').forEach(ele=>{
  214. if(ele.parentElement.innerHTML.indexOf("当前账户余额") != -1){
  215. ele.parentElement.innerHTML+='<div><li class="fa fa-question-circle gray"><a href="/mission/daily" > 查看签到页面</a></li></div>'
  216. }
  217. });
  218. }, 0);// end setTimeout
  219. }
  220. //余额页面显示签到页面链接
  221.  
  222. //在签到页面显示了上次领取铜币数量
  223. if(location.pathname == "/mission/daily" && typeof localStorage.getItem("Signmoney") == 'string'){
  224. setTimeout(() => {
  225. if(localStorage.signuser == document.querySelector('a[href^="/member/"]').innerHTML)
  226. document.querySelectorAll('div[class="cell"]').forEach(ele=>{
  227. if(ele.innerHTML.indexOf("已连续登录") == 0 ){
  228. ele.innerHTML += ",最近一次领取了 "+localStorage.signmoney+" 铜币。";
  229. }
  230. })
  231. }, 0);// end setTimeout
  232. }//end 签到页上次领取铜币数量
  233.  
  234. // 新消息界面,显示消息序号,页码链接显示序号范围
  235. if (location.href.indexOf("/notifications") != -1){
  236. setTimeout(() => {
  237. var page_index=new URL(window.location.href).searchParams.get('p');
  238. var before_index=0;
  239. if(page_index!=null){
  240. before_index=(page_index-1)*50;
  241. }
  242. document.querySelectorAll("a[onclick^=delete]").forEach((ele,i)=>{
  243. var index_ele=document.createElement("span");
  244. index_ele.innerText=(i+1+before_index)+". ";
  245. ele.parentElement.insertBefore(index_ele,ele.parentElement.firstElementChild)
  246. })
  247. var allmsgcount=document.querySelectorAll(".header .gray")[0].innerText;//消息总数
  248. document.querySelectorAll(".page_current,.page_normal").forEach((ele)=>{
  249. var index_a=(ele.innerText-1)*50+1;
  250. var index_b=(ele.innerText-1)*50+50;
  251. var title_str=index_a+"-"+index_b;
  252. if(allmsgcount-index_a<50){
  253. title_str=index_a+"-"+allmsgcount;
  254. }
  255. ele.setAttribute("title",title_str)
  256. })
  257. }, 0);
  258. }// end 新消息界面,序号和翻页按钮优化
  259.  
  260. // 新消息界面,回复提醒对比感谢提醒更加醒目
  261. if (location.href.indexOf("/notifications") != -1){
  262. setTimeout(() => {
  263. if(document.querySelectorAll(".payload").length > 0){
  264. document.querySelectorAll(".payload").forEach((ele) => {
  265. if(ele.parentElement.innerText.indexOf("时提到了你") != -1
  266. || ele.parentElement.innerText.indexOf("里回复了你") != -1 ){
  267. //1、被人@提醒。2、回复我的主题提醒。
  268. ele.style.backgroundColor="#F9EA9A";
  269. }
  270. })
  271. }
  272. }, 0);// end setTimeout
  273. }// end 新消息界面优化
  274.  
  275. // 帖子中的图片添加背景,解决偶尔看不到加载失败图片
  276. if(location.pathname.indexOf("/t/") != -1){
  277. setTimeout(() => {
  278. var css=`
  279. .topic_content img,
  280. .reply_content img {
  281. min-width: 16px;
  282. min-height: 16px;
  283. background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' style='min-width: 16px; min-height: 16px; background-color: white;'%3E%3Crect x='0' y='0' width='16' height='16' fill='%23eee' stroke='%23ccc' stroke-width='1'/%3E%3Cpolygon points='3,12 5,8 9,11 13,6 13,12' fill='%23aaa'/%3E%3Ccircle cx='3' cy='3' r='2' fill='%23888'/%3E%3C/svg%3E");
  284. background-repeat: no-repeat;
  285. background-size: 16px 16px;
  286. display: inline-block;
  287. }
  288. `;
  289. var style=document.createElement('style');
  290. style.textContent = css;
  291. document.head.append(style);
  292. }, 0);
  293. }// end 帖子中的图片添加背景
  294.  
  295. //清理一些这样那样的东西
  296. if(new Date().toISOString().split("T")[0] != localStorage.cleardate){
  297. setTimeout(() => {
  298. for (var i = localStorage.length-1; i >= 0 ; i--) {
  299. if(localStorage.key(i).indexOf("lscache") == 0){
  300. localStorage.removeItem(localStorage.key(i));
  301. }
  302. }
  303. if(typeof localStorage.getItem("SigninInfo") == 'string'){
  304. localStorage.removeItem("SigninInfo");
  305. }
  306. }, 0);// end setTimeout
  307. }// end 清理东西
  308.  
  309. },0);// end