Wod 团仓耗材清理助手

帮助快速勾选要清仓耗材及提供已勾选物品的数量统计

// ==UserScript==
// @name         Wod 团仓耗材清理助手
// @icon           http://info.world-of-dungeons.org/wod/css/WOD.gif
// @namespace    http://tampermonkey.net/
// @description  帮助快速勾选要清仓耗材及提供已勾选物品的数量统计
// @author       Christophero
// @version      2022.10.02.1
// @match        http*://*.world-of-dungeons.org/wod/spiel/hero/items.php?*
// @require      https://code.jquery.com/jquery-3.3.1.min.js
// @require      https://code.jquery.com/ui/1.13.2/jquery-ui.min.js
// @grant        none
// ==/UserScript==

(function () {
  ("use strict");

  /**
   * 插入Css样式
   * @param {*} select
   * @param {*} styles
   */
  function insertCss(select, styles) {
    if (document.styleSheets.length === 0) {
      //如果没有style标签,则创建一个style标签
      var style = document.createElement("style");
      document.head.appendChild(style);
    }
    var styleSheet = document.styleSheets[document.styleSheets.length - 1]; //如果有style 标签.则插入到最后一个style标签中
    var str = select + " {"; //插入的内容必须是字符串,所以得把obj转化为字符串
    for (var prop in styles) {
      str +=
        prop.replace(/([A-Z])/g, function (item) {
          //使用正则把大写字母替换成 '-小写字母'
          return "-" + item.toLowerCase();
        }) +
        ":" +
        styles[prop] +
        ";";
    }
    str += "}";
    styleSheet.insertRule(str, styleSheet.cssRules.length); //插入样式到最后一个style标签中的最后面
  }

  let defaultMap = {
    normal: { 一大块精炼硫磺: 100, 一小把寒炼沙土: 200, 盛精炼硫磺的坩埚: 150 },
    medicaments: {
      斑叶防风: 100,
      胡姜瘿: 100,
      蓝越橘: 100,
      龙血树胶: 100,
      智者秃顶: 100,
      曼陀罗: 100,
      巨魔肌腱: 100,
      北境柳: 100,
      铅树心木: 100,
      蟹蛛丝囊: 100,
      铁杉鳞果: 100,
      萤火虫软壳: 100,
      魔鬼爪: 100,
      黑枭眼珠: 100,
      紫龙舌兰: 100,
      血榆实: 100,
      阿伊恩萿: 100,
      莲花芯: 100,
      银刺嫩叶: 100,
      雪蓟: 100,
      白鬼帽: 100,
      毛冬青: 100,
    },
  };
  let cleanMap;

  const CLEAN_WARE_HOUSE_KEY = "cleanWareHouseSettings";

  main();

  /**
   * 插件主入口
   */
  function main() {
    const isWarehouse = $(
      'h1:contains("团队仓库"),h1:contains("所有英雄共用的贮藏室")'
    ).length;
    if (!isWarehouse) return;
    initJQUIStyle();
    cleanMap = loadSettings();
    addOptBtn();
    addRowOptBtn();
  }

  /**
   * 初始化JqueryUI的样式表
   */
  function initJQUIStyle() {
    let $toolbarCss = $("<link>");
    $("head").prepend($toolbarCss);
    $toolbarCss.attr({
      rel: "stylesheet",
      type: "text/css",
      href: "https://code.jquery.com/ui/1.13.2/themes/humanity/jquery-ui.css",
    });
    document.querySelector("style").textContent +=
      ".ui-dialog { height: 600px !important; } " +
      ".ui-dialog .ui-dialog-content { height: 90% !important; } " +
      "@media (min-width:1px) { .ui-dialog { width: 95% !important; } } " +
      "@media (min-width: 768px) { .ui-dialog { max-width:900px !important; width:95% !important; } }";
  }

  /**
   * 加载设置
   */
  function loadSettings() {
    let settings = {};
    const textSettings = localStorage.getItem(CLEAN_WARE_HOUSE_KEY);
    if (!textSettings) {
      settings = JSON.parse(JSON.stringify(defaultMap));
    } else {
      settings = JSON.parse(textSettings);
    }
    return settings;
  }

  /**
   * 保存设置
   * @param {Object} settings 设置对象
   */
  function saveSettings(settings) {
    localStorage.setItem(CLEAN_WARE_HOUSE_KEY, JSON.stringify(settings));
  }

  /**
   * 添加操作按钮
   */
  function addOptBtn() {
    const $checkBtn = $(
      '<input type="button" name="batchSelect" value="批量耗材勾选" class="button clickable">'
    );

    const $overviewBtn = $(
      '<input type="button" name="overview" value="勾选概览" class="button clickable">'
    );

    const $settingsBtn = $(
      '<input type="button" name="settings" value="管理预设" class="button clickable">'
    );

    const $exportBtn = $(
      '<input type="button" name="export" value="导出" class="button clickable">'
    );

    const $fileBtn = $(
      '<input type="file" id="cleanFile" name="cleanFile" runat="server"  style="position:relative; z-index:10; filter:alpha(opacity=0);-moz-opacity:0; opacity:0; width: 65px; left:-65px;">'
    );

    const $importBtn = $(
      '<input type="button" name="button" value="导入" class="button clickable" style="width:65px;">'
    );

    const $baseInput = $('input[value="应用改动"]:first');
    $baseInput.after(
      $checkBtn,
      $overviewBtn,
      $settingsBtn,
      $exportBtn,
      $importBtn,
      $fileBtn
    );

    $checkBtn.click(checkItems);
    $overviewBtn.click(showSelectOverview);
    $settingsBtn.click(showSettings);
    $exportBtn.click(exportSettings);
    $fileBtn.change(importSettings);
  }

  /**
   * 添加行操作按钮
   */
  function addRowOptBtn() {
    const sellIndex = getSellIndex();
    const $trs = $('table.content_table>tbody>tr[class^="row"]');
    if (!$trs.length || $trs.length < 2) {
      return;
    }
    $trs.each(function () {
      const $sellTd = $(this).find(`td:eq(${sellIndex})`);
      const $addBtn = $(
        '<input type="button" name="add" value="加入" style="float: left;" class="button clickable">'
      );
      const $removeBtn = $(
        '<input type="button" name="remove" value="移除" style="float: left;" class="button clickable">'
      );
      $sellTd.prepend($removeBtn).prepend($addBtn);
      $addBtn.click(function () {
        const result = $(this)
          .parents("tr:first")
          .find("td:eq(1)")
          .text()
          .match(/(.+)\((\d+)\/(\d+)\)/);
        if (result) {
          let name = result[1].trim();
          let retainCount = 100;
          if (cleanMap.normal.hasOwnProperty(name)) {
            retainCount = parseInt(
              window.prompt(
                `更新耗材[${name}]的保留数量,0表示不保留`,
                cleanMap.normal[name]
              )
            );
          } else {
            retainCount = parseInt(
              window.prompt(`输入耗材[${name}]的保留数量,0表示不保留`, 50)
            );
          }
          if (isNaN(retainCount)) {
            alert("输入不是一个数字,取消操作");
          } else {
            cleanMap.normal[name] = retainCount;
            saveSettings(cleanMap);
            alert(`已更新耗材[${name}]的保留数量为 ${retainCount}`);
          }
        } else {
          alert("该物品不是耗材");
        }
      });
      $removeBtn.click(function () {
        const result = $(this)
          .parents("tr:first")
          .find("td:eq(1)")
          .text()
          .match(/(.+)\((\d+)\/(\d+)\)/);
        if (result) {
          let name = result[1].trim();
          if (cleanMap.normal.hasOwnProperty(name)) {
            if (confirm(`是否将该物品移出清理清单`)) {
              delete cleanMap.normal[name];
              saveSettings(cleanMap);
              alert(`已移除[${name}]`);
            }
          } else {
            alert("该物品不在清理清单列表中");
          }
        } else {
          alert("该物品不是耗材");
        }
      });
    });
  }

  /**
   * 显示当前已选择对象概览
   */
  function showSelectOverview() {
    let $overviewDiv = $("div#overview");
    if (!$overviewDiv.length) {
      $("body:first").append(
        $('<div style="visibility: hidden;"><div id="overview"></div></div>')
      );
      $overviewDiv = $("div#overview");
    } else {
      $overviewDiv.empty();
    }
    const $trs = $('table.content_table>tbody>tr[class^="row"]');
    if (!$trs.length) {
      alert("没有物品展示");
      return;
    }
    let normalMap = {};
    let medicamentMap = {};
    itemStatistics($trs, normalMap, medicamentMap, true);
    let normalNames = Object.keys(normalMap);
    if (normalNames) {
      $overviewDiv.append(
        `<div><h3 style="text-shadow: 1px 2px 6px black;"> 一般物品 </h3></div>`
      );
      normalNames.forEach((key) => {
        const total = normalMap[key];
        const $row = $(
          `<div><span>${key} </span><span class="rep_bonus bonus_positive">${total}</span></div>`
        );
        $overviewDiv.append($row);
      });
    }
    let medicamentNames = Object.keys(medicamentMap);
    if (medicamentNames) {
      $overviewDiv.append(
        `<div><h3 style="text-shadow: 1px 2px 6px black;"> 魔力药剂 </h3></div>`
      );
      Object.keys(medicamentMap).forEach((key) => {
        const total = medicamentMap[key];
        const $row = $(
          `<div><span>${key} </span><span class="rep_bonus bonus_positive">${total}</span></div>`
        );
        $overviewDiv.append($row);
      });
    }

    $("#overview").dialog({
      autoOpen: true, // 是否自动弹出窗口
      modal: true, // 设置为模态对话框
      resizable: true,
      width: 540, //弹出框宽度
      height: 620, //弹出框高度
      title: "勾选概览", //弹出框标题
      position: { my: "center", at: "center", of: window }, //窗口显示的位置
      buttons: {
        关闭: function () {
          $(this).dialog("close");
        },
      },
    });
  }

  /**
   * 显示设置概览
   */
  function showSettings() {
    let $overviewDiv = $("div#overview");
    if (!$overviewDiv.length) {
      $("body:first").append(
        $('<div style="visibility: hidden;"><div id="overview"></div></div>')
      );
      $overviewDiv = $("div#overview");
    } else {
      $overviewDiv.empty();
    }
    const $trs = $('table.content_table>tbody>tr[class^="row"]');
    if (!$trs.length) {
      alert("没有物品展示");
      return;
    }
    let normalMap = cleanMap.normal;
    let medicamentMap = cleanMap.medicaments;
    let normalNames = Object.keys(normalMap);
    if (normalNames) {
      $overviewDiv.append(
        `<div><h2 style="text-shadow: 1px 2px 6px black;"> 一般物品 </h2></div>`
      );
      normalNames.forEach((key) => {
        let retainCount = normalMap[key];
        const $modifyBtn = $(
          '<input type="button" name="modify" value="变更" class="button clickable">'
        );
        const $delBtn = $(
          '<input type="button" name="del" value="移除" class="button clickable">'
        );
        const $name = $(`<span>${key} </span>`);
        const $count = $(
          `<span class="rep_bonus bonus_positive">${retainCount}</span>`
        );
        const $row = $(`<div style="display: flex;"></div>`);
        const $optDiv = $('<div style="text-align: right; flex: 1;"></div>');
        $optDiv.append($modifyBtn, $delBtn);
        $row.append($name, $count, $optDiv);
        $overviewDiv.append($row);
        $modifyBtn.click(function () {
          retainCount = parseInt(
            window.prompt(`更新耗材[${key}]的保留数量,0表示不保留`, retainCount)
          );
          if (isNaN(retainCount)) {
            alert("输入不是一个数字,取消操作");
            return;
          }
          normalMap[key] = retainCount;
          $count.text(retainCount);
          saveSettings(cleanMap);
          alert(`已更新耗材[${key}]的保留数量为 ${retainCount}`);
        });

        $delBtn.click(function () {
          if (confirm(`是否将该物品移出清理清单`)) {
            $row.remove();
            delete normalMap[key];
            saveSettings(cleanMap);
          }
        });
      });
    }
    let medicamentNames = Object.keys(medicamentMap);
    if (medicamentNames) {
      $overviewDiv.append(
        `<div><h2 style="text-shadow: 1px 2px 6px black;"> 魔力药剂 </h2></div>`
      );
      Object.keys(medicamentMap).forEach((key) => {
        let retainCount = medicamentMap[key];
        const $modifyBtn = $(
          '<input type="button" name="modify" value="数量变更" class="button clickable">'
        );
        const $name = $(`<span>${key} </span>`);
        const $count = $(
          `<span class="rep_bonus bonus_positive">${retainCount}</span>`
        );
        const $row = $(`<div style="display: flex;"></div>`);
        const $optDiv = $('<div style="text-align: right; flex: 1;"></div>');
        $optDiv.append($modifyBtn);
        $row.append($name, $count, $optDiv);
        $overviewDiv.append($row);
        $modifyBtn.click(function () {
          retainCount = parseInt(
            window.prompt(`更新耗材[${key}]的保留数量,0表示不保留`, retainCount)
          );
          if (isNaN(retainCount)) {
            alert("输入不是一个数字,取消操作");
            return;
          }
          medicamentMap[key] = retainCount;
          $count.text(retainCount);
          saveSettings(cleanMap);
          alert(`已更新耗材[${key}]的保留数量为 ${retainCount}`);
        });
      });
    }

    $("#overview").dialog({
      autoOpen: true, // 是否自动弹出窗口
      modal: true, // 设置为模态对话框
      resizable: true,
      width: 540, //弹出框宽度
      height: 620, //弹出框高度
      title: "预设概览", //弹出框标题
      position: { my: "center", at: "center", of: window }, //窗口显示的位置
      buttons: {
        关闭: function () {
          $(this).dialog("close");
        },
      },
    });
  }

  /**
   * 导出设置JSON文件
   */
  function exportSettings() {
    exportFileJSON(cleanMap);
  }

  /**
   * 从JSON文件导入设置
   */
  function importSettings(event) {
    importFileJSON(event)
      .then((res) => {
        console.log("读取到的数据", res);
        // 回显数据
        cleanMap = res;
        saveSettings(cleanMap);
        showSettings();
      })
      .catch((err) => {
        console.log(err);
      });
  }

  /**
   * 获得出售列在哪一列
   * @returns 出售列索引,从0开始
   */
  function getSellIndex() {
    const $headThs = $("table.content_table thead>tr:last th");
    const $sellTh = $(
      'table.content_table thead>tr:last input[value="出售"]'
    ).parent();
    return $headThs.index($sellTh);
  }

  /**
   * 物品数量统计
   * @param {*} $trs 物品行的jquery数组对象
   * @param {*} normalMap 容纳一般物品统计数量的对象
   * @param {*} medicamentMap 容纳药剂统计数量的对象
   * @param {*} onlyChecked 为true时仅统计已勾选的行
   */
  function itemStatistics($trs, normalMap, medicamentMap, onlyChecked) {
    const sellIndex = getSellIndex();
    $trs.each(function () {
      const result = $(this)
        .find("td:eq(1)")
        .text()
        .match(/(.+)\((\d+)\/(\d+)\)/);
      if (result) {
        const checked = $(this)
          .find(`td:eq(${sellIndex}) input:checkbox`)
          .prop("checked");
        if (onlyChecked && !checked) return;
        let name = result[1].trim();
        const unit = parseInt(result[2]);
        if (name.endsWith("酊剂") || name.endsWith("煎剂")) {
          const poResult = name.match(/(.+)的(.+)[酊煎]剂/);
          const herbName = poResult[2];
          let total = medicamentMap[herbName] || 0;
          medicamentMap[herbName] = total + unit;
        } else {
          let total = normalMap[name] || 0;
          normalMap[name] = total + unit;
        }
      }
    });
  }

  function checkItems() {
    const sellIndex = getSellIndex();
    let normalConsumables = cleanMap.normal;
    let medicaments = cleanMap.medicaments;
    const $trs = $('table.content_table>tbody>tr[class^="row"]');
    if (!$trs.length) {
      alert("没有物品需要清理");
      return;
    }
    let selectedMap = {};
    let normalMap = {};
    let medicamentMap = {};
    itemStatistics($trs, normalMap, medicamentMap, false);

    $trs.each(function () {
      const result = $(this)
        .find("td:eq(1)")
        .text()
        .match(/(.+)\((\d+)\/(\d+)\)/);
      if (result) {
        let name = result[1].trim();
        const unit = parseInt(result[2]);
        let total = 0;
        let selected = 0;
        let min = 0;
        if (name.endsWith("酊剂") || name.endsWith("煎剂")) {
          const poResult = name.match(/(.+)的(.+)[酊煎]剂/);
          name = poResult[2];
          total = medicamentMap[name] || 0;
          selected = selectedMap[name] || 0;
          min = medicaments[name];
        } else {
          total = normalMap[name] || 0;
          selected = selectedMap[name] || 0;
          min = normalConsumables[name];
        }
        if (total - (selected + unit) > min) {
          // 当前可以选择
          $(this)
            .find(`td:eq(${sellIndex}) input:checkbox`)
            .prop("checked", true);
          selectedMap[name] = selected + unit;
        } else {
          return;
        }
      }
    });
    alert("勾选完毕");
  }

  // 导出数据 --------------------------------------- Start
  /**
   * 导出JSON文件的方法,导出并直接进行下载
   *
   * @param {Object|JSONString} 传入导出json文件的数据, 格式为json对象或者json字符串
   * @param {String}  导出json文件的文件名称
   */
  const exportFileJSON = (data = {}, filename = "cleanWarehouse.json") => {
    if (typeof data === "object") {
      data = JSON.stringify(data, null, 4);
    }
    // 导出数据
    const blob = new Blob([data], { type: "text/json" });
    const e = new MouseEvent("click");
    const a = document.createElement("a");

    a.download = filename;
    a.href = window.URL.createObjectURL(blob);
    a.dataset.downloadurl = ["text/json", a.download, a.href].join(":");
    a.dispatchEvent(e);
  };

  const importFileJSON = (ev) => {
    return new Promise((resolve, reject) => {
      const fileDom = ev.target,
        file = fileDom.files[0];

      // 格式判断
      if (file.type !== "application/json") {
        reject("仅允许上传json文件");
      }
      // 检验是否支持FileRender
      if (typeof FileReader === "undefined") {
        reject("当前浏览器不支持FileReader");
      }

      // 执行后清空input的值,防止下次选择同一个文件不会触发onchange事件
      ev.target.value = "";

      // 执行读取json数据操作
      const reader = new FileReader();
      reader.readAsText(file); // 读取的结果还有其他读取方式,我认为text最为方便

      reader.onerror = (err) => {
        reject("json文件解析失败", err);
      };

      reader.onload = () => {
        const resultData = reader.result;
        if (resultData) {
          try {
            const importData = JSON.parse(resultData);
            resolve(importData);
          } catch (error) {
            reject("读取数据解析失败", error);
          }
        } else {
          reject("读取数据解析失败", error);
        }
      };
    });
  };
})();