// ==UserScript== // @name 【cxalloy】一键通过 · 全页操作 ·(UI优化版) // @namespace http://tampermonkey.net/ // @version 2.1.0 // @description 双击主按钮:仅点击未选中的 Passed。取消设置面板。UI已居中,字体大小已统一。 // @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); } // ====================== // 🛠️ 安全挂载 // ====================== 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.CLICK_GAP = 300; this.lastClickedYesButtons = []; // 记录所有点击过的 yes 按钮 this.container = null; this.mainBtn = null; this.mainTextSpan = null; this.undoBtn = 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.container.appendChild(this.mainBtn); this.container.appendChild(this.undoBtn); // 设置默认文字和图标 this.mainTextSpan.textContent = 'Passed'; this.undoBtn.innerHTML = '撤销'; // 保持原有内容 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; /* 使用 Flexbox 实现内容居中 */ 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'); // 移除 margin-right,让文字在 flex 容器中居中 // this.mainTextSpan.style.marginRight = '20px'; 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; /* 使用 Flexbox 实现内容居中 */ 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)'); } }); 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.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'); } } // ✅【撤回逻辑 - 撤回最后一批操作】 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, // left坐标 y: parseFloat(style.top) // 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)'; 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)); // 转换回 right 和 top 定位 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)'; 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) { btn.style.background = btn === this.mainBtn ? 'linear-gradient(135deg, #4CAF50, #2E7D32)' : 'linear-gradient(135deg, #FF9800, #E65100)'; } 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; }); })();