MaaCopilotPlus

增强MAA作业站的筛选功能

// ==UserScript==
// @name         MaaCopilotPlus
// @namespace    https://github.com/HauKuen
// @license MIT
// @version      1.5
// @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);

  // 添加一个安全的查询函数
  function safeQuerySelector(selector, parent = document) {
    try {
      return parent.querySelector(selector);
    } catch (e) {
      console.warn(`查询选择器失败: ${selector}`, e);
      return null;
    }
  }

  function safeQuerySelectorAll(selector, parent = document) {
    try {
      return parent.querySelectorAll(selector);
    } catch (e) {
      console.warn(`查询选择器失败: ${selector}`, e);
      return [];
    }
  }

  // 创建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(filteredCount) {
    try {
      const status = document.getElementById("maa-status");
      if (status) {
        let statusText = `已导入 ${myOperators.length} 个角色`;
        if (filterEnabled) {
          statusText += filteredCount !== undefined
            ? `, 筛选掉 ${filteredCount} 个不符合条件的攻略`
            : " (筛选已启用)";
        } else {
          statusText += " (筛选已禁用)";
        }
        status.textContent = statusText;
        status.style.color = filterEnabled ? "green" : "gray";
      }
    } catch (e) {
      console.warn("更新状态显示失败:", e);
    }
  }

  // 导入角色对话框
  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() {
    try {
      if (myOperators.length === 0) {
        console.warn("未导入角色列表");
        return;
      }

      if (!filterEnabled) {
        return;
      }

      // 更新选择器以匹配新的卡片结构
      const guideCards = safeQuerySelectorAll(
        'div[class*="bp4-card"]'
      );

      if (!guideCards.length) {
        console.warn("未找到攻略卡片");
        return;
      }

      let filteredCount = 0;

      guideCards.forEach((card) => {
        try {
          // 更新干员标签选择器
          const operatorTags = safeQuerySelectorAll(
            'div:has(> div.text-sm.text-zinc-600) .bp4-tag span.bp4-fill',
            card
          );

          if (!operatorTags.length) return;

          // 重置高亮样式
          operatorTags.forEach(tag => {
            const tagElement = tag.closest('.bp4-tag');
            if (!tagElement) return;

            tagElement.style.color = "";
            tagElement.style.backgroundColor = "";
            tagElement.style.border = "";
            tagElement.style.transition = "";
            tagElement.onmouseover = null;
            tagElement.onmouseout = null;
            tagElement.title = "";
          });

          let missingOperators = 0;
          let lastMissingTag = null;

          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 (missingOperators === 1) {
                lastMissingTag = tag;
              }
            }
          });

          // 高亮缺失的一个干员
          if (allowOneMissing && missingOperators === 1 && lastMissingTag) {
            const tagElement = lastMissingTag.closest('.bp4-tag');
            if (tagElement) {
              tagElement.style.backgroundColor = "rgba(59, 130, 246, 0.1)";
              tagElement.style.color = "#2563eb";
              tagElement.style.border = "1px solid rgba(59, 130, 246, 0.5)";
              tagElement.style.transition = "all 0.2s ease";
              tagElement.title = "缺少此干员/精英化等级不足";
            }
          }

          // 判断是否隐藏卡片
          const shouldHide = (!allowOneMissing && missingOperators > 0) ||
            (allowOneMissing && missingOperators > 1);

          if (shouldHide) {
            card.style.display = "none";
            filteredCount++;
          } else {
            card.style.display = "";
          }
        } catch (e) {
          console.warn("处理卡片时出错:", e);
        }
      });

      updateStatus(filteredCount);
    } catch (e) {
      console.error("筛选功能出错:", e);
    }
  }

  // 重置筛选
  function resetFilter() {
    try {
      // 获取所有攻略卡片
      const guideCards = safeQuerySelectorAll(
        'div[class*="bp4-card"]'
      );

      guideCards.forEach((card) => {
        // 恢复显示
        card.style.display = "";

        // 重置所有干员标签的样式
        const operatorTags = safeQuerySelectorAll(
          'div:has(> div.text-sm.text-zinc-600) .bp4-tag',
          card
        );

        operatorTags.forEach(tag => {
          tag.style.color = "";
          tag.style.backgroundColor = "";
          tag.style.border = "";
          tag.style.transition = "";
          tag.style.boxShadow = "";
          tag.onmouseover = null;
          tag.onmouseout = null;
          tag.title = "";
        });
      });

      // 更新状态
      updateStatus();
    } catch (e) {
      console.warn("重置筛选时出错:", e);
    }
  }

  let lastUrl = location.href;

  // MutationObserver 监视DOM变化
  const observer = new MutationObserver((mutations) => {
    try {
      // URL 变化检查
      if (lastUrl !== location.href) {
        lastUrl = location.href;
        setTimeout(() => {
          try {
            if (filterEnabled && myOperators.length > 0) {
              filterGuides();
            }
          } catch (e) {
            console.warn("URL变化后筛选失败:", e);
          }
        }, 1000);
        return; // URL 变化时直接返回,避免重复处理
      }

      // 检查攻略列表变化
      for (const mutation of mutations) {
        // 只处理节点添加的情况
        if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
          // 检查是否有新的攻略卡片添加
          const hasNewCards = Array.from(mutation.addedNodes).some(node => {
            return node.nodeType === 1 && // 元素节点
              (node.classList.contains('bp4-card') || // 直接是卡片
                node.querySelector('div[class*="bp4-card"]')); // 包含卡片
          });

          if (hasNewCards) {
            // 给一点延时确保 DOM 完全更新
            setTimeout(() => {
              try {
                if (filterEnabled && myOperators.length > 0) {
                  console.log("检测到新的攻略卡片,重新应用筛选");
                  filterGuides();
                }
              } catch (e) {
                console.warn("处理新卡片时出错:", e);
              }
            }, 200);
            break; // 找到新卡片后退出循环
          }
        }
      }

      // 移除广告
      try {
        removeAds();
      } catch (e) {
        console.warn("移除广告失败:", e);
      }
    } catch (e) {
      console.error("Observer错误:", e);
    }
  });

  // 修改观察配置,增加更多细节监听
  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['class', 'style'] // 只监听类名和样式变化
  });

  const removeAds = () => {
    // 移除侧边广告
    const sideAd = safeQuerySelector(
      "body > main > div > div:nth-child(2) > div > div:nth-child(2) > div > a"
    );
    if (sideAd) {
      sideAd.style.display = "none";
    }

    // 移除所有广告链接
    const adSelectors = [
      'a[href*="gad.netease.com"]',
      'a[href*="ldmnq.com"]',
      'a[href*="ldy/ldymuban"]',
      'a[class*="block relative"]'
    ];

    adSelectors.forEach(selector => {
      safeQuerySelectorAll(selector).forEach(ad => {
        ad.style.display = "none";
      });
    });
  };

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

    removeAds();

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

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