// ==UserScript== // @name 【cxalloy】一键通过 · 全页操作 ·(UI优化版-高速-修复版) // @namespace http://tampermonkey.net/ // @version 2.7.0 // @description 修复:ID流程现在通过class="line__content__container"定位父容器,更加精确。 // @author zhudaoyou // @match https://tq.cxalloy.com/* // @grant none // ==/UserScript== (function() { 'use strict'; // ====================== // ⚙️ 配置选项 // ====================== const CONFIG = { BATCH_MODE: true, // 设为 true 使用批量模式(更快),设为 false 使用串行模式(更稳) TIMEOUT_MS: 10000, // 单次等待超时时间(毫秒),在批量模式下,这是等待所有操作完成的总时间 DEBUG_LOGS: false // 设为 true 可在控制台查看详细日志,用于调试 }; // ====================== // 🔧 全局变量 - 持久化撤回队列与控制标志 // ====================== let globalUndoQueue = []; let shouldAutoUndo = false; // 新增标志:是否应该启用自动撤回 function log(...args) { if (CONFIG.DEBUG_LOGS) { console.log('[AutoPass]', ...args); } } 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; } } /** * 监视DOM变化,直到满足条件为止 * @param {Function} condition - 判断条件的函数,返回true表示满足 * @param {number} timeoutMs - 超时时间,单位毫秒 * @returns {Promise} - 成功返回true,超时返回false */ function waitForMutation(condition, timeoutMs = CONFIG.TIMEOUT_MS) { return new Promise((resolve) => { const observer = new MutationObserver((mutations) => { if (condition(mutations)) { observer.disconnect(); log('Mutation observed and condition met.'); resolve(true); } }); const timeoutId = setTimeout(() => { observer.disconnect(); log('WaitForMutation timed out.'); resolve(false); // 超时 }, timeoutMs); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); }); } /** * 批量监视DOM变化,直到所有指定元素都满足条件 * @param {Array} elements - 要监视的DOM元素列表 * @param {Function} condition - 判断单个元素是否满足条件的函数 * @param {number} timeoutMs - 总超时时间 * @returns {Promise<{fulfilled: number, rejected: number}>} - 返回成功和失败的数量 */ async function waitForAllMutations(elements, condition, timeoutMs = CONFIG.TIMEOUT_MS) { log(`Waiting for ${elements.length} elements to fulfill condition...`); // 为每个元素创建一个Promise const promises = elements.map(element => waitForMutation(() => condition(element), timeoutMs) .then(fulfilled => ({ element, fulfilled })) ); const results = await Promise.allSettled(promises); const fulfilled = results.filter(r => r.status === 'fulfilled' && r.value.fulfilled).length; const rejected = results.length - fulfilled; log(`Batch wait completed. Fulfilled: ${fulfilled}, Rejected: ${rejected}`); return { fulfilled, rejected }; } // ====================== // 🛠️ 安全挂载 // ====================== 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.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); this.undoInterval = null; // 用于自动撤回的定时器 } 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(); // 这个函数现在是异步的,但不会阻塞UI线程 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) { log('Manual undo triggered via triple-click.'); this.undoLastAction(); // 手动触发撤回 this.undoClicks = 0; this.undoLast = 0; this.animateButton(this.undoBtn, null, 'scale(1.25)'); } }); 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'); } // ✅【核心逻辑 - 全页处理 - 支持批量/串行】 async executeAutoClick() { const rows = document.querySelectorAll('.js-line-values.button-group.line__values.line__standard-question__values'); let targetButtons = []; // 第一步:筛选出所有需要点击的按钮 for (const row of rows) { 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; const yesButton = row.querySelector('.js-line-value.line__value.line__standard-question__value.yes:not(.line__value--selected)'); if (yesButton) { targetButtons.push(yesButton); } } if (targetButtons.length === 0) { showToast('ℹ️ 所有行均已处理或无需操作', 'info'); return; } log(`Found ${targetButtons.length} buttons to click.`); if (CONFIG.BATCH_MODE) { // --- 批量模式 --- log('Running in BATCH mode...'); // 1. 快速点击所有按钮 targetButtons.forEach(btn => { btn.click(); // 关键:将按钮加入全局撤回队列 globalUndoQueue.push(btn); }); showToast(`⏳ 批量点击中,请稍候...`, 'info'); // 2. 统一等待所有按钮被选中 const condition = (element) => element.classList.contains('line__value--selected'); const result = await waitForAllMutations(targetButtons, condition); if (result.rejected > 0) { showToast(`⚠️ 批量操作部分失败,${result.fulfilled} 成功,${result.rejected} 超时`, 'warning'); } else { showToast(`✅ 批量操作成功,共 ${result.fulfilled} 个 “Yes”`, 'success'); } } else { // --- 串行模式 --- log('Running in SERIAL mode...'); let count = 0; for (const btn of targetButtons) { btn.click(); // 关键:将按钮加入全局撤回队列 globalUndoQueue.push(btn); const wasSelected = await waitForMutation((mutations) => { return btn.classList.contains('line__value--selected'); }); if (wasSelected) { count++; } else { console.warn('警告:等待按钮选中状态超时', btn); showToast(`⚠️ 操作超时,可能影响后续结果`, 'warning'); break; } } if (count > 0) { showToast(`✅ 已点击 ${count} 个 “Yes”`, 'success'); } } // 操作完成后,不再自动启动撤回检查。只有手动触发撤销才会启动。 log('操作完成,未启动自动撤回。'); } // ✅【流程 - Copy & Paste from Connected Equipment Text to Equipment ID】 async executeCopyPasteToEquipmentIDFlow() { try { // 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(); if (!linkText) { showToast('⚠️ 标签中没有有效的文本内容', 'warning'); return; } log(`步骤1: 成功获取链接文本 "${linkText}"`); // 2. 定位到 class="js-line-description line__description--regular" 其中有 "Equipment ID:" 文本的行 const equipmentIdLabelDiv = Array.from(document.querySelectorAll('.js-line-description.line__description--regular')) .find(div => (div.textContent || div.innerText).trim() === 'Equipment ID:'); if (!equipmentIdLabelDiv) { showToast('⚠️ 未找到文本为 "Equipment ID:" 的描述标签', 'warning'); return; } log(`步骤2: 成功定位到 "Equipment ID:" 标签元素。`, equipmentIdLabelDiv); // 3. 查找最近的祖先元素,其 class 属性包含 "line__content__container" let targetContainer = equipmentIdLabelDiv.parentElement; while (targetContainer && targetContainer !== document.body) { if (targetContainer.classList && targetContainer.classList.contains('line__content__container')) { break; } targetContainer = targetContainer.parentElement; } if (!targetContainer) { showToast('❌ 未能找到 class="line__content__container" 的祖先容器', 'warning'); return; } log(`步骤2.5: 已定位到目标容器。`, targetContainer); // 4. 在目标容器内点击 class="js-line-note-btn line__options__button undisablable" const targetButton = targetContainer.querySelector('.js-line-note-btn.line__options__button.undisablable'); if (!targetButton) { showToast('⚠️ 在目标行的容器中未找到备注按钮', 'warning'); return; } targetButton.click(); log(`步骤3: 已点击备注按钮。`); // 添加短暂延迟以等待页面元素加载 await new Promise(resolve => setTimeout(resolve, 500)); // 5. 在目标容器内查找 class="js-line-note-textarea sectionline-notetextarea" const noteTextarea = targetContainer.querySelector('.js-line-note-textarea.sectionline-notetextarea'); if (!noteTextarea) { showToast('⚠️ 在目标行的容器中未找到备注文本框', 'warning'); return; } noteTextarea.value = linkText; noteTextarea.dispatchEvent(new Event('input', { bubbles: true })); log(`步骤4: 已将文本 "${linkText}" 粘贴至备注框。`); // 6. 在目标容器内点击 class="js-line-note-save inline-action primary submit" const saveButton = targetContainer.querySelector('.js-line-note-save.inline-action.primary.submit'); if (!saveButton) { showToast('⚠️ 在目标行的容器中未找到保存按钮', 'warning'); return; } saveButton.click(); log(`步骤5: 正在点击保存按钮...`); // 等待保存操作完成 (可以检查保存按钮是否消失) const wasSaved = await waitForMutation((mutations) => { return !targetContainer.contains(saveButton); }); if (wasSaved) { showToast(`✅ 已成功将 "${linkText}" 保存至 Equipment ID`, 'success'); } else { showToast(`⚠️ 保存操作可能未完成`, 'warning'); } } catch (error) { console.error('执行 ID 流程时发生错误:', error); showToast('❌ 执行 ID 流程时发生错误,请查看控制台', 'warning'); } } // ✅【撤回逻辑 - 撤回全局队列中的所有有效操作】 async undoLastAction() { // 清理队列:移除页面上不存在或未选中的按钮 globalUndoQueue = globalUndoQueue.filter( btn => btn && btn.isConnected && btn.classList.contains('line__value--selected') ); if (globalUndoQueue.length === 0) { showToast('ℹ️ 无可撤回的操作', 'info'); return; } log(`Attempting to undo ${globalUndoQueue.length} actions from the queue.`); if (CONFIG.BATCH_MODE) { // --- 批量撤回 --- log('Running UNDO in BATCH mode...'); // 1. 快速点击所有队列中的按钮以取消选中 globalUndoQueue.forEach(btn => btn.click()); showToast(`⏳ 批量撤回中,请稍候...`, 'info'); // 2. 统一等待所有按钮被取消选中 const condition = (element) => !element.classList.contains('line__value--selected'); const result = await waitForAllMutations(globalUndoQueue, condition); if (result.rejected > 0) { showToast(`⚠️ 批量撤回部分失败,${result.fulfilled} 成功,${result.rejected} 超时`, 'warning'); log(`There are still ${result.rejected} items in the undo queue that are not yet unselected.`); } else { showToast(`✅ 批量撤回成功,共 ${result.fulfilled} 个 “Yes”`, 'success'); // 如果全部成功,则清空队列 globalUndoQueue = []; } } else { // --- 串行撤回 --- log('Running UNDO in SERIAL mode...'); let undoneCount = 0; for (let i = 0; i < globalUndoQueue.length; i++) { const btn = globalUndoQueue[i]; btn.click(); const wasUnselected = await waitForMutation((mutations) => { return !btn.classList.contains('line__value--selected'); }); if (wasUnselected) { undoneCount++; } else { console.warn('警告:等待按钮取消选中状态超时', btn); log(`Failed to unselect button at index ${i}. Removing from queue.`); } } if (undoneCount > 0) { showToast(`↩️ 已撤回 ${undoneCount} 个 “Yes”`, 'success'); } // 清空整个队列,因为我们已经对队列中的所有项目都尝试了操作 globalUndoQueue = []; } } // 启动自动撤回检查(此函数现在只在手动触发撤销后被调用) startAutoUndoCheck() { // 如果已有定时器在运行,则先清除它 if (this.undoInterval) { clearInterval(this.undoInterval); } // 设置一个定时器,持续检查队列 this.undoInterval = setInterval(() => { if (globalUndoQueue.length > 0) { log('Auto-undo check found pending operations. Initiating auto-undo.'); this.undoLastAction(); // 撤回操作后,停止自动检查 clearInterval(this.undoInterval); this.undoInterval = null; } }, 1000); // 每秒检查一次 } // ===== 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; }); })();