Browse Source

✨ feat(renderer): 实现鼠标事件录制和回放功能

- 重命名"鼠标事件拦截功能"为"鼠标事件录制和回放功能"以准确反映功能
- 添加录制状态管理(isRecording, recordedEvents, recordingStartTime)
- 实现事件录制功能,记录事件类型、目标元素信息、坐标和时间戳
- 添加录制控制函数(startRecording, stopRecording, playRecording等)
- 实现录制事件回放功能,支持按时间顺序重放鼠标事件
- 添加录制数据导入导出功能(saveRecording, loadRecording)
- 创建可视化控制面板,包含录制状态显示和操作按钮
- 优化事件目标元素识别,添加元素选择器获取功能
- 增强事件详情记录,支持不同类型鼠标事件的详细信息捕获
yourname 1 month ago
parent
commit
e4591262da
1 changed files with 299 additions and 13 deletions
  1. 299 13
      src/server/renderer.tsx

+ 299 - 13
src/server/renderer.tsx

@@ -128,9 +128,9 @@ export const Rooter = () => {
             console.log('XHR拦截器已启用,将拦截', INTERCEPT_DOMAIN, '的请求并重定向到当前域');
           })();
         `}} />
-        {/* 鼠标事件拦截功能 */}
+        {/* 鼠标事件录制和回放功能 */}
         <script dangerouslySetInnerHTML={{ __html: `
-          // 鼠标事件拦截功能
+          // 鼠标事件录制和回放功能
           (function() {
             // 定义要拦截的鼠标事件类型
             const mouseEvents = [
@@ -139,6 +139,11 @@ export const Rooter = () => {
               'contextmenu', 'wheel'
             ];
 
+            // 录制状态
+            let isRecording = false;
+            let recordedEvents = [];
+            let recordingStartTime = 0;
+
             // 保存原始的事件监听器添加方法
             const originalAddEventListener = EventTarget.prototype.addEventListener;
 
@@ -147,16 +152,36 @@ export const Rooter = () => {
               // 如果是鼠标事件,包装监听器
               if (mouseEvents.includes(type)) {
                 const wrappedListener = function(event) {
-                  // 使用console.debug输出鼠标事件信息
-                  console.debug('鼠标事件拦截:', {
-                    type: event.type,
-                    target: event.target?.tagName || 'unknown',
-                    className: event.target?.className || 'none',
-                    id: event.target?.id || 'none',
-                    x: event.clientX,
-                    y: event.clientY,
-                    timestamp: Date.now()
-                  });
+                  // 录制事件
+                  if (isRecording) {
+                    const eventData = {
+                      type: event.type,
+                      target: {
+                        tagName: event.target?.tagName || 'unknown',
+                        className: event.target?.className || 'none',
+                        id: event.target?.id || 'none',
+                        selector: getElementSelector(event.target)
+                      },
+                      x: event.clientX,
+                      y: event.clientY,
+                      timestamp: Date.now() - recordingStartTime,
+                      detail: getEventDetail(event)
+                    };
+                    recordedEvents.push(eventData);
+
+                    // 使用console.debug输出鼠标事件信息
+                    console.debug('鼠标事件录制:', eventData);
+                  } else {
+                    console.debug('鼠标事件拦截:', {
+                      type: event.type,
+                      target: event.target?.tagName || 'unknown',
+                      className: event.target?.className || 'none',
+                      id: event.target?.id || 'none',
+                      x: event.clientX,
+                      y: event.clientY,
+                      timestamp: Date.now()
+                    });
+                  }
 
                   // 调用原始监听器
                   return listener.call(this, event);
@@ -170,7 +195,268 @@ export const Rooter = () => {
               return originalAddEventListener.call(this, type, listener, options);
             };
 
-            console.log('鼠标事件拦截器已启用,将拦截所有鼠标事件并输出到console.debug');
+            // 获取元素选择器
+            function getElementSelector(element) {
+              if (!element || !element.tagName) return '';
+
+              let selector = element.tagName.toLowerCase();
+              if (element.id) {
+                selector += '#' + element.id;
+              }
+              if (element.className && typeof element.className === 'string') {
+                selector += '.' + element.className.split(' ').join('.');
+              }
+              return selector;
+            }
+
+            // 获取事件详细信息
+            function getEventDetail(event) {
+              const detail = {};
+
+              switch (event.type) {
+                case 'click':
+                case 'dblclick':
+                case 'mousedown':
+                case 'mouseup':
+                  detail.button = event.button;
+                  detail.buttons = event.buttons;
+                  break;
+                case 'wheel':
+                  detail.deltaX = event.deltaX;
+                  detail.deltaY = event.deltaY;
+                  detail.deltaZ = event.deltaZ;
+                  detail.deltaMode = event.deltaMode;
+                  break;
+                case 'contextmenu':
+                  detail.preventDefault = true;
+                  break;
+              }
+
+              return detail;
+            }
+
+            // 录制控制函数
+            window.startRecording = function() {
+              isRecording = true;
+              recordedEvents = [];
+              recordingStartTime = Date.now();
+              console.log('开始录制鼠标事件...');
+              updateControlPanel();
+            };
+
+            window.stopRecording = function() {
+              isRecording = false;
+              console.log('停止录制,共录制了 ' + recordedEvents.length + ' 个事件');
+              updateControlPanel();
+            };
+
+            window.playRecording = function() {
+              if (recordedEvents.length === 0) {
+                console.log('没有录制的事件可以播放');
+                return;
+              }
+
+              console.log('开始播放录制的事件...');
+
+              recordedEvents.forEach((eventData, index) => {
+                setTimeout(() => {
+                  replayEvent(eventData);
+                }, eventData.timestamp);
+              });
+            };
+
+            window.clearRecording = function() {
+              recordedEvents = [];
+              console.log('已清除所有录制的事件');
+              updateControlPanel();
+            };
+
+            window.saveRecording = function() {
+              const dataStr = JSON.stringify(recordedEvents, null, 2);
+              const dataBlob = new Blob([dataStr], {type: 'application/json'});
+              const url = URL.createObjectURL(dataBlob);
+              const a = document.createElement('a');
+              a.href = url;
+              a.download = 'mouse-recording-' + new Date().toISOString().replace(/:/g, '-') + '.json';
+              document.body.appendChild(a);
+              a.click();
+              document.body.removeChild(a);
+              URL.revokeObjectURL(url);
+              console.log('录制数据已保存');
+            };
+
+            window.loadRecording = function(event) {
+              const file = event.target.files[0];
+              if (!file) return;
+
+              const reader = new FileReader();
+              reader.onload = function(e) {
+                try {
+                  recordedEvents = JSON.parse(e.target.result);
+                  console.log('录制数据已加载,共 ' + recordedEvents.length + ' 个事件');
+                  updateControlPanel();
+                } catch (error) {
+                  console.error('加载录制数据失败:', error);
+                }
+              };
+              reader.readAsText(file);
+            };
+
+            // 回放事件
+            function replayEvent(eventData) {
+              let targetElement;
+
+              // 尝试通过选择器找到目标元素
+              if (eventData.target.selector) {
+                targetElement = document.querySelector(eventData.target.selector);
+              }
+
+              // 如果找不到元素,尝试通过坐标找到最近的元素
+              if (!targetElement && eventData.x && eventData.y) {
+                targetElement = document.elementFromPoint(eventData.x, eventData.y);
+              }
+
+              if (targetElement) {
+                const event = createEventFromData(eventData, targetElement);
+                targetElement.dispatchEvent(event);
+                console.debug('回放事件:', eventData.type, '目标:', eventData.target.selector);
+              } else {
+                console.warn('无法找到目标元素:', eventData.target.selector);
+              }
+            }
+
+            // 根据事件数据创建事件对象
+            function createEventFromData(eventData, targetElement) {
+              let event;
+
+              switch (eventData.type) {
+                case 'click':
+                  event = new MouseEvent('click', {
+                    clientX: eventData.x,
+                    clientY: eventData.y,
+                    button: eventData.detail?.button || 0,
+                    bubbles: true,
+                    cancelable: true
+                  });
+                  break;
+                case 'mousedown':
+                case 'mouseup':
+                  event = new MouseEvent(eventData.type, {
+                    clientX: eventData.x,
+                    clientY: eventData.y,
+                    button: eventData.detail?.button || 0,
+                    buttons: eventData.detail?.buttons || 0,
+                    bubbles: true,
+                    cancelable: true
+                  });
+                  break;
+                case 'mousemove':
+                  event = new MouseEvent('mousemove', {
+                    clientX: eventData.x,
+                    clientY: eventData.y,
+                    bubbles: true,
+                    cancelable: true
+                  });
+                  break;
+                case 'wheel':
+                  event = new WheelEvent('wheel', {
+                    clientX: eventData.x,
+                    clientY: eventData.y,
+                    deltaX: eventData.detail?.deltaX || 0,
+                    deltaY: eventData.detail?.deltaY || 0,
+                    deltaZ: eventData.detail?.deltaZ || 0,
+                    deltaMode: eventData.detail?.deltaMode || 0,
+                    bubbles: true,
+                    cancelable: true
+                  });
+                  break;
+                default:
+                  event = new MouseEvent(eventData.type, {
+                    clientX: eventData.x,
+                    clientY: eventData.y,
+                    bubbles: true,
+                    cancelable: true
+                  });
+              }
+
+              return event;
+            }
+
+            // 更新控制面板状态
+            function updateControlPanel() {
+              const panel = document.getElementById('mouse-recorder-panel');
+              if (panel) {
+                const status = panel.querySelector('.recorder-status');
+                const count = panel.querySelector('.event-count');
+
+                if (status) {
+                  status.textContent = isRecording ? '录制中...' : '已停止';
+                  status.style.color = isRecording ? '#ff4444' : '#666';
+                }
+
+                if (count) {
+                  count.textContent = recordedEvents.length + ' 个事件';
+                }
+              }
+            }
+
+            // 创建控制面板
+            function createControlPanel() {
+              const panel = document.createElement('div');
+              panel.id = 'mouse-recorder-panel';
+              panel.style.cssText = \`
+                position: fixed;
+                top: 10px;
+                right: 10px;
+                background: rgba(255, 255, 255, 0.95);
+                border: 1px solid #ccc;
+                border-radius: 8px;
+                padding: 15px;
+                box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+                font-family: Arial, sans-serif;
+                font-size: 12px;
+                z-index: 10000;
+                min-width: 200px;
+              \`;
+
+              panel.innerHTML = \`
+                <div style="margin-bottom: 10px; font-weight: bold; border-bottom: 1px solid #eee; padding-bottom: 5px;">
+                  鼠标事件录制器
+                </div>
+                <div style="margin-bottom: 10px;">
+                  <span>状态: </span>
+                  <span class="recorder-status" style="color: #666;">已停止</span>
+                </div>
+                <div style="margin-bottom: 10px;">
+                  <span>事件数: </span>
+                  <span class="event-count" style="color: #666;">0 个事件</span>
+                </div>
+                <div style="display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px;">
+                  <button onclick="window.startRecording()" style="padding: 5px 10px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer;">开始录制</button>
+                  <button onclick="window.stopRecording()" style="padding: 5px 10px; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer;">停止录制</button>
+                  <button onclick="window.playRecording()" style="padding: 5px 10px; background: #2196F3; color: white; border: none; border-radius: 3px; cursor: pointer;">播放</button>
+                  <button onclick="window.clearRecording()" style="padding: 5px 10px; background: #FF9800; color: white; border: none; border-radius: 3px; cursor: pointer;">清除</button>
+                </div>
+                <div style="display: flex; gap: 5px;">
+                  <button onclick="window.saveRecording()" style="padding: 5px 10px; background: #9C27B0; color: white; border: none; border-radius: 3px; cursor: pointer; flex: 1;">保存</button>
+                  <label style="padding: 5px 10px; background: #607D8B; color: white; border: none; border-radius: 3px; cursor: pointer; text-align: center; flex: 1;">
+                    加载
+                    <input type="file" accept=".json" onchange="window.loadRecording(event)" style="display: none;">
+                  </label>
+                </div>
+              \`;
+
+              document.body.appendChild(panel);
+            }
+
+            // 页面加载完成后创建控制面板
+            if (document.readyState === 'loading') {
+              document.addEventListener('DOMContentLoaded', createControlPanel);
+            } else {
+              createControlPanel();
+            }
+
+            console.log('鼠标事件录制和回放功能已启用');
           })();
         `}} />
         <script dangerouslySetInnerHTML={{ __html: `