NGA Filter

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

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==

// @name              NGA Filter
// @name:zh-CN        NGA 屏蔽插件

// @description       NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名、提醒过滤。troll must die。
// @description:zh-CN NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名、提醒过滤。troll must die。

// @namespace         https://greasyfork.org/users/263018
// @version           2.6.1
// @author            snyssss
// @license           MIT

// @match             *://bbs.nga.cn/*
// @match             *://ngabbs.com/*
// @match             *://nga.178.com/*

// @require           https://update.greasyfork.org/scripts/486070/1416483/NGA%20Library.js

// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_registerMenuCommand
// @grant             unsafeWindow

// @run-at            document-start
// @noframes

// ==/UserScript==

(() => {
  // 声明泥潭主模块、主题模块、回复模块、提醒模块
  let commonui, topicModule, replyModule, notificationModule;

  // KEY
  const DATA_KEY = "NGAFilter";
  const PRE_FILTER_KEY = "PRE_FILTER_KEY";
  const NOTIFICATION_FILTER_KEY = "NOTIFICATION_FILTER_KEY";

  // TIPS
  const TIPS = {
    filterMode:
      "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:显示 &gt; 隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承",
    addTags: `一次性添加多个标记用"|"隔开,不会添加重名标记`,
    keyword: `支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。`,
    forumOrSubset:
      "输入版面或合集的完整链接,如:<br/>https://bbs.nga.cn/thread.php?fid=xxx<br/>https://bbs.nga.cn/thread.php?stid=xxx",
    hunter: "猎巫模块需要占用额外的资源,请谨慎开启",
    filterNotification: "目前支持过滤回复提醒,暂不支持过滤短消息。",
    filterTopicPerPage:
      "泥潭每页帖子数量约为30条,本功能即为检测连续的30条帖子内相同用户的发帖数量",
    error: "目前泥潭对查询接口增加了限制,功能基本失效",
  };

  // 主题
  const THEMES = {
    system: {
      name: "系统",
    },
    classic: {
      name: "经典",
      fontColor: "crimson",
      borderColor: "#66BAB7",
      backgroundColor: "#81C7D4",
    },
  };

  /**
   * 设置
   *
   * 暂时整体处理模块设置,后续再拆分
   */
  class Settings {
    /**
     * 缓存管理
     */
    cache;

    /**
     * 当前设置
     */
    data = null;

    /**
     * 初始化并绑定缓存管理
     * @param {Cache} cache 缓存管理
     */
    constructor(cache) {
      this.cache = cache;
    }

    /**
     * 读取设置
     */
    async load() {
      // 读取设置
      if (this.data === null) {
        // 默认配置
        const defaultData = {
          tags: {},
          users: {},
          keywords: {},
          locations: {},
          forumOrSubsets: {},
          options: {
            theme: "system",
            filterRegdateLimit: 0,
            filterPostnumLimit: 0,
            filterTopicRateLimit: 100,
            filterTopicPerPageLimit: NaN,
            filterReputationLimit: NaN,
            filterAnony: false,
            filterThumb: false,
            filterMode: "隐藏",
          },
        };

        // 读取数据
        const storedData = await this.cache
          .get(DATA_KEY)
          .then((values) => values || {});

        // 写入缓存
        this.data = Tools.merge({}, defaultData, storedData);

        // 写入默认模块选项
        if (Object.hasOwn(this.data, "modules") === false) {
          this.data.modules = ["user", "tag", "misc"];

          if (Object.keys(this.data.keywords).length > 0) {
            this.data.modules.push("keyword");
          }

          if (Object.keys(this.data.locations).length > 0) {
            this.data.modules.push("location");
          }
        }
      }

      // 返回设置
      return this.data;
    }

    /**
     * 写入设置
     */
    async save() {
      return this.cache.put(DATA_KEY, this.data);
    }

    /**
     * 获取模块列表
     */
    get modules() {
      return this.data.modules;
    }

    /**
     * 设置模块列表
     */
    set modules(values) {
      this.data.modules = values;
      this.save();
    }

    /**
     * 获取标签列表
     */
    get tags() {
      return this.data.tags;
    }

    /**
     * 设置标签列表
     */
    set tags(values) {
      this.data.tags = values;
      this.save();
    }

    /**
     * 获取用户列表
     */
    get users() {
      return this.data.users;
    }

    /**
     * 设置用户列表
     */
    set users(values) {
      this.data.users = values;
      this.save();
    }

    /**
     * 获取关键字列表
     */
    get keywords() {
      return this.data.keywords;
    }

    /**
     * 设置关键字列表
     */
    set keywords(values) {
      this.data.keywords = values;
      this.save();
    }

    /**
     * 获取属地列表
     */
    get locations() {
      return this.data.locations;
    }

    /**
     * 设置属地列表
     */
    set locations(values) {
      this.data.locations = values;
      this.save();
    }

    /**
     * 获取版面或合集列表
     */
    get forumOrSubsets() {
      return this.data.forumOrSubsets;
    }

    /**
     * 设置版面或合集列表
     */
    set forumOrSubsets(values) {
      this.data.forumOrSubsets = values;
      this.save();
    }

    /**
     * 获取默认过滤模式
     */
    get defaultFilterMode() {
      return this.data.options.filterMode;
    }

    /**
     * 设置默认过滤模式
     */
    set defaultFilterMode(value) {
      this.data.options.filterMode = value;
      this.save();
    }

    /**
     * 获取主题
     */
    get theme() {
      return this.data.options.theme;
    }

    /**
     * 设置主题
     */
    set theme(value) {
      this.data.options.theme = value;
      this.save();
    }

    /**
     * 获取注册时间限制
     */
    get filterRegdateLimit() {
      return this.data.options.filterRegdateLimit || 0;
    }

    /**
     * 设置注册时间限制
     */
    set filterRegdateLimit(value) {
      this.data.options.filterRegdateLimit = value;
      this.save();
    }

    /**
     * 获取发帖数量限制
     */
    get filterPostnumLimit() {
      return this.data.options.filterPostnumLimit || 0;
    }

    /**
     * 设置发帖数量限制
     */
    set filterPostnumLimit(value) {
      this.data.options.filterPostnumLimit = value;
      this.save();
    }

    /**
     * 获取发帖比例限制
     */
    get filterTopicRateLimit() {
      return this.data.options.filterTopicRateLimit || 100;
    }

    /**
     * 设置发帖比例限制
     */
    set filterTopicRateLimit(value) {
      this.data.options.filterTopicRateLimit = value;
      this.save();
    }

    /**
     * 获取每页主题数量限制
     */
    get filterTopicPerPageLimit() {
      return this.data.options.filterTopicPerPageLimit || NaN;
    }

    /**
     * 设置每页主题数量限制
     */
    set filterTopicPerPageLimit(value) {
      this.data.options.filterTopicPerPageLimit = value;
      this.save();
    }

    /**
     * 获取版面声望限制
     */
    get filterReputationLimit() {
      return this.data.options.filterReputationLimit || NaN;
    }

    /**
     * 设置版面声望限制
     */
    set filterReputationLimit(value) {
      this.data.options.filterReputationLimit = value;
      this.save();
    }

    /**
     * 获取是否过滤匿名
     */
    get filterAnonymous() {
      return this.data.options.filterAnony || false;
    }

    /**
     * 设置是否过滤匿名
     */
    set filterAnonymous(value) {
      this.data.options.filterAnony = value;
      this.save();
    }

    /**
     * 获取是否过滤缩略图
     */
    get filterThumbnail() {
      return this.data.options.filterThumb || false;
    }

    /**
     * 设置是否过滤缩略图
     */
    set filterThumbnail(value) {
      this.data.options.filterThumb = value;
      this.save();
    }

    /**
     * 获取是否启用前置过滤
     */
    get preFilterEnabled() {
      return this.cache.get(PRE_FILTER_KEY).then((value) => {
        if (value === undefined) {
          return true;
        }

        return value;
      });
    }

    /**
     * 设置是否启用前置过滤
     */
    set preFilterEnabled(value) {
      this.cache.put(PRE_FILTER_KEY, value).then(() => {
        location.reload();
      });
    }

    /**
     * 获取是否启用提醒过滤
     */
    get notificationFilterEnabled() {
      return this.cache.get(NOTIFICATION_FILTER_KEY).then((value) => {
        if (value === undefined) {
          return false;
        }

        return value;
      });
    }

    /**
     * 设置是否启用提醒过滤
     */
    set notificationFilterEnabled(value) {
      this.cache.put(NOTIFICATION_FILTER_KEY, value).then(() => {
        location.reload();
      });
    }

    /**
     * 获取过滤模式列表
     *
     * 模拟成从配置中获取
     */
    get filterModes() {
      return ["继承", "标记", "遮罩", "隐藏", "显示"];
    }

    /**
     * 获取指定下标过滤模式
     * @param {Number} index 下标
     */
    getNameByMode(index) {
      const modes = this.filterModes;

      return modes[index] || "";
    }

    /**
     * 获取指定过滤模式下标
     * @param {String} name 过滤模式
     */
    getModeByName(name) {
      const modes = this.filterModes;

      return modes.indexOf(name);
    }

    /**
     * 切换过滤模式
     * @param   {String} value 过滤模式
     * @returns {String}       过滤模式
     */
    switchModeByName(value) {
      const index = this.getModeByName(value);

      const nextIndex = (index + 1) % this.filterModes.length;

      return this.filterModes[nextIndex];
    }
  }

  /**
   * UI
   */
  class UI {
    /**
     * 标签
     */
    static label = "屏蔽";

    /**
     * 设置
     */
    settings;

    /**
     * API
     */
    api;

    /**
     * 模块列表
     */
    modules = {};

    /**
     * 菜单元素
     */
    menu = null;

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化并绑定设置、API,注册脚本菜单
     * @param {Settings} settings 设置
     * @param {API}      api      API
     */
    constructor(settings, api) {
      this.settings = settings;
      this.api = api;

      this.init();
    }

    /**
     * 初始化,创建基础视图,初始化通用设置
     */
    init() {
      const tabs = this.createTabs({
        className: "right_",
      });

      const content = this.createElement("DIV", [], {
        style: "width: 80vw;",
      });

      const container = this.createElement("DIV", [tabs, content]);

      this.views = {
        tabs,
        content,
        container,
      };

      this.initSettings();
    }

    /**
     * 初始化设置
     */
    initSettings() {
      // 创建基础视图
      const settings = this.createElement("DIV", []);

      // 添加设置项
      const add = (order, ...elements) => {
        const items = [...settings.childNodes];

        if (items.find((item) => item.order === order)) {
          return;
        }

        const item = this.createElement(
          "DIV",
          [...elements, this.createElement("BR", [])],
          {
            order,
          }
        );

        const anchor = items.find((item) => item.order > order);

        settings.insertBefore(item, anchor || null);

        return item;
      };

      // 绑定事件
      Object.assign(settings, {
        add,
      });

      // 合并视图
      Object.assign(this.views, {
        settings,
      });

      // 创建标签页
      const { tabs, content } = this.views;

      this.createTab(tabs, "设置", Number.MAX_SAFE_INTEGER, {
        onclick: () => {
          content.innerHTML = "";
          content.appendChild(settings);
        },
      });
    }

    /**
     * 弹窗确认
     * @param   {String}  message 提示信息
     * @returns {Promise}
     */
    confirm(message = "是否确认?") {
      return new Promise((resolve, reject) => {
        const result = confirm(message);

        if (result) {
          resolve();
          return;
        }

        reject();
      });
    }

    /**
     * 折叠
     * @param {String | Number} key     标识
     * @param {HTMLElement}     element 目标元素
     * @param {String}          content 内容
     */
    collapse(key, element, content) {
      key = "collapsed_" + key;

      element.innerHTML = `
        <div class="lessernuke filter-mask-collapse" onclick="[...document.getElementsByName('${key}')].forEach(item => item.style.display = '')">
          <span class="filter-mask-hint">Troll must die.</span>
          <div style="display: none;" name="${key}">
            ${content}
          </div>
        </div>`;
    }

    /**
     * 创建元素
     * @param   {String}                               tagName    标签
     * @param   {HTMLElement | HTMLElement[] | String} content    内容,元素或者 innerHTML
     * @param   {*}                                    properties 额外属性
     * @returns {HTMLElement}                                     元素
     */
    createElement(tagName, content, properties = {}) {
      const element = document.createElement(tagName);

      // 写入内容
      if (typeof content === "string") {
        element.innerHTML = content;
      } else {
        if (Array.isArray(content) === false) {
          content = [content];
        }

        content.forEach((item) => {
          if (item === null) {
            return;
          }

          if (typeof item === "string") {
            element.append(item);
            return;
          }

          element.appendChild(item);
        });
      }

      // 对 A 标签的额外处理
      if (tagName.toUpperCase() === "A") {
        if (Object.hasOwn(properties, "href") === false) {
          properties.href = "javascript: void(0);";
        }
      }

      // 附加属性
      Object.entries(properties).forEach(([key, value]) => {
        element[key] = value;
      });

      return element;
    }

    /**
     * 创建按钮
     * @param {String}   text       文字
     * @param {Function} onclick    点击事件
     * @param {*}        properties 额外属性
     */
    createButton(text, onclick, properties = {}) {
      return this.createElement("BUTTON", text, {
        ...properties,
        onclick,
      });
    }

    /**
     * 创建按钮组
     * @param {Array} buttons 按钮集合
     */
    createButtonGroup(...buttons) {
      return this.createElement("DIV", buttons, {
        className: "filter-button-group",
      });
    }

    /**
     * 创建表格
     * @param   {Array}       headers    表头集合
     * @param   {*}           properties 额外属性
     * @returns {HTMLElement}            元素和相关函数
     */
    createTable(headers, properties = {}) {
      const rows = [];

      const ths = headers.map((item, index) =>
        this.createElement("TH", item.label, {
          ...item,
          className: `c${index + 1}`,
        })
      );

      const tr =
        ths.length > 0
          ? this.createElement("TR", ths, {
              className: "block_txt_c0",
            })
          : null;

      const thead = tr !== null ? this.createElement("THEAD", tr) : null;

      const tbody = this.createElement("TBODY", []);

      const table = this.createElement("TABLE", [thead, tbody], {
        ...properties,
        className: "filter-table forumbox",
      });

      const wrapper = this.createElement("DIV", table, {
        className: "filter-table-wrapper",
      });

      const intersectionObserver = new IntersectionObserver((entries) => {
        if (entries[0].intersectionRatio <= 0) return;

        const list = rows.splice(0, 10);

        if (list.length === 0) {
          return;
        }

        intersectionObserver.disconnect();

        tbody.append(...list);

        intersectionObserver.observe(tbody.lastElementChild);
      });

      const add = (...columns) => {
        const tds = columns.map((column, index) => {
          if (ths[index]) {
            const { center, ellipsis } = ths[index];

            const properties = {};

            if (center) {
              properties.style = "text-align: center;";
            }

            if (ellipsis) {
              properties.className = "filter-text-ellipsis";
            }

            column = this.createElement("DIV", column, properties);
          }

          return this.createElement("TD", column, {
            className: `c${index + 1}`,
          });
        });

        const tr = this.createElement("TR", tds, {
          className: `row${(rows.length % 2) + 1}`,
        });

        intersectionObserver.disconnect();

        rows.push(tr);

        intersectionObserver.observe(tbody.lastElementChild || tbody);
      };

      const update = (e, ...columns) => {
        const row = e.target.closest("TR");

        if (row) {
          const tds = row.querySelectorAll("TD");

          columns.map((column, index) => {
            if (ths[index]) {
              const { center, ellipsis } = ths[index];

              const properties = {};

              if (center) {
                properties.style = "text-align: center;";
              }

              if (ellipsis) {
                properties.className = "filter-text-ellipsis";
              }

              column = this.createElement("DIV", column, properties);
            }

            if (tds[index]) {
              tds[index].innerHTML = "";
              tds[index].append(column);
            }
          });
        }
      };

      const remove = (e) => {
        const row = e.target.closest("TR");

        if (row) {
          tbody.removeChild(row);
        }
      };

      const clear = () => {
        rows.splice(0);
        intersectionObserver.disconnect();

        tbody.innerHTML = "";
      };

      Object.assign(wrapper, {
        add,
        update,
        remove,
        clear,
      });

      return wrapper;
    }

    /**
     * 创建标签组
     * @param {*} properties 额外属性
     */
    createTabs(properties = {}) {
      const tabs = this.createElement(
        "DIV",
        `<table class="stdbtn" cellspacing="0">
          <tbody>
            <tr></tr>
          </tbody>
        </table>`,
        properties
      );

      return this.createElement(
        "DIV",
        [
          tabs,
          this.createElement("DIV", [], {
            className: "clear",
          }),
        ],
        {
          style: "display: none; margin-bottom: 5px;",
        }
      );
    }

    /**
     * 创建标签
     * @param {Element} tabs       标签组
     * @param {String}  label      标签名称
     * @param {Number}  order      标签顺序,重复则跳过
     * @param {*}       properties 额外属性
     */
    createTab(tabs, label, order, properties = {}) {
      const group = tabs.querySelector("TR");

      const items = [...group.childNodes];

      if (items.find((item) => item.order === order)) {
        return;
      }

      if (items.length > 0) {
        tabs.style.removeProperty("display");
      }

      const tab = this.createElement("A", label, {
        ...properties,
        className: "nobr silver",
        onclick: () => {
          if (tab.className === "nobr") {
            return;
          }

          group.querySelectorAll("A").forEach((item) => {
            if (item === tab) {
              item.className = "nobr";
            } else {
              item.className = "nobr silver";
            }
          });

          if (properties.onclick) {
            properties.onclick();
          }
        },
      });

      const wrapper = this.createElement("TD", tab, {
        order,
      });

      const anchor = items.find((item) => item.order > order);

      group.insertBefore(wrapper, anchor || null);

      return wrapper;
    }

    /**
     * 创建对话框
     * @param {HTMLElement | null} anchor  要绑定的元素,如果为空,直接弹出
     * @param {String}             title   对话框的标题
     * @param {HTMLElement}        content 对话框的内容
     */
    createDialog(anchor, title, content) {
      let window;

      const show = () => {
        if (window === undefined) {
          window = commonui.createCommmonWindow();
        }

        window._.addContent(null);
        window._.addTitle(title);
        window._.addContent(content);
        window._.show();
      };

      if (anchor) {
        anchor.onclick = show;
      } else {
        show();
      }

      return window;
    }

    /**
     * 渲染菜单
     */
    renderMenu() {
      // 菜单尚未加载完成
      if (
        commonui.mainMenu === undefined ||
        commonui.mainMenu.dataReady === null
      ) {
        return;
      }

      // 定位右侧菜单容器
      const right = document.querySelector("#mainmenu .right");

      if (right === null) {
        return;
      }

      // 定位搜索框,如果有搜索框,放在搜索框左侧,没有放在最后
      const searchInput = right.querySelector("#menusearchinput");

      // 初始化菜单并绑定
      if (this.menu === null) {
        const menu = this.createElement("A", this.constructor.label, {
          className: "mmdefault nobr",
        });

        this.menu = menu;
      }

      // 插入菜单
      const container = this.createElement("DIV", this.menu, {
        className: "td",
      });

      if (searchInput) {
        searchInput.closest("DIV").before(container);
      } else {
        right.appendChild(container);
      }
    }

    /**
     * 渲染视图
     */
    renderView() {
      // 如果菜单还没有渲染,说明模块尚未加载完毕,跳过
      if (this.menu === null) {
        return;
      }

      // 绑定菜单点击事件.
      this.createDialog(
        this.menu,
        this.constructor.label,
        this.views.container
      );

      // 启用第一个模块
      this.views.tabs.querySelector("A").click();
    }

    /**
     * 渲染
     */
    render() {
      this.renderMenu();
      this.renderView();
    }
  }

  /**
   * 基础模块
   */
  class Module {
    /**
     * 模块名称
     */
    static name;

    /**
     * 模块标签
     */
    static label;

    /**
     * 顺序
     */
    static order;

    /**
     * 依赖模块
     */
    static depends = [];

    /**
     * 附加模块
     */
    static addons = [];

    /**
     * 设置
     */
    settings;

    /**
     * API
     */
    api;

    /**
     * UI
     */
    ui;

    /**
     * 过滤列表
     */
    data = [];

    /**
     * 依赖模块
     */
    depends = {};

    /**
     * 附加模块
     */
    addons = {};

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化并绑定设置、API、UI、过滤列表,注册 UI
     * @param {Settings} settings 设置
     * @param {API}      api      API
     * @param {UI}       ui       UI
     */
    constructor(settings, api, ui, data) {
      this.settings = settings;
      this.api = api;
      this.ui = ui;

      this.data = data;

      this.init();
    }

    /**
     * 创建实例
     * @param   {Settings}      settings 设置
     * @param   {API}           api      API
     * @param   {UI}            ui       UI
     * @param   {Array}         data     过滤列表
     * @returns {Module | null}          成功后返回模块实例
     */
    static create(settings, api, ui, data) {
      // 读取设置里的模块列表
      const modules = settings.modules;

      // 如果不包含自己或依赖的模块,则返回空
      const index = [this, ...this.depends].findIndex(
        (module) => modules.includes(module.name) === false
      );

      if (index >= 0) {
        return null;
      }

      // 创建实例
      const instance = new this(settings, api, ui, data);

      // 返回实例
      return instance;
    }

    /**
     * 判断指定附加模块是否启用
     * @param {typeof Module} module 模块
     */
    hasAddon(module) {
      return Object.hasOwn(this.addons, module.name);
    }

    /**
     * 初始化,创建基础视图和组件
     */
    init() {
      if (this.views.container) {
        this.destroy();
      }

      const { ui } = this;

      const container = ui.createElement("DIV", []);

      this.views = {
        container,
      };

      this.initComponents();
    }

    /**
     * 初始化组件
     */
    initComponents() {}

    /**
     * 销毁
     */
    destroy() {
      Object.values(this.views).forEach((view) => {
        if (view.parentNode) {
          view.parentNode.removeChild(view);
        }
      });

      this.views = {};
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      container.innerHTML = "";
      container.appendChild(this.views.container);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {}

    /**
     * 通知
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async notify(item, result) {}
  }

  /**
   * 过滤器
   */
  class Filter {
    /**
     * 设置
     */
    settings;

    /**
     * API
     */
    api;

    /**
     * UI
     */
    ui;

    /**
     * 过滤列表
     */
    data = [];

    /**
     * 模块列表
     */
    modules = {};

    /**
     * 初始化并绑定设置、API、UI
     * @param {Settings} settings 设置
     * @param {API}      api      API
     * @param {UI}       ui       UI
     */
    constructor(settings, api, ui) {
      this.settings = settings;
      this.api = api;
      this.ui = ui;
    }

    /**
     * 绑定两个模块的互相关系
     * @param {Module} moduleA 模块A
     * @param {Module} moduleB 模块B
     */
    bindModule(moduleA, moduleB) {
      const nameA = moduleA.constructor.name;
      const nameB = moduleB.constructor.name;

      // A 依赖 B
      if (moduleA.constructor.depends.findIndex((i) => i.name === nameB) >= 0) {
        moduleA.depends[nameB] = moduleB;
        moduleA.init();
      }

      // B 依赖 A
      if (moduleB.constructor.depends.findIndex((i) => i.name === nameA) >= 0) {
        moduleB.depends[nameA] = moduleA;
        moduleB.init();
      }

      // A 附加 B
      if (moduleA.constructor.addons.findIndex((i) => i.name === nameB) >= 0) {
        moduleA.addons[nameB] = moduleB;
        moduleA.init();
      }

      // B 附加 A
      if (moduleB.constructor.addons.findIndex((i) => i.name === nameA) >= 0) {
        moduleB.addons[nameA] = moduleA;
        moduleB.init();
      }
    }

    /**
     * 加载模块
     * @param {typeof Module} module 模块
     */
    initModule(module) {
      // 如果已经加载过则跳过
      if (Object.hasOwn(this.modules, module.name)) {
        return;
      }

      // 创建模块
      const instance = module.create(
        this.settings,
        this.api,
        this.ui,
        this.data
      );

      // 如果创建失败则跳过
      if (instance === null) {
        return;
      }

      // 绑定依赖模块和附加模块
      Object.values(this.modules).forEach((item) => {
        this.bindModule(item, instance);
      });

      // 合并模块
      this.modules[module.name] = instance;

      // 按照顺序重新整理模块
      this.modules = Tools.sortBy(
        Object.values(this.modules),
        (item) => item.constructor.order
      ).reduce(
        (result, item) => ({
          ...result,
          [item.constructor.name]: item,
        }),
        {}
      );
    }

    /**
     * 加载模块列表
     * @param {typeof Module[]} modules 模块列表
     */
    initModules(...modules) {
      // 根据依赖和附加模块决定初始化的顺序
      Tools.sortBy(
        modules,
        (item) => item.depends.length,
        (item) => item.addons.length
      ).forEach((module) => {
        this.initModule(module);
      });
    }

    /**
     * 添加到过滤列表
     * @param {*} item 绑定的 nFilter
     */
    pushData(item) {
      // 清除掉无效数据
      for (let i = 0; i < this.data.length; ) {
        if (document.body.contains(this.data[i].container) === false) {
          this.data.splice(i, 1);
          continue;
        }

        i += 1;
      }

      // 加入过滤列表
      if (this.data.includes(item) === false) {
        this.data.push(item);
      }
    }

    /**
     * 判断指定 UID 是否是自己
     * @param {Number} uid 用户 ID
     */
    isSelf(uid) {
      return unsafeWindow.__CURRENT_UID === uid;
    }

    /**
     * 获取过滤模式
     * @param {*} item 绑定的 nFilter
     */
    async getFilterMode(item) {
      // 获取链接参数
      const params = new URLSearchParams(location.search);

      // 跳过屏蔽(插件自定义)
      if (params.has("nofilter")) {
        return;
      }

      // 收藏
      if (params.has("favor")) {
        return;
      }

      // 只看某人
      if (params.has("authorid")) {
        return;
      }

      // 跳过自己
      if (this.isSelf(item.uid)) {
        return;
      }

      // 声明结果
      const result = {
        mode: -1,
        reason: ``,
      };

      // 根据模块依次过滤
      for (const module of Object.values(this.modules)) {
        await module.filter(item, result);
      }

      // 写入过滤模式和过滤原因
      item.filterMode = this.settings.getNameByMode(result.mode);
      item.reason = result.reason;

      // 通知各模块过滤结果
      for (const module of Object.values(this.modules)) {
        await module.notify(item, result);
      }

      // 继承模式下返回默认过滤模式
      if (item.filterMode === "继承") {
        return this.settings.defaultFilterMode;
      }

      // 返回结果
      return item.filterMode;
    }

    /**
     * 过滤主题
     * @param {*} item 主题内容,见 commonui.topicArg.data
     */
    filterTopic(item) {
      // 绑定事件
      if (item.nFilter === undefined) {
        // 主题 ID
        const tid = item[8];

        // 主题版面 ID
        const fid = item[7];

        // 主题标题
        const title = item[1];
        const subject = title.innerText;

        // 主题作者
        const author = item[2];
        const uid =
          parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) || 0;
        const username = author.innerText;

        // 增加操作角标
        const action = (() => {
          const anchor = item[2].parentNode;

          const element = this.ui.createElement("DIV", "", {
            style: Object.entries({
              position: "absolute",
              right: 0,
              bottom: 0,
              padding: "6px",
              "clip-path": "polygon(100% 0, 100% 100%, 0 100%)",
            })
              .map(([key, value]) => `${key}: ${value}`)
              .join(";"),
          });

          anchor.style.position = "relative";
          anchor.appendChild(element);

          return element;
        })();

        // 主题杂项
        const topicMisc = item[16];

        // 主题容器
        const container = title.closest("tr");

        // 过滤函数
        const execute = async () => {
          // 获取过滤模式
          const filterMode = await this.getFilterMode(item.nFilter);

          // 样式处理
          (() => {
            // 还原样式
            // TODO 应该整体采用 className 来实现
            (() => {
              // 标记模式
              title.style.removeProperty("textDecoration");

              // 遮罩模式
              title.classList.remove("filter-mask");
              author.classList.remove("filter-mask");
            })();

            // 样式处理
            (() => {
              // 标记模式下,主题标记会有删除线标识
              if (filterMode === "标记") {
                title.style.textDecoration = "line-through";
                return;
              }

              // 遮罩模式下,主题和作者会有遮罩样式
              if (filterMode === "遮罩") {
                title.classList.add("filter-mask");
                author.classList.add("filter-mask");
                return;
              }

              // 隐藏模式下,容器会被隐藏
              if (filterMode === "隐藏") {
                container.style.display = "none";
                return;
              }
            })();

            // 非隐藏模式下,恢复显示
            if (filterMode !== "隐藏") {
              container.style.removeProperty("display");
            }
          })();
        };

        // 绑定事件
        item.nFilter = {
          tid,
          pid: 0,
          uid,
          fid,
          username,
          container,
          title,
          author,
          subject,
          topicMisc,
          action,
          tags: null,
          execute,
        };

        // 添加至列表
        this.pushData(item.nFilter);
      }

      // 开始过滤
      item.nFilter.execute();
    }

    /**
     * 过滤回复
     * @param {*} item 回复内容,见 commonui.postArg.data
     */
    filterReply(item) {
      // 跳过泥潭增加的额外内容
      if (Tools.getType(item) !== "object") {
        return;
      }

      // 绑定事件
      if (item.nFilter === undefined) {
        // 主题 ID
        const tid = item.tid;

        // 回复 ID
        const pid = item.pid;

        // 判断是否是楼层
        const isFloor = typeof item.i === "number";

        // 回复容器
        const container = isFloor
          ? item.uInfoC.closest("tr")
          : item.uInfoC.closest(".comment_c");

        // 回复标题
        const title = item.subjectC;
        const subject = title.innerText;

        // 回复内容
        const content = item.contentC;
        const contentBak = content.innerHTML;

        // 回复作者
        const author =
          container.querySelector(".posterInfoLine") || item.uInfoC;
        const uid = parseInt(item.pAid, 10) || 0;
        const username = author.querySelector(".author").innerText;
        const avatar = author.querySelector(".avatar");

        // 找到用户 ID,将其视为操作按钮
        const action = container.querySelector('[name="uid"]');

        // 创建一个元素,用于展示标记列表
        // 贴条和高赞不显示
        const tags = (() => {
          if (isFloor === false) {
            return null;
          }

          const element = document.createElement("div");

          element.className = "filter-tags";

          author.appendChild(element);

          return element;
        })();

        // 过滤函数
        const execute = async () => {
          // 获取过滤模式
          const filterMode = await this.getFilterMode(item.nFilter);

          // 样式处理
          (() => {
            // 还原样式
            // TODO 应该整体采用 className 来实现
            (() => {
              // 标记模式
              if (avatar) {
                avatar.style.removeProperty("display");
              }

              content.innerHTML = contentBak;

              // 遮罩模式
              const caption = container.parentNode.querySelector("CAPTION");

              if (caption) {
                container.parentNode.removeChild(caption);
                container.style.removeProperty("display");
              }
            })();

            // 样式处理
            (() => {
              // 标记模式下,隐藏头像,采用泥潭的折叠样式
              if (filterMode === "标记") {
                if (avatar) {
                  avatar.style.display = "none";
                }

                this.ui.collapse(uid, content, contentBak);
                return;
              }

              // 遮罩模式下,楼层会有遮罩样式
              if (filterMode === "遮罩") {
                const caption = document.createElement("CAPTION");

                if (isFloor) {
                  caption.className = "filter-mask filter-mask-block";
                } else {
                  caption.className = "filter-mask filter-mask-block left";
                  caption.style.width = "47%";
                }

                caption.style.textAlign = "center";
                caption.innerHTML = `<span class="filter-mask-hint">Troll must die.</span>`;
                caption.onclick = () => {
                  const caption = container.parentNode.querySelector("CAPTION");

                  if (caption) {
                    container.parentNode.removeChild(caption);
                    container.style.removeProperty("display");
                  }
                };

                container.parentNode.insertBefore(caption, container);
                container.style.display = "none";
                return;
              }

              // 隐藏模式下,容器会被隐藏
              if (filterMode === "隐藏") {
                container.style.display = "none";
                return;
              }
            })();

            // 非隐藏模式下,恢复显示
            // 楼层的遮罩模式下仍需隐藏
            if (["遮罩", "隐藏"].includes(filterMode) === false) {
              container.style.removeProperty("display");
            }
          })();

          // 过滤引用
          this.filterQuote(item);
        };

        // 绑定事件
        item.nFilter = {
          tid,
          pid,
          uid,
          fid: null,
          username,
          container,
          title,
          author,
          subject,
          content: content.innerText,
          topicMisc: "",
          action,
          tags,
          execute,
        };

        // 添加至列表
        this.pushData(item.nFilter);
      }

      // 开始过滤
      item.nFilter.execute();
    }

    /**
     * 过滤引用
     * @param {*} item 回复内容,见 commonui.postArg.data
     */
    filterQuote(item) {
      // 未绑定事件,直接跳过
      if (item.nFilter === undefined) {
        return;
      }

      // 回复内容
      const content = item.contentC;

      // 找到所有引用
      const quotes = content.querySelectorAll(".quote");

      // 处理引用
      [...quotes].map(async (quote) => {
        const { uid, username } = (() => {
          const ele = quote.querySelector("A[href^='/nuke.php']");

          if (ele) {
            const res = ele.getAttribute("href").match(/uid=(\S+)/);

            if (res) {
              return {
                uid: parseInt(res[1], 10),
                username: ele.innerText.substring(1, ele.innerText.length - 1),
              };
            }
          }

          return {};
        })();

        const { tid, pid } = (() => {
          const ele = quote.querySelector("[title='快速浏览这个帖子']");

          if (ele) {
            const res = ele
              .getAttribute("onclick")
              .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);

            if (res) {
              return {
                tid: parseInt(res[2], 10),
                pid: parseInt(res[3], 10) || 0,
              };
            }
          }

          return {};
        })();

        // 临时的 nFilter
        const nFilter = {
          tid,
          pid,
          uid,
          fid: null,
          username,
          subject: "",
          content: quote.innerText,
          topicMisc: "",
          action: null,
          tags: null,
        };

        // 获取过滤模式
        const filterMode = await this.getFilterMode(nFilter);

        (() => {
          if (filterMode === "标记") {
            this.ui.collapse(uid, quote, quote.innerHTML);
            return;
          }

          if (filterMode === "遮罩") {
            const source = document.createElement("DIV");

            source.innerHTML = quote.innerHTML;
            source.style.display = "none";

            const caption = document.createElement("CAPTION");

            caption.className = "filter-mask filter-mask-block";

            caption.style.textAlign = "center";
            caption.innerHTML = `<span class="filter-mask-hint">Troll must die.</span>`;
            caption.onclick = () => {
              quote.removeChild(caption);

              source.style.display = "";
            };

            quote.innerHTML = "";
            quote.appendChild(source);
            quote.appendChild(caption);
            return;
          }

          if (filterMode === "隐藏") {
            quote.innerHTML = "";
            return;
          }
        })();

        // 绑定引用
        item.nFilter.quotes = item.nFilter.quotes || {};
        item.nFilter.quotes[uid] = nFilter.filterMode;
      });
    }

    /**
     * 过滤提醒
     * @param {*} container 提醒容器
     * @param {*} item      提醒内容
     */
    async filterNotification(container, item) {
      // 临时的 nFilter
      const nFilter = {
        ...item,
        fid: null,
        topicMisc: "",
        action: null,
        tags: null,
      };

      // 获取过滤模式
      const filterMode = await this.getFilterMode(nFilter);

      // 样式处理
      (() => {
        // 标记模式下,容器会有删除线标识
        if (filterMode === "标记") {
          container.style.textDecoration = "line-through";
          return;
        }

        // 遮罩模式下,容器会有遮罩样式
        if (filterMode === "遮罩") {
          container.classList.add("filter-mask");
          return;
        }

        // 隐藏模式下,容器会被隐藏
        if (filterMode === "隐藏") {
          container.style.display = "none";
          return;
        }
      })();
    }
  }

  /**
   * 列表模块
   */
  class ListModule extends Module {
    /**
     * 模块名称
     */
    static name = "list";

    /**
     * 模块标签
     */
    static label = "列表";

    /**
     * 顺序
     */
    static order = 10;

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "内容", ellipsis: true },
        { label: "过滤模式", center: true, width: 1 },
        { label: "原因", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 绑定的 nFilter
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { tid, pid, filterMode, reason } = item;

      // 移除 BR 标签
      item.content = (item.content || "").replace(/<br>/g, "");

      // 内容
      const content = (() => {
        if (pid) {
          return ui.createElement("A", item.content, {
            href: `/read.php?pid=${pid}&nofilter`,
            title: item.content,
          });
        }

        // 如果有 TID 但没有标题,是引用,采用内容逻辑
        if (item.subject.length === 0) {
          return ui.createElement("A", item.content, {
            href: `/read.php?tid=${tid}&nofilter`,
            title: item.content,
          });
        }

        return ui.createElement("A", item.subject, {
          href: `/read.php?tid=${tid}&nofilter`,
          title: item.content,
          className: "b nobr",
        });
      })();

      return [content, filterMode, reason];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { tabs, content } = this.ui.views;

      const table = this.ui.createTable(this.columns());

      const tab = this.ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        const list = this.data.filter((item) => {
          return (item.filterMode || "显示") !== "显示";
        });

        Object.values(list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });
      }
    }

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify() {
      // 获取过滤后的数量
      const count = this.data.filter((item) => {
        return (item.filterMode || "显示") !== "显示";
      }).length;

      // 更新菜单文字
      const { ui } = this;
      const { menu } = ui;

      if (menu === null) {
        return;
      }

      if (count) {
        menu.innerHTML = `${ui.constructor.label} <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
      } else {
        menu.innerHTML = `${ui.constructor.label}`;
      }

      // 重新渲染
      // TODO 应该给 table 增加一个判重的逻辑,这样只需要更新过滤后的内容即可
      const { tab } = this.views;

      if (tab.querySelector("A").className === "nobr") {
        this.render(ui.views.content);
      }
    }
  }

  /**
   * 用户模块
   */
  class UserModule extends Module {
    /**
     * 模块名称
     */
    static name = "user";

    /**
     * 模块标签
     */
    static label = "用户";

    /**
     * 顺序
     */
    static order = 20;

    /**
     * 获取列表
     */
    get list() {
      return this.settings.users;
    }

    /**
     * 获取用户
     * @param {Number} uid 用户 ID
     */
    get(uid) {
      // 获取列表
      const list = this.list;

      // 如果存在,则返回信息
      if (list[uid]) {
        return list[uid];
      }

      return null;
    }

    /**
     * 添加用户
     * @param {Number} uid 用户 ID
     */
    add(uid, values) {
      // 获取列表
      const list = this.list;

      // 如果已存在,则返回信息
      if (list[uid]) {
        return list[uid];
      }

      // 写入用户信息
      list[uid] = values;

      // 保存数据
      this.settings.users = list;

      // 重新过滤
      this.reFilter(uid);

      // 返回添加的用户
      return values;
    }

    /**
     * 编辑用户
     * @param {Number} uid    用户 ID
     * @param {*}      values 用户信息
     */
    update(uid, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, uid) === false) {
        return null;
      }

      // 获取用户
      const entity = list[uid];

      // 更新用户
      Object.assign(entity, values);

      // 保存数据
      this.settings.users = list;

      // 重新过滤
      this.reFilter(uid);

      // 返回编辑的用户
      return entity;
    }

    /**
     * 删除用户
     * @param   {Number}        uid 用户 ID
     * @returns {Object | null}     删除的用户
     */
    remove(uid) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, uid) === false) {
        return null;
      }

      // 获取用户
      const entity = list[uid];

      // 删除用户
      delete list[uid];

      // 保存数据
      this.settings.users = list;

      // 重新过滤
      this.reFilter(uid);

      // 返回删除的用户
      return entity;
    }

    /**
     * 格式化
     * @param {Number}             uid  用户 ID
     * @param {String | undefined} name 用户名称
     */
    format(uid, name) {
      if (uid <= 0) {
        return null;
      }

      const { ui } = this;

      const user = this.get(uid);

      if (user) {
        name = user.name;
      }

      const username = name ? "@" + name : "#" + uid;

      return ui.createElement("A", `[${username}]`, {
        className: "b nobr",
        href: `/nuke.php?func=ucp&uid=${uid}`,
      });
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "昵称" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 用户信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, name, filterMode } = item;

      // 昵称
      const user = this.format(id, name);

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          this.update(id, {
            filterMode: newMode,
          });

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(remove);
      })();

      return [user, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content, settings } = ui.views;
      const { add } = settings;

      const table = ui.createTable(this.columns());

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      const keywordFilter = (() => {
        const input = ui.createElement("INPUT", [], {
          style: "flex: 1;",
          placeholder: "输入昵称关键字进行筛选",
        });

        const button = ui.createButton("筛选", () => {
          this.render(content, input.value);
        });

        const wrapper = ui.createElement("DIV", [input, button], {
          style: "display: flex; margin-top: 10px;",
        });

        return wrapper;
      })();

      Object.assign(this.views, {
        tab,
        table,
        keywordFilter,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(keywordFilter);

      // 删除非激活中的用户
      {
        const list = ui.createElement("DIV", [], {
          style: "white-space: normal;",
        });

        const button = ui.createButton("删除非激活中的用户", () => {
          ui.confirm().then(() => {
            list.innerHTML = "";

            const users = Object.values(this.list);

            const waitingQueue = users.map(
              ({ id }) =>
                () =>
                  this.api.getUserInfo(id).then(({ bit }) => {
                    const activeInfo = commonui.activeInfo(0, 0, bit);
                    const activeType = activeInfo[1];

                    if (["ACTIVED", "LINKED"].includes(activeType)) {
                      return;
                    }

                    list.append(this.format(id));

                    this.remove(id);
                  })
            );

            const queueLength = waitingQueue.length;

            const execute = () => {
              if (waitingQueue.length) {
                const next = waitingQueue.shift();

                button.disabled = true;
                button.innerHTML = `删除非激活中的用户 (${
                  queueLength - waitingQueue.length
                }/${queueLength})`;

                next().finally(execute);
                return;
              }

              button.disabled = false;
            };

            execute();
          });
        });

        const element = ui.createElement("DIV", [button, list]);

        add(this.constructor.order + 0, element);
      }
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     * @param {String}      keyword   关键字
     */
    render(container, keyword = "") {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        const list = keyword
          ? Object.values(this.list).filter((item) =>
              item.name.includes(keyword)
            )
          : Object.values(this.list);

        list.forEach((item) => {
          const column = this.column(item);

          add(...column);
        });
      }
    }

    /**
     * 渲染详情
     * @param {Number}             uid      用户 ID
     * @param {String | undefined} name     用户名称
     * @param {Function}           callback 回调函数
     */
    renderDetails(uid, name, callback = () => {}) {
      const { ui, settings } = this;

      // 只允许同时存在一个详情页
      if (this.views.details) {
        if (this.views.details.parentNode) {
          this.views.details.parentNode.removeChild(this.views.details);
        }
      }

      // 获取用户信息
      const user = this.get(uid);

      if (user) {
        name = user.name;
      }

      const title =
        (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;

      const filterMode = user ? user.filterMode : settings.filterModes[0];

      const switchMode = ui.createButton(filterMode, () => {
        const newMode = settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      const buttons = ui.createElement(
        "DIV",
        (() => {
          const remove = user
            ? ui.createButton("删除", () => {
                ui.confirm().then(() => {
                  this.remove(uid);

                  this.views.details._.hide();

                  callback("REMOVE");
                });
              })
            : null;

          const save = ui.createButton("保存", () => {
            if (user === null) {
              const entity = this.add(uid, {
                id: uid,
                name,
                tags: [],
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("ADD", entity);
            } else {
              const entity = this.update(uid, {
                name,
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("UPDATE", entity);
            }
          });

          return ui.createButtonGroup(remove, save);
        })(),
        {
          className: "right_",
        }
      );

      const actions = ui.createElement(
        "DIV",
        [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
        {
          style: "margin-top: 10px;",
        }
      );

      const tips = ui.createElement("DIV", TIPS.filterMode, {
        className: "silver",
        style: "margin-top: 10px;",
      });

      const content = ui.createElement("DIV", [actions, tips], {
        style: "width: 80vw",
      });

      // 创建弹出框
      this.views.details = ui.createDialog(null, title, content);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取用户信息
      const user = this.get(item.uid);

      // 没有则跳过
      if (user === null) {
        return;
      }

      // 获取用户过滤模式
      const mode = this.settings.getModeByName(user.filterMode);

      // 不高于当前过滤模式则跳过
      if (mode <= result.mode) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `用户模式: ${user.filterMode}`;
    }

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify(item) {
      const { uid, username, action } = item;

      // 如果没有 action 组件则跳过
      if (action === null) {
        return;
      }

      // 如果是匿名,隐藏组件
      if (uid <= 0) {
        action.style.display = "none";
        return;
      }

      // 获取当前用户
      const user = this.get(uid);

      // 修改操作按钮文字
      if (action.tagName === "A") {
        action.innerText = "屏蔽";
      } else {
        action.title = "屏蔽";
      }

      // 修改操作按钮颜色
      if (user) {
        action.style.background = "#CB4042";
      } else {
        action.style.background = "#AAA";
      }

      // 绑定事件
      action.onclick = () => {
        this.renderDetails(uid, username);
      };
    }

    /**
     * 重新过滤
     * @param {Number} uid 用户 ID
     */
    reFilter(uid) {
      this.data.forEach((item) => {
        // 如果用户 ID 一致,则重新过滤
        if (item.uid === uid) {
          item.execute();
          return;
        }

        // 如果有引用,也重新过滤
        if (Object.hasOwn(item.quotes || {}, uid)) {
          item.execute();
          return;
        }
      });
    }
  }

  /**
   * 标记模块
   */
  class TagModule extends Module {
    /**
     * 模块名称
     */
    static name = "tag";

    /**
     * 模块标签
     */
    static label = "标记";

    /**
     * 顺序
     */
    static order = 30;

    /**
     * 依赖模块
     */
    static depends = [UserModule];

    /**
     * 依赖的用户模块
     * @returns {UserModule} 用户模块
     */
    get userModule() {
      return this.depends[UserModule.name];
    }

    /**
     * 获取列表
     */
    get list() {
      return this.settings.tags;
    }

    /**
     * 获取标记
     * @param {Number} id   标记 ID
     * @param {String} name 标记名称
     */
    get({ id, name }) {
      // 获取列表
      const list = this.list;

      // 通过 ID 获取标记
      if (list[id]) {
        return list[id];
      }

      // 通过名称获取标记
      if (name) {
        const tag = Object.values(list).find((item) => item.name === name);

        if (tag) {
          return tag;
        }
      }

      return null;
    }

    /**
     * 添加标记
     * @param {String} name 标记名称
     */
    add(name) {
      // 获取对应的标记
      const tag = this.get({ name });

      // 如果标记已存在,则返回标记信息,否则增加标记
      if (tag) {
        return tag;
      }

      // 获取列表
      const list = this.list;

      // ID 为最大值 + 1
      const id = Math.max(...Object.keys(list), 0) + 1;

      // 标记的颜色
      const color = Tools.generateColor(name);

      // 写入标记信息
      list[id] = {
        id,
        name,
        color,
        filterMode: this.settings.filterModes[0],
      };

      // 保存数据
      this.settings.tags = list;

      // 返回添加的标记
      return list[id];
    }

    /**
     * 编辑标记
     * @param {Number} id     标记 ID
     * @param {*}      values 标记信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取标记
      const entity = list[id];

      // 获取相关的用户
      const users = Object.values(this.userModule.list).filter((user) =>
        user.tags.includes(id)
      );

      // 更新标记
      Object.assign(entity, values);

      // 保存数据
      this.settings.tags = list;

      // 重新过滤
      this.reFilter(users);
    }

    /**
     * 删除标记
     * @param {Number} id 标记 ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取标记
      const entity = list[id];

      // 获取相关的用户
      const users = Object.values(this.userModule.list).filter((user) =>
        user.tags.includes(id)
      );

      // 删除标记
      delete list[id];

      // 删除相关的用户标记
      users.forEach((user) => {
        const index = user.tags.findIndex((item) => item === id);

        if (index >= 0) {
          user.tags.splice(index, 1);
        }
      });

      // 保存数据
      this.settings.tags = list;

      // 重新过滤
      this.reFilter(users);

      // 返回删除的标记
      return entity;
    }

    /**
     * 格式化
     * @param {Number}             id   标记 ID
     * @param {String | undefined} name 标记名称
     * @param {String | undefined} name 标记颜色
     */
    format(id, name, color) {
      const { ui } = this;

      if (id >= 0) {
        const tag = this.get({ id });

        if (tag) {
          name = tag.name;
          color = tag.color;
        }
      }

      if (name && color) {
        return ui.createElement("B", name, {
          className: "block_txt nobr",
          style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
        });
      }

      return "";
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "标记", width: 1 },
        { label: "列表" },
        { label: "过滤模式", width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, filterMode } = item;

      // 标记
      const tag = this.format(id);

      // 用户列表
      const list = Object.values(this.userModule.list)
        .filter(({ tags }) => tags.includes(id))
        .map(({ id }) => this.userModule.format(id));

      const group = ui.createElement("DIV", list, {
        style: "white-space: normal; display: none;",
      });

      const switchButton = ui.createButton(list.length.toString(), () => {
        if (group.style.display === "none") {
          group.style.removeProperty("display");
        } else {
          group.style.display = "none";
        }
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          this.update(id, {
            filterMode: newMode,
          });

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(remove);
      })();

      return [tag, [switchButton, group], switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content, settings } = ui.views;
      const { add } = settings;

      const table = ui.createTable(this.columns());

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);

      // 删除没有标记的用户
      {
        const button = ui.createButton("删除没有标记的用户", () => {
          ui.confirm().then(() => {
            const users = Object.values(this.userModule.list);

            users.forEach(({ id, tags }) => {
              if (tags.length > 0) {
                return;
              }

              this.userModule.remove(id);
            });
          });
        });

        const element = ui.createElement("DIV", button);

        add(this.constructor.order + 0, element);
      }

      // 删除没有用户的标记
      {
        const button = ui.createButton("删除没有用户的标记", () => {
          ui.confirm().then(() => {
            const items = Object.values(this.list);
            const users = Object.values(this.userModule.list);

            items.forEach(({ id }) => {
              if (users.find(({ tags }) => tags.includes(id))) {
                return;
              }

              this.remove(id);
            });
          });
        });

        const element = ui.createElement("DIV", button);

        add(this.constructor.order + 1, element);
      }
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });
      }
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取用户信息
      const user = this.userModule.get(item.uid);

      // 没有则跳过
      if (user === null) {
        return;
      }

      // 获取用户标记
      const tags = user.tags;

      // 取最高的过滤模式
      // 低于当前的过滤模式则跳过
      let max = result.mode;
      let tag = null;

      for (const id of tags) {
        const entity = this.get({ id });

        if (entity === null) {
          continue;
        }

        // 获取过滤模式
        const mode = this.settings.getModeByName(entity.filterMode);

        if (mode < max) {
          continue;
        }

        if (mode === max && result.reason.includes("用户模式") === false) {
          continue;
        }

        max = mode;
        tag = entity;
      }

      // 没有匹配的则跳过
      if (tag === null) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = max;
      result.reason = `标记: ${tag.name}`;
    }

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify(item) {
      const { uid, tags } = item;

      // 如果没有 tags 组件则跳过
      if (tags === null) {
        return;
      }

      // 如果是匿名,隐藏组件
      if (uid <= 0) {
        tags.style.display = "none";
        return;
      }

      // 删除旧标记
      [...tags.querySelectorAll("[tid]")].forEach((item) => {
        tags.removeChild(item);
      });

      // 获取当前用户
      const user = this.userModule.get(uid);

      // 如果没有用户,则跳过
      if (user === null) {
        return;
      }

      // 格式化标记
      const items = user.tags.map((id) => {
        const item = this.format(id);

        if (item) {
          item.setAttribute("tid", id);
        }

        return item;
      });

      // 加入组件
      items.forEach((item) => {
        if (item) {
          tags.appendChild(item);
        }
      });
    }

    /**
     * 重新过滤
     * @param {Array} users 用户集合
     */
    reFilter(users) {
      users.forEach((user) => {
        this.userModule.reFilter(user.id);
      });
    }
  }

  /**
   * 关键字模块
   */
  class KeywordModule extends Module {
    /**
     * 模块名称
     */
    static name = "keyword";

    /**
     * 模块标签
     */
    static label = "关键字";

    /**
     * 顺序
     */
    static order = 40;

    /**
     * 获取列表
     */
    get list() {
      return this.settings.keywords;
    }

    /**
     * 将多个布尔值转换为二进制
     */
    boolsToBinary(...args) {
      let res = 0;

      for (let i = 0; i < args.length; i += 1) {
        if (args[i]) {
          res |= 1 << i;
        }
      }

      return res;
    }

    /**
     * 获取关键字
     * @param {Number} id 关键字 ID
     */
    get(id) {
      // 获取列表
      const list = this.list;

      // 如果存在,则返回信息
      if (list[id]) {
        return list[id];
      }

      return null;
    }

    /**
     * 添加关键字
     * @param {String} keyword     关键字
     * @param {String} filterMode  过滤模式
     * @param {Number} filterType  过滤类型,为一个二进制数,0b1 - 过滤标题,0b10 - 过滤内容,0b100 - 过滤昵称
     */
    add(keyword, filterMode, filterType) {
      // 获取列表
      const list = this.list;

      // ID 为最大值 + 1
      const id = Math.max(...Object.keys(list), 0) + 1;

      // 写入关键字信息
      list[id] = {
        id,
        keyword,
        filterMode,
        filterType,
      };

      // 保存数据
      this.settings.keywords = list;

      // 重新过滤
      this.reFilter();

      // 返回添加的关键字
      return list[id];
    }

    /**
     * 编辑关键字
     * @param {Number} id     关键字 ID
     * @param {*}      values 关键字信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取关键字
      const entity = list[id];

      // 更新关键字
      Object.assign(entity, values);

      // 保存数据
      this.settings.keywords = list;

      // 重新过滤
      this.reFilter();
    }

    /**
     * 删除关键字
     * @param {Number} id 关键字 ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取关键字
      const entity = list[id];

      // 删除关键字
      delete list[id];

      // 保存数据
      this.settings.keywords = list;

      // 重新过滤
      this.reFilter();

      // 返回删除的关键字
      return entity;
    }

    /**
     * 获取帖子数据
     * @param {*} item 绑定的 nFilter
     */
    async getPostInfo(item) {
      const { tid, pid } = item;

      // 请求帖子数据
      const { subject, content, userInfo, reputation } =
        await this.api.getPostInfo(tid, pid);

      // 绑定用户信息和声望
      if (userInfo) {
        item.userInfo = userInfo;
        item.username = userInfo.username;
        item.reputation = reputation;
      }

      // 绑定标题和内容
      item.subject = subject;
      item.content = content;
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "关键字" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "过滤标题", center: true, width: 1 },
        { label: "过滤内容", center: true, width: 1 },
        { label: "过滤昵称", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, keyword, filterMode } = item;

      // 兼容旧版本数据
      const filterType =
        item.filterType !== undefined
          ? item.filterType
          : item.filterLevel > 0
          ? 0b11
          : 0b01;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
        value: keyword,
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          switchMode.innerText = newMode;
        }
      );

      // 过滤标题
      const switchTitle = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: filterType & 0b1,
      });

      // 过滤内容
      const switchContent = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: filterType & 0b10,
      });

      // 过滤昵称
      const switchUsername = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: filterType & 0b100,
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(id, {
            keyword: input.value,
            filterMode: switchMode.innerText,
            filterType: this.boolsToBinary(
              switchTitle.checked,
              switchContent.checked,
              switchUsername.checked
            ),
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [
        inputWrapper,
        switchMode,
        switchTitle,
        switchContent,
        switchUsername,
        buttons,
      ];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement("DIV", TIPS.keyword, {
        className: "silver",
      });

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });

        this.renderNewLine();
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(this.settings.filterModes[0], () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 过滤标题
      const switchTitle = ui.createElement("INPUT", [], {
        type: "checkbox",
      });

      // 过滤内容
      const switchContent = ui.createElement("INPUT", [], {
        type: "checkbox",
      });

      // 过滤昵称
      const switchUsername = ui.createElement("INPUT", [], {
        type: "checkbox",
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", (e) => {
          const entity = this.add(
            input.value,
            switchMode.innerText,
            this.boolsToBinary(
              switchTitle.checked,
              switchContent.checked,
              switchUsername.checked
            )
          );

          table.update(e, ...this.column(entity));

          this.renderNewLine();
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(
        inputWrapper,
        switchMode,
        switchTitle,
        switchContent,
        switchUsername,
        buttons
      );
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取列表
      const list = this.list;

      // 跳过低于当前的过滤模式
      const filtered = Object.values(list).filter(
        (item) => this.settings.getModeByName(item.filterMode) > result.mode
      );

      // 没有则跳过
      if (filtered.length === 0) {
        return;
      }

      // 根据过滤模式依次判断
      const sorted = Tools.sortBy(filtered, (item) =>
        this.settings.getModeByName(item.filterMode)
      );

      for (let i = 0; i < sorted.length; i += 1) {
        const { keyword, filterMode } = sorted[i];

        // 兼容旧版本数据
        // 过滤类型,为一个二进制数,0b1 - 过滤标题,0b10 - 过滤内容,0b100 - 过滤昵称
        const filterType =
          sorted[i].filterType !== undefined
            ? sorted[i].filterType
            : sorted[i].filterLevel > 0
            ? 0b11
            : 0b01;

        // 过滤标题
        if (filterType & 0b1) {
          const { subject } = item;

          const match = subject.match(keyword);

          if (match) {
            const mode = this.settings.getModeByName(filterMode);

            // 更新过滤模式和原因
            result.mode = mode;
            result.reason = `关键字: ${match[0]}`;
            return;
          }
        }

        // 过滤内容
        if (filterType & 0b10) {
          // 如果没有内容,则请求
          if (item.content === undefined) {
            await this.getPostInfo(item);
          }

          const { content } = item;

          if (content) {
            const match = content.match(keyword);

            if (match) {
              const mode = this.settings.getModeByName(filterMode);

              // 更新过滤模式和原因
              result.mode = mode;
              result.reason = `关键字: ${match[0]}`;
              return;
            }
          }
        }

        // 过滤昵称
        if (filterType & 0b100) {
          const { username } = item;

          if (username) {
            const match = username.match(keyword);

            if (match) {
              const mode = this.settings.getModeByName(filterMode);

              // 更新过滤模式和原因
              result.mode = mode;
              result.reason = `关键字: ${match[0]}`;
              return;
            }
          }
        }
      }
    }

    /**
     * 重新过滤
     */
    reFilter() {
      // 实际上应该根据过滤模式来筛选要过滤的部分
      this.data.forEach((item) => {
        item.execute();
      });
    }
  }

  /**
   * 属地模块
   */
  class LocationModule extends Module {
    /**
     * 模块名称
     */
    static name = "location";

    /**
     * 模块标签
     */
    static label = "属地";

    /**
     * 顺序
     */
    static order = 50;

    /**
     * 请求缓存
     */
    cache = {};

    /**
     * 获取列表
     */
    get list() {
      return this.settings.locations;
    }

    /**
     * 获取属地
     * @param {Number} id 属地 ID
     */
    get(id) {
      // 获取列表
      const list = this.list;

      // 如果存在,则返回信息
      if (list[id]) {
        return list[id];
      }

      return null;
    }

    /**
     * 添加属地
     * @param {String} keyword     关键字
     * @param {String} filterMode  过滤模式
     */
    add(keyword, filterMode) {
      // 获取列表
      const list = this.list;

      // ID 为最大值 + 1
      const id = Math.max(...Object.keys(list), 0) + 1;

      // 写入属地信息
      list[id] = {
        id,
        keyword,
        filterMode,
      };

      // 保存数据
      this.settings.locations = list;

      // 重新过滤
      this.reFilter();

      // 返回添加的属地
      return list[id];
    }

    /**
     * 编辑属地
     * @param {Number} id     属地 ID
     * @param {*}      values 属地信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取属地
      const entity = list[id];

      // 更新属地
      Object.assign(entity, values);

      // 保存数据
      this.settings.locations = list;

      // 重新过滤
      this.reFilter();
    }

    /**
     * 删除属地
     * @param {Number} id 属地 ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取属地
      const entity = list[id];

      // 删除属地
      delete list[id];

      // 保存数据
      this.settings.locations = list;

      // 重新过滤
      this.reFilter();

      // 返回删除的属地
      return entity;
    }

    /**
     * 获取 IP 属地
     * @param {*} item 绑定的 nFilter
     */
    async getIpLocation(item) {
      const { uid } = item;

      // 如果是匿名直接跳过
      if (uid <= 0) {
        return null;
      }

      // 如果已有缓存,直接返回
      if (Object.hasOwn(this.cache, uid)) {
        return this.cache[uid];
      }

      // 请求属地
      const ipLocations = await this.api.getIpLocations(uid);

      // 写入缓存
      if (ipLocations.length > 0) {
        this.cache[uid] = ipLocations[0].ipLoc;
      }

      // 返回结果
      return null;
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "关键字" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, keyword, filterMode } = item;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
        value: keyword,
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(id, {
            keyword: input.value,
            filterMode: switchMode.innerText,
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [inputWrapper, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement("DIV", TIPS.keyword, {
        className: "silver",
      });

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });

        this.renderNewLine();
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(this.settings.filterModes[0], () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", (e) => {
          const entity = this.add(input.value, switchMode.innerText);

          table.update(e, ...this.column(entity));

          this.renderNewLine();
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(inputWrapper, switchMode, buttons);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取列表
      const list = this.list;

      // 跳过低于当前的过滤模式
      const filtered = Object.values(list).filter(
        (item) => this.settings.getModeByName(item.filterMode) > result.mode
      );

      // 没有则跳过
      if (filtered.length === 0) {
        return;
      }

      // 获取当前属地
      const location = await this.getIpLocation(item);

      // 请求失败则跳过
      if (location === null) {
        return;
      }

      // 根据过滤模式依次判断
      const sorted = Tools.sortBy(filtered, (item) =>
        this.settings.getModeByName(item.filterMode)
      );

      for (let i = 0; i < sorted.length; i += 1) {
        const { keyword, filterMode } = sorted[i];

        const match = location.match(keyword);

        if (match) {
          const mode = this.settings.getModeByName(filterMode);

          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `属地: ${match[0]}`;
          return;
        }
      }
    }

    /**
     * 重新过滤
     */
    reFilter() {
      // 实际上应该根据过滤模式来筛选要过滤的部分
      this.data.forEach((item) => {
        item.execute();
      });
    }
  }

  /**
   * 版面或合集模块
   */
  class ForumOrSubsetModule extends Module {
    /**
     * 模块名称
     */
    static name = "forumOrSubset";

    /**
     * 模块标签
     */
    static label = "版面/合集";

    /**
     * 顺序
     */
    static order = 60;

    /**
     * 请求缓存
     */
    cache = {};

    /**
     * 获取列表
     */
    get list() {
      return this.settings.forumOrSubsets;
    }

    /**
     * 获取版面或合集
     * @param {Number} id ID
     */
    get(id) {
      // 获取列表
      const list = this.list;

      // 如果存在,则返回信息
      if (list[id]) {
        return list[id];
      }

      return null;
    }

    /**
     * 添加版面或合集
     * @param {Number} value       版面或合集链接
     * @param {String} filterMode  过滤模式
     */
    async add(value, filterMode) {
      // 获取链接参数
      const params = new URLSearchParams(value.split("?")[1]);

      // 获取 FID
      const fid = parseInt(params.get("fid"), 10);

      // 获取 STID
      const stid = parseInt(params.get("stid"), 10);

      // 如果 FID 或 STID 不存在,则提示错误
      if (fid === NaN && stid === NaN) {
        alert("版面或合集ID有误");
        return;
      }

      // 获取列表
      const list = this.list;

      // ID 为 FID 或者 t + STID
      const id = fid ? fid : `t${stid}`;

      // 如果版面或合集 ID 已存在,则提示错误
      if (Object.hasOwn(list, id)) {
        alert("已有相同版面或合集ID");
        return;
      }

      // 请求版面或合集信息
      const info = await (async () => {
        if (fid) {
          return await this.api.getForumInfo(fid);
        }

        if (stid) {
          const postInfo = await this.api.getPostInfo(stid);

          if (postInfo) {
            return {
              name: postInfo.subject,
            };
          }
        }

        return null;
      })();

      // 如果版面或合集不存在,则提示错误
      if (info === null || info === undefined) {
        alert("版面或合集ID有误");
        return;
      }

      // 写入版面或合集信息
      list[id] = {
        fid,
        stid,
        name: info.name,
        filterMode,
      };

      // 保存数据
      this.settings.forumOrSubsets = list;

      // 重新过滤
      this.reFilter();

      // 返回添加的版面或合集
      return list[id];
    }

    /**
     * 编辑版面或合集
     * @param {Number} id     ID
     * @param {*}      values 版面或合集信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取版面或合集
      const entity = list[id];

      // 更新版面或合集
      Object.assign(entity, values);

      // 保存数据
      this.settings.forumOrSubsets = list;

      // 重新过滤
      this.reFilter();
    }

    /**
     * 删除版面或合集
     * @param {Number} id ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取版面或合集
      const entity = list[id];

      // 删除版面或合集
      delete list[id];

      // 保存数据
      this.settings.forumOrSubsets = list;

      // 重新过滤
      this.reFilter();

      // 返回删除的版面或合集
      return entity;
    }

    /**
     * 格式化版面或合集
     * @param {Number} fid  版面 ID
     * @param {Number} stid 合集 ID
     * @param {String} name 版面或合集名称
     */
    formatForumOrSubset(fid, stid, name) {
      const { ui } = this;

      return ui.createElement("A", `[${name}]`, {
        className: "b nobr",
        href: fid ? `/thread.php?fid=${fid}` : `/thread.php?stid=${stid}`,
      });
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "版面/合集" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 版面或合集信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { fid, stid, name, filterMode } = item;

      // ID 为 FID 或者 t + STID
      const id = fid ? fid : `t${stid}`;

      // 版面或合集
      const forum = this.formatForumOrSubset(fid, stid, name);

      // 切换过滤模式
      const switchMode = ui.createButton(filterMode || "隐藏", () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(id, {
            filterMode: switchMode.innerText,
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [forum, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement("DIV", TIPS.forumOrSubset, {
        className: "silver",
      });

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });

        this.renderNewLine();
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 版面或合集 ID
      const forumInput = ui.createElement("INPUT", [], {
        type: "text",
      });

      const forumInputWrapper = ui.createElement("DIV", forumInput, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton("隐藏", () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", async (e) => {
          const entity = await this.add(forumInput.value, switchMode.innerText);

          if (entity) {
            table.update(e, ...this.column(entity));

            this.renderNewLine();
          }
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(forumInputWrapper, switchMode, buttons);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 没有版面 ID 或主题杂项则跳过
      if (item.fid === null && item.topicMisc.length === 0) {
        return;
      }

      // 获取列表
      const list = this.list;

      // 跳过低于当前的过滤模式
      const filtered = Object.values(list).filter(
        (item) => this.settings.getModeByName(item.filterMode) > result.mode
      );

      // 没有则跳过
      if (filtered.length === 0) {
        return;
      }

      // 解析主题杂项
      const { _SFID, _STID } = commonui.topicMiscVar.unpack(item.topicMisc);

      // 根据过滤模式依次判断
      const sorted = Tools.sortBy(filtered, (item) =>
        this.settings.getModeByName(item.filterMode)
      );

      for (let i = 0; i < sorted.length; i += 1) {
        const { fid, stid, name, filterMode } = sorted[i];

        if (fid) {
          if (fid === parseInt(item.fid, 10) || fid === _SFID) {
            const mode = this.settings.getModeByName(filterMode);

            // 更新过滤模式和原因
            result.mode = mode;
            result.reason = `版面: ${name}`;
            return;
          }
        }

        if (stid) {
          if (stid === parseInt(item.tid, 10) || stid === _STID) {
            const mode = this.settings.getModeByName(filterMode);

            // 更新过滤模式和原因
            result.mode = mode;
            result.reason = `合集: ${name}`;
            return;
          }
        }
      }
    }

    /**
     * 重新过滤
     */
    reFilter() {
      // 实际上应该根据过滤模式来筛选要过滤的部分
      this.data.forEach((item) => {
        item.execute();
      });
    }
  }

  /**
   * 猎巫模块
   *
   * 其实是通过 Cache 模块读取配置,而非 Settings
   */
  class HunterModule extends Module {
    /**
     * 模块名称
     */
    static name = "hunter";

    /**
     * 模块标签
     */
    static label = "猎巫";

    /**
     * 顺序
     */
    static order = 70;

    /**
     * 请求缓存
     */
    cache = {};

    /**
     * 请求队列
     */
    queue = [];

    /**
     * 获取列表
     */
    get list() {
      return this.settings.cache
        .get("WITCH_HUNT")
        .then((values) => values || []);
    }

    /**
     * 获取猎巫
     * @param {Number} fid 版面 ID
     */
    async get(fid) {
      // 获取列表
      const list = await this.list;

      // 如果存在,则返回信息
      if (list[fid]) {
        return list[fid];
      }

      return null;
    }

    /**
     * 添加猎巫
     * @param {Number} fid         版面 ID
     * @param {String} label       标签
     * @param {String} filterMode  过滤模式
     * @param {Number} filterLevel 过滤等级: 0 - 仅标记; 1 - 标记并过滤
     */
    async add(fid, label, filterMode, filterLevel) {
      // FID 只能是数字
      fid = parseInt(fid, 10);

      // 获取列表
      const list = await this.list;

      // 如果版面 ID 已存在,则提示错误
      if (Object.keys(list).includes(fid)) {
        alert("已有相同版面ID");
        return;
      }

      // 请求版面信息
      const info = await this.api.getForumInfo(fid);

      // 如果版面不存在,则提示错误
      if (info === null || info === undefined) {
        alert("版面ID有误");
        return;
      }

      // 计算标记颜色
      const color = Tools.generateColor(info.name);

      // 写入猎巫信息
      list[fid] = {
        fid,
        name: info.name,
        label,
        color,
        filterMode,
        filterLevel,
      };

      // 保存数据
      this.settings.cache.put("WITCH_HUNT", list);

      // 重新过滤
      this.reFilter(true);

      // 返回添加的猎巫
      return list[fid];
    }

    /**
     * 编辑猎巫
     * @param {Number} fid    版面 ID
     * @param {*}      values 猎巫信息
     */
    async update(fid, values) {
      // 获取列表
      const list = await this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, fid) === false) {
        return null;
      }

      // 获取猎巫
      const entity = list[fid];

      // 更新猎巫
      Object.assign(entity, values);

      // 保存数据
      this.settings.cache.put("WITCH_HUNT", list);

      // 重新过滤,更新样式即可
      this.reFilter(false);
    }

    /**
     * 删除猎巫
     * @param {Number} fid 版面 ID
     */
    async remove(fid) {
      // 获取列表
      const list = await this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, fid) === false) {
        return null;
      }

      // 获取猎巫
      const entity = list[fid];

      // 删除猎巫
      delete list[fid];

      // 保存数据
      this.settings.cache.put("WITCH_HUNT", list);

      // 重新过滤
      this.reFilter(true);

      // 返回删除的猎巫
      return entity;
    }

    /**
     * 格式化版面
     * @param {Number} fid  版面 ID
     * @param {String} name 版面名称
     */
    formatForum(fid, name) {
      const { ui } = this;

      return ui.createElement("A", `[${name}]`, {
        className: "b nobr",
        href: `/thread.php?fid=${fid}`,
      });
    }

    /**
     * 格式化标签
     * @param {String} name 标签名称
     * @param {String} name 标签颜色
     */
    formatLabel(name, color) {
      const { ui } = this;

      return ui.createElement("B", name, {
        className: "block_txt nobr",
        style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
      });
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "版面", width: 200 },
        { label: "标签" },
        { label: "启用过滤", center: true, width: 1 },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 猎巫信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { fid, name, label, color, filterMode, filterLevel } = item;

      // 版面
      const forum = this.formatForum(fid, name);

      // 标签
      const labelElement = this.formatLabel(label, color);

      // 启用过滤
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: filterLevel > 0,
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(fid, {
            filterMode: switchMode.innerText,
            filterLevel: switchLevel.checked ? 1 : 0,
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(async () => {
            await this.remove(fid);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [forum, labelElement, switchLevel, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement(
        "DIV",
        [TIPS.hunter, TIPS.error].join("<br/>"),
        {
          className: "silver",
        }
      );

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        this.list.then((values) => {
          Object.values(values).forEach((item) => {
            const column = this.column(item);

            add(...column);
          });

          this.renderNewLine();
        });
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 版面 ID
      const forumInput = ui.createElement("INPUT", [], {
        type: "text",
      });

      const forumInputWrapper = ui.createElement("DIV", forumInput, {
        className: "filter-input-wrapper",
      });

      // 标签
      const labelInput = ui.createElement("INPUT", [], {
        type: "text",
      });

      const labelInputWrapper = ui.createElement("DIV", labelInput, {
        className: "filter-input-wrapper",
      });

      // 启用过滤
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(this.settings.filterModes[0], () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", async (e) => {
          const entity = await this.add(
            forumInput.value,
            labelInput.value,
            switchMode.innerText,
            switchLevel.checked ? 1 : 0
          );

          if (entity) {
            table.update(e, ...this.column(entity));

            this.renderNewLine();
          }
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(
        forumInputWrapper,
        labelInputWrapper,
        switchLevel,
        switchMode,
        buttons
      );
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取当前猎巫结果
      const hunter = item.hunter || [];

      // 如果没有猎巫结果,则跳过
      if (hunter.length === 0) {
        return;
      }

      // 获取列表
      const items = await this.list;

      // 筛选出匹配的猎巫
      const list = Object.values(items).filter(({ fid }) =>
        hunter.includes(fid)
      );

      // 取最高的过滤模式
      // 低于当前的过滤模式则跳过
      let max = result.mode;
      let res = null;

      for (const entity of list) {
        const { filterLevel, filterMode } = entity;

        // 仅标记
        if (filterLevel === 0) {
          continue;
        }

        // 获取过滤模式
        const mode = this.settings.getModeByName(filterMode);

        if (mode <= max) {
          continue;
        }

        max = mode;
        res = entity;
      }

      // 没有匹配的则跳过
      if (res === null) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = max;
      result.reason = `猎巫: ${res.label}`;
    }

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify(item) {
      const { uid, tags } = item;

      // 如果没有 tags 组件则跳过
      if (tags === null) {
        return;
      }

      // 如果是匿名,隐藏组件
      if (uid <= 0) {
        tags.style.display = "none";
        return;
      }

      // 删除旧标签
      [...tags.querySelectorAll("[fid]")].forEach((item) => {
        tags.removeChild(item);
      });

      // 如果没有请求,开始请求
      if (Object.hasOwn(item, "hunter") === false) {
        this.execute(item);
        return;
      }

      // 获取当前猎巫结果
      const hunter = item.hunter;

      // 如果没有猎巫结果,则跳过
      if (hunter.length === 0) {
        return;
      }

      // 格式化标签
      const items = await Promise.all(
        hunter.map(async (fid) => {
          const item = await this.get(fid);

          if (item) {
            const element = this.formatLabel(item.label, item.color);

            element.setAttribute("fid", fid);

            return element;
          }

          return null;
        })
      );

      // 加入组件
      items.forEach((item) => {
        if (item) {
          tags.appendChild(item);
        }
      });
    }

    /**
     * 重新过滤
     * @param {Boolean} clear 是否清除缓存
     */
    reFilter(clear) {
      // 清除缓存
      if (clear) {
        this.cache = {};
      }

      // 重新过滤
      this.data.forEach((item) => {
        // 不需要清除缓存的话,只要重新加载标记
        if (clear === false) {
          item.hunter = [];
        }

        // 重新猎巫
        this.execute(item);
      });
    }

    /**
     * 猎巫
     * @param {*} item 绑定的 nFilter
     */
    async execute(item) {
      const { uid } = item;
      const { api, cache, queue, list } = this;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 初始化猎巫结果,用于标识正在猎巫
      item.hunter = item.hunter || [];

      // 获取列表
      const items = await list;

      // 没有设置且没有旧数据,直接跳过
      if (items.length === 0 && item.hunter.length === 0) {
        return;
      }

      // 重新过滤
      const reload = (newValue) => {
        const isEqual = newValue.sort().join() === item.hunter.sort().join();

        if (isEqual) {
          return;
        }

        item.hunter = newValue;
        item.execute();
      };

      // 创建任务
      const task = async () => {
        // 如果缓存里没有记录,请求数据并写入缓存
        if (Object.hasOwn(cache, uid) === false) {
          cache[uid] = [];

          await Promise.all(
            Object.keys(items).map(async (fid) => {
              // 转换为数字格式
              const id = parseInt(fid, 10);

              // 当前版面发言记录
              const result = await api.getForumPosted(id, uid);

              // 写入当前设置
              if (result) {
                cache[uid].push(id);
              }
            })
          );
        }

        // 重新过滤
        reload(cache[uid]);

        // 将当前任务移出队列
        queue.shift();

        // 如果还有任务,继续执行
        if (queue.length > 0) {
          queue[0]();
        }
      };

      // 队列里已经有任务
      const isRunning = queue.length > 0;

      // 加入队列
      queue.push(task);

      // 如果没有正在执行的任务,则立即执行
      if (isRunning === false) {
        task();
      }
    }
  }

  /**
   * 杂项模块
   */
  class MiscModule extends Module {
    /**
     * 模块名称
     */
    static name = "misc";

    /**
     * 模块标签
     */
    static label = "杂项";

    /**
     * 顺序
     */
    static order = 80;

    /**
     * 请求缓存
     */
    cache = {
      topicNums: {},
      topicPerPages: {},
    };

    /**
     * 获取用户信息(从页面上)
     * @param {*} item 绑定的 nFilter
     */
    getUserInfo(item) {
      const { uid } = item;

      // 如果是匿名直接跳过
      if (uid <= 0) {
        return;
      }

      // 回复页面可以直接获取到用户信息和声望
      if (commonui.userInfo) {
        // 取得用户信息
        const userInfo = commonui.userInfo.users[uid];

        // 绑定用户信息和声望
        if (userInfo) {
          item.userInfo = userInfo;
          item.username = userInfo.username;

          item.reputation = (() => {
            const reputations = commonui.userInfo.reputations;

            if (reputations) {
              for (let fid in reputations) {
                return reputations[fid][uid] || 0;
              }
            }

            return NaN;
          })();
        }
      }
    }

    /**
     * 获取帖子数据
     * @param {*} item 绑定的 nFilter
     */
    async getPostInfo(item) {
      const { tid, pid } = item;

      // 请求帖子数据
      const { subject, content, userInfo, reputation } =
        await this.api.getPostInfo(tid, pid);

      // 绑定用户信息和声望
      if (userInfo) {
        item.userInfo = userInfo;
        item.username = userInfo.username;
        item.reputation = reputation;
      }

      // 绑定标题和内容
      item.subject = subject;
      item.content = content;
    }

    /**
     * 获取主题数量
     * @param {*} item 绑定的 nFilter
     */
    async getTopicNum(item) {
      const { uid } = item;

      // 如果是匿名直接跳过
      if (uid <= 0) {
        return;
      }

      // 如果已有缓存,直接返回
      if (Object.hasOwn(this.cache.topicNums, uid)) {
        return this.cache.topicNums[uid];
      }

      // 请求数量
      const number = await this.api.getTopicNum(uid);

      // 写入缓存
      this.cache.topicNums[uid] = number;

      // 返回结果
      return number;
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { settings, ui } = this;
      const { tabs, content } = ui.views;

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
      });

      const add = (order, ...elements) => {
        this.views.container.appendChild(
          ui.createElement("DIV", [...elements, ui.createElement("BR", [])], {
            order,
          })
        );
      };

      // 小号过滤(注册时间)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterRegdateLimit / 86400000,
          maxLength: 4,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = (() => {
            const result = parseInt(input.value, 10);

            if (result > 0) {
              return result;
            }

            return 0;
          })();

          settings.filterRegdateLimit = newValue * 86400000;

          input.value = newValue;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏注册时间小于",
          input,
          "天的用户",
          button,
        ]);

        add(this.constructor.order + 0, element);
      }

      // 小号过滤(发帖数)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterPostnumLimit,
          maxLength: 5,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = (() => {
            const result = parseInt(input.value, 10);

            if (result > 0) {
              return result;
            }

            return 0;
          })();

          settings.filterPostnumLimit = newValue;

          input.value = newValue;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏发帖数量小于",
          input,
          "贴的用户",
          button,
        ]);

        add(this.constructor.order + 1, element);
      }

      // 流量号过滤(主题比例)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterTopicRateLimit,
          maxLength: 3,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = (() => {
            const result = parseInt(input.value, 10);

            if (result > 0 && result <= 100) {
              return result;
            }

            return 100;
          })();

          settings.filterTopicRateLimit = newValue;

          input.value = newValue;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏发帖比例大于",
          input,
          "%的用户",
          button,
        ]);

        const tips = ui.createElement("DIV", TIPS.error, {
          className: "silver",
        });

        add(
          this.constructor.order + 2,
          ui.createElement("DIV", [element, tips])
        );
      }

      // 流量号过滤(每页主题数量)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterTopicPerPageLimit || "",
          maxLength: 3,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = (() => {
            const result = parseInt(input.value, 10);

            if (result > 0) {
              return result;
            }

            return NaN;
          })();

          settings.filterTopicPerPageLimit = newValue;

          input.value = newValue || "";

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏每页主题大于",
          input,
          "贴的用户",
          button,
        ]);

        const tips = ui.createElement("DIV", TIPS.filterTopicPerPage, {
          className: "silver",
        });

        add(
          this.constructor.order + 3,
          ui.createElement("DIV", [element, tips])
        );
      }

      // 声望过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterReputationLimit || "",
          maxLength: 4,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = parseInt(input.value, 10);

          settings.filterReputationLimit = newValue;

          input.value = newValue || "";

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏版面声望低于",
          input,
          "点的用户",
          button,
        ]);

        add(this.constructor.order + 4, element);
      }

      // 匿名过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "checkbox",
          checked: settings.filterAnonymous,
        });

        const label = ui.createElement("LABEL", ["隐藏匿名的用户", input], {
          style: "display: flex;",
        });

        const element = ui.createElement("DIV", label);

        input.onchange = () => {
          settings.filterAnonymous = input.checked;

          this.reFilter();
        };

        add(this.constructor.order + 5, element);
      }

      // 缩略图过滤
      {
        const items = {
          none: "禁用",
          all: "启用",
          thumbnail: "仅图片",
        };

        const select = ui.createElement(
          "SELECT",
          Object.keys(items).map((value) =>
            ui.createElement("OPTION", items[value], { value })
          ),
          {
            value: settings.filterThumbnail || "none",
          }
        );

        const label = ui.createElement(
          "LABEL",
          ["隐藏含缩略图的帖子", select],
          {
            style: "display: flex;",
          }
        );

        const element = ui.createElement("DIV", label);

        select.onchange = () => {
          settings.filterThumbnail = select.value;

          this.reFilter();
        };

        add(this.constructor.order + 6, element);
      }
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 缩略图过滤
      await this.filterByThumbnail(item, result);

      // 匿名过滤
      await this.filterByAnonymous(item, result);

      // 注册时间过滤
      await this.filterByRegdate(item, result);

      // 发帖数量过滤
      await this.filterByPostnum(item, result);

      // 发帖比例过滤
      await this.filterByTopicRate(item, result);

      // 每页主题数量过滤
      await this.filterByTopicPerPage(item, result);

      // 版面声望过滤
      await this.filterByReputation(item, result);
    }

    /**
     * 根据缩略图过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByThumbnail(item, result) {
      const { tid, title } = item;

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 找到对应数据
      const data = topicModule.data.find((item) => item[8] === tid);

      // 如果不含缩略图,则跳过
      if (data === undefined || data[20] === null) {
        return;
      }

      // 调整缩略图
      const handleThumbnail = () => {
        // 获取过滤缩略图设置
        const filterThumbnail = this.settings.filterThumbnail || "none";

        // 获取容器
        const container = title.parentNode;

        // 定位锚点
        const anchor = (() => {
          const img = container.querySelector("IMG");

          if (img) {
            return img.closest("DIV");
          }

          return null;
        })();

        // 如果没有锚点,则增加观察
        if (anchor === null) {
          const observer = new MutationObserver(handleThumbnail);

          observer.observe(container, {
            childList: true,
          });
        } else if (filterThumbnail === "none") {
          anchor.style.removeProperty("display");
        } else {
          anchor.style.display = "none";
        }

        return filterThumbnail;
      };

      // 更新过滤模式和原因
      if (handleThumbnail() === "all") {
        result.mode = mode;
        result.reason = "缩略图";
      }
    }

    /**
     * 根据匿名过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByAnonymous(item, result) {
      const { uid, username } = item;

      // 如果不是匿名,则跳过
      if (uid > 0) {
        return;
      }

      // 如果没有引用,则跳过
      if (username === undefined) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取过滤匿名设置
      const filterAnonymous = this.settings.filterAnonymous;

      if (filterAnonymous) {
        // 更新过滤模式和原因
        result.mode = mode;
        result.reason = "匿名";
      }
    }

    /**
     * 根据注册时间过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByRegdate(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取注册时间限制
      const filterRegdateLimit = this.settings.filterRegdateLimit;

      // 未启用则跳过
      if (filterRegdateLimit <= 0) {
        return;
      }

      // 没有用户信息,优先从页面上获取
      if (item.userInfo === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.userInfo === undefined) {
        await this.getPostInfo(item);
      }

      // 获取注册时间
      const { regdate } = item.userInfo || {};

      // 获取失败则跳过
      if (regdate === undefined) {
        return;
      }

      // 转换时间格式,泥潭接口只精确到秒
      const date = new Date(regdate * 1000);

      // 计算时间差
      const diff = Date.now() - date;

      // 判断是否符合条件
      if (diff > filterRegdateLimit) {
        return;
      }

      // 转换为天数
      const days = Math.floor(diff / 86400000);

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `注册时间: ${days}天`;
    }

    /**
     * 根据发帖数量过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByPostnum(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取发帖数量限制
      const filterPostnumLimit = this.settings.filterPostnumLimit;

      // 未启用则跳过
      if (filterPostnumLimit <= 0) {
        return;
      }

      // 没有用户信息,优先从页面上获取
      if (item.userInfo === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.userInfo === undefined) {
        await this.getPostInfo(item);
      }

      // 获取发帖数量
      const { postnum } = item.userInfo || {};

      // 获取失败则跳过
      if (postnum === undefined) {
        return;
      }

      // 判断是否符合条件
      if (postnum >= filterPostnumLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `发帖数量: ${postnum}`;
    }

    /**
     * 根据发帖比例过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByTopicRate(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取发帖比例限制
      const filterTopicRateLimit = this.settings.filterTopicRateLimit;

      // 未启用则跳过
      if (filterTopicRateLimit <= 0 || filterTopicRateLimit >= 100) {
        return;
      }

      // 没有用户信息,优先从页面上获取
      if (item.userInfo === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.userInfo === undefined) {
        await this.getPostInfo(item);
      }

      // 获取发帖数量
      const { postnum } = item.userInfo || {};

      // 获取失败则跳过
      if (postnum === undefined) {
        return;
      }

      // 获取主题数量
      const topicNum = await this.getTopicNum(item);

      // 计算发帖比例
      const topicRate = Math.ceil((topicNum / postnum) * 100);

      // 判断是否符合条件
      if (topicRate < filterTopicRateLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `发帖比例: ${topicRate}% (${topicNum}/${postnum})`;
    }

    /**
     * 根据每页主题数量过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByTopicPerPage(item, result) {
      const { uid, tid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取每页主题数量限制
      const filterTopicPerPageLimit = this.settings.filterTopicPerPageLimit;

      // 未启用则跳过
      if (Number.isNaN(filterTopicPerPageLimit)) {
        return;
      }

      // 已有标记,直接斩杀
      // 但需要判断斩杀线是否有变动
      if (Object.hasOwn(this.cache.topicPerPages, uid)) {
        if (this.cache.topicPerPages[uid] > filterTopicPerPageLimit) {
          // 重新计算数量
          const num = this.data.filter((item) => item.uid === uid).length;

          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `本页主题: ${num}`;

          return;
        }

        delete this.cache.topicPerPages[uid];
      }

      // 获取页面上所有主题
      const list = [...topicModule.data].map((item) => item.nFilter || {});

      // 预检测
      const max = list.filter((item) => item.uid === uid).length;

      // 多页的主题数量累计仍不符合,直接跳过
      if (max <= filterTopicPerPageLimit) {
        return;
      }

      // 获取当前主题下标
      const index = list.findIndex((item) => item.tid === tid);

      // 没有找到,跳过
      if (index < 0) {
        return;
      }

      // 每页主题数量
      const topicPerPage = 30;

      // 符合条件的主题下标
      let indexPrev = index;
      let indexNext = index;

      // 符合的主题
      let checked = 1;

      // 从当前主题往前和往后检测
      for (let i = 1; i < topicPerPage - (indexNext - indexPrev); i += 1) {
        const topicPrev = list[indexPrev - i];
        const topicNext = list[indexNext + i];

        if (topicPrev === undefined && topicNext === undefined) {
          break;
        }

        if (topicPrev && topicPrev.uid === uid) {
          indexPrev -= i;
          checked += 1;

          i = 0;
          continue;
        }

        if (topicNext && topicNext.uid === uid) {
          indexNext += i;
          checked += 1;

          i = 0;
          continue;
        }
      }

      // 判断是否符合条件
      if (checked <= filterTopicPerPageLimit) {
        return;
      }

      // 写入斩杀标记
      this.cache.topicPerPages[uid] = checked;

      // 重新计算相关帖子
      this.reFilter(uid);
    }

    /**
     * 根据版面声望过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByReputation(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取版面声望限制
      const filterReputationLimit = this.settings.filterReputationLimit;

      // 未启用则跳过
      if (Number.isNaN(filterReputationLimit)) {
        return;
      }

      // 没有声望信息,优先从页面上获取
      if (item.reputation === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.reputation === undefined) {
        await this.getPostInfo(item);
      }

      // 获取版面声望
      const reputation = item.reputation || 0;

      // 判断是否符合条件
      if (reputation >= filterReputationLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `版面声望: ${reputation}`;
    }

    /**
     * 重新过滤
     * @param {Number} uid 用户 ID
     */
    reFilter(uid = null) {
      this.data.forEach((item) => {
        if (item.uid === uid || uid === null) {
          item.execute();
        }
      });
    }
  }

  /**
   * 设置模块
   */
  class SettingsModule extends Module {
    /**
     * 模块名称
     */
    static name = "settings";

    /**
     * 顺序
     */
    static order = 0;

    /**
     * 创建实例
     * @param   {Settings}      settings 设置
     * @param   {API}           api      API
     * @param   {UI}            ui       UI
     * @param   {Array}         data     过滤列表
     * @returns {Module | null}          成功后返回模块实例
     */
    static create(settings, api, ui, data) {
      // 读取设置里的模块列表
      const modules = settings.modules;

      // 如果不包含自己,加入列表中,因为设置模块是必须的
      if (modules.includes(this.name) === false) {
        settings.modules = [...modules, this.name];
      }

      // 创建实例
      return super.create(settings, api, ui, data);
    }

    /**
     * 初始化,增加设置
     */
    initComponents() {
      super.initComponents();

      const { settings, ui } = this;
      const { add } = ui.views.settings;

      // 前置过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "checkbox",
        });

        const label = ui.createElement("LABEL", ["前置过滤", input], {
          style: "display: flex;",
        });

        settings.preFilterEnabled.then((checked) => {
          input.checked = checked;
          input.onchange = () => {
            settings.preFilterEnabled = !checked;
          };
        });

        add(this.constructor.order + 0, label);
      }

      // 提醒过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "checkbox",
        });

        const label = ui.createElement("LABEL", ["提醒过滤", input], {
          style: "display: flex;",
        });

        const tips = ui.createElement("DIV", TIPS.filterNotification, {
          className: "silver",
        });

        settings.notificationFilterEnabled.then((checked) => {
          input.checked = checked;
          input.onchange = () => {
            settings.notificationFilterEnabled = !checked;
          };
        });

        add(this.constructor.order + 1, label, tips);
      }

      // 模块选择
      {
        const modules = [
          ListModule,
          UserModule,
          TagModule,
          KeywordModule,
          LocationModule,
          ForumOrSubsetModule,
          HunterModule,
          MiscModule,
        ];

        const items = modules.map((item) => {
          const input = ui.createElement("INPUT", [], {
            type: "checkbox",
            value: item.name,
            checked: settings.modules.includes(item.name),
            onchange: () => {
              const checked = input.checked;

              modules.map((m, index) => {
                const isDepend = checked
                  ? item.depends.find((i) => i.name === m.name)
                  : m.depends.find((i) => i.name === item.name);

                if (isDepend) {
                  const element = items[index].querySelector("INPUT");

                  if (element) {
                    element.checked = checked;
                  }
                }
              });
            },
          });

          const label = ui.createElement("LABEL", [item.label, input], {
            style: "display: flex; margin-right: 10px;",
          });

          return label;
        });

        const button = ui.createButton("确认", () => {
          const checked = group.querySelectorAll("INPUT:checked");
          const values = [...checked].map((item) => item.value);

          settings.modules = values;

          location.reload();
        });

        const group = ui.createElement("DIV", [...items, button], {
          style: "display: flex;",
        });

        const label = ui.createElement("LABEL", "启用模块");

        add(this.constructor.order + 2, label, group);
      }

      // 默认过滤模式
      {
        const modes = ["标记", "遮罩", "隐藏"].map((item) => {
          const input = ui.createElement("INPUT", [], {
            type: "radio",
            name: "defaultFilterMode",
            value: item,
            checked: settings.defaultFilterMode === item,
            onchange: () => {
              settings.defaultFilterMode = item;

              this.reFilter();
            },
          });

          const label = ui.createElement("LABEL", [item, input], {
            style: "display: flex; margin-right: 10px;",
          });

          return label;
        });

        const group = ui.createElement("DIV", modes, {
          style: "display: flex;",
        });

        const label = ui.createElement("LABEL", "默认过滤模式");

        const tips = ui.createElement("DIV", TIPS.filterMode, {
          className: "silver",
        });

        add(this.constructor.order + 3, label, group, tips);
      }

      // 主题
      {
        const themes = Object.keys(THEMES).map((item) => {
          const input = ui.createElement("INPUT", [], {
            type: "radio",
            name: "theme",
            value: item,
            checked: settings.theme === item,
            onchange: () => {
              settings.theme = item;

              handleStyle(settings);
            },
          });

          const { name } = THEMES[item];

          const label = ui.createElement("LABEL", [name, input], {
            style: "display: flex; margin-right: 10px;",
          });

          return label;
        });

        const group = ui.createElement("DIV", themes, {
          style: "display: flex;",
        });

        const label = ui.createElement("LABEL", "主题");

        add(this.constructor.order + 4, label, group);
      }
    }

    /**
     * 重新过滤
     */
    reFilter() {
      // 目前仅在修改默认过滤模式时重新过滤
      this.data.forEach((item) => {
        // 如果过滤模式是继承,则重新过滤
        if (item.filterMode === "继承") {
          item.execute();
        }

        // 如果有引用,也重新过滤
        if (Object.values(item.quotes || {}).includes("继承")) {
          item.execute();
          return;
        }
      });
    }
  }

  /**
   * 增强的列表模块,增加了用户作为附加模块
   */
  class ListEnhancedModule extends ListModule {
    /**
     * 模块名称
     */
    static name = "list";

    /**
     * 附加模块
     */
    static addons = [UserModule];

    /**
     * 附加的用户模块
     * @returns {UserModule} 用户模块
     */
    get userModule() {
      return this.addons[UserModule.name];
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      const hasAddon = this.hasAddon(UserModule);

      if (hasAddon === false) {
        return super.columns();
      }

      return [
        { label: "用户", width: 1 },
        { label: "内容", ellipsis: true },
        { label: "过滤模式", center: true, width: 1 },
        { label: "原因", width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 绑定的 nFilter
     * @returns {Array}      表格项集合
     */
    column(item) {
      const column = super.column(item);

      const hasAddon = this.hasAddon(UserModule);

      if (hasAddon === false) {
        return column;
      }

      const { ui } = this;
      const { table } = this.views;
      const { uid, username } = item;

      const user = this.userModule.format(uid, username);

      const buttons = (() => {
        if (uid <= 0) {
          return null;
        }

        const block = ui.createButton("屏蔽", (e) => {
          this.userModule.renderDetails(uid, username, (type) => {
            // 删除失效数据,等待重新过滤
            table.remove(e);

            // 如果是新增,不会因为用户重新过滤,需要主动触发
            if (type === "ADD") {
              this.userModule.reFilter(uid);
            }
          });
        });

        return ui.createButtonGroup(block);
      })();

      return [user, ...column, buttons];
    }
  }

  /**
   * 增强的用户模块,增加了标记作为附加模块
   */
  class UserEnhancedModule extends UserModule {
    /**
     * 模块名称
     */
    static name = "user";

    /**
     * 附加模块
     */
    static addons = [TagModule];

    /**
     * 附加的标记模块
     * @returns {TagModule} 标记模块
     */
    get tagModule() {
      return this.addons[TagModule.name];
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      const hasAddon = this.hasAddon(TagModule);

      if (hasAddon === false) {
        return super.columns();
      }

      return [
        { label: "昵称", width: 1 },
        { label: "标记" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 用户信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const column = super.column(item);

      const hasAddon = this.hasAddon(TagModule);

      if (hasAddon === false) {
        return column;
      }

      const { ui } = this;
      const { table } = this.views;
      const { id, name } = item;

      const tags = ui.createElement(
        "DIV",
        item.tags.map((id) => this.tagModule.format(id))
      );

      const newColumn = [...column];

      newColumn.splice(1, 0, tags);

      const buttons = column[column.length - 1];

      const update = ui.createButton("编辑", (e) => {
        this.renderDetails(id, name, (type, newValue) => {
          if (type === "UPDATE") {
            table.update(e, ...this.column(newValue));
          }

          if (type === "REMOVE") {
            table.remove(e);
          }
        });
      });

      buttons.insertBefore(update, buttons.firstChild);

      return newColumn;
    }

    /**
     * 渲染详情
     * @param {Number}             uid      用户 ID
     * @param {String | undefined} name     用户名称
     * @param {Function}           callback 回调函数
     */
    renderDetails(uid, name, callback = () => {}) {
      const hasAddon = this.hasAddon(TagModule);

      if (hasAddon === false) {
        return super.renderDetails(uid, name, callback);
      }

      const { ui, settings } = this;

      // 只允许同时存在一个详情页
      if (this.views.details) {
        if (this.views.details.parentNode) {
          this.views.details.parentNode.removeChild(this.views.details);
        }
      }

      // 获取用户信息
      const user = this.get(uid);

      if (user) {
        name = user.name;
      }

      // TODO 需要优化

      const title =
        (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;

      const table = ui.createTable([]);

      {
        const size = Math.floor((screen.width * 0.8) / 200);

        const items = Object.values(this.tagModule.list).map(({ id }) => {
          const checked = user && user.tags.includes(id) ? "checked" : "";

          return `
            <td class="c1">
              <label for="s-tag-${id}" style="display: block; cursor: pointer;">
                ${this.tagModule.format(id).outerHTML}
              </label>
            </td>
            <td class="c2" width="1">
              <input id="s-tag-${id}" type="checkbox" value="${id}" ${checked}/>
            </td>
          `;
        });

        const rows = [...new Array(Math.ceil(items.length / size))].map(
          (_, index) => `
            <tr class="row${(index % 2) + 1}">
              ${items.slice(size * index, size * (index + 1)).join("")}
            </tr>
          `
        );

        table.querySelector("TBODY").innerHTML = rows.join("");
      }

      const input = ui.createElement("INPUT", [], {
        type: "text",
        placeholder: TIPS.addTags,
        style: "width: -webkit-fill-available;",
      });

      const inputWrapper = ui.createElement("DIV", input, {
        style: "margin-top: 10px;",
      });

      const filterMode = user ? user.filterMode : settings.filterModes[0];

      const switchMode = ui.createButton(filterMode, () => {
        const newMode = settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      const buttons = ui.createElement(
        "DIV",
        (() => {
          const remove = user
            ? ui.createButton("删除", () => {
                ui.confirm().then(() => {
                  this.remove(uid);

                  this.views.details._.hide();

                  callback("REMOVE");
                });
              })
            : null;

          const save = ui.createButton("保存", () => {
            const checked = [...table.querySelectorAll("INPUT:checked")].map(
              (input) => parseInt(input.value, 10)
            );

            const newTags = input.value
              .split("|")
              .filter((item) => item.length)
              .map((item) => this.tagModule.add(item))
              .filter((tag) => tag !== null)
              .map((tag) => tag.id);

            const tags = [...new Set([...checked, ...newTags])].sort();

            if (user === null) {
              const entity = this.add(uid, {
                id: uid,
                name,
                tags,
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("ADD", entity);
            } else {
              const entity = this.update(uid, {
                name,
                tags,
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("UPDATE", entity);
            }
          });

          return ui.createButtonGroup(remove, save);
        })(),
        {
          className: "right_",
        }
      );

      const actions = ui.createElement(
        "DIV",
        [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
        {
          style: "margin-top: 10px;",
        }
      );

      const tips = ui.createElement("DIV", TIPS.filterMode, {
        className: "silver",
        style: "margin-top: 10px;",
      });

      const content = ui.createElement(
        "DIV",
        [table, inputWrapper, actions, tips],
        {
          style: "width: 80vw",
        }
      );

      // 创建弹出框
      this.views.details = ui.createDialog(null, title, content);
    }
  }

  /**
   * 处理 topicArg 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui.topicArg
   */
  const handleTopicModule = async (filter, value) => {
    // 绑定主题模块
    topicModule = value;

    if (value === undefined) {
      return;
    }

    // 是否启用前置过滤
    const preFilterEnabled = await filter.settings.preFilterEnabled;

    // 前置过滤
    // 先直接隐藏,等过滤完毕后再放出来
    const beforeGet = (...args) => {
      if (preFilterEnabled) {
        // 主题标题
        const title = document.getElementById(args[1]);

        // 主题容器
        const container = title.closest("tr");

        // 隐藏元素
        container.style.display = "none";
      }

      return args;
    };

    // 过滤
    const afterGet = (_, args) => {
      // 主题 ID
      const tid = args[8];

      // 回复 ID
      const pid = args[9];

      // 找到对应数据
      const data = topicModule.data.find(
        (item) => item[8] === tid && item[9] === pid
      );

      // 开始过滤
      if (data) {
        filter.filterTopic(data);
      }
    };

    // 如果已经有数据,则直接过滤
    Object.values(topicModule.data).forEach((item) => {
      filter.filterTopic(item);
    });

    // 拦截 add 函数,这是泥潭的主题添加事件
    Tools.interceptProperty(topicModule, "add", {
      beforeGet,
      afterGet,
    });
  };

  /**
   * 处理 postArg 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui.postArg
   */
  const handleReplyModule = async (filter, value) => {
    // 绑定回复模块
    replyModule = value;

    if (value === undefined) {
      return;
    }

    // 是否启用前置过滤
    const preFilterEnabled = await filter.settings.preFilterEnabled;

    // 前置过滤
    // 先直接隐藏,等过滤完毕后再放出来
    const beforeGet = (...args) => {
      if (preFilterEnabled) {
        // 楼层号
        const index = args[0];

        // 判断是否是楼层
        const isFloor = typeof index === "number";

        // 评论额外标签
        const prefix = isFloor ? "" : "comment";

        // 用户容器
        const uInfoC = document.querySelector(`#${prefix}posterinfo${index}`);

        // 回复容器
        const container = isFloor
          ? uInfoC.closest("tr")
          : uInfoC.closest(".comment_c");

        // 隐藏元素
        container.style.display = "none";
      }

      return args;
    };

    // 过滤
    const afterGet = (_, args) => {
      // 楼层号
      const index = args[0];

      // 找到对应数据
      const data = replyModule.data[index];

      // 开始过滤
      if (data) {
        filter.filterReply(data);
      }
    };

    // 如果已经有数据,则直接过滤
    Object.values(replyModule.data).forEach((item) => {
      filter.filterReply(item);
    });

    // 拦截 proc 函数,这是泥潭的回复添加事件
    Tools.interceptProperty(replyModule, "proc", {
      beforeGet,
      afterGet,
    });
  };

  /**
   * 处理 notification 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui.notification
   */
  const handleNotificationModule = async (filter, value) => {
    // 绑定提醒模块
    notificationModule = value;

    if (value === undefined) {
      return;
    }

    // 是否启用提醒过滤
    const notificationFilterEnabled = await filter.settings
      .notificationFilterEnabled;

    if (notificationFilterEnabled === false) {
      return;
    }

    // 由于过滤需要异步进行,所以要重写泥潭的提醒弹窗展示事件,在此处理需要过滤的数据,并根据结果确认是否展示弹窗
    const { openBox } = value;

    // 重写泥潭的提醒弹窗展示事件
    notificationModule.openBox = async (...args) => {
      // 回复列表,小屏幕下也作为短消息和系统提醒,参考 js_notification.js 的 createBox
      const replyList = notificationModule._tab[0];

      // 筛选出有作者的条目
      const authorList = replyList.querySelectorAll("A[href^='/nuke.php']");

      // 依次过滤
      await Promise.all(
        [...authorList].map(async (item) => {
          // 找到提醒主容器
          const container = item.closest("SPAN[class^='bg']");

          // 找到回复链接
          const reply = container.querySelector("A[href^='/read.php?pid=']");

          // 找到主题链接
          const topic = container.querySelector("A[href^='/read.php?tid=']");

          // 不符合则跳过
          if (reply === null || topic === null) {
            return;
          }

          // 获取 UID 和 昵称
          const { uid, username } = (() => {
            const res = item.getAttribute("href").match(/uid=(\S+)/);

            if (res) {
              return {
                uid: parseInt(res[1], 10),
                username: item.innerText,
              };
            }

            return {};
          })();

          // 获取 PID
          const pid = (() => {
            const res = reply.getAttribute("href").match(/pid=(\S+)/);

            if (res) {
              return parseInt(res[1], 10);
            }

            return 0;
          })();

          // 获取 TID
          const tid = (() => {
            const res = topic.getAttribute("href").match(/tid=(\S+)/);

            if (res) {
              return parseInt(res[1], 10);
            }

            return 0;
          })();

          // 过滤
          await filter.filterNotification(container, {
            tid,
            pid,
            uid,
            username,
            subject: "",
          });
        })
      );

      // 判断过滤后是否还有可见的数据
      let visible = false;

      for (const tab in notificationModule._tab) {
        const items =
          notificationModule._tab[tab].querySelectorAll("SPAN[class^='bg']");

        const filtered = [...items].filter((item) => {
          return item.style.display !== "none";
        });

        if (filtered.length > 0) {
          visible = true;
          break;
        }

        notificationModule._tab[tab].style.display = "none";
      }

      // 显示弹窗
      if (visible) {
        openBox.apply(notificationModule, args);
      }
    };
  };

  /**
   * 处理样式
   * @param {Settings} settings 设置
   */
  const handleStyle = (settings) => {
    const afterSet = (value) => {
      if (value === undefined) {
        return;
      }

      // 更新样式
      Object.assign(THEMES["system"], {
        fontColor: value.gbg3,
        borderColor: value.gbg4,
        backgroundColor: value.gbg4,
      });

      // 读取设置
      const theme = settings.theme;

      // 读取样式
      const { fontColor, borderColor, backgroundColor } = (() => {
        if (Object.hasOwn(THEMES, theme)) {
          return THEMES[theme];
        }

        return THEMES["system"];
      })();

      // 更新样式
      Tools.addStyle(
        `
        .filter-table-wrapper {
            max-height: 80vh;
            overflow-y: auto;
        }
        .filter-table {
            margin: 0;
        }
        .filter-table th,
        .filter-table td {
            position: relative;
            white-space: nowrap;
        }
        .filter-table th {
            position: sticky;
            top: 2px;
            z-index: 1;
        }
        .filter-table input:not([type]), .filter-table input[type="text"] {
            margin: 0;
            box-sizing: border-box;
            height: 100%;
            width: 100%;
        }
        .filter-input-wrapper {
            position: absolute;
            top: 6px;
            right: 6px;
            bottom: 6px;
            left: 6px;
        }
        .filter-text-ellipsis {
            display: flex;
        }
        .filter-text-ellipsis > * {
            flex: 1;
            width: 1px;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .filter-button-group {
            margin: -.1em -.2em;
        }
        .filter-tags {
            margin: 2px -0.2em 0;
            text-align: left;
        }
        .filter-mask {
            margin: 1px;
            color: ${backgroundColor};
            background: ${backgroundColor};
        }
        .filter-mask:hover {
            color: ${backgroundColor};
        }
        .filter-mask * {
            background: ${backgroundColor};
        }
        .filter-mask-block {
            display: block;
            border: 1px solid ${borderColor};
            text-align: center;
        }
        .filter-mask-collapse {
            border: 1px solid ${borderColor};
            background: ${backgroundColor};
        }
        .filter-mask-hint {
            color: ${fontColor};
        }
        .filter-input-wrapper {
            position: absolute;
            top: 6px;
            right: 6px;
            bottom: 6px;
            left: 6px;
        }
      `,
        "s-filter"
      );
    };

    if (unsafeWindow.__COLOR) {
      afterSet(unsafeWindow.__COLOR);
      return;
    }

    Tools.interceptProperty(unsafeWindow, "__COLOR", {
      afterSet,
    });
  };

  /**
   * 处理 commonui 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui
   */
  const handleCommonui = (filter, value) => {
    // 绑定主模块
    commonui = value;

    // 拦截 mainMenu 模块,UI 需要在 init 后加载
    Tools.interceptProperty(commonui, "mainMenu", {
      afterSet: (value) => {
        Tools.interceptProperty(value, "init", {
          afterGet: () => {
            filter.ui.render();
          },
          afterSet: () => {
            filter.ui.render();
          },
        });
      },
    });

    // 拦截 topicArg 模块,这是泥潭的主题入口
    Tools.interceptProperty(commonui, "topicArg", {
      afterSet: (value) => {
        handleTopicModule(filter, value);
      },
    });

    // 拦截 postArg 模块,这是泥潭的回复入口
    Tools.interceptProperty(commonui, "postArg", {
      afterSet: (value) => {
        handleReplyModule(filter, value);
      },
    });

    // 拦截 notification 模块,这是泥潭的提醒入口
    Tools.interceptProperty(commonui, "notification", {
      afterSet: (value) => {
        handleNotificationModule(filter, value);
      },
    });
  };

  /**
   * 注册脚本菜单
   * @param {Settings} settings 设置
   */
  const registerMenu = async (settings) => {
    const enabled = await settings.preFilterEnabled;

    GM_registerMenuCommand(`前置过滤:${enabled ? "是" : "否"}`, () => {
      settings.preFilterEnabled = !enabled;
    });
  };

  // 主函数
  (async () => {
    // 初始化缓存和 API
    const { cache, api } = initCacheAndAPI();

    // 初始化设置
    const settings = new Settings(cache);

    // 读取设置
    await settings.load();

    // 初始化 UI
    const ui = new UI(settings, api);

    // 初始化过滤器
    const filter = new Filter(settings, api, ui);

    // 加载模块
    filter.initModules(
      SettingsModule,
      ListEnhancedModule,
      UserEnhancedModule,
      TagModule,
      KeywordModule,
      LocationModule,
      ForumOrSubsetModule,
      HunterModule,
      MiscModule
    );

    // 注册脚本菜单
    registerMenu(settings);

    // 处理样式
    handleStyle(settings);

    // 处理 commonui 模块
    if (unsafeWindow.commonui) {
      handleCommonui(filter, unsafeWindow.commonui);
      return;
    }

    Tools.interceptProperty(unsafeWindow, "commonui", {
      afterSet: (value) => {
        handleCommonui(filter, value);
      },
    });
  })();
})();