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