// ==UserScript== // @name 网页操作录制器 // @namespace http://tampermonkey.net/ // @version 1.4 // @description 记录并回放鼠标点击操作 // @author You // @match *://*/* // @grant none // @run-at document-end // @noframes // @downloadURL https://update.greasyfork.icu/scripts/520143/%E7%BD%91%E9%A1%B5%E6%93%8D%E4%BD%9C%E5%BD%95%E5%88%B6%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/520143/%E7%BD%91%E9%A1%B5%E6%93%8D%E4%BD%9C%E5%BD%95%E5%88%B6%E5%99%A8.meta.js // ==/UserScript== (function() { 'use strict'; // 全局变量 let recording = false; let actions = []; let startTime; let isRunning = false; let stopRunning = false; let abortController = null; let editModeRecording = false; // 设置选项 const settings = { opacity: parseFloat(localStorage.getItem('recorder_opacity')) || 0.8, useBlur: localStorage.getItem('recorder_use_blur') !== 'false', showClickAnimation: localStorage.getItem('recorder_show_click_animation') !== 'false', maxAnimations: parseInt(localStorage.getItem('recorder_max_animations')) || 10 }; const COLORS = { NORMAL: '#1890ff', NEW_ACTION: '#52c41a', ACTIVE: '#ff4d4f', TEXT: '#000' // 固定文字颜色 }; const CONSTANTS = { DRAG_DELAY: 200, Z_INDEX: { BASE: 2147483645, ACTIVE: 2147483646, EDIT_PANEL: 2147483647 } }; // 样式初始化 function initStyles() { const style = document.createElement('style'); style.textContent = ` .top-edit-panel { position: fixed; top: 20px; left: 25%; background: rgba(255, 255, 255, ${settings.opacity}); padding: 10px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); z-index: ${CONSTANTS.Z_INDEX.EDIT_PANEL}; display: flex; flex-wrap: wrap; gap: 8px; width: 50vw; max-height: 80vh; overflow-y: auto; cursor: move; resize: none; color: ${COLORS.TEXT}; // 固定文字颜色 ${settings.useBlur ? 'backdrop-filter: blur(4px);' : ''} } .point-item { display: flex; align-items: center; padding: 8px 10px; background: rgba(245, 245, 245, 0.7); border-radius: 4px; cursor: pointer; width: 170px; color: ${COLORS.TEXT}; // 固定文字颜色 } .point-item.active { background: rgba(23, 144, 255, 0.7); } .edit-point { width: 24px; height: 24px; min-width: 24px; background: ${COLORS.NORMAL}; border-radius: 50%; margin-right: 10px; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; font-weight: bold; } .point-item.active .edit-point { background: ${COLORS.ACTIVE}; } .point-data { display: flex; align-items: center; margin: 4px 0; cursor: pointer; } .point-input { width: 60px; padding: 4px 6px; margin: 0 4px; border: 1px solid #d9d9d9; border-radius: 2px; font-size: 12px; cursor: pointer; } .point-input:focus { outline: none; } .click-point { position: fixed; width: 20px; height: 20px; background: #1890ff; border: 1px solid #000; border-radius: 50%; transform: translate(-50%, -50%); z-index: ${CONSTANTS.Z_INDEX.BASE}; display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: bold; cursor: move; user-select: none; opacity: 0.8; } .click-point.active { background: #ff4d4f; z-index: ${CONSTANTS.Z_INDEX.ACTIVE}; opacity: 1; box-shadow: 0 0 12px rgba(255, 77, 79, 0.8); } .click-point:hover { opacity: 1; } .click-effect { position: fixed; pointer-events: none; width: 20px; height: 20px; background: rgba(255, 77, 79, 0.8); border: 2px solid rgba(255, 77, 79, 0.9); border-radius: 50%; z-index: ${CONSTANTS.Z_INDEX.BASE - 1}; opacity: 0; transform: translate(-50%, -50%) scale(0.3); display: none; left: var(--x); top: var(--y); } .click-effect.active { display: block; animation: clickEffect 0.6s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; } @keyframes clickEffect { 0% { opacity: 1; transform: translate(-50%, -50%) scale(0.3); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(2); } } .toast-message { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.7); color: white; padding: 8px 16px; border-radius: 4px; font-size: 14px; z-index: ${CONSTANTS.Z_INDEX.EDIT_PANEL}; pointer-events: none; animation: fadeOut 1.5s ease-in-out forwards; } .custom-checkbox { appearance: none; width: 14px; height: 14px; border: 2px solid ${COLORS.NORMAL}; border-radius: 2px; margin: 0; cursor: pointer; position: relative; transition: background-color 0.2s; pointer-events: all; } .custom-checkbox:checked { background: ${COLORS.NORMAL}; } .custom-checkbox:checked::after { content: ''; position: absolute; left: 2px; top: 0px; width: 3px; height: 7px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); } .checkbox-wrapper { width: 14px; height: 14px; position: relative; pointer-events: none; } .checkbox-wrapper input { pointer-events: all; } .checkbox-wrapper + span { pointer-events: none; } .click-point.dragging { cursor: grabbing; opacity: 0.8; box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.8), 0 0 0 4px rgba(255, 255, 255, 0.8), 0 0 16px rgba(0, 0, 0, 0.5); } .scheme-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; cursor: pointer; padding: 2px 4px; border-radius: 2px; color: ${COLORS.TEXT}; // 固定文字颜色 } .scheme-name:hover { background: rgba(0, 0, 0, 0.05); } .scheme-name-input { width: 100%; font-size: 13px; padding: 2px 4px; border: 1px solid #1890ff; border-radius: 2px; outline: none; } `; document.head.appendChild(style); } // 点击效果管理器 const ClickEffectManager = { activeEffects: new Set(), effectPool: [], poolSize: 20, // 对象池大小 init() { // 初始化对象池 for (let i = 0; i < this.poolSize; i++) { const effect = this.createEffectElement(); const innerEffect = this.createEffectElement(true); this.effectPool.push({ effect, innerEffect, inUse: false }); } }, createEffectElement(isInner = false) { const effect = document.createElement('div'); effect.className = 'click-effect'; if (isInner) { effect.style.animation = 'clickEffect 0.4s cubic-bezier(0.22, 0.61, 0.36, 1) forwards'; effect.style.background = 'rgba(255, 77, 79, 0.4)'; effect.style.border = '2px solid rgba(255, 77, 79, 0.6)'; } effect.style.display = 'none'; document.body.appendChild(effect); return effect; }, getFromPool() { // 从池中获取可用的效果元素 let poolItem = this.effectPool.find(item => !item.inUse); // 如果池中没有可用元素,创建新的 if (!poolItem) { const effect = this.createEffectElement(); const innerEffect = this.createEffectElement(true); poolItem = { effect, innerEffect, inUse: false }; this.effectPool.push(poolItem); } poolItem.inUse = true; return poolItem; }, returnToPool(poolItem) { // 重置元素状态并返回池中 const { effect, innerEffect } = poolItem; effect.style.display = 'none'; effect.classList.remove('active'); innerEffect.style.display = 'none'; innerEffect.classList.remove('active'); poolItem.inUse = false; this.activeEffects.delete(effect); this.activeEffects.delete(innerEffect); }, show(x, y) { if (!settings.showClickAnimation) return; // 检查是否超过最大动画数量限制 if (settings.maxAnimations > 0 && this.activeEffects.size >= settings.maxAnimations) { const oldestEffect = this.activeEffects.values().next().value; if (oldestEffect) { const poolItem = this.effectPool.find(item => item.effect === oldestEffect || item.innerEffect === oldestEffect ); if (poolItem) { this.returnToPool(poolItem); } } } // 从对象池获取效果元素 const poolItem = this.getFromPool(); const { effect, innerEffect } = poolItem; // 设置位置 effect.style.setProperty('--x', `${x}px`); effect.style.setProperty('--y', `${y}px`); innerEffect.style.setProperty('--x', `${x}px`); innerEffect.style.setProperty('--y', `${y}px`); // 显示效果 effect.style.display = 'block'; innerEffect.style.display = 'block'; this.activeEffects.add(effect); this.activeEffects.add(innerEffect); // 启动动画 requestAnimationFrame(() => { effect.classList.add('active'); innerEffect.classList.add('active'); }); // 动画结束后回收到对象池 effect.addEventListener('animationend', () => { this.returnToPool(poolItem); }, { once: true }); }, cleanup() { // 清理所有效果元素 this.effectPool.forEach(({ effect, innerEffect }) => { effect.remove(); innerEffect.remove(); }); this.effectPool = []; this.activeEffects.clear(); } }; // 点击点管理器 const ClickPointManager = { points: new Map(), activePoint: null, init() { // 初始化全局点击事件监听 document.addEventListener('click', (e) => { if (!e.target.closest('.click-point')) { this.clearActivePoint(); } }); }, createPoints(actions) { this.cleanup(); actions.forEach((action, index) => { const point = this.createSinglePoint(action, index); this.points.set(index, point); document.body.appendChild(point); }); }, createSinglePoint(action, index) { const point = document.createElement('div'); point.className = 'click-point edit-mode'; point.setAttribute('data-point-index', index); point.textContent = index + 1; point.style.left = action.x + 'px'; point.style.top = action.y + 'px'; this.initDragEvents(point); return point; }, initDragEvents(point) { let offsetX, offsetY; const onMouseMove = (e) => { e.preventDefault(); requestAnimationFrame(() => { point.style.left = (e.clientX - offsetX) + 'px'; point.style.top = (e.clientY - offsetY) + 'px'; }); }; const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); this.handleDragEnd(); }; const onMouseDown = (e) => { if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); const rect = point.getBoundingClientRect(); offsetX = e.clientX - rect.left - rect.width / 2; offsetY = e.clientY - rect.top - rect.height / 2; this.setActivePoint(point); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; point.addEventListener('mousedown', onMouseDown); point.addEventListener('click', (e) => { e.stopPropagation(); this.setActivePoint(point); }); }, handleDragEnd() { if (!this.activePoint) return; const index = parseInt(this.activePoint.getAttribute('data-point-index')); if (index >= 0 && index < actions.length) { actions[index].x = parseInt(this.activePoint.style.left); actions[index].y = parseInt(this.activePoint.style.top); EditManager.markAsChanged(); } }, setActivePoint(point) { this.clearActivePoint(); this.activePoint = point; point.classList.add('active'); // 高亮编辑界面对应的点 const index = parseInt(point.getAttribute('data-point-index')); document.querySelectorAll('.point-item').forEach((item) => { const itemIndex = parseInt(item.getAttribute('data-point-index')); item.classList.toggle('active', itemIndex === index); }); }, clearActivePoint() { if (this.activePoint) { this.activePoint.classList.remove('active'); this.activePoint = null; } document.querySelectorAll('.point-item').forEach(item => { item.classList.remove('active'); }); }, cleanup() { this.points.forEach(point => point.remove()); this.points.clear(); this.activePoint = null; }, addNewPoint(action, index) { const point = this.createSinglePoint(action, index); this.points.set(index, point); document.body.appendChild(point); return point; } }; // 存储管理器 const StorageManager = { STORAGE_KEY: 'recordSchemes', cache: null, saveTimeout: null, initialized: false, // 初始化缓存 init() { if (this.initialized) return; this.cache = this.loadFromStorage(); this.initialized = true; }, // 从localStorage加载数据 loadFromStorage() { try { return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '{}'); } catch (error) { console.error('Failed to load schemes from storage:', error); return {}; } }, // 将缓存保存到localStorage saveToStorage() { try { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.cache)); return true; } catch (error) { console.error('Failed to save schemes to storage:', error); return false; } }, // 延迟保存,防止频繁写入 debounceSave() { if (this.saveTimeout) { clearTimeout(this.saveTimeout); } this.saveTimeout = setTimeout(() => { this.saveToStorage(); this.saveTimeout = null; }, 300); // 300ms延迟 }, // 获取所有方案 getAllSchemes() { this.init(); return this.cache; }, // 获取单个方案 getScheme(name) { this.init(); return this.cache[name]; }, // 保存单个方案 saveScheme(name, scheme) { this.init(); this.cache[name] = scheme; this.debounceSave(); }, // 批量保存方案 saveSchemeBatch(schemes) { this.init(); Object.assign(this.cache, schemes); this.debounceSave(); }, // 删除方案 deleteScheme(name) { this.init(); delete this.cache[name]; this.debounceSave(); }, // 重命名方案 renameScheme(oldName, newName) { this.init(); if (this.cache[oldName]) { this.cache[newName] = this.cache[oldName]; delete this.cache[oldName]; this.debounceSave(); return true; } return false; }, // 更新方案的特定字段 updateSchemeField(name, field, value) { this.init(); const scheme = this.cache[name]; if (scheme) { scheme[field] = value; this.debounceSave(); return true; } return false; }, // 创建新方案 createScheme(name, data) { this.init(); const scheme = { actions: data.actions || [], timestamp: Date.now(), site: window.location.hostname, infiniteLoop: data.infiniteLoop || false, loopCount: data.loopCount || 1, loopDelay: data.loopDelay || 0, ...data }; this.cache[name] = scheme; this.debounceSave(); return scheme; }, // 强制立即保存 forceSave() { if (this.saveTimeout) { clearTimeout(this.saveTimeout); this.saveTimeout = null; } return this.saveToStorage(); }, // 清除缓存 clearCache() { this.cache = null; this.initialized = false; } }; // 录制管理器 const RecorderManager = { start() { actions = []; startTime = Date.now(); recording = true; const recordToggleBtn = document.getElementById('recordToggle'); recordToggleBtn.textContent = '停止录制'; recordToggleBtn.style.background = '#ff4d4f'; document.getElementById('record-status').style.background = '#ff0000'; }, stop() { recording = false; const recordToggleBtn = document.getElementById('recordToggle'); recordToggleBtn.textContent = '开始录制'; recordToggleBtn.style.background = '#52c41a'; document.getElementById('record-status').style.background = '#ccc'; if (actions.length > 0) { this.save(); StorageManager.forceSave(); // 确保立即保存 } }, save() { const now = new Date(); const defaultName = now.getFullYear() + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0') + '_' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0'); StorageManager.createScheme(defaultName, { actions }); updateSchemeList(); showToast('保存成功'); } }; // 编辑管理器 const EditManager = { currentPanelPosition: null, hasUnsavedChanges: false, initialState: null, enter(name, editBtn) { this.exit(); this.hasUnsavedChanges = false; editBtn.textContent = '退出'; editBtn.style.background = '#ff4d4f'; editBtn.setAttribute('data-editing', 'true'); const scheme = StorageManager.getScheme(name); actions = [...scheme.actions]; this.initialState = { infiniteLoop: scheme.infiniteLoop, loopCount: scheme.loopCount, loopDelay: scheme.loopDelay, actions: JSON.stringify(actions) }; this.createTopEditPanel(name, scheme); ClickPointManager.createPoints(actions); }, markAsChanged() { this.hasUnsavedChanges = true; }, exit() { const editBtn = document.querySelector('.edit-btn[data-editing="true"]'); if (!editBtn) return; // 检查是否有实际改动 const currentState = this.getCurrentState(); const hasChanges = this.initialState && ( currentState.infiniteLoop !== this.initialState.infiniteLoop || currentState.loopCount !== this.initialState.loopCount || currentState.loopDelay !== this.initialState.loopDelay || currentState.actions !== this.initialState.actions ); if (hasChanges) { this.saveEditState(); StorageManager.forceSave(); // 确保立即保存 showToast('已保存编辑'); } // 保存当前面板位置 const panel = document.querySelector('.top-edit-panel'); if (panel) { this.currentPanelPosition = { left: panel.style.left, top: panel.style.top }; } ClickPointManager.cleanup(); document.querySelector('.top-edit-panel')?.remove(); editBtn.textContent = '编辑'; editBtn.style.background = '#52c41a'; editBtn.removeAttribute('data-editing'); editModeRecording = false; this.hasUnsavedChanges = false; this.initialState = null; }, getCurrentState() { const infiniteLoop = document.querySelector('#infiniteLoop'); const loopCount = document.querySelector('#loopCount'); const loopDelay = document.querySelector('#loopDelay'); return { infiniteLoop: infiniteLoop ? infiniteLoop.checked : false, loopCount: loopCount ? parseInt(loopCount.value) || 1 : 1, loopDelay: loopDelay ? parseInt(loopDelay.value) || 0 : 0, actions: JSON.stringify(actions) }; }, // 新增:更新点击点位置的通用方法 updateClickPoints() { document.querySelectorAll('.click-point').forEach((point, index) => { if (index < actions.length) { actions[index].x = parseInt(point.style.left); actions[index].y = parseInt(point.style.top); } }); }, // 新增:保存方案的通用方法 saveSchemeWithPoints(name, scheme) { this.updateClickPoints(); scheme.actions = actions; StorageManager.saveScheme(name, scheme); }, saveEditState() { const editBtn = document.querySelector('.edit-btn[data-editing="true"]'); if (!editBtn) return; const name = editBtn.closest('[data-scheme]').getAttribute('data-scheme'); const scheme = StorageManager.getScheme(name); // 更新循环设置 const infiniteLoop = document.querySelector('#infiniteLoop'); const loopCount = document.querySelector('#loopCount'); const loopDelay = document.querySelector('#loopDelay'); if (infiniteLoop && loopCount && loopDelay) { scheme.infiniteLoop = infiniteLoop.checked; scheme.loopCount = parseInt(loopCount.value) || 1; scheme.loopDelay = parseInt(loopDelay.value) || 0; } this.saveSchemeWithPoints(name, scheme); }, createTopEditPanel(name, scheme) { const topPanel = document.createElement('div'); topPanel.className = 'top-edit-panel'; // 如果有保存的位置,应用它 if (this.currentPanelPosition) { topPanel.style.left = this.currentPanelPosition.left; topPanel.style.top = this.currentPanelPosition.top; } // 应用全局设置的样式 topPanel.style.background = `rgba(255, 255, 255, ${settings.opacity})`; topPanel.style.backdropFilter = settings.useBlur ? 'blur(4px)' : 'none'; // 添加标题和按钮 const headerDiv = document.createElement('div'); headerDiv.style.cssText = ` width: 100%; display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee; `; const exitBtn = document.createElement('button'); exitBtn.textContent = '退出编辑'; exitBtn.style.cssText = ` padding: 4px 12px; background: #ff4d4f; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; `; exitBtn.onclick = this.exit.bind(this); const addActionBtn = document.createElement('button'); addActionBtn.textContent = '添加操作'; addActionBtn.style.cssText = ` padding: 4px 12px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 8px; `; addActionBtn.onclick = (e) => { e.stopPropagation(); if (!editModeRecording) { editModeRecording = true; startTime = Date.now(); addActionBtn.textContent = '停止添加'; addActionBtn.style.background = '#ff4d4f'; } else { editModeRecording = false; addActionBtn.textContent = '添加操作'; addActionBtn.style.background = '#1890ff'; if (actions.length > 0) { this.markAsChanged(); // 保存当前面板位置 const panel = document.querySelector('.top-edit-panel'); if (panel) { this.currentPanelPosition = { left: panel.style.left, top: panel.style.top }; } this.mergeActionsIntoScheme(name); } } }; headerDiv.appendChild(exitBtn); headerDiv.appendChild(addActionBtn); topPanel.appendChild(headerDiv); // 添加循环设置面板 const loopSettingsDiv = document.createElement('div'); loopSettingsDiv.style.cssText = ` width: 100%; padding: 10px; background: #f5f5f5; border-radius: 4px; margin-bottom: 10px; `; loopSettingsDiv.innerHTML = `