NodeSeek X

【原NodeSeek增强】自动签到、无缝翻页帖子评论、快捷回复、代码高亮、屏蔽用户、屏蔽帖子、楼主低等级提醒

当前为 2025-06-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NodeSeek X
  3. // @namespace http://www.nodeseek.com/
  4. // @version 0.3-beta.13
  5. // @description 【原NodeSeek增强】自动签到、无缝翻页帖子评论、快捷回复、代码高亮、屏蔽用户、屏蔽帖子、楼主低等级提醒
  6. // @author dabao
  7. // @match *://www.nodeseek.com/*
  8. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACz0lEQVR4Ae3B32tVdQAA8M85u7aVHObmzJVD0+ssiphstLEM62CBlCBEIAYhUoGGD/kiRUo+9CIEElFZgZJFSApBVhCUX2WFrVQKf5Qy26SgdK4pN7eZu+cbtyfJ/gLx83HD9SAhlEyXupiPhUSTeonRfNw1ws2aRJeN5jHcolFhJJ9M8Zj99piDTnv12SjzfzIb9dmrC7Pttt8ykjDVLsu8ZZ1GH1oqeDofJLtJh4fMEw3Y72jlCuEO2+W+sNJFr3vOZ1YIi8NIGA29hDWhGgZDJ2Rt2ZvZSBazmMUsZsPZ1qwVQmcYDNWwhtAbRsNIWJx6WLPDfgxNVkm9nR8hm+XduLba7F9RtcXztmUzyY/YJrUqNPvBYc0eSS3CwXxMl4WG7CarsyEuvU2HOkRNujSw3PosxR6DFurKxx3E/akFohPo0aDfEO61os5LdrtLVWG1TzxokifdiSH9GnTjuGhBqsWE39GOo3kVi8wsmeVW00SJ200zA9r0kFcdQzv+MKElVW/S+L5EE86pmUth3BV/SzCOCUjMVXMWzfsSYybVl1SlSlESkagpuOI1nzshFX1gyAF1UKhJEKOkJFVNXVBv+pJoBK1qBkh86z1/SaR+9o5zEgoDaloxsiSart6F1Bkl83ESHWEKvvEbqZJETaokgSH9hCk6cBLtSs6kDqEb/cZ0K+MnO0X/VdhRGUBZjzH9uA+HUl+a0BvmO+J7bVZSKWz1kehqhfe9oWalNoccDmW9JnyV+toxsy3PK3aY9Gx4gMp567ziV4WawpCXra+MEhZ5xqTtecVycxzXlxA22OK4ZYbt9LjvrM5PkNUp6zVPdNpBv1QKwt126Paxp8zwqXu8kG8pYZdHlT2Rvxo2aVG2ObyYn65UnXLKVULZZrP02ZRfCms1OmAXCSHRYqrLzuZFaDFV6s/8omuERs0Kl/LzITVTvTHDeXTD9eAftAsSYhXYOWUAAAAASUVORK5CYII=
  9. // @require https://s4.zstatic.net/ajax/libs/layui/2.9.9/layui.min.js
  10. // @resource highlightStyle https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/styles/atom-one-light.min.css
  11. // @resource highlightStyle_dark https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_deleteValue
  16. // @grant GM_notification
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_unregisterMenuCommand
  19. // @grant GM_getResourceURL
  20. // @grant GM_addElement
  21. // @grant GM_addStyle
  22. // @grant GM_openInTab
  23. // @grant unsafeWindow
  24. // @run-at document-end
  25. // @license GPL-3.0 License
  26. // @supportURL https://www.nodeseek.com/post-36263-1
  27. // @homepageURL https://www.nodeseek.com/post-36263-1
  28. // ==/UserScript==
  29.  
  30. (function () {
  31. 'use strict';
  32.  
  33. const { version, author, name, icon } = GM_info.script;
  34.  
  35. const BASE_URL = "https://www.nodeseek.com";
  36.  
  37. const util = {
  38. clog:(c) => {
  39. console.group(`%c %c [${name}]-v${version} by ${author}`, `background:url(${icon}) center/12px no-repeat;padding:3px`, "");
  40. console.log(c);
  41. console.groupEnd();
  42. },
  43. getValue: (name, defaultValue) => GM_getValue(name, defaultValue),
  44. setValue: (name, value) => GM_setValue(name, value),
  45. sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
  46. addStyle(id, tag, css) {
  47. tag = tag || 'style';
  48. let doc = document, styleDom = doc.head.querySelector(`#${id}`);
  49. if (styleDom) return;
  50. let style = doc.createElement(tag);
  51. style.rel = 'stylesheet';
  52. style.id = id;
  53. tag === 'style' ? style.innerHTML = css : style.href = css;
  54. doc.head.appendChild(style);
  55. },
  56. removeStyle(id,tag){
  57. tag = tag || 'style';
  58. let doc = document, styleDom = doc.head.querySelector(`#${id}`);
  59. if (styleDom) { doc.head.removeChild(styleDom) };
  60. },
  61. getAttrsByPrefix(element, prefix) {
  62. return Array.from(element.attributes).reduce((acc, { name, value }) => {
  63. if (name.startsWith(prefix)) acc[name] = value;
  64. return acc;
  65. }, {});
  66. },
  67. data(element, key, value) {
  68. if (arguments.length < 2) return undefined;
  69. if (value !== undefined) element.dataset[key] = value;
  70. return element.dataset[key];
  71. },
  72. async post(url, data, headers, responseType = 'json') {
  73. return this.fetchData(url, 'POST', data, headers, responseType);
  74. },
  75. async get(url, headers, responseType = 'json') {
  76. return this.fetchData(url, 'GET', null, headers, responseType);
  77. },
  78. async fetchData(url, method='GET', data=null, headers={}, responseType='json') {
  79. const options = {
  80. method,
  81. headers: { 'Content-Type':'application/json',...headers},
  82. body: data ? JSON.stringify(data) : undefined
  83. };
  84. const response = await fetch(url.startsWith("http") ? url : BASE_URL + url, options);
  85. const result = await response[responseType]().catch(() => null);
  86. return response.ok ? result : Promise.reject(result);
  87. },
  88. getCurrentDate() {
  89. const localTimezoneOffset = (new Date()).getTimezoneOffset();
  90. const beijingOffset = 8 * 60;
  91. const beijingTime = new Date(Date.now() + (localTimezoneOffset + beijingOffset) * 60 * 1000);
  92. const timeNow = `${beijingTime.getFullYear()}/${(beijingTime.getMonth() + 1)}/${beijingTime.getDate()}`;
  93. return timeNow;
  94. },
  95. createElement(tagName, options = {}, childrens = [], doc = document, namespace = null) {
  96. if (Array.isArray(options)) {
  97. if (childrens.length !== 0) {
  98. throw new Error("If options is an array, childrens should not be provided.");
  99. }
  100. childrens = options;
  101. options = {};
  102. }
  103.  
  104. const { staticClass = '', dynamicClass = '', attrs = {}, on = {} } = options;
  105.  
  106. const ele = namespace ? doc.createElementNS(namespace, tagName) : doc.createElement(tagName);
  107.  
  108. if (staticClass) {
  109. staticClass.split(' ').forEach(cls => ele.classList.add(cls.trim()));
  110. }
  111. if (dynamicClass) {
  112. dynamicClass.split(' ').forEach(cls => ele.classList.add(cls.trim()));
  113. }
  114.  
  115. Object.entries(attrs).forEach(([key, value]) => {
  116. if (key === 'style' && typeof value === 'object') {
  117. Object.entries(value).forEach(([styleKey, styleValue]) => {
  118. ele.style[styleKey] = styleValue;
  119. });
  120. } else {
  121. if (value !== undefined) ele.setAttribute(key, value);
  122. }
  123. });
  124.  
  125. Object.entries(on).forEach(([event, handler]) => {
  126. ele.addEventListener(event, handler);
  127. });
  128.  
  129. childrens.forEach(child => {
  130. if (typeof child === 'string') {
  131. child = doc.createTextNode(child);
  132. }
  133. ele.appendChild(child);
  134. });
  135.  
  136. return ele;
  137. },
  138. b64DecodeUnicode(str) {
  139. // Going backwards: from bytestream, to percent-encoding, to original string.
  140. return decodeURIComponent(atob(str).split('').map(function (c) {
  141. return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
  142. }).join(''));
  143. }
  144. };
  145.  
  146. const opts = {
  147. post: {
  148. pathPattern: /^\/(categories\/|page|award|search|$)/,
  149. scrollThreshold: 1500,
  150. nextPagerSelector: '.nsk-pager a.pager-next',
  151. postListSelector: 'ul.post-list:not(.topic-carousel-panel)',
  152. topPagerSelector: 'div.nsk-pager.pager-top',
  153. bottomPagerSelector: 'div.nsk-pager.pager-bottom',
  154. },
  155. comment: {
  156. pathPattern: /^\/post-/,
  157. scrollThreshold: 690,
  158. nextPagerSelector: '.nsk-pager a.pager-next',
  159. postListSelector: 'ul.comments',
  160. topPagerSelector: 'div.nsk-pager.post-top-pager',
  161. bottomPagerSelector: 'div.nsk-pager.post-bottom-pager',
  162. },
  163. setting: {
  164. SETTING_SIGN_IN_STATUS: 'setting_sign_in_status',
  165. SETTING_SIGN_IN_LAST_DATE: 'setting_sign_in_last_date',
  166. SETTING_SIGN_IN_IGNORE_DATE: 'setting_sign_in_ignore_date',
  167. SETTING_AUTO_LOADING_STATUS: 'setting_auto_loading_status'
  168. },
  169. settings:{
  170. "version": version,
  171. "sign_in": { "enabled": true, "method": 0, "last_date": "", "ignore_date": "" },
  172. "signin_tips": { "enabled": true },
  173. "re_signin": { "enabled": true },
  174. "auto_jump_external_links": { "enabled": true },
  175. "loading_post": { "enabled": true },
  176. "loading_comment": { "enabled": true },
  177. "quick_comment": { "enabled": true },
  178. "open_post_in_new_tab": { "enabled": false },
  179. "block_members": { "enabled": true },
  180. "block_posts": { "enabled": true,"keywords":[] },
  181. "level_tag": { "enabled": true, "low_lv_alarm":false, "low_lv_max_days":30 },
  182. "code_highlight": { "enabled": true },
  183. "image_slide":{ "enabled":true },
  184. "visited_links":{ "enabled": true, "link_color":"","visited_color":"","dark_link_color":"","dark_visited_color":"" },
  185. "user_card_ext": { "enabled":true }
  186. }
  187. };
  188. layui.use(function () {
  189. const layer = layui.layer;
  190. const dropdown = layui.dropdown;
  191. const message = {
  192. info: (text) => message.__msg(text, { "background-color": "#4D82D6" }),
  193. success: (text) => message.__msg(text, { "background-color": "#57BF57" }),
  194. warning: (text) => message.__msg(text, { "background-color": "#D6A14D" }),
  195. error: (text) => message.__msg(text, { "background-color": "#E1715B" }),
  196. __msg: (text, style) => { let index = layer.msg(text, { offset: 't', area: ['100%', 'auto'], anim: 'slideDown' }); layer.style(index, Object.assign({ opacity: 0.9 }, style)); }
  197. };
  198.  
  199. const Config = {
  200. // 初始化配置数据
  201. initValue() {
  202. const value = [
  203. { name: opts.setting.SETTING_SIGN_IN_STATUS, defaultValue: 0 },
  204. { name: opts.setting.SETTING_SIGN_IN_LAST_DATE, defaultValue: '1753/1/1' },
  205. { name: opts.setting.SETTING_SIGN_IN_IGNORE_DATE, defaultValue: '1753/1/1' },
  206. { name: opts.setting.SETTING_AUTO_LOADING_STATUS, defaultValue: 1 }
  207. ];
  208. this.upgradeConfig();
  209. value.forEach((v) => util.getValue(v.name) === undefined && util.setValue(v.name, v.defaultValue));
  210. },
  211. // 升级配置项
  212. upgradeConfig() {
  213. const upgradeConfItem = (oldConfKey, newConfKey) => {
  214. if (util.getValue(oldConfKey) && util.getValue(newConfKey) === undefined) {
  215. util.clog(`升级配置项 ${oldConfKey} ${newConfKey}`);
  216. util.setValue(newConfKey, util.getValue(oldConfKey));
  217. GM_deleteValue(oldConfKey);
  218. }
  219. };
  220. upgradeConfItem('menu_signInTime', opts.setting.SETTING_SIGN_IN_LAST_DATE);
  221. },
  222. initializeConfig() {
  223. const defaultConfig = opts.settings;
  224. if (!util.getValue('settings')) {
  225. util.setValue('settings', defaultConfig);
  226. return;
  227. }
  228. if(this.getConfig('version')===version) return;
  229. // 从存储中获取当前配置
  230. let storedConfig = util.getValue('settings');
  231.  
  232. // 递归地删除不在默认配置中的项
  233. const cleanDefaults = (stored, defaults) => {
  234. Object.keys(stored).forEach(key => {
  235. if (defaults[key] === undefined) {
  236. delete stored[key]; // 如果默认配置中没有这个键,删除它
  237. } else if (typeof stored[key] === 'object' && stored[key] !== null && !(stored[key] instanceof Array)) {
  238. cleanDefaults(stored[key], defaults[key]); // 递归检查
  239. }
  240. });
  241. };
  242.  
  243. // 递归地将默认配置中的新项合并到存储的配置中
  244. const mergeDefaults = (stored, defaults) => {
  245. Object.keys(defaults).forEach(key => {
  246. if (typeof defaults[key] === 'object' && defaults[key] !== null && !(defaults[key] instanceof Array)) {
  247. if (!stored[key]) stored[key] = {};
  248. mergeDefaults(stored[key], defaults[key]);
  249. } else {
  250. if (stored[key] === undefined) {
  251. stored[key] = defaults[key];
  252. }
  253. }
  254. });
  255. };
  256.  
  257. mergeDefaults(storedConfig, defaultConfig);
  258. //...这里将旧设置项的值迁移到新设置项
  259. cleanDefaults(storedConfig, defaultConfig);
  260. storedConfig.version = version;
  261. util.setValue('settings',storedConfig);
  262. },updateConfig(path, value) {
  263. let config = util.getValue('settings');
  264. let keys = path.split('.');
  265. let lastKey = keys.pop();
  266. let lastObj = keys.reduce((obj, key) => obj[key], config);
  267. lastObj[lastKey] = value;
  268. util.setValue('settings', config);
  269. },getConfig(path) {
  270. let config = GM_getValue('settings');
  271. let keys = path.split('.');
  272. return keys.reduce((obj, key) => obj[key], config);
  273. }
  274. };
  275.  
  276. const FeatureFlags={
  277. isEnabled(featureName) {
  278. if (Config.getConfig(featureName)) {
  279. return Config.getConfig(`${featureName}.enabled`);
  280. } else {
  281. console.error(`Feature '${featureName}' does not exist.`);
  282. return false;
  283. }
  284. }
  285. };
  286.  
  287. const main = {
  288. loginStatus: false,
  289. //检查是否登陆
  290. checkLogin() {
  291. if (unsafeWindow.__config__ && unsafeWindow.__config__.user) {
  292. this.loginStatus = true;
  293. util.clog(`当前登录用户 ${unsafeWindow.__config__.user.member_name} (ID ${unsafeWindow.__config__.user.member_id})`);
  294. }
  295. },
  296. // 自动签到
  297. autoSignIn(rand) {
  298. if(!FeatureFlags.isEnabled('sign_in')) return;
  299.  
  300. if (!this.loginStatus) return
  301. if (util.getValue(opts.setting.SETTING_SIGN_IN_STATUS) === 0) return;
  302.  
  303. rand = rand || (util.getValue(opts.setting.SETTING_SIGN_IN_STATUS) === 1);
  304.  
  305. let timeNow = util.getCurrentDate(),
  306. timeOld = util.getValue(opts.setting.SETTING_SIGN_IN_LAST_DATE);
  307. if (!timeOld || timeOld != timeNow) { // 是新的一天
  308. util.setValue(opts.setting.SETTING_SIGN_IN_LAST_DATE, timeNow); // 写入签到时间以供后续比较
  309. this.signInRequest(rand);
  310. }
  311. },
  312. // 重新签到
  313. reSignIn() {
  314. if (!this.loginStatus) return;
  315. if (util.getValue(opts.setting.SETTING_SIGN_IN_STATUS) === 0) {
  316. unsafeWindow.mscAlert('提示', this.getMenuStateText(this._menus[0], 0) + ' 状态时不支持重新签到!');
  317. return;
  318. }
  319.  
  320. util.setValue(opts.setting.SETTING_SIGN_IN_LAST_DATE, '1753/1/1');
  321. location.reload();
  322. },
  323. addSignTips() {
  324. if(!FeatureFlags.isEnabled('signin_tips')) return;
  325.  
  326. if (!this.loginStatus) return
  327. if (util.getValue(opts.setting.SETTING_SIGN_IN_STATUS) !== 0) return;
  328.  
  329. const timeNow = util.getCurrentDate();
  330. const { SETTING_SIGN_IN_IGNORE_DATE, SETTING_SIGN_IN_LAST_DATE } = opts.setting;
  331. const timeIgnore = util.getValue(SETTING_SIGN_IN_IGNORE_DATE);
  332. const timeOld = util.getValue(SETTING_SIGN_IN_LAST_DATE);
  333.  
  334. if (timeNow === timeIgnore || timeNow === timeOld) return;
  335.  
  336. const _this = this;
  337. let tip = util.createElement("div", { staticClass: 'nsplus-tip' });
  338. let tip_p = util.createElement('p');
  339. tip_p.innerHTML = '今天你还没有签到哦!&emsp;【<a class="sign_in_btn" data-rand="true" href="javascript:;">随机抽个鸡腿</a>】&emsp;【<a class="sign_in_btn" data-rand="false" href="javascript:;">只要5个鸡腿</a>】&emsp;【<a id="sign_in_ignore" href="javascript:;">今天不再提示</a>】';
  340. tip.appendChild(tip_p);
  341. tip.querySelectorAll('.sign_in_btn').forEach(function (item) {
  342. item.addEventListener("click", function (e) {
  343. const rand = util.data(this, 'rand');
  344. _this.signInRequest(rand);
  345. tip.remove();
  346. util.setValue(SETTING_SIGN_IN_LAST_DATE, timeNow); // 写入签到时间以供后续比较
  347. })
  348. });
  349. tip.querySelector('#sign_in_ignore').addEventListener("click", function (e) {
  350. tip.remove();
  351. util.setValue(SETTING_SIGN_IN_IGNORE_DATE, timeNow);
  352. });
  353.  
  354. document.querySelector('header').append(tip);
  355. },
  356. async signInRequest(rand) {
  357. await util.post('/api/attendance?random=' + (rand || false), {}, { "Content-Type": "application/json" }).then(json => {
  358. if (json.success) {
  359. message.success(`签到成功!今天午饭+${json.gain}个鸡腿; 积攒了${json.current}个鸡腿了`);
  360. }
  361. else {
  362. message.info(json.message);
  363. }
  364. }).catch(error => {
  365. message.info(error.message || "发生未知错误");
  366. util.clog(error);
  367. });
  368. util.clog(`[${name}] 签到完成`);
  369. },
  370. is_show_quick_comment: false,
  371. quickComment() {
  372. if (!this.loginStatus || !opts.comment.pathPattern.test(location.pathname)) return;
  373. if (util.getValue(opts.setting.SETTING_AUTO_LOADING_STATUS) === 0) return;
  374.  
  375. const _this = this;
  376.  
  377.  
  378. const onClick = (e) => {
  379. if (_this.is_show_quick_comment) {
  380. return;
  381. }
  382. e.preventDefault();
  383.  
  384. const mdEditor = document.querySelector('.md-editor');
  385. const clientHeight = document.documentElement.clientHeight, clientWidth = document.documentElement.clientWidth;
  386. const mdHeight = mdEditor.clientHeight, mdWidth = mdEditor.clientWidth;
  387. const top = (clientHeight / 2) - (mdHeight / 2), left = (clientWidth / 2) - (mdWidth / 2);
  388. mdEditor.style.cssText = `position: fixed; top: ${top}px; left: ${left}px; margin: 30px 0px; width: 100%; max-width: ${mdWidth}px; z-index: 999;`;
  389. const moveEl = mdEditor.querySelector('.tab-select.window_header');
  390. moveEl.style.cursor = "move";
  391. moveEl.addEventListener('mousedown', startDrag);
  392. addEditorCloseButton();
  393. _this.is_show_quick_comment = true;
  394. };
  395. const commentDiv = document.querySelector('#fast-nav-button-group #back-to-parent').cloneNode(true);
  396. commentDiv.id = 'back-to-comment';
  397. commentDiv.innerHTML = '<svg class="iconpark-icon" style="width: 24px; height: 24px;"><use href="#comments"></use></svg>';
  398. commentDiv.addEventListener("click", onClick);
  399. document.querySelector('#back-to-parent').before(commentDiv);
  400. document.querySelectorAll('.nsk-post .comment-menu,.comment-container .comments').forEach(x=>x.addEventListener("click",(event) =>{ if(!["引用", "回复", "编辑"].includes(event.target.textContent)) return; onClick(event);},true));//使用冒泡法给按钮引用、回复添加事件
  401.  
  402. function addEditorCloseButton() {
  403. const fullScreenToolbar = document.querySelector('#editor-body .window_header > :last-child');
  404. const cloneToolbar = fullScreenToolbar.cloneNode(true);
  405. cloneToolbar.setAttribute('title', '关闭');
  406. cloneToolbar.querySelector('span').classList.replace('i-icon-full-screen-one', 'i-icon-close');
  407. cloneToolbar.querySelector('span').innerHTML = '<svg width="16" height="16" viewBox="0 0 48 48" fill="none"><path d="M8 8L40 40" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8 40L40 8" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
  408. cloneToolbar.addEventListener("click", function (e) {
  409. const mdEditor = document.querySelector('.md-editor');
  410. mdEditor.style = "";
  411. const moveEl = mdEditor.querySelector('.tab-select.window_header');
  412. moveEl.style.cursor = "";
  413. moveEl.removeEventListener('mousedown', startDrag);
  414.  
  415. this.remove();
  416. _this.is_show_quick_comment = false;
  417. });
  418. fullScreenToolbar.after(cloneToolbar);
  419. }
  420. function startDrag(event) {
  421. if (event.button !== 0) return;
  422.  
  423. const draggableElement = document.querySelector('.md-editor');
  424. const parentMarginTop = parseInt(window.getComputedStyle(draggableElement).marginTop);
  425. const initialX = event.clientX - draggableElement.offsetLeft;
  426. const initialY = event.clientY - draggableElement.offsetTop + parentMarginTop;
  427. document.onmousemove = function (event) {
  428. const newX = event.clientX - initialX;
  429. const newY = event.clientY - initialY;
  430. draggableElement.style.left = newX + 'px';
  431. draggableElement.style.top = newY + 'px';
  432. };
  433. document.onmouseup = function () {
  434. document.onmousemove = null;
  435. document.onmouseup = null;
  436. };
  437. }
  438. },
  439. //自动点击跳转页链接
  440. autoJump() {
  441. document.querySelectorAll('a[href*="/jump?to="]').forEach(link => {
  442. try {
  443. const urlObj = new URL(link.href);
  444. const encodedUrl = urlObj.searchParams.get('to');
  445. if (encodedUrl) {
  446. const decodedUrl = decodeURIComponent(encodedUrl);
  447. link.href = decodedUrl;
  448. }
  449. } catch (e) {
  450. console.error('处理链接时出错:', e);
  451. }
  452. });
  453. if (!/^\/jump/.test(location.pathname)) return;
  454. document.querySelector('.btn').click();
  455. },
  456. blockPost(ele) {
  457. ele = ele || document;
  458. ele.querySelectorAll('.post-title>a[href]').forEach(function (item) {
  459. if (item.textContent.toLowerCase().includes("__keys__")) {
  460. item.closest(".post-list-item").classList.add('blocked-post')
  461. }
  462. });
  463. },
  464. blockPostsByViewLevel(ele) {
  465. ele = ele || document;
  466. let level=0;
  467. if (this.loginStatus) level = unsafeWindow.__config__.user.rank;
  468. [...ele.querySelectorAll('.post-list-item use[href="#lock"]')].forEach(el => {
  469. const n = +el.closest('span')?.textContent.match(/\d+/)?.[0] || 0;
  470. if (n > level) el.closest('.post-list-item')?.classList.add('blocked-post');
  471. });
  472. },
  473. //屏蔽用户
  474. blockMemberDOMInsert() {
  475. if (!this.loginStatus) return;
  476.  
  477. const _this = this;
  478. Array.from(document.querySelectorAll(".post-list .post-list-item,.content-item")).forEach((function (t, n) {
  479. var r = t.querySelector('.avatar-normal');
  480. r.addEventListener("click", (function (n) {
  481. n.preventDefault();
  482. let intervalId = setInterval(async () => {
  483. const userCard = document.querySelector('div.user-card.hover-user-card');
  484. const pmButton = document.querySelector('div.user-card.hover-user-card a.btn');
  485. if (userCard && pmButton) {
  486. clearInterval(intervalId);
  487. const dataVAttrs = util.getAttrsByPrefix(userCard, 'data-v');
  488. const userName = userCard.querySelector('a.Username').textContent;
  489. dataVAttrs.style = "float:left; background-color:rgba(0,0,0,.3)";
  490. const blockBtn = util.createElement("a", {
  491. staticClass: "btn", attrs: dataVAttrs, on: {
  492. click: function (e) {
  493. e.preventDefault();
  494. unsafeWindow.mscConfirm(`确定要屏蔽“${userName}”吗?`, '你可以在本站的 设置=>屏蔽用户 中解除屏蔽', function () { blockMember(userName); })
  495. }
  496. }
  497. }, ["屏蔽"]);
  498. pmButton.after(blockBtn);
  499. }
  500. }, 50);
  501. }))
  502. }))
  503. function blockMember(userName) {
  504. util.post("/api/block-list/add", { "block_member_name": userName }, { "Content-Type": "application/json" }).then(function (data) {
  505. if (data.success) {
  506. let msg = '屏蔽用户【' + userName + '】成功!';
  507. unsafeWindow.mscAlert(msg);
  508. util.clog(msg);
  509. } else {
  510. let msg = '屏蔽用户【' + userName + '】失败!' + data.message;
  511. unsafeWindow.mscAlert(msg);
  512. util.clog(msg);
  513. }
  514. }).catch(function (err) {
  515. util.clog(err);
  516. });
  517. }
  518. },
  519. addImageSlide() {
  520. if (!opts.comment.pathPattern.test(location.pathname)) return;
  521.  
  522. const posts = document.querySelectorAll('article.post-content');
  523. posts.forEach(function (post, i) {
  524. const images = post.querySelectorAll('img:not(.sticker)');
  525. if (images.length === 0) return;
  526.  
  527. images.forEach(function (image, i) {
  528. const newImg = image.cloneNode(true);
  529. image.parentNode.replaceChild(newImg, image);
  530. newImg.addEventListener('click', function (e) {
  531. e.preventDefault();
  532. const imgArr = Array.from(post.querySelectorAll('img:not(.sticker)'));
  533. const clickedIndex = imgArr.indexOf(this);
  534. const photoData = imgArr.map((img, i) => ({ alt: img.alt, pid: i + 1, src: img.src }));
  535. layer.photos({ photos: { "title": "图片预览", "start": clickedIndex, "data": photoData } });
  536. }, true);
  537. });
  538. });
  539. },
  540. addLevelTag() {//添加等级标签
  541. if (!this.loginStatus) return;
  542. if (!opts.comment.pathPattern.test(location.pathname)) return;
  543.  
  544. this.getUserInfo(unsafeWindow.__config__.postData.op.uid).then((user) => {
  545. let warningInfo = '';
  546. const daysDiff = Math.floor((new Date() - new Date(user.created_at)) / (1000 * 60 * 60 * 24));
  547. if (daysDiff < 30) {
  548. warningInfo = `⚠️`;
  549. }
  550. console.log(user);
  551. const span = util.createElement("span", { staticClass: `nsk-badge role-tag user-level user-lv${user.rank}`, on: { mouseenter: function (e) { layer.tips(`注册 <span class="layui-badge">${daysDiff}</span> 天;帖子 ${user.nPost};评论 ${user.nComment}`, this, { tips: 3, time: 0 }); }, mouseleave: function (e) { layer.closeAll(); } } }, [util.createElement("span", [`${warningInfo}Lv ${user.rank}`])]);
  552.  
  553. const authorLink = document.querySelector('#nsk-body .nsk-post .nsk-content-meta-info .author-info>a');
  554. if (authorLink != null) {
  555. authorLink.after(span);
  556. }
  557. });
  558. },
  559. getUserInfo(uid) {
  560. return new Promise((resolve, reject) => {
  561. util.get(`/api/account/getInfo/${uid}`, {}, 'json').then((data) => {
  562. if (!data.success) {
  563. util.clog(data);
  564. return;
  565. }
  566. resolve(data.detail);
  567. }).catch((err) => reject(err));
  568. })
  569. },
  570. userCardEx() {
  571. if (!this.loginStatus) return;
  572. if (!(opts.post.pathPattern.test(location.pathname)|| opts.comment.pathPattern.test(location.pathname))) return;
  573.  
  574. const updateNotificationElement = (element, href, iconHref, text, count) => {
  575. element.querySelector("a").setAttribute("href", `${href}`);
  576. element.querySelector("a > svg > use").setAttribute("href", `${iconHref}`);
  577. element.querySelector("a > :nth-child(2)").textContent = `${text} `;
  578. element.querySelector("a > :last-child").textContent = count;
  579. const countEl = element.querySelector("a > :last-child");
  580. countEl.classList.toggle("notify-count", count > 0);
  581.  
  582. return element;
  583. };
  584.  
  585. const userCard = document.querySelector(".user-card .user-stat");
  586. const lastElement = userCard.querySelector(".stat-block:first-child > :last-child");
  587.  
  588. const atMeElement = lastElement.cloneNode(true);
  589. const msgElement = lastElement.cloneNode(true);
  590.  
  591. lastElement.after(atMeElement);
  592. userCard.querySelector(".stat-block:last-child").append(msgElement);
  593.  
  594. // 初始化通知显示
  595. const updateAllCounts = (counts) => {
  596. updateNotificationElement(atMeElement, "/notification#/atMe", "#at-sign", "我", counts.atMe);
  597. updateNotificationElement(msgElement, "/notification#/message?mode=list", "#envelope-one", "私信", counts.message);
  598. updateNotificationElement(lastElement, "/notification#/reply", "#remind-6nce9p47", "回复", counts.reply);
  599. };
  600.  
  601. // 初始使用 unsafeWindow.__config__.user.unViewedCount(首次加载)
  602. updateAllCounts(unsafeWindow.__config__.user.unViewedCount);
  603.  
  604. // 启动定时刷新,每 60 秒更新一次
  605. setInterval(() => {
  606. fetch("https://www.nodeseek.com/api/notification/unread-count", { credentials: "include" })
  607. .then(res => res.json())
  608. .then(data => {
  609. if (data.success && data.unreadCount) {
  610. updateAllCounts(data.unreadCount);
  611. }
  612. }).catch(console.error);
  613. }, 15000); // 15,000 毫秒 = 15 秒
  614. },
  615. // 自动翻页
  616. autoLoading() {
  617. if (util.getValue(opts.setting.SETTING_AUTO_LOADING_STATUS) === 0) return;
  618. let opt = {};
  619. if (opts.post.pathPattern.test(location.pathname)) { opt = opts.post; }
  620. else if (opts.comment.pathPattern.test(location.pathname)) { opt = opts.comment; }
  621. else { return; }
  622. let is_requesting = false;
  623. let _this = this;
  624. this.windowScroll(function (direction, e) {
  625. if (direction === 'down') { // 下滑才准备翻页
  626. let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
  627. if (document.documentElement.scrollHeight <= document.documentElement.clientHeight + scrollTop + opt.scrollThreshold && !is_requesting) {
  628. if (!document.querySelector(opt.nextPagerSelector)) return;
  629. let nextUrl = document.querySelector(opt.nextPagerSelector).attributes.href.value;
  630. is_requesting = true;
  631. util.get(nextUrl, {}, 'text').then(function (data) {
  632. let doc = new DOMParser().parseFromString(data, "text/html");
  633. _this.blockPost(doc);//过滤帖子
  634. _this.blockPostsByViewLevel(doc);
  635. if (opts.comment.pathPattern.test(location.pathname)){
  636. // 取加载页的评论数据追加到原评论数据
  637. let el = doc.getElementById('temp-script')
  638. let jsonText = el.textContent;
  639. if (jsonText) {
  640. let conf = JSON.parse(util.b64DecodeUnicode(jsonText))
  641. unsafeWindow.__config__.postData.comments.push(...conf.postData.comments);
  642. }
  643. }
  644. document.querySelector(opt.postListSelector).append(...doc.querySelector(opt.postListSelector).childNodes);
  645. document.querySelector(opt.topPagerSelector).innerHTML = doc.querySelector(opt.topPagerSelector).innerHTML;
  646. document.querySelector(opt.bottomPagerSelector).innerHTML = doc.querySelector(opt.bottomPagerSelector).innerHTML;
  647. history.pushState(null, null, nextUrl);
  648. // 评论菜单条
  649. if (opts.comment.pathPattern.test(location.pathname)){
  650. const vue = document.querySelector('.comment-menu').__vue__;
  651. Array.from(document.querySelectorAll(".content-item")).forEach(function (t,e) {
  652. var n = t.querySelector(".comment-menu-mount");
  653. if(!n) return;
  654. let o = new vue.$root.constructor(vue.$options);
  655. o.setIndex(e);
  656. o.$mount(n);
  657. });
  658. }
  659. is_requesting = false;
  660. }).catch(function (err) {
  661. is_requesting = false;
  662. util.clog(err);
  663. });
  664. }
  665. }
  666. });
  667. },
  668. // 滚动条事件
  669. windowScroll(fn1) {
  670. let beforeScrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop,
  671. fn = fn1 || function () { };
  672. setTimeout(function () { // 延时执行,避免刚载入到页面就触发翻页事件
  673. window.addEventListener('scroll', function (e) {
  674. const afterScrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop,
  675. delta = afterScrollTop - beforeScrollTop;
  676. if (delta == 0) return false;
  677. fn(delta > 0 ? 'down' : 'up', e);
  678. beforeScrollTop = afterScrollTop;
  679. }, false);
  680. }, 1000)
  681. },
  682. // 平滑滚动
  683. smoothScroll(){
  684. const scroll = (selector, top = 0) => {
  685. const btn = document.querySelector(selector);
  686. if (btn) {
  687. // 移除现有事件监听器
  688. btn.onclick = null;
  689. btn.removeAttribute('onclick');
  690. // 添加新的事件处理器
  691. btn.addEventListener('click', e => {
  692. e.preventDefault();
  693. e.stopImmediatePropagation();
  694. if(e.target.querySelector('use[href="#down"]')){
  695. top = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
  696. }
  697. window.scrollTo({ top, behavior: 'smooth' });
  698. }, true);
  699. }
  700. };
  701. scroll('#back-to-top', 0);
  702. scroll('#back-to-bottom');
  703. },
  704. async switchOpenPostInNewTab(){
  705. try {
  706. const db = await unsafeWindow.IdbManager.get('nodeseekIDB');
  707. const store = db.transaction(['Preference'], 'readwrite').objectStore('Preference');
  708. const result = await new Promise((resolve, reject) => {
  709. const request = store.get('configuration');
  710. request.onsuccess = () => resolve(request.result);
  711. request.onerror = () => reject("查询失败");
  712. });
  713.  
  714. result.openPostInNewPage = !result.openPostInNewPage;
  715.  
  716. await new Promise((resolve, reject) => {
  717. const request = store.put(result);
  718. request.onsuccess = resolve;
  719. request.onerror = () => reject("保存失败");
  720. }).then(()=>{ unsafeWindow.mscAlert(`已${result.openPostInNewPage?'开启':'关闭'}新标签页打开链接`)});
  721.  
  722. console.log(result);
  723. } catch (error) {
  724. console.error(error);
  725. }
  726. },
  727. history: ()=>{
  728. const STORAGE_KEY = 'nsx_browsing_history';
  729. const PAGE_SIZE = 10;
  730. let saveLimit = 'all';
  731.  
  732. const POST_URL_PATTERN = /^https:\/\/www\.nodeseek\.com\/post-(\d+)-\d+.*$/;
  733. const getCurrentTime = () => layui.util.toDateString(new Date(),"yyyy-MM-ddTHH:mm:ss.SSS");
  734.  
  735. const getBrowsingHistory = () => {
  736. return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
  737. };
  738.  
  739. const saveBrowsingHistory = (history) => {
  740. if (saveLimit !== 'all') {
  741. history = history.slice(-saveLimit);
  742. }
  743. localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
  744. };
  745.  
  746. const addOrUpdateHistory = (url, title) => {
  747. const match = url.match(POST_URL_PATTERN);
  748. if (!match) return; // 只保存匹配的帖子记录
  749.  
  750. const normalizedUrl = `https://www.nodeseek.com/post-${match[1]}-1`; // 只判断第1页,即不区分页码
  751. const history = getBrowsingHistory();
  752. const index = history.findIndex(item => item.url === normalizedUrl);
  753. const entry = { url: normalizedUrl, title, time: getCurrentTime() };
  754. if (index > -1) {
  755. history[index] = entry;
  756. }
  757. else {
  758. history.push(entry);
  759. }
  760. saveBrowsingHistory(history);
  761. };
  762.  
  763. const getHistory = (page = 1) => {
  764. const history = getBrowsingHistory();
  765. const totalPages = Math.ceil(history.length / PAGE_SIZE);
  766. const sortedData = history.sort((a, b) => new Date(b.time) - new Date(a.time));
  767. if(page===0) return sortedData;
  768. return sortedData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
  769. };
  770.  
  771. const showHistory = (page = 1) => {
  772. const history = getBrowsingHistory();
  773. const totalPages = Math.ceil(history.length / PAGE_SIZE);
  774. const pageHistory = history.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
  775. console.clear();
  776. console.log(`浏览历史 - ${page} 页,共 ${totalPages} 页`);
  777. pageHistory.forEach((item, i) => {
  778. console.log(`${(page - 1) * PAGE_SIZE + i + 1}. [${item.time}] ${item.title} - ${item.url}`);
  779. });
  780. if (page < totalPages) {
  781. console.log(`输入 showHistory(${page + 1}) 查看下一页`);
  782. }
  783. };
  784.  
  785. const setSaveLimit = (limit) => {
  786. if (typeof limit === 'number' && limit > 0 || limit === 'all') {
  787. saveLimit = limit;
  788. console.log(`保存限制已设置为:${limit === 'all' ? '全部' : `最近 ${limit} 条`}`);
  789. }
  790. else {
  791. console.error('无效的保存限制。请输入正整数或 "all"');
  792. }
  793. };
  794.  
  795. const injectDom=()=>{
  796. const svg = util.createElement("svg", { staticClass: "iconpark-icon", attrs: { "style": "width: 17px;height: 17px;" }},[ util.createElement("use",{ attrs: { "href": "#history"} }, [], document, "http://www.w3.org/2000/svg") ], document, "http://www.w3.org/2000/svg");
  797. const originalSwitcher = document.querySelector('#nsk-head .color-theme-switcher');
  798. if (originalSwitcher) {
  799. const svgWrap = originalSwitcher.cloneNode();
  800. svgWrap.classList.replace('color-theme-switcher', 'history-dropdown-on');
  801. svgWrap.setAttribute('lay-options', '{trigger:"hover"}');
  802.  
  803. // 判断是否为移动端(li 元素)并移除 SVG 的 style 属性
  804. if (originalSwitcher.tagName.toLowerCase() === 'li') {
  805. svg.removeAttribute('style');
  806. }
  807.  
  808. svgWrap.appendChild(svg);
  809. originalSwitcher.insertAdjacentElement('beforebegin', svgWrap);
  810. }
  811.  
  812. const history=getHistory(0);
  813. const maxLength=20;
  814. // 按天分组
  815. const grouped = history.reduce((result, item, i) => {
  816. const date = item.time.split("T")[0];
  817. if (!result[date]) {
  818. result[date] = [];
  819. }
  820. const truncatedTitle = item.title.length > maxLength
  821. ? item.title.slice(0, maxLength) + "..."
  822. : item.title;
  823. result[date].push({
  824. id: 1000+i+1,
  825. title: `${truncatedTitle}(${layui.util.toDateString(item.time,'HH:mm')})`,
  826. href: item.url,
  827. time: item.time
  828. });
  829. return result;
  830. }, {});
  831.  
  832. // 转换为目标结构
  833. const result = Object.entries(grouped).map(([day, items], index) => ({
  834. id: index + 1,
  835. title: day,
  836. type: "group",
  837. child: items // 将子项包裹在数组中
  838. }));
  839.  
  840. console.log(result);
  841.  
  842. dropdown.render({
  843. elem: '.history-dropdown-on',
  844. // trigger: 'click' // trigger 已配置在元素 `lay-options` 属性上
  845. data: result,
  846. style: 'width: 370px; height: 200px;'
  847. });
  848. };
  849.  
  850. addOrUpdateHistory(window.location.href, document.title);
  851. injectDom();
  852. },
  853. initInstantPage:() => {
  854. const prefetchedUrls = new Set(); // 用于存储已经尝试预加载的 URL
  855. let prefetcher = document.createElement('link');
  856. prefetcher.rel = 'prefetch';
  857.  
  858. document.body.addEventListener('mouseover', (event) => {
  859. const target = event.target.closest('a');
  860.  
  861. if (!target || !target.href || target.hasAttribute('data-no-instant')) {
  862. return;
  863. }
  864.  
  865. const href = target.href;
  866.  
  867. if (!href.startsWith('https://www.nodeseek.com/post-')) {
  868. return;
  869. }
  870.  
  871. if (prefetchedUrls.has(href)) {
  872. console.log('跳过已预加载链接:', href);
  873. return;
  874. }
  875.  
  876. setTimeout(() => {
  877. if (target.matches(':hover')) {
  878. prefetcher.href = href;
  879. document.head.appendChild(prefetcher);
  880. prefetchedUrls.add(href);
  881. console.log('预加载链接已启动:', href);
  882. }
  883. }, 65); // 65毫秒延迟
  884. });
  885. },
  886. switchMultiState(stateName, states) {//多态顺序切换
  887. let currState = util.getValue(stateName);
  888. currState = (currState + 1) % states.length;
  889. util.setValue(stateName, currState);
  890. this.registerMenus();
  891. },
  892. getMenuStateText(menu, stateVal) {
  893. return `${menu.states[stateVal].s1} ${menu.text}(${menu.states[stateVal].s2})`;
  894. },
  895. _menus: [
  896. { name: opts.setting.SETTING_SIGN_IN_STATUS, callback: (name, states) => main.switchMultiState(name, states), accessKey: '', text: '自动签到', states: [{ s1: '❌', s2: '关闭' }, { s1: '🎲', s2: '随机🍗' }, { s1: '📌', s2: '5个🍗' }] },
  897. { name: 're_sign_in', callback: (name, states) => main.reSignIn(), accessKey: '', text: '🔂 重新签到', states: [] },
  898. { name: opts.setting.SETTING_AUTO_LOADING_STATUS, callback: (name, states) => main.switchMultiState(name, states), accessKey: '', text: '无缝加载', states: [{ s1: '❌', s2: '关闭' }, { s1: '✅', s2: '开启' }] },
  899. { name: 'open_post_in_new_tab', callback: (name, states) => main.switchOpenPostInNewTab(), accessKey: '', text: '切换新标签页打开链接', states: []},
  900. { name: 'advanced_settings', callback: (name, states) => main.advancedSettings(), accessKey: '', text: '⚙️ 高级设置', states: [] },
  901. { name: 'feedback', callback: (name, states) => GM_openInTab('https://greasyfork.org/zh-CN/scripts/479426/feedback', { active: true, insert: true, setParent: true }), accessKey: '', text: '💬 反馈 & 建议', states: [] }
  902. ],
  903. _menuIds: [],
  904. registerMenus() {
  905. this._menuIds.forEach(function (id) {
  906. GM_unregisterMenuCommand(id);
  907. });
  908. this._menuIds = [];
  909.  
  910. const _this = this;
  911. this._menus.forEach(function (menu) {
  912. let k = menu.text;
  913. if (menu.states.length > 0) {
  914. k = _this.getMenuStateText(menu, util.getValue(menu.name));
  915. }
  916. let autoClose = menu.hasOwnProperty('autoClose') ? menu.autoClose : true;
  917. let menuId = GM_registerMenuCommand(k, function () { menu.callback(menu.name, menu.states) }, { autoClose: autoClose });
  918. menuId = menuId || k;
  919. _this._menuIds.push(menuId);
  920. });
  921. },
  922. advancedSettings() {
  923. let layerWidth = layui.device().mobile ? '100%' : '620px';
  924. layer.open({
  925. type: 1,
  926. offset: 'r',
  927. anim: 'slideLeft', // 从右往左
  928. area: [layerWidth, '100%'],
  929. scrollbar: false,
  930. shade: 0.1,
  931. shadeClose: false,
  932. btn: ["保存设置"],
  933. btnAlign: 'r',
  934. title: 'NodeSeek X 设置',
  935. id: 'setting-layer-direction-r',
  936. content: `<div class="layui-row" style="display:flex;height:100%">
  937. <div class="layui-panel layui-col-xs3 layui-col-sm3 layui-col-md3" id="demo-menu">
  938. <ul class="layui-menu" lay-filter="demo"></ul>
  939. </div>
  940. <div class="layui-col-xs9 layui-col-sm9 layui-col-md9" style="overflow-y: auto; padding-left: 10px" id="demo-content">
  941. <fieldset id="group1" class="layui-elem-field layui-field-title">
  942. <legend>基本设置</legend>
  943. </fieldset>
  944. <div style="height: 500px;">Content for Group 1</div>
  945. <fieldset id="group2" class="layui-elem-field layui-field-title">
  946. <legend>扩展设置</legend>
  947. </fieldset>
  948. <div style="height: 500px;">Content for Group 2</div>
  949. <fieldset id="group3" class="layui-elem-field layui-field-title">
  950. <legend>实验设置</legend>
  951. </fieldset>
  952. <div style="height: 500px;">Content for Group 3</div>
  953. </div>
  954. </div>
  955. <script>
  956. document.querySelectorAll('#demo-content > fieldset').forEach(function (el, i) {
  957. let li = document.createElement('li');
  958. if (i === 0) li.classList = 'layui-menu-item-checked';
  959. let div = document.createElement('div');
  960. div.classList = 'layui-menu-body-title';
  961. let a = document.createElement('a');
  962. a.href = '#' + el.id;
  963. a.textContent = el.textContent;
  964. a.addEventListener('click', aClick);
  965. li.append(div);
  966. div.append(a);
  967. document.querySelector('#demo-menu>ul').append(li);
  968. });
  969. const docContent = document.querySelector('#demo-content');
  970. docContent.addEventListener('scroll', function (e) {
  971. var scrollPos = docContent.scrollTop;
  972. console.log(scrollPos);
  973. docContent.querySelectorAll('fieldset').forEach(function (el) {
  974. var topPos = el.offsetTop - 10;
  975. if (scrollPos >= topPos) {
  976. var id = el.getAttribute('id');
  977. document.querySelectorAll('.layui-menu > li.layui-menu-item-checked').forEach(function (navItem) {
  978. navItem.classList.remove('layui-menu-item-checked');
  979. });
  980. var navItem = document.querySelector('.layui-menu > li a[href="#' + id + '"]').closest('li');
  981. navItem.classList.add('layui-menu-item-checked');
  982. }
  983. });
  984. });
  985. function aClick(e) {
  986. e.preventDefault();
  987. var id = this.getAttribute('href');
  988. var target = document.querySelector(id);
  989. docContent.scrollTo({
  990. top: target.offsetTop - 10,
  991. // behavior: 'smooth'
  992. });
  993. }
  994. <\/script>`,
  995. yes: function(index, layero, that){
  996. layer.msg('111');
  997. layer.close(index); // 关闭弹层
  998. }
  999. });
  1000. },
  1001. addCodeHighlight() {
  1002. const codes = document.querySelectorAll(".post-content pre code");
  1003. if (codes) {
  1004. codes.forEach(function (code) {
  1005. const copyBtn = util.createElement("span", { staticClass: "copy-code", attrs: { title: "复制代码" }, on: { click: copyCode } }, [util.createElement("svg", { staticClass: 'iconpark-icon' }, [util.createElement("use", { attrs: { href: "#copy" } }, [], document, "http://www.w3.org/2000/svg")], document, "http://www.w3.org/2000/svg")]);
  1006. code.after(copyBtn);
  1007. });
  1008. }
  1009. function copyCode(e) {
  1010. const pre = this.closest('pre');
  1011. const selection = window.getSelection();
  1012. const range = document.createRange();
  1013. range.selectNodeContents(pre.querySelector("code"));
  1014. selection.removeAllRanges();
  1015. selection.addRange(range);
  1016. document.execCommand('copy');
  1017. selection.removeAllRanges();
  1018. updateCopyButton(this);
  1019. layer.tips(`复制成功`, this, { tips: 4, time: 1000 })
  1020. }
  1021. function updateCopyButton(ele) {
  1022. ele.querySelector("use").setAttribute("href", "#check");
  1023. util.sleep(1000).then(() => ele.querySelector("use").setAttribute("href", "#copy"));
  1024. }
  1025. },
  1026. addRunCode(){
  1027. // 首先添加弹出层样式到页面头部
  1028. const modalStyle = document.createElement('style');
  1029. modalStyle.textContent = `
  1030. .html-preview-modal {
  1031. display: none;
  1032. position: fixed;
  1033. z-index: 1000;
  1034. left: 0;
  1035. top: 0;
  1036. width: 100%;
  1037. height: 100%;
  1038. overflow: auto;
  1039. background-color: rgba(0, 0, 0, 0.5);
  1040. }
  1041.  
  1042. .html-preview-modal-content {
  1043. position: relative;
  1044. background-color: #fefefe;
  1045. margin: 5% auto;
  1046. padding: 20px;
  1047. border: 1px solid #888;
  1048. width: 80%;
  1049. max-width: 900px;
  1050. border-radius: 5px;
  1051. box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  1052. }
  1053.  
  1054. .html-preview-close {
  1055. color: #aaa;
  1056. float: right;
  1057. font-size: 28px;
  1058. font-weight: bold;
  1059. cursor: pointer;
  1060. margin-top: -5px;
  1061. }
  1062.  
  1063. .html-preview-close:hover,
  1064. .html-preview-close:focus {
  1065. color: black;
  1066. text-decoration: none;
  1067. }
  1068.  
  1069. .html-preview-iframe {
  1070. width: 100%;
  1071. min-height: 600px;
  1072. border: 1px solid #ddd;
  1073. margin-top: 15px;
  1074. background-color: white;
  1075. }
  1076.  
  1077. .run-html-btn {
  1078. position: absolute;
  1079. right: .5em;
  1080. bottom: 1.5em;
  1081. margin-top: 10px;
  1082. padding: 8px 12px;
  1083. background-color: #4CAF50;
  1084. color: white;
  1085. border: none;
  1086. border-radius: 4px;
  1087. cursor: pointer;
  1088. }
  1089.  
  1090. .run-html-btn:hover {
  1091. background-color: #45a049;
  1092. }
  1093. `;
  1094. document.head.appendChild(modalStyle);
  1095.  
  1096. // 创建全局模态框元素
  1097. const modal = document.createElement('div');
  1098. modal.className = 'html-preview-modal';
  1099. modal.innerHTML = `
  1100. <div class="html-preview-modal-content">
  1101. <span class="html-preview-close">&times;</span>
  1102. <h3>HTML 预览</h3>
  1103. <div id="iframe-container"></div>
  1104. </div>
  1105. `;
  1106. document.body.appendChild(modal);
  1107.  
  1108. // 获取模态框元素
  1109. const previewModal = document.querySelector('.html-preview-modal');
  1110. const closeBtn = document.querySelector('.html-preview-close');
  1111. const iframeContainer = document.querySelector('#iframe-container');
  1112.  
  1113. // 关闭并销毁预览内容的函数
  1114. function closeAndDestroyPreview() {
  1115. // 隐藏模态框
  1116. previewModal.style.display = "none";
  1117.  
  1118. // 销毁iframe内容
  1119. iframeContainer.innerHTML = '';
  1120. }
  1121.  
  1122. // 关闭按钮事件
  1123. closeBtn.onclick = closeAndDestroyPreview;
  1124.  
  1125. // 点击模态框外部关闭
  1126. window.onclick = function(event) {
  1127. if (event.target == previewModal) {
  1128. closeAndDestroyPreview();
  1129. }
  1130. };
  1131.  
  1132. // 预览HTML内容的函数
  1133. function previewHtmlContent(content) {
  1134. // 清空容器(销毁之前的内容)
  1135. iframeContainer.innerHTML = '';
  1136.  
  1137. // 创建一个新的iframe
  1138. const iframe = document.createElement('iframe');
  1139. iframe.className = 'html-preview-iframe';
  1140. iframe.sandbox = 'allow-scripts allow-same-origin allow-popups';
  1141. iframeContainer.appendChild(iframe);
  1142.  
  1143. // 显示模态框
  1144. previewModal.style.display = "block";
  1145.  
  1146. // 使用srcdoc属性设置内容
  1147. iframe.srcdoc = content;
  1148.  
  1149. // 调整iframe高度
  1150. iframe.onload = function() {
  1151. try {
  1152. const height = iframe.contentDocument.body.scrollHeight;
  1153. iframe.style.height = (height + 30) + 'px';
  1154.  
  1155. // 添加事件监听,允许iframe内容动态改变高度
  1156. const resizeObserver = new ResizeObserver(() => {
  1157. try {
  1158. const newHeight = iframe.contentDocument.body.scrollHeight;
  1159. iframe.style.height = (newHeight + 30) + 'px';
  1160. } catch (e) {
  1161. console.log("无法访问iframe内容高度");
  1162. }
  1163. });
  1164.  
  1165. try {
  1166. resizeObserver.observe(iframe.contentDocument.body);
  1167. } catch (e) {
  1168. console.log("无法观察iframe内容变化");
  1169. }
  1170. } catch (e) {
  1171. console.log("无法访问iframe内容");
  1172. iframe.style.height = '400px'; // 默认高度
  1173. }
  1174. };
  1175. }
  1176.  
  1177. // 查找所有HTML代码块并添加运行按钮
  1178. document.querySelectorAll('pre code.language-html').forEach((codeBlock) => {
  1179. const pre = codeBlock.parentNode;
  1180.  
  1181. // 创建运行按钮
  1182. const runButton = document.createElement('button');
  1183. runButton.textContent = '运行代码';
  1184. runButton.className = 'run-html-btn';
  1185.  
  1186. // 运行按钮点击事件
  1187. runButton.onclick = function() {
  1188. // 获取当前代码块的内容
  1189. const codeContent = codeBlock.textContent;
  1190.  
  1191. // 预览该内容
  1192. previewHtmlContent(codeContent);
  1193. };
  1194.  
  1195. // 将按钮添加到代码块后面
  1196. pre.appendChild(runButton);
  1197. });
  1198.  
  1199. // 添加键盘事件监听器 - 按ESC键关闭模态框
  1200. document.addEventListener('keydown', function(event) {
  1201. if (event.key === 'Escape' && previewModal.style.display === 'block') {
  1202. closeAndDestroyPreview();
  1203. }
  1204. });
  1205. },
  1206. addPluginStyle() {
  1207. let style = `
  1208. .nsplus-tip { background-color: rgba(255, 217, 0, 0.8); border: 0px solid black; padding: 3px; text-align: center;animation: blink 5s cubic-bezier(.68,.05,.46,.96) infinite;}
  1209. /* @keyframes blink{ 0%{background-color: red;} 25%{background-color: yellow;} 50%{background-color: blue;} 75%{background-color: green;} 100%{background-color: red;} } */
  1210. .nsplus-tip p,.nsplus-tip p a { color: #f00 }
  1211. .nsplus-tip p a:hover {color: #0ff}
  1212. #back-to-comment{display:flex;}
  1213. #fast-nav-button-group .nav-item-btn:nth-last-child(4){bottom:120px;}
  1214.  
  1215. header div.history-dropdown-on { color: var(--link-hover-color); cursor: pointer; padding: 0 5px; position: absolute; right: 50px}
  1216.  
  1217. body.light-layout .post-list .post-title a:visited{color:#681da8}
  1218. body.dark-layout .post-list .post-title a:visited {color:#999}
  1219. .role-tag.user-level.user-lv0 {background-color: rgb(199 194 194); border: 1px solid rgb(199 194 194); color: #fafafa;}
  1220. .role-tag.user-level.user-lv1 {background-color: #ff9400; border: 1px solid #ff9400; color: #fafafa;}
  1221. .role-tag.user-level.user-lv2 {background-color: #ff9400; border: 1px solid #ff9400; color: #fafafa;}
  1222. .role-tag.user-level.user-lv3 {background-color: #ff3a55; border: 1px solid #ff3a55; color: #fafafa;}
  1223. .role-tag.user-level.user-lv4 {background-color: #ff3a55; border: 1px solid #ff3a55; color: #fafafa;}
  1224. .role-tag.user-level.user-lv5 {background-color: #de00ff; border: 1px solid #de00ff; color: #fafafa;}
  1225. .role-tag.user-level.user-lv6 {background-color: #de00ff; border: 1px solid #de00ff; color: #fafafa;}
  1226. .role-tag.user-level.user-lv7 {background-color: #ff0000; border: 1px solid #ff0000; color: #fafafa;}
  1227. .role-tag.user-level.user-lv8 {background-color: #3478f7; border: 1px solid #3478f7; color: #fafafa;}
  1228.  
  1229. .post-content pre { position: relative; }
  1230. .post-content pre span.copy-code { position: absolute; right: .5em; top: .5em; cursor: pointer;color: #c1c7cd; }
  1231. .post-content pre .iconpark-icon {width:16px;height:16px;margin:3px;}
  1232. .post-content pre .iconpark-icon:hover {color:var(--link-hover-color)}
  1233. .dark-layout .post-content pre code.hljs { padding: 1em !important; }
  1234. `;
  1235. if (document.head) {
  1236. util.addStyle('nsplus-style', 'style', style);
  1237. util.addStyle('layui-style', 'link', 'https://s.cfn.pp.ua/layui/2.9.9/css/layui.css');
  1238. util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle"));
  1239. }
  1240. },
  1241. addPluginScript() {
  1242. GM_addElement(document.body, 'script', {
  1243. src: 'https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/highlight.min.js'
  1244. });
  1245. GM_addElement(document.body, 'script', {
  1246. textContent: 'window.onload = function(){hljs.highlightAll();}'
  1247. });
  1248. GM_addElement(document.body, "script", { textContent: `!function(e){var t,n,d,o,i,a,r='<svg><symbol id="envelope-one" viewBox="0 0 48 48" fill="none"><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M36 16V8H4v24h8" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-width="4" stroke="currentColor" d="M12 40h32V16H12v24Z" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="m12 16 16 12 16-12" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M32 16H12v15" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M44 31V16H24" data-follow-stroke="currentColor"/></symbol><symbol id="at-sign" viewBox="0 0 48 48" fill="none"><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M44 24c0-11.046-8.954-20-20-20S4 12.954 4 24s8.954 20 20 20v0c4.989 0 9.55-1.827 13.054-4.847" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-width="4" stroke="currentColor" d="M24 32a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M32 24a6 6 0 0 0 6 6v0a6 6 0 0 0 6-6m-12 1v-9" data-follow-stroke="currentColor"/></symbol><symbol id="copy" viewBox="0 0 48 48" fill="none"><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M13 12.432v-4.62A2.813 2.813 0 0 1 15.813 5h24.374A2.813 2.813 0 0 1 43 7.813v24.375A2.813 2.813 0 0 1 40.187 35h-4.67" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-width="4" stroke="currentColor" d="M32.188 13H7.811A2.813 2.813 0 0 0 5 15.813v24.374A2.813 2.813 0 0 0 7.813 43h24.375A2.813 2.813 0 0 0 35 40.187V15.814A2.813 2.813 0 0 0 32.187 13Z" data-follow-stroke="currentColor"/></symbol><symbol id="history" viewBox="0 0 48 48" fill="none"><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M5.818 6.727V14h7.273" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M4 24c0 11.046 8.954 20 20 20v0c11.046 0 20-8.954 20-20S35.046 4 24 4c-7.402 0-13.865 4.021-17.323 9.998" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="m24.005 12-.001 12.009 8.48 8.48" data-follow-stroke="currentColor"/></symbol></svg>';function c(){i||(i=!0,d())}t=function(){var e,t,n;(n=document.createElement("div")).innerHTML=r,r=null,(t=n.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",e=t,(n=document.body).firstChild?(t=n.firstChild).parentNode.insertBefore(e,t):n.appendChild(e))},document.addEventListener?["complete","loaded","interactive"].indexOf(document.readyState)>-1?setTimeout(t,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),t()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(d=t,o=e.document,i=!1,(a=function(){try{o.documentElement.doScroll("left")}catch(e){return void setTimeout(a,50)}c()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,c())})}(window);` });
  1249. },
  1250. darkMode(){
  1251. // 选择要监视的目标元素(body元素)
  1252. const targetNode = document.querySelector('body');
  1253. // 进入页面时判断是否是深色模式
  1254. if(targetNode.classList.contains('dark-layout')){
  1255. util.addStyle('layuicss-theme-dark','link','https://s.cfn.pp.ua/layui/theme-dark/2.9.7/css/layui-theme-dark.css');
  1256. util.removeStyle('hightlight-style');
  1257. util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle_dark"));
  1258. }
  1259.  
  1260. // 配置MutationObserver的选项
  1261. const observerConfig = {
  1262. attributes: true, // 监视属性变化
  1263. attributeFilter: ['class'], // 只监视类属性
  1264. };
  1265.  
  1266. // 创建一个新的MutationObserver,并指定触发变化时的回调函数
  1267. const observer = new MutationObserver((mutationsList, observer) => {
  1268. for(let mutation of mutationsList) {
  1269. if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
  1270. if(targetNode.classList.contains('dark-layout')){
  1271. util.addStyle('layuicss-theme-dark','link','https://s.cfn.pp.ua/layui/theme-dark/2.9.7/css/layui-theme-dark.css');
  1272. util.removeStyle('hightlight-style');
  1273. util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle_dark"));
  1274. }else{
  1275. util.removeStyle('layuicss-theme-dark');
  1276. util.removeStyle('hightlight-style');
  1277. util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle"));
  1278. }
  1279. }
  1280. }
  1281. });
  1282.  
  1283. // 使用给定的配置选项开始观察目标节点
  1284. observer.observe(targetNode, observerConfig);
  1285. },
  1286. init() {
  1287. Config.initValue();
  1288. Config.initializeConfig();
  1289. this.addPluginStyle();
  1290. this.checkLogin();
  1291. const codeMirrorElement = document.querySelector('.CodeMirror');
  1292. if (codeMirrorElement) {
  1293. const codeMirrorInstance = codeMirrorElement.CodeMirror;
  1294. if (codeMirrorInstance) {
  1295. let btnSubmit = document.querySelector('.md-editor button.submit.btn.focus-visible');
  1296. btnSubmit.innerText=btnSubmit.innerText+'(Ctrl+Enter)';
  1297. codeMirrorInstance.addKeyMap({"Ctrl-Enter":function(cm){ btnSubmit.click();}});
  1298. }
  1299. }
  1300. this.autoSignIn();//自动签到
  1301. this.addSignTips();//签到提示
  1302. this.autoJump();//自动点击跳转页
  1303. this.autoLoading();//无缝加载帖子和评论
  1304. this.blockMemberDOMInsert();//屏蔽用户
  1305. this.blockPost();//屏蔽帖子
  1306. this.blockPostsByViewLevel();
  1307. this.quickComment();//快捷评论
  1308. this.addLevelTag();//添加等级标签
  1309. this.userCardEx();//用户卡片扩展
  1310. this.registerMenus();
  1311. this.addPluginScript();
  1312. this.addCodeHighlight();
  1313. this.addRunCode();
  1314. this.addImageSlide();
  1315. this.darkMode();
  1316. this.history();
  1317. this.initInstantPage();
  1318. this.smoothScroll();
  1319. }
  1320. }
  1321. main.init();
  1322. });
  1323. })();