// ==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);
}
});
})();