班固米-条目职位自定义排序与折叠

对[动画]条目的制作人员信息进行职位的自定义排序与折叠,可在[设置-隐私]页面进行相关设置

目前为 2024-11-24 提交的版本。查看 最新版本

// ==UserScript==
// @name         班固米-条目职位自定义排序与折叠
// @namespace    https://github.com/weiduhuo/scripts
// @version      1.2.2-1.1
// @description  对[动画]条目的制作人员信息进行职位的自定义排序与折叠,可在[设置-隐私]页面进行相关设置
// @author       weiduhuo
// @match        *://bgm.tv/subject/*
// @match        *://bgm.tv/settings/privacy*
// @match        *://bangumi.tv/subject/*
// @match        *://bangumi.tv/settings/privacy*
// @match        *://chii.in/subject/*
// @match        *://chii.in/settings/privacy*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';
  const SCRIPT_NAME = '班固米-职位排序组件';
  // 图标
  const ICON = {
    // 三角形顶点向右,可表展开按键
    TRIANGLE_RIGHT: `
      <svg xmlns='http://www.w3.org/2000/svg' viewbox='0 0 13 13' height=' 0.7em'>
        <polygon points='0.5,0 12.5,6.5 0.5,13'  fill='currentColor' />
      </svg>
    `,
    // 三角形顶点向下,可表折叠按键
    TRIANGLE_DOWN: `
      <svg xmlns='http://www.w3.org/2000/svg' viewbox='0 0 13 13' height=' 0.7em'>
        <polygon points='0,0.5 13,0.5 6.5,12.5'  fill='currentColor' />
      </svg>
    `,
    // 三角形顶点向上,可表折叠按键
    TRIANGLE_UP: `
      <svg xmlns='http://www.w3.org/2000/svg' viewbox='0 0 13 13' height=' 0.7em'>
        <polygon points='0,12.5 13,12.5 6.5,0.5'  fill='currentColor' />
      </svg>
    `,
  };

  // 条目类型
  const SubjectType = {
    // 所支持的类型
    ANIME: 'anime',
    // 待支持的类型
    // BOOK: 'book', MUSIC: 'music', GAME: 'game', REAL: 'real', CHARACTER: 'character', PERSON: 'person',
    getAll() {
      return Object.values(this);
    },
    prase(value) {
      if (this.getAll().includes(value)) return value;
      return null;
    },
    // needPrase(value) {
    //   return value !== this.CHARACTER && value !== this.PERSON;
    // },
  };

  /**
   * 职位的排序列表`jobOrder`与默认折叠的职位`foldableJobs`的合并信息
   * 基本类型:`
      type = [Job | [boolean | Job, ...Job[]]]
      Job = string | RegExp
   * `其中`boolean`表示子序列内的职位是否默认折叠,缺损值为`False`,需位于子序列的首位才有意义
   * (下文`,,`表示在`JSON`数组中插入`null`元素,用于输出格式化文本时标记换行)
   */
  const staffMapList = [,
    "中文名", "类型", "适合年龄", /地区/, "语言", "对白", "话数", "总话数", [true, "季数"], ,
    "放送开始", "开始", "放送星期", "放送时间", "上映年度", /上映/, "发售日", "片长", /片长/, ,
    ,
    "原作", "原案", "人物原案", "原作插图", [true, "原作协力"], ,
    "团长", "总导演", "导演", "副导演", "执行导演", "主任导演", "联合导演", "系列监督", ,
    "系列构成", "脚本", "编剧", [true, /脚本|内容|故事|文艺|主笔/], ,
    "分镜", "OP・ED 分镜", "主演出", "演出", [true, "演出助理"], ,
    "人物设定", ,
    ,
    "总作画监督", [false, "作画监督"], [true, "作画监督助理"], "动作作画监督", "机械作画监督", "特效作画监督", /.*作画.*(监|导)/, ,
    "主动画师", "主要动画师", [true, "构图"], [false, "原画"], [true, "第二原画", "补间动画"], "数码绘图", /(原画|动画|動画)(?!制|检查)/, ,
    "动画检查", [true, /动画检查/], ,
    ,
    "设定", "背景设定", "道具设计", /(?<!色彩|美术|美術)(设|設)(?!.*制)/, /Design|デザイン|Motion Graphic|モーショングラフィック/, ,
    "色彩设计", [false, "色彩指定", "色指定"], [true, "上色", /(?<!角)色/], ,
    "美术监督", /(美术|美術).*导演/, [false, "美术设计"], "概念美术", "视觉概念", "概念艺术", ,
    [false, "背景美术"], [true, /景/], /ART WORK|美术|美術|艺术|工艺|创意|绘制/, ,
    ,
    /CG.*导演/, "3DCG", [false, /2D|3D|CG|コンピュータ/], "模型监督", "绑骨监督", [/建模|模型|动作|表情|骨/], ,
    "摄影监督", "副摄影监督", "后期监督", [true, "摄影", "特效", "照明", "特技", /摄影|モニター|特效|动效|合成|拍|Effect|技术/], ,
    "現像", /タイトル|标题|字幕/, ,
    [false, "剪辑", "编集"], [true, /(?<!音.*)剪辑/], "编辑", [true, "场记"], ,
    "监修", /监修|監修/, "顾问", /顾问/, ,
    ,
    "音响监督", [true, "音响", "音响制作", "音效", "拟音", /音响/], "录音", [true, "录音助理", "混声", /录音|声/], ,
    "配音", "主演", "キャスティング", [true, /配音|((?<!歌)演出)/], ,
    "音乐", "音乐制作", [true, "音乐制作人", "音乐助理"], [true, /音/], ,
    "主题歌演出", [true, "主题歌作词", "主题歌作曲", "主题歌编曲"], "插入歌演出", ,
    [true, "插入歌作词", "插入歌作曲", "插入歌编曲"], [true, "选曲"], [true, /曲|歌/], ,
    ,
    "企画", [true, "企画协力"], "企划制作人", /企画|企划|出品|策划/, "监制", /监制/, ,
    "执行制片人", "总制片人", "制片人", "总制片", "制片", [true, "副制片人", "联合制片人", "助理制片人", /(?<!动画|動画)制片/], ,
    [true, /行政|审/, "责任编辑"], [true, /法务/], [true, "宣传", /宣传|宣伝|広報/], /市场|运营|衍生/, ,
    "制作", "製作", [true, "制作著作"], ,
    "动画制片人", [true, "制作管理", "制作统筹", "制作主任", "制作助理"], ,
    [true, "设定制作"], [true, "计划管理", "制作进行", "制作进行协力"], ,
    "制作协调", "制作协力", "制作助手", "协力", /协力|協力/, [true, /取材/], [true, "特别鸣谢", /鸣谢|Thanks/], ,
    ,
    "动画制作", [true, /制作|製作/], ,
    "别名", /.+名$/, ,
    "发行", "官方网站", "在线播放平台", "链接", "播放电视台", "其他电视台", "配给", /配(?!音)|連載|番組|版|播放(?!结束)/, ,
    "播放结束", "结束", ,
    ,
    "其他", /其他/, ,
    "===此处插入未被匹配的职位===", ,
    "Copyright",
  ];
  // 职位的排序列表
  const jobOrder = [];
  // 默认折叠的职位
  const foldableJobs = [];
  // 默认值的格式化文本的缓存
  let mapListTextBuffer = null;
  // 超过此行数的职位信息将被二次折叠 (现为固定值,在之后的版本用户可以自定义)
  let maxRefoldLines = 4;

  // 是否对职位信息进行了折叠 (依此判断 `更多制作人员` 开关的必要性)
  let hasFolded = false;
  // 职位信息的行距
  let jobLineHeight = null;

  const pathname = window.location.pathname;

  // 匹配相应 URL 类型
  const urlPatterns = [
    { type: 'subject', regex: /^\/subject\/\d+$/, handler: handlerSubject },
    // { type: 'character', regex: /^\/character\/\d+$/, handler: trySortStaff },
    // { type: 'person', regex: /^\/person\/\d+$/, handler: trySortStaff },
    { type: 'settings', regex: /^\/settings\/privacy$/, handler: handlerSettings },
  ];
  function main() {
    for (const pattern of urlPatterns) {
      if (pattern.regex.test(pathname)) {
        pattern.handler(pattern.type);
        break;
      }
    }
  }

  function handlerSettings() {
    const ui = buildSettingUI({ id: 'staff_sorting' });
    document.getElementById('columnA').appendChild(ui);
    loadSettingStyle();
    // 支持 url.hash = ID 进行导引
    if (location.hash.slice(1) === 'staff_sorting') {
      ui.scrollIntoView({ behavior: 'smooth' });
    }
  }

  function handlerSubject(subType) {
    // if (needPrase(subType))
    subType = SubjectType.prase(getSubjectType());
    if (!subType) return; // 不支持该类型条目
    loadMapList();
    const ul = document.querySelector('#infobox');
    if (staffMapList.length) {
      sortStaff(ul);
    } else {
      addFoldableTag(ul);
      console.log(`${SCRIPT_NAME}:staffMapList 设置空缺,网页实行原有的职位顺序与折叠`);
    }
    loadStaffStyle();
    changeExpandToToggleButton(ul);
    addRefoldToggleButton(ul);
  }

  /**
   * 巧妙地使用非常便捷的方法,获取当前条目的类型
   * 源自 https://bangumi.tv/dev/app/2723/gadget/1242
   * 替代了下方的原有方法
   */
  function getSubjectType() {
    const href = document.querySelector("#navMenuNeue .focus").getAttribute("href");
    return href.split("/")[1];
  }

  function sortStaff(ul) {
    // 职位信息字典
    const staffDict = getStaffDict(ul);
    // 清空原始的`staff`列表
    ul.innerHTML = '';

    // 未能匹配职位的待插入位置
    let liAfterIntsert = null;
    let insterTag = false;
    let insertFold = false;

    // 按照预定顺序添加到 DOM
    jobOrder.forEach(item => {
      const matchingRoles = [];
      // 1.正则匹配
      if (item instanceof RegExp) {
        matchingRoles.push(...Object.keys(staffDict).filter(key => item.test(key)));
        if (matchingRoles.length) {
          console.log(`${SCRIPT_NAME}:使用正则表达式 "${item}" 成功匹配 \{${matchingRoles}\}`);
        } else return;
      } else if (typeof item === 'string') {
        // 2.键值匹配
        if (item && item in staffDict) {
          matchingRoles.push(item);
        // 3.特殊关键字处理
        } else if (item.startsWith('==')) {
          // 激活待插入位置
          insterTag = true;
          insertFold = foldableJobs.includes(item);
        } else return
      // 4.其余情形均忽略(且对于意外类型不报错)
      } else return;

      // 添加职位,并判断是否默认折叠
      matchingRoles.forEach(role => {
        const li = staffDict[role];
        if (typeof item === 'string' && foldableJobs.includes(role)
          || item instanceof RegExp && foldableJobs.includes(item)) {
          li.classList.add('folded', 'foldable');
          if (!hasFolded) hasFolded = true;
        }
        ul.appendChild(li);
        delete staffDict[role]; // 从字典中删除已处理的职位
        // 保存待插入位置
        if (insterTag) {
          liAfterIntsert = li;
          insterTag = false;
        }
      });
    });

    // 将剩余未被匹配的职位按原顺序添加到待插入位置
    const unmatchedJobs = Object.keys(staffDict);
    if (unmatchedJobs.length === 0) {
      return;
    }
    unmatchedJobs.forEach(role => {
      const li = document.createElement('li');
      li.innerHTML = staffDict[role];
      if (insertFold) li.classList.add('folded', 'foldable');
      if (liAfterIntsert) ul.insertBefore(li, liAfterIntsert);
      // 未设置待插入位置,则默认插入到末尾,且默认不折叠
      else ul.appendChild(li);
    });
    console.log(`${SCRIPT_NAME}:未能匹配到的职位 ${JSON.stringify(staffDict, null, 2)}`);
    if (liAfterIntsert) console.log(`${SCRIPT_NAME}:激活将未能匹配职位插入指定位置`);
  }

  /**
   * 获取一个字典来存储网页中的职位信息,
   * 并对职位信息进行二次折叠
   */
  function getStaffDict(ul) {
    const staffDict = {};
    const lis = ul.querySelectorAll(':scope > li');
    lis.forEach(li => {
      const tip = li.querySelector('span.tip');
      if (tip) {
        const role = tip.innerText.trim().slice(0, -1); // 去掉最后的冒号
        // 为了正确计算元素高度,需使其 display
        li.classList.remove('folded');
        refoldStaff(li, tip);
        staffDict[role] = li;
        // li.folded 属性已经失效无需还原
      }
    });
    return staffDict;
  }

  /**
   * 为网页原有的`folded`类别添加`foldable`便签,用于实现切换,
   * 并对职位信息进行二次折叠
   */
  function addFoldableTag(ul) {
    const lis = ul.querySelectorAll(':scope > li');
    lis.forEach(li => {
      let flag = li.classList.contains('folded');
      if (flag) {
        if (!hasFolded) hasFolded = true;
        // 为了正确计算元素高度,需先使其 display
        li.classList.remove('folded');
      }
      const tip = li.querySelector('span.tip');
      if (tip) refoldStaff(li, tip);
      if (flag) li.classList.add('folded', 'foldable');
    });
  }

  /**
   * 对超出限制行数的职位信息进行二次折叠,并添加开关。
   * 实现类似于`summary`但是动态摘要的功能。
   * 过滤`别名`等不定行高的`infobox`信息
   */
  function refoldStaff(li, tip) {
    if (li.classList.contains('sub_container')) return; // 不定行高的 infobox 信息
    const lineCnt = getLineCnt(li);
    if (lineCnt <= maxRefoldLines) return;
    // 添加二次折叠效果 (样式将在随后通过 loadStaffStyle 动态载入)
    nestElementWithChildren(li, 'div', {class: 'refoldable refolded'});
    // 尝试不修改 DOM 结构仅通过样式添加折叠效果,但未果,故改为内嵌一层新元素

    // 添加开关状态图标
    const icon = createElement('i');
    icon.innerHTML = ICON.TRIANGLE_RIGHT;
    /* 尝试使用<symbol><use>模板或直接使用JS构建实例的方法均失败...
     * 最终改为直接修改innerHTML */ 
    updateSubElements(tip, icon, 'prepend');
    tip.classList.add('switch');
  }

  /**
   * 为二次折叠按钮绑定开关事件,
   * 采用`事件委托`形式绑定事件 (事件冒泡机制)
   */
  function addRefoldToggleButton(ul) {
    ul.addEventListener('click', (event) => {
      /* 检查点击的元素是否是开关本身或其子元素
       * 使用 .closest('.switch') 替代 classList.contains('switch')
       * 使得子元素也能响应点击事件 */
      const tip = event.target.closest('.switch');
      if (!tip || !ul.contains(tip)) return;
      // 职位名称或开关状态图标被点击了
      const parent = tip.parentElement;
      if (parent.classList.contains('refolded')) {
        parent.classList.remove('refolded')
        tip.firstChild.innerHTML = ICON.TRIANGLE_DOWN;
      } else {
        parent.classList.add('refolded')
        tip.firstChild.innerHTML = ICON.TRIANGLE_RIGHT;
      }
    });
    /* 在 mousedown 阶段阻止用户拖动或双击时的默认选中行为。
     * 由于 span.switch 本质仍然是内容段落的一部分,
     * 不通过 user-select: none 这钟粗暴的方法禁止用户的一切选中行为
     * 而是采用温和的方法阻止部分情形下对该区域的选中行为 */
    ul.addEventListener('mousedown', (event) => {
      if (event.target.closest('.switch')) event.preventDefault();
    });
  }

  /**
   * 获取固定行高元素显示的行数
   * 经测试,职员信息除了`8px`的`padding`还有`0.03555px`的`border`因为不影响行数计算忽略
   */
  function getLineCnt(el, padding = 8, border = 0) {
    jobLineHeight ??= getLineHeight(el);
    const height = el.getBoundingClientRect().height - padding - border;
    return ~~(height / jobLineHeight);
  }
  
  /**
   * 将原本存在的`更多制作人员+`一次性按钮,转绑新事件,并改为永久性开关
   * 使用网页原有的`folded`元素类别,实现对立于`sortStaff`功能
   * 添加不存在的`更多制作人员+`按钮,否则一些职位信息将永不可见
    <div class="infobox_expand">
      <a href="javascript:void(0)">更多制作人员 +</a>
      <!-- href 属性需保留,不然鼠标悬浮效果会失效 -->
    </div>
   */
  function changeExpandToToggleButton(ul) {
    const buttonValue = { on: '更多制作人员 +', off: '更多制作人员 -' };
    let moreLink = document.querySelector('#infobox + .infobox_expand a'); // 无法实现 :scope +
    if (!hasFolded) {
      // 无必要,不进行事件绑定与可能的添加,并将原有的开关隐藏
      if (moreLink) {
        moreLink.style.display = 'none';
        console.log(`${SCRIPT_NAME} - 将原有的 '${buttonValue.on}' 隐藏`);
      }
      return;
    }
    if (!moreLink) {
      moreLink = createElement('a', { href: 'javascript:void(0)' }, buttonValue.on);
      const expand = createElement('div', { class: 'infobox_expand' }, [moreLink]);
      ul.parentElement.appendChild(expand);
      console.log(`${SCRIPT_NAME}:添加原不存在的 '${buttonValue.on}' 按钮`);
    }
    moreLink.addEventListener('click', function (event) {
      event.stopImmediatePropagation(); // 阻止其他事件的触发
      const foldedLis = document.querySelectorAll('.foldable');
      const isHidden = moreLink.innerText == buttonValue.on;
      foldedLis.forEach(li => {
        if (isHidden) {
          li.classList.remove('folded');
        } else {
          li.classList.add('folded');
        }
      });
      moreLink.innerText = isHidden ? buttonValue.off : buttonValue.on;
    }, { capture: true }); // 使事件处理函数在捕获阶段运行
  }

  /**
   * 创建用户设置`UI`界面
   * 仿照`#columnA`中的同类元素进行构建,使用原有的结构与样式
    <table class="settings">
      <tbody>
        <tr>
          <td>
            <h2 class="subtitle">条目职位排序 · 默认折叠的职位</h2>
          </td>
        </tr>
        <!-- 此处添加子模块 -->
      </tbody>
    </table>
   */
  function buildSettingUI(mainStyle) {
    const mainTitle = createElement('tr', null, [
      createElement('td', null, [
        createElement('h2', { class: 'subtitle' }, '条目职位排序 · 默认折叠的职位')
      ])
    ]);
    // const lineLimitBlock = buildLineLimitBlock();
    const animeBlock = buildAnimeBlock();
    const ui = createElement('div', mainStyle, [
      createElement('table',
        { class: 'settings' }, [
        createElement('tbody', null, [
          mainTitle,
          // lineLimitBlock, // 未完成开发,暂不启用
          animeBlock,
          // 可拓展其他类型条目的模块
        ])
      ])
    ]);
    return ui;
  }

  /**
   * 创建职位信息二次折叠的行高限制设置界面
   */
  function buildLineLimitBlock() {
    const subTitle = createElement('h2', { class: 'subtitle' }, '职位信息高度 限制');
    // 搭建滑动开关
    const [toggle, toggleCntr] = buildToggleSlider('refold_switch');
    // 搭建数字输入框与控制器,不使用 input.type = 'number' 而是自我搭建相关控制
    const label = createElement('span', { class: 'text'}, '行数');
    const inputNum = createElement('input', { class: 'inputtext input_num', type: 'text', maxlength: '2' });
    const incBtn = createElement('div', { name: 'inc_btn' });
    const decBtn = createElement('div', { name: 'dec_btn' });
    // 搭建外部框架
    const numInputCntr = createElement('fieldset', { class: 'num_input_cntr' },[
      label, inputNum,
      createElement('div', { class: 'num_ctrs' }, [incBtn, decBtn])
    ]);
    const block = createElement('tr', null, [
      createElement('td', { class: 'line_limit_block' }, [
        subTitle, createElement('div', {class: 'right_inline'}, [numInputCntr, toggleCntr])
      ])
    ]);

    // 初始化
    const minNum = {int: 1, str: '1'};
    toggle.checked = true;
    inputNum.value = '4';
    incBtn.innerHTML = ICON.TRIANGLE_UP;
    decBtn.innerHTML = ICON.TRIANGLE_DOWN;

    // 绑定事件
    toggle.addEventListener('click', () => {
      if (toggle.checked) {
        numInputCntr.style.display = 'flex';
      } else {
        numInputCntr.style.display = 'none';
      }
    });
    // 限制输入为正整数
    inputNum.addEventListener('input', () => {
      let value = inputNum.value.replace(/[^0-9]/g, '');
      if (value === '' || parseInt(value) === 0) value = minNum.str;
      inputNum.value = value;
    });
    // 限制键盘输入行为,禁止非数字键输入
    inputNum.addEventListener('keydown', (event) => {
      if (!/^[0-9]$/.test(event.key) && event.key !== 'Backspace'
        && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') event.preventDefault();
    });
    // 增加加减按键功能
    incBtn.addEventListener('click', () => {
      let value = parseInt(inputNum.value) || minNum.int;
      inputNum.value = value + minNum.int;
    });
    decBtn.addEventListener('click', () => {
      let value = parseInt(inputNum.value) || minNum.int;
      if (value > minNum.int) inputNum.value = value - minNum.int;
    });

    return block;
  }

  /**
   * 创建`staffMapList`文本内容编辑界面
   * 对于`textarea`,`button`等控件仍然使用原有的结构与样式
  <tr>
    <td class="subject_staff_block">
      <h2 class="subtitle"><!-- subject type --></h2>
      <div class="staffMapList_editor">
        <div class="markItUp">
          <textarea class="quick markItUpEditor hasEditor codeHighlight" name="staff_map_list">
            <!-- staffMapListText -->
          </textarea>
        </div>
        <div>
          <input class="inputBtn" type="submit" name="submit_context" value="保存">
          <input class="inputBtn" type="submit" name="reset_context" value="恢复默认">
          <p class="tip_j" style="display: inline;"><!-- message --></p>
        </div>
        <!-- margin-right 为移动端预留的 mainpage 滑动空间 -->
      </div>
    </td>
  </tr>
  */
  function buildAnimeBlock() {
    // 搭建标题
    const subTitle = createElement('h2', { class: 'subtitle' });
    // 搭建滑动开关
    // const selector = new TriStateSlider('anime_staff_sort');
    // selector.build();
    // 搭建文本框
    const textArea = createElement('textarea', {
      class: 'quick markItUpEditor hasEditor codeHighlight', name: 'staff_map_list'
    });
    // 搭建提交按钮
    const submitBtn = createElement('input', {
      class: 'inputBtn', type: 'submit', name: 'submit_context', value: '保存'
    });
    // 搭建重置按钮
    const resetBtn = createElement('input', {
      class: 'inputBtn', type: 'submit', name: 'reset_context', value: '恢复默认'
    });
    // 搭建简易提示框
    const editorMsgBox = createElement('p', { class: 'tip_j'});
    const selectorMsgBox = createElement('p', { class: 'tip_j' });
    // 搭建外部结构
    const block = createElement('tr', null, [
      createElement('td', {class: 'subject_staff_block'}, [
        subTitle,
        // createElement('div', {class: 'right_inline'}, [selectorMsgBox, selector.root]), // 未完成开发,暂不启用
        // 可拓展折叠效果
        createElement('div', { class: 'staffMapList_editor'}, [
          createElement('div', { class: 'markItUp' }, textArea),
          createElement('div', null, [submitBtn, resetBtn, editorMsgBox])
        ])
      ])
    ]);
    function setToggleMsgBox(state) {
      switch (state) {
        case '1':
          setMessage(selectorMsgBox, '禁用设置,但仍可编辑保存');
          break;
        case '2':
          setMessage(selectorMsgBox, '仅启用排序,禁用折叠');
          break;
        case '3':
          setMessage(selectorMsgBox, '启用自定义 / 默认设置');
          break;
      }
    }

    // 初始化
    let hasInputted = false;
    let {text, isDefault} = getMapListText(false);
    subTitle.textContent = '动画条目';
    textArea.textContent = text;
    // selector.state = '3';
    // setToggleMsgBox(selector.state);
    if (isDefault) setMessage(editorMsgBox, '现为默认设置'); // 初始化时,提醒用户已为默认设置
    if (text.trim() === "") setMessage(editorMsgBox, '现为设置空缺'); // 网页实行原有的职位顺序与折叠

    // 绑定事件
    // selector.onStateChange = (newState) => {
    //   setToggleMsgBox(newState);
    // };
    textArea.addEventListener('input', () => {
      if (!hasInputted) hasInputted = true;
      if (isDefault) isDefault = false;
      // console.log("IS INPUTTING");
    });
    resetBtn.addEventListener('click', async () => {
      if (isDefault) return setMessage(editorMsgBox, '已为默认内容');
      await trySetText(textArea, editorMsgBox, getMapListText(true).text, '已恢复默认内容', false);
      // 需进行同步等待,由于 setText 可能会触发 input 事件
      isDefault = true;
      hasInputted = false;
    })
    submitBtn.addEventListener('click', () => {
      // 判断是否为重置后未对默认内容进行修改
      if (isDefault && !hasInputted) {
        resetMapList();
        setMessage(editorMsgBox, '保存成功!恢复默认设置');
        // 恢复初始状态
        hasInputted = false;
        return;
      }
      const [modifiedData, isModified, curCursorPos] = modifyMapListJSON(textArea);
      // 强制将用户输入的文本外层嵌套 `[]`,若为重复嵌套可在 loadMapList 中识别并去除
      const savedDate = `[${modifiedData}]`;
      const parsedData = parseMapListJSON(savedDate);
      if (parsedData) {
        // 保存数据
        saveMapListText(savedDate);
        // 页面显示
        if (modifiedData.trim() === "") setMessage(editorMsgBox, '保存成功!空缺设置');
        else if (isModified) trySetText(textArea, editorMsgBox, modifiedData, '保存成功!并自动纠错', true, curCursorPos);
        else setMessage(editorMsgBox, '保存成功!');
      } else setMessage(editorMsgBox, '保存失败!格式存在错误');
      // 恢复初始状态
      hasInputted = false;
    });

    return block;
  }

  /**
   * 三态滑动选择器
   */
  class TriStateSlider {
    // 可选状态
    static states = ['1', '2', '3'];
    // 所用样式的类名
    static _selectorCls = 'tri_state_selector';
    static _radioCls = 'radio_input';
    static _labelCls = 'radio_label';
    static _sliderCls = 'select_slider';
    static _indicatorCls = 'select_indicator';
    /**
     * @type {(newState: string) => void | null} 
     * 回调函数,当状态变化时被调用
     */
    onStateChange = null;

    /**
     * 构造函数
     * @param {string} idPref - 选择器的`ID`前缀
     * @param {'1'|'2'|'3'} [initState='1'] - 初始状态
     */
    constructor(idPref, initState = '1') {
      this.root = createElement('div', { class: 'tri_state_selector' });
      this.radios = {};
      this.idPref = idPref;
      this.initState = initState;
      this._stateHis = {pre: this.initState, pre2: this.initState};
      this._initStateHis();
    }

    /**
     * 设置选择器状态
     * @param {'1'|'2'|'3'} state - 状态
     */
    set state(state) {
      this.initState = state;
      this._initStateHis(state);
      this.radios[state].checked = true;
    }

    /**
     * 获取选择器当前的状态
     * @returns {'1'|'2'|'3'}  当前状态
     */
    get state() {
      for (const [state, radio] of Object.entries(this.radios)) {
        if (radio.checked) return state;
      }
      return this.initState;
    }

    /**
     * 构造`DOM`树,并绑定事件
     */
    build() {
      // 构建单选格,radio 本体将通过样式隐藏
      TriStateSlider.states.forEach((state) => {
        const radioId = `${this.idPref}_${state}`;
        const radio = createElement('input', {
          type: 'radio', name: `${this.idPref}_group`, id: radioId,
          value: state, class: TriStateSlider._radioCls
        });
        const label = createElement('label', { htmlFor: radioId, class: 'radio_label' });
        this.radios[state] = radio;
        this.root.append(radio, label);
      });
      // 构建滑动外观
      this.root.append(
        createElement('div', { class: 'select_slider' },
          createElement('div', { class: 'select_indicator' })
        ));
      // 初始化状态并绑定事件
      this.radios[this.initState].checked = true;
      // 1) 箭头函数每次事件触发时,都会创建一个新的匿名函数,影响性能
      // this.selector.addEventListener('click', (event) => this._onClick(event));
      // 2) 事件监听器的回调函数本身会改变 this,使得它从指向类的实例对象,变为指向事件触发的元素
      // this.selector.addEventListener('click', this._onClick);
      // 3) 使用绑定后的函数
      this.root.addEventListener('click', this._onClick.bind(this));
    }

    _initStateHis() {
      this._stateHis.pre = this.initState;
      this._stateHis.pre2 = this.initState === TriStateSlider.states[1]
        ? TriStateSlider.states[2] : TriStateSlider.states[1]; // [[1,3] 2]->[2 3]
    }

    /**
     * 采用事件委托的形式处理点击事件,
     * 将原本的`radio`操作体验处理为`ToggleSlider`手感
     */
    _onClick(event) {
      if (!event.target.classList.contains('radio_input')) return;
      let curState = event.target.value;
      // 现在与过去互异,正常不处理;现在与过去的过去互异,模拟 Toggle
      if (curState === this._stateHis.pre && curState !== this._stateHis.pre2) {
        this.radios[this._stateHis.pre2].checked = true;
        curState = this._stateHis.pre2;
      }
      this._stateHis.pre2 = this._stateHis.pre;
      this._stateHis.pre = curState;
      // 使用回调函数通知外部
      if (this.onStateChange) this.onStateChange(curState);
    }
  }

  /**
   * 创建一个滑动开关
   * @param {string} sliderId - 开关的`ID`
   * @returns {[HTMLElement, HTMLElement]} 返回`开关`与`开关容器`构成的数组
   */
  function buildToggleSlider(sliderId) {
    const toggle = createElement('input', { class: 'toggle_input', type: 'checkbox', id: sliderId });
    const toggleCntr = createElement('div', { class: 'toggle' },
      [toggle, createElement('label', { class: 'toggle_slider', htmlFor: sliderId })]
    );
    return [toggle, toggleCntr];
  }

  /**
   * 优先尝试使用`execCommand`方法改写文本框,使得改写前的用户历史记录不被浏览器清除
   * (虽然`execCommand`方法已被弃用...但仍然是实现该功能最便捷的途径)
   */
  async function trySetText(textArea, msgBox, text, msg, isRestore, setCursorPos, transTime = 100) {
    let {scrollVert, cursorPos} = getTextAreaPos(textArea);
    try {
      setMessage(msgBox);
      await clearAndSetTextarea(textArea, text, transTime);
      setMessage(msgBox, `${msg},可快捷键撤销`, 0);
    } catch (e) {
      textArea.value = '';
      await new Promise(resolve => setTimeout(resolve, transTime));
      textArea.value = text;
      setMessage(msgBox, msg, 0);
      console.log(`${SCRIPT_NAME}:浏览器不支持 execCommand 方法,改为直接重置文本框,将无法通过快捷键撤销重置`)
    }
    if (isRestore) {
      setCursorPos ??= cursorPos; // 可以使用外部计算获取的光标位置
      restorePos();
    }

    /**
     * 恢复滚动位置和光标位置
     */
    function restorePos() {
      const currentTextLen = textArea.value.length;
      if (setCursorPos > currentTextLen) setCursorPos = currentTextLen;
      textArea.scrollTop = Math.min(scrollVert, textArea.scrollHeight);
      // textArea.scrollLeft = Math.min(scrollHoriz, textArea.scrollWidth - textArea.clientWidth);
      textArea.setSelectionRange(setCursorPos, setCursorPos);
    }
  }

  /**
   * 获取文本框的滚动位置和光标位置
   */
  function getTextAreaPos(textArea) {
    return {
      scrollVert: textArea.scrollTop,
      scrollHoriz: textArea.scrollLeft,
      cursorPos: textArea.selectionStart
    };
  }

  async function clearAndSetTextarea(textarea, newText, timeout = 100) {
    textarea.focus();
    // 全选文本框内容并删除
    textarea.setSelectionRange(0, textarea.value.length);
    document.execCommand('delete');
    // 延迟一段时间后,插入新的内容
    await new Promise(resolve => setTimeout(resolve, timeout));
    document.execCommand('insertText', false, newText);
  }

  async function setMessage(container, message, timeout = 100) {
    container.style.display = 'none';
    if (!message) return; // 无信息输入,则隐藏
    // 隐藏一段时间后,展现新内容
    if (timeout) await new Promise(resolve => setTimeout(resolve, timeout));
    container.textContent = message;
    container.style.display = 'inline';
  }

  /**
   * 载入`StaffMapList`数据,并对其作最终解析
   */
  function loadMapList() {
    // 读取可能的非默认设置
    let jsonString = localStorage.getItem('BangumiStaffSorting_animeStaffMapList');
    if (jsonString) {
      let parsedData = parseMapListJSON(jsonString);
      if (parsedData) {
        // 修复外层重复嵌套 `[]` 的形式 (忽略存在的漏洞,形如:[[true, ["a"], "b"]] )
        if (parsedData.length === 1 && Array.isArray(parsedData[0]) && typeof parsedData[0][0] !== 'boolean') {
          parsedData = parsedData[0];
        }
        staffMapList.length = 0;
        staffMapList.push(...parsedData);
      } else console.log(`${SCRIPT_NAME}:自定义 staffMapList 解析失败,将使用脚本默认的数据`);
    }
    // 将数据拆解为 jobOrder 与 foldableJobs
    staffMapList.forEach(item => {
      if (Array.isArray(item) && item.length) {
        // 对数组进行完全展平,提高对非标多层数组的兼容性
        item = item.flat(Infinity);
        // 对于标准格式,仅当 Boolean 为一级子序列的首元素时,对该子序列的全部元素生效
        // 此时更广义的表述为,仅当 Boolean 为一级子序列的最左节点时,对该子序列的全部元素生效
        if (typeof item[0] === 'boolean') {
          if (item[0]) foldableJobs.push(...item.slice(1));
          jobOrder.push(...item.slice(1));
        } else {
          jobOrder.push(...item);
        }
      } else if (typeof item !== 'undefined') {
        jobOrder.push(item);
      }
    });
  }

  function resetMapList() {
    localStorage.removeItem('BangumiStaffSorting_animeStaffMapList');
    console.log(`${SCRIPT_NAME}:删除自定义 staffMapList 数据,恢复默认设置`)
  }

  function saveMapListText(jsonStr) {
    localStorage.setItem('BangumiStaffSorting_animeStaffMapList', jsonStr);
    console.log(jsonStr);
    console.log(`${SCRIPT_NAME}:保存自定义 staffMapList 数据`);
  }

  function getMapListText(useDefault) {
    let jsonStr = null;
    if (!useDefault) {
      jsonStr = localStorage.getItem('BangumiStaffSorting_animeStaffMapList');
    }
    const isDefault = jsonStr === null;
    if (jsonStr) {
      jsonStr = jsonStr.slice(1, -1); // 消除首尾的 `[]`
    } else if (mapListTextBuffer) {
      jsonStr = mapListTextBuffer;
    } else {
      // 将默认数据转化为格式化文本
      jsonStr = JSON.stringify(staffMapList, regexReplacer, 1).replace(
        /(null,\n )|(\n\s+)/g, (match, g1, g2) => {
          if (g1) return '\n';
          if (g2) return ' ';
          return match;
        }).slice(3, -2); // 消除首部 `[ \n` 与尾部 `\n]`
      // 使得 `[ `->`[` 同时 ` ]`->`]`
      // jsonStr = JSON.stringify(staffMapList, regexReplacer, 1).replace(
      //   /(null,)|(?<!\[)(\n\s+)(?!])|(\[\s+)|(\s+],)/g, (match, g1, g2, g3, g4) => {
      //   if (g1) return '\n';
      //   if (g2) return ' ';
      //   if (g3) return '[';
      //   if (g4) return '],';
      //   return match;
      // }).slice(3, -2);
      mapListTextBuffer = jsonStr;
    }
    return {text: jsonStr, isDefault: isDefault};
  }

  /**
   * 对用户输入可能的常见语法与格式错误,进行自动纠错,以满足`JSON`格式
   * 并计算文本修改后,光标的适宜位置
   * 已基本兼容`JavaScript`格式的文本数据,实现格式转化
   * `group2`与`group4`致使正则表达式中不允许出现`/'"`三种字符
   */
  function modifyMapListJSON(textArea) {
    const preCursorPos = getTextAreaPos(textArea).cursorPos;
    let curCursorPos = preCursorPos;
    let flags = new Array(5).fill(false);
    const rslt = textArea.value.replace(
      /(,\s*(?=]|$))|(,\s*)+(?=,)|(')|(?<!'|")(\/[^\/'"]+\/[gimsuy]*)(?!'|")/g,
      (match, g1, g2, g3, g4, offset) => {
        isTriggered(0, '删除序列末尾元素后的 `,` 逗号', g1);
        isTriggered(2, '删除连续重复的 `,` 逗号', g2);
        isTriggered(1, '将单引号替换为双引号', g3);
        isTriggered(3, '将正则表达式以双引号包裹', g4);
        if (g1 || g2) {
          let diff = preCursorPos - offset;
          if (diff > 0) curCursorPos -= Math.min(diff, match.length);
          return '';
        }
        if (g3) return '"';
        if (g4) {
          if (offset < preCursorPos && preCursorPos < offset + match.length) curCursorPos += 1;
          else if (preCursorPos >= offset + match.length) curCursorPos += 2;
          return `"${match}"`;
        }
        return match;
      });
    return [rslt, booleanOr(...flags), curCursorPos];

    function isTriggered(index, msg, ...groups) {
      if (!flags[index] && booleanOr(...groups)) {
        console.log(`${SCRIPT_NAME}:触发自动纠错 - ${msg}`);
        flags[index] = true;
      }
    }
    function booleanOr(...values) {
      return values.reduce((acc, val) => acc || val, false);
    }
  }

  /**
   * 初步解析`staffMapListJSON`字符串
   * 仅检查:
   *   1.是否满足`JSON`格式
   *   2.是否为数组类型
   *   3.字符串样式的正则表达式,是否满足规定格式
   * 更进一步的解析,将在`loadMapList`中进行
   */
  function parseMapListJSON(text) {
    let parsedData;
    try {
      parsedData = JSON.parse(text, regexReviver);
    } catch (e) {
      console.error(`${SCRIPT_NAME}:staffMapList 解析失败 - ${e}`);
      return null;
    }
    if (!Array.isArray(parsedData)) {
      console.error(`${SCRIPT_NAME}:staffMapList 类型错误 - 非数类型`);
      return null;
    }
    return parsedData;
  }

  /**
   * 解析`JSON`字符串中的正则表达式
   */
  function regexReviver(key, value) {
    if (typeof value === 'string' && value.startsWith('/')) {
      const regexParttern = /^\/(.+)\/([gimsuy]*)$/;
      const match = value.match(regexParttern);
      if (match) {
        try {
          return new RegExp(match[1], match[2]);
        } catch (e) {
          throw new Error(`正则表达式 "${value}" 非法 - ${e}`);
        }
      } else throw new Error(`正则表达式 "${value}" 不符合 ${regexParttern} 格式`);
    }
    return value;
  }

  /**
   * 将正则表达式转化为字符串,以满足`JSON`格式
   */
  function regexReplacer(key, value) {
    if (value instanceof RegExp) return value.toString();
    return value;
  }

  /**
   * 获取元素的行高
   */
  function getLineHeight(el) {
    // 获取元素的计算样式
    const compStyle = window.getComputedStyle(el); // 不会返回 em 单位的数据
    const fontSize = parseFloat(compStyle.fontSize);
    let lineHeight = compStyle.lineHeight;
    console.log(`${SCRIPT_NAME}:fontSize:${fontSize}px, lineHeight:${lineHeight}`);
    // normal,估算为 1.2 倍
    if (lineHeight === 'normal') return fontSize * 1.2;
    // 百分比,如 150%
    if (lineHeight.endsWith('%')) return fontSize * parseFloat(lineHeight) / 100;
    // 像素单位,如 18px
    if (lineHeight.endsWith('px')) return parseFloat(lineHeight);
    // 倍数,如 1.5
    return fontSize * parseFloat(lineHeight);
  }

  /** 
   * 在子元素的外层与父元素间嵌套一层元素
   */
  function nestElementWithChildren(parent, newTagName, options) {
    const newElement = createElement(newTagName, options, Array.from(parent.childNodes));
    parent.innerHTML = '';
    parent.appendChild(newElement);
  }

  /**
   * 创建元素实例
   * @param {string} tagName - 类名
   * @param {object} options - 属性
   * @param {Array.<HTMLElement|string>|undefined} subElements - 子元素
   * @param {object.<string, Function>} eventHandlers - 绑定的事件
   */
  function createElement(tagName, options, subElements, eventHandlers) {
    const element = document.createElement(tagName);
    if (options) {
      for (let opt in options) {
        if (opt === 'class') element.className = options[opt];
        else if (['maxlength'].includes(opt)) element.setAttribute(opt, options[opt]);
        else if (opt === 'dataset' || opt === 'style') {
          for (let key in options[opt]) {
            element[opt][key] = options[opt][key];
          }
        } else element[opt] = options[opt];
      }
    }
    if (subElements) updateSubElements(element, subElements);
    if (eventHandlers) {
      for (let e in eventHandlers) {
        element.addEventListener(e, eventHandlers[e]);
      }
    }
    return element;
  }

  /**
   * 更新子元素的内容
   * @param {HTMLElement} parent - 父元素
   * @param {Array.<HTMLElement|string>|HTMLElement|string|undefined} subElements - 要插入的子元素
   * @param {'append'|'prepend'|'replace'} [actionType='append'] - 操作类型,可以是以下之一:
   *   `prepend` - 将元素插入到父元素的首位
   *   `append`  - 将元素插入到父元素的末尾
   *   `replace` - 清空父元素内容并插入元素
   */
  function updateSubElements(parent, subElements, actionType = 'append') {
    if (actionType === 'replace') parent.innerHTML = '';
    if (!subElements) return parent;
    if (!Array.isArray(subElements)) subElements = [subElements];
    for (let e of subElements) {
      const child = typeof e === 'string' ? document.createTextNode(e) : e;
      switch (actionType) {
        case "append":
        case "replace":
          parent.appendChild(child);
          break;
        case "prepend":
          parent.insertBefore(child, parent.firstChild);
          break;
        default:
          throw new Error(`'${actionType}' is invalid action type of updateElements!`);
      }
    }
    return parent;
  }

  /**
   * 动态载入职位排序的样式,
   * 依据的职位信息行高`jobLineHeight`与设置的限制行数`maxRefoldLines`
   */
  function loadStaffStyle() {
    const style = createElement('style', {class: 'staff_sorting'});
    style.innerHTML = `
      /* 职位信息二次折叠 */
      #infobox div.refoldable {
        height: auto;
        overflow: visible;
      }
      #infobox div.refolded {
        height: ${maxRefoldLines * jobLineHeight}px;
        overflow: clip;
        mask-image: 
          linear-gradient(160deg, black 10%, transparent 90%), 
          linear-gradient(black, black);
        mask-size: 100% ${jobLineHeight}px, 100% calc(100% - ${jobLineHeight}px);
        mask-position: 0 100%, 0 0;
        mask-repeat: no-repeat;
        mask-composite: add;
      }
      #infobox .tip.switch {
        cursor: pointer;
      }
      #infobox .tip.switch:hover {
        color: #000;
      }
      html[data-theme='dark'] #infobox .tip.switch:hover {
        color: #FFF;
      }
      #infobox .tip.switch:hover i {
        color: #2ea6ff;
      }
    `;
    document.head.appendChild(style);
  }

  // 载入设置界面的样式
  function loadSettingStyle() {
    const style = createElement('style', {class: 'staff_sorting'});
    style.innerHTML = `
      /* 设置界面的样式 */
      :root {
        --tri-state-selector-size: 22px;
        --tri-state-selector-step: 19px;
      }
      #staff_sorting > .settings {
        margin-left: 5px;
      }
      #staff_sorting .right_inline {
        height: 22px;
        float: right;
        display: flex;
        align-items: center;
      }
      #staff_sorting td[class$="block"] > h2 {
        font-size: 16px;
        display: inline-block;
      }
      #staff_sorting .staffMapList_editor {
        padding-right: 10%;
        margin-bottom: 5px;
      }

      /* 各类型条目的职位设置模块 */
      .subject_staff_block textarea {
        font-size: 15px;
        line-height: 21px;
      }
      .subject_staff_block .inputBtn {
        margin-right: 5px;
      }
      .subject_staff_block .tip_j {
        display: none;
        margin: 0 5px;
      }
      .subject_staff_block .right_inline .tip_j {
        display: none;
        margin-right: 15px;
      }

      /* 滑动开关 */
      .toggle {
        position: relative;
        width: 44px;
        height: 22px;
        display: block;
        float: right;
      }
      .toggle_input {
        display: none;
      }
      .toggle_slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #eaeaea;
        border-radius: 22px;
        box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.2);
        transition: background-color 0.2s ease-in;
      }
      html[data-theme="dark"] .toggle_slider {
        background-color: #9a9a9a;
      }
      .toggle_slider::before {
        content: "";
        position: absolute;
        height: 16px;
        width: 16px;
        left: 3px;
        bottom: 3px;
        background-color: white;
        border-radius: 50%;
        box-shadow: 0 2px 3px rgba(0, 0, 0, 0.3);
        transition: transform 0.2s ease-in;
      }
      .toggle_input:checked + .toggle_slider {
        background-color: #72b6e3;
      }
      html[data-theme="dark"] .toggle_input:checked + .toggle_slider {
        background-color: #3072dc;
      }
      .toggle_input:checked + .toggle_slider::before {
        transform: translateX(22px);
      }

      /* 数字输入框与控制器 */
      .num_input_cntr {
        display: flex;
        float: left;
        align-items: center;
        gap: 5px;
        margin-right: 30px;
      }
      .num_input_cntr .text {
        font-size: 14px;
        margin-right: 2px;
      }
      .inputtext.input_num {
        width: 30px;
        height: 12px;
        text-align: center;
        font-size: 15px;
      }
      .num_ctrs {
        display: flex;
        flex-direction: column;
        background-color: white;
        border: 1px solid #d9d9d9;
        border-radius: 4px;
        box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
        gap: 0;
      }
      html[data-theme="dark"] .num_ctrs {
        background-color: black;
        border: 1px solid #757575;
      }
      .num_ctrs div {
        display: flex;
        text-align: center;
        width: 12px;
        height: 7px;
        padding: 2px;
        cursor: pointer;
      }
      .num_ctrs div:first-child {
        border-radius: 3px 3px 0 0;
      }
      .num_ctrs div:last-child {
        border-radius: 0 0 3px 3px;
      }
      .num_ctrs div svg {
        width: 100%;
        height: 100%;
      }
      .num_ctrs div:active {
        background-color: #2ea6ff;
      }

      /* 三态滑动选择器 */
      .tri_state_selector {
        position: relative;
        width: calc(
          var(--tri-state-selector-size) + var(--tri-state-selector-step) * 2
        );
        height: var(--tri-state-selector-size);
        display: inline-block;
      }
      .radio_input {
        position: absolute;
        opacity: 0;
        z-index: 2;
      }
      .select_slider {
        position: relative;
        width: 100%;
        height: 100%;
        background-color: #eaeaea;
        border-radius: var(--tri-state-selector-size);
        box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2);
        z-index: 1;
        overflow: hidden;
        transition: background-color 0.2s ease-in;
      }
      html[data-theme="dark"] .select_slider {
        background-color: #9a9a9a;
      }
      .select_indicator {
        position: absolute;
        width: calc(var(--tri-state-selector-size) - 4px);
        height: calc(var(--tri-state-selector-size) - 4px);
        top: 2px;
        left: 2px;
        background-color: white;
        border-radius: 50%;
        box-shadow: 0 2px 3px rgba(0, 0, 0, 0.3);
        z-index: 1;
        transition: transform 0.2s ease-in;
      }
      .radio_label {
        position: absolute;
        width: var(--tri-state-selector-step);
        height: 100%;
        top: 0;
        cursor: pointer;
        z-index: 3;
      }
      label.radio_label:nth-of-type(1) {
        left: 0;
      }
      label.radio_label:nth-of-type(2) {
        left: var(--tri-state-selector-step);
      }
      label.radio_label:nth-of-type(3) {
        width: var(--tri-state-selector-size);
        left: calc(var(--tri-state-selector-step) * 2);
      }
      input.radio_input:nth-of-type(2):checked ~ .select_slider {
        background-color: #f47a88;
      }
      input.radio_input:nth-of-type(3):checked ~ .select_slider {
        background-color: #72b6e3;
      }
      html[data-theme="dark"] input.radio_input:nth-of-type(2):checked ~ .select_slider {
        background-color: #ff668a;
      }
      html[data-theme="dark"] input.radio_input:nth-of-type(3):checked ~ .select_slider {
        background-color: #3072dc;
      }
      input.radio_input:nth-of-type(1):checked ~ .select_slider .select_indicator {
        transform: translateX(0);
      }
      input.radio_input:nth-of-type(2):checked ~ .select_slider .select_indicator {
        transform: translateX(var(--tri-state-selector-step));
      }
      input.radio_input:nth-of-type(3):checked ~ .select_slider .select_indicator {
        transform: translateX(calc(var(--tri-state-selector-step) * 2));
      }
      .select_slider::after {
        content: "";
        position: absolute;
        width: calc(var(--tri-state-selector-size) + var(--tri-state-selector-step));
        height: var(--tri-state-selector-size);
        left: var(--tri-state-selector-step);
        border-radius: calc(var(--tri-state-selector-size) / 2);
        box-shadow: 0 0 3px rgba(0, 0, 0, 0.1), inset 0 0 6px rgba(0, 0, 0, 0.3);
        transition: transform 0.2s ease-in-out;
      }
      input.radio_input:nth-of-type(1):checked ~ .select_slider::after {
        transform: translateX(calc(0px - var(--tri-state-selector-step)));
      }
    `;
    document.head.appendChild(style);
  }

  main();

})();

QingJ © 2025

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