MaaCopilotPlus

增强MAA作业站的筛选功能

目前為 2025-02-28 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

          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, skillNumber] = operatorText.split(" ");
        console.log(`提取干员: ${operatorName}, 技能: ${skillNumber}`);
        // 检查是否在我的干员列表中
        if (
          operatorName &&
          !myOperators.some((op) => op.name === operatorName)
        ) {
          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;
        }
      }
    }
  });

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

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

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