// ==UserScript==
// @name 游戏盈亏监控
// @namespace https://greasyfork.org/users/your-id
// @version 2.6.2
// @description 高效监控游戏平台用户盈亏数据,支持自动选择状态和分页,优化大数据处理
// @author Cisco
// @match https://7777m.topcms.org/*
// @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
// @downloadURL none
// ==/UserScript==
(function(){
'use strict';
// ========== 配置对象 ==========
const config = {
checkInterval: 2000, // 轮询间隔 ms
profitThreshold: null, // 盈利阈值
lossThreshold: null, // 亏损阈值
monitoring: false, // 监控状态
currentIndex: 0, // 当前行索引
currentPage: 1, // 当前页
totalPages: 1, // 总页数
totalItems: 0, // 总条数
itemsPerPage: 10, // 每页条数
batchSize: 5, // 每批处理条数(非并发限制,只是批次)
maxParallel: 3, // 最大并发请求数
activeRequests: 0, // 当前活跃请求数
processedItems: 0, // 已处理条数
monitoringDuration: 40, // 监控时长 分钟
lastCheckTime: 0, // 上次检查时间戳
startTime: 0, // 启动时间戳
panelCollapsed: false, // 面板收起状态
profitAlerts: 0, // 盈利报警计数
lossAlerts: 0, // 亏损报警计数
dialogQueue: [], // 弹窗队列:存放待处理的弹窗任务函数
isDialogProcessing: false, // 是否正在处理弹窗(保证串行)
activeDialogs: {}, // 当前活跃弹窗信息,格式 {dialogId: {userName, callback}}
dialogCounter: 0 // 弹窗唯一ID计数器
};
// ========== 本地存储辅助 ==========
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('Storage get 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('Storage set 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 = `
状态:未启动
进度:0/0条
页数:1/1
速度:0条/分钟
预计剩余时间:计算中...
盈利超标:0
亏损超标:0
`;
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', 3);
config.profitAlerts = storage.get('profitAlerts', 0);
config.lossAlerts = storage.get('lossAlerts', 0);
if(savedProfit !== null) document.getElementById('profitThresholdInput').value = savedProfit;
if(savedLoss !== null) 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) || 3;
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 = '已停止';
}
}
// ========== 启动监控主流程 ==========
function startMonitoring() {
if(!config.monitoring) return;
const firstPageBtn = document.querySelector('.el-pager .number:first-child');
if(firstPageBtn && !firstPageBtn.classList.contains('active')) {
safeClick(firstPageBtn, () => {
setTimeout(() => initMonitoring(), 2000);
});
} else {
initMonitoring();
}
}
// ========== 模拟点击事件 ==========
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;
}
try {
element.click();
return true;
} catch(e) {
// 尝试MouseEvent
}
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) {}
try {
const event = document.createEvent('Event');
event.initEvent('click', true, true);
element.dispatchEvent(event);
return true;
} catch(e) {}
console.log('所有点击模拟方法均失败');
return false;
}
// ========== 初始化监控设置 ==========
function initMonitoring() {
config.currentPage = 1;
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(), 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;
config.itemsPerPage = config.itemsPerPage || 200;
config.totalPages = Math.ceil(config.totalItems / config.itemsPerPage);
}
document.getElementById('totalItems').textContent = config.totalItems;
document.getElementById('totalPages').textContent = config.totalPages;
updateProgressDisplay();
console.log('更新分页信息 - 总条数:', config.totalItems, '每页:', config.itemsPerPage, '总页数:', config.totalPages);
}
// ========== 点击查询按钮 ==========
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 = (config.currentPage - 1) * config.itemsPerPage + config.currentIndex + 1;
document.getElementById('currentPosition').textContent = Math.min(currentPos, config.totalItems);
document.getElementById('displayPage').textContent = config.currentPage;
const progressPercent = config.totalItems ? (currentPos / config.totalItems) * 100 : 0;
document.getElementById('progressBar').style.width = `${progressPercent.toFixed(2)}%`;
// 速度计算
const elapsedMinutes = Math.max((Date.now() - config.startTime) / 60000, 0.01);
const speed = config.processedItems / elapsedMinutes;
document.getElementById('speedText').textContent = `${speed.toFixed(1)} 条/分钟`;
// 预计剩余时间
const remainItems = config.totalItems - config.processedItems;
const remainMinutes = speed > 0 ? (remainItems / speed) : 0;
const remainText = remainMinutes > 60
? `${(remainMinutes/60).toFixed(2)} 小时`
: `${Math.ceil(remainMinutes)} 分钟`;
document.getElementById('timeRemaining').textContent = `预计剩余时间: ${remainText}`;
}
// ========== 查询用户列表 ==========
function checkUsers() {
if(!config.monitoring) return;
// 当前页面所有数据行
const rows = [...document.querySelectorAll('.el-table__row:not(.el-table__row--level)')];
if(rows.length === 0) {
console.log('当前页无数据,尝试翻页或结束');
if(config.currentPage < config.totalPages){
config.currentPage++;
jumpToPage(config.currentPage, () => {
setTimeout(() => {
updatePaginationInfo();
config.currentIndex = 0;
checkUsers();
}, 1500);
});
} else {
console.log('已处理完所有页,停止监控');
stopMonitoring();
}
return;
}
// 分批处理,控制并发
processBatch(rows, config.currentIndex);
}
// ========== 跳转到指定页 ==========
function jumpToPage(pageNum, callback) {
const pageBtns = [...document.querySelectorAll('.el-pagination .number')];
const targetBtn = pageBtns.find(btn => btn.textContent == pageNum);
if(targetBtn && !targetBtn.classList.contains('active')) {
safeClick(targetBtn, () => {
setTimeout(() => {
if(callback) callback();
}, 1500);
});
} else if(callback) {
callback();
}
}
// ========== 批量处理条目,控制最大并发 ==========
function processBatch(rows, startIndex) {
if(!config.monitoring) return;
// 计算本次处理数量,最多batchSize条
const batch = rows.slice(startIndex, startIndex + config.batchSize);
if(batch.length === 0) {
// 本页处理完,翻页
if(config.currentPage < config.totalPages){
config.currentPage++;
config.currentIndex = 0;
jumpToPage(config.currentPage, () => {
setTimeout(() => {
updatePaginationInfo();
checkUsers();
}, 1500);
});
} else {
stopMonitoring();
}
return;
}
// 并发请求限制
let i = 0;
function nextRequest() {
if(i >= batch.length) return;
if(config.activeRequests < config.maxParallel) {
const row = batch[i];
i++;
config.activeRequests++;
processRow(row)
.then(() => {
config.activeRequests--;
config.processedItems++;
config.currentIndex++;
updateProgressDisplay();
// 继续发起下一个请求
nextRequest();
})
.catch(err => {
console.error('处理用户异常:', err);
config.activeRequests--;
config.processedItems++;
config.currentIndex++;
updateProgressDisplay();
nextRequest();
});
// 继续尝试发起并发请求,直到达到maxParallel或无剩余
nextRequest();
} else {
// 达到最大并发,等待下一个请求完成后调用 nextRequest
}
}
nextRequest();
}
// ========== 处理单行数据 ==========
async function processRow(row) {
return new Promise(async (resolve) => {
try {
// 正确获取用户名
const userNameElement = row.querySelector('td.el-table_3_column_21 .el-tooltip');
const userName = userNameElement ? userNameElement.textContent.trim() : null;
if(!userName){
console.warn('该行找不到用户名元素');
resolve();
return;
}
userNameElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
const balance = await fetchUserBalance(userName);
if(balance !== null) {
console.log(`[${userName}] 余额: ${balance}`);
if(config.profitThreshold !== null && balance >= config.profitThreshold) {
const exceed = (balance - config.profitThreshold).toFixed(2);
await queueDialog({
username: userName,
type: 'profit',
balance,
message: `用户${userName}余额超标: ${balance} (超过${exceed})`
});
incrementAlertCount('profit');
} else if(config.lossThreshold !== null && balance <= config.lossThreshold) {
const below = (config.lossThreshold - balance).toFixed(2);
await queueDialog({
username: userName,
type: 'loss',
balance,
message: `用户${userName}余额不足: ${balance} (低于${below})`
});
incrementAlertCount('loss');
}
} else {
console.warn(`[${userName}] 余额读取失败`);
}
} catch(e) {
console.error('processRow 异常:', e);
} finally {
resolve();
}
});
}
// ========== fetchUserBalance 里的定位用户行也改 ==========
function fetchUserBalance(username) {
return new Promise((resolve) => {
try {
const rows = [...document.querySelectorAll('.el-table__row:not(.el-table__row--level)')];
const targetRow = rows.find(row => {
const userNameElement = row.querySelector('td.el-table_3_column_21 .el-tooltip');
const userName = userNameElement ? userNameElement.textContent.trim() : null;
return userName === username;
});
if(!targetRow) {
console.warn(`找不到用户名为${username}的行`);
resolve(null);
return;
}
let btn = targetRow.querySelector('button.btn-view-balance');
if(!btn) {
const buttons = targetRow.querySelectorAll('button,a');
btn = [...buttons].find(b => /余额|查看/.test(b.textContent));
}
if(!btn) {
console.warn(`找不到${username}对应的余额查看按钮`);
resolve(null);
return;
}
if(!simulateClick(btn)){
console.warn(`无法点击${username}余额按钮`);
resolve(null);
return;
}
const maxWaitTime = 3000;
const interval = 100;
let waited = 0;
const intervalId = setInterval(() => {
waited += interval;
const dialog = document.querySelector('.modal-balance, .balance-dialog, .balance-popup');
if(dialog){
const balanceEl = dialog.querySelector('.balance-amount, .amount, .balance-value');
if(balanceEl){
let balanceText = balanceEl.textContent.trim();
balanceText = balanceText.replace(/[^\d\.\-]/g, '');
clearInterval(intervalId);
const closeBtn = dialog.querySelector('button.btn-close, button.close, .close-btn, .dialog-close');
if(closeBtn){
simulateClick(closeBtn);
} else {
document.dispatchEvent(new KeyboardEvent('keydown', {key:'Escape'}));
}
resolve(parseFloat(balanceText));
}
}
if(waited >= maxWaitTime){
clearInterval(intervalId);
console.warn(`等待余额弹窗超时 - 用户:${username}`);
resolve(null);
}
}, interval);
} catch(e) {
console.error('fetchUserBalance 异常', e);
resolve(null);
}
});
}
// ========== 弹窗队列管理 ==========
function queueDialog(dialogInfo) {
config.dialogQueue.push(dialogInfo);
if(!config.isDialogProcessing) {
processDialogQueue();
}
}
// ========== 串行处理弹窗 ==========
function processDialogQueue() {
if(config.dialogQueue.length === 0) {
config.isDialogProcessing = false;
return;
}
config.isDialogProcessing = true;
const dialogInfo = config.dialogQueue.shift();
showDialog(dialogInfo).then(() => {
setTimeout(() => {
processDialogQueue();
}, 300); // 弹窗关闭间隔
});
}
// ========== 弹窗显示实现 ==========
function showDialog({username, type, balance, message}) {
return new Promise((resolve) => {
const dialogId = `monitorDialog_${++config.dialogCounter}`;
// 创建遮罩层
const mask = document.createElement('div');
mask.style.position = 'fixed';
mask.style.top = 0;
mask.style.left = 0;
mask.style.width = '100vw';
mask.style.height = '100vh';
mask.style.backgroundColor = 'rgba(0,0,0,0.3)';
mask.style.zIndex = 99999;
// 创建弹窗容器
const dialog = document.createElement('div');
dialog.style.position = 'fixed';
dialog.style.top = '50%';
dialog.style.left = '50%';
dialog.style.transform = 'translate(-50%, -50%)';
dialog.style.background = 'white';
dialog.style.borderRadius = '8px';
dialog.style.padding = '20px 30px';
dialog.style.boxShadow = '0 2px 15px rgba(0,0,0,0.3)';
dialog.style.zIndex = 100000;
dialog.style.minWidth = '320px';
dialog.style.fontFamily = 'Arial,sans-serif';
dialog.style.color = '#333';
const title = document.createElement('h3');
title.textContent = type === 'profit' ? '盈利警告' : '亏损警告';
title.style.marginTop = '0';
title.style.marginBottom = '15px';
title.style.color = type === 'profit' ? '#67C23A' : '#F56C6C';
const msg = document.createElement('p');
msg.textContent = message;
msg.style.marginBottom = '20px';
const closeBtn = document.createElement('button');
closeBtn.textContent = '确定';
closeBtn.style.padding = '8px 20px';
closeBtn.style.border = 'none';
closeBtn.style.backgroundColor = '#409EFF';
closeBtn.style.color = 'white';
closeBtn.style.borderRadius = '4px';
closeBtn.style.cursor = 'pointer';
closeBtn.addEventListener('click', () => {
document.body.removeChild(dialog);
document.body.removeChild(mask);
resolve();
});
dialog.appendChild(title);
dialog.appendChild(msg);
dialog.appendChild(closeBtn);
document.body.appendChild(mask);
document.body.appendChild(dialog);
});
}
// ========== 停止监控 ==========
function stopMonitoring() {
config.monitoring = false;
const btn = document.getElementById('toggleMonitor');
btn.textContent = '开始监控';
btn.classList.remove('stop');
document.getElementById('statusText').textContent = '已停止';
alert('监控已停止');
}
// ========== 主入口 ==========
function main() {
createControlPanel();
}
main();
})();