// ==UserScript==
// @name 【cxalloy】一键通过 · 全页操作 ·(UI优化版)
// @namespace http://tampermonkey.net/
// @version 2.2.0
// @description 更新自动添加设备ID按钮。
// @author zhudaoyou
// @match https://tq.cxalloy.com/project/41416/checklists/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// ======================
// 🔧 工具函数
// ======================
function showToast(message, type = 'info') {
if (!document.getElementById('auto-pass-toast-style')) {
const style = document.createElement('style');
style.id = 'auto-pass-toast-style';
style.textContent = `
@keyframes fadeInOut {
0% { opacity: 0; transform: translateY(10px); }
20% { opacity: 1; transform: translateY(0); }
80% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-10px); }
}
.auto-pass-toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 8px 16px;
border-radius: 20px;
color: white;
font-size: 14px;
font-weight: 500;
z-index: 999999999;
pointer-events: none;
animation: fadeInOut 3s forwards;
}
`;
document.head?.appendChild(style);
}
const toast = document.createElement('div');
toast.className = 'auto-pass-toast';
toast.textContent = message;
toast.style.background = type === 'success' ? '#4CAF50' : type === 'warning' ? '#FF9800' : '#2196F3';
document.body?.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// 尝试向剪贴板写入文本
async function writeToClipboard(text) {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
} else {
console.warn("无法安全地写入剪贴板。请确保脚本运行在 HTTPS 环境下。");
return false;
}
} catch (err) {
console.error('写入剪贴板失败:', err);
return false;
}
}
// ======================
// 🛠️ 安全挂载
// ======================
class SafeMountManager {
constructor(createUIFunc) {
this.createUIFunc = createUIFunc;
this.containerId = 'auto-pass-widget-container';
this.init();
}
init() {
const waitForBody = () => {
if (document.body) {
this.createAndObserve();
} else {
setTimeout(waitForBody, 100);
}
};
waitForBody();
}
createAndObserve() {
this.createUI();
this.startObserver();
}
createUI() {
if (document.getElementById(this.containerId)) return;
const container = this.createUIFunc();
if (container) {
container.id = this.containerId;
document.body.appendChild(container);
}
}
startObserver() {
this.observer = new MutationObserver(() => {
if (!document.getElementById(this.containerId)) {
this.createUI();
}
});
this.observer.observe(document.body, { childList: true, subtree: true });
}
}
// ======================
// 🧩 主控类(无设置)
// ======================
class AutoPassWidget {
constructor() {
this.canMove = false;
this.isDragging = false;
this.mainClicks = 0;
this.mainLast = 0;
this.undoClicks = 0;
this.undoLast = 0;
this.copyPasteClicks = 0;
this.copyPasteLast = 0;
this.CLICK_GAP = 300;
this.lastClickedYesButtons = []; // 记录所有点击过的 yes 按钮
this.container = null;
this.mainBtn = null;
this.mainTextSpan = null;
this.undoBtn = null;
this.copyPasteBtn = null; // 新增
this.lockBtn = null;
this.boundDragHandler = this.dragHandler.bind(this);
this.boundStopDragging = this.stopDragging.bind(this);
}
createUIElements() {
this.container = document.createElement('div');
this.container.style.cssText = `
position: fixed;
top: 60px; /* 修改: 向下偏移 */
right: 20px;
z-index: 99999999;
display: flex;
flex-direction: column;
gap: 10px;
min-width: 100px;
transform: translateZ(0); /* 保持原有的硬件加速提示 */
`;
this.mainBtn = this.createMainButton();
this.undoBtn = this.createUndoButton();
this.copyPasteBtn = this.createCopyPasteButton(); // 新增
this.container.appendChild(this.mainBtn);
this.container.appendChild(this.undoBtn);
this.container.appendChild(this.copyPasteBtn); // 新增
// 设置默认文字和图标
this.mainTextSpan.textContent = 'Passed';
this.undoBtn.innerHTML = '撤销';
this.copyPasteBtn.innerHTML = 'ID'; // 新增
return this.container;
}
createMainButton() {
const btn = document.createElement('button');
btn.style.cssText = `
padding: 10px 14px;
font-size: 14px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #4CAF50, #2E7D32);
border: none;
border-radius: 12px;
cursor: pointer;
box-shadow: 0 3px 8px rgba(0,0,0,0.15), 0 5px 10px rgba(0,0,0,0.1);
outline: none;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
will-change: transform, box-shadow;
`;
this.mainTextSpan = document.createElement('span');
btn.appendChild(this.mainTextSpan);
this.lockBtn = this.createCornerButton('🔒', 'top: -6px; right: -6px;', '#6c757d', '长按1秒解锁移动');
btn.appendChild(this.lockBtn);
return btn;
}
createCornerButton(text, position, bg, title) {
const btn = document.createElement('button');
btn.innerHTML = text;
btn.title = title;
btn.style.cssText = `
position: absolute;
${position}
width: 20px;
height: 20px;
background: ${bg};
color: white;
border: none;
border-radius: 50%;
font-size: 11px;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
padding: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background 0.2s ease;
`;
return btn;
}
createUndoButton() {
const btn = document.createElement('button');
btn.title = '三击撤回上一步操作';
btn.style.cssText = `
padding: 10px 0;
font-size: 14px;
color: white;
background: linear-gradient(135deg, #FF9800, #E65100);
border: none;
border-radius: 12px;
cursor: pointer;
box-shadow: 0 3px 8px rgba(0,0,0,0.15), 0 5px 10px rgba(0,0,0,0.1);
outline: none;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
will-change: transform, box-shadow;
`;
return btn;
}
createCopyPasteButton() { // 新增
const btn = document.createElement('button');
btn.title = '双击执行添加ID 流程';
btn.style.cssText = `
padding: 10px 0;
font-size: 14px;
color: white;
background: linear-gradient(135deg, #2196F3, #0D47A1);
border: none;
border-radius: 12px;
cursor: pointer;
box-shadow: 0 3px 8px rgba(0,0,0,0.15), 0 5px 10px rgba(0,0,0,0.1);
outline: none;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
will-change: transform, box-shadow;
`;
return btn;
}
bindEvents() {
this.mainBtn.addEventListener('click', () => {
const now = Date.now();
this.mainClicks = (now - this.mainLast < this.CLICK_GAP) ? this.mainClicks + 1 : 1;
this.mainLast = now;
if (this.mainClicks >= 2) {
this.executeAutoClick();
this.mainClicks = 0;
this.mainLast = 0;
this.animateButton(this.mainBtn, '#81C784');
}
});
this.undoBtn.addEventListener('click', () => {
const now = Date.now();
this.undoClicks = (now - this.undoLast < this.CLICK_GAP) ? this.undoClicks + 1 : 1;
this.undoLast = now;
if (this.undoClicks >= 3) {
this.undoLastAction();
this.undoClicks = 0;
this.undoLast = 0;
this.animateButton(this.undoBtn, null, 'scale(1.25)');
}
});
// 新增: Copy & Paste 按钮事件
this.copyPasteBtn.addEventListener('click', () => {
const now = Date.now();
this.copyPasteClicks = (now - this.copyPasteLast < this.CLICK_GAP) ? this.copyPasteClicks + 1 : 1;
this.copyPasteLast = now;
if (this.copyPasteClicks >= 2) {
this.executeCopyPasteToEquipmentIDFlow();
this.copyPasteClicks = 0;
this.copyPasteLast = 0;
this.animateButton(this.copyPasteBtn, '#64B5F6');
}
});
this.lockBtn.addEventListener('mousedown', (e) => {
e.stopPropagation();
e.preventDefault();
this.startLongPress(e);
});
this.addHoverEffect(this.mainBtn, '0 5px 12px rgba(0,0,0,0.2), 0 7px 14px rgba(0,0,0,0.15)');
this.addHoverEffect(this.undoBtn, '0 5px 12px rgba(0,0,0,0.2), 0 7px 14px rgba(0,0,0,0.15)');
this.addHoverEffect(this.copyPasteBtn, '0 5px 12px rgba(0,0,0,0.2), 0 7px 14px rgba(0,0,0,0.15)'); // 新增
this.lockBtn.addEventListener('mouseenter', () => this.lockBtn.style.background = '#868e96');
this.lockBtn.addEventListener('mouseleave', () => this.lockBtn.style.background = '#6c757d');
}
// ✅【核心逻辑 - 全页处理】
executeAutoClick() {
const rows = document.querySelectorAll('.js-line-values.button-group.line__values.line__standard-question__values');
let count = 0;
for (const row of rows) {
// 检查该行是否已有 no 或 na 被选中
const hasNoSelected = row.querySelector('.js-line-value.line__value.line__standard-question__value.no.line__value--selected');
const hasNaSelected = row.querySelector('.js-line-value.line__value.line__standard-question__value.na.line__value--selected');
if (hasNoSelected || hasNaSelected) {
continue; // 跳过该行
}
// 查找未选中的 yes 按钮
const yesButton = row.querySelector('.js-line-value.line__value.line__standard-question__value.yes:not(.line__value--selected)');
if (yesButton) {
yesButton.click();
this.lastClickedYesButtons.push(yesButton); // 记录
count++;
}
}
if (count > 0) {
showToast(`✅ 已点击 ${count} 个 “Yes”`, 'success');
} else {
showToast('ℹ️ 所有行均已处理或无需操作', 'info');
}
}
// ✅【流程 - Copy & Paste from Connected Equipment Text to Equipment ID】
async executeCopyPasteToEquipmentIDFlow() {
// 1. 定位 class="connected equipment" 的元素,并获取其中 标签的文本内容
const connectedEquipmentElement = document.querySelector('.connected.equipment');
if (!connectedEquipmentElement) {
showToast('⚠️ 未找到 class="connected equipment" 的元素', 'warning');
return;
}
const anchorTag = connectedEquipmentElement.querySelector('a');
if (!anchorTag) {
showToast('⚠️ 未在 "connected equipment" 元素中找到 标签', 'warning');
return;
}
const linkText = (anchorTag.textContent || anchorTag.innerText).trim(); // 获取显示文本而非 href
if (!linkText) {
showToast('⚠️ 标签中没有有效的文本内容', 'warning');
return;
}
// 2. 查找 class="line__content__container" 包含 "Equipment ID:" 的行
const contentContainers = Array.from(document.querySelectorAll('.line__content__container'));
let targetContainer = null;
for (const container of contentContainers) {
if (container.textContent.includes('Equipment ID:')) {
targetContainer = container;
break;
}
}
if (!targetContainer) {
showToast('⚠️ 未找到包含 "Equipment ID:" 的行', 'warning');
return;
}
// 3. 在该容器内查找指定按钮
const targetButton = targetContainer.querySelector('.js-line-note-btn.line__options__button.undisablable');
if (!targetButton) {
showToast('⚠️ 未找到备注按钮', 'warning');
return;
}
// 4. 点击按钮
targetButton.click();
// 添加短暂延迟以等待页面元素加载
await new Promise(resolve => setTimeout(resolve, 500));
// 5. 查找文本域
const noteTextarea = document.querySelector('.js-line-note-textarea.sectionline-notetextarea');
if (!noteTextarea) {
showToast('⚠️ 未找到备注文本框', 'warning');
return;
}
// 6. 将链接文本粘贴到文本域
noteTextarea.value = linkText;
// 触发 input 事件,通知框架值已更改
noteTextarea.dispatchEvent(new Event('input', { bubbles: true }));
showToast(`✅ 已将文本 "${linkText}" 粘贴至备注框`, 'success');
// 7. 查找并点击保存按钮
const saveButton = document.querySelector('.js-line-note-save.inline-action.primary.submit');
if (!saveButton) {
showToast('⚠️ 未找到保存按钮', 'warning');
return;
}
saveButton.click();
showToast(`✅ 已点击保存按钮`, 'success');
}
// ✅【撤回逻辑 - 撤回最后一批操作】
undoLastAction() {
if (this.lastClickedYesButtons.length === 0) {
showToast('ℹ️ 无可撤回的操作', 'info');
return;
}
let undoneCount = 0;
// 从后往前撤回,恢复最近的一批操作
for (let i = this.lastClickedYesButtons.length - 1; i >= 0; i--) {
const button = this.lastClickedYesButtons[i];
if (button && button.isConnected && button.classList.contains('line__value--selected')) {
button.click(); // 尝试取消选中
undoneCount++;
}
}
if (undoneCount > 0) {
showToast(`↩️ 已撤回 ${undoneCount} 个 “Yes”`, 'success');
} else {
showToast('⚠️ 选中状态已变更,无法撤回', 'warning');
}
// 清空记录
this.lastClickedYesButtons = [];
}
// ===== UI 方法 =====
startLongPress(e) {
this.dragStart = { x: e.clientX, y: e.clientY };
const style = window.getComputedStyle(this.container);
const windowWidth = window.innerWidth;
this.elementStart = {
x: windowWidth - parseFloat(style.right) - this.container.offsetWidth,
y: parseFloat(style.top)
};
this.longPressTimer = setTimeout(() => {
this.enableDragging();
}, 1000);
}
enableDragging() {
this.canMove = true;
this.container.style.cursor = 'grabbing';
this.lockBtn.innerHTML = '🔓';
this.lockBtn.style.background = '#007BFF';
this.mainBtn.style.boxShadow = '0 0 15px rgba(76, 175, 80, 0.7)';
this.undoBtn.style.boxShadow = '0 0 15px rgba(255, 152, 0, 0.7)';
this.copyPasteBtn.style.boxShadow = '0 0 15px rgba(33, 150, 243, 0.7)'; // 新增
document.addEventListener('mousemove', this.boundDragHandler);
document.addEventListener('mouseup', this.boundStopDragging);
}
dragHandler(e) {
if (!this.canMove) return;
const deltaX = e.clientX - this.dragStart.x;
const deltaY = e.clientY - this.dragStart.y;
const newLeft = this.elementStart.x + deltaX;
const newTop = this.elementStart.y + deltaY;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const width = this.container.offsetWidth;
const height = this.container.offsetHeight;
const clampedLeft = Math.max(0, Math.min(windowWidth - width, newLeft));
const clampedTop = Math.max(0, Math.min(windowHeight - height, newTop));
this.container.style.right = `${windowWidth - clampedLeft - width}px`;
this.container.style.top = `${clampedTop}px`;
}
stopDragging() {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
this.canMove = false;
this.container.style.cursor = 'default';
this.lockBtn.innerHTML = '🔒';
this.lockBtn.style.background = '#6c757d';
this.mainBtn.style.boxShadow = '0 3px 8px rgba(0,0,0,0.15), 0 5px 10px rgba(0,0,0,0.1)';
this.undoBtn.style.boxShadow = '0 3px 8px rgba(0,0,0,0.15), 0 5px 10px rgba(0,0,0,0.1)';
this.copyPasteBtn.style.boxShadow = '0 3px 8px rgba(0,0,0,0.15), 0 5px 10px rgba(0,0,0,0.1)'; // 新增
document.removeEventListener('mousemove', this.boundDragHandler);
document.removeEventListener('mouseup', this.boundStopDragging);
}
animateButton(btn, bgColor = null, transform = 'translateY(-3px)') {
if (bgColor) btn.style.background = bgColor;
btn.style.transform = transform;
setTimeout(() => {
if (bgColor) {
if (btn === this.mainBtn) {
btn.style.background = 'linear-gradient(135deg, #4CAF50, #2E7D32)';
} else if (btn === this.undoBtn) {
btn.style.background = 'linear-gradient(135deg, #FF9800, #E65100)';
} else if (btn === this.copyPasteBtn) { // 新增
btn.style.background = 'linear-gradient(135deg, #2196F3, #0D47A1)';
}
}
btn.style.transform = 'translateY(0)';
}, 150);
}
addHoverEffect(btn, hoverShadow) {
const normalShadow = btn.style.boxShadow;
btn.addEventListener('mouseenter', () => {
if (!this.canMove) {
btn.style.transform = 'translateY(-3px)';
btn.style.boxShadow = hoverShadow;
}
});
btn.addEventListener('mouseleave', () => {
if (!this.canMove) {
btn.style.transform = 'translateY(0)';
btn.style.boxShadow = normalShadow;
}
});
}
}
// ======================
// 🚀 启动
// ======================
const widget = new AutoPassWidget();
const mounter = new SafeMountManager(() => {
const container = widget.createUIElements();
widget.bindEvents();
return container;
});
})();