// ==UserScript== // @name MT菠菜数据分析 // @namespace http://tampermonkey.net/ // @version 2.4 // @description 等级分布表只显示前3行,其余折叠可点击展开 // @author YourName // @match https://kp.m-team.cc/* // @grant GM_addStyle // @grant GM_notification // @require https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js // ==/UserScript== (function() { 'use strict'; // ======================= 配置 ======================= const CONFIG = { CHART_COLORS: ['#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#E53935', '#8BC34A', '#3ad', '#d8a520', '#8B008B', '#B8860B'], DATA_XPATH: '/html/body/div[1]/div/div/div[3]/div/div/div[2]/div[2]/div[2]/div/div/div[2]/div/ul/div/div/div', PANEL_WIDTH: 450, AUTO_CHECK_INTERVAL: 1000, PANEL_OFFSET: 50, // 修改order字段即可更改等级排序(高到低,数字越大越高) USER_LEVELS: [ { color: 'rgb(0, 159, 0)', name: 'Vip', order: 9 }, { color: 'rgb(236, 56, 78)', name: '大臣/mTorrent Master', order: 8 }, { color: 'rgb(0, 100, 0)', name: '總督/Ultimate User', order: 7 }, { color: 'rgb(255, 140, 0)', name: '府尹/Extreme User', order: 6 }, { color: 'rgb(72, 61, 139)', name: '府丞/Veteran User', order: 5 }, { color: 'rgb(139, 0, 139)', name: '知州/Insane User', order: 4 }, { color: 'rgb(0, 191, 255)', name: '通判/Crazy User', order: 3 }, { color: 'rgb(0, 139, 139)', name: '知縣/Elite User', order: 2 }, { color: 'rgb(218, 165, 32)', name: '捕頭/Power User', order: 1 }, { color: 'rgb(51, 51, 51)', name: '小卒/User', order: 0 } ], VISIBLE_LEVELS: 3 // 前几名等级常显 }; // ======================= 样式 ======================= GM_addStyle(` .bet-stats-container { position: fixed; top: 50%; right: 20px; transform: translateY(-50%); width: ${CONFIG.PANEL_WIDTH}px; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 9999; font-family: 'Segoe UI', Arial, sans-serif; max-height: 85vh; overflow-y: auto; transition: all 0.3s ease; opacity: 0; visibility: hidden; } .bet-stats-container.visible { opacity: 1; visibility: visible; } .bet-stats-title { font-weight: 600; margin-bottom: 12px; font-size: 18px; color: #2c3e50; text-align: center; padding-bottom: 8px; border-bottom: 2px solid #f0f3f6; } .bet-stats-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; } .bet-stats-table th { background-color: #f8f9fa; padding: 8px 12px; border-bottom: 2px solid #e9ecef; color: #495057; } .bet-stats-table td { padding: 8px 12px; border-bottom: 1px solid #e9ecef; color: #6c757d; } .bet-stats-table tr:hover td { background-color: #f8f9fa; } .chart-container { margin: 16px 0; position: relative; height: 220px; } .bet-stats-summary { padding: 12px; background-color: #f8f9fa; border-radius: 6px; margin-top: 16px; font-size: 13px; } .highlight { color: #2c3e50; font-weight: 600; } .analyze-btn { position: fixed; top: 50%; right: 20px; transform: translateY(-50%); z-index: 10000; padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 25px; cursor: pointer; font-size: 14px; box-shadow: 0 3px 8px rgba(76,175,80,0.3); transition: all 0.3s ease; } .analyze-btn:hover { transform: translateY(-50%) translateY(-2px); box-shadow: 0 5px 12px rgba(76,175,80,0.4); } .analyze-btn:disabled { background: #bdc3c7; cursor: not-allowed; } .analyze-btn.hidden { opacity: 0; visibility: hidden; transform: translateY(-50%) translateX(20px); } .close-btn { position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; border-radius: 50%; background: #f8f9fa; border: none; color: #6c757d; cursor: pointer; transition: all 0.2s; } .close-btn:hover { background: #e9ecef; transform: rotate(90deg); } .top-bettors-panel { margin-top: 16px; border: 1px solid #e0e0e0; border-radius: 6px; overflow: hidden; } .top-bettors-header { padding: 10px 15px; background-color: #f8f9fa; cursor: pointer; font-weight: 600; display: flex; justify-content: space-between; align-items: center; } .top-bettors-header:hover { background-color: #e9ecef; } .top-bettors-content { padding: 0; max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; } .top-bettors-content.expanded { max-height: 1000px; padding: 10px; } .top-bettors-option { margin-bottom: 10px; } .top-bettors-option-title { font-weight: 600; margin-bottom: 5px; color: #2c3e50; } .top-bettors-list { list-style-type: none; padding-left: 15px; margin: 0; } .top-bettors-list li { margin-bottom: 3px; display: flex; justify-content: space-between; } .toggle-icon { transition: transform 0.2s; margin-left: 8px; } .toggle-icon.expanded { transform: rotate(180deg); } .color-sample { display: inline-block; width: 12px; height: 12px; border-radius: 2px; margin-right: 6px; vertical-align: middle; } .color-stats-table { margin-top: 16px; width: 100%; border-collapse: collapse; font-size: 13px; } .color-stats-table th, .color-stats-table td { padding: 8px 12px; border-bottom: 1px solid #e9ecef; } .color-stats-table th { background-color: #f8f9fa; text-align: left; } .color-stats-table tr:hover td { background-color: #f8f9fa; } .option-header { background-color: #e9ecef !important; font-weight: bold; } .level-name { white-space: nowrap; } /*新增,用户等级统计表折叠部分*/ .collapsed-extra-levels-row {display: none;} .collapsed-extra-levels-row.show {display: table-row;} .collapse-toggle-row { cursor: pointer; background-color: #fff7e0 !important; font-weight: 600; user-select: none; } .collapse-toggle-arrow { display: inline-block; width: 15px; margin-right: 6px; color: #f39c12; transition: transform 0.25s; } .collapse-toggle-arrow.opened { transform: rotate(90deg); } `); // ======================= 功能代码 ======================= const BetAnalyzer = { getUserLevel: (color) => { if (!color) return { name: '未知', order: -1 }; const level = CONFIG.USER_LEVELS.find(l => l.color.toLowerCase() === color.toLowerCase()); return level || { name: '未知', order: -1 }; }, parseData: () => { const result = document.evaluate(CONFIG.DATA_XPATH, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); return Array.from({length: result.snapshotLength}, (_, i) => { const node = result.snapshotItem(i); const text = node.textContent.trim(); if (text === '') return null; // 查找用户名的span元素 const userSpan = node.querySelector('span[style*="color"]'); let color = null; if (userSpan) { color = userSpan.style.color || null; } return { text, color }; }) .filter(item => item !== null) .reduce((acc, item, index) => { if (index % 4 === 0) acc.push([]); acc[acc.length-1].push(item); return acc; }, []) .map(([time, user, option, magic]) => ({ time: time.text, user: user.text, userColor: user.color, userLevel: BetAnalyzer.getUserLevel(user.color), option: option.text.replace(/^选项/, '').trim(), magic: parseInt(magic.text.replace(/,/g, ''), 10) || 0 })); }, calculateStats: (data) => { const stats = data.reduce((acc, bet) => { const opt = bet.option; if (!acc[opt]) { acc[opt] = { count: 0, total: 0, topBettors: [], levelStats: {} }; } acc[opt].count++; acc[opt].total += bet.magic; // 记录每个选项的投注者 acc[opt].topBettors.push({ user: bet.user, magic: bet.magic, color: bet.userColor, level: bet.userLevel }); // 记录每个选项的等级统计 const levelName = bet.userLevel.name; if (!acc[opt].levelStats[levelName]) { acc[opt].levelStats[levelName] = { count: 0, order: bet.userLevel.order }; } acc[opt].levelStats[levelName].count++; return acc; }, {}); // 对每个选项的投注者按魔力排序,取前三名 Object.keys(stats).forEach(opt => { stats[opt].topBettors.sort((a, b) => b.magic - a.magic); stats[opt].topBettors = stats[opt].topBettors.slice(0, 3); }); return { stats, totalBets: data.length, totalMagic: data.reduce((sum, bet) => sum + bet.magic, 0) }; } }; // ======================= UI代码 ======================= const UIComponents = { createChart: (container, stats, totalBets) => { const canvas = document.createElement('canvas'); container.appendChild(canvas); const ctx = canvas.getContext('2d'); new window.Chart(ctx, { type: 'doughnut', data: { labels: Object.keys(stats), datasets: [{ data: Object.values(stats).map(s => s.count), backgroundColor: CONFIG.CHART_COLORS, borderWidth: 0, hoverOffset: 8 }] }, options: { cutout: '60%', responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 16, font: { size: 12 } } }, tooltip: { backgroundColor: 'rgba(0,0,0,0.85)', bodyFont: { size: 12 }, callbacks: { label: (context) => { const value = context.parsed; const percentage = ((value / totalBets) * 100).toFixed(1); return `${context.label}: ${value}人 (${percentage}%)`; } } } } } }); }, createTopBettorsPanel: (stats) => { const panel = document.createElement('div'); panel.className = 'top-bettors-panel'; const header = document.createElement('div'); header.className = 'top-bettors-header'; header.innerHTML = ` 📊 各选项投注大佬 (点击展开) `; const content = document.createElement('div'); content.className = 'top-bettors-content'; Object.entries(stats).forEach(([opt, data]) => { if (data.topBettors.length > 0) { const optionDiv = document.createElement('div'); optionDiv.className = 'top-bettors-option'; const title = document.createElement('div'); title.className = 'top-bettors-option-title'; title.textContent = `选项 ${opt}`; const list = document.createElement('ul'); list.className = 'top-bettors-list'; data.topBettors.forEach(bettor => { const li = document.createElement('li'); const colorSpan = bettor.color ? `` : ''; li.innerHTML = `${colorSpan}${bettor.user} (${bettor.level.name.split('/')[0]}) ${bettor.magic.toLocaleString()} 魔力`; list.appendChild(li); }); optionDiv.appendChild(title); optionDiv.appendChild(list); content.appendChild(optionDiv); } }); header.addEventListener('click', () => { content.classList.toggle('expanded'); header.querySelector('.toggle-icon').classList.toggle('expanded'); }); panel.appendChild(header); panel.appendChild(content); return panel; }, createColorStatsTable: (stats) => { const table = document.createElement('table'); table.className = 'color-stats-table'; const thead = document.createElement('thead'); thead.innerHTML = ` 等级 ${Object.keys(stats).map(opt => `${opt}`).join('')} 总计 `; table.appendChild(thead); const tbody = document.createElement('tbody'); // 等级顺序按 order 字段高到低 const orderedLevels = [...CONFIG.USER_LEVELS].sort((a, b) => b.order - a.order); const visibleN = CONFIG.VISIBLE_LEVELS; // 分隔前五和折叠剩余 const visibleLevels = orderedLevels.slice(0, visibleN); const collapsedLevels = orderedLevels.slice(visibleN); // 前N名常显 visibleLevels.forEach(level => tbody.appendChild(makeLevelRow(level, stats))); // 如果有折叠 let collapseRows = []; if(collapsedLevels.length){ collapseRows = collapsedLevels.map(level=>{ const row = makeLevelRow(level, stats); row.classList.add('collapsed-extra-levels-row'); return row; }); // 折叠行按钮 const collapseTr = document.createElement('tr'); collapseTr.className = 'collapse-toggle-row'; const th = document.createElement('td'); th.colSpan = Object.keys(stats).length + 2; th.innerHTML = ` 展示更多等级`; collapseTr.appendChild(th); let opened = false; collapseTr.addEventListener('click', function(){ opened = !opened; for(const crow of collapseRows){ if(opened) crow.classList.add('show'); else crow.classList.remove('show'); } th.innerHTML = ` ${opened?'收起隐藏':'展示更多等级'}`; }); tbody.appendChild(collapseTr); collapseRows.forEach(r=>tbody.appendChild(r)); } // 总计行 const totalRow = document.createElement('tr'); totalRow.className = 'option-header'; totalRow.innerHTML = `总计`; let grandTotal = 0; Object.values(stats).forEach(optionData => { const optionTotal = Object.values(optionData.levelStats).reduce((sum, stat) => sum + stat.count, 0); grandTotal += optionTotal; totalRow.innerHTML += `${optionTotal}`; }); totalRow.innerHTML += `${grandTotal}`; tbody.appendChild(totalRow); table.appendChild(tbody); return table; //=== 组装一行等级分布 function makeLevelRow(level, stats){ const row = document.createElement('tr'); const levelCell = document.createElement('td'); levelCell.innerHTML = `${level.name.split('/')[0]}`; row.appendChild(levelCell); let totalCount = 0; Object.values(stats).forEach(optionData => { const count = optionData.levelStats[level.name]?.count || 0; totalCount += count; const cell = document.createElement('td'); cell.textContent = count > 0 ? count : '-'; row.appendChild(cell); }); const totalCell = document.createElement('td'); totalCell.textContent = totalCount > 0 ? totalCount : '-'; row.appendChild(totalCell); return row; } }, createPanel: (stats, total, btn) => { const panel = document.createElement('div'); panel.className = 'bet-stats-container'; const sortedByCount = Object.entries(stats).sort((a, b) => b[1].count - a[1].count); const mainOption = sortedByCount[0][0]; const mainOptionData = sortedByCount[0][1]; panel.innerHTML = `
📊 菠菜分析报告
${sortedByCount.map(([opt, data]) => ` `).join('')}
选项 人数 占比 总魔力 人均
${opt} ${data.count} ${((data.count / total.totalBets) * 100).toFixed(1)}% ${data.total.toLocaleString()} ${Math.round(data.total/data.count).toLocaleString()}
🏆 主力选项:${mainOption} (${mainOptionData.count}人, ${((mainOptionData.count / total.totalBets) * 100).toFixed(1)}%)
📈 总投注:${total.totalBets} 人 / ${total.totalMagic.toLocaleString()} 魔力
💰 平均魔力:${Math.round(total.totalMagic / total.totalBets).toLocaleString()}
`; // 图表 UIComponents.createChart(panel.querySelector('.chart-container'), stats, total.totalBets); // 等级统计 const colorStatsTitle = document.createElement('div'); colorStatsTitle.className = 'bet-stats-title'; colorStatsTitle.textContent = '🎨 用户等级分布 (按选项)'; panel.appendChild(colorStatsTitle); panel.appendChild(UIComponents.createColorStatsTable(stats)); // 大户面板 panel.appendChild(UIComponents.createTopBettorsPanel(stats)); // 关闭按钮 panel.querySelector('.close-btn').onclick = () => { panel.classList.remove('visible'); setTimeout(() => panel.remove(), 300); btn.classList.remove('hidden'); }; // 显示面板并隐藏按钮 document.body.appendChild(panel); setTimeout(() => { panel.classList.add('visible'); btn.classList.add('hidden'); }, 10); return panel; } }; // ======================= 主控制逻辑 ======================= class AppController { static async analyze() { const btn = document.getElementById('analyze-btn'); try { btn.disabled = true; btn.innerHTML = '⏳ 分析中...'; const rawData = BetAnalyzer.parseData(); if (rawData.length === 0) throw new Error('未找到有效投注数据'); const { stats, ...total } = BetAnalyzer.calculateStats(rawData); UIComponents.createPanel(stats, total, btn); GM_notification({ title: '✅ 分析完成', text: `成功处理 ${rawData.length} 条记录`, timeout: 2000 }); } catch (e) { console.error('[分析错误]', e); GM_notification({ title: '❌ 分析失败', text: e.message, timeout: 3000 }); } finally { btn.disabled = false; btn.innerHTML = '🔍 重新分析'; } } static init() { if (document.getElementById('analyze-btn')) return; const checkDataExists = () => { try { const result = document.evaluate(CONFIG.DATA_XPATH, document, null, XPathResult.ANY_TYPE, null); return result.iterateNext() !== null; } catch { return false; } }; const addButton = () => { const btn = document.createElement('button'); btn.id = 'analyze-btn'; btn.className = 'analyze-btn'; btn.textContent = '🔍 开始分析'; btn.onclick = AppController.analyze; document.body.appendChild(btn); }; if (checkDataExists()) { addButton(); return; } const observer = new MutationObserver(() => { if (checkDataExists()) { addButton(); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false }); const intervalId = setInterval(() => { if (checkDataExists()) { addButton(); clearInterval(intervalId); } }, CONFIG.AUTO_CHECK_INTERVAL); setTimeout(() => clearInterval(intervalId), 10000); } } // ======================= 启动 ======================= if (document.readyState === 'complete') { AppController.init(); } else { window.addEventListener('load', AppController.init); } })();