// ==UserScript== // @name 鼠标点击录制助手-比例坐标版-多目标区域+优化+UI提升+调试 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 增强版:支持多目标区域,优化回放,UI和调试,详尽调试信息。 // @author ChatGPT // @match *://*/* // @grant none // ==/UserScript== // 匿名立即执行函数,防止变量污染全局作用域 (function() { 'use strict'; // 开启严格模式,有助于编写更规范、更少错误的代码 // -------------- 配置参数 -------------- const DEBUG_MODE = false; // 定义一个常量,用于控制是否开启调试模式。如果为 true,则会在控制台输出调试信息。 // 快捷键配置(无需修改,支持自定义) const defaultHotkeys = { // 定义默认的快捷键配置对象 startRecord: { ctrlKey: true, altKey:false, shiftKey:false, key: 'w' }, // Ctrl+W 开始录制 pauseRecord: { ctrlKey: true, altKey:false, shiftKey:false, key: 'e' }, // Ctrl+E 暂停录制 resumeRecord: { ctrlKey: true, altKey:false, shiftKey:false, key: 'r' }, // Ctrl+R 继续录制 stopRecord: { ctrlKey: true, altKey:false, shiftKey:false, key: 'q' }, // Ctrl+Q 停止录制 startPlayback: { ctrlKey: true, altKey:false, shiftKey:false, key: 'p' }, // Ctrl+P 开始回放 stopPlayback: { ctrlKey: true, altKey:false, shiftKey:false, key: 'o' }, // Ctrl+O 停止回放 }; // -------------- 变量定义 -------------- let hotkeys = JSON.parse(localStorage.getItem('mouseRecHotkeys')) || defaultHotkeys; // 从 localStorage 加载自定义快捷键,如果不存在则使用默认值 let isRecording = false, isPaused = false; // 记录是否正在录制、是否暂停录制的状态 let lastClickTime = 0; // 记录上一次点击的时间戳,用于计算点击间隔 let recordedClicks = []; // 存储录制到的所有点击事件数据 let totalRecordedTime = 0; // 记录所有点击事件的总延迟时间 let playbackTimer = null; // 回放计时器 ID,用于控制回放的暂停和停止 let playbackIndex = 0; // 回放时当前点击事件的索引 let playbackStartTime = 0; // 回放开始的时间戳,用于计算已回放时长 // 目标区域数组 let targetAreas = JSON.parse(localStorage.getItem('mouseRecTargetAreas')) || []; // 从 localStorage 加载用户定义的目标区域,如果不存在则为空数组 // UI位置记忆 let savedPanelPos = JSON.parse(localStorage.getItem('mouseRecPanelPos')) || null; // 从 localStorage 加载面板的保存位置 let savedBtnPos = JSON.parse(localStorage.getItem('mouseRecBtnPos')) || null; // 从 localStorage 加载入口按钮的保存位置 // 录制时是否阻止网页事件 let preventClicksDuringRecord = false; // 记录录制时是否阻止页面默认点击事件的标志 // 调试开关 let debugMode = DEBUG_MODE; // 将配置参数 DEBUG_MODE 赋值给局部变量 debugMode function debugLog(msg) { if(debugMode) console.log('[录制调试]', msg); } // 调试日志函数,只有在 debugMode 为 true 时才输出信息 // -------------- UI元素创建 -------------- //【入口按钮】 const entryBtn = document.createElement('button'); // 创建一个按钮元素作为脚本的入口 entryBtn.textContent = '寻到大千脚本'; // 设置按钮的文本内容 Object.assign(entryBtn.style, { // 使用 Object.assign 批量设置按钮的 CSS 样式 position: 'fixed', // 固定定位,使其不随页面滚动 top: savedBtnPos?.top ?? '50px', // 从 localStorage 加载顶部位置,否则默认为 50px right: savedBtnPos?.right ?? '10px', // 从 localStorage 加载右侧位置,否则默认为 10px zIndex: 999999, // 设置高层级,确保按钮显示在其他元素之上 padding: '6px 12px', // 内边距 fontSize: '14px', // 字体大小 cursor: 'move', // 鼠标悬停时显示为移动光标,提示可拖动 backgroundColor: '#d9534f', // 背景颜色 color: 'white', // 文本颜色 border: 'none', // 无边框 borderRadius: '4px', // 圆角 userSelect: 'none', // 禁止用户选择文本 transition: 'background-color 0.2s', // 鼠标悬停时背景颜色变化的过渡效果 }); document.body.appendChild(entryBtn); // 将入口按钮添加到页面的 body 中 // 拖动入口按钮 (function dragElement(el, saveKey){ // 定义一个用于拖动元素的匿名立即执行函数,并接受元素和保存键作为参数 let isDown = false, offsetX=0, offsetY=0; // 记录鼠标是否按下、鼠标相对于元素左上角的偏移量 el.addEventListener('mousedown', e => { // 监听元素的鼠标按下事件 // 如果不是左键点击 或者 点击目标是按钮本身,则不进行拖动 if(e.button!==0 || e.target.tagName==='BUTTON') return; isDown = true; // 标记鼠标已按下 const rect = el.getBoundingClientRect(); // 获取元素的尺寸和位置 offsetX = e.clientX - rect.left; // 计算鼠标点击位置与元素左边缘的距离 offsetY = e.clientY - rect.top; // 计算鼠标点击位置与元素上边缘的距离 document.body.style.userSelect = 'none'; // 拖动时禁止页面文本选择 }); window.addEventListener('mousemove', e => { // 监听窗口的鼠标移动事件 if(!isDown) return; // 如果鼠标未按下,则不执行 let left=e.clientX - offsetX, top=e.clientY - offsetY; // 计算元素的新位置 // 限制边界,确保元素不会拖出可视区域 left = Math.min(Math.max(0, left), window.innerWidth - el.offsetWidth); top = Math.min(Math.max(0, top), window.innerHeight - el.offsetHeight); el.style.left=left+'px'; el.style.top=top+'px'; // 更新元素的 left 和 top 样式 el.style.right='auto'; el.style.bottom='auto'; // 清除 right 和 bottom 样式,避免冲突 }); window.addEventListener('mouseup', e=>{ // 监听窗口的鼠标松开事件 if(isDown){ isDown=false; document.body.style.userSelect='';} // 标记鼠标已松开,恢复页面文本选择 // 位置存储:将元素当前的位置保存到 localStorage localStorage.setItem(saveKey, JSON.stringify({top:el.style.top, left:el.style.left, right:el.style.right, bottom:el.style.bottom})); }); })(entryBtn,'mouseRecBtnPos'); // 立即执行拖动函数,传入入口按钮和其位置的 localStorage 键名 // 目标区域设置提示 function alertMsg(msg) { alert(msg); } // 封装一个简单的弹窗提示函数 // 面板 const panel = document.createElement('div'); // 创建主功能面板元素 Object.assign(panel.style, { // 批量设置面板的 CSS 样式 position: 'fixed', // 固定定位 top: savedPanelPos?.top ?? '50px', // 从 localStorage 加载顶部位置,否则默认为 50px right: savedPanelPos?.right ?? '10px', // 从 localStorage 加载右侧位置,否则默认为 10px width: '370px', // 固定宽度 maxHeight:'90vh', // 最大高度为视口高度的 90% backgroundColor: '#fafafa', // 背景颜色 border: '2px solid #555', // 边框 padding: '12px', // 内边距 boxShadow: '0 0 12px rgba(0,0,0,0.25)', // 阴影效果 borderRadius: '6px', // 圆角 fontFamily: '"Segoe UI","微软雅黑",Tahoma,Arial,sans-serif', // 字体 fontSize: '14px', // 字体大小 color: '#222', // 文本颜色 userSelect: 'none', // 禁止用户选择文本 zIndex: 999999, // 高层级,确保显示在最上层 display: 'none', // 默认隐藏面板 overflowY: 'auto' // 当内容超出高度时显示垂直滚动条 }); document.body.appendChild(panel); // 将功能面板添加到页面的 body 中 // 设置面板的内部 HTML 结构和内容 panel.innerHTML = `

鼠标点击录制助手

状态:空闲
点数:0,时长:0.00秒
回放时长:0秒,已播放:0秒
`; // -------------- 功能封装 -------------- // 位置拖动存储(与入口按钮的拖动逻辑类似,用于面板) (function dragElement(el, saveKey){ // 定义一个用于拖动元素的匿名立即执行函数 let isDown = false, offsetX=0, offsetY=0; // 记录鼠标是否按下、鼠标相对于元素左上角的偏移量 el.addEventListener('mousedown', e => { // 监听面板的鼠标按下事件 // 如果点击目标是按钮、文本域或输入框,则不进行拖动(允许这些元素内部的操作) if(e.target.tagName==='BUTTON' || e.target.tagName==='TEXTAREA' || e.target.tagName==='INPUT') return; if(e.button!==0) return; // 如果不是左键点击,则不进行拖动 isDown=true; // 标记鼠标已按下 const rect=el.getBoundingClientRect(); // 获取面板的尺寸和位置 offsetX=e.clientX-rect.left; // 计算鼠标点击位置与面板左边缘的距离 offsetY=e.clientY-rect.top; // 计算鼠标点击位置与面板上边缘的距离 document.body.style.userSelect='none'; // 拖动时禁止页面文本选择 }); window.addEventListener('mousemove', e=> { // 监听窗口的鼠标移动事件 if(!isDown) return; // 如果鼠标未按下,则不执行 let left = e.clientX - offsetX; // 计算元素的新 left 位置 let top = e.clientY - offsetY; // 计算元素的新 top 位置 // 限制边界,确保元素不会拖出可视区域 left=Math.min(Math.max(0,left), window.innerWidth - el.offsetWidth); top=Math.min(Math.max(0,top), window.innerHeight - el.offsetHeight); el.style.left=left+'px'; el.style.top=top+'px'; // 更新元素的 left 和 top 样式 el.style.right='auto'; el.style.bottom='auto'; // 清除 right 和 bottom 样式 }); window.addEventListener('mouseup', e => { // 监听窗口的鼠标松开事件 if(isDown){ isDown=false; document.body.style.userSelect='';} // 标记鼠标已松开,恢复页面文本选择 // 位置存储:将元素当前的位置保存到 localStorage localStorage.setItem(saveKey, JSON.stringify({top:el.style.top, left:el.style.left, right:el.style.right, bottom:el.style.bottom})); }); })(panel, 'mouseRecPanelPos'); // 立即执行拖动函数,传入面板和其位置的 localStorage 键名 // -------------- 控件绑定 ---------------- // 通过 ID 获取面板中的各个交互元素 const btnBackEntry = document.querySelector('#btnBackEntry'); // 返回入口按钮 const btnClearRecord = document.querySelector('#btnClearRecord'); // 清空录制按钮 const btnStartRec = document.querySelector('#btnStartRec'); // 开始录制按钮 const btnPauseRec = document.querySelector('#btnPauseRec'); // 暂停录制按钮 const btnResumeRec = document.querySelector('#btnResumeRec'); // 继续录制按钮 const btnStopRec = document.querySelector('#btnStopRec'); // 停止录制按钮 const btnPlay = document.querySelector('#btnPlay'); // 开始回放按钮 const btnStopPlay = document.querySelector('#btnStopPlay'); // 停止回放按钮 const btnSaveFile = document.querySelector('#btnSaveFile'); // 保存文件按钮 const btnLoadFile = document.querySelector('#btnLoadFile'); // 导入文件按钮 const fileInput = document.querySelector('#fileInput'); // 文件输入框(隐藏的) const recData = document.querySelector('#recData'); // 录制数据显示文本域 const statusArea = document.querySelector('#statusArea'); // 状态显示区域 const btnSetHotkeys = document.querySelector('#btnSetHotkeys'); // 设置快捷键按钮 const hotkeySettingsPanel = document.querySelector('#hotkeySettingsPanel'); // 快捷键设置面板 const hotkeyForm = document.querySelector('#hotkeyForm'); // 快捷键设置表单 const btnSaveHotkeys = document.querySelector('#btnSaveHotkeys'); // 保存快捷键按钮 const btnCancelHotkeys = document.querySelector('#btnCancelHotkeys'); // 取消快捷键设置按钮 const chkPreventClicks = document.querySelector('#chkPreventClicks'); // 阻止点击事件复选框 // 复选框初始化 chkPreventClicks.checked = preventClicksDuringRecord; // 根据当前配置初始化复选框状态 chkPreventClicks.onchange = e => { // 监听复选框的改变事件 preventClicksDuringRecord=e.target.checked; // 更新阻止点击事件的配置 updateStatusAndUI(); // 更新状态和UI显示 debugLog(`阻止点击事件设置:${preventClicksDuringRecord}`); // 输出调试信息 }; // -------------- 快捷键配置 ---------------- // 将快捷键对象转换为字符串,例如 { ctrlKey: true, key: 'w' } -> "Ctrl+W" function hotkeyObjToStr(hk) { if(!hk.key) return ''; // 如果没有 key,返回空字符串 const parts = []; // 用于存储快捷键组合的数组 if(hk.ctrlKey) parts.push('Ctrl'); // 如果 Ctrl 键被按下,添加 'Ctrl' if(hk.altKey) parts.push('Alt'); // 如果 Alt 键被按下,添加 'Alt' if(hk.shiftKey) parts.push('Shift'); // 如果 Shift 键被按下,添加 'Shift' parts.push(hk.key.length===1 ? hk.key.toUpperCase() : hk.key); // 添加按键本身,如果是单个字母则转为大写 return parts.join('+'); // 用 '+' 连接所有部分 } // 将快捷键字符串转换为对象,例如 "Ctrl+W" -> { ctrlKey: true, key: 'w' } function hotkeyStrToObj(str) { // 分割字符串,并转换为小写,去除空格 const parts = str.trim().split(/[\+\-\s]+/).map(s=>s.trim()); let hk = { ctrlKey:false, altKey:false, shiftKey:false, key:'' }; // 初始化快捷键对象 for(const p of parts) { // 遍历分割后的部分 const pp=p.toLowerCase(); // 转换为小写进行判断 if(pp==='ctrl') hk.ctrlKey=true; // 如果是 'ctrl',设置 ctrlKey 为 true else if(pp==='alt') hk.altKey=true; // 如果是 'alt',设置 altKey 为 true else if(pp==='shift') hk.shiftKey=true; // 如果是 'shift',设置 shiftKey 为 true else if(p.length>0) hk.key=p.toLowerCase(); // 如果是按键本身,设置 key(转为小写) } if(!hk.key) return null; // 如果没有按键,返回 null return hk; // 返回转换后的快捷键对象 } // 更新快捷键设置面板中的输入框显示(此函数代码略有冗余,直接赋值即可) function updateHotkeyInputs() { // 这段代码是尝试遍历更新,但hotkeyForm不是一个直接的DOM元素,它只是ID。 // hotkeyForm[id] = ... 这行实际上是无效的。 // 正确的做法是直接通过querySelector获取元素并赋值。 ['startRecord','pauseRecord','resumeRecord','stopRecord','startPlayback','stopPlayback'] .forEach(id => { const hk=hotkeys[id]; // 这里的hotkeyForm[id] = ... 实际上是无效的,因为hotkeyForm不是一个map或object,而是一个DOM元素。 // 这导致了后续需要单独赋值。 // hotkeyForm[id]=({}) => { // return hotkeyObjToStr(hk); // } // hotkeyForm[`#${id}`].value= hotkeyObjToStr(hk); }); //单独赋值:确保每个输入框显示正确的快捷键字符串 document.querySelector('#hkStart').value=hotkeyObjToStr(hotkeys.startRecord); document.querySelector('#hkPause').value=hotkeyObjToStr(hotkeys.pauseRecord); document.querySelector('#hkResume').value=hotkeyObjToStr(hotkeys.resumeRecord); document.querySelector('#hkStop').value=hotkeyObjToStr(hotkeys.stopRecord); document.querySelector('#hkPlay').value=hotkeyObjToStr(hotkeys.startPlayback); document.querySelector('#hkStopPlay').value=hotkeyObjToStr(hotkeys.stopPlayback); } // 更新状态显示区域的文本内容 function updateStatus(text) { statusArea.innerHTML = text.replace(/\n/g,'
'); // 将换行符转换为
标签以便在 HTML 中正确显示 } // 更新所有状态和UI元素显示 function updateStatusAndUI() { const seconds = (totalRecordedTime/1000).toFixed(2); // 将总录制时间转换为秒并保留两位小数 let recStatus='空闲'; // 初始化录制状态为 '空闲' if (isRecording) { // 如果正在录制 recStatus=isPaused?'暂停中':'录制中'; // 根据是否暂停显示 '暂停中' 或 '录制中' if(!isPaused && preventClicksDuringRecord){ recStatus+='(阻止网页事件)';} // 如果未暂停且阻止点击,添加提示 } updateStatus( // 更新状态区域的显示 `状态:${recStatus}
`+ // 显示当前状态 `点数:${recordedClicks.length},时长:${seconds}秒
`+ // 显示录制点数和总时长 `回放时长:${calcPlaybackTotalTime()}秒,已播放:0秒` // 显示回放总时长和已播放时长 ); // 根据状态启用/禁用按钮 btnStartRec.disabled = isRecording; btnPauseRec.disabled = !isRecording || isPaused; btnResumeRec.disabled = !isRecording || !isPaused; btnStopRec.disabled = !isRecording; btnPlay.disabled = recordedClicks.length === 0 || isRecording; btnStopPlay.disabled = !playbackTimer; // 只有在回放进行中才启用停止回放按钮 btnSaveFile.disabled = recordedClicks.length === 0; btnClearRecord.disabled = recordedClicks.length === 0 && !isRecording; } // 计算回放的总时长 function calcPlaybackTotalTime() { if(recordedClicks.length===0) return '0.00'; // 如果没有录制数据,返回 '0.00' const totalSeconds = recordedClicks.reduce((acc,c)=>acc+c.delay,0)/1000; // 累加所有点击的延迟时间并转换为秒 return totalSeconds.toFixed(2); // 保留两位小数 } // -------------- 录制操作 ---------------- function startRecording() { if(isRecording) return; // 如果已经在录制中,则直接返回 recordedClicks=[]; totalRecordedTime=0; lastClickTime=Date.now(); // 初始化录制数据、总时长和上次点击时间 isRecording=true; isPaused=false; // 设置录制状态为进行中,未暂停 // 启用/禁用相关按钮 btnStartRec.disabled=true; btnPauseRec.disabled=false; btnResumeRec.disabled=true; btnStopRec.disabled=false; btnPlay.disabled=true; btnStopPlay.disabled=true; btnSaveFile.disabled=true; btnClearRecord.disabled=false; updateStatusAndUI(); // 更新状态和UI显示 debugLog('开始录制'); // 输出调试信息 } function pauseRecording() { if(!isRecording || isPaused) return; // 如果不在录制中或已暂停,则直接返回 isPaused=true; // 设置录制状态为暂停 btnPauseRec.disabled=true; btnResumeRec.disabled=false; // 禁用暂停按钮,启用继续按钮 updateStatusAndUI(); // 更新状态和UI显示 debugLog('暂停录制'); // 输出调试信息 } function resumeRecording() { if(!isRecording || !isPaused) return; // 如果不在录制中或未暂停,则直接返回 isPaused=false; lastClickTime=Date.now(); // 取消暂停,更新上次点击时间 btnPauseRec.disabled=false; btnResumeRec.disabled=true; // 启用暂停按钮,禁用继续按钮 updateStatusAndUI(); // 更新状态和UI显示 debugLog('继续录制'); // 输出调试信息 } function stopRecording() { if(!isRecording) return; // 如果不在录制中,则直接返回 isRecording=false; isPaused=false; // 设置录制状态为空闲,未暂停 // 启用/禁用相关按钮 btnStartRec.disabled=false; btnPauseRec.disabled=true; btnResumeRec.disabled=true; btnStopRec.disabled=true; btnPlay.disabled=recordedClicks.length===0; btnSaveFile.disabled=recordedClicks.length===0; btnClearRecord.disabled=recordedClicks.length===0; totalRecordedTime = recordedClicks.reduce((acc,c)=>acc+c.delay,0); // 重新计算总录制时长 recData.value=JSON.stringify(recordedClicks, null, 2); // 将录制数据格式化后显示在文本域中 updateStatusAndUI(); // 更新状态和UI显示 debugLog('停止录制,点数:'+recordedClicks.length); // 输出调试信息 } function clearRecordingData() { if(isRecording) { stopRecording(); } // 如果正在录制,先停止录制 recordedClicks=[]; totalRecordedTime=0; recData.value=''; // 清空录制数据 btnPlay.disabled=true; btnSaveFile.disabled=true; btnClearRecord.disabled=true; // 禁用相关按钮 updateStatusAndUI(); // 更新状态和UI显示 debugLog('清空录制数据'); // 输出调试信息 } // -------------- 鼠标点击录制 ---------------- // 判断给定的坐标 (x, y) 是否在任何一个目标区域内 function isInTargetAreas(x,y) { if(targetAreas.length===0) return true; // 如果没有设置目标区域,则认为所有点击都在目标区域内 for(const area of targetAreas) { // 遍历所有目标区域 // 判断点击坐标是否在当前区域的矩形范围内 if(x>=area.x && x<=area.x+area.width && y>=area.y && y<=area.y+area.height){ return true; // 如果在任何一个区域内,返回 true } } return false; // 如果不在任何区域内,返回 false } // 处理鼠标点击事件,进行录制 function onClickRecord(e) { if(!isRecording || isPaused) return; // 如果不在录制中或已暂停,则不处理点击事件 const x=e.clientX, y=e.clientY; // 获取点击的页面坐标 if(targetAreas.length>0 && !isInTargetAreas(x,y)){ // 如果设置了目标区域且点击不在区域内 debugLog(`点击点(${x},${y})未在目标区域,跳过`); // 输出调试信息 return; // 跳过此点击 } const now=Date.now(); // 获取当前时间戳 const delay= recordedClicks.length===0 ? 0 : (now-lastClickTime); // 计算与上次点击的时间间隔,如果是第一个点击则间隔为0 lastClickTime=now; // 更新上次点击的时间戳 // 将绝对坐标转换为相对坐标,以适应不同屏幕分辨率 const relX= x/window.innerWidth; // 相对于窗口宽度的 X 坐标比例 const relY= y/window.innerHeight; // 相对于窗口高度的 Y 坐标比例 recordedClicks.push({relativeX:relX,relativeY:relY, delay}); // 将相对坐标和延迟时间保存到录制数组中 totalRecordedTime+=delay; // 累加总录制时长 updateStatusAndUI(); // 更新状态和UI显示 if(preventClicksDuringRecord){ // 如果配置为阻止点击事件 e.preventDefault(); // 阻止默认事件(例如链接跳转) e.stopPropagation(); // 阻止事件冒泡到父元素 } debugLog(`录制点:${relX.toFixed(4)},${relY.toFixed(4)},间隔:${delay}ms`); // 输出调试信息 } document.addEventListener('click', onClickRecord,true); // 监听全局的点击事件,使用捕获阶段(true)确保在其他点击事件之前触发 // -------------- 回放逻辑 ---------------- // 突出显示被模拟点击的元素 function highlightElement(el) { if(!el) return; // 如果元素不存在,则返回 el.style.transition='box-shadow 0.3s ease'; // 添加过渡效果 el.style.boxShadow='0 0 10px #f39c12'; // 设置一个黄色的阴影 setTimeout(()=>{el.style.boxShadow='';},900); // 900ms 后清除阴影效果 } // 在指定坐标 (x, y) 模拟点击事件 function simulateClickAt(x,y) { const el= document.elementFromPoint(x,y); // 获取指定坐标下的元素 highlightElement(el); // 突出显示该元素 if(!el) return; // 如果没有找到元素,则返回 // 依次派发 mousedown, mouseup, click 事件,模拟完整的鼠标点击过程 ['mousedown','mouseup','click'].forEach(t=>{ el.dispatchEvent(new MouseEvent(t,{bubbles:true, cancelable:true, clientX:x, clientY:y, button:0})); }); debugLog(`模拟点击:(${x.toFixed(0)},${y.toFixed(0)}) 元素:${el.tagName}`); // 输出调试信息 } // 播放点击序列的主函数 function playClickSequence() { if(playbackIndex>=recordedClicks.length){ // 如果所有点击都已播放完毕 // 完成回放 playbackTimer=null; // 清除回放计时器 setTimeout(()=> alert('回放完成!'),50); // 稍作延迟后弹出回放完成提示 // 更新状态 updateStatus(`状态:回放完成
点数:${recordedClicks.length},时长:${(totalRecordedTime/1000).toFixed(2)}秒`); btnPlay.disabled=false; btnStopPlay.disabled=true; // 启用开始回放按钮,禁用停止回放按钮 debugLog('回放结束'); // 输出调试信息 return; } const c=recordedClicks[playbackIndex]; // 获取当前要播放的点击数据 const delay=c.delay; // 获取点击延迟时间 let x= c.relativeX * window.innerWidth; // 将相对 X 坐标转换为当前窗口的绝对 X 坐标 let y= c.relativeY * window.innerHeight; // 将相对 Y 坐标转换为当前窗口的绝对 Y 坐标 simulateClickAt(x,y); // 模拟点击 playbackIndex++; // 移动到下一个点击事件 // 动画平滑(此部分代码是动画的框架,但实际未实现复杂动画效果,只是一个示意) let startTime= performance.now(); // 记录当前时间 const animate = ()=> { // 定义动画函数 let elapsed= performance.now() - startTime; // 计算从上次点击开始经过的时间 const progress= Math.min(elapsed/delay,1); // 计算动画进度,限制在 0 到 1 之间 // 暂时不做复杂动画(例如鼠标移动轨迹动画),这里只是一个空循环直到 delay 结束 if(progress<1){ // 可以做鼠标动画机场 requestAnimationFrame(animate); // 在下一帧继续执行动画 } else { // 下一个:动画结束后,更新回放状态并设置下一个点击的计时器 updatePlaybackStatus((performance.now() - playbackStartTime)/1000); // 更新已播放时长 playbackTimer= setTimeout(playClickSequence,0); // 设置一个 0 毫秒的延迟来播放下一个点击(实际上会放到事件队列末尾) } }; // 如果没有延迟,直接播放下一个;否则启动动画 if (delay === 0) { updatePlaybackStatus((performance.now() - playbackStartTime)/1000); playbackTimer = setTimeout(playClickSequence, 0); } else { playbackTimer = setTimeout(animate, delay); // 延迟 delay 毫秒后执行动画 } } // 更新回放状态区域的显示 function updatePlaybackStatus(elapsedSeconds=0) { const totalSec= parseFloat(calcPlaybackTotalTime()); // 获取总回放时长 const playedSecs= elapsedSeconds.toFixed(2); // 格式化已播放时长 updateStatus(`状态:回放中
点数:${recordedClicks.length},时长:${totalSec}秒
已播放:${playedSecs}秒`); // 更新状态信息 } // 开始回放 function startPlayback() { if(playbackTimer) return; // 如果回放计时器已存在(正在回放中),则返回 if(recordedClicks.length===0) { // 如果没有录制数据 updateStatus('无录制数据,无法回放'); // 更新状态提示 return; } playbackIndex=0; // 重置回放索引 playbackStartTime=performance.now(); // 记录回放开始时间 btnPlay.disabled=true; btnStopPlay.disabled=false; // 禁用开始回放按钮,启用停止回放按钮 updatePlaybackStatus(0); // 初始化回放状态显示 debugLog('开始回放'); // 输出调试信息 playClickSequence(); // 开始播放点击序列 } // 停止回放 function stopPlayback() { if(playbackTimer) { // 如果回放计时器存在 clearTimeout(playbackTimer); // 清除计时器,停止回放 playbackTimer=null; // 清空计时器 ID } btnPlay.disabled=false; btnStopPlay.disabled=true; // 启用开始回放按钮,禁用停止回放按钮 const elapsedSec= ((performance.now()-playbackStartTime)/1000).toFixed(2); // 计算已播放时长 updateStatus(`状态:回放已停止
点数:${recordedClicks.length},时长:${(totalRecordedTime/1000).toFixed(2)}秒
已播放:${elapsedSec}秒`); // 更新状态信息 debugLog('回放停止'); // 输出调试信息 } // -------------- 导出导入 ---------------- // 将录制数据保存到文件 function saveToFile() { if(recordedClicks.length===0) return; // 如果没有录制数据,则返回 // 创建一个 Blob 对象,包含录制数据(JSON 格式) const blob=new Blob([JSON.stringify(recordedClicks,null,2)], {type:'application/json'}); const url= URL.createObjectURL(blob); // 创建一个 Blob URL const a=document.createElement('a'); // 创建一个元素用于下载 a.href= url; // 设置下载链接 a.download= `mouse_recording_${(new Date()).toISOString().replace(/[:.]/g,'-')}.json`; // 设置下载文件名(包含时间戳) a.click(); // 模拟点击下载链接 URL.revokeObjectURL(url); // 释放 Blob URL debugLog('导出录制数据'); // 输出调试信息 } // 从文件加载录制数据 function loadFromFile(file) { const reader= new FileReader(); // 创建一个 FileReader 对象 reader.onload=e => { // 监听文件读取完成事件 try{ const data= JSON.parse(e.target.result); // 解析文件内容为 JSON 对象 // 验证数据格式:确保是数组,且每个元素包含 delay 和 (relativeX/relativeY 或 x/y) if(Array.isArray(data) && data.every(v => ('delay' in v) && (('relativeX' in v && 'relativeY' in v) || ('x' in v && 'y' in v)))) { recordedClicks= data; // 更新录制数据 totalRecordedTime= recordedClicks.reduce((a,c)=>a+c.delay, 0); // 重新计算总时长 recData.value= JSON.stringify(recordedClicks,null,2); // 更新文本域显示 btnPlay.disabled= false; // 启用回放按钮 btnSaveFile.disabled= false; // 启用保存按钮 btnClearRecord.disabled= false; // 启用清空按钮 updateStatus(`导入成功,点数:${recordedClicks.length},时长:${(totalRecordedTime/1000).toFixed(2)}秒`); // 更新状态 debugLog('导入录制数据成功'); // 输出调试信息 } else { updateStatus('文件格式错误'); // 文件格式不正确 } } catch(e){ updateStatus('读取失败'); } // JSON 解析失败 }; reader.readAsText(file); // 以文本格式读取文件内容 } // -------------- 快捷键设置 ---------------- // 打开快捷键设置面板 function openHotkeyPanel() { document.querySelector('#hotkeySettingsPanel').style.display='block'; // 显示设置面板 // 填充输入框,确保显示当前配置的快捷键 document.querySelector('#hkStart').value= hotkeyObjToStr(hotkeys.startRecord); document.querySelector('#hkPause').value= hotkeyObjToStr(hotkeys.pauseRecord); document.querySelector('#hkResume').value= hotkeyObjToStr(hotkeys.resumeRecord); document.querySelector('#hkStop').value= hotkeyObjToStr(hotkeys.stopRecord); document.querySelector('#hkPlay').value= hotkeyObjToStr(hotkeys.startPlayback); document.querySelector('#hkStopPlay').value= hotkeyObjToStr(hotkeys.stopPlayback); } // 关闭快捷键设置面板 function closeHotkeyPanel() { document.querySelector('#hotkeySettingsPanel').style.display='none'; // 隐藏设置面板 } // 绑定快捷键输入框的按键捕获事件 ['hkStart','hkPause','hkResume','hkStop','hkPlay','hkStopPlay'].forEach(id => { // 遍历所有快捷键输入框的 ID const input= document.querySelector('#'+id); // 获取对应的输入框元素 input.onkeydown= e => { // 监听输入框的按键按下事件 e.preventDefault(); // 阻止默认行为(例如输入字符) const keys= []; // 用于存储按下的修饰键和主键 if(e.ctrlKey) keys.push('Ctrl'); // 如果 Ctrl 键被按下,添加 'Ctrl' if(e.altKey) keys.push('Alt'); // 如果 Alt 键被按下,添加 'Alt' if(e.shiftKey) keys.push('Shift'); // 如果 Shift 键被按下,添加 'Shift' let keyName= e.key; // 获取按键名称 // 过滤掉单独的修饰键(避免输入 Ctrl 或 Shift 等) if(keyName==='Control'|| keyName==='Shift'|| keyName==='Alt'|| keyName==='Meta') { // 做跳过 } else { keyName= keyName.length===1 ? keyName.toUpperCase() : keyName; // 如果是单个字母,转为大写 if(!['Control','Shift','Alt','Meta'].includes(e.key)) { // 再次检查是否是修饰键 keys.push(keyName); // 添加主键名称 } input.value= keys.join('+'); // 将组合键显示在输入框中 } }; }); // 保存快捷键设置按钮的点击事件 document.querySelector('#btnSaveHotkeys').onclick= ()=> { try{ const newHK={}; // 创建新的快捷键配置对象 // 从输入框获取值并转换为快捷键对象 newHK.startRecord= hotkeyStrToObj(document.querySelector('#hkStart').value); newHK.pauseRecord= hotkeyStrToObj(document.querySelector('#hkPause').value); newHK.resumeRecord= hotkeyStrToObj(document.querySelector('#hkResume').value); newHK.stopRecord= hotkeyStrToObj(document.querySelector('#hkStop').value); newHK.startPlayback= hotkeyStrToObj(document.querySelector('#hkPlay').value); newHK.stopPlayback= hotkeyStrToObj(document.querySelector('#hkStopPlay').value); hotkeys= newHK; // 更新全局快捷键配置 localStorage.setItem('mouseRecHotkeys', JSON.stringify(hotkeys)); // 保存到 localStorage alert('快捷键保存成功'); // 提示保存成功 closeHotkeyPanel(); // 关闭设置面板 } catch(e){ alert('快捷键设置错误'); } // 捕获错误并提示 }; document.querySelector('#btnCancelHotkeys').onclick= ()=> { closeHotkeyPanel(); }; // 取消按钮点击事件,关闭面板 // 自定义快捷键设置按钮的点击事件,用于切换设置面板的显示/隐藏 document.querySelector('#btnSetHotkeys').onclick= ()=> { if(document.querySelector('#hotkeySettingsPanel').style.display==='block') { closeHotkeyPanel(); // 如果已显示,则关闭 } else { openHotkeyPanel(); // 如果已隐藏,则打开 } }; // -------------- UI按钮事件 ---------------- // 入口按钮点击事件:隐藏入口按钮,显示主面板 entryBtn.onclick= ()=>{ entryBtn.style.display='none'; panel.style.display='block'; }; // 返回入口按钮点击事件:隐藏主面板,显示入口按钮 document.querySelector('#btnBackEntry').onclick= ()=>{ panel.style.display='none'; entryBtn.style.display='block'; }; // 添加目标区域按钮点击事件 document.querySelector('#btnAddTargetArea').onclick= ()=>{ alertMsg('请框选目标区域(按住鼠标拖拽后松开)'); // 提示用户操作 let isDragging=false, startX=0, startY=0, rect=null; // 拖动状态、起始坐标、绘制的矩形元素 const overlay=document.createElement('div'); // 创建一个覆盖层 Object.assign(overlay.style,{ // 设置覆盖层样式 position:'fixed', top:0, left:0,width:'100%', height:'100%', background:'rgba(0,0,0,0.2)', cursor:'crosshair', zIndex:9999999 // 半透明背景,十字光标,高层级 }); document.body.appendChild(overlay); // 将覆盖层添加到 body 中 overlay.onmousedown= e => { // 覆盖层鼠标按下事件 if(e.button!==0) return; // 只响应左键 isDragging=true; startX=e.clientX; startY=e.clientY; // 标记拖动开始,记录起始坐标 rect=document.createElement('div'); // 创建一个用于显示选择区域的矩形框 Object.assign(rect.style, { // 设置矩形框样式 position:'absolute', border:'2px dashed #333', background:'rgba(0,0,0,0.1)', left:startX+'px', top:startY+'px', width:'0px', height:'0px' }); overlay.appendChild(rect); // 将矩形框添加到覆盖层 }; overlay.onmousemove= e => { // 覆盖层鼠标移动事件 if(!isDragging) return; // 如果未拖动,则返回 const currX=e.clientX, currY=e.clientY; // 获取当前鼠标坐标 rect.style.left= Math.min(startX,currX)+'px'; // 更新矩形框的 left 位置 rect.style.top= Math.min(startY,currY)+'px'; // 更新矩形框的 top 位置 rect.style.width= Math.abs(currX-startX)+'px'; // 更新矩形框的宽度 rect.style.height= Math.abs(currY-startY)+'px'; // 更新矩形框的高度 }; overlay.onmouseup= e => { // 覆盖层鼠标松开事件 if(!isDragging) return; // 如果未拖动,则返回 isDragging=false; // 标记拖动结束 const x= parseFloat(rect.style.left); // 获取最终矩形框的 X 坐标 const y= parseFloat(rect.style.top); // 获取最终矩形框的 Y 坐标 const width= parseFloat(rect.style.width); // 获取最终矩形框的宽度 const height= parseFloat(rect.style.height); // 获取最终矩形框的高度 // 存入目标区域 targetAreas.push({x,y,width,height}); // 将新目标区域添加到数组 localStorage.setItem('mouseRecTargetAreas', JSON.stringify(targetAreas)); // 保存到 localStorage refreshTargetArea(); // 刷新目标区域列表显示 document.body.removeChild(overlay); // 移除覆盖层 debugLog(`新目标区域:x:${x}, y:${y}, w:${width}, h:${height}`); // 输出调试信息 }; }; // 目标区域刷新显示函数 function refreshTargetArea() { const listDiv= document.querySelector('#targetAreaList'); // 获取目标区域列表容器 listDiv.innerHTML=''; // 清空现有内容 targetAreas.forEach((area, idx) => { // 遍历所有目标区域 const div= document.createElement('div'); // 创建一个 div 显示区域信息 div.textContent= `区域${idx+1}: [${area.x.toFixed(0)}, ${area.y.toFixed(0)}, ${area.width.toFixed(0)}x${area.height.toFixed(0)}]`; // 显示区域坐标和尺寸 //删除按钮 const btn= document.createElement('button'); // 创建删除按钮 btn.textContent='删除'; btn.style.marginLeft='8px'; btn.style.fontSize='12px'; btn.onclick= ()=> { // 删除按钮点击事件 targetAreas.splice(idx,1); // 从数组中删除当前区域 localStorage.setItem('mouseRecTargetAreas', JSON.stringify(targetAreas)); // 更新 localStorage refreshTargetArea(); // 刷新显示 }; div.appendChild(btn); // 将删除按钮添加到区域信息 div listDiv.appendChild(div); // 将区域信息 div 添加到列表容器 }); } // 初始化显示:页面加载时刷新目标区域列表 refreshTargetArea(); // -------------- 按钮事件绑定 ---------------- // 为所有功能按钮绑定对应的事件处理函数 document.querySelector('#btnStartRec').onclick= startRecording; // 开始录制 document.querySelector('#btnPauseRec').onclick= pauseRecording; // 暂停录制 document.querySelector('#btnResumeRec').onclick=resumeRecording; // 继续录制 document.querySelector('#btnStopRec').onclick= stopRecording; // 停止录制 document.querySelector('#btnPlay').onclick= startPlayback; // 开始回放 document.querySelector('#btnStopPlay').onclick= stopPlayback; // 停止回放 document.querySelector('#btnSaveFile').onclick= saveToFile; // 保存文件 document.querySelector('#btnLoadFile').onclick= ()=> fileInput.click(); // 导入文件(触发隐藏的文件输入框点击) document.querySelector('#btnClearRecord').onclick= clearRecordingData; // 清空录制数据 // 文件输入框的改变事件(当选择文件后触发) fileInput.onchange= e => { if(e.target.files.length>0) { // 如果选择了文件 loadFromFile(e.target.files[0]); // 加载第一个文件 e.target.value=''; // 清空文件输入框,以便再次选择相同文件时也能触发 change 事件 } }; // -------------- 全局快捷键监听 -------------- window.addEventListener('keydown', e => { // 监听窗口的按键按下事件 // 判断当前按键事件是否与某个快捷键配置匹配 function matchHotkey(hk,ev) { // 比较 Ctrl, Alt, Shift 键的状态,以及主键(不区分大小写) return (!!hk.ctrlKey===ev.ctrlKey) && (!!hk.altKey===ev.altKey) && (!!hk.shiftKey===ev.shiftKey) && (hk.key.toLowerCase()===ev.key.toLowerCase()); } // 根据匹配结果调用相应的功能函数,并阻止默认行为 if(matchHotkey(hotkeys.startRecord, e)){ e.preventDefault(); startRecording();} else if(matchHotkey(hotkeys.pauseRecord, e)){ e.preventDefault(); pauseRecording();} else if(matchHotkey(hotkeys.resumeRecord, e)){ e.preventDefault(); resumeRecording();} else if(matchHotkey(hotkeys.stopRecord, e)){ e.preventDefault(); stopRecording();} else if(matchHotkey(hotkeys.startPlayback, e)){ e.preventDefault(); startPlayback();} else if(matchHotkey(hotkeys.stopPlayback, e)){ e.preventDefault(); stopPlayback();} }); // -------------- 面板快捷键设置 ---------------- // 再次绑定快捷键保存按钮的点击事件(与前面重复,这里是确保在面板内操作时也能正确保存) document.querySelector('#btnSaveHotkeys').onclick= ()=> { try{ // 从输入框获取值并转换为快捷键对象 hotkeys.startRecord= hotkeyStrToObj(document.querySelector('#hkStart').value); hotkeys.pauseRecord= hotkeyStrToObj(document.querySelector('#hkPause').value); hotkeys.resumeRecord= hotkeyStrToObj(document.querySelector('#hkResume').value); hotkeys.stopRecord= hotkeyStrToObj(document.querySelector('#hkStop').value); hotkeys.startPlayback= hotkeyStrToObj(document.querySelector('#hkPlay').value); hotkeys.stopPlayback= hotkeyStrToObj(document.querySelector('#hkStopPlay').value); localStorage.setItem('mouseRecHotkeys', JSON.stringify(hotkeys)); // 保存到 localStorage alert('快捷键已保存'); // 提示保存成功 closeHotkeyPanel(); // 关闭设置面板 } catch(e){ alert('快捷键格式有误'); } // 捕获错误并提示 }; document.querySelector('#btnCancelHotkeys').onclick= ()=> { closeHotkeyPanel(); }; // 取消按钮点击事件,关闭面板 document.querySelector('#btnSetHotkeys').onclick= ()=> { // 设置快捷键按钮的点击事件 if(document.querySelector('#hotkeySettingsPanel').style.display==='block') { closeHotkeyPanel(); // 如果面板已显示,则关闭 } else { openHotkeyPanel(); // 如果面板已隐藏,则打开 } }; // -------------- 显示状态 ---------------- updateStatusAndUI(); // 脚本加载时,初始化UI状态显示 })(); // 匿名立即执行函数结束