NGA Filter

NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。

当前为 2024-04-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NGA Filter
  3. // @name:zh-CN NGA 屏蔽插件
  4. // @namespace https://gf.qytechs.cn/users/263018
  5. // @version 2.3.0
  6. // @author snyssss
  7. // @description NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。
  8. // @license MIT
  9.  
  10. // @match *://bbs.nga.cn/*
  11. // @match *://ngabbs.com/*
  12. // @match *://nga.178.com/*
  13.  
  14. // @require https://update.gf.qytechs.cn/scripts/486070/1357004/NGA%20Library.js
  15.  
  16. // @grant GM_addStyle
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_registerMenuCommand
  20. // @grant unsafeWindow
  21.  
  22. // @run-at document-start
  23. // @noframes
  24. // ==/UserScript==
  25.  
  26. (() => {
  27. // 声明泥潭主模块、主题模块、回复模块
  28. let commonui, topicModule, replyModule;
  29.  
  30. // KEY
  31. const DATA_KEY = "NGAFilter";
  32. const PRE_FILTER_KEY = "PRE_FILTER_KEY";
  33.  
  34. // TIPS
  35. const TIPS = {
  36. filterMode:
  37. "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:显示 &gt; 隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承",
  38. addTags: `一次性添加多个标记用"|"隔开,不会添加重名标记`,
  39. keyword: `支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。`,
  40. forumOrSubset:
  41. "输入版面或合集的完整链接,如:<br/>https://bbs.nga.cn/thread.php?fid=xxx<br/>https://bbs.nga.cn/thread.php?stid=xxx",
  42. hunter: "猎巫模块需要占用额外的资源,请谨慎开启",
  43. error: "目前泥潭对查询接口增加了限制,功能基本失效",
  44. };
  45.  
  46. // STYLE
  47. GM_addStyle(`
  48. .filter-table-wrapper {
  49. max-height: 80vh;
  50. overflow-y: auto;
  51. }
  52. .filter-table {
  53. margin: 0;
  54. }
  55. .filter-table th,
  56. .filter-table td {
  57. position: relative;
  58. white-space: nowrap;
  59. }
  60. .filter-table th {
  61. position: sticky;
  62. top: 2px;
  63. z-index: 1;
  64. }
  65. .filter-table input:not([type]), .filter-table input[type="text"] {
  66. margin: 0;
  67. box-sizing: border-box;
  68. height: 100%;
  69. width: 100%;
  70. }
  71. .filter-input-wrapper {
  72. position: absolute;
  73. top: 6px;
  74. right: 6px;
  75. bottom: 6px;
  76. left: 6px;
  77. }
  78. .filter-text-ellipsis {
  79. display: flex;
  80. }
  81. .filter-text-ellipsis > * {
  82. flex: 1;
  83. width: 1px;
  84. overflow: hidden;
  85. text-overflow: ellipsis;
  86. }
  87. .filter-button-group {
  88. margin: -.1em -.2em;
  89. }
  90. .filter-tags {
  91. margin: 2px -0.2em 0;
  92. text-align: left;
  93. }
  94. .filter-mask {
  95. margin: 1px;
  96. color: #81C7D4;
  97. background: #81C7D4;
  98. }
  99. .filter-mask-block {
  100. display: block;
  101. border: 1px solid #66BAB7;
  102. text-align: center !important;
  103. }
  104. .filter-input-wrapper {
  105. position: absolute;
  106. top: 6px;
  107. right: 6px;
  108. bottom: 6px;
  109. left: 6px;
  110. }
  111. `);
  112.  
  113. /**
  114. * 设置
  115. *
  116. * 暂时整体处理模块设置,后续再拆分
  117. */
  118. class Settings {
  119. /**
  120. * 缓存管理
  121. */
  122. cache;
  123.  
  124. /**
  125. * 当前设置
  126. */
  127. data = null;
  128.  
  129. /**
  130. * 初始化并绑定缓存管理
  131. * @param {Cache} cache 缓存管理
  132. */
  133. constructor(cache) {
  134. this.cache = cache;
  135. }
  136.  
  137. /**
  138. * 读取设置
  139. */
  140. async load() {
  141. // 读取设置
  142. if (this.data === null) {
  143. // 默认配置
  144. const defaultData = {
  145. tags: {},
  146. users: {},
  147. keywords: {},
  148. locations: {},
  149. forumOrSubsets: {},
  150. options: {
  151. filterRegdateLimit: 0,
  152. filterPostnumLimit: 0,
  153. filterTopicRateLimit: 100,
  154. filterReputationLimit: NaN,
  155. filterAnony: false,
  156. filterMode: "隐藏",
  157. },
  158. };
  159.  
  160. // 读取数据
  161. const storedData = await this.cache
  162. .get(DATA_KEY)
  163. .then((values) => values || {});
  164.  
  165. // 写入缓存
  166. this.data = Tools.merge({}, defaultData, storedData);
  167.  
  168. // 写入默认模块选项
  169. if (Object.hasOwn(this.data, "modules") === false) {
  170. this.data.modules = ["user", "tag", "misc"];
  171.  
  172. if (Object.keys(this.data.keywords).length > 0) {
  173. this.data.modules.push("keyword");
  174. }
  175.  
  176. if (Object.keys(this.data.locations).length > 0) {
  177. this.data.modules.push("location");
  178. }
  179. }
  180. }
  181.  
  182. // 返回设置
  183. return this.data;
  184. }
  185.  
  186. /**
  187. * 写入设置
  188. */
  189. async save() {
  190. return this.cache.put(DATA_KEY, this.data);
  191. }
  192.  
  193. /**
  194. * 获取模块列表
  195. */
  196. get modules() {
  197. return this.data.modules;
  198. }
  199.  
  200. /**
  201. * 设置模块列表
  202. */
  203. set modules(values) {
  204. this.data.modules = values;
  205. this.save();
  206. }
  207.  
  208. /**
  209. * 获取标签列表
  210. */
  211. get tags() {
  212. return this.data.tags;
  213. }
  214.  
  215. /**
  216. * 设置标签列表
  217. */
  218. set tags(values) {
  219. this.data.tags = values;
  220. this.save();
  221. }
  222.  
  223. /**
  224. * 获取用户列表
  225. */
  226. get users() {
  227. return this.data.users;
  228. }
  229.  
  230. /**
  231. * 设置用户列表
  232. */
  233. set users(values) {
  234. this.data.users = values;
  235. this.save();
  236. }
  237.  
  238. /**
  239. * 获取关键字列表
  240. */
  241. get keywords() {
  242. return this.data.keywords;
  243. }
  244.  
  245. /**
  246. * 设置关键字列表
  247. */
  248. set keywords(values) {
  249. this.data.keywords = values;
  250. this.save();
  251. }
  252.  
  253. /**
  254. * 获取属地列表
  255. */
  256. get locations() {
  257. return this.data.locations;
  258. }
  259.  
  260. /**
  261. * 设置属地列表
  262. */
  263. set locations(values) {
  264. this.data.locations = values;
  265. this.save();
  266. }
  267.  
  268. /**
  269. * 获取版面或合集列表
  270. */
  271. get forumOrSubsets() {
  272. return this.data.forumOrSubsets;
  273. }
  274.  
  275. /**
  276. * 设置版面或合集列表
  277. */
  278. set forumOrSubsets(values) {
  279. this.data.forumOrSubsets = values;
  280. this.save();
  281. }
  282.  
  283. /**
  284. * 获取默认过滤模式
  285. */
  286. get defaultFilterMode() {
  287. return this.data.options.filterMode;
  288. }
  289.  
  290. /**
  291. * 设置默认过滤模式
  292. */
  293. set defaultFilterMode(value) {
  294. this.data.options.filterMode = value;
  295. this.save();
  296. }
  297.  
  298. /**
  299. * 获取注册(不可用)时间限制
  300. */
  301. get filterRegdateLimit() {
  302. return this.data.options.filterRegdateLimit || 0;
  303. }
  304.  
  305. /**
  306. * 设置注册(不可用)时间限制
  307. */
  308. set filterRegdateLimit(value) {
  309. this.data.options.filterRegdateLimit = value;
  310. this.save();
  311. }
  312.  
  313. /**
  314. * 获取发帖数量限制
  315. */
  316. get filterPostnumLimit() {
  317. return this.data.options.filterPostnumLimit || 0;
  318. }
  319.  
  320. /**
  321. * 设置发帖数量限制
  322. */
  323. set filterPostnumLimit(value) {
  324. this.data.options.filterPostnumLimit = value;
  325. this.save();
  326. }
  327.  
  328. /**
  329. * 获取发帖比例限制
  330. */
  331. get filterTopicRateLimit() {
  332. return this.data.options.filterTopicRateLimit || 100;
  333. }
  334.  
  335. /**
  336. * 设置发帖比例限制
  337. */
  338. set filterTopicRateLimit(value) {
  339. this.data.options.filterTopicRateLimit = value;
  340. this.save();
  341. }
  342.  
  343. /**
  344. * 获取版面声望限制
  345. */
  346. get filterReputationLimit() {
  347. return this.data.options.filterReputationLimit || NaN;
  348. }
  349.  
  350. /**
  351. * 设置版面声望限制
  352. */
  353. set filterReputationLimit(value) {
  354. this.data.options.filterReputationLimit = value;
  355. this.save();
  356. }
  357.  
  358. /**
  359. * 获取是否过滤匿名
  360. */
  361. get filterAnonymous() {
  362. return this.data.options.filterAnony || false;
  363. }
  364.  
  365. /**
  366. * 设置是否过滤匿名
  367. */
  368. set filterAnonymous(value) {
  369. this.data.options.filterAnony = value;
  370. this.save();
  371. }
  372.  
  373. /**
  374. * 获取是否启用前置过滤
  375. */
  376. get preFilterEnabled() {
  377. return this.cache.get(PRE_FILTER_KEY).then((value) => {
  378. if (value === undefined) {
  379. return true;
  380. }
  381.  
  382. return value;
  383. });
  384. }
  385.  
  386. /**
  387. * 设置是否启用前置过滤
  388. */
  389. set preFilterEnabled(value) {
  390. this.cache.put(PRE_FILTER_KEY, value).then(() => {
  391. location.reload();
  392. });
  393. }
  394.  
  395. /**
  396. * 获取过滤模式列表
  397. *
  398. * 模拟成从配置中获取
  399. */
  400. get filterModes() {
  401. return ["继承", "标记", "遮罩", "隐藏", "显示"];
  402. }
  403.  
  404. /**
  405. * 获取指定下标过滤模式
  406. * @param {Number} index 下标
  407. */
  408. getNameByMode(index) {
  409. const modes = this.filterModes;
  410.  
  411. return modes[index] || "";
  412. }
  413.  
  414. /**
  415. * 获取指定过滤模式下标
  416. * @param {String} name 过滤模式
  417. */
  418. getModeByName(name) {
  419. const modes = this.filterModes;
  420.  
  421. return modes.indexOf(name);
  422. }
  423.  
  424. /**
  425. * 切换过滤模式
  426. * @param {String} value 过滤模式
  427. * @returns {String} 过滤模式
  428. */
  429. switchModeByName(value) {
  430. const index = this.getModeByName(value);
  431.  
  432. const nextIndex = (index + 1) % this.filterModes.length;
  433.  
  434. return this.filterModes[nextIndex];
  435. }
  436. }
  437.  
  438. /**
  439. * UI
  440. */
  441. class UI {
  442. /**
  443. * 标签
  444. */
  445. static label = "屏蔽";
  446.  
  447. /**
  448. * 设置
  449. */
  450. settings;
  451.  
  452. /**
  453. * API
  454. */
  455. api;
  456.  
  457. /**
  458. * 模块列表
  459. */
  460. modules = {};
  461.  
  462. /**
  463. * 菜单元素
  464. */
  465. menu = null;
  466.  
  467. /**
  468. * 视图元素
  469. */
  470. views = {};
  471.  
  472. /**
  473. * 初始化并绑定设置、API,注册(不可用)脚本菜单
  474. * @param {Settings} settings 设置
  475. * @param {API} api API
  476. */
  477. constructor(settings, api) {
  478. this.settings = settings;
  479. this.api = api;
  480.  
  481. this.init();
  482. }
  483.  
  484. /**
  485. * 初始化,创建基础视图,初始化通用设置
  486. */
  487. init() {
  488. const tabs = this.createTabs({
  489. className: "right_",
  490. });
  491.  
  492. const content = this.createElement("DIV", [], {
  493. style: "width: 80vw;",
  494. });
  495.  
  496. const container = this.createElement("DIV", [tabs, content]);
  497.  
  498. this.views = {
  499. tabs,
  500. content,
  501. container,
  502. };
  503.  
  504. this.initSettings();
  505. }
  506.  
  507. /**
  508. * 初始化设置
  509. */
  510. initSettings() {
  511. // 创建基础视图
  512. const settings = this.createElement("DIV", []);
  513.  
  514. // 添加设置项
  515. const add = (order, ...elements) => {
  516. const items = [...settings.childNodes];
  517.  
  518. if (items.find((item) => item.order === order)) {
  519. return;
  520. }
  521.  
  522. const item = this.createElement(
  523. "DIV",
  524. [...elements, this.createElement("BR", [])],
  525. {
  526. order,
  527. }
  528. );
  529.  
  530. const anchor = items.find((item) => item.order > order);
  531.  
  532. settings.insertBefore(item, anchor || null);
  533.  
  534. return item;
  535. };
  536.  
  537. // 绑定事件
  538. Object.assign(settings, {
  539. add,
  540. });
  541.  
  542. // 合并视图
  543. Object.assign(this.views, {
  544. settings,
  545. });
  546.  
  547. // 创建标签页
  548. const { tabs, content } = this.views;
  549.  
  550. this.createTab(tabs, "设置", Number.MAX_SAFE_INTEGER, {
  551. onclick: () => {
  552. content.innerHTML = "";
  553. content.appendChild(settings);
  554. },
  555. });
  556. }
  557.  
  558. /**
  559. * 弹窗确认
  560. * @param {String} message 提示信息
  561. * @returns {Promise}
  562. */
  563. confirm(message = "是否确认?") {
  564. return new Promise((resolve, reject) => {
  565. const result = confirm(message);
  566.  
  567. if (result) {
  568. resolve();
  569. return;
  570. }
  571.  
  572. reject();
  573. });
  574. }
  575.  
  576. /**
  577. * 折叠
  578. * @param {String | Number} key 标识
  579. * @param {HTMLElement} element 目标元素
  580. * @param {String} content 内容
  581. */
  582. collapse(key, element, content) {
  583. key = "collapsed_" + key;
  584.  
  585. element.innerHTML = `
  586. <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7;">
  587. <span class="crimson">Troll must die.</span>
  588. <a href="javascript:void(0)" onclick="[...document.getElementsByName('${key}')].forEach(item => item.style.display = '')">点击查看</a>
  589. <div style="display: none;" name="${key}">
  590. ${content}
  591. </div>
  592. </div>`;
  593. }
  594.  
  595. /**
  596. * 创建元素
  597. * @param {String} tagName 标签
  598. * @param {HTMLElement | HTMLElement[] | String} content 内容,元素或者 innerHTML
  599. * @param {*} properties 额外属性
  600. * @returns {HTMLElement} 元素
  601. */
  602. createElement(tagName, content, properties = {}) {
  603. const element = document.createElement(tagName);
  604.  
  605. // 写入内容
  606. if (typeof content === "string") {
  607. element.innerHTML = content;
  608. } else {
  609. if (Array.isArray(content) === false) {
  610. content = [content];
  611. }
  612.  
  613. content.forEach((item) => {
  614. if (item === null) {
  615. return;
  616. }
  617.  
  618. if (typeof item === "string") {
  619. element.append(item);
  620. return;
  621. }
  622.  
  623. element.appendChild(item);
  624. });
  625. }
  626.  
  627. // 对 A 标签的额外处理
  628. if (tagName.toUpperCase() === "A") {
  629. if (Object.hasOwn(properties, "href") === false) {
  630. properties.href = "javascript: void(0);";
  631. }
  632. }
  633.  
  634. // 附加属性
  635. Object.entries(properties).forEach(([key, value]) => {
  636. element[key] = value;
  637. });
  638.  
  639. return element;
  640. }
  641.  
  642. /**
  643. * 创建按钮
  644. * @param {String} text 文字
  645. * @param {Function} onclick 点击事件
  646. * @param {*} properties 额外属性
  647. */
  648. createButton(text, onclick, properties = {}) {
  649. return this.createElement("BUTTON", text, {
  650. ...properties,
  651. onclick,
  652. });
  653. }
  654.  
  655. /**
  656. * 创建按钮组
  657. * @param {Array} buttons 按钮集合
  658. */
  659. createButtonGroup(...buttons) {
  660. return this.createElement("DIV", buttons, {
  661. className: "filter-button-group",
  662. });
  663. }
  664.  
  665. /**
  666. * 创建表格
  667. * @param {Array} headers 表头集合
  668. * @param {*} properties 额外属性
  669. * @returns {HTMLElement} 元素和相关函数
  670. */
  671. createTable(headers, properties = {}) {
  672. const rows = [];
  673.  
  674. const ths = headers.map((item, index) =>
  675. this.createElement("TH", item.label, {
  676. ...item,
  677. className: `c${index + 1}`,
  678. })
  679. );
  680.  
  681. const tr =
  682. ths.length > 0
  683. ? this.createElement("TR", ths, {
  684. className: "block_txt_c0",
  685. })
  686. : null;
  687.  
  688. const thead = tr !== null ? this.createElement("THEAD", tr) : null;
  689.  
  690. const tbody = this.createElement("TBODY", []);
  691.  
  692. const table = this.createElement("TABLE", [thead, tbody], {
  693. ...properties,
  694. className: "filter-table forumbox",
  695. });
  696.  
  697. const wrapper = this.createElement("DIV", table, {
  698. className: "filter-table-wrapper",
  699. });
  700.  
  701. const intersectionObserver = new IntersectionObserver((entries) => {
  702. if (entries[0].intersectionRatio <= 0) return;
  703.  
  704. const list = rows.splice(0, 10);
  705.  
  706. if (list.length === 0) {
  707. return;
  708. }
  709.  
  710. intersectionObserver.disconnect();
  711.  
  712. tbody.append(...list);
  713.  
  714. intersectionObserver.observe(tbody.lastElementChild);
  715. });
  716.  
  717. const add = (...columns) => {
  718. const tds = columns.map((column, index) => {
  719. if (ths[index]) {
  720. const { center, ellipsis } = ths[index];
  721.  
  722. const properties = {};
  723.  
  724. if (center) {
  725. properties.style = "text-align: center;";
  726. }
  727.  
  728. if (ellipsis) {
  729. properties.className = "filter-text-ellipsis";
  730. }
  731.  
  732. column = this.createElement("DIV", column, properties);
  733. }
  734.  
  735. return this.createElement("TD", column, {
  736. className: `c${index + 1}`,
  737. });
  738. });
  739.  
  740. const tr = this.createElement("TR", tds, {
  741. className: `row${(rows.length % 2) + 1}`,
  742. });
  743.  
  744. intersectionObserver.disconnect();
  745.  
  746. rows.push(tr);
  747.  
  748. intersectionObserver.observe(tbody.lastElementChild || tbody);
  749. };
  750.  
  751. const update = (e, ...columns) => {
  752. const row = e.target.closest("TR");
  753.  
  754. if (row) {
  755. const tds = row.querySelectorAll("TD");
  756.  
  757. columns.map((column, index) => {
  758. if (ths[index]) {
  759. const { center, ellipsis } = ths[index];
  760.  
  761. const properties = {};
  762.  
  763. if (center) {
  764. properties.style = "text-align: center;";
  765. }
  766.  
  767. if (ellipsis) {
  768. properties.className = "filter-text-ellipsis";
  769. }
  770.  
  771. column = this.createElement("DIV", column, properties);
  772. }
  773.  
  774. if (tds[index]) {
  775. tds[index].innerHTML = "";
  776. tds[index].append(column);
  777. }
  778. });
  779. }
  780. };
  781.  
  782. const remove = (e) => {
  783. const row = e.target.closest("TR");
  784.  
  785. if (row) {
  786. tbody.removeChild(row);
  787. }
  788. };
  789.  
  790. const clear = () => {
  791. rows.splice(0);
  792. intersectionObserver.disconnect();
  793.  
  794. tbody.innerHTML = "";
  795. };
  796.  
  797. Object.assign(wrapper, {
  798. add,
  799. update,
  800. remove,
  801. clear,
  802. });
  803.  
  804. return wrapper;
  805. }
  806.  
  807. /**
  808. * 创建标签组
  809. * @param {*} properties 额外属性
  810. */
  811. createTabs(properties = {}) {
  812. const tabs = this.createElement(
  813. "DIV",
  814. `<table class="stdbtn" cellspacing="0">
  815. <tbody>
  816. <tr></tr>
  817. </tbody>
  818. </table>`,
  819. properties
  820. );
  821.  
  822. return this.createElement(
  823. "DIV",
  824. [
  825. tabs,
  826. this.createElement("DIV", [], {
  827. className: "clear",
  828. }),
  829. ],
  830. {
  831. style: "display: none; margin-bottom: 5px;",
  832. }
  833. );
  834. }
  835.  
  836. /**
  837. * 创建标签
  838. * @param {Element} tabs 标签组
  839. * @param {String} label 标签名称
  840. * @param {Number} order 标签顺序,重复则跳过
  841. * @param {*} properties 额外属性
  842. */
  843. createTab(tabs, label, order, properties = {}) {
  844. const group = tabs.querySelector("TR");
  845.  
  846. const items = [...group.childNodes];
  847.  
  848. if (items.find((item) => item.order === order)) {
  849. return;
  850. }
  851.  
  852. if (items.length > 0) {
  853. tabs.style.removeProperty("display");
  854. }
  855.  
  856. const tab = this.createElement("A", label, {
  857. ...properties,
  858. className: "nobr silver",
  859. onclick: () => {
  860. if (tab.className === "nobr") {
  861. return;
  862. }
  863.  
  864. group.querySelectorAll("A").forEach((item) => {
  865. if (item === tab) {
  866. item.className = "nobr";
  867. } else {
  868. item.className = "nobr silver";
  869. }
  870. });
  871.  
  872. if (properties.onclick) {
  873. properties.onclick();
  874. }
  875. },
  876. });
  877.  
  878. const wrapper = this.createElement("TD", tab, {
  879. order,
  880. });
  881.  
  882. const anchor = items.find((item) => item.order > order);
  883.  
  884. group.insertBefore(wrapper, anchor || null);
  885.  
  886. return wrapper;
  887. }
  888.  
  889. /**
  890. * 创建对话框
  891. * @param {HTMLElement | null} anchor 要绑定的元素,如果为空,直接弹出
  892. * @param {String} title 对话框的标题
  893. * @param {HTMLElement} content 对话框的内容
  894. */
  895. createDialog(anchor, title, content) {
  896. let window;
  897.  
  898. const show = () => {
  899. if (window === undefined) {
  900. window = commonui.createCommmonWindow();
  901. }
  902.  
  903. window._.addContent(null);
  904. window._.addTitle(title);
  905. window._.addContent(content);
  906. window._.show();
  907. };
  908.  
  909. if (anchor) {
  910. anchor.onclick = show;
  911. } else {
  912. show();
  913. }
  914.  
  915. return window;
  916. }
  917.  
  918. /**
  919. * 渲染菜单
  920. */
  921. renderMenu() {
  922. // 如果泥潭的右上角菜单还没有加载完成,说明模块尚未加载完毕,跳过
  923. const anchor = document.querySelector("#mainmenu .td:last-child");
  924.  
  925. if (anchor === null) {
  926. return;
  927. }
  928.  
  929. const menu = this.createElement("A", this.constructor.label, {
  930. className: "mmdefault nobr",
  931. });
  932.  
  933. const container = this.createElement("DIV", menu, {
  934. className: "td",
  935. });
  936.  
  937. // 插入菜单
  938. anchor.before(container);
  939.  
  940. // 绑定菜单元素
  941. this.menu = menu;
  942. }
  943.  
  944. /**
  945. * 渲染视图
  946. */
  947. renderView() {
  948. // 如果菜单还没有渲染,说明模块尚未加载完毕,跳过
  949. if (this.menu === null) {
  950. return;
  951. }
  952.  
  953. // 绑定菜单点击事件.
  954. this.createDialog(
  955. this.menu,
  956. this.constructor.label,
  957. this.views.container
  958. );
  959.  
  960. // 启用第一个模块
  961. this.views.tabs.querySelector("A").click();
  962. }
  963.  
  964. /**
  965. * 渲染
  966. */
  967. render() {
  968. this.renderMenu();
  969. this.renderView();
  970. }
  971. }
  972.  
  973. /**
  974. * 基础模块
  975. */
  976. class Module {
  977. /**
  978. * 模块名称
  979. */
  980. static name;
  981.  
  982. /**
  983. * 模块标签
  984. */
  985. static label;
  986.  
  987. /**
  988. * 顺序
  989. */
  990. static order;
  991.  
  992. /**
  993. * 依赖模块
  994. */
  995. static depends = [];
  996.  
  997. /**
  998. * 附加模块
  999. */
  1000. static addons = [];
  1001.  
  1002. /**
  1003. * 设置
  1004. */
  1005. settings;
  1006.  
  1007. /**
  1008. * API
  1009. */
  1010. api;
  1011.  
  1012. /**
  1013. * UI
  1014. */
  1015. ui;
  1016.  
  1017. /**
  1018. * 过滤列表
  1019. */
  1020. data = [];
  1021.  
  1022. /**
  1023. * 依赖模块
  1024. */
  1025. depends = {};
  1026.  
  1027. /**
  1028. * 附加模块
  1029. */
  1030. addons = {};
  1031.  
  1032. /**
  1033. * 视图元素
  1034. */
  1035. views = {};
  1036.  
  1037. /**
  1038. * 初始化并绑定设置、API、UI、过滤列表,注册(不可用) UI
  1039. * @param {Settings} settings 设置
  1040. * @param {API} api API
  1041. * @param {UI} ui UI
  1042. */
  1043. constructor(settings, api, ui, data) {
  1044. this.settings = settings;
  1045. this.api = api;
  1046. this.ui = ui;
  1047.  
  1048. this.data = data;
  1049.  
  1050. this.init();
  1051. }
  1052.  
  1053. /**
  1054. * 创建实例
  1055. * @param {Settings} settings 设置
  1056. * @param {API} api API
  1057. * @param {UI} ui UI
  1058. * @param {Array} data 过滤列表
  1059. * @returns {Module | null} 成功后返回模块实例
  1060. */
  1061. static create(settings, api, ui, data) {
  1062. // 读取设置里的模块列表
  1063. const modules = settings.modules;
  1064.  
  1065. // 如果不包含自己或依赖的模块,则返回空
  1066. const index = [this, ...this.depends].findIndex(
  1067. (module) => modules.includes(module.name) === false
  1068. );
  1069.  
  1070. if (index >= 0) {
  1071. return null;
  1072. }
  1073.  
  1074. // 创建实例
  1075. const instance = new this(settings, api, ui, data);
  1076.  
  1077. // 返回实例
  1078. return instance;
  1079. }
  1080.  
  1081. /**
  1082. * 判断指定附加模块是否启用
  1083. * @param {typeof Module} module 模块
  1084. */
  1085. hasAddon(module) {
  1086. return Object.hasOwn(this.addons, module.name);
  1087. }
  1088.  
  1089. /**
  1090. * 初始化,创建基础视图和组件
  1091. */
  1092. init() {
  1093. if (this.views.container) {
  1094. this.destroy();
  1095. }
  1096.  
  1097. const { ui } = this;
  1098.  
  1099. const container = ui.createElement("DIV", []);
  1100.  
  1101. this.views = {
  1102. container,
  1103. };
  1104.  
  1105. this.initComponents();
  1106. }
  1107.  
  1108. /**
  1109. * 初始化组件
  1110. */
  1111. initComponents() {}
  1112.  
  1113. /**
  1114. * 销毁
  1115. */
  1116. destroy() {
  1117. Object.values(this.views).forEach((view) => {
  1118. if (view.parentNode) {
  1119. view.parentNode.removeChild(view);
  1120. }
  1121. });
  1122.  
  1123. this.views = {};
  1124. }
  1125.  
  1126. /**
  1127. * 渲染
  1128. * @param {HTMLElement} container 容器
  1129. */
  1130. render(container) {
  1131. container.innerHTML = "";
  1132. container.appendChild(this.views.container);
  1133. }
  1134.  
  1135. /**
  1136. * 过滤
  1137. * @param {*} item 绑定的 nFilter
  1138. * @param {*} result 过滤结果
  1139. */
  1140. async filter(item, result) {}
  1141.  
  1142. /**
  1143. * 通知
  1144. * @param {*} item 绑定的 nFilter
  1145. * @param {*} result 过滤结果
  1146. */
  1147. async notify(item, result) {}
  1148. }
  1149.  
  1150. /**
  1151. * 过滤器
  1152. */
  1153. class Filter {
  1154. /**
  1155. * 设置
  1156. */
  1157. settings;
  1158.  
  1159. /**
  1160. * API
  1161. */
  1162. api;
  1163.  
  1164. /**
  1165. * UI
  1166. */
  1167. ui;
  1168.  
  1169. /**
  1170. * 过滤列表
  1171. */
  1172. data = [];
  1173.  
  1174. /**
  1175. * 模块列表
  1176. */
  1177. modules = {};
  1178.  
  1179. /**
  1180. * 初始化并绑定设置、API、UI
  1181. * @param {Settings} settings 设置
  1182. * @param {API} api API
  1183. * @param {UI} ui UI
  1184. */
  1185. constructor(settings, api, ui) {
  1186. this.settings = settings;
  1187. this.api = api;
  1188. this.ui = ui;
  1189. }
  1190.  
  1191. /**
  1192. * 绑定两个模块的互相关系
  1193. * @param {Module} moduleA 模块A
  1194. * @param {Module} moduleB 模块B
  1195. */
  1196. bindModule(moduleA, moduleB) {
  1197. const nameA = moduleA.constructor.name;
  1198. const nameB = moduleB.constructor.name;
  1199.  
  1200. // A 依赖 B
  1201. if (moduleA.constructor.depends.findIndex((i) => i.name === nameB) >= 0) {
  1202. moduleA.depends[nameB] = moduleB;
  1203. moduleA.init();
  1204. }
  1205.  
  1206. // B 依赖 A
  1207. if (moduleB.constructor.depends.findIndex((i) => i.name === nameA) >= 0) {
  1208. moduleB.depends[nameA] = moduleA;
  1209. moduleB.init();
  1210. }
  1211.  
  1212. // A 附加 B
  1213. if (moduleA.constructor.addons.findIndex((i) => i.name === nameB) >= 0) {
  1214. moduleA.addons[nameB] = moduleB;
  1215. moduleA.init();
  1216. }
  1217.  
  1218. // B 附加 A
  1219. if (moduleB.constructor.addons.findIndex((i) => i.name === nameA) >= 0) {
  1220. moduleB.addons[nameA] = moduleA;
  1221. moduleB.init();
  1222. }
  1223. }
  1224.  
  1225. /**
  1226. * 加载模块
  1227. * @param {typeof Module} module 模块
  1228. */
  1229. initModule(module) {
  1230. // 如果已经加载过则跳过
  1231. if (Object.hasOwn(this.modules, module.name)) {
  1232. return;
  1233. }
  1234.  
  1235. // 创建模块
  1236. const instance = module.create(
  1237. this.settings,
  1238. this.api,
  1239. this.ui,
  1240. this.data
  1241. );
  1242.  
  1243. // 如果创建失败则跳过
  1244. if (instance === null) {
  1245. return;
  1246. }
  1247.  
  1248. // 绑定依赖模块和附加模块
  1249. Object.values(this.modules).forEach((item) => {
  1250. this.bindModule(item, instance);
  1251. });
  1252.  
  1253. // 合并模块
  1254. this.modules[module.name] = instance;
  1255.  
  1256. // 按照顺序重新整理模块
  1257. this.modules = Tools.sortBy(
  1258. Object.values(this.modules),
  1259. (item) => item.constructor.order
  1260. ).reduce(
  1261. (result, item) => ({
  1262. ...result,
  1263. [item.constructor.name]: item,
  1264. }),
  1265. {}
  1266. );
  1267. }
  1268.  
  1269. /**
  1270. * 加载模块列表
  1271. * @param {typeof Module[]} modules 模块列表
  1272. */
  1273. initModules(...modules) {
  1274. // 根据依赖和附加模块决定初始化的顺序
  1275. Tools.sortBy(
  1276. modules,
  1277. (item) => item.depends.length,
  1278. (item) => item.addons.length
  1279. ).forEach((module) => {
  1280. this.initModule(module);
  1281. });
  1282. }
  1283.  
  1284. /**
  1285. * 添加到过滤列表
  1286. * @param {*} item 绑定的 nFilter
  1287. */
  1288. pushData(item) {
  1289. // 清除掉无效数据
  1290. for (let i = 0; i < this.data.length; ) {
  1291. if (document.body.contains(this.data[i].container) === false) {
  1292. this.data.splice(i, 1);
  1293. continue;
  1294. }
  1295.  
  1296. i += 1;
  1297. }
  1298.  
  1299. // 加入过滤列表
  1300. if (this.data.includes(item) === false) {
  1301. this.data.push(item);
  1302. }
  1303. }
  1304.  
  1305. /**
  1306. * 判断指定 UID 是否是自己
  1307. * @param {Number} uid 用户 ID
  1308. */
  1309. isSelf(uid) {
  1310. return unsafeWindow.__CURRENT_UID === uid;
  1311. }
  1312.  
  1313. /**
  1314. * 获取过滤模式
  1315. * @param {*} item 绑定的 nFilter
  1316. */
  1317. async getFilterMode(item) {
  1318. // 获取链接参数
  1319. const params = new URLSearchParams(location.search);
  1320.  
  1321. // 跳过屏蔽(插件自定义)
  1322. if (params.has("nofilter")) {
  1323. return;
  1324. }
  1325.  
  1326. // 收藏
  1327. if (params.has("favor")) {
  1328. return;
  1329. }
  1330.  
  1331. // 只看某人
  1332. if (params.has("authorid")) {
  1333. return;
  1334. }
  1335.  
  1336. // 跳过自己
  1337. if (this.isSelf(item.uid)) {
  1338. return;
  1339. }
  1340.  
  1341. // 声明结果
  1342. const result = {
  1343. mode: -1,
  1344. reason: ``,
  1345. };
  1346.  
  1347. // 根据模块依次过滤
  1348. for (const module of Object.values(this.modules)) {
  1349. await module.filter(item, result);
  1350. }
  1351.  
  1352. // 写入过滤模式和过滤原因
  1353. item.filterMode = this.settings.getNameByMode(result.mode);
  1354. item.reason = result.reason;
  1355.  
  1356. // 通知各模块过滤结果
  1357. for (const module of Object.values(this.modules)) {
  1358. await module.notify(item, result);
  1359. }
  1360.  
  1361. // 继承模式下返回默认过滤模式
  1362. if (item.filterMode === "继承") {
  1363. return this.settings.defaultFilterMode;
  1364. }
  1365.  
  1366. // 返回结果
  1367. return item.filterMode;
  1368. }
  1369.  
  1370. /**
  1371. * 过滤主题
  1372. * @param {*} item 主题内容,见 commonui.topicArg.data
  1373. */
  1374. filterTopic(item) {
  1375. // 绑定事件
  1376. if (item.nFilter === undefined) {
  1377. // 主题 ID
  1378. const tid = item[8];
  1379.  
  1380. // 主题版面 ID
  1381. const fid = item[7];
  1382.  
  1383. // 主题标题
  1384. const title = item[1];
  1385. const subject = title.innerText;
  1386.  
  1387. // 主题作者
  1388. const author = item[2];
  1389. const uid =
  1390. parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) || 0;
  1391. const username = author.innerText;
  1392.  
  1393. // 增加操作角标
  1394. const action = (() => {
  1395. const anchor = item[2].parentNode;
  1396.  
  1397. const element = this.ui.createElement("DIV", "", {
  1398. style: Object.entries({
  1399. position: "absolute",
  1400. right: 0,
  1401. bottom: 0,
  1402. padding: "6px",
  1403. "clip-path": "polygon(100% 0, 100% 100%, 0 100%)",
  1404. })
  1405. .map(([key, value]) => `${key}: ${value}`)
  1406. .join(";"),
  1407. });
  1408.  
  1409. anchor.style.position = "relative";
  1410. anchor.appendChild(element);
  1411.  
  1412. return element;
  1413. })();
  1414.  
  1415. // 主题杂项
  1416. const topicMisc = item[16];
  1417.  
  1418. // 主题容器
  1419. const container = title.closest("tr");
  1420.  
  1421. // 过滤函数
  1422. const execute = async () => {
  1423. // 获取过滤模式
  1424. const filterMode = await this.getFilterMode(item.nFilter);
  1425.  
  1426. // 样式处理
  1427. (() => {
  1428. // 还原样式
  1429. // TODO 应该整体采用 className 来实现
  1430. (() => {
  1431. // 标记模式
  1432. title.style.removeProperty("textDecoration");
  1433.  
  1434. // 遮罩模式
  1435. title.classList.remove("filter-mask");
  1436. author.classList.remove("filter-mask");
  1437. })();
  1438.  
  1439. // 样式处理
  1440. (() => {
  1441. // 标记模式下,主题标记会有删除线标识
  1442. if (filterMode === "标记") {
  1443. title.style.textDecoration = "line-through";
  1444. return;
  1445. }
  1446.  
  1447. // 遮罩模式下,主题和作者会有遮罩样式
  1448. if (filterMode === "遮罩") {
  1449. title.classList.add("filter-mask");
  1450. author.classList.add("filter-mask");
  1451. return;
  1452. }
  1453.  
  1454. // 隐藏模式下,容器会被隐藏
  1455. if (filterMode === "隐藏") {
  1456. container.style.display = "none";
  1457. return;
  1458. }
  1459. })();
  1460.  
  1461. // 非隐藏模式下,恢复显示
  1462. if (filterMode !== "隐藏") {
  1463. container.style.removeProperty("display");
  1464. }
  1465. })();
  1466. };
  1467.  
  1468. // 绑定事件
  1469. item.nFilter = {
  1470. tid,
  1471. pid: 0,
  1472. uid,
  1473. fid,
  1474. username,
  1475. container,
  1476. title,
  1477. author,
  1478. subject,
  1479. topicMisc,
  1480. action,
  1481. tags: null,
  1482. execute,
  1483. };
  1484.  
  1485. // 添加至列表
  1486. this.pushData(item.nFilter);
  1487. }
  1488.  
  1489. // 开始过滤
  1490. item.nFilter.execute();
  1491. }
  1492.  
  1493. /**
  1494. * 过滤回复
  1495. * @param {*} item 回复内容,见 commonui.postArg.data
  1496. */
  1497. filterReply(item) {
  1498. // 绑定事件
  1499. if (item.nFilter === undefined) {
  1500. // 主题 ID
  1501. const tid = item.tid;
  1502.  
  1503. // 回复 ID
  1504. const pid = item.pid;
  1505.  
  1506. // 判断是否是楼层
  1507. const isFloor = typeof item.i === "number";
  1508.  
  1509. // 回复容器
  1510. const container = isFloor
  1511. ? item.uInfoC.closest("tr")
  1512. : item.uInfoC.closest(".comment_c");
  1513.  
  1514. // 回复标题
  1515. const title = item.subjectC;
  1516. const subject = title.innerText;
  1517.  
  1518. // 回复内容
  1519. const content = item.contentC;
  1520. const contentBak = content.innerHTML;
  1521.  
  1522. // 回复作者
  1523. const author =
  1524. container.querySelector(".posterInfoLine") || item.uInfoC;
  1525. const uid = parseInt(item.pAid, 10) || 0;
  1526. const username = author.querySelector(".author").innerText;
  1527. const avatar = author.querySelector(".avatar");
  1528.  
  1529. // 找到用户 ID,将其视为操作按钮
  1530. const action = container.querySelector('[name="uid"]');
  1531.  
  1532. // 创建一个元素,用于展示标记列表
  1533. // 贴条和高赞不显示
  1534. const tags = (() => {
  1535. if (isFloor === false) {
  1536. return null;
  1537. }
  1538.  
  1539. const element = document.createElement("div");
  1540.  
  1541. element.className = "filter-tags";
  1542.  
  1543. author.appendChild(element);
  1544.  
  1545. return element;
  1546. })();
  1547.  
  1548. // 过滤函数
  1549. const execute = async () => {
  1550. // 获取过滤模式
  1551. const filterMode = await this.getFilterMode(item.nFilter);
  1552.  
  1553. // 样式处理
  1554. (() => {
  1555. // 还原样式
  1556. // TODO 应该整体采用 className 来实现
  1557. (() => {
  1558. // 标记模式
  1559. if (avatar) {
  1560. avatar.style.removeProperty("display");
  1561. }
  1562.  
  1563. content.innerHTML = contentBak;
  1564.  
  1565. // 遮罩模式
  1566. const caption = container.parentNode.querySelector("CAPTION");
  1567.  
  1568. if (caption) {
  1569. container.parentNode.removeChild(caption);
  1570. container.style.removeProperty("display");
  1571. }
  1572. })();
  1573.  
  1574. // 样式处理
  1575. (() => {
  1576. // 标记模式下,隐藏头像,采用泥潭的折叠样式
  1577. if (filterMode === "标记") {
  1578. if (avatar) {
  1579. avatar.style.display = "none";
  1580. }
  1581.  
  1582. this.ui.collapse(uid, content, contentBak);
  1583. return;
  1584. }
  1585.  
  1586. // 遮罩模式下,楼层会有遮罩样式
  1587. if (filterMode === "遮罩") {
  1588. const caption = document.createElement("CAPTION");
  1589.  
  1590. if (isFloor) {
  1591. caption.className = "filter-mask filter-mask-block";
  1592. } else {
  1593. caption.className = "filter-mask filter-mask-block left";
  1594. caption.style.width = "47%";
  1595. }
  1596.  
  1597. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  1598. caption.onclick = () => {
  1599. const caption = container.parentNode.querySelector("CAPTION");
  1600.  
  1601. if (caption) {
  1602. container.parentNode.removeChild(caption);
  1603. container.style.removeProperty("display");
  1604. }
  1605. };
  1606.  
  1607. container.parentNode.insertBefore(caption, container);
  1608. container.style.display = "none";
  1609. return;
  1610. }
  1611.  
  1612. // 隐藏模式下,容器会被隐藏
  1613. if (filterMode === "隐藏") {
  1614. container.style.display = "none";
  1615. return;
  1616. }
  1617. })();
  1618.  
  1619. // 非隐藏模式下,恢复显示
  1620. // 楼层的遮罩模式下仍需隐藏
  1621. if (["遮罩", "隐藏"].includes(filterMode) === false) {
  1622. container.style.removeProperty("display");
  1623. }
  1624. })();
  1625.  
  1626. // 过滤引用
  1627. this.filterQuote(item);
  1628. };
  1629.  
  1630. // 绑定事件
  1631. item.nFilter = {
  1632. tid,
  1633. pid,
  1634. uid,
  1635. fid: null,
  1636. username,
  1637. container,
  1638. title,
  1639. author,
  1640. subject,
  1641. content: content.innerText,
  1642. topicMisc: "",
  1643. action,
  1644. tags,
  1645. execute,
  1646. };
  1647.  
  1648. // 添加至列表
  1649. this.pushData(item.nFilter);
  1650. }
  1651.  
  1652. // 开始过滤
  1653. item.nFilter.execute();
  1654. }
  1655.  
  1656. /**
  1657. * 过滤引用
  1658. * @param {*} item 回复内容,见 commonui.postArg.data
  1659. */
  1660. filterQuote(item) {
  1661. // 未绑定事件,直接跳过
  1662. if (item.nFilter === undefined) {
  1663. return;
  1664. }
  1665.  
  1666. // 回复内容
  1667. const content = item.contentC;
  1668.  
  1669. // 找到所有引用
  1670. const quotes = content.querySelectorAll(".quote");
  1671.  
  1672. // 处理引用
  1673. [...quotes].map(async (quote) => {
  1674. const uid = (() => {
  1675. const ele = quote.querySelector("a[href^='/nuke.php']");
  1676.  
  1677. if (ele) {
  1678. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  1679.  
  1680. if (res) {
  1681. return parseInt(res[1], 10);
  1682. }
  1683. }
  1684.  
  1685. return 0;
  1686. })();
  1687.  
  1688. const { tid, pid } = (() => {
  1689. const ele = quote.querySelector("[title='快速浏览这个帖子']");
  1690.  
  1691. if (ele) {
  1692. const res = ele
  1693. .getAttribute("onclick")
  1694. .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);
  1695.  
  1696. if (res) {
  1697. return {
  1698. tid: parseInt(res[2], 10),
  1699. pid: parseInt(res[3], 10) || 0,
  1700. };
  1701. }
  1702. }
  1703.  
  1704. return {};
  1705. })();
  1706.  
  1707. // 临时的 nFilter
  1708. const nFilter = {
  1709. uid,
  1710. tid,
  1711. pid,
  1712. fid: null,
  1713. subject: "",
  1714. content: quote.innerText,
  1715. topicMisc: "",
  1716. action: null,
  1717. tags: null,
  1718. };
  1719.  
  1720. // 获取过滤模式
  1721. const filterMode = await this.getFilterMode(nFilter);
  1722.  
  1723. (() => {
  1724. if (filterMode === "标记") {
  1725. this.ui.collapse(uid, quote, quote.innerHTML);
  1726. return;
  1727. }
  1728.  
  1729. if (filterMode === "遮罩") {
  1730. const source = document.createElement("DIV");
  1731.  
  1732. source.innerHTML = quote.innerHTML;
  1733. source.style.display = "none";
  1734.  
  1735. const caption = document.createElement("CAPTION");
  1736.  
  1737. caption.className = "filter-mask filter-mask-block";
  1738.  
  1739. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  1740. caption.onclick = () => {
  1741. quote.removeChild(caption);
  1742.  
  1743. source.style.display = "";
  1744. };
  1745.  
  1746. quote.innerHTML = "";
  1747. quote.appendChild(source);
  1748. quote.appendChild(caption);
  1749. return;
  1750. }
  1751.  
  1752. if (filterMode === "隐藏") {
  1753. quote.innerHTML = "";
  1754. return;
  1755. }
  1756. })();
  1757.  
  1758. // 绑定引用
  1759. item.nFilter.quotes = item.nFilter.quotes || {};
  1760. item.nFilter.quotes[uid] = nFilter.filterMode;
  1761. });
  1762. }
  1763. }
  1764.  
  1765. /**
  1766. * 列表模块
  1767. */
  1768. class ListModule extends Module {
  1769. /**
  1770. * 模块名称
  1771. */
  1772. static name = "list";
  1773.  
  1774. /**
  1775. * 模块标签
  1776. */
  1777. static label = "列表";
  1778.  
  1779. /**
  1780. * 顺序
  1781. */
  1782. static order = 10;
  1783.  
  1784. /**
  1785. * 表格列
  1786. * @returns {Array} 表格列集合
  1787. */
  1788. columns() {
  1789. return [
  1790. { label: "内容", ellipsis: true },
  1791. { label: "过滤模式", center: true, width: 1 },
  1792. { label: "原因", width: 1 },
  1793. ];
  1794. }
  1795.  
  1796. /**
  1797. * 表格项
  1798. * @param {*} item 绑定的 nFilter
  1799. * @returns {Array} 表格项集合
  1800. */
  1801. column(item) {
  1802. const { ui } = this;
  1803. const { tid, pid, filterMode, reason } = item;
  1804.  
  1805. // 移除 BR 标签
  1806. item.content = (item.content || "").replace(/<br>/g, "");
  1807.  
  1808. // 内容
  1809. const content = (() => {
  1810. if (pid) {
  1811. return ui.createElement("A", item.content, {
  1812. href: `/read.php?pid=${pid}&nofilter`,
  1813. title: item.content,
  1814. });
  1815. }
  1816.  
  1817. // 如果有 TID 但没有标题,是引用,采用内容逻辑
  1818. if (item.subject.length === 0) {
  1819. return ui.createElement("A", item.content, {
  1820. href: `/read.php?tid=${tid}&nofilter`,
  1821. title: item.content,
  1822. });
  1823. }
  1824.  
  1825. return ui.createElement("A", item.subject, {
  1826. href: `/read.php?tid=${tid}&nofilter`,
  1827. title: item.content,
  1828. className: "b nobr",
  1829. });
  1830. })();
  1831.  
  1832. return [content, filterMode, reason];
  1833. }
  1834.  
  1835. /**
  1836. * 初始化组件
  1837. */
  1838. initComponents() {
  1839. super.initComponents();
  1840.  
  1841. const { tabs, content } = this.ui.views;
  1842.  
  1843. const table = this.ui.createTable(this.columns());
  1844.  
  1845. const tab = this.ui.createTab(
  1846. tabs,
  1847. this.constructor.label,
  1848. this.constructor.order,
  1849. {
  1850. onclick: () => {
  1851. this.render(content);
  1852. },
  1853. }
  1854. );
  1855.  
  1856. Object.assign(this.views, {
  1857. tab,
  1858. table,
  1859. });
  1860.  
  1861. this.views.container.appendChild(table);
  1862. }
  1863.  
  1864. /**
  1865. * 渲染
  1866. * @param {HTMLElement} container 容器
  1867. */
  1868. render(container) {
  1869. super.render(container);
  1870.  
  1871. const { table } = this.views;
  1872.  
  1873. if (table) {
  1874. const { add, clear } = table;
  1875.  
  1876. clear();
  1877.  
  1878. const list = this.data.filter((item) => {
  1879. return (item.filterMode || "显示") !== "显示";
  1880. });
  1881.  
  1882. Object.values(list).forEach((item) => {
  1883. const column = this.column(item);
  1884.  
  1885. add(...column);
  1886. });
  1887. }
  1888. }
  1889.  
  1890. /**
  1891. * 通知
  1892. * @param {*} item 绑定的 nFilter
  1893. */
  1894. async notify() {
  1895. // 获取过滤后的数量
  1896. const count = this.data.filter((item) => {
  1897. return (item.filterMode || "显示") !== "显示";
  1898. }).length;
  1899.  
  1900. // 更新菜单文字
  1901. const { ui } = this;
  1902. const { menu } = ui;
  1903.  
  1904. if (menu === null) {
  1905. return;
  1906. }
  1907.  
  1908. if (count) {
  1909. menu.innerHTML = `${ui.constructor.label} <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
  1910. } else {
  1911. menu.innerHTML = `${ui.constructor.label}`;
  1912. }
  1913.  
  1914. // 重新渲染
  1915. // TODO 应该给 table 增加一个判重的逻辑,这样只需要更新过滤后的内容即可
  1916. const { tab } = this.views;
  1917.  
  1918. if (tab.querySelector("A").className === "nobr") {
  1919. this.render(ui.views.content);
  1920. }
  1921. }
  1922. }
  1923.  
  1924. /**
  1925. * 用户模块
  1926. */
  1927. class UserModule extends Module {
  1928. /**
  1929. * 模块名称
  1930. */
  1931. static name = "user";
  1932.  
  1933. /**
  1934. * 模块标签
  1935. */
  1936. static label = "用户";
  1937.  
  1938. /**
  1939. * 顺序
  1940. */
  1941. static order = 20;
  1942.  
  1943. /**
  1944. * 获取列表
  1945. */
  1946. get list() {
  1947. return this.settings.users;
  1948. }
  1949.  
  1950. /**
  1951. * 获取用户
  1952. * @param {Number} uid 用户 ID
  1953. */
  1954. get(uid) {
  1955. // 获取列表
  1956. const list = this.list;
  1957.  
  1958. // 如果存在,则返回信息
  1959. if (list[uid]) {
  1960. return list[uid];
  1961. }
  1962.  
  1963. return null;
  1964. }
  1965.  
  1966. /**
  1967. * 添加用户
  1968. * @param {Number} uid 用户 ID
  1969. */
  1970. add(uid, values) {
  1971. // 获取列表
  1972. const list = this.list;
  1973.  
  1974. // 如果已存在,则返回信息
  1975. if (list[uid]) {
  1976. return list[uid];
  1977. }
  1978.  
  1979. // 写入用户信息
  1980. list[uid] = values;
  1981.  
  1982. // 保存数据
  1983. this.settings.users = list;
  1984.  
  1985. // 重新过滤
  1986. this.reFilter(uid);
  1987.  
  1988. // 返回添加的用户
  1989. return values;
  1990. }
  1991.  
  1992. /**
  1993. * 编辑用户
  1994. * @param {Number} uid 用户 ID
  1995. * @param {*} values 用户信息
  1996. */
  1997. update(uid, values) {
  1998. // 获取列表
  1999. const list = this.list;
  2000.  
  2001. // 如果不存在则跳过
  2002. if (Object.hasOwn(list, uid) === false) {
  2003. return null;
  2004. }
  2005.  
  2006. // 获取用户
  2007. const entity = list[uid];
  2008.  
  2009. // 更新用户
  2010. Object.assign(entity, values);
  2011.  
  2012. // 保存数据
  2013. this.settings.users = list;
  2014.  
  2015. // 重新过滤
  2016. this.reFilter(uid);
  2017.  
  2018. // 返回编辑的用户
  2019. return entity;
  2020. }
  2021.  
  2022. /**
  2023. * 删除用户
  2024. * @param {Number} uid 用户 ID
  2025. * @returns {Object | null} 删除的用户
  2026. */
  2027. remove(uid) {
  2028. // 获取列表
  2029. const list = this.list;
  2030.  
  2031. // 如果不存在则跳过
  2032. if (Object.hasOwn(list, uid) === false) {
  2033. return null;
  2034. }
  2035.  
  2036. // 获取用户
  2037. const entity = list[uid];
  2038.  
  2039. // 删除用户
  2040. delete list[uid];
  2041.  
  2042. // 保存数据
  2043. this.settings.users = list;
  2044.  
  2045. // 重新过滤
  2046. this.reFilter(uid);
  2047.  
  2048. // 返回删除的用户
  2049. return entity;
  2050. }
  2051.  
  2052. /**
  2053. * 格式化
  2054. * @param {Number} uid 用户 ID
  2055. * @param {String | undefined} name 用户名称
  2056. */
  2057. format(uid, name) {
  2058. if (uid <= 0) {
  2059. return null;
  2060. }
  2061.  
  2062. const { ui } = this;
  2063.  
  2064. const user = this.get(uid);
  2065.  
  2066. if (user) {
  2067. name = user.name;
  2068. }
  2069.  
  2070. const username = name ? "@" + name : "#" + uid;
  2071.  
  2072. return ui.createElement("A", `[${username}]`, {
  2073. className: "b nobr",
  2074. href: `/nuke.php?func=ucp&uid=${uid}`,
  2075. });
  2076. }
  2077.  
  2078. /**
  2079. * 表格列
  2080. * @returns {Array} 表格列集合
  2081. */
  2082. columns() {
  2083. return [
  2084. { label: "昵称" },
  2085. { label: "过滤模式", center: true, width: 1 },
  2086. { label: "操作", width: 1 },
  2087. ];
  2088. }
  2089.  
  2090. /**
  2091. * 表格项
  2092. * @param {*} item 用户信息
  2093. * @returns {Array} 表格项集合
  2094. */
  2095. column(item) {
  2096. const { ui } = this;
  2097. const { table } = this.views;
  2098. const { id, name, filterMode } = item;
  2099.  
  2100. // 昵称
  2101. const user = this.format(id, name);
  2102.  
  2103. // 切换过滤模式
  2104. const switchMode = ui.createButton(
  2105. filterMode || this.settings.filterModes[0],
  2106. () => {
  2107. const newMode = this.settings.switchModeByName(switchMode.innerText);
  2108.  
  2109. this.update(id, {
  2110. filterMode: newMode,
  2111. });
  2112.  
  2113. switchMode.innerText = newMode;
  2114. }
  2115. );
  2116.  
  2117. // 操作
  2118. const buttons = (() => {
  2119. const remove = ui.createButton("删除", (e) => {
  2120. ui.confirm().then(() => {
  2121. this.remove(id);
  2122.  
  2123. table.remove(e);
  2124. });
  2125. });
  2126.  
  2127. return ui.createButtonGroup(remove);
  2128. })();
  2129.  
  2130. return [user, switchMode, buttons];
  2131. }
  2132.  
  2133. /**
  2134. * 初始化组件
  2135. */
  2136. initComponents() {
  2137. super.initComponents();
  2138.  
  2139. const { ui } = this;
  2140. const { tabs, content, settings } = ui.views;
  2141. const { add } = settings;
  2142.  
  2143. const table = ui.createTable(this.columns());
  2144.  
  2145. const tab = ui.createTab(
  2146. tabs,
  2147. this.constructor.label,
  2148. this.constructor.order,
  2149. {
  2150. onclick: () => {
  2151. this.render(content);
  2152. },
  2153. }
  2154. );
  2155.  
  2156. Object.assign(this.views, {
  2157. tab,
  2158. table,
  2159. });
  2160.  
  2161. this.views.container.appendChild(table);
  2162.  
  2163. // 删除非激活中的用户
  2164. {
  2165. const list = ui.createElement("DIV", [], {
  2166. style: "white-space: normal;",
  2167. });
  2168.  
  2169. const button = ui.createButton("删除非激活中的用户", () => {
  2170. ui.confirm().then(() => {
  2171. list.innerHTML = "";
  2172.  
  2173. const users = Object.values(this.list);
  2174.  
  2175. const waitingQueue = users.map(
  2176. ({ id }) =>
  2177. () =>
  2178. this.api.getUserInfo(id).then(({ bit }) => {
  2179. const activeInfo = commonui.activeInfo(0, 0, bit);
  2180. const activeType = activeInfo[1];
  2181.  
  2182. if (["ACTIVED", "LINKED"].includes(activeType)) {
  2183. return;
  2184. }
  2185.  
  2186. list.append(this.format(id));
  2187.  
  2188. this.remove(id);
  2189. })
  2190. );
  2191.  
  2192. const queueLength = waitingQueue.length;
  2193.  
  2194. const execute = () => {
  2195. if (waitingQueue.length) {
  2196. const next = waitingQueue.shift();
  2197.  
  2198. button.disabled = true;
  2199. button.innerHTML = `删除非激活中的用户 (${
  2200. queueLength - waitingQueue.length
  2201. }/${queueLength})`;
  2202.  
  2203. next().finally(execute);
  2204. return;
  2205. }
  2206.  
  2207. button.disabled = false;
  2208. };
  2209.  
  2210. execute();
  2211. });
  2212. });
  2213.  
  2214. const element = ui.createElement("DIV", [button, list]);
  2215.  
  2216. add(this.constructor.order + 0, element);
  2217. }
  2218. }
  2219.  
  2220. /**
  2221. * 渲染
  2222. * @param {HTMLElement} container 容器
  2223. */
  2224. render(container) {
  2225. super.render(container);
  2226.  
  2227. const { table } = this.views;
  2228.  
  2229. if (table) {
  2230. const { add, clear } = table;
  2231.  
  2232. clear();
  2233.  
  2234. Object.values(this.list).forEach((item) => {
  2235. const column = this.column(item);
  2236.  
  2237. add(...column);
  2238. });
  2239. }
  2240. }
  2241.  
  2242. /**
  2243. * 渲染详情
  2244. * @param {Number} uid 用户 ID
  2245. * @param {String | undefined} name 用户名称
  2246. * @param {Function} callback 回调函数
  2247. */
  2248. renderDetails(uid, name, callback = () => {}) {
  2249. const { ui, settings } = this;
  2250.  
  2251. // 只允许同时存在一个详情页
  2252. if (this.views.details) {
  2253. if (this.views.details.parentNode) {
  2254. this.views.details.parentNode.removeChild(this.views.details);
  2255. }
  2256. }
  2257.  
  2258. // 获取用户信息
  2259. const user = this.get(uid);
  2260.  
  2261. if (user) {
  2262. name = user.name;
  2263. }
  2264.  
  2265. const title =
  2266. (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;
  2267.  
  2268. const filterMode = user ? user.filterMode : settings.filterModes[0];
  2269.  
  2270. const switchMode = ui.createButton(filterMode, () => {
  2271. const newMode = settings.switchModeByName(switchMode.innerText);
  2272.  
  2273. switchMode.innerText = newMode;
  2274. });
  2275.  
  2276. const buttons = ui.createElement(
  2277. "DIV",
  2278. (() => {
  2279. const remove = user
  2280. ? ui.createButton("删除", () => {
  2281. ui.confirm().then(() => {
  2282. this.remove(uid);
  2283.  
  2284. this.views.details._.hide();
  2285.  
  2286. callback("REMOVE");
  2287. });
  2288. })
  2289. : null;
  2290.  
  2291. const save = ui.createButton("保存", () => {
  2292. if (user === null) {
  2293. const entity = this.add(uid, {
  2294. id: uid,
  2295. name,
  2296. tags: [],
  2297. filterMode: switchMode.innerText,
  2298. });
  2299.  
  2300. this.views.details._.hide();
  2301.  
  2302. callback("ADD", entity);
  2303. } else {
  2304. const entity = this.update(uid, {
  2305. name,
  2306. filterMode: switchMode.innerText,
  2307. });
  2308.  
  2309. this.views.details._.hide();
  2310.  
  2311. callback("UPDATE", entity);
  2312. }
  2313. });
  2314.  
  2315. return ui.createButtonGroup(remove, save);
  2316. })(),
  2317. {
  2318. className: "right_",
  2319. }
  2320. );
  2321.  
  2322. const actions = ui.createElement(
  2323. "DIV",
  2324. [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
  2325. {
  2326. style: "margin-top: 10px;",
  2327. }
  2328. );
  2329.  
  2330. const tips = ui.createElement("DIV", TIPS.filterMode, {
  2331. className: "silver",
  2332. style: "margin-top: 10px;",
  2333. });
  2334.  
  2335. const content = ui.createElement("DIV", [actions, tips], {
  2336. style: "width: 80vw",
  2337. });
  2338.  
  2339. // 创建弹出框
  2340. this.views.details = ui.createDialog(null, title, content);
  2341. }
  2342.  
  2343. /**
  2344. * 过滤
  2345. * @param {*} item 绑定的 nFilter
  2346. * @param {*} result 过滤结果
  2347. */
  2348. async filter(item, result) {
  2349. // 获取用户信息
  2350. const user = this.get(item.uid);
  2351.  
  2352. // 没有则跳过
  2353. if (user === null) {
  2354. return;
  2355. }
  2356.  
  2357. // 获取用户过滤模式
  2358. const mode = this.settings.getModeByName(user.filterMode);
  2359.  
  2360. // 不高于当前过滤模式则跳过
  2361. if (mode <= result.mode) {
  2362. return;
  2363. }
  2364.  
  2365. // 更新过滤模式和原因
  2366. result.mode = mode;
  2367. result.reason = `用户模式: ${user.filterMode}`;
  2368. }
  2369.  
  2370. /**
  2371. * 通知
  2372. * @param {*} item 绑定的 nFilter
  2373. */
  2374. async notify(item) {
  2375. const { uid, username, action } = item;
  2376.  
  2377. // 如果没有 action 组件则跳过
  2378. if (action === null) {
  2379. return;
  2380. }
  2381.  
  2382. // 如果是匿名,隐藏组件
  2383. if (uid <= 0) {
  2384. action.style.display = "none";
  2385. return;
  2386. }
  2387.  
  2388. // 获取当前用户
  2389. const user = this.get(uid);
  2390.  
  2391. // 修改操作按钮文字
  2392. if (action.tagName === "A") {
  2393. action.innerText = "屏蔽";
  2394. } else {
  2395. action.title = "屏蔽";
  2396. }
  2397.  
  2398. // 修改操作按钮颜色
  2399. if (user) {
  2400. action.style.background = "#CB4042";
  2401. } else {
  2402. action.style.background = "#AAA";
  2403. }
  2404.  
  2405. // 绑定事件
  2406. action.onclick = () => {
  2407. this.renderDetails(uid, username);
  2408. };
  2409. }
  2410.  
  2411. /**
  2412. * 重新过滤
  2413. * @param {Number} uid 用户 ID
  2414. */
  2415. reFilter(uid) {
  2416. this.data.forEach((item) => {
  2417. // 如果用户 ID 一致,则重新过滤
  2418. if (item.uid === uid) {
  2419. item.execute();
  2420. return;
  2421. }
  2422.  
  2423. // 如果有引用,也重新过滤
  2424. if (Object.hasOwn(item.quotes || {}, uid)) {
  2425. item.execute();
  2426. return;
  2427. }
  2428. });
  2429. }
  2430. }
  2431.  
  2432. /**
  2433. * 标记模块
  2434. */
  2435. class TagModule extends Module {
  2436. /**
  2437. * 模块名称
  2438. */
  2439. static name = "tag";
  2440.  
  2441. /**
  2442. * 模块标签
  2443. */
  2444. static label = "标记";
  2445.  
  2446. /**
  2447. * 顺序
  2448. */
  2449. static order = 30;
  2450.  
  2451. /**
  2452. * 依赖模块
  2453. */
  2454. static depends = [UserModule];
  2455.  
  2456. /**
  2457. * 依赖的用户模块
  2458. * @returns {UserModule} 用户模块
  2459. */
  2460. get userModule() {
  2461. return this.depends[UserModule.name];
  2462. }
  2463.  
  2464. /**
  2465. * 获取列表
  2466. */
  2467. get list() {
  2468. return this.settings.tags;
  2469. }
  2470.  
  2471. /**
  2472. * 获取标记
  2473. * @param {Number} id 标记 ID
  2474. * @param {String} name 标记名称
  2475. */
  2476. get({ id, name }) {
  2477. // 获取列表
  2478. const list = this.list;
  2479.  
  2480. // 通过 ID 获取标记
  2481. if (list[id]) {
  2482. return list[id];
  2483. }
  2484.  
  2485. // 通过名称获取标记
  2486. if (name) {
  2487. const tag = Object.values(list).find((item) => item.name === name);
  2488.  
  2489. if (tag) {
  2490. return tag;
  2491. }
  2492. }
  2493.  
  2494. return null;
  2495. }
  2496.  
  2497. /**
  2498. * 添加标记
  2499. * @param {String} name 标记名称
  2500. */
  2501. add(name) {
  2502. // 获取对应的标记
  2503. const tag = this.get({ name });
  2504.  
  2505. // 如果标记已存在,则返回标记信息,否则增加标记
  2506. if (tag) {
  2507. return tag;
  2508. }
  2509.  
  2510. // 获取列表
  2511. const list = this.list;
  2512.  
  2513. // ID 为最大值 + 1
  2514. const id = Math.max(...Object.keys(list), 0) + 1;
  2515.  
  2516. // 标记的颜色
  2517. const color = Tools.generateColor(name);
  2518.  
  2519. // 写入标记信息
  2520. list[id] = {
  2521. id,
  2522. name,
  2523. color,
  2524. filterMode: this.settings.filterModes[0],
  2525. };
  2526.  
  2527. // 保存数据
  2528. this.settings.tags = list;
  2529.  
  2530. // 返回添加的标记
  2531. return list[id];
  2532. }
  2533.  
  2534. /**
  2535. * 编辑标记
  2536. * @param {Number} id 标记 ID
  2537. * @param {*} values 标记信息
  2538. */
  2539. update(id, values) {
  2540. // 获取列表
  2541. const list = this.list;
  2542.  
  2543. // 如果不存在则跳过
  2544. if (Object.hasOwn(list, id) === false) {
  2545. return null;
  2546. }
  2547.  
  2548. // 获取标记
  2549. const entity = list[id];
  2550.  
  2551. // 获取相关的用户
  2552. const users = Object.values(this.userModule.list).filter((user) =>
  2553. user.tags.includes(id)
  2554. );
  2555.  
  2556. // 更新标记
  2557. Object.assign(entity, values);
  2558.  
  2559. // 保存数据
  2560. this.settings.tags = list;
  2561.  
  2562. // 重新过滤
  2563. this.reFilter(users);
  2564. }
  2565.  
  2566. /**
  2567. * 删除标记
  2568. * @param {Number} id 标记 ID
  2569. */
  2570. remove(id) {
  2571. // 获取列表
  2572. const list = this.list;
  2573.  
  2574. // 如果不存在则跳过
  2575. if (Object.hasOwn(list, id) === false) {
  2576. return null;
  2577. }
  2578.  
  2579. // 获取标记
  2580. const entity = list[id];
  2581.  
  2582. // 获取相关的用户
  2583. const users = Object.values(this.userModule.list).filter((user) =>
  2584. user.tags.includes(id)
  2585. );
  2586.  
  2587. // 删除标记
  2588. delete list[id];
  2589.  
  2590. // 删除相关的用户标记
  2591. users.forEach((user) => {
  2592. const index = user.tags.findIndex((item) => item === id);
  2593.  
  2594. if (index >= 0) {
  2595. user.tags.splice(index, 1);
  2596. }
  2597. });
  2598.  
  2599. // 保存数据
  2600. this.settings.tags = list;
  2601.  
  2602. // 重新过滤
  2603. this.reFilter(users);
  2604.  
  2605. // 返回删除的标记
  2606. return entity;
  2607. }
  2608.  
  2609. /**
  2610. * 格式化
  2611. * @param {Number} id 标记 ID
  2612. * @param {String | undefined} name 标记名称
  2613. * @param {String | undefined} name 标记颜色
  2614. */
  2615. format(id, name, color) {
  2616. const { ui } = this;
  2617.  
  2618. if (id >= 0) {
  2619. const tag = this.get({ id });
  2620.  
  2621. if (tag) {
  2622. name = tag.name;
  2623. color = tag.color;
  2624. }
  2625. }
  2626.  
  2627. if (name && color) {
  2628. return ui.createElement("B", name, {
  2629. className: "block_txt nobr",
  2630. style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
  2631. });
  2632. }
  2633.  
  2634. return "";
  2635. }
  2636.  
  2637. /**
  2638. * 表格列
  2639. * @returns {Array} 表格列集合
  2640. */
  2641. columns() {
  2642. return [
  2643. { label: "标记", width: 1 },
  2644. { label: "列表" },
  2645. { label: "过滤模式", width: 1 },
  2646. { label: "操作", width: 1 },
  2647. ];
  2648. }
  2649.  
  2650. /**
  2651. * 表格项
  2652. * @param {*} item 标记信息
  2653. * @returns {Array} 表格项集合
  2654. */
  2655. column(item) {
  2656. const { ui } = this;
  2657. const { table } = this.views;
  2658. const { id, filterMode } = item;
  2659.  
  2660. // 标记
  2661. const tag = this.format(id);
  2662.  
  2663. // 用户列表
  2664. const list = Object.values(this.userModule.list)
  2665. .filter(({ tags }) => tags.includes(id))
  2666. .map(({ id }) => this.userModule.format(id));
  2667.  
  2668. const group = ui.createElement("DIV", list, {
  2669. style: "white-space: normal; display: none;",
  2670. });
  2671.  
  2672. const switchButton = ui.createButton(list.length.toString(), () => {
  2673. if (group.style.display === "none") {
  2674. group.style.removeProperty("display");
  2675. } else {
  2676. group.style.display = "none";
  2677. }
  2678. });
  2679.  
  2680. // 切换过滤模式
  2681. const switchMode = ui.createButton(
  2682. filterMode || this.settings.filterModes[0],
  2683. () => {
  2684. const newMode = this.settings.switchModeByName(switchMode.innerText);
  2685.  
  2686. this.update(id, {
  2687. filterMode: newMode,
  2688. });
  2689.  
  2690. switchMode.innerText = newMode;
  2691. }
  2692. );
  2693.  
  2694. // 操作
  2695. const buttons = (() => {
  2696. const remove = ui.createButton("删除", (e) => {
  2697. ui.confirm().then(() => {
  2698. this.remove(id);
  2699.  
  2700. table.remove(e);
  2701. });
  2702. });
  2703.  
  2704. return ui.createButtonGroup(remove);
  2705. })();
  2706.  
  2707. return [tag, [switchButton, group], switchMode, buttons];
  2708. }
  2709.  
  2710. /**
  2711. * 初始化组件
  2712. */
  2713. initComponents() {
  2714. super.initComponents();
  2715.  
  2716. const { ui } = this;
  2717. const { tabs, content, settings } = ui.views;
  2718. const { add } = settings;
  2719.  
  2720. const table = ui.createTable(this.columns());
  2721.  
  2722. const tab = ui.createTab(
  2723. tabs,
  2724. this.constructor.label,
  2725. this.constructor.order,
  2726. {
  2727. onclick: () => {
  2728. this.render(content);
  2729. },
  2730. }
  2731. );
  2732.  
  2733. Object.assign(this.views, {
  2734. tab,
  2735. table,
  2736. });
  2737.  
  2738. this.views.container.appendChild(table);
  2739.  
  2740. // 删除没有标记的用户
  2741. {
  2742. const button = ui.createButton("删除没有标记的用户", () => {
  2743. ui.confirm().then(() => {
  2744. const users = Object.values(this.userModule.list);
  2745.  
  2746. users.forEach(({ id, tags }) => {
  2747. if (tags.length > 0) {
  2748. return;
  2749. }
  2750.  
  2751. this.userModule.remove(id);
  2752. });
  2753. });
  2754. });
  2755.  
  2756. const element = ui.createElement("DIV", button);
  2757.  
  2758. add(this.constructor.order + 0, element);
  2759. }
  2760.  
  2761. // 删除没有用户的标记
  2762. {
  2763. const button = ui.createButton("删除没有用户的标记", () => {
  2764. ui.confirm().then(() => {
  2765. const items = Object.values(this.list);
  2766. const users = Object.values(this.userModule.list);
  2767.  
  2768. items.forEach(({ id }) => {
  2769. if (users.find(({ tags }) => tags.includes(id))) {
  2770. return;
  2771. }
  2772.  
  2773. this.remove(id);
  2774. });
  2775. });
  2776. });
  2777.  
  2778. const element = ui.createElement("DIV", button);
  2779.  
  2780. add(this.constructor.order + 1, element);
  2781. }
  2782. }
  2783.  
  2784. /**
  2785. * 渲染
  2786. * @param {HTMLElement} container 容器
  2787. */
  2788. render(container) {
  2789. super.render(container);
  2790.  
  2791. const { table } = this.views;
  2792.  
  2793. if (table) {
  2794. const { add, clear } = table;
  2795.  
  2796. clear();
  2797.  
  2798. Object.values(this.list).forEach((item) => {
  2799. const column = this.column(item);
  2800.  
  2801. add(...column);
  2802. });
  2803. }
  2804. }
  2805.  
  2806. /**
  2807. * 过滤
  2808. * @param {*} item 绑定的 nFilter
  2809. * @param {*} result 过滤结果
  2810. */
  2811. async filter(item, result) {
  2812. // 获取用户信息
  2813. const user = this.userModule.get(item.uid);
  2814.  
  2815. // 没有则跳过
  2816. if (user === null) {
  2817. return;
  2818. }
  2819.  
  2820. // 获取用户标记
  2821. const tags = user.tags;
  2822.  
  2823. // 取最高的过滤模式
  2824. // 低于当前的过滤模式则跳过
  2825. let max = result.mode;
  2826. let tag = null;
  2827.  
  2828. for (const id of tags) {
  2829. const entity = this.get({ id });
  2830.  
  2831. if (entity === null) {
  2832. continue;
  2833. }
  2834.  
  2835. // 获取过滤模式
  2836. const mode = this.settings.getModeByName(entity.filterMode);
  2837.  
  2838. if (mode < max) {
  2839. continue;
  2840. }
  2841.  
  2842. if (mode === max && result.reason.includes("用户模式") === false) {
  2843. continue;
  2844. }
  2845.  
  2846. max = mode;
  2847. tag = entity;
  2848. }
  2849.  
  2850. // 没有匹配的则跳过
  2851. if (tag === null) {
  2852. return;
  2853. }
  2854.  
  2855. // 更新过滤模式和原因
  2856. result.mode = max;
  2857. result.reason = `标记: ${tag.name}`;
  2858. }
  2859.  
  2860. /**
  2861. * 通知
  2862. * @param {*} item 绑定的 nFilter
  2863. */
  2864. async notify(item) {
  2865. const { uid, tags } = item;
  2866.  
  2867. // 如果没有 tags 组件则跳过
  2868. if (tags === null) {
  2869. return;
  2870. }
  2871.  
  2872. // 如果是匿名,隐藏组件
  2873. if (uid <= 0) {
  2874. tags.style.display = "none";
  2875. return;
  2876. }
  2877.  
  2878. // 删除旧标记
  2879. [...tags.querySelectorAll("[tid]")].forEach((item) => {
  2880. tags.removeChild(item);
  2881. });
  2882.  
  2883. // 获取当前用户
  2884. const user = this.userModule.get(uid);
  2885.  
  2886. // 如果没有用户,则跳过
  2887. if (user === null) {
  2888. return;
  2889. }
  2890.  
  2891. // 格式化标记
  2892. const items = user.tags.map((id) => {
  2893. const item = this.format(id);
  2894.  
  2895. if (item) {
  2896. item.setAttribute("tid", id);
  2897. }
  2898.  
  2899. return item;
  2900. });
  2901.  
  2902. // 加入组件
  2903. items.forEach((item) => {
  2904. if (item) {
  2905. tags.appendChild(item);
  2906. }
  2907. });
  2908. }
  2909.  
  2910. /**
  2911. * 重新过滤
  2912. * @param {Array} users 用户集合
  2913. */
  2914. reFilter(users) {
  2915. users.forEach((user) => {
  2916. this.userModule.reFilter(user.id);
  2917. });
  2918. }
  2919. }
  2920.  
  2921. /**
  2922. * 关键字模块
  2923. */
  2924. class KeywordModule extends Module {
  2925. /**
  2926. * 模块名称
  2927. */
  2928. static name = "keyword";
  2929.  
  2930. /**
  2931. * 模块标签
  2932. */
  2933. static label = "关键字";
  2934.  
  2935. /**
  2936. * 顺序
  2937. */
  2938. static order = 40;
  2939.  
  2940. /**
  2941. * 获取列表
  2942. */
  2943. get list() {
  2944. return this.settings.keywords;
  2945. }
  2946.  
  2947. /**
  2948. * 获取关键字
  2949. * @param {Number} id 关键字 ID
  2950. */
  2951. get(id) {
  2952. // 获取列表
  2953. const list = this.list;
  2954.  
  2955. // 如果存在,则返回信息
  2956. if (list[id]) {
  2957. return list[id];
  2958. }
  2959.  
  2960. return null;
  2961. }
  2962.  
  2963. /**
  2964. * 添加关键字
  2965. * @param {String} keyword 关键字
  2966. * @param {String} filterMode 过滤模式
  2967. * @param {Number} filterLevel 过滤等级: 0 - 仅过滤标题; 1 - 过滤标题和内容
  2968. */
  2969. add(keyword, filterMode, filterLevel) {
  2970. // 获取列表
  2971. const list = this.list;
  2972.  
  2973. // ID 为最大值 + 1
  2974. const id = Math.max(...Object.keys(list), 0) + 1;
  2975.  
  2976. // 写入关键字信息
  2977. list[id] = {
  2978. id,
  2979. keyword,
  2980. filterMode,
  2981. filterLevel,
  2982. };
  2983.  
  2984. // 保存数据
  2985. this.settings.keywords = list;
  2986.  
  2987. // 重新过滤
  2988. this.reFilter();
  2989.  
  2990. // 返回添加的关键字
  2991. return list[id];
  2992. }
  2993.  
  2994. /**
  2995. * 编辑关键字
  2996. * @param {Number} id 关键字 ID
  2997. * @param {*} values 关键字信息
  2998. */
  2999. update(id, values) {
  3000. // 获取列表
  3001. const list = this.list;
  3002.  
  3003. // 如果不存在则跳过
  3004. if (Object.hasOwn(list, id) === false) {
  3005. return null;
  3006. }
  3007.  
  3008. // 获取关键字
  3009. const entity = list[id];
  3010.  
  3011. // 更新关键字
  3012. Object.assign(entity, values);
  3013.  
  3014. // 保存数据
  3015. this.settings.keywords = list;
  3016.  
  3017. // 重新过滤
  3018. this.reFilter();
  3019. }
  3020.  
  3021. /**
  3022. * 删除关键字
  3023. * @param {Number} id 关键字 ID
  3024. */
  3025. remove(id) {
  3026. // 获取列表
  3027. const list = this.list;
  3028.  
  3029. // 如果不存在则跳过
  3030. if (Object.hasOwn(list, id) === false) {
  3031. return null;
  3032. }
  3033.  
  3034. // 获取关键字
  3035. const entity = list[id];
  3036.  
  3037. // 删除关键字
  3038. delete list[id];
  3039.  
  3040. // 保存数据
  3041. this.settings.keywords = list;
  3042.  
  3043. // 重新过滤
  3044. this.reFilter();
  3045.  
  3046. // 返回删除的关键字
  3047. return entity;
  3048. }
  3049.  
  3050. /**
  3051. * 获取帖子数据
  3052. * @param {*} item 绑定的 nFilter
  3053. */
  3054. async getPostInfo(item) {
  3055. const { tid, pid } = item;
  3056.  
  3057. // 请求帖子数据
  3058. const { subject, content, userInfo, reputation } =
  3059. await this.api.getPostInfo(tid, pid);
  3060.  
  3061. // 绑定用户信息和声望
  3062. if (userInfo) {
  3063. item.userInfo = userInfo;
  3064. item.username = userInfo.username;
  3065. item.reputation = reputation;
  3066. }
  3067.  
  3068. // 绑定标题和内容
  3069. item.subject = subject;
  3070. item.content = content;
  3071. }
  3072.  
  3073. /**
  3074. * 表格列
  3075. * @returns {Array} 表格列集合
  3076. */
  3077. columns() {
  3078. return [
  3079. { label: "关键字" },
  3080. { label: "过滤模式", center: true, width: 1 },
  3081. { label: "包括内容", center: true, width: 1 },
  3082. { label: "操作", width: 1 },
  3083. ];
  3084. }
  3085.  
  3086. /**
  3087. * 表格项
  3088. * @param {*} item 标记信息
  3089. * @returns {Array} 表格项集合
  3090. */
  3091. column(item) {
  3092. const { ui } = this;
  3093. const { table } = this.views;
  3094. const { id, keyword, filterLevel, filterMode } = item;
  3095.  
  3096. // 关键字
  3097. const input = ui.createElement("INPUT", [], {
  3098. type: "text",
  3099. value: keyword,
  3100. });
  3101.  
  3102. const inputWrapper = ui.createElement("DIV", input, {
  3103. className: "filter-input-wrapper",
  3104. });
  3105.  
  3106. // 切换过滤模式
  3107. const switchMode = ui.createButton(
  3108. filterMode || this.settings.filterModes[0],
  3109. () => {
  3110. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3111.  
  3112. switchMode.innerText = newMode;
  3113. }
  3114. );
  3115.  
  3116. // 包括内容
  3117. const switchLevel = ui.createElement("INPUT", [], {
  3118. type: "checkbox",
  3119. checked: filterLevel > 0,
  3120. });
  3121.  
  3122. // 操作
  3123. const buttons = (() => {
  3124. const save = ui.createButton("保存", () => {
  3125. this.update(id, {
  3126. keyword: input.value,
  3127. filterMode: switchMode.innerText,
  3128. filterLevel: switchLevel.checked ? 1 : 0,
  3129. });
  3130. });
  3131.  
  3132. const remove = ui.createButton("删除", (e) => {
  3133. ui.confirm().then(() => {
  3134. this.remove(id);
  3135.  
  3136. table.remove(e);
  3137. });
  3138. });
  3139.  
  3140. return ui.createButtonGroup(save, remove);
  3141. })();
  3142.  
  3143. return [inputWrapper, switchMode, switchLevel, buttons];
  3144. }
  3145.  
  3146. /**
  3147. * 初始化组件
  3148. */
  3149. initComponents() {
  3150. super.initComponents();
  3151.  
  3152. const { ui } = this;
  3153. const { tabs, content } = ui.views;
  3154.  
  3155. const table = ui.createTable(this.columns());
  3156.  
  3157. const tips = ui.createElement("DIV", TIPS.keyword, {
  3158. className: "silver",
  3159. });
  3160.  
  3161. const tab = ui.createTab(
  3162. tabs,
  3163. this.constructor.label,
  3164. this.constructor.order,
  3165. {
  3166. onclick: () => {
  3167. this.render(content);
  3168. },
  3169. }
  3170. );
  3171.  
  3172. Object.assign(this.views, {
  3173. tab,
  3174. table,
  3175. });
  3176.  
  3177. this.views.container.appendChild(table);
  3178. this.views.container.appendChild(tips);
  3179. }
  3180.  
  3181. /**
  3182. * 渲染
  3183. * @param {HTMLElement} container 容器
  3184. */
  3185. render(container) {
  3186. super.render(container);
  3187.  
  3188. const { table } = this.views;
  3189.  
  3190. if (table) {
  3191. const { add, clear } = table;
  3192.  
  3193. clear();
  3194.  
  3195. Object.values(this.list).forEach((item) => {
  3196. const column = this.column(item);
  3197.  
  3198. add(...column);
  3199. });
  3200.  
  3201. this.renderNewLine();
  3202. }
  3203. }
  3204.  
  3205. /**
  3206. * 渲染新行
  3207. */
  3208. renderNewLine() {
  3209. const { ui } = this;
  3210. const { table } = this.views;
  3211.  
  3212. // 关键字
  3213. const input = ui.createElement("INPUT", [], {
  3214. type: "text",
  3215. });
  3216.  
  3217. const inputWrapper = ui.createElement("DIV", input, {
  3218. className: "filter-input-wrapper",
  3219. });
  3220.  
  3221. // 切换过滤模式
  3222. const switchMode = ui.createButton(this.settings.filterModes[0], () => {
  3223. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3224.  
  3225. switchMode.innerText = newMode;
  3226. });
  3227.  
  3228. // 包括内容
  3229. const switchLevel = ui.createElement("INPUT", [], {
  3230. type: "checkbox",
  3231. });
  3232.  
  3233. // 操作
  3234. const buttons = (() => {
  3235. const save = ui.createButton("添加", (e) => {
  3236. const entity = this.add(
  3237. input.value,
  3238. switchMode.innerText,
  3239. switchLevel.checked ? 1 : 0
  3240. );
  3241.  
  3242. table.update(e, ...this.column(entity));
  3243.  
  3244. this.renderNewLine();
  3245. });
  3246.  
  3247. return ui.createButtonGroup(save);
  3248. })();
  3249.  
  3250. // 添加至列表
  3251. table.add(inputWrapper, switchMode, switchLevel, buttons);
  3252. }
  3253.  
  3254. /**
  3255. * 过滤
  3256. * @param {*} item 绑定的 nFilter
  3257. * @param {*} result 过滤结果
  3258. */
  3259. async filter(item, result) {
  3260. // 获取列表
  3261. const list = this.list;
  3262.  
  3263. // 跳过低于当前的过滤模式
  3264. const filtered = Object.values(list).filter(
  3265. (item) => this.settings.getModeByName(item.filterMode) > result.mode
  3266. );
  3267.  
  3268. // 没有则跳过
  3269. if (filtered.length === 0) {
  3270. return;
  3271. }
  3272.  
  3273. // 根据过滤模式依次判断
  3274. const sorted = Tools.sortBy(filtered, (item) =>
  3275. this.settings.getModeByName(item.filterMode)
  3276. );
  3277.  
  3278. for (let i = 0; i < sorted.length; i += 1) {
  3279. const { keyword, filterMode } = sorted[i];
  3280.  
  3281. // 过滤等级,0 为只过滤标题,1 为过滤标题和内容
  3282. const filterLevel = sorted[i].filterLevel || 0;
  3283.  
  3284. // 过滤标题
  3285. if (filterLevel >= 0) {
  3286. const { subject } = item;
  3287.  
  3288. const match = subject.match(keyword);
  3289.  
  3290. if (match) {
  3291. const mode = this.settings.getModeByName(filterMode);
  3292.  
  3293. // 更新过滤模式和原因
  3294. result.mode = mode;
  3295. result.reason = `关键字: ${match[0]}`;
  3296. return;
  3297. }
  3298. }
  3299.  
  3300. // 过滤内容
  3301. if (filterLevel >= 1) {
  3302. // 如果没有内容,则请求
  3303. if (item.content === undefined) {
  3304. await this.getPostInfo(item);
  3305. }
  3306.  
  3307. const { content } = item;
  3308.  
  3309. if (content) {
  3310. const match = content.match(keyword);
  3311.  
  3312. if (match) {
  3313. const mode = this.settings.getModeByName(filterMode);
  3314.  
  3315. // 更新过滤模式和原因
  3316. result.mode = mode;
  3317. result.reason = `关键字: ${match[0]}`;
  3318. return;
  3319. }
  3320. }
  3321. }
  3322. }
  3323. }
  3324.  
  3325. /**
  3326. * 重新过滤
  3327. */
  3328. reFilter() {
  3329. // 实际上应该根据过滤模式来筛选要过滤的部分
  3330. this.data.forEach((item) => {
  3331. item.execute();
  3332. });
  3333. }
  3334. }
  3335.  
  3336. /**
  3337. * 属地模块
  3338. */
  3339. class LocationModule extends Module {
  3340. /**
  3341. * 模块名称
  3342. */
  3343. static name = "location";
  3344.  
  3345. /**
  3346. * 模块标签
  3347. */
  3348. static label = "属地";
  3349.  
  3350. /**
  3351. * 顺序
  3352. */
  3353. static order = 50;
  3354.  
  3355. /**
  3356. * 请求缓存
  3357. */
  3358. cache = {};
  3359.  
  3360. /**
  3361. * 获取列表
  3362. */
  3363. get list() {
  3364. return this.settings.locations;
  3365. }
  3366.  
  3367. /**
  3368. * 获取属地
  3369. * @param {Number} id 属地 ID
  3370. */
  3371. get(id) {
  3372. // 获取列表
  3373. const list = this.list;
  3374.  
  3375. // 如果存在,则返回信息
  3376. if (list[id]) {
  3377. return list[id];
  3378. }
  3379.  
  3380. return null;
  3381. }
  3382.  
  3383. /**
  3384. * 添加属地
  3385. * @param {String} keyword 关键字
  3386. * @param {String} filterMode 过滤模式
  3387. */
  3388. add(keyword, filterMode) {
  3389. // 获取列表
  3390. const list = this.list;
  3391.  
  3392. // ID 为最大值 + 1
  3393. const id = Math.max(...Object.keys(list), 0) + 1;
  3394.  
  3395. // 写入属地信息
  3396. list[id] = {
  3397. id,
  3398. keyword,
  3399. filterMode,
  3400. };
  3401.  
  3402. // 保存数据
  3403. this.settings.locations = list;
  3404.  
  3405. // 重新过滤
  3406. this.reFilter();
  3407.  
  3408. // 返回添加的属地
  3409. return list[id];
  3410. }
  3411.  
  3412. /**
  3413. * 编辑属地
  3414. * @param {Number} id 属地 ID
  3415. * @param {*} values 属地信息
  3416. */
  3417. update(id, values) {
  3418. // 获取列表
  3419. const list = this.list;
  3420.  
  3421. // 如果不存在则跳过
  3422. if (Object.hasOwn(list, id) === false) {
  3423. return null;
  3424. }
  3425.  
  3426. // 获取属地
  3427. const entity = list[id];
  3428.  
  3429. // 更新属地
  3430. Object.assign(entity, values);
  3431.  
  3432. // 保存数据
  3433. this.settings.locations = list;
  3434.  
  3435. // 重新过滤
  3436. this.reFilter();
  3437. }
  3438.  
  3439. /**
  3440. * 删除属地
  3441. * @param {Number} id 属地 ID
  3442. */
  3443. remove(id) {
  3444. // 获取列表
  3445. const list = this.list;
  3446.  
  3447. // 如果不存在则跳过
  3448. if (Object.hasOwn(list, id) === false) {
  3449. return null;
  3450. }
  3451.  
  3452. // 获取属地
  3453. const entity = list[id];
  3454.  
  3455. // 删除属地
  3456. delete list[id];
  3457.  
  3458. // 保存数据
  3459. this.settings.locations = list;
  3460.  
  3461. // 重新过滤
  3462. this.reFilter();
  3463.  
  3464. // 返回删除的属地
  3465. return entity;
  3466. }
  3467.  
  3468. /**
  3469. * 获取 IP 属地
  3470. * @param {*} item 绑定的 nFilter
  3471. */
  3472. async getIpLocation(item) {
  3473. const { uid } = item;
  3474.  
  3475. // 如果是匿名直接跳过
  3476. if (uid <= 0) {
  3477. return null;
  3478. }
  3479.  
  3480. // 如果已有缓存,直接返回
  3481. if (Object.hasOwn(this.cache, uid)) {
  3482. return this.cache[uid];
  3483. }
  3484.  
  3485. // 请求属地
  3486. const ipLocations = await this.api.getIpLocations(uid);
  3487.  
  3488. // 写入缓存
  3489. if (ipLocations.length > 0) {
  3490. this.cache[uid] = ipLocations[0].ipLoc;
  3491. }
  3492.  
  3493. // 返回结果
  3494. return null;
  3495. }
  3496.  
  3497. /**
  3498. * 表格列
  3499. * @returns {Array} 表格列集合
  3500. */
  3501. columns() {
  3502. return [
  3503. { label: "关键字" },
  3504. { label: "过滤模式", center: true, width: 1 },
  3505. { label: "操作", width: 1 },
  3506. ];
  3507. }
  3508.  
  3509. /**
  3510. * 表格项
  3511. * @param {*} item 标记信息
  3512. * @returns {Array} 表格项集合
  3513. */
  3514. column(item) {
  3515. const { ui } = this;
  3516. const { table } = this.views;
  3517. const { id, keyword, filterMode } = item;
  3518.  
  3519. // 关键字
  3520. const input = ui.createElement("INPUT", [], {
  3521. type: "text",
  3522. value: keyword,
  3523. });
  3524.  
  3525. const inputWrapper = ui.createElement("DIV", input, {
  3526. className: "filter-input-wrapper",
  3527. });
  3528.  
  3529. // 切换过滤模式
  3530. const switchMode = ui.createButton(
  3531. filterMode || this.settings.filterModes[0],
  3532. () => {
  3533. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3534.  
  3535. switchMode.innerText = newMode;
  3536. }
  3537. );
  3538.  
  3539. // 操作
  3540. const buttons = (() => {
  3541. const save = ui.createButton("保存", () => {
  3542. this.update(id, {
  3543. keyword: input.value,
  3544. filterMode: switchMode.innerText,
  3545. });
  3546. });
  3547.  
  3548. const remove = ui.createButton("删除", (e) => {
  3549. ui.confirm().then(() => {
  3550. this.remove(id);
  3551.  
  3552. table.remove(e);
  3553. });
  3554. });
  3555.  
  3556. return ui.createButtonGroup(save, remove);
  3557. })();
  3558.  
  3559. return [inputWrapper, switchMode, buttons];
  3560. }
  3561.  
  3562. /**
  3563. * 初始化组件
  3564. */
  3565. initComponents() {
  3566. super.initComponents();
  3567.  
  3568. const { ui } = this;
  3569. const { tabs, content } = ui.views;
  3570.  
  3571. const table = ui.createTable(this.columns());
  3572.  
  3573. const tips = ui.createElement("DIV", TIPS.keyword, {
  3574. className: "silver",
  3575. });
  3576.  
  3577. const tab = ui.createTab(
  3578. tabs,
  3579. this.constructor.label,
  3580. this.constructor.order,
  3581. {
  3582. onclick: () => {
  3583. this.render(content);
  3584. },
  3585. }
  3586. );
  3587.  
  3588. Object.assign(this.views, {
  3589. tab,
  3590. table,
  3591. });
  3592.  
  3593. this.views.container.appendChild(table);
  3594. this.views.container.appendChild(tips);
  3595. }
  3596.  
  3597. /**
  3598. * 渲染
  3599. * @param {HTMLElement} container 容器
  3600. */
  3601. render(container) {
  3602. super.render(container);
  3603.  
  3604. const { table } = this.views;
  3605.  
  3606. if (table) {
  3607. const { add, clear } = table;
  3608.  
  3609. clear();
  3610.  
  3611. Object.values(this.list).forEach((item) => {
  3612. const column = this.column(item);
  3613.  
  3614. add(...column);
  3615. });
  3616.  
  3617. this.renderNewLine();
  3618. }
  3619. }
  3620.  
  3621. /**
  3622. * 渲染新行
  3623. */
  3624. renderNewLine() {
  3625. const { ui } = this;
  3626. const { table } = this.views;
  3627.  
  3628. // 关键字
  3629. const input = ui.createElement("INPUT", [], {
  3630. type: "text",
  3631. });
  3632.  
  3633. const inputWrapper = ui.createElement("DIV", input, {
  3634. className: "filter-input-wrapper",
  3635. });
  3636.  
  3637. // 切换过滤模式
  3638. const switchMode = ui.createButton(this.settings.filterModes[0], () => {
  3639. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3640.  
  3641. switchMode.innerText = newMode;
  3642. });
  3643.  
  3644. // 操作
  3645. const buttons = (() => {
  3646. const save = ui.createButton("添加", (e) => {
  3647. const entity = this.add(input.value, switchMode.innerText);
  3648.  
  3649. table.update(e, ...this.column(entity));
  3650.  
  3651. this.renderNewLine();
  3652. });
  3653.  
  3654. return ui.createButtonGroup(save);
  3655. })();
  3656.  
  3657. // 添加至列表
  3658. table.add(inputWrapper, switchMode, buttons);
  3659. }
  3660.  
  3661. /**
  3662. * 过滤
  3663. * @param {*} item 绑定的 nFilter
  3664. * @param {*} result 过滤结果
  3665. */
  3666. async filter(item, result) {
  3667. // 获取列表
  3668. const list = this.list;
  3669.  
  3670. // 跳过低于当前的过滤模式
  3671. const filtered = Object.values(list).filter(
  3672. (item) => this.settings.getModeByName(item.filterMode) > result.mode
  3673. );
  3674.  
  3675. // 没有则跳过
  3676. if (filtered.length === 0) {
  3677. return;
  3678. }
  3679.  
  3680. // 获取当前属地
  3681. const location = await this.getIpLocation(item);
  3682.  
  3683. // 请求失败则跳过
  3684. if (location === null) {
  3685. return;
  3686. }
  3687.  
  3688. // 根据过滤模式依次判断
  3689. const sorted = Tools.sortBy(filtered, (item) =>
  3690. this.settings.getModeByName(item.filterMode)
  3691. );
  3692.  
  3693. for (let i = 0; i < sorted.length; i += 1) {
  3694. const { keyword, filterMode } = sorted[i];
  3695.  
  3696. const match = location.match(keyword);
  3697.  
  3698. if (match) {
  3699. const mode = this.settings.getModeByName(filterMode);
  3700.  
  3701. // 更新过滤模式和原因
  3702. result.mode = mode;
  3703. result.reason = `属地: ${match[0]}`;
  3704. return;
  3705. }
  3706. }
  3707. }
  3708.  
  3709. /**
  3710. * 重新过滤
  3711. */
  3712. reFilter() {
  3713. // 实际上应该根据过滤模式来筛选要过滤的部分
  3714. this.data.forEach((item) => {
  3715. item.execute();
  3716. });
  3717. }
  3718. }
  3719.  
  3720. /**
  3721. * 版面或合集模块
  3722. */
  3723. class ForumOrSubsetModule extends Module {
  3724. /**
  3725. * 模块名称
  3726. */
  3727. static name = "forumOrSubset";
  3728.  
  3729. /**
  3730. * 模块标签
  3731. */
  3732. static label = "版面/合集";
  3733.  
  3734. /**
  3735. * 顺序
  3736. */
  3737. static order = 60;
  3738.  
  3739. /**
  3740. * 请求缓存
  3741. */
  3742. cache = {};
  3743.  
  3744. /**
  3745. * 获取列表
  3746. */
  3747. get list() {
  3748. return this.settings.forumOrSubsets;
  3749. }
  3750.  
  3751. /**
  3752. * 获取版面或合集
  3753. * @param {Number} id ID
  3754. */
  3755. get(id) {
  3756. // 获取列表
  3757. const list = this.list;
  3758.  
  3759. // 如果存在,则返回信息
  3760. if (list[id]) {
  3761. return list[id];
  3762. }
  3763.  
  3764. return null;
  3765. }
  3766.  
  3767. /**
  3768. * 添加版面或合集
  3769. * @param {Number} value 版面或合集链接
  3770. * @param {String} filterMode 过滤模式
  3771. */
  3772. async add(value, filterMode) {
  3773. // 获取链接参数
  3774. const params = new URLSearchParams(value.split("?")[1]);
  3775.  
  3776. // 获取 FID
  3777. const fid = parseInt(params.get("fid"), 10);
  3778.  
  3779. // 获取 STID
  3780. const stid = parseInt(params.get("stid"), 10);
  3781.  
  3782. // 如果 FID 或 STID 不存在,则提示错误
  3783. if (fid === NaN && stid === NaN) {
  3784. alert("版面或合集ID有误");
  3785. return;
  3786. }
  3787.  
  3788. // 获取列表
  3789. const list = this.list;
  3790.  
  3791. // ID 为 FID 或者 t + STID
  3792. const id = fid ? fid : `t${stid}`;
  3793.  
  3794. // 如果版面或合集 ID 已存在,则提示错误
  3795. if (Object.hasOwn(list, id)) {
  3796. alert("已有相同版面或合集ID");
  3797. return;
  3798. }
  3799.  
  3800. // 请求版面或合集信息
  3801. const info = await (async () => {
  3802. if (fid) {
  3803. return await this.api.getForumInfo(fid);
  3804. }
  3805.  
  3806. if (stid) {
  3807. const postInfo = await this.api.getPostInfo(stid);
  3808.  
  3809. if (postInfo) {
  3810. return {
  3811. name: postInfo.subject,
  3812. };
  3813. }
  3814. }
  3815.  
  3816. return null;
  3817. })();
  3818.  
  3819. // 如果版面或合集不存在,则提示错误
  3820. if (info === null || info === undefined) {
  3821. alert("版面或合集ID有误");
  3822. return;
  3823. }
  3824.  
  3825. // 写入版面或合集信息
  3826. list[id] = {
  3827. fid,
  3828. stid,
  3829. name: info.name,
  3830. filterMode,
  3831. };
  3832.  
  3833. // 保存数据
  3834. this.settings.forumOrSubsets = list;
  3835.  
  3836. // 重新过滤
  3837. this.reFilter();
  3838.  
  3839. // 返回添加的版面或合集
  3840. return list[id];
  3841. }
  3842.  
  3843. /**
  3844. * 编辑版面或合集
  3845. * @param {Number} id ID
  3846. * @param {*} values 版面或合集信息
  3847. */
  3848. update(id, values) {
  3849. // 获取列表
  3850. const list = this.list;
  3851.  
  3852. // 如果不存在则跳过
  3853. if (Object.hasOwn(list, id) === false) {
  3854. return null;
  3855. }
  3856.  
  3857. // 获取版面或合集
  3858. const entity = list[id];
  3859.  
  3860. // 更新版面或合集
  3861. Object.assign(entity, values);
  3862.  
  3863. // 保存数据
  3864. this.settings.forumOrSubsets = list;
  3865.  
  3866. // 重新过滤
  3867. this.reFilter();
  3868. }
  3869.  
  3870. /**
  3871. * 删除版面或合集
  3872. * @param {Number} id ID
  3873. */
  3874. remove(id) {
  3875. // 获取列表
  3876. const list = this.list;
  3877.  
  3878. // 如果不存在则跳过
  3879. if (Object.hasOwn(list, id) === false) {
  3880. return null;
  3881. }
  3882.  
  3883. // 获取版面或合集
  3884. const entity = list[id];
  3885.  
  3886. // 删除版面或合集
  3887. delete list[id];
  3888.  
  3889. // 保存数据
  3890. this.settings.forumOrSubsets = list;
  3891.  
  3892. // 重新过滤
  3893. this.reFilter();
  3894.  
  3895. // 返回删除的版面或合集
  3896. return entity;
  3897. }
  3898.  
  3899. /**
  3900. * 格式化版面或合集
  3901. * @param {Number} fid 版面 ID
  3902. * @param {Number} stid 合集 ID
  3903. * @param {String} name 版面或合集名称
  3904. */
  3905. formatForumOrSubset(fid, stid, name) {
  3906. const { ui } = this;
  3907.  
  3908. return ui.createElement("A", `[${name}]`, {
  3909. className: "b nobr",
  3910. href: fid ? `/thread.php?fid=${fid}` : `/thread.php?stid=${stid}`,
  3911. });
  3912. }
  3913.  
  3914. /**
  3915. * 表格列
  3916. * @returns {Array} 表格列集合
  3917. */
  3918. columns() {
  3919. return [
  3920. { label: "版面/合集" },
  3921. { label: "过滤模式", center: true, width: 1 },
  3922. { label: "操作", width: 1 },
  3923. ];
  3924. }
  3925.  
  3926. /**
  3927. * 表格项
  3928. * @param {*} item 版面或合集信息
  3929. * @returns {Array} 表格项集合
  3930. */
  3931. column(item) {
  3932. const { ui } = this;
  3933. const { table } = this.views;
  3934. const { fid, stid, name, filterMode } = item;
  3935.  
  3936. // ID 为 FID 或者 t + STID
  3937. const id = fid ? fid : `t${stid}`;
  3938.  
  3939. // 版面或合集
  3940. const forum = this.formatForumOrSubset(fid, stid, name);
  3941.  
  3942. // 切换过滤模式
  3943. const switchMode = ui.createButton(filterMode || "隐藏", () => {
  3944. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3945.  
  3946. switchMode.innerText = newMode;
  3947. });
  3948.  
  3949. // 操作
  3950. const buttons = (() => {
  3951. const save = ui.createButton("保存", () => {
  3952. this.update(id, {
  3953. filterMode: switchMode.innerText,
  3954. });
  3955. });
  3956.  
  3957. const remove = ui.createButton("删除", (e) => {
  3958. ui.confirm().then(() => {
  3959. this.remove(id);
  3960.  
  3961. table.remove(e);
  3962. });
  3963. });
  3964.  
  3965. return ui.createButtonGroup(save, remove);
  3966. })();
  3967.  
  3968. return [forum, switchMode, buttons];
  3969. }
  3970.  
  3971. /**
  3972. * 初始化组件
  3973. */
  3974. initComponents() {
  3975. super.initComponents();
  3976.  
  3977. const { ui } = this;
  3978. const { tabs, content } = ui.views;
  3979.  
  3980. const table = ui.createTable(this.columns());
  3981.  
  3982. const tips = ui.createElement("DIV", TIPS.forumOrSubset, {
  3983. className: "silver",
  3984. });
  3985.  
  3986. const tab = ui.createTab(
  3987. tabs,
  3988. this.constructor.label,
  3989. this.constructor.order,
  3990. {
  3991. onclick: () => {
  3992. this.render(content);
  3993. },
  3994. }
  3995. );
  3996.  
  3997. Object.assign(this.views, {
  3998. tab,
  3999. table,
  4000. });
  4001.  
  4002. this.views.container.appendChild(table);
  4003. this.views.container.appendChild(tips);
  4004. }
  4005.  
  4006. /**
  4007. * 渲染
  4008. * @param {HTMLElement} container 容器
  4009. */
  4010. render(container) {
  4011. super.render(container);
  4012.  
  4013. const { table } = this.views;
  4014.  
  4015. if (table) {
  4016. const { add, clear } = table;
  4017.  
  4018. clear();
  4019.  
  4020. Object.values(this.list).forEach((item) => {
  4021. const column = this.column(item);
  4022.  
  4023. add(...column);
  4024. });
  4025.  
  4026. this.renderNewLine();
  4027. }
  4028. }
  4029.  
  4030. /**
  4031. * 渲染新行
  4032. */
  4033. renderNewLine() {
  4034. const { ui } = this;
  4035. const { table } = this.views;
  4036.  
  4037. // 版面或合集 ID
  4038. const forumInput = ui.createElement("INPUT", [], {
  4039. type: "text",
  4040. });
  4041.  
  4042. const forumInputWrapper = ui.createElement("DIV", forumInput, {
  4043. className: "filter-input-wrapper",
  4044. });
  4045.  
  4046. // 切换过滤模式
  4047. const switchMode = ui.createButton("隐藏", () => {
  4048. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4049.  
  4050. switchMode.innerText = newMode;
  4051. });
  4052.  
  4053. // 操作
  4054. const buttons = (() => {
  4055. const save = ui.createButton("添加", async (e) => {
  4056. const entity = await this.add(forumInput.value, switchMode.innerText);
  4057.  
  4058. if (entity) {
  4059. table.update(e, ...this.column(entity));
  4060.  
  4061. this.renderNewLine();
  4062. }
  4063. });
  4064.  
  4065. return ui.createButtonGroup(save);
  4066. })();
  4067.  
  4068. // 添加至列表
  4069. table.add(forumInputWrapper, switchMode, buttons);
  4070. }
  4071.  
  4072. /**
  4073. * 过滤
  4074. * @param {*} item 绑定的 nFilter
  4075. * @param {*} result 过滤结果
  4076. */
  4077. async filter(item, result) {
  4078. // 没有版面 ID 或主题杂项则跳过
  4079. if (item.fid === null && item.topicMisc.length === 0) {
  4080. return;
  4081. }
  4082.  
  4083. // 获取列表
  4084. const list = this.list;
  4085.  
  4086. // 跳过低于当前的过滤模式
  4087. const filtered = Object.values(list).filter(
  4088. (item) => this.settings.getModeByName(item.filterMode) > result.mode
  4089. );
  4090.  
  4091. // 没有则跳过
  4092. if (filtered.length === 0) {
  4093. return;
  4094. }
  4095.  
  4096. // 解析主题杂项
  4097. const { _SFID, _STID } = commonui.topicMiscVar.unpack(item.topicMisc);
  4098.  
  4099. // 根据过滤模式依次判断
  4100. const sorted = Tools.sortBy(filtered, (item) =>
  4101. this.settings.getModeByName(item.filterMode)
  4102. );
  4103.  
  4104. for (let i = 0; i < sorted.length; i += 1) {
  4105. const { fid, stid, name, filterMode } = sorted[i];
  4106.  
  4107. if (fid) {
  4108. if (fid === parseInt(item.fid, 10) || fid === _SFID) {
  4109. const mode = this.settings.getModeByName(filterMode);
  4110.  
  4111. // 更新过滤模式和原因
  4112. result.mode = mode;
  4113. result.reason = `版面: ${name}`;
  4114. return;
  4115. }
  4116. }
  4117.  
  4118. if (stid) {
  4119. if (stid === parseInt(item.tid, 10) || stid === _STID) {
  4120. const mode = this.settings.getModeByName(filterMode);
  4121.  
  4122. // 更新过滤模式和原因
  4123. result.mode = mode;
  4124. result.reason = `合集: ${name}`;
  4125. return;
  4126. }
  4127. }
  4128. }
  4129. }
  4130.  
  4131. /**
  4132. * 重新过滤
  4133. */
  4134. reFilter() {
  4135. // 实际上应该根据过滤模式来筛选要过滤的部分
  4136. this.data.forEach((item) => {
  4137. item.execute();
  4138. });
  4139. }
  4140. }
  4141.  
  4142. /**
  4143. * 猎巫模块
  4144. *
  4145. * 其实是通过 Cache 模块读取配置,而非 Settings
  4146. */
  4147. class HunterModule extends Module {
  4148. /**
  4149. * 模块名称
  4150. */
  4151. static name = "hunter";
  4152.  
  4153. /**
  4154. * 模块标签
  4155. */
  4156. static label = "猎巫";
  4157.  
  4158. /**
  4159. * 顺序
  4160. */
  4161. static order = 70;
  4162.  
  4163. /**
  4164. * 请求缓存
  4165. */
  4166. cache = {};
  4167.  
  4168. /**
  4169. * 请求队列
  4170. */
  4171. queue = [];
  4172.  
  4173. /**
  4174. * 获取列表
  4175. */
  4176. get list() {
  4177. return this.settings.cache
  4178. .get("WITCH_HUNT")
  4179. .then((values) => values || []);
  4180. }
  4181.  
  4182. /**
  4183. * 获取猎巫
  4184. * @param {Number} fid 版面 ID
  4185. */
  4186. async get(fid) {
  4187. // 获取列表
  4188. const list = await this.list;
  4189.  
  4190. // 如果存在,则返回信息
  4191. if (list[fid]) {
  4192. return list[fid];
  4193. }
  4194.  
  4195. return null;
  4196. }
  4197.  
  4198. /**
  4199. * 添加猎巫
  4200. * @param {Number} fid 版面 ID
  4201. * @param {String} label 标签
  4202. * @param {String} filterMode 过滤模式
  4203. * @param {Number} filterLevel 过滤等级: 0 - 仅标记; 1 - 标记并过滤
  4204. */
  4205. async add(fid, label, filterMode, filterLevel) {
  4206. // FID 只能是数字
  4207. fid = parseInt(fid, 10);
  4208.  
  4209. // 获取列表
  4210. const list = await this.list;
  4211.  
  4212. // 如果版面 ID 已存在,则提示错误
  4213. if (Object.keys(list).includes(fid)) {
  4214. alert("已有相同版面ID");
  4215. return;
  4216. }
  4217.  
  4218. // 请求版面信息
  4219. const info = await this.api.getForumInfo(fid);
  4220.  
  4221. // 如果版面不存在,则提示错误
  4222. if (info === null || info === undefined) {
  4223. alert("版面ID有误");
  4224. return;
  4225. }
  4226.  
  4227. // 计算标记颜色
  4228. const color = Tools.generateColor(info.name);
  4229.  
  4230. // 写入猎巫信息
  4231. list[fid] = {
  4232. fid,
  4233. name: info.name,
  4234. label,
  4235. color,
  4236. filterMode,
  4237. filterLevel,
  4238. };
  4239.  
  4240. // 保存数据
  4241. this.settings.cache.put("WITCH_HUNT", list);
  4242.  
  4243. // 重新过滤
  4244. this.reFilter(true);
  4245.  
  4246. // 返回添加的猎巫
  4247. return list[fid];
  4248. }
  4249.  
  4250. /**
  4251. * 编辑猎巫
  4252. * @param {Number} fid 版面 ID
  4253. * @param {*} values 猎巫信息
  4254. */
  4255. async update(fid, values) {
  4256. // 获取列表
  4257. const list = await this.list;
  4258.  
  4259. // 如果不存在则跳过
  4260. if (Object.hasOwn(list, fid) === false) {
  4261. return null;
  4262. }
  4263.  
  4264. // 获取猎巫
  4265. const entity = list[fid];
  4266.  
  4267. // 更新猎巫
  4268. Object.assign(entity, values);
  4269.  
  4270. // 保存数据
  4271. this.settings.cache.put("WITCH_HUNT", list);
  4272.  
  4273. // 重新过滤,更新样式即可
  4274. this.reFilter(false);
  4275. }
  4276.  
  4277. /**
  4278. * 删除猎巫
  4279. * @param {Number} fid 版面 ID
  4280. */
  4281. async remove(fid) {
  4282. // 获取列表
  4283. const list = await this.list;
  4284.  
  4285. // 如果不存在则跳过
  4286. if (Object.hasOwn(list, fid) === false) {
  4287. return null;
  4288. }
  4289.  
  4290. // 获取猎巫
  4291. const entity = list[fid];
  4292.  
  4293. // 删除猎巫
  4294. delete list[fid];
  4295.  
  4296. // 保存数据
  4297. this.settings.cache.put("WITCH_HUNT", list);
  4298.  
  4299. // 重新过滤
  4300. this.reFilter(true);
  4301.  
  4302. // 返回删除的猎巫
  4303. return entity;
  4304. }
  4305.  
  4306. /**
  4307. * 格式化版面
  4308. * @param {Number} fid 版面 ID
  4309. * @param {String} name 版面名称
  4310. */
  4311. formatForum(fid, name) {
  4312. const { ui } = this;
  4313.  
  4314. return ui.createElement("A", `[${name}]`, {
  4315. className: "b nobr",
  4316. href: `/thread.php?fid=${fid}`,
  4317. });
  4318. }
  4319.  
  4320. /**
  4321. * 格式化标签
  4322. * @param {String} name 标签名称
  4323. * @param {String} name 标签颜色
  4324. */
  4325. formatLabel(name, color) {
  4326. const { ui } = this;
  4327.  
  4328. return ui.createElement("B", name, {
  4329. className: "block_txt nobr",
  4330. style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
  4331. });
  4332. }
  4333.  
  4334. /**
  4335. * 表格列
  4336. * @returns {Array} 表格列集合
  4337. */
  4338. columns() {
  4339. return [
  4340. { label: "版面", width: 200 },
  4341. { label: "标签" },
  4342. { label: "启用过滤", center: true, width: 1 },
  4343. { label: "过滤模式", center: true, width: 1 },
  4344. { label: "操作", width: 1 },
  4345. ];
  4346. }
  4347.  
  4348. /**
  4349. * 表格项
  4350. * @param {*} item 猎巫信息
  4351. * @returns {Array} 表格项集合
  4352. */
  4353. column(item) {
  4354. const { ui } = this;
  4355. const { table } = this.views;
  4356. const { fid, name, label, color, filterMode, filterLevel } = item;
  4357.  
  4358. // 版面
  4359. const forum = this.formatForum(fid, name);
  4360.  
  4361. // 标签
  4362. const labelElement = this.formatLabel(label, color);
  4363.  
  4364. // 启用过滤
  4365. const switchLevel = ui.createElement("INPUT", [], {
  4366. type: "checkbox",
  4367. checked: filterLevel > 0,
  4368. });
  4369.  
  4370. // 切换过滤模式
  4371. const switchMode = ui.createButton(
  4372. filterMode || this.settings.filterModes[0],
  4373. () => {
  4374. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4375.  
  4376. switchMode.innerText = newMode;
  4377. }
  4378. );
  4379.  
  4380. // 操作
  4381. const buttons = (() => {
  4382. const save = ui.createButton("保存", () => {
  4383. this.update(fid, {
  4384. filterMode: switchMode.innerText,
  4385. filterLevel: switchLevel.checked ? 1 : 0,
  4386. });
  4387. });
  4388.  
  4389. const remove = ui.createButton("删除", (e) => {
  4390. ui.confirm().then(async () => {
  4391. await this.remove(fid);
  4392.  
  4393. table.remove(e);
  4394. });
  4395. });
  4396.  
  4397. return ui.createButtonGroup(save, remove);
  4398. })();
  4399.  
  4400. return [forum, labelElement, switchLevel, switchMode, buttons];
  4401. }
  4402.  
  4403. /**
  4404. * 初始化组件
  4405. */
  4406. initComponents() {
  4407. super.initComponents();
  4408.  
  4409. const { ui } = this;
  4410. const { tabs, content } = ui.views;
  4411.  
  4412. const table = ui.createTable(this.columns());
  4413.  
  4414. const tips = ui.createElement(
  4415. "DIV",
  4416. [TIPS.hunter, TIPS.error].join("<br/>"),
  4417. {
  4418. className: "silver",
  4419. }
  4420. );
  4421.  
  4422. const tab = ui.createTab(
  4423. tabs,
  4424. this.constructor.label,
  4425. this.constructor.order,
  4426. {
  4427. onclick: () => {
  4428. this.render(content);
  4429. },
  4430. }
  4431. );
  4432.  
  4433. Object.assign(this.views, {
  4434. tab,
  4435. table,
  4436. });
  4437.  
  4438. this.views.container.appendChild(table);
  4439. this.views.container.appendChild(tips);
  4440. }
  4441.  
  4442. /**
  4443. * 渲染
  4444. * @param {HTMLElement} container 容器
  4445. */
  4446. render(container) {
  4447. super.render(container);
  4448.  
  4449. const { table } = this.views;
  4450.  
  4451. if (table) {
  4452. const { add, clear } = table;
  4453.  
  4454. clear();
  4455.  
  4456. this.list.then((values) => {
  4457. Object.values(values).forEach((item) => {
  4458. const column = this.column(item);
  4459.  
  4460. add(...column);
  4461. });
  4462.  
  4463. this.renderNewLine();
  4464. });
  4465. }
  4466. }
  4467.  
  4468. /**
  4469. * 渲染新行
  4470. */
  4471. renderNewLine() {
  4472. const { ui } = this;
  4473. const { table } = this.views;
  4474.  
  4475. // 版面 ID
  4476. const forumInput = ui.createElement("INPUT", [], {
  4477. type: "text",
  4478. });
  4479.  
  4480. const forumInputWrapper = ui.createElement("DIV", forumInput, {
  4481. className: "filter-input-wrapper",
  4482. });
  4483.  
  4484. // 标签
  4485. const labelInput = ui.createElement("INPUT", [], {
  4486. type: "text",
  4487. });
  4488.  
  4489. const labelInputWrapper = ui.createElement("DIV", labelInput, {
  4490. className: "filter-input-wrapper",
  4491. });
  4492.  
  4493. // 启用过滤
  4494. const switchLevel = ui.createElement("INPUT", [], {
  4495. type: "checkbox",
  4496. });
  4497.  
  4498. // 切换过滤模式
  4499. const switchMode = ui.createButton(this.settings.filterModes[0], () => {
  4500. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4501.  
  4502. switchMode.innerText = newMode;
  4503. });
  4504.  
  4505. // 操作
  4506. const buttons = (() => {
  4507. const save = ui.createButton("添加", async (e) => {
  4508. const entity = await this.add(
  4509. forumInput.value,
  4510. labelInput.value,
  4511. switchMode.innerText,
  4512. switchLevel.checked ? 1 : 0
  4513. );
  4514.  
  4515. if (entity) {
  4516. table.update(e, ...this.column(entity));
  4517.  
  4518. this.renderNewLine();
  4519. }
  4520. });
  4521.  
  4522. return ui.createButtonGroup(save);
  4523. })();
  4524.  
  4525. // 添加至列表
  4526. table.add(
  4527. forumInputWrapper,
  4528. labelInputWrapper,
  4529. switchLevel,
  4530. switchMode,
  4531. buttons
  4532. );
  4533. }
  4534.  
  4535. /**
  4536. * 过滤
  4537. * @param {*} item 绑定的 nFilter
  4538. * @param {*} result 过滤结果
  4539. */
  4540. async filter(item, result) {
  4541. // 获取当前猎巫结果
  4542. const hunter = item.hunter || [];
  4543.  
  4544. // 如果没有猎巫结果,则跳过
  4545. if (hunter.length === 0) {
  4546. return;
  4547. }
  4548.  
  4549. // 获取列表
  4550. const items = await this.list;
  4551.  
  4552. // 筛选出匹配的猎巫
  4553. const list = Object.values(items).filter(({ fid }) =>
  4554. hunter.includes(fid)
  4555. );
  4556.  
  4557. // 取最高的过滤模式
  4558. // 低于当前的过滤模式则跳过
  4559. let max = result.mode;
  4560. let res = null;
  4561.  
  4562. for (const entity of list) {
  4563. const { filterLevel, filterMode } = entity;
  4564.  
  4565. // 仅标记
  4566. if (filterLevel === 0) {
  4567. continue;
  4568. }
  4569.  
  4570. // 获取过滤模式
  4571. const mode = this.settings.getModeByName(filterMode);
  4572.  
  4573. if (mode <= max) {
  4574. continue;
  4575. }
  4576.  
  4577. max = mode;
  4578. res = entity;
  4579. }
  4580.  
  4581. // 没有匹配的则跳过
  4582. if (res === null) {
  4583. return;
  4584. }
  4585.  
  4586. // 更新过滤模式和原因
  4587. result.mode = max;
  4588. result.reason = `猎巫: ${res.label}`;
  4589. }
  4590.  
  4591. /**
  4592. * 通知
  4593. * @param {*} item 绑定的 nFilter
  4594. */
  4595. async notify(item) {
  4596. const { uid, tags } = item;
  4597.  
  4598. // 如果没有 tags 组件则跳过
  4599. if (tags === null) {
  4600. return;
  4601. }
  4602.  
  4603. // 如果是匿名,隐藏组件
  4604. if (uid <= 0) {
  4605. tags.style.display = "none";
  4606. return;
  4607. }
  4608.  
  4609. // 删除旧标签
  4610. [...tags.querySelectorAll("[fid]")].forEach((item) => {
  4611. tags.removeChild(item);
  4612. });
  4613.  
  4614. // 如果没有请求,开始请求
  4615. if (Object.hasOwn(item, "hunter") === false) {
  4616. this.execute(item);
  4617. return;
  4618. }
  4619.  
  4620. // 获取当前猎巫结果
  4621. const hunter = item.hunter;
  4622.  
  4623. // 如果没有猎巫结果,则跳过
  4624. if (hunter.length === 0) {
  4625. return;
  4626. }
  4627.  
  4628. // 格式化标签
  4629. const items = await Promise.all(
  4630. hunter.map(async (fid) => {
  4631. const item = await this.get(fid);
  4632.  
  4633. if (item) {
  4634. const element = this.formatLabel(item.label, item.color);
  4635.  
  4636. element.setAttribute("fid", fid);
  4637.  
  4638. return element;
  4639. }
  4640.  
  4641. return null;
  4642. })
  4643. );
  4644.  
  4645. // 加入组件
  4646. items.forEach((item) => {
  4647. if (item) {
  4648. tags.appendChild(item);
  4649. }
  4650. });
  4651. }
  4652.  
  4653. /**
  4654. * 重新过滤
  4655. * @param {Boolean} clear 是否清除缓存
  4656. */
  4657. reFilter(clear) {
  4658. // 清除缓存
  4659. if (clear) {
  4660. this.cache = {};
  4661. }
  4662.  
  4663. // 重新过滤
  4664. this.data.forEach((item) => {
  4665. // 不需要清除缓存的话,只要重新加载标记
  4666. if (clear === false) {
  4667. item.hunter = [];
  4668. }
  4669.  
  4670. // 重新猎巫
  4671. this.execute(item);
  4672. });
  4673. }
  4674.  
  4675. /**
  4676. * 猎巫
  4677. * @param {*} item 绑定的 nFilter
  4678. */
  4679. async execute(item) {
  4680. const { uid } = item;
  4681. const { api, cache, queue, list } = this;
  4682.  
  4683. // 如果是匿名,则跳过
  4684. if (uid <= 0) {
  4685. return;
  4686. }
  4687.  
  4688. // 初始化猎巫结果,用于标识正在猎巫
  4689. item.hunter = item.hunter || [];
  4690.  
  4691. // 获取列表
  4692. const items = await list;
  4693.  
  4694. // 没有设置且没有旧数据,直接跳过
  4695. if (items.length === 0 && item.hunter.length === 0) {
  4696. return;
  4697. }
  4698.  
  4699. // 重新过滤
  4700. const reload = (newValue) => {
  4701. const isEqual = newValue.sort().join() === item.hunter.sort().join();
  4702.  
  4703. if (isEqual) {
  4704. return;
  4705. }
  4706.  
  4707. item.hunter = newValue;
  4708. item.execute();
  4709. };
  4710.  
  4711. // 创建任务
  4712. const task = async () => {
  4713. // 如果缓存里没有记录,请求数据并写入缓存
  4714. if (Object.hasOwn(cache, uid) === false) {
  4715. cache[uid] = [];
  4716.  
  4717. await Promise.all(
  4718. Object.keys(items).map(async (fid) => {
  4719. // 转换为数字格式
  4720. const id = parseInt(fid, 10);
  4721.  
  4722. // 当前版面发言记录
  4723. const result = await api.getForumPosted(id, uid);
  4724.  
  4725. // 写入当前设置
  4726. if (result) {
  4727. cache[uid].push(id);
  4728. }
  4729. })
  4730. );
  4731. }
  4732.  
  4733. // 重新过滤
  4734. reload(cache[uid]);
  4735.  
  4736. // 将当前任务移出队列
  4737. queue.shift();
  4738.  
  4739. // 如果还有任务,继续执行
  4740. if (queue.length > 0) {
  4741. queue[0]();
  4742. }
  4743. };
  4744.  
  4745. // 队列里已经有任务
  4746. const isRunning = queue.length > 0;
  4747.  
  4748. // 加入队列
  4749. queue.push(task);
  4750.  
  4751. // 如果没有正在执行的任务,则立即执行
  4752. if (isRunning === false) {
  4753. task();
  4754. }
  4755. }
  4756. }
  4757.  
  4758. /**
  4759. * 杂项模块
  4760. */
  4761. class MiscModule extends Module {
  4762. /**
  4763. * 模块名称
  4764. */
  4765. static name = "misc";
  4766.  
  4767. /**
  4768. * 模块标签
  4769. */
  4770. static label = "杂项";
  4771.  
  4772. /**
  4773. * 顺序
  4774. */
  4775. static order = 100;
  4776.  
  4777. /**
  4778. * 请求缓存
  4779. */
  4780. cache = {
  4781. topicNums: {},
  4782. };
  4783.  
  4784. /**
  4785. * 获取用户信息(从页面上)
  4786. * @param {*} item 绑定的 nFilter
  4787. */
  4788. getUserInfo(item) {
  4789. const { uid } = item;
  4790.  
  4791. // 如果是匿名直接跳过
  4792. if (uid <= 0) {
  4793. return;
  4794. }
  4795.  
  4796. // 回复页面可以直接获取到用户信息和声望
  4797. if (commonui.userInfo) {
  4798. // 取得用户信息
  4799. const userInfo = commonui.userInfo.users[uid];
  4800.  
  4801. // 绑定用户信息和声望
  4802. if (userInfo) {
  4803. item.userInfo = userInfo;
  4804. item.username = userInfo.username;
  4805.  
  4806. item.reputation = (() => {
  4807. const reputations = commonui.userInfo.reputations;
  4808.  
  4809. if (reputations) {
  4810. for (let fid in reputations) {
  4811. return reputations[fid][uid] || 0;
  4812. }
  4813. }
  4814.  
  4815. return NaN;
  4816. })();
  4817. }
  4818. }
  4819. }
  4820.  
  4821. /**
  4822. * 获取帖子数据
  4823. * @param {*} item 绑定的 nFilter
  4824. */
  4825. async getPostInfo(item) {
  4826. const { tid, pid } = item;
  4827.  
  4828. // 请求帖子数据
  4829. const { subject, content, userInfo, reputation } =
  4830. await this.api.getPostInfo(tid, pid);
  4831.  
  4832. // 绑定用户信息和声望
  4833. if (userInfo) {
  4834. item.userInfo = userInfo;
  4835. item.username = userInfo.username;
  4836. item.reputation = reputation;
  4837. }
  4838.  
  4839. // 绑定标题和内容
  4840. item.subject = subject;
  4841. item.content = content;
  4842. }
  4843.  
  4844. /**
  4845. * 获取主题数量
  4846. * @param {*} item 绑定的 nFilter
  4847. */
  4848. async getTopicNum(item) {
  4849. const { uid } = item;
  4850.  
  4851. // 如果是匿名直接跳过
  4852. if (uid <= 0) {
  4853. return;
  4854. }
  4855.  
  4856. // 如果已有缓存,直接返回
  4857. if (Object.hasOwn(this.cache.topicNums, uid)) {
  4858. return this.cache.topicNums[uid];
  4859. }
  4860.  
  4861. // 请求数量
  4862. const number = await this.api.getTopicNum(uid);
  4863.  
  4864. // 写入缓存
  4865. this.cache.topicNums[uid] = number;
  4866.  
  4867. // 返回结果
  4868. return number;
  4869. }
  4870.  
  4871. /**
  4872. * 初始化,增加设置
  4873. */
  4874. initComponents() {
  4875. super.initComponents();
  4876.  
  4877. const { settings, ui } = this;
  4878. const { add } = ui.views.settings;
  4879.  
  4880. // 小号过滤(注册(不可用)时间)
  4881. {
  4882. const input = ui.createElement("INPUT", [], {
  4883. type: "text",
  4884. value: settings.filterRegdateLimit / 86400000,
  4885. maxLength: 4,
  4886. style: "width: 48px;",
  4887. });
  4888.  
  4889. const button = ui.createButton("确认", () => {
  4890. const newValue = parseInt(input.value, 10) || 0;
  4891.  
  4892. if (newValue < 0) {
  4893. return;
  4894. }
  4895.  
  4896. settings.filterRegdateLimit = newValue * 86400000;
  4897.  
  4898. this.reFilter();
  4899. });
  4900.  
  4901. const element = ui.createElement("DIV", [
  4902. "隐藏注册(不可用)时间小于",
  4903. input,
  4904. "天的用户",
  4905. button,
  4906. ]);
  4907.  
  4908. add(this.constructor.order + 0, element);
  4909. }
  4910.  
  4911. // 小号过滤(发帖数)
  4912. {
  4913. const input = ui.createElement("INPUT", [], {
  4914. type: "text",
  4915. value: settings.filterPostnumLimit,
  4916. maxLength: 5,
  4917. style: "width: 48px;",
  4918. });
  4919.  
  4920. const button = ui.createButton("确认", () => {
  4921. const newValue = parseInt(input.value, 10) || 0;
  4922.  
  4923. if (newValue < 0) {
  4924. return;
  4925. }
  4926.  
  4927. settings.filterPostnumLimit = newValue;
  4928.  
  4929. this.reFilter();
  4930. });
  4931.  
  4932. const element = ui.createElement("DIV", [
  4933. "隐藏发帖数量小于",
  4934. input,
  4935. "贴的用户",
  4936. button,
  4937. ]);
  4938.  
  4939. add(this.constructor.order + 1, element);
  4940. }
  4941.  
  4942. // 流量号过滤(主题比例)
  4943. {
  4944. const input = ui.createElement("INPUT", [], {
  4945. type: "text",
  4946. value: settings.filterTopicRateLimit,
  4947. maxLength: 3,
  4948. style: "width: 48px;",
  4949. });
  4950.  
  4951. const button = ui.createButton("确认", () => {
  4952. const newValue = parseInt(input.value, 10) || 100;
  4953.  
  4954. if (newValue <= 0 || newValue > 100) {
  4955. return;
  4956. }
  4957.  
  4958. settings.filterTopicRateLimit = newValue;
  4959.  
  4960. this.reFilter();
  4961. });
  4962.  
  4963. const element = ui.createElement("DIV", [
  4964. "隐藏发帖比例大于",
  4965. input,
  4966. "%的用户",
  4967. button,
  4968. ]);
  4969.  
  4970. const tips = ui.createElement("DIV", TIPS.error, {
  4971. className: "silver",
  4972. });
  4973.  
  4974. add(
  4975. this.constructor.order + 2,
  4976. ui.createElement("DIV", [element, tips])
  4977. );
  4978. }
  4979.  
  4980. // 声望过滤
  4981. {
  4982. const input = ui.createElement("INPUT", [], {
  4983. type: "text",
  4984. value: settings.filterReputationLimit || "",
  4985. maxLength: 4,
  4986. style: "width: 48px;",
  4987. });
  4988.  
  4989. const button = ui.createButton("确认", () => {
  4990. const newValue = parseInt(input.value, 10);
  4991.  
  4992. settings.filterReputationLimit = newValue;
  4993.  
  4994. this.reFilter();
  4995. });
  4996.  
  4997. const element = ui.createElement("DIV", [
  4998. "隐藏版面声望低于",
  4999. input,
  5000. "点的用户",
  5001. button,
  5002. ]);
  5003.  
  5004. add(this.constructor.order + 3, element);
  5005. }
  5006.  
  5007. // 匿名过滤
  5008. {
  5009. const input = ui.createElement("INPUT", [], {
  5010. type: "checkbox",
  5011. checked: settings.filterAnonymous,
  5012. });
  5013.  
  5014. const label = ui.createElement("LABEL", ["隐藏匿名的用户", input], {
  5015. style: "display: flex;",
  5016. });
  5017.  
  5018. const element = ui.createElement("DIV", label);
  5019.  
  5020. input.onchange = () => {
  5021. settings.filterAnonymous = input.checked;
  5022.  
  5023. this.reFilter();
  5024. };
  5025.  
  5026. add(this.constructor.order + 4, element);
  5027. }
  5028. }
  5029.  
  5030. /**
  5031. * 过滤
  5032. * @param {*} item 绑定的 nFilter
  5033. * @param {*} result 过滤结果
  5034. */
  5035. async filter(item, result) {
  5036. // 获取隐藏模式下标
  5037. const mode = this.settings.getModeByName("隐藏");
  5038.  
  5039. // 如果当前模式不低于隐藏模式,则跳过
  5040. if (result.mode >= mode) {
  5041. return;
  5042. }
  5043.  
  5044. // 匿名过滤
  5045. await this.filterByAnonymous(item, result);
  5046.  
  5047. // 注册(不可用)时间过滤
  5048. await this.filterByRegdate(item, result);
  5049.  
  5050. // 发帖数量过滤
  5051. await this.filterByPostnum(item, result);
  5052.  
  5053. // 发帖比例过滤
  5054. await this.filterByTopicRate(item, result);
  5055.  
  5056. // 版面声望过滤
  5057. await this.filterByReputation(item, result);
  5058. }
  5059.  
  5060. /**
  5061. * 根据匿名过滤
  5062. * @param {*} item 绑定的 nFilter
  5063. * @param {*} result 过滤结果
  5064. */
  5065. async filterByAnonymous(item, result) {
  5066. const { uid } = item;
  5067.  
  5068. // 如果不是匿名,则跳过
  5069. if (uid > 0) {
  5070. return;
  5071. }
  5072.  
  5073. // 获取隐藏模式下标
  5074. const mode = this.settings.getModeByName("隐藏");
  5075.  
  5076. // 如果当前模式不低于隐藏模式,则跳过
  5077. if (result.mode >= mode) {
  5078. return;
  5079. }
  5080.  
  5081. // 获取过滤匿名设置
  5082. const filterAnonymous = this.settings.filterAnonymous;
  5083.  
  5084. if (filterAnonymous) {
  5085. // 更新过滤模式和原因
  5086. result.mode = mode;
  5087. result.reason = "匿名";
  5088. }
  5089. }
  5090.  
  5091. /**
  5092. * 根据注册(不可用)时间过滤
  5093. * @param {*} item 绑定的 nFilter
  5094. * @param {*} result 过滤结果
  5095. */
  5096. async filterByRegdate(item, result) {
  5097. const { uid } = item;
  5098.  
  5099. // 如果是匿名,则跳过
  5100. if (uid <= 0) {
  5101. return;
  5102. }
  5103.  
  5104. // 获取隐藏模式下标
  5105. const mode = this.settings.getModeByName("隐藏");
  5106.  
  5107. // 如果当前模式不低于隐藏模式,则跳过
  5108. if (result.mode >= mode) {
  5109. return;
  5110. }
  5111.  
  5112. // 获取注册(不可用)时间限制
  5113. const filterRegdateLimit = this.settings.filterRegdateLimit;
  5114.  
  5115. // 未启用则跳过
  5116. if (filterRegdateLimit <= 0) {
  5117. return;
  5118. }
  5119.  
  5120. // 没有用户信息,优先从页面上获取
  5121. if (item.userInfo === undefined) {
  5122. this.getUserInfo(item);
  5123. }
  5124.  
  5125. // 没有再从接口获取
  5126. if (item.userInfo === undefined) {
  5127. await this.getPostInfo(item);
  5128. }
  5129.  
  5130. // 获取注册(不可用)时间
  5131. const { regdate } = item.userInfo || {};
  5132.  
  5133. // 获取失败则跳过
  5134. if (regdate === undefined) {
  5135. return;
  5136. }
  5137.  
  5138. // 转换时间格式,泥潭接口只精确到秒
  5139. const date = new Date(regdate * 1000);
  5140.  
  5141. // 计算时间差
  5142. const diff = Date.now() - date;
  5143.  
  5144. // 判断是否符合条件
  5145. if (diff > filterRegdateLimit) {
  5146. return;
  5147. }
  5148.  
  5149. // 转换为天数
  5150. const days = Math.floor(diff / 86400000);
  5151.  
  5152. // 更新过滤模式和原因
  5153. result.mode = mode;
  5154. result.reason = `注册(不可用)时间: ${days}天`;
  5155. }
  5156.  
  5157. /**
  5158. * 根据发帖数量过滤
  5159. * @param {*} item 绑定的 nFilter
  5160. * @param {*} result 过滤结果
  5161. */
  5162. async filterByPostnum(item, result) {
  5163. const { uid } = item;
  5164.  
  5165. // 如果是匿名,则跳过
  5166. if (uid <= 0) {
  5167. return;
  5168. }
  5169.  
  5170. // 获取隐藏模式下标
  5171. const mode = this.settings.getModeByName("隐藏");
  5172.  
  5173. // 如果当前模式不低于隐藏模式,则跳过
  5174. if (result.mode >= mode) {
  5175. return;
  5176. }
  5177.  
  5178. // 获取发帖数量限制
  5179. const filterPostnumLimit = this.settings.filterPostnumLimit;
  5180.  
  5181. // 未启用则跳过
  5182. if (filterPostnumLimit <= 0) {
  5183. return;
  5184. }
  5185.  
  5186. // 没有用户信息,优先从页面上获取
  5187. if (item.userInfo === undefined) {
  5188. this.getUserInfo(item);
  5189. }
  5190.  
  5191. // 没有再从接口获取
  5192. if (item.userInfo === undefined) {
  5193. await this.getPostInfo(item);
  5194. }
  5195.  
  5196. // 获取发帖数量
  5197. const { postnum } = item.userInfo || {};
  5198.  
  5199. // 获取失败则跳过
  5200. if (postnum === undefined) {
  5201. return;
  5202. }
  5203.  
  5204. // 判断是否符合条件
  5205. if (postnum >= filterPostnumLimit) {
  5206. return;
  5207. }
  5208.  
  5209. // 更新过滤模式和原因
  5210. result.mode = mode;
  5211. result.reason = `发帖数量: ${postnum}`;
  5212. }
  5213.  
  5214. /**
  5215. * 根据发帖比例过滤
  5216. * @param {*} item 绑定的 nFilter
  5217. * @param {*} result 过滤结果
  5218. */
  5219. async filterByTopicRate(item, result) {
  5220. const { uid } = item;
  5221.  
  5222. // 如果是匿名,则跳过
  5223. if (uid <= 0) {
  5224. return;
  5225. }
  5226.  
  5227. // 获取隐藏模式下标
  5228. const mode = this.settings.getModeByName("隐藏");
  5229.  
  5230. // 如果当前模式不低于隐藏模式,则跳过
  5231. if (result.mode >= mode) {
  5232. return;
  5233. }
  5234.  
  5235. // 获取发帖比例限制
  5236. const filterTopicRateLimit = this.settings.filterTopicRateLimit;
  5237.  
  5238. // 未启用则跳过
  5239. if (filterTopicRateLimit <= 0 || filterTopicRateLimit >= 100) {
  5240. return;
  5241. }
  5242.  
  5243. // 没有用户信息,优先从页面上获取
  5244. if (item.userInfo === undefined) {
  5245. this.getUserInfo(item);
  5246. }
  5247.  
  5248. // 没有再从接口获取
  5249. if (item.userInfo === undefined) {
  5250. await this.getPostInfo(item);
  5251. }
  5252.  
  5253. // 获取发帖数量
  5254. const { postnum } = item.userInfo || {};
  5255.  
  5256. // 获取失败则跳过
  5257. if (postnum === undefined) {
  5258. return;
  5259. }
  5260.  
  5261. // 获取主题数量
  5262. const topicNum = await this.getTopicNum(item);
  5263.  
  5264. // 计算发帖比例
  5265. const topicRate = Math.ceil((topicNum / postnum) * 100);
  5266.  
  5267. // 判断是否符合条件
  5268. if (topicRate < filterTopicRateLimit) {
  5269. return;
  5270. }
  5271.  
  5272. // 更新过滤模式和原因
  5273. result.mode = mode;
  5274. result.reason = `发帖比例: ${topicRate}% (${topicNum}/${postnum})`;
  5275. }
  5276.  
  5277. /**
  5278. * 根据版面声望过滤
  5279. * @param {*} item 绑定的 nFilter
  5280. * @param {*} result 过滤结果
  5281. */
  5282. async filterByReputation(item, result) {
  5283. const { uid } = item;
  5284.  
  5285. // 如果是匿名,则跳过
  5286. if (uid <= 0) {
  5287. return;
  5288. }
  5289.  
  5290. // 获取隐藏模式下标
  5291. const mode = this.settings.getModeByName("隐藏");
  5292.  
  5293. // 如果当前模式不低于隐藏模式,则跳过
  5294. if (result.mode >= mode) {
  5295. return;
  5296. }
  5297.  
  5298. // 获取版面声望限制
  5299. const filterReputationLimit = this.settings.filterReputationLimit;
  5300.  
  5301. // 未启用则跳过
  5302. if (Number.isNaN(filterReputationLimit)) {
  5303. return;
  5304. }
  5305.  
  5306. // 没有声望信息,优先从页面上获取
  5307. if (item.reputation === undefined) {
  5308. this.getUserInfo(item);
  5309. }
  5310.  
  5311. // 没有再从接口获取
  5312. if (item.reputation === undefined) {
  5313. await this.getPostInfo(item);
  5314. }
  5315.  
  5316. // 获取版面声望
  5317. const reputation = item.reputation || 0;
  5318.  
  5319. // 判断是否符合条件
  5320. if (reputation >= filterReputationLimit) {
  5321. return;
  5322. }
  5323.  
  5324. // 更新过滤模式和原因
  5325. result.mode = mode;
  5326. result.reason = `版面声望: ${reputation}`;
  5327. }
  5328.  
  5329. /**
  5330. * 重新过滤
  5331. */
  5332. reFilter() {
  5333. this.data.forEach((item) => {
  5334. item.execute();
  5335. });
  5336. }
  5337. }
  5338.  
  5339. /**
  5340. * 设置模块
  5341. */
  5342. class SettingsModule extends Module {
  5343. /**
  5344. * 模块名称
  5345. */
  5346. static name = "settings";
  5347.  
  5348. /**
  5349. * 顺序
  5350. */
  5351. static order = 0;
  5352.  
  5353. /**
  5354. * 创建实例
  5355. * @param {Settings} settings 设置
  5356. * @param {API} api API
  5357. * @param {UI} ui UI
  5358. * @param {Array} data 过滤列表
  5359. * @returns {Module | null} 成功后返回模块实例
  5360. */
  5361. static create(settings, api, ui, data) {
  5362. // 读取设置里的模块列表
  5363. const modules = settings.modules;
  5364.  
  5365. // 如果不包含自己,加入列表中,因为设置模块是必须的
  5366. if (modules.includes(this.name) === false) {
  5367. settings.modules = [...modules, this.name];
  5368. }
  5369.  
  5370. // 创建实例
  5371. return super.create(settings, api, ui, data);
  5372. }
  5373.  
  5374. /**
  5375. * 初始化,增加设置
  5376. */
  5377. initComponents() {
  5378. super.initComponents();
  5379.  
  5380. const { settings, ui } = this;
  5381. const { add } = ui.views.settings;
  5382.  
  5383. // 前置过滤
  5384. {
  5385. const input = ui.createElement("INPUT", [], {
  5386. type: "checkbox",
  5387. });
  5388.  
  5389. const label = ui.createElement("LABEL", ["前置过滤", input], {
  5390. style: "display: flex;",
  5391. });
  5392.  
  5393. settings.preFilterEnabled.then((checked) => {
  5394. input.checked = checked;
  5395. input.onchange = () => {
  5396. settings.preFilterEnabled = !checked;
  5397. };
  5398. });
  5399.  
  5400. add(this.constructor.order + 0, label);
  5401. }
  5402.  
  5403. // 模块选择
  5404. {
  5405. const modules = [
  5406. ListModule,
  5407. UserModule,
  5408. TagModule,
  5409. KeywordModule,
  5410. LocationModule,
  5411. ForumOrSubsetModule,
  5412. HunterModule,
  5413. MiscModule,
  5414. ];
  5415.  
  5416. const items = modules.map((item) => {
  5417. const input = ui.createElement("INPUT", [], {
  5418. type: "checkbox",
  5419. value: item.name,
  5420. checked: settings.modules.includes(item.name),
  5421. onchange: () => {
  5422. const checked = input.checked;
  5423.  
  5424. modules.map((m, index) => {
  5425. const isDepend = checked
  5426. ? item.depends.find((i) => i.name === m.name)
  5427. : m.depends.find((i) => i.name === item.name);
  5428.  
  5429. if (isDepend) {
  5430. const element = items[index].querySelector("INPUT");
  5431.  
  5432. if (element) {
  5433. element.checked = checked;
  5434. }
  5435. }
  5436. });
  5437. },
  5438. });
  5439.  
  5440. const label = ui.createElement("LABEL", [item.label, input], {
  5441. style: "display: flex; margin-right: 10px;",
  5442. });
  5443.  
  5444. return label;
  5445. });
  5446.  
  5447. const button = ui.createButton("确认", () => {
  5448. const checked = group.querySelectorAll("INPUT:checked");
  5449. const values = [...checked].map((item) => item.value);
  5450.  
  5451. settings.modules = values;
  5452.  
  5453. location.reload();
  5454. });
  5455.  
  5456. const group = ui.createElement("DIV", [...items, button], {
  5457. style: "display: flex;",
  5458. });
  5459.  
  5460. const label = ui.createElement("LABEL", "启用模块");
  5461.  
  5462. add(this.constructor.order + 1, label, group);
  5463. }
  5464.  
  5465. // 默认过滤模式
  5466. {
  5467. const modes = ["标记", "遮罩", "隐藏"].map((item) => {
  5468. const input = ui.createElement("INPUT", [], {
  5469. type: "radio",
  5470. name: "defaultFilterMode",
  5471. value: item,
  5472. checked: settings.defaultFilterMode === item,
  5473. onchange: () => {
  5474. settings.defaultFilterMode = item;
  5475.  
  5476. this.reFilter();
  5477. },
  5478. });
  5479.  
  5480. const label = ui.createElement("LABEL", [item, input], {
  5481. style: "display: flex; margin-right: 10px;",
  5482. });
  5483.  
  5484. return label;
  5485. });
  5486.  
  5487. const group = ui.createElement("DIV", modes, {
  5488. style: "display: flex;",
  5489. });
  5490.  
  5491. const label = ui.createElement("LABEL", "默认过滤模式");
  5492.  
  5493. const tips = ui.createElement("DIV", TIPS.filterMode, {
  5494. className: "silver",
  5495. });
  5496.  
  5497. add(this.constructor.order + 2, label, group, tips);
  5498. }
  5499. }
  5500.  
  5501. /**
  5502. * 重新过滤
  5503. */
  5504. reFilter() {
  5505. // 目前仅在修改默认过滤模式时重新过滤
  5506. this.data.forEach((item) => {
  5507. // 如果过滤模式是继承,则重新过滤
  5508. if (item.filterMode === "继承") {
  5509. item.execute();
  5510. }
  5511.  
  5512. // 如果有引用,也重新过滤
  5513. if (Object.values(item.quotes || {}).includes("继承")) {
  5514. item.execute();
  5515. return;
  5516. }
  5517. });
  5518. }
  5519. }
  5520.  
  5521. /**
  5522. * 增强的列表模块,增加了用户作为附加模块
  5523. */
  5524. class ListEnhancedModule extends ListModule {
  5525. /**
  5526. * 模块名称
  5527. */
  5528. static name = "list";
  5529.  
  5530. /**
  5531. * 附加模块
  5532. */
  5533. static addons = [UserModule];
  5534.  
  5535. /**
  5536. * 附加的用户模块
  5537. * @returns {UserModule} 用户模块
  5538. */
  5539. get userModule() {
  5540. return this.addons[UserModule.name];
  5541. }
  5542.  
  5543. /**
  5544. * 表格列
  5545. * @returns {Array} 表格列集合
  5546. */
  5547. columns() {
  5548. const hasAddon = this.hasAddon(UserModule);
  5549.  
  5550. if (hasAddon === false) {
  5551. return super.columns();
  5552. }
  5553.  
  5554. return [
  5555. { label: "用户", width: 1 },
  5556. { label: "内容", ellipsis: true },
  5557. { label: "过滤模式", center: true, width: 1 },
  5558. { label: "原因", width: 1 },
  5559. { label: "操作", width: 1 },
  5560. ];
  5561. }
  5562.  
  5563. /**
  5564. * 表格项
  5565. * @param {*} item 绑定的 nFilter
  5566. * @returns {Array} 表格项集合
  5567. */
  5568. column(item) {
  5569. const column = super.column(item);
  5570.  
  5571. const hasAddon = this.hasAddon(UserModule);
  5572.  
  5573. if (hasAddon === false) {
  5574. return column;
  5575. }
  5576.  
  5577. const { ui } = this;
  5578. const { table } = this.views;
  5579. const { uid, username } = item;
  5580.  
  5581. const user = this.userModule.format(uid, username);
  5582.  
  5583. const buttons = (() => {
  5584. if (uid <= 0) {
  5585. return null;
  5586. }
  5587.  
  5588. const block = ui.createButton("屏蔽", (e) => {
  5589. this.userModule.renderDetails(uid, username, (type) => {
  5590. // 删除失效数据,等待重新过滤
  5591. table.remove(e);
  5592.  
  5593. // 如果是新增,不会因为用户重新过滤,需要主动触发
  5594. if (type === "ADD") {
  5595. this.userModule.reFilter(uid);
  5596. }
  5597. });
  5598. });
  5599.  
  5600. return ui.createButtonGroup(block);
  5601. })();
  5602.  
  5603. return [user, ...column, buttons];
  5604. }
  5605. }
  5606.  
  5607. /**
  5608. * 增强的用户模块,增加了标记作为附加模块
  5609. */
  5610. class UserEnhancedModule extends UserModule {
  5611. /**
  5612. * 模块名称
  5613. */
  5614. static name = "user";
  5615.  
  5616. /**
  5617. * 附加模块
  5618. */
  5619. static addons = [TagModule];
  5620.  
  5621. /**
  5622. * 附加的标记模块
  5623. * @returns {TagModule} 标记模块
  5624. */
  5625. get tagModule() {
  5626. return this.addons[TagModule.name];
  5627. }
  5628.  
  5629. /**
  5630. * 表格列
  5631. * @returns {Array} 表格列集合
  5632. */
  5633. columns() {
  5634. const hasAddon = this.hasAddon(TagModule);
  5635.  
  5636. if (hasAddon === false) {
  5637. return super.columns();
  5638. }
  5639.  
  5640. return [
  5641. { label: "昵称", width: 1 },
  5642. { label: "标记" },
  5643. { label: "过滤模式", center: true, width: 1 },
  5644. { label: "操作", width: 1 },
  5645. ];
  5646. }
  5647.  
  5648. /**
  5649. * 表格项
  5650. * @param {*} item 用户信息
  5651. * @returns {Array} 表格项集合
  5652. */
  5653. column(item) {
  5654. const column = super.column(item);
  5655.  
  5656. const hasAddon = this.hasAddon(TagModule);
  5657.  
  5658. if (hasAddon === false) {
  5659. return column;
  5660. }
  5661.  
  5662. const { ui } = this;
  5663. const { table } = this.views;
  5664. const { id, name } = item;
  5665.  
  5666. const tags = ui.createElement(
  5667. "DIV",
  5668. item.tags.map((id) => this.tagModule.format(id))
  5669. );
  5670.  
  5671. const newColumn = [...column];
  5672.  
  5673. newColumn.splice(1, 0, tags);
  5674.  
  5675. const buttons = column[column.length - 1];
  5676.  
  5677. const update = ui.createButton("编辑", (e) => {
  5678. this.renderDetails(id, name, (type, newValue) => {
  5679. if (type === "UPDATE") {
  5680. table.update(e, ...this.column(newValue));
  5681. }
  5682.  
  5683. if (type === "REMOVE") {
  5684. table.remove(e);
  5685. }
  5686. });
  5687. });
  5688.  
  5689. buttons.insertBefore(update, buttons.firstChild);
  5690.  
  5691. return newColumn;
  5692. }
  5693.  
  5694. /**
  5695. * 渲染详情
  5696. * @param {Number} uid 用户 ID
  5697. * @param {String | undefined} name 用户名称
  5698. * @param {Function} callback 回调函数
  5699. */
  5700. renderDetails(uid, name, callback = () => {}) {
  5701. const hasAddon = this.hasAddon(TagModule);
  5702.  
  5703. if (hasAddon === false) {
  5704. return super.renderDetails(uid, name, callback);
  5705. }
  5706.  
  5707. const { ui, settings } = this;
  5708.  
  5709. // 只允许同时存在一个详情页
  5710. if (this.views.details) {
  5711. if (this.views.details.parentNode) {
  5712. this.views.details.parentNode.removeChild(this.views.details);
  5713. }
  5714. }
  5715.  
  5716. // 获取用户信息
  5717. const user = this.get(uid);
  5718.  
  5719. if (user) {
  5720. name = user.name;
  5721. }
  5722.  
  5723. // TODO 需要优化
  5724.  
  5725. const title =
  5726. (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;
  5727.  
  5728. const table = ui.createTable([]);
  5729.  
  5730. {
  5731. const size = Math.floor((screen.width * 0.8) / 200);
  5732.  
  5733. const items = Object.values(this.tagModule.list).map(({ id }) => {
  5734. const checked = user && user.tags.includes(id) ? "checked" : "";
  5735.  
  5736. return `
  5737. <td class="c1">
  5738. <label for="s-tag-${id}" style="display: block; cursor: pointer;">
  5739. ${this.tagModule.format(id).outerHTML}
  5740. </label>
  5741. </td>
  5742. <td class="c2" width="1">
  5743. <input id="s-tag-${id}" type="checkbox" value="${id}" ${checked}/>
  5744. </td>
  5745. `;
  5746. });
  5747.  
  5748. const rows = [...new Array(Math.ceil(items.length / size))].map(
  5749. (_, index) => `
  5750. <tr class="row${(index % 2) + 1}">
  5751. ${items.slice(size * index, size * (index + 1)).join("")}
  5752. </tr>
  5753. `
  5754. );
  5755.  
  5756. table.querySelector("TBODY").innerHTML = rows.join("");
  5757. }
  5758.  
  5759. const input = ui.createElement("INPUT", [], {
  5760. type: "text",
  5761. placeholder: TIPS.addTags,
  5762. style: "width: -webkit-fill-available;",
  5763. });
  5764.  
  5765. const inputWrapper = ui.createElement("DIV", input, {
  5766. style: "margin-top: 10px;",
  5767. });
  5768.  
  5769. const filterMode = user ? user.filterMode : settings.filterModes[0];
  5770.  
  5771. const switchMode = ui.createButton(filterMode, () => {
  5772. const newMode = settings.switchModeByName(switchMode.innerText);
  5773.  
  5774. switchMode.innerText = newMode;
  5775. });
  5776.  
  5777. const buttons = ui.createElement(
  5778. "DIV",
  5779. (() => {
  5780. const remove = user
  5781. ? ui.createButton("删除", () => {
  5782. ui.confirm().then(() => {
  5783. this.remove(uid);
  5784.  
  5785. this.views.details._.hide();
  5786.  
  5787. callback("REMOVE");
  5788. });
  5789. })
  5790. : null;
  5791.  
  5792. const save = ui.createButton("保存", () => {
  5793. const checked = [...table.querySelectorAll("INPUT:checked")].map(
  5794. (input) => parseInt(input.value, 10)
  5795. );
  5796.  
  5797. const newTags = input.value
  5798. .split("|")
  5799. .filter((item) => item.length)
  5800. .map((item) => this.tagModule.add(item))
  5801. .filter((tag) => tag !== null)
  5802. .map((tag) => tag.id);
  5803.  
  5804. const tags = [...new Set([...checked, ...newTags])].sort();
  5805.  
  5806. if (user === null) {
  5807. const entity = this.add(uid, {
  5808. id: uid,
  5809. name,
  5810. tags,
  5811. filterMode: switchMode.innerText,
  5812. });
  5813.  
  5814. this.views.details._.hide();
  5815.  
  5816. callback("ADD", entity);
  5817. } else {
  5818. const entity = this.update(uid, {
  5819. name,
  5820. tags,
  5821. filterMode: switchMode.innerText,
  5822. });
  5823.  
  5824. this.views.details._.hide();
  5825.  
  5826. callback("UPDATE", entity);
  5827. }
  5828. });
  5829.  
  5830. return ui.createButtonGroup(remove, save);
  5831. })(),
  5832. {
  5833. className: "right_",
  5834. }
  5835. );
  5836.  
  5837. const actions = ui.createElement(
  5838. "DIV",
  5839. [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
  5840. {
  5841. style: "margin-top: 10px;",
  5842. }
  5843. );
  5844.  
  5845. const tips = ui.createElement("DIV", TIPS.filterMode, {
  5846. className: "silver",
  5847. style: "margin-top: 10px;",
  5848. });
  5849.  
  5850. const content = ui.createElement(
  5851. "DIV",
  5852. [table, inputWrapper, actions, tips],
  5853. {
  5854. style: "width: 80vw",
  5855. }
  5856. );
  5857.  
  5858. // 创建弹出框
  5859. this.views.details = ui.createDialog(null, title, content);
  5860. }
  5861. }
  5862.  
  5863. /**
  5864. * 处理 topicArg 模块
  5865. * @param {Filter} filter 过滤器
  5866. * @param {*} value commonui.topicArg
  5867. */
  5868. const handleTopicModule = async (filter, value) => {
  5869. // 绑定主题模块
  5870. topicModule = value;
  5871.  
  5872. // 是否启用前置过滤
  5873. const preFilterEnabled = await filter.settings.preFilterEnabled;
  5874.  
  5875. // 前置过滤
  5876. // 先直接隐藏,等过滤完毕后再放出来
  5877. const beforeGet = (...args) => {
  5878. if (preFilterEnabled) {
  5879. // 主题标题
  5880. const title = document.getElementById(args[1]);
  5881.  
  5882. // 主题容器
  5883. const container = title.closest("tr");
  5884.  
  5885. // 隐藏元素
  5886. container.style.display = "none";
  5887. }
  5888.  
  5889. return args;
  5890. };
  5891.  
  5892. // 过滤
  5893. const afterGet = (_, args) => {
  5894. // 主题 ID
  5895. const tid = args[8];
  5896.  
  5897. // 回复 ID
  5898. const pid = args[9];
  5899.  
  5900. // 找到对应数据
  5901. const data = topicModule.data.find(
  5902. (item) => item[8] === tid && item[9] === pid
  5903. );
  5904.  
  5905. // 开始过滤
  5906. if (data) {
  5907. filter.filterTopic(data);
  5908. }
  5909. };
  5910.  
  5911. // 如果已经有数据,则直接过滤
  5912. Object.values(topicModule.data).forEach((item) => {
  5913. filter.filterTopic(item);
  5914. });
  5915.  
  5916. // 拦截 add 函数,这是泥潭的主题添加事件
  5917. Tools.interceptProperty(topicModule, "add", {
  5918. beforeGet,
  5919. afterGet,
  5920. });
  5921. };
  5922.  
  5923. /**
  5924. * 处理 postArg 模块
  5925. * @param {Filter} filter 过滤器
  5926. * @param {*} value commonui.postArg
  5927. */
  5928. const handleReplyModule = async (filter, value) => {
  5929. // 绑定回复模块
  5930. replyModule = value;
  5931.  
  5932. // 是否启用前置过滤
  5933. const preFilterEnabled = await filter.settings.preFilterEnabled;
  5934.  
  5935. // 前置过滤
  5936. // 先直接隐藏,等过滤完毕后再放出来
  5937. const beforeGet = (...args) => {
  5938. if (preFilterEnabled) {
  5939. // 楼层号
  5940. const index = args[0];
  5941.  
  5942. // 判断是否是楼层
  5943. const isFloor = typeof index === "number";
  5944.  
  5945. // 评论额外标签
  5946. const prefix = isFloor ? "" : "comment";
  5947.  
  5948. // 用户容器
  5949. const uInfoC = document.querySelector(`#${prefix}posterinfo${index}`);
  5950.  
  5951. // 回复容器
  5952. const container = isFloor
  5953. ? uInfoC.closest("tr")
  5954. : uInfoC.closest(".comment_c");
  5955.  
  5956. // 隐藏元素
  5957. container.style.display = "none";
  5958. }
  5959.  
  5960. return args;
  5961. };
  5962.  
  5963. // 过滤
  5964. const afterGet = (_, args) => {
  5965. // 楼层号
  5966. const index = args[0];
  5967.  
  5968. // 找到对应数据
  5969. const data = replyModule.data[index];
  5970.  
  5971. // 开始过滤
  5972. if (data) {
  5973. filter.filterReply(data);
  5974. }
  5975. };
  5976.  
  5977. // 如果已经有数据,则直接过滤
  5978. Object.values(replyModule.data).forEach((item) => {
  5979. filter.filterReply(item);
  5980. });
  5981.  
  5982. // 拦截 proc 函数,这是泥潭的回复添加事件
  5983. Tools.interceptProperty(replyModule, "proc", {
  5984. beforeGet,
  5985. afterGet,
  5986. });
  5987. };
  5988.  
  5989. /**
  5990. * 处理 commonui 模块
  5991. * @param {Filter} filter 过滤器
  5992. * @param {*} value commonui
  5993. */
  5994. const handleCommonui = (filter, value) => {
  5995. // 绑定主模块
  5996. commonui = value;
  5997.  
  5998. // 拦截 mainMenu 模块,UI 需要在 init 后加载
  5999. Tools.interceptProperty(commonui, "mainMenu", {
  6000. afterSet: (value) => {
  6001. Tools.interceptProperty(value, "init", {
  6002. afterGet: () => {
  6003. filter.ui.render();
  6004. },
  6005. afterSet: () => {
  6006. filter.ui.render();
  6007. },
  6008. });
  6009. },
  6010. });
  6011.  
  6012. // 拦截 topicArg 模块,这是泥潭的主题入口
  6013. Tools.interceptProperty(commonui, "topicArg", {
  6014. afterSet: (value) => {
  6015. handleTopicModule(filter, value);
  6016. },
  6017. });
  6018.  
  6019. // 拦截 postArg 模块,这是泥潭的回复入口
  6020. Tools.interceptProperty(commonui, "postArg", {
  6021. afterSet: (value) => {
  6022. handleReplyModule(filter, value);
  6023. },
  6024. });
  6025. };
  6026.  
  6027. /**
  6028. * 注册(不可用)脚本菜单
  6029. * @param {Settings} settings 设置
  6030. */
  6031. const registerMenu = async (settings) => {
  6032. const enabled = await settings.preFilterEnabled;
  6033.  
  6034. GM_registerMenuCommand(`前置过滤:${enabled ? "是" : "否"}`, () => {
  6035. settings.preFilterEnabled = !enabled;
  6036. });
  6037. };
  6038.  
  6039. // 主函数
  6040. (async () => {
  6041. // 初始化缓存和 API
  6042. const { cache, api } = initCacheAndAPI();
  6043.  
  6044. // 初始化设置
  6045. const settings = new Settings(cache);
  6046.  
  6047. // 读取设置
  6048. await settings.load();
  6049.  
  6050. // 初始化 UI
  6051. const ui = new UI(settings, api);
  6052.  
  6053. // 初始化过滤器
  6054. const filter = new Filter(settings, api, ui);
  6055.  
  6056. // 加载模块
  6057. filter.initModules(
  6058. SettingsModule,
  6059. ListEnhancedModule,
  6060. UserEnhancedModule,
  6061. TagModule,
  6062. KeywordModule,
  6063. LocationModule,
  6064. ForumOrSubsetModule,
  6065. HunterModule,
  6066. MiscModule
  6067. );
  6068.  
  6069. // 注册(不可用)脚本菜单
  6070. registerMenu(settings);
  6071.  
  6072. // 处理 commonui 模块
  6073. if (unsafeWindow.commonui) {
  6074. handleCommonui(filter, unsafeWindow.commonui);
  6075. return;
  6076. }
  6077.  
  6078. Tools.interceptProperty(unsafeWindow, "commonui", {
  6079. afterSet: (value) => {
  6080. handleCommonui(filter, value);
  6081. },
  6082. });
  6083. })();
  6084. })();

QingJ © 2025

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