// ==UserScript== // @name 标注助手 - 框数统计 // @namespace https://dawnchan.online // @version 1.0 // @description 自动累加标注框数,支持手动累加和自动遍历,可自定义等待时间、帧数,面板可拖动/缩放 // @author cc // @match *://*/*pointcloud* // 请根据实际页面修改 // @match *://*/w/pointcloud/* // @grant none // ==/UserScript== (function() { 'use strict'; console.log('标注助手脚本已加载'); // ========== 配置 ========== const STORAGE_KEY = 'daily_box_total'; const DEFAULT_WAIT_MS = 1500; const DEFAULT_MAX_FRAMES = 100; // ========== 核心功能 ========== function loadTotal() { let val = localStorage.getItem(STORAGE_KEY); return val ? parseInt(val, 10) : 0; } function saveTotal(total) { localStorage.setItem(STORAGE_KEY, total); } function updateDisplay() { const totalSpan = document.getElementById('box-total-display'); if (totalSpan) totalSpan.textContent = loadTotal(); } // 累加当前帧,返回 {frameTotal, runningTotal} function addCurrentFrame() { let frameTotal = 0; const elements = document.querySelectorAll('.left-seg-num'); elements.forEach(el => { let match = el.innerText.match(/\(\s*(\d+)\s*\)/); if (match) frameTotal += parseInt(match[1], 10); }); let runningTotal = loadTotal(); runningTotal += frameTotal; saveTotal(runningTotal); updateDisplay(); return { frameTotal, runningTotal }; } function resetTotal() { if (confirm('确定要重置累计框数吗?')) { localStorage.removeItem(STORAGE_KEY); updateDisplay(); appendLog('累计已重置为0'); } } // 日志 function appendLog(msg) { const logArea = document.getElementById('stat-log'); if (logArea) { const entry = document.createElement('div'); entry.textContent = msg; entry.style.borderBottom = '1px solid #444'; entry.style.padding = '2px 0'; logArea.appendChild(entry); logArea.scrollTop = logArea.scrollHeight; } else { console.log(msg); } } function clearLog() { const logArea = document.getElementById('stat-log'); if (logArea) logArea.innerHTML = ''; } // 自动遍历(修复逻辑) async function autoCountAllFrames(waitMs, maxFrames) { const nextBtn = document.querySelector('#page-actions button:last-child'); if (!nextBtn) { alert('未找到“下一帧”按钮,请检查页面结构'); return; } waitMs = parseInt(waitMs, 10); if (isNaN(waitMs) || waitMs <= 0) waitMs = DEFAULT_WAIT_MS; maxFrames = parseInt(maxFrames, 10); if (isNaN(maxFrames) || maxFrames <= 0) maxFrames = Infinity; if (!confirm(`将自动统计最多 ${maxFrames === Infinity ? '全部' : maxFrames} 帧,每帧等待 ${waitMs} 毫秒。\n请确保当前是第一帧且数字已加载。\n确定开始吗?`)) return; appendLog(`开始自动统计,等待 ${waitMs}ms,最多 ${maxFrames === Infinity ? '不限' : maxFrames} 帧`); let countedFrames = 0; // 1. 先累加当前帧 const first = addCurrentFrame(); countedFrames++; appendLog(`帧 ${countedFrames}: ${first.frameTotal} 框 | 累计: ${first.runningTotal}`); // 2. 循环累加后续帧 while (countedFrames < maxFrames) { // 检查按钮是否禁用(最后一帧) if (nextBtn.disabled || nextBtn.classList.contains('is-disabled')) { appendLog(`已到达最后一帧,共统计 ${countedFrames} 帧`); alert(`自动统计完成!共统计 ${countedFrames} 帧,累计框数:${loadTotal()}`); return; } // 点击下一帧 nextBtn.click(); // 等待加载 await new Promise(resolve => setTimeout(resolve, waitMs)); // 累加新帧 const result = addCurrentFrame(); countedFrames++; appendLog(`帧 ${countedFrames}: ${result.frameTotal} 框 | 累计: ${result.runningTotal}`); } // 达到最大帧数 appendLog(`已达到设定的最大帧数 ${maxFrames},自动停止`); alert(`已达到最大帧数 ${maxFrames},累计框数:${loadTotal()}`); } // ========== 可拖动 & 可缩放面板 ========== function makeDraggableAndResizable(panel) { // 拖动 let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; const header = panel.querySelector('.panel-header'); if (!header) return; header.style.cursor = 'move'; header.onmousedown = dragMouseDown; function dragMouseDown(e) { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; let top = panel.offsetTop - pos2; let left = panel.offsetLeft - pos1; // 边界限制(防止拖出屏幕) top = Math.min(Math.max(0, top), window.innerHeight - panel.offsetHeight); left = Math.min(Math.max(0, left), window.innerWidth - panel.offsetWidth); panel.style.top = top + 'px'; panel.style.left = left + 'px'; panel.style.bottom = 'auto'; panel.style.right = 'auto'; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } // 缩放(右下角) const resizeHandle = document.createElement('div'); resizeHandle.style.cssText = ` position: absolute; bottom: 0; right: 0; width: 15px; height: 15px; cursor: nw-resize; background: #7f8c8d; border-radius: 2px 0 2px 0; `; panel.appendChild(resizeHandle); let startX, startY, startWidth, startHeight; resizeHandle.onmousedown = function(e) { e.preventDefault(); startX = e.clientX; startY = e.clientY; startWidth = panel.offsetWidth; startHeight = panel.offsetHeight; document.onmousemove = resize; document.onmouseup = stopResize; }; function resize(e) { let newWidth = startWidth + (e.clientX - startX); let newHeight = startHeight + (e.clientY - startY); // 限制最小/最大尺寸 newWidth = Math.min(Math.max(260, newWidth), 600); newHeight = Math.min(Math.max(280, newHeight), 600); panel.style.width = newWidth + 'px'; panel.style.height = newHeight + 'px'; } function stopResize() { document.onmousemove = null; document.onmouseup = null; } } // ========== UI 构建 ========== function createControlPanel() { if (document.getElementById('box-helper-panel')) return; const panel = document.createElement('div'); panel.id = 'box-helper-panel'; panel.style.cssText = ` position: fixed; top: 10px; right: 10px; background: #2c3e50; color: #ecf0f1; border-radius: 8px; padding: 12px; z-index: 999999; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 13px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); width: 280px; height: auto; text-align: left; resize: both; overflow: auto; `; // 标题(用于拖动) const header = document.createElement('div'); header.className = 'panel-header'; header.textContent = '框数统计'; header.style.fontWeight = 'bold'; header.style.marginBottom = '8px'; header.style.fontSize = '16px'; header.style.padding = '4px 0'; header.style.userSelect = 'none'; panel.appendChild(header); // 累计显示 const totalDiv = document.createElement('div'); totalDiv.style.marginBottom = '12px'; totalDiv.style.fontSize = '14px'; totalDiv.innerHTML = `累计框数:${loadTotal()}`; panel.appendChild(totalDiv); // 按钮组 const btnGroup = document.createElement('div'); btnGroup.style.display = 'flex'; btnGroup.style.gap = '8px'; btnGroup.style.marginBottom = '12px'; const addBtn = document.createElement('button'); addBtn.textContent = '累加当前帧'; addBtn.style.cssText = `flex:1; background:#3498db; border:none; color:#fff; padding:6px; border-radius:4px; cursor:pointer;`; addBtn.onclick = () => { const { frameTotal, runningTotal } = addCurrentFrame(); appendLog(`累加当前帧: ${frameTotal} 框 | 累计: ${runningTotal}`); }; const resetBtn = document.createElement('button'); resetBtn.textContent = '重置累计'; resetBtn.style.cssText = `flex:1; background:#e67e22; border:none; color:#fff; padding:6px; border-radius:4px; cursor:pointer;`; resetBtn.onclick = resetTotal; btnGroup.appendChild(addBtn); btnGroup.appendChild(resetBtn); panel.appendChild(btnGroup); // 自定义参数区域 const paramDiv = document.createElement('div'); paramDiv.style.marginBottom = '12px'; paramDiv.style.display = 'flex'; paramDiv.style.gap = '8px'; paramDiv.style.alignItems = 'center'; paramDiv.style.flexWrap = 'wrap'; const waitLabel = document.createElement('span'); waitLabel.textContent = '等待(ms):'; waitLabel.style.fontSize = '12px'; const waitInput = document.createElement('input'); waitInput.type = 'number'; waitInput.value = DEFAULT_WAIT_MS; waitInput.style.width = '70px'; waitInput.style.padding = '4px'; waitInput.style.borderRadius = '4px'; waitInput.style.border = 'none'; const maxLabel = document.createElement('span'); maxLabel.textContent = '最多帧数:'; maxLabel.style.fontSize = '12px'; const maxInput = document.createElement('input'); maxInput.type = 'number'; maxInput.value = ''; maxInput.placeholder = '不限'; maxInput.style.width = '70px'; maxInput.style.padding = '4px'; maxInput.style.borderRadius = '4px'; maxInput.style.border = 'none'; const autoBtn = document.createElement('button'); autoBtn.textContent = '自动遍历'; autoBtn.style.cssText = `background:#2ecc71; border:none; color:#fff; padding:4px 8px; border-radius:4px; cursor:pointer;`; autoBtn.onclick = () => { autoCountAllFrames(waitInput.value, maxInput.value); }; // 创建容器 const maxGroup = document.createElement('span'); maxGroup.style.whiteSpace = 'nowrap'; // 将标签和输入框放入容器 maxGroup.appendChild(maxLabel); maxGroup.appendChild(maxInput); paramDiv.appendChild(maxGroup); paramDiv.appendChild(waitLabel); paramDiv.appendChild(waitInput); // paramDiv.appendChild(maxLabel); // paramDiv.appendChild(maxInput); paramDiv.appendChild(autoBtn); panel.appendChild(paramDiv); // 日志区域 const logContainer = document.createElement('div'); logContainer.style.marginTop = '8px'; logContainer.style.borderTop = '1px solid #555'; logContainer.style.paddingTop = '8px'; const logHeader = document.createElement('div'); logHeader.style.display = 'flex'; logHeader.style.justifyContent = 'space-between'; logHeader.style.marginBottom = '4px'; const logTitle = document.createElement('span'); logTitle.textContent = '统计记录'; logTitle.style.fontWeight = 'bold'; const clearLogBtn = document.createElement('button'); clearLogBtn.textContent = '清空'; clearLogBtn.style.cssText = `background:#7f8c8d; border:none; color:#fff; padding:2px 6px; border-radius:4px; cursor:pointer; font-size:11px;`; clearLogBtn.onclick = clearLog; logHeader.appendChild(logTitle); logHeader.appendChild(clearLogBtn); logContainer.appendChild(logHeader); const logArea = document.createElement('div'); logArea.id = 'stat-log'; logArea.style.maxHeight = '200px'; logArea.style.overflowY = 'auto'; logArea.style.backgroundColor = '#1e2a36'; logArea.style.padding = '4px'; logArea.style.borderRadius = '4px'; logArea.style.fontSize = '11px'; logArea.style.fontFamily = 'monospace'; logContainer.appendChild(logArea); panel.appendChild(logContainer); document.body.appendChild(panel); console.log('✅ 控制面板已添加到页面'); makeDraggableAndResizable(panel); } function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(createControlPanel, 500); }); } else { setTimeout(createControlPanel, 500); } } init(); })();