您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
高效监控游戏平台用户盈亏数据,支持自动选择状态和分页,优化大数据处理
// ==UserScript== // @name 游戏盈亏监控 // @namespace https://greasyfork.org/users/your-id // @version 2.8.0 // @description 高效监控游戏平台用户盈亏数据,支持自动选择状态和分页,优化大数据处理 // @author Cisco // @match https://*.topcms.org/* // @icon https://7777m.topcms.org/favicon.ico // @license MIT // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @run-at document-end // ==/UserScript== (function() { 'use strict'; // 配置参数 const config = { checkInterval: 2000, // 检查间隔(毫秒) profitThreshold: null, // 盈利阈值(>=) lossThreshold: null, // 亏损阈值(<) monitoring: false, // 监控状态 currentIndex: 0, // 当前处理索引(反向模式从最后开始) columnIndex: 0, // 当前列索引 currentPage: 1, // 当前页码 totalPages: 1, // 总页数 totalItems: 0, // 总条目数 itemsPerPage: 10, // 每页条目数 batchSize: 1, // 每批处理数量 maxParallel: 1, // 最大并行数 activeRequests: 0, // 活跃请求数 processedItems: 0, // 已处理条目数(仅成功计数) monitoringDuration: 40, // 监控时长(分钟) lastCheckTime: 0, // 最后检查时间 startTime: 0, // 开始时间 panelCollapsed: false, // 面板是否收起 profitAlerts: 0, // 盈利警报数 lossAlerts: 0, // 亏损警报数 reverseMode: true, // 是否反向模式(从下往上) initialLoadDone: false, // 初始加载完成标志 isProcessing: false // 是否正在处理用户 }; // 存储对象 const storage = { get: function(key, defaultValue) { try { if (typeof GM_getValue !== 'undefined') { return GM_getValue(key, defaultValue); } const value = localStorage.getItem(`monitor_${key}`); return value !== null ? JSON.parse(value) : defaultValue; } catch (e) { console.error('存储读取错误:', e); return defaultValue; } }, set: function(key, value) { try { if (typeof GM_setValue !== 'undefined') { GM_setValue(key, value); } else { localStorage.setItem(`monitor_${key}`, JSON.stringify(value)); } } catch (e) { console.error('存储写入错误:', e); } } }; // 添加样式 function addStyles() { const css = ` .monitor-panel { position: fixed; top: 20px; right: 20px; z-index: 9999; background: white; padding: 15px; border: 1px solid #ddd; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); font-family: Arial, sans-serif; width: 320px; max-height: 90vh; overflow-y: auto; transition: all 0.3s ease; } .monitor-panel.collapsed { width: 40px; height: 40px; overflow: hidden; padding: 5px; } .toggle-panel { position: absolute; top: 5px; right: 5px; width: 30px; height: 30px; border: none; background: #f0f0f0; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; z-index: 10000; } .toggle-panel:hover { background: #e0e0e0; } .collapsed .panel-content { display: none; } .monitor-header { margin: 0 0 15px 0; color: #333; font-size: 16px; font-weight: bold; border-bottom: 1px solid #eee; padding-bottom: 10px; } .monitor-input-group { margin-bottom: 12px; } .monitor-label { display: block; margin-bottom: 5px; color: #666; font-size: 13px; } .monitor-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } .monitor-button { width: 100%; padding: 10px; background: #409EFF; color: white; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; transition: background 0.3s; } .monitor-button.stop { background: #F56C6C; } .monitor-stats { margin-top: 15px; font-size: 12px; color: #666; border-top: 1px solid #eee; padding-top: 10px; } .monitor-stat-row { display: flex; justify-content: space-between; margin-bottom: 5px; } .monitor-progress-container { margin: 10px 0; height: 10px; background: #f0f0f0; border-radius: 5px; overflow: hidden; } .monitor-progress-bar { height: 100%; background: linear-gradient(to right, #67C23A, #409EFF); transition: width 0.3s; } .monitor-speed { font-size: 11px; color: #999; text-align: right; } .monitor-alert-count { display: flex; justify-content: space-between; margin-top: 5px; } .monitor-alert-badge { display: inline-block; padding: 2px 6px; border-radius: 10px; font-size: 11px; font-weight: bold; } .profit-badge { background: #f0f9eb; color: #67C23A; } .loss-badge { background: #fef0f0; color: #F56C6C; } `; const style = document.createElement('style'); style.type = 'text/css'; style.appendChild(document.createTextNode(css)); document.head.appendChild(style); } // 创建控制面板 function createControlPanel() { addStyles(); const panel = document.createElement('div'); panel.className = 'monitor-panel'; panel.id = 'monitorPanel'; // 添加收起/展开按钮 const toggleBtn = document.createElement('button'); toggleBtn.className = 'toggle-panel'; toggleBtn.innerHTML = '×'; toggleBtn.title = '收起/展开控制面板'; toggleBtn.addEventListener('click', togglePanel); // 面板内容 const panelContent = document.createElement('div'); panelContent.className = 'panel-content'; panelContent.innerHTML = ` <h3 class="monitor-header">游戏盈亏监控</h3> <div class="monitor-input-group"> <label class="monitor-label">余额阈值(>=)</label> <input type="number" id="profitThresholdInput" placeholder="输入正数" class="monitor-input"> </div> <div class="monitor-input-group"> <label class="monitor-label">余额阈值(<)</label> <input type="number" id="lossThresholdInput" placeholder="输入正数" class="monitor-input"> </div> <div class="monitor-input-group"> <label class="monitor-label">监控时长(分钟)</label> <input type="number" id="minutesInput" value="40" min="1" class="monitor-input"> </div> <div class="monitor-input-group"> <label class="monitor-label">并行数量</label> <input type="number" id="parallelInput" value="1" min="1" max="10" class="monitor-input" disabled> </div> <button id="toggleMonitor" class="monitor-button">开始监控</button> <div class="monitor-stats"> <div class="monitor-stat-row"> <span>状态:</span> <span id="statusText">未启动</span> </div> <div class="monitor-stat-row"> <span>进度:</span> <span id="currentPosition">0</span>/<span id="totalItems">0</span> 条 </div> <div class="monitor-stat-row"> <span>页数:</span> <span id="displayPage">1</span>/<span id="totalPages">1</span> </div> <div class="monitor-stat-row"> <span>速度:</span> <span id="speedText">0 条/分钟</span> </div> <div class="monitor-progress-container"> <div id="progressBar" class="monitor-progress-bar" style="width: 0%"></div> </div> <div class="monitor-speed" id="timeRemaining">此轮预计剩余时间: 计算中...</div> <div class="monitor-alert-count"> <span class="monitor-alert-badge profit-badge">余额超标: <span id="profitAlerts">0</span></span> <span class="monitor-alert-badge loss-badge">余额过低: <span id="lossAlerts">0</span></span> </div> </div> `; panel.appendChild(toggleBtn); panel.appendChild(panelContent); document.body.appendChild(panel); // 恢复面板状态和设置 config.panelCollapsed = storage.get('panelCollapsed', false); if (config.panelCollapsed) { panel.classList.add('collapsed'); toggleBtn.innerHTML = '≡'; } const savedProfit = storage.get('profitThreshold', null); const savedLoss = storage.get('lossThreshold', null); const savedMinutes = storage.get('monitoringDuration', 40); const savedParallel = storage.get('parallelCount', 2); config.profitAlerts = storage.get('profitAlerts', 0); config.lossAlerts = storage.get('lossAlerts', 0); if (savedProfit) document.getElementById('profitThresholdInput').value = savedProfit; if (savedLoss) document.getElementById('lossThresholdInput').value = savedLoss; document.getElementById('minutesInput').value = savedMinutes; document.getElementById('parallelInput').value = savedParallel; document.getElementById('profitAlerts').textContent = config.profitAlerts; document.getElementById('lossAlerts').textContent = config.lossAlerts; document.getElementById('toggleMonitor').addEventListener('click', toggleMonitoring); } // 收起/展开面板 function togglePanel() { const panel = document.getElementById('monitorPanel'); config.panelCollapsed = !panel.classList.contains('collapsed'); if (config.panelCollapsed) { panel.classList.add('collapsed'); this.innerHTML = '≡'; } else { panel.classList.remove('collapsed'); this.innerHTML = '×'; } storage.set('panelCollapsed', config.panelCollapsed); } // 切换监控状态 function toggleMonitoring() { const profitVal = parseFloat(document.getElementById('profitThresholdInput').value); const lossVal = parseFloat(document.getElementById('lossThresholdInput').value); const minutes = parseInt(document.getElementById('minutesInput').value) || 40; const parallel = parseInt(document.getElementById('parallelInput').value) || 1; if (isNaN(profitVal) && isNaN(lossVal)) { alert('请至少设置一个阈值'); return; } storage.set('profitThreshold', isNaN(profitVal) ? null : profitVal); storage.set('lossThreshold', isNaN(lossVal) ? null : Math.abs(lossVal)); storage.set('monitoringDuration', minutes); storage.set('parallelCount', parallel); config.profitThreshold = isNaN(profitVal) ? null : profitVal; config.lossThreshold = isNaN(lossVal) ? null : Math.abs(lossVal); config.monitoringDuration = minutes; config.maxParallel = Math.min(Math.max(parallel, 1), 10); config.monitoring = !config.monitoring; const btn = document.getElementById('toggleMonitor'); const status = document.getElementById('statusText'); if (config.monitoring) { btn.textContent = '停止监控'; btn.classList.add('stop'); let statusMsg = '监控中 ('; if (config.profitThreshold) statusMsg += `余额>=${config.profitThreshold}`; if (config.lossThreshold) statusMsg += `${config.profitThreshold ? ' ' : ''}余额<${config.lossThreshold}`; status.textContent = statusMsg + ')'; config.startTime = Date.now(); config.processedItems = 0; config.lastCheckTime = Date.now(); startMonitoring(); } else { btn.textContent = '开始监控'; btn.classList.remove('stop'); status.textContent = '已停止'; // 清除所有正在进行的请求和队列 config.activeRequests = 0; config.isProcessing = false; // 重置计数器 config.currentIndex = 0; config.processedItems = 0; // 更新UI显示 document.getElementById('currentPosition').textContent = '0'; document.getElementById('progressBar').style.width = '0%'; } } // 主监控流程,支持反向模式 function startMonitoring() { if (!config.monitoring) { console.log('监控未启用,不启动'); return; } console.log('启动监控流程...'); config.startTime = Date.now(); config.processedItems = 0; config.currentIndex = 0; config.currentPage = 1; // 重置为第一页开始 config.initialLoadDone = false; config.profitAlerts = 0; config.lossAlerts = 0; config.isProcessing = false; // 先初始化监控设置,完成后跳转到最后一页 initMonitoring(() => { goToLastPage(() => { console.log('初始化完成并已跳转到最后一页'); config.initialLoadDone = true; processBatch(); }); }); } // 跳转到最后一页函数 function goToLastPage(callback) { console.log('尝试跳转到最后一页,当前页:', config.currentPage, '总页数:', config.totalPages); updatePaginationInfo(); if (config.currentPage === config.totalPages) { console.log('已在最后一页'); waitForTableData(callback); return; } const lastPageNumber = document.querySelector('.el-pager .number:last-child'); if (lastPageNumber && !lastPageNumber.classList.contains('active')) { safeClick(lastPageNumber, () => { setTimeout(() => { config.currentPage = config.totalPages; updatePaginationInfo(); console.log('点击最后一页按钮后,等待数据刷新...'); waitForTableData(callback); }, 2000); }); return; } const nextBtn = document.querySelector('.el-pagination .btn-next:not([disabled])'); if (nextBtn) { let remainingPages = config.totalPages - config.currentPage; const clickNext = () => { if (remainingPages <= 0) { config.currentPage = config.totalPages; updatePaginationInfo(); console.log('已通过下一页按钮到达最后一页'); waitForTableData(callback); return; } safeClick(nextBtn, () => { remainingPages--; config.currentPage++; console.log('点击下一页,剩余:', remainingPages); setTimeout(clickNext, 2500); }); }; clickNext(); return; } console.warn('分页按钮全部失效,直接设置为最后一页'); config.currentPage = config.totalPages; waitForTableData(callback); } // 安全点击函数(带回调) function safeClick(element, callback) { if (simulateClick(element)) { setTimeout(() => { if (callback) callback(); }, 500); } else if (callback) { callback(); } } // 可靠的模拟点击函数 function simulateClick(element) { if (!element) { console.log('模拟点击失败:元素不存在'); return false; } // 方法1: 直接调用click方法 try { element.click(); return true; } catch (e) { console.log('直接click方法失败,尝试其他方法'); } // 方法2: 创建并派发鼠标事件 try { const mouseDown = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }); element.dispatchEvent(mouseDown); const mouseUp = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }); element.dispatchEvent(mouseUp); const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }); element.dispatchEvent(clickEvent); return true; } catch (e) { console.log('标准MouseEvent创建失败:', e); } // 方法3: 最简事件 try { const event = document.createEvent('Event'); event.initEvent('click', true, true); element.dispatchEvent(event); return true; } catch (e) { console.log('最简事件创建失败:', e); } console.log('所有点击模拟方法均失败'); return false; } // 选择下拉选项 function selectDropdownOption(selector, optionText, callback) { const dropdown = document.querySelector(selector); if (!dropdown) { console.log('未找到下拉框:', selector); if (callback) callback(false); return; } // 先检查当前是否已经是目标值 const currentValue = dropdown.querySelector('.el-input__inner')?.value; if (currentValue === optionText) { if (callback) callback(true); return; } // 点击打开下拉框 safeClick(dropdown, () => { setTimeout(() => { // 查找页面中所有可见的下拉菜单 const allMenus = Array.from(document.querySelectorAll('.el-select-dropdown')); const visibleMenus = allMenus.filter(menu => { return !menu.style.display || menu.style.display !== 'none'; }); // 查找与当前下拉框关联的菜单 let targetMenu = null; const dropdownRect = dropdown.getBoundingClientRect(); for (const menu of visibleMenus) { const menuRect = menu.getBoundingClientRect(); // 检查菜单是否出现在下拉框附近 if (Math.abs(menuRect.left - dropdownRect.left) < 50 && (Math.abs(menuRect.top - dropdownRect.bottom) < 20 || Math.abs(menuRect.bottom - dropdownRect.top) < 20)) { targetMenu = menu; break; } } if (!targetMenu) { console.log('未找到关联的下拉菜单'); if (callback) callback(false); return; } // 查找匹配的选项 const options = targetMenu.querySelectorAll('.el-select-dropdown__item'); let optionFound = false; for (const option of options) { if (option.textContent.trim() === optionText) { // 确保选项可见 option.scrollIntoView({ behavior: 'instant', block: 'nearest' }); // 点击选项 setTimeout(() => { if (simulateClick(option)) { optionFound = true; // 等待下拉框关闭 setTimeout(() => { if (callback) callback(true); }, 800); } else { if (callback) callback(false); } }, 200); break; } } if (!optionFound) { console.log('未找到选项:', optionText); if (callback) callback(false); } }, 500); }); } // 设置每页显示条数 function setPageSize(size, callback) { selectDropdownOption('.el-pagination__sizes .el-select', `${size}条/页`, (success) => { if (success) { config.itemsPerPage = size; // 设置成功后强制重新加载数据 setTimeout(() => { updatePaginationInfo(); if (callback) callback(true); }, 1500); // 等待数据重新加载 } else { if (callback) callback(false); } }); } // 选择订单状态 function selectOrderStatus(status, callback) { selectDropdownOption('.el-select.filter-item', status, callback); } // 设置时间范围 function setTimeRange(callback) { const now = new Date(); // 计算开始时间为当前时间减去监控时长 时区需要-8小时 const startTime = new Date(now.getTime() - (8 * 60 * 60000) - (config.monitoringDuration * 60000)); const endTime = new Date(now.getTime() - (8 * 60 * 60000)); const formatTime = (date) => { const pad = num => num.toString().padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }; console.log('准备设置时间范围:', formatTime(startTime), '至', formatTime(endTime)); // 1. 确保日期选择器已打开 const datePicker = document.querySelector('.el-date-editor.el-range-editor'); if (!datePicker) { console.error('未找到日期选择器元素'); if (callback) callback(); return; } // 点击日期选择器以打开面板 safeClick(datePicker, () => { console.log('已点击日期选择器,等待面板打开...'); // 2. 等待面板动画完成 setTimeout(() => { const timeInputs = document.querySelectorAll('.el-range-input'); if (timeInputs.length < 2) { console.error('未找到时间输入框'); if (callback) callback(); return; } // 3. 设置时间值 const startStr = formatTime(startTime); const endStr = formatTime(endTime); console.log('正在设置时间值:', startStr, endStr); // 直接设置输入框值并触发事件 timeInputs[0].value = startStr; timeInputs[1].value = endStr; timeInputs.forEach(input => { input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); }); // 4. 尝试点击确定按钮 setTimeout(() => { // 查找确定按钮(多种可能的选择器) const confirmBtn = document.querySelector('.el-picker-panel__footer .el-button:not(.el-button--text), ' + '.el-date-range-picker__footer .el-button:not(.el-button--text)'); if (confirmBtn) { console.log('找到确定按钮,正在点击...'); safeClick(confirmBtn, () => { console.log('时间范围已确认'); if (callback) setTimeout(callback, 500); }); } else { console.log('未找到确定按钮,尝试直接提交查询'); // 直接触发查询作为后备方案 if (callback) setTimeout(callback, 500); } }, 800); }, 1000); }); } // 初始化监控 function initMonitoring(callback) { // 先重置分页信息(但不重置currentPage) config.currentIndex = 0; const initSteps = [ (next) => selectOrderStatus('已支付', (success) => { console.log(success ? '状态设置成功' : '状态设置失败'); next(); }), (next) => setTimeRange(() => { console.log('时间设置完成'); next(); }), (next) => setPageSize(200, (success) => { console.log(success ? '分页设置成功' : '分页设置失败'); next(); }), () => { console.log('开始查询'); updatePaginationInfo(); setTimeout(() => { clickQueryButton(); if (callback) callback(); }, 1000); } ]; function executeStep() { if (initSteps.length > 0) { const step = initSteps.shift(); step(executeStep); } } executeStep(); } // 更新分页信息 function updatePaginationInfo() { const pagination = document.querySelector('.el-pagination'); if (!pagination) { const rows = document.querySelectorAll('.el-table__row:not(.el-table__row--level)'); config.totalItems = rows.length; config.itemsPerPage = rows.length || config.itemsPerPage; config.totalPages = 1; } else { // 获取总条数 const totalText = pagination.querySelector('.el-pagination__total')?.textContent || ''; const totalMatch = totalText.match(/(\d+)(?=\s*条)/) || [null, 0]; config.totalItems = parseInt(totalMatch[1]) || 0; // 优先从分页按钮获取总页数 const pageButtons = pagination.querySelectorAll('.el-pager .number'); if (pageButtons.length > 0) { const lastPageBtn = pageButtons[pageButtons.length - 1]; config.totalPages = parseInt(lastPageBtn.textContent) || 1; console.log('从分页按钮获取总页数:', config.totalPages); } else { // 回退计算方式 config.itemsPerPage = config.itemsPerPage || 200; config.totalPages = Math.ceil(config.totalItems / config.itemsPerPage); console.log('计算得到总页数:', config.totalPages); } // 获取当前页码 const activePage = pagination.querySelector('.el-pager .number.active'); if (activePage) { config.currentPage = parseInt(activePage.textContent) || 1; } } // 更新UI显示 document.getElementById('totalItems').textContent = config.totalItems; document.getElementById('totalPages').textContent = config.totalPages; document.getElementById('displayPage').textContent = config.currentPage; updateProgressDisplay(); console.log('分页信息更新 - 当前页:', config.currentPage, '总页数:', config.totalPages, '总条数:', config.totalItems); } // 点击查询按钮 function clickQueryButton() { const queryBtn = [...document.querySelectorAll('.filter-container button.el-button')] .find(btn => !btn.classList.contains('is-disabled') && btn.textContent.includes('查询')); if (queryBtn) { safeClick(queryBtn, () => { setTimeout(() => checkUsers(), 3000); }); } else { console.log('未找到查询按钮'); setTimeout(() => { if (config.monitoring) clickQueryButton(); }, 1000); } } // 更新进度显示函数 function updateProgressDisplay() { // 计算已完成数量 const currentPos = parseInt(document.getElementById('currentPosition').textContent); const totalItems = parseInt(document.getElementById('totalItems').textContent); // 计算进度百分比 const progressPercent = (currentPos / totalItems * 100).toFixed(1); document.getElementById('progressBar').style.width = `${progressPercent}%`; // 计算处理速度 const now = Date.now(); const elapsedMinutes = (now - config.startTime) / 60000; const speed = elapsedMinutes > 0 ? Math.round(config.processedItems / elapsedMinutes) : 0; document.getElementById('speedText').textContent = `${speed} 条/分钟`; // 计算剩余时间 if (speed > 0) { const remainingItems = totalItems - currentPos; const remainingMinutes = Math.ceil(remainingItems / speed); document.getElementById('timeRemaining').textContent = `此轮预计剩余时间: ${remainingMinutes} 分钟`; } } // 检查用户列表 function checkUsers() { const userRows = document.querySelectorAll('.el-table__row:not(.el-table__row--level)'); if (userRows.length === 0) { setTimeout(() => { if (config.monitoring) startMonitoring(); }, config.checkInterval); return; } config.currentIndex = 0; config.initialLoadDone = true; processBatch(); } // 修改resetForNewRound函数 function resetForNewRound() { console.log('准备开始新一轮完整监控流程'); // 重置所有关键状态 config.currentIndex = 0; config.processedItems = 0; config.activeRequests = 0; config.profitAlerts = 0; config.lossAlerts = 0; config.isProcessing = false; // 更新UI显示 document.getElementById('currentPosition').textContent = '0'; document.getElementById('progressBar').style.width = '0%'; document.getElementById('profitAlerts').textContent = '0'; document.getElementById('lossAlerts').textContent = '0'; // 重新执行完整初始化 initMonitoring(() => { // 初始化完成后跳转到最后一页 goToLastPage(() => { console.log('新一轮监控准备就绪,从最后一页开始'); config.initialLoadDone = true; processBatch(); }); }); } // 处理单个用户 async function processSingleUser(row, userIdElement, userNameElement) { try { // 步骤1: 关闭所有现有弹窗 await closeAllDialogs(); const userId = userIdElement.textContent.trim(); // 步骤2: 滚动到用户行 userNameElement.scrollIntoView({ behavior: 'auto', block: 'center' }); await new Promise(resolve => setTimeout(resolve, 300)); // 步骤3: 打开用户弹窗 if (!simulateClick(userNameElement)) { throw new Error('点击用户元素失败'); } // 步骤4: 等待弹窗出现 const dialog = await waitForElement('.el-dialog__body .el-table', { multiple: true, notEmpty: true, timeout: 8000 }); if (!dialog) { throw new Error('弹窗加载超时'); } // 步骤5: 检查余额 const balance = await getBalanceFromSecondTable(dialog); if (balance === null) { throw new Error('获取余额失败'); } // 步骤6: 阈值检查 if (config.profitThreshold !== null && balance >= config.profitThreshold) { const exceed = (balance - config.profitThreshold).toFixed(2); await showAlert(`用户${userId} 余额超标: ${balance} (超过${exceed})`, 'profit'); incrementAlertCount('profit'); } else if (config.lossThreshold !== null && balance < config.lossThreshold) { const below = (config.lossThreshold - balance).toFixed(2); await showAlert(`用户${userId} 余额不足: ${balance} (低于${below})`, 'loss'); incrementAlertCount('loss'); } } catch (e) { console.error('处理用户失败:', e); } finally { // 步骤7: 确保关闭弹窗 await closeAllDialogs(); } } async function getBalanceFromSecondTable(dialog) { const secondTable = dialog[1]; // 等待第二个表格中至少有一行数据出现 const dataRows = await waitForElement('.el-table__body-wrapper tbody tr', { root: secondTable, multiple: true, timeout: 8000 }); if (!dataRows || dataRows.length === 0) { console.log('❌ 第二个表格中没有数据行'); return null; } const firstRow = dataRows[0]; const cells = firstRow.querySelectorAll('td'); if (cells.length === 0) { console.log('❌ 第一行中没有单元格'); return null; } const balanceText = cells[0].innerText.trim(); // 根据需要调整列索引 console.log('✅ 余额:', balanceText); return balanceText; } // 关闭所有弹窗 async function closeAllDialogs() { const dialogs = document.querySelectorAll('.el-dialog__wrapper:not([style*="display: none"])'); for (const dialog of dialogs) { const closeBtn = dialog.querySelector('.el-dialog__headerbtn'); if (closeBtn) { simulateClick(closeBtn); await new Promise(resolve => setTimeout(resolve, 300)); } } } /** * 等待指定元素加载完成 * @param {string} selector CSS 选择器 * @param {Object} options 可选项 * @param {number} [options.timeout=5000] 超时时间,毫秒 * @param {Element} [options.root=document] 查询范围(默认整个文档) * @param {boolean} [options.multiple=false] 是否等待多个元素(querySelectorAll) * @param {boolean} [options.notEmpty=false] 是否等待元素有子节点/非空 * @returns {Promise<Element|Element[]|null>} */ function waitForElement(selector, options = {}) { const { timeout = 5000, root = document, multiple = false, notEmpty = false } = options; return new Promise((resolve) => { const startTime = Date.now(); const check = () => { let element = multiple ? root.querySelectorAll(selector) : root.querySelector(selector); let found = multiple ? element.length > 0 : !!element; if (found && notEmpty) { if (multiple) { found = Array.from(element).every(el => el.textContent.trim().length > 0); } else { found = element.textContent.trim().length > 0; } } if (found) { resolve(element); } else if (Date.now() - startTime < timeout) { setTimeout(check, 100); } else { resolve(null); } }; check(); }); } // 向前翻页函数 function goToPreviousPage() { console.log('尝试转到上一页...'); const userRows = document.querySelectorAll('.el-table__row:not(.el-table__row--level)'); config.currentIndex = Math.min(config.currentIndex, userRows.length); // 限制索引不超过用户数 const prevBtn = document.querySelector('.el-pagination .btn-prev:not([disabled])'); if (prevBtn) { safeClick(prevBtn, () => { setTimeout(() => { config.currentPage--; config.currentIndex = 0; // 重置为0,准备从新页的最后开始 updatePaginationInfo(); console.log(`已转到第 ${config.currentPage} 页`); setTimeout(() => checkUsers(), 2000); }, 1500); }); } else { console.log('已经是第一页,重新开始循环'); resetForNewRound(); } } // 等待表格数据加载 function waitForTableData(callback, retries = 10) { const interval = 500; const check = () => { const rows = document.querySelectorAll('.el-table__row:not(.el-table__row--level)'); if (rows.length > 0) { console.log('表格数据已加载,共', rows.length, '行'); if (callback) callback(); } else if (retries > 0) { console.log('等待表格数据加载...'); setTimeout(() => waitForTableData(callback, retries - 1), interval); } else { console.warn('等待表格数据超时,强制进入流程'); if (callback) callback(); // 即使失败也执行 } }; check(); } /** * 处理单个批次(顺序处理) */ async function processBatch() { // 添加更严格的检查条件 if (!config.monitoring || config.isProcessing) { console.log('监控未启用或正在处理中,跳过'); return; } const userRows = document.querySelectorAll('.el-table__row:not(.el-table__row--level)'); if (userRows.length === 0) { console.log('当前页没有用户行'); return; } // 改进的索引计算 let startIndex = userRows.length - 1 - config.currentIndex; console.log(`处理批次 - 当前索引: ${config.currentIndex}, 计算起始索引: ${startIndex}`); // 更严格的边界检查 if (startIndex < 0 || startIndex >= userRows.length) { console.log(`索引越界 (${startIndex}),准备翻页`); if (config.currentPage > 1) { await goToPreviousPage(); } else { await resetForNewRound(); } return; } // 更严格的处理锁 try { config.isProcessing = true; const currentRow = userRows[startIndex]; const userIdElement = currentRow.querySelector('div[style*="height: 20px; color: gray;"]'); const userNameElement = currentRow.querySelector('.el-tooltip[style*="color: rgb(24, 144, 255)"]'); if (!userNameElement) { console.log('跳过无效用户条目'); config.currentIndex++; return; } // 添加处理前检查 const userId = userIdElement?.textContent.trim(); if (config.lastProcessedUserId === userId) { console.log('检测到重复用户ID,跳过:', userId); config.currentIndex++; return; } await processSingleUser(currentRow, userIdElement, userNameElement); // 成功处理后更新状态 config.lastProcessedUserId = userId; config.currentIndex++; config.processedItems++; // 立即更新UI document.getElementById('currentPosition').textContent = config.processedItems; updateProgressDisplay(); } catch (e) { console.error('处理用户失败:', e); } finally { config.isProcessing = false; // 添加更智能的延迟 const delay = config.activeRequests > 0 ? 500 : 300; setTimeout(processBatch, delay); } } // 增加警报计数 function incrementAlertCount(type) { const elementId = type === 'profit' ? 'profitAlerts' : 'lossAlerts'; const currentCount = parseInt(document.getElementById(elementId).textContent) || 0; const newCount = currentCount + 1; document.getElementById(elementId).textContent = newCount; if (type === 'profit') { config.profitAlerts = newCount; storage.set('profitAlerts', newCount); } else { config.lossAlerts = newCount; storage.set('lossAlerts', newCount); } } // 改进的通知函数 async function showAlert(message, type) { // 1. 控制台日志 console.warn(`[ALERT] ${message}`); // 提取userId部分(匹配"用户ID:数字"格式) const userIdMatch = message.match(/用户ID:(\d+)/); const userId = userIdMatch ? userIdMatch[1] : ''; // 创建Promise以便暂停执行 return new Promise((resolve) => { // 创建自定义alert弹窗 const alertWrapper = document.createElement('div'); alertWrapper.style.position = 'fixed'; alertWrapper.style.top = '0'; alertWrapper.style.left = '0'; alertWrapper.style.width = '100%'; alertWrapper.style.height = '100%'; alertWrapper.style.backgroundColor = 'rgba(0,0,0,0.5)'; alertWrapper.style.display = 'flex'; alertWrapper.style.justifyContent = 'center'; alertWrapper.style.alignItems = 'center'; alertWrapper.style.zIndex = '99999'; const alertBox = document.createElement('div'); alertBox.style.backgroundColor = 'white'; alertBox.style.padding = '20px'; alertBox.style.borderRadius = '5px'; alertBox.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; alertBox.style.maxWidth = '80%'; alertBox.style.wordBreak = 'break-all'; // 高亮显示用户ID并添加复制功能 if (userId) { const highlightedMessage = message.replace( `用户ID:${userId}`, `用户ID:<span id="userIdSpan" style="color: #1890ff; cursor: pointer; font-weight: bold; text-decoration: underline;" title="点击复制用户ID"> ${userId} </span> <span id="copyFeedback" style="color: green; display: none; margin-left: 5px;">已复制!</span>` ); alertBox.innerHTML = highlightedMessage; // 添加点击事件处理程序 setTimeout(() => { const userIdSpan = document.getElementById('userIdSpan'); const copyFeedback = document.getElementById('copyFeedback'); if (userIdSpan && copyFeedback) { userIdSpan.onclick = () => { navigator.clipboard.writeText(userId).then(() => { copyFeedback.style.display = 'inline'; setTimeout(() => { copyFeedback.style.display = 'none'; }, 1000); }); }; } }, 0); } else { alertBox.textContent = message; } // 添加确定按钮 const okButton = document.createElement('button'); okButton.textContent = '确定'; okButton.style.marginTop = '15px'; okButton.style.padding = '5px 15px'; okButton.style.backgroundColor = '#409EFF'; okButton.style.color = 'white'; okButton.style.border = 'none'; okButton.style.borderRadius = '3px'; okButton.style.cursor = 'pointer'; okButton.onclick = () => { document.body.removeChild(alertWrapper); resolve(); // 解决Promise,继续执行 }; alertBox.appendChild(document.createElement('br')); alertBox.appendChild(okButton); alertWrapper.appendChild(alertBox); document.body.appendChild(alertWrapper); // 2. 使用GM_notification或浏览器通知 if (typeof GM_notification !== 'undefined') { try { GM_notification({ title: type === 'profit' ? '超标报警' : '不足报警', text: message, timeout: 5000, onclick: function () { window.focus(); } }); return; } catch (e) { console.error('GM_notification失败:', e); } } // 3. 使用Web Notification if (window.Notification && Notification.permission === 'granted') { new Notification(type === 'profit' ? '超标报警' : '不足报警', { body: message, icon: 'https://7777m.topcms.org/favicon.ico' }); return; } else if (window.Notification && Notification.permission !== 'denied') { Notification.requestPermission().then(function (permission) { if (permission === 'granted') { new Notification(type === 'profit' ? '超标报警' : '不足报警', { body: message, icon: 'https://7777m.topcms.org/favicon.ico' }); } else { fallbackAlert(message); } }); return; } // 4. 最终弹窗方案 fallbackAlert(message); }); } function fallbackAlert(message) { // 创建自定义弹窗 const alertDiv = document.createElement('div'); alertDiv.style.position = 'fixed'; alertDiv.style.top = '20px'; alertDiv.style.right = '20px'; alertDiv.style.padding = '15px'; alertDiv.style.background = '#f8f8f8'; alertDiv.style.border = '1px solid #ddd'; alertDiv.style.borderRadius = '5px'; alertDiv.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)'; alertDiv.style.zIndex = '99999'; alertDiv.textContent = message; document.body.appendChild(alertDiv); // 5秒后自动消失 setTimeout(() => { alertDiv.remove(); }, 5000); } // 初始化 function init() { const checkTable = setInterval(() => { if (document.querySelector('.el-table')) { clearInterval(checkTable); createControlPanel(); console.log('脚本初始化完成'); } }, 500); } if (document.readyState === 'complete') { init(); } else { window.addEventListener('load', init); } })();