MaaCopilotPlus

增强MAA作业站的筛选功能

当前为 2025-03-02 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MaaCopilotPlus
// @namespace    https://github.com/HauKuen
// @license MIT
// @version      1.3
// @description  增强MAA作业站的筛选功能
// @author       haukuen
// @match        https://prts.plus/*
// @icon         https://prts.plus/favicon-32x32.png?v=1
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
  "use strict";

  // 初始化角色列表
  let myOperators = GM_getValue("myOperators", []);
  // 筛选开关状态
  let filterEnabled = GM_getValue("filterEnabled", true);
  // 允许缺少一个干员 
  let allowOneMissing = GM_getValue("allowOneMissing", false);

  // 创建UI
  function createUI() {
    // 创建插件控制面板
    const controlPanel = document.createElement("div");
    controlPanel.id = "maa-copilot-plus";
    controlPanel.style.position = "fixed";
    controlPanel.style.top = "10px";
    controlPanel.style.right = "10px";
    controlPanel.style.zIndex = "9999";
    controlPanel.style.backgroundColor = "#f0f0f0";
    controlPanel.style.padding = "10px";
    controlPanel.style.borderRadius = "5px";
    controlPanel.style.boxShadow = "0 0 10px rgba(0,0,0,0.2)";
    controlPanel.style.cursor = "move";

    // 添加拖拽功能
    let isDragging = false;
    let currentX;
    let currentY;
    let initialX;
    let initialY;

    controlPanel.addEventListener("mousedown", (e) => {
      isDragging = true;

      // 获取鼠标相对于面板的初始位置
      initialX = e.clientX - controlPanel.offsetLeft;
      initialY = e.clientY - controlPanel.offsetTop;

      controlPanel.style.opacity = "0.8";
      controlPanel.style.transition = "none";
    });

    document.addEventListener("mousemove", (e) => {
      if (isDragging) {
        e.preventDefault();

        // 计算新位置
        currentX = e.clientX - initialX;
        currentY = e.clientY - initialY;

        // 限制在窗口内
        const maxX = window.innerWidth - controlPanel.offsetWidth;
        const maxY = window.innerHeight - controlPanel.offsetHeight;

        currentX = Math.max(0, Math.min(currentX, maxX));
        currentY = Math.max(0, Math.min(currentY, maxY));

        // 更新位置
        controlPanel.style.left = currentX + "px";
        controlPanel.style.top = currentY + "px";
        controlPanel.style.right = "auto"; // 清除right属性以避免冲突
      }
    });

    document.addEventListener("mouseup", () => {
      if (isDragging) {
        isDragging = false;
        // 恢复正常样式
        controlPanel.style.opacity = "1";
        controlPanel.style.transition = "opacity 0.2s";
      }
    });

    // 保存面板位置到本地存储
    window.addEventListener("beforeunload", () => {
      if (controlPanel.style.left) {
        // 只有在面板被移动过时才保存
        GM_setValue("panelPosition", {
          left: controlPanel.style.left,
          top: controlPanel.style.top,
        });
      }
    });

    // 恢复上次保存的位置
    const savedPosition = GM_getValue("panelPosition", null);
    if (savedPosition) {
      controlPanel.style.left = savedPosition.left;
      controlPanel.style.top = savedPosition.top;
      controlPanel.style.right = "auto";
    }

    // 创建标题
    const title = document.createElement("h3");
    title.textContent = "MAA Copilot Plus";
    title.style.margin = "0 0 10px 0";
    title.style.cursor = "move"; // 标题也可以用来拖动

    const buttonContainer = document.createElement("div");
    buttonContainer.style.display = "flex";
    buttonContainer.style.marginBottom = "10px";

    const importButton = document.createElement("button");
    importButton.textContent = "导入角色列表";
    importButton.onclick = openImportDialog;

    buttonContainer.appendChild(importButton);

    // 创建开关容器
    const toggleContainer = document.createElement("div");
    toggleContainer.style.display = "flex";
    toggleContainer.style.alignItems = "center";
    toggleContainer.style.marginBottom = "10px";

    // 创建开关
    const toggleLabel = document.createElement("label");
    toggleLabel.style.display = "flex";
    toggleLabel.style.alignItems = "center";
    toggleLabel.style.cursor = "pointer";

    const toggleInput = document.createElement("input");
    toggleInput.type = "checkbox";
    toggleInput.checked = filterEnabled;
    toggleInput.style.margin = "0 5px 0 0";
    toggleInput.onchange = function () {
      filterEnabled = this.checked;
      GM_setValue("filterEnabled", filterEnabled);
      updateStatus();
      if (filterEnabled) {
        filterGuides();
      } else {
        resetFilter();
      }
    };

    const toggleText = document.createElement("span");
    toggleText.textContent = "启用筛选";

    toggleLabel.appendChild(toggleInput);
    toggleLabel.appendChild(toggleText);
    toggleContainer.appendChild(toggleLabel);

    // 创建允许缺少一个干员的设置
    const missingContainer = document.createElement("div");
    missingContainer.style.display = "flex";
    missingContainer.style.alignItems = "center";
    missingContainer.style.marginBottom = "10px";

    const missingLabel = document.createElement("label");
    missingLabel.style.display = "flex";
    missingLabel.style.alignItems = "center";
    missingLabel.style.cursor = "pointer";

    const missingInput = document.createElement("input");
    missingInput.type = "checkbox";
    missingInput.checked = allowOneMissing;
    missingInput.style.margin = "0 5px 0 0";
    missingInput.onchange = function () {
      allowOneMissing = this.checked;
      GM_setValue("allowOneMissing", allowOneMissing);
      if (filterEnabled) {
        filterGuides();
      }
    };

    const missingText = document.createElement("span");
    missingText.textContent = "允许缺少一个干员";

    missingLabel.appendChild(missingInput);
    missingLabel.appendChild(missingText);
    missingContainer.appendChild(missingLabel);



    // 创建状态显示
    const status = document.createElement("div");
    status.id = "maa-status";
    status.style.fontSize = "12px";

    // 组装控制面板
    controlPanel.appendChild(title);
    controlPanel.appendChild(buttonContainer);
    controlPanel.appendChild(toggleContainer);
    controlPanel.appendChild(missingContainer);
    controlPanel.appendChild(status);

    document.body.appendChild(controlPanel);

    // 初始化状态显示
    updateStatus();
  }

  // 更新状态显示
  function updateStatus() {
    const status = document.getElementById("maa-status");
    if (status) {
      let statusText = `已导入 ${myOperators.length} 个角色`;
      statusText += filterEnabled ? " (筛选已启用)" : " (筛选已禁用)";
      status.textContent = statusText;

      // 更新状态颜色
      status.style.color = filterEnabled ? "green" : "gray";
    }
  }

  // 导入角色对话框
  function openImportDialog() {
    // 创建模态对话框
    const modal = document.createElement("div");
    modal.style.position = "fixed";
    modal.style.top = "0";
    modal.style.left = "0";
    modal.style.width = "100%";
    modal.style.height = "100%";
    modal.style.backgroundColor = "rgba(0,0,0,0.5)";
    modal.style.display = "flex";
    modal.style.justifyContent = "center";
    modal.style.alignItems = "center";
    modal.style.zIndex = "10000";

    // 创建对话框内容
    const dialog = document.createElement("div");
    dialog.style.backgroundColor = "white";
    dialog.style.padding = "20px";
    dialog.style.borderRadius = "5px";
    dialog.style.width = "80%";
    dialog.style.maxWidth = "600px";
    dialog.style.maxHeight = "80%";
    dialog.style.overflow = "auto";

    // 创建标题
    const title = document.createElement("h3");
    title.textContent = "导入角色列表";
    title.style.marginTop = "0";

    // 创建文本区域
    const textarea = document.createElement("textarea");
    textarea.style.width = "100%";
    textarea.style.height = "200px";
    textarea.style.marginBottom = "10px";
    textarea.placeholder = "粘贴角色列表 JSON 数据...";

    // 创建按钮
    const buttonContainer = document.createElement("div");
    buttonContainer.style.display = "flex";
    buttonContainer.style.justifyContent = "flex-end";

    const cancelButton = document.createElement("button");
    cancelButton.textContent = "取消";
    cancelButton.style.marginRight = "10px";
    cancelButton.onclick = () => document.body.removeChild(modal);

    const importButton = document.createElement("button");
    importButton.textContent = "导入";
    importButton.onclick = () => {
      try {
        const data = JSON.parse(textarea.value);

        if (Array.isArray(data)) {
          myOperators = data
            .filter((op) => op.own)
            .map((op) => ({
              name: op.name,
              elite: op.elite,
              level: op.level,
              rarity: op.rarity,
              // 根据精英化等级计算已解锁的最大技能
              maxSkill: op.elite === 0 ? 1 : op.elite === 1 ? 2 : 3
            }));

          GM_setValue("myOperators", myOperators);
          updateStatus();

          document.body.removeChild(modal);

          // 导入成功后自动筛选(如果筛选功能已启用)
          if (filterEnabled) {
            filterGuides();
          }
        } else {
          alert("无效的数据格式,请确保是有效的 JSON 数组");
        }
      } catch (e) {
        alert("解析失败: " + e.message);
      }
    };

    buttonContainer.appendChild(cancelButton);
    buttonContainer.appendChild(importButton);

    // 组装对话框
    dialog.appendChild(title);
    dialog.appendChild(textarea);
    dialog.appendChild(buttonContainer);
    modal.appendChild(dialog);

    document.body.appendChild(modal);
  }

  // 筛选攻略
  function filterGuides() {
    if (myOperators.length === 0) {
      alert("请先导入角色列表");
      return;
    }

    if (!filterEnabled) {
      return;
    }

    // 获取所有攻略卡片
    const guideCards = document.querySelectorAll(
      "body > main > div > div:nth-child(2) > div > div:nth-child(1) > div:nth-child(2) > div > div"
    );
    let filteredCount = 0;

    guideCards.forEach((card) => {
      // 获取干员区域
      const operatorSection = card.querySelector(
        "div:has(> div.text-sm.text-zinc-600)"
      );
      if (!operatorSection) return;

      // 获取卡片中的干员标签,只获取干员区域内的标签
      const operatorTags = operatorSection.querySelectorAll(
        "span.bp4-tag > span.bp4-fill"
      );
      let missingOperators = 0;

      operatorTags.forEach((tag) => {
        const operatorText = tag.textContent.trim();

        // 检查是否为标签类型 (如 [奶盾])
        if (operatorText.match(/^\[.*\]$/)) {
          return; // 跳过标签类型
        }

        // 提取干员名和技能
        const [operatorName, skillText] = operatorText.split(" ");
        // 将技能文本转换为数字
        const skillNumber = skillText ? parseInt(skillText.replace(/[^0-9]/g, "")) : 1;

        // 检查是否在干员列表中且技能已解锁
        const operator = myOperators.find(op => op.name === operatorName);
        if (!operator || skillNumber > operator.maxSkill) {
          missingOperators++;
          if (!allowOneMissing || missingOperators > 1) {
            // 如果不允许缺少干员,或缺少超过一个干员,则隐藏卡片
            card.style.display = "none";
            filteredCount++;
            return false;
          }
        }
      });

      // 显示卡片
      if (missingOperators === 0 || (allowOneMissing && missingOperators === 1)) {
        card.style.display = "";
      } else {
        card.style.display = "none";
        filteredCount++;
      }
    });

    // 更新状态
    const status = document.getElementById("maa-status");
    if (status) {
      status.textContent = `已导入 ${myOperators.length} 个干员,筛选掉 ${filteredCount} 个不符合条件的攻略 (筛选已启用)`;
      status.style.color = "green";
    }
  }

  // 重置筛选
  function resetFilter() {
    // 获取所有攻略卡片
    const guideCards = document.querySelectorAll(
      "body > main > div > div:nth-child(2) > div > div:nth-child(1) > div:nth-child(2) > div > div"
    );

    guideCards.forEach((card) => {
      // 恢复显示并移除高亮效果
      card.style.display = "";
      card.style.boxShadow = "";
    });

    // 更新状态
    updateStatus();
  }

  // 监听URL变化,用于在页面切换或搜索结果更新时重新筛选
  let lastUrl = location.href;

  // MutationObserver 监视DOM变化
  const observer = new MutationObserver((mutations) => {
    // 检查URL是否变化
    if (lastUrl !== location.href) {
      lastUrl = location.href;

      // 给页面加载时间
      setTimeout(() => {
        if (filterEnabled && myOperators.length > 0) {
          filterGuides();
        }
      }, 1000);
    }

    // 检查是否有新的攻略卡片加载
    const guideContainer = document.querySelector(
      "body > main > div > div:nth-child(2) > div > div:nth-child(1) > div:nth-child(2) > div"
    );
    if (guideContainer) {
      for (const mutation of mutations) {
        if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
          // 如果有新节点添加,且筛选功能已启用,且有角色列表,重新筛选
          if (filterEnabled && myOperators.length > 0) {
            filterGuides();
          }
          break;
        }
      }
    }

    // 我嘞个广告啊
    const removeAds = () => {
      const sideAd = document.querySelector(
        "body > main > div > div:nth-child(2) > div > div:nth-child(2) > div > a"
      );
      if (sideAd) {
        sideAd.style.display = "none";
      }

      // 移除其他可能的 mumu 广告容器
      document.querySelectorAll('a[href*="gad.netease.com"]').forEach(ad => {
        ad.style.display = "none";
      });
    };

    // 立即执行一次广告移除
    removeAds();
  });

  // 等待页面加载完成
  window.addEventListener("load", () => {
    createUI();

    const removeAds = () => {
      const sideAd = document.querySelector(
        "body > main > div > div:nth-child(2) > div > div:nth-child(2) > div > a"
      );
      if (sideAd) {
        sideAd.style.display = "none";
      }
      document.querySelectorAll('a[href*="gad.netease.com"]').forEach(ad => {
        ad.style.display = "none";
      });
    };

    removeAds();

    // 观察DOM变化
    observer.observe(document.body, { childList: true, subtree: true });

    // 如果已有角色列表且筛选功能已启用,自动筛选
    if (filterEnabled && myOperators.length > 0) {
      setTimeout(filterGuides, 1000);
    }
  });
})();