您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
统计GMGN任意代币前排地址的数据,让数字来说话!新增首次记录和涨跌提醒功能,所有数字可点击查看详情,弹框显示净流入数据,负数红色显示,点击外部关闭
// ==UserScript== // @name GMGN 前排统计 // @namespace http://tampermonkey.net/ // @version 4.9 // @description 统计GMGN任意代币前排地址的数据,让数字来说话!新增首次记录和涨跌提醒功能,所有数字可点击查看详情,弹框显示净流入数据,负数红色显示,点击外部关闭 // @match https://gmgn.ai/* // @match https://www.gmgn.ai/* // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @require https://code.jquery.com/jquery-3.6.0.min.js // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // 全局变量 - 跟踪下载按钮状态 let isDownloadInProgress = false; let currentCAAddress = ''; // 现代化提示框函数 function showModernToast(message, type = 'success', duration = 3000) { // 移除现有的提示框 const existingToast = document.querySelector('.modern-toast'); const existingOverlay = document.querySelector('.modern-toast-overlay'); if (existingToast) existingToast.remove(); if (existingOverlay) existingOverlay.remove(); // 创建遮罩层 const overlay = document.createElement('div'); overlay.className = 'modern-toast-overlay'; // 创建提示框 const toast = document.createElement('div'); toast.className = 'modern-toast'; // 根据类型设置图标 let icon, iconClass; switch (type) { case 'success': icon = '✓'; iconClass = 'success'; break; case 'error': icon = '✕'; iconClass = 'error'; break; case 'info': icon = 'ℹ'; iconClass = 'info'; break; default: icon = '✓'; iconClass = 'success'; } toast.innerHTML = ` <div class="modern-toast-content"> <div class="modern-toast-icon ${iconClass}">${icon}</div> <div class="modern-toast-text">${message}</div> <button class="modern-toast-close">×</button> </div> `; // 添加到页面 document.body.appendChild(overlay); document.body.appendChild(toast); // 关闭函数 const closeToast = () => { toast.style.animation = 'toastSlideOut 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards'; overlay.style.animation = 'overlayFadeIn 0.3s ease reverse'; setTimeout(() => { if (toast.parentNode) toast.remove(); if (overlay.parentNode) overlay.remove(); }, 300); }; // 绑定关闭事件 const closeBtn = toast.querySelector('.modern-toast-close'); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); closeToast(); }); // 点击遮罩层关闭 overlay.addEventListener('click', closeToast); // 点击提示框本身也可以关闭 toast.addEventListener('click', closeToast); // 自动关闭 if (duration > 0) { setTimeout(closeToast, duration); } // ESC键关闭 const escHandler = (e) => { if (e.key === 'Escape') { closeToast(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); // 返回关闭函数,允许手动关闭 return closeToast; } // 动态添加 CSS const style = document.createElement('style'); style.textContent = ` .statistic-gmgn-stats-container { background-color: transparent; border-radius: 4px; font-family: Arial, sans-serif; margin-right: 8px; margin-bottom:8px; border: 1px solid #333; /* 精细的右侧和下侧发光效果 */ box-shadow: 2px 2px 4px rgba(0, 119, 255, 0.6), /* 右下外发光(更小的偏移和模糊) */ 1px 1px 2px rgba(0, 119, 255, 0.4), /* 精细的次级发光 */ inset 0 0 3px rgba(0, 119, 255, 0.2); /* 更细腻的内发光 */ padding: 4px 6px; max-width: fit-content; } .statistic-gmgn-stats-header, .statistic-gmgn-stats-data { display: grid; grid-template-columns: repeat(12, 1fr); text-align: center; gap: 6px; font-weight: normal; font-size: 13px; } .statistic-gmgn-stats-header.sol-network, .statistic-gmgn-stats-data.sol-network { grid-template-columns: repeat(13, minmax(auto, 1fr)); gap: 4px; font-size: 12px; } .statistic-gmgn-stats-header span { color: #ccc; font-weight: normal; padding: 1px 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .statistic-gmgn-stats-header.sol-network span { font-size: 11px; padding: 1px; } .statistic-gmgn-stats-data span { color: #00ff00; font-weight: normal; cursor: default; transition: all 0.2s ease; padding: 1px 3px; border-radius: 2px; min-width: 0; white-space: nowrap; } .statistic-gmgn-stats-data span.clickable { cursor: pointer; } .statistic-gmgn-stats-data span.clickable:hover { background-color: rgba(0, 255, 0, 0.1); border-radius: 3px; transform: scale(1.03); } .statistic-gmgn-stats-data.sol-network span { padding: 1px 2px; font-size: 12px; } .statistic-gmgn-stats-data span .statistic-up-arrow, .statistic-up-arrow { color: green !important; margin-left: 2px; font-weight: bold; } .statistic-gmgn-stats-data span .statistic-down-arrow, .statistic-down-arrow { color: red !important; margin-left: 2px; font-weight: bold; } /* 完整弹框CSS样式 - 现代化设计 */ .statistic-gmgn-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); /* 简化为纯色,提升性能 */ /* backdrop-filter: blur(8px); */ /* 移除性能杀手 */ display: flex; align-items: center; justify-content: center; z-index: 1000; /* animation: modalFadeIn 0.3s ease-out; */ /* 移除动画,提升性能 */ } .statistic-gmgn-modal-content { background: #1e293b !important; /* 简化为纯色,提升性能 */ border-radius: 16px !important; width: 85% !important; max-width: 900px !important; max-height: 85vh !important; overflow-y: auto !important; padding: 24px !important; color: white !important; position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) scale(0.95) !important; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05), inset 0 1px 0 0 rgba(255, 255, 255, 0.1) !important; margin: 0 !important; z-index: 100000 !important; box-sizing: border-box !important; min-height: auto !important; min-width: 320px !important; pointer-events: auto !important; /* 移除动画,直接显示 */ backface-visibility: hidden !important; contain: layout style paint !important; /* 优化滚动性能 */ overflow-anchor: none !important; scroll-behavior: smooth !important; -webkit-overflow-scrolling: touch !important; } .statistic-gmgn-modal-header { display: flex !important; justify-content: space-between !important; align-items: center !important; margin-bottom: 24px !important; padding: 16px 20px !important; margin: -24px -24px 24px -24px !important; background: rgba(99, 102, 241, 0.1) !important; /* 简化为纯色,提升性能 */ border-radius: 16px 16px 0 0 !important; border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important; /* backdrop-filter: blur(10px) !important; */ /* 移除性能杀手 */ } .statistic-gmgn-modal-title { font-size: 20px !important; font-weight: 700 !important; color: white !important; margin: 0 !important; color: #ffffff !important; /* 简化文本渐变为纯色,提升性能 */ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important; } .statistic-gmgn-modal-close { background: rgba(148, 163, 184, 0.1) !important; border: 1px solid rgba(148, 163, 184, 0.2) !important; color: #94a3b8 !important; font-size: 18px !important; cursor: pointer !important; padding: 8px !important; line-height: 1 !important; width: 36px !important; height: 36px !important; border-radius: 50% !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: background-color 0.2s ease !important; /* 简化过渡,提升性能 */ } .statistic-gmgn-modal-close:hover { color: #fff !important; background: #ef4444 !important; /* 简化为纯色,提升性能 */ border-color: #ef4444 !important; /* transform: scale(1.1) !important; */ /* 移除复杂变换,提升性能 */ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4) !important; } .statistic-gmgn-result-item { background: rgba(51, 65, 85, 0.6); /* 简化为纯色,提升性能 */ border-radius: 12px; padding: 16px; margin-bottom: 16px; border: 1px solid rgba(255, 255, 255, 0.1); transition: background-color 0.2s ease; /* 简化过渡,提升性能 */ position: relative; overflow: hidden; /* 性能优化 - 硬件加速 */ will-change: transform, opacity; transform: translateZ(0); backface-visibility: hidden; contain: layout style; /* 减少backdrop-filter在大数据量时的性能消耗 */ } .statistic-gmgn-result-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, #3b82f6, #8b5cf6, #06b6d4); opacity: 0; transition: opacity 0.3s ease; transform: translateZ(0); } .statistic-gmgn-result-item:hover { background: rgba(51, 65, 85, 0.8); /* 简化为纯色,提升性能 */ transform: translateY(-2px) translateZ(0); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.1); border-color: rgba(59, 130, 246, 0.3); } .statistic-gmgn-result-item:hover::before { opacity: 1; } .statistic-gmgn-analysis-summary { margin-bottom: 24px; padding: 20px; background: linear-gradient(135deg, rgba(38, 50, 56, 0.6) 0%, rgba(30, 41, 59, 0.8) 100%); border-radius: 12px; display: flex; justify-content: space-between; align-items: center; border: 1px solid rgba(255, 255, 255, 0.1); /* backdrop-filter: blur(10px); */ /* 移除性能杀手 */ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1); } .statistic-gmgn-summary-stats { display: flex; gap: 32px; flex-wrap: wrap; } .statistic-gmgn-stat-item { display: flex; flex-direction: column; align-items: flex-start; padding: 8px 12px; background: rgba(255, 255, 255, 0.05); border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; min-width: 80px; } .statistic-gmgn-stat-item:hover { background: rgba(255, 255, 255, 0.1); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); } .statistic-gmgn-stat-label { color: #94a3b8; font-size: 12px; font-weight: 500; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; } .statistic-gmgn-stat-value { font-weight: 700; font-size: 18px; background: #3b82f6; /* 简化为纯色,提升性能 */ background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); } .statistic-gmgn-result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-wrap: wrap; gap: 8px; } .statistic-gmgn-result-rank { font-size: 14px; color: #94a3b8; font-weight: 600; min-width: 30px; } .statistic-gmgn-result-address { font-weight: 600; word-break: break-all; cursor: pointer; padding: 8px 12px; border-radius: 8px; transition: background-color 0.2s ease; /* 简化过渡,提升性能 */ background: linear-gradient(135deg, rgba(71, 85, 105, 0.6), rgba(51, 65, 85, 0.8)); border: 1px solid rgba(0, 255, 136, 0.3); flex: 1; min-width: 200px; color: #00ff88; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; position: relative; overflow: hidden; } .statistic-gmgn-result-address::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(0, 255, 136, 0.2), transparent); transition: left 0.5s ease; } .statistic-gmgn-result-address:hover { background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), rgba(51, 65, 85, 0.9)); border-color: #00ff88; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 255, 136, 0.3); } .statistic-gmgn-result-address:hover::before { left: 100%; } .statistic-gmgn-detail-section { margin-bottom: 12px; } .statistic-gmgn-section-title { font-size: 13px; font-weight: 600; color: #94a3b8; margin-bottom: 8px; display: flex; align-items: center; flex-wrap: wrap; } .statistic-gmgn-detail-grid { display: grid; grid-template-columns: 80px 1fr 80px 1fr; gap: 4px 8px; align-items: start; font-size: 12px; } .statistic-gmgn-detail-label { color: #94a3b8; font-size: 12px; padding: 2px 0; align-self: start; } .statistic-gmgn-detail-value { font-size: 12px; color: #e2e8f0; padding: 2px 0; word-break: break-word; line-height: 1.4; } .statistic-gmgn-value-highlight { color: #3b82f6; font-weight: 600; } .statistic-gmgn-compact-details .statistic-gmgn-detail-section { margin-bottom: 8px; } .statistic-gmgn-compact-details .statistic-gmgn-detail-section { margin-left: 10px; } .statistic-gmgn-address-jump-btn { background: #10b981; /* 简化为纯色,提升性能 */ color: white; padding: 6px 12px; border-radius: 8px; font-size: 11px; font-weight: 600; margin-left: 12px; cursor: pointer; transition: background-color 0.2s ease; /* 简化过渡,提升性能 */ text-decoration: none; display: inline-flex; align-items: center; gap: 4px; border: 1px solid rgba(16, 185, 129, 0.3); position: relative; overflow: hidden; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2); } .statistic-gmgn-address-jump-btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.4s ease; } .statistic-gmgn-address-jump-btn:hover { background: #059669; /* 简化为纯色,提升性能 */ transform: translateY(-2px) scale(1.05); box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4); border-color: #10b981; } .statistic-gmgn-address-jump-btn:hover::before { left: 100%; } .statistic-gmgn-address-jump-btn:active { transform: translateY(0) scale(1); } .statistic-gmgn-profit-positive { color: #00ff88 !important; } .statistic-gmgn-profit-negative { color: #ff4444 !important; } .statistic-gmgn-empty-message { text-align: center; color: #ccc; padding: 20px; margin: 0; } .statistic-gmgn-stats-info { text-align: center !important; margin-bottom: 15px !important; padding: 10px !important; background: rgba(0, 119, 255, 0.1) !important; border-radius: 8px !important; border: 1px solid rgba(0, 119, 255, 0.3) !important; color: #fff !important; font-size: 14px !important; } .statistic-gmgn-export-btn { background: linear-gradient(135deg, #10b981, #059669) !important; color: white !important; border: 1px solid rgba(16, 185, 129, 0.3) !important; padding: 12px 20px !important; border-radius: 12px !important; font-size: 13px !important; font-weight: 600 !important; cursor: pointer !important; transition: background-color 0.2s ease !important; /* 简化过渡,提升性能 */ display: flex !important; align-items: center !important; gap: 8px !important; position: relative !important; overflow: hidden !important; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2) !important; } .statistic-gmgn-export-btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.5s ease; } .statistic-gmgn-export-btn:hover { background: linear-gradient(135deg, #059669, #047857) !important; transform: translateY(-2px) !important; box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4) !important; border-color: #10b981 !important; } .statistic-gmgn-export-btn:hover::before { left: 100% !important; } .statistic-gmgn-export-btn:active { transform: translateY(0) !important; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3) !important; } /* 移除动画关键帧,直接显示弹出框 */ /* 响应式设计优化 */ @media (max-width: 768px) { .statistic-gmgn-modal-content { width: 95% !important; padding: 16px !important; margin: 10px !important; } .statistic-gmgn-modal-header { padding: 12px 16px !important; margin: -16px -16px 16px -16px !important; } .statistic-gmgn-summary-stats { gap: 16px; flex-wrap: wrap; } .statistic-gmgn-stat-item { min-width: 60px; padding: 6px 8px; } .statistic-gmgn-result-address { font-size: 11px; padding: 6px 8px; } } /* 自定义滚动条 */ .statistic-gmgn-modal-content::-webkit-scrollbar { width: 8px; } .statistic-gmgn-modal-content::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); border-radius: 4px; } .statistic-gmgn-modal-content::-webkit-scrollbar-thumb { background: #3b82f6; /* 简化为纯色,提升性能 */ border-radius: 4px; } .statistic-gmgn-modal-content::-webkit-scrollbar-thumb:hover { background: #2563eb; /* 简化为纯色,提升性能 */ } /* 加载状态动画 */ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .statistic-gmgn-loading { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } /* 分页控制样式 */ .statistic-gmgn-pagination-info { background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(139, 92, 246, 0.1)); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px; padding: 8px 12px; margin-bottom: 16px; text-align: center; } .statistic-pagination-text { color: #3b82f6; font-size: 12px; font-weight: 500; } .statistic-gmgn-pagination-controls { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; margin: 16px -24px -24px -24px; background: linear-gradient(135deg, rgba(30, 41, 59, 0.8), rgba(15, 23, 42, 0.9)); border-radius: 0 0 16px 16px; border-top: 1px solid rgba(255, 255, 255, 0.1); } .statistic-pagination-btn { background: #3b82f6; /* 简化为纯色,提升性能 */ color: white; border: none; padding: 8px 16px; border-radius: 8px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; transform: translateZ(0); } .statistic-pagination-btn:hover:not(:disabled) { background: #2563eb; /* 简化为纯色,提升性能 */ transform: translateY(-1px); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); } .statistic-pagination-btn:disabled { background: rgba(148, 163, 184, 0.3); color: rgba(148, 163, 184, 0.6); cursor: not-allowed; transform: none; } .statistic-pagination-current { color: #e2e8f0; font-size: 13px; font-weight: 500; } /* 可疑地址类型标识样式 */ .statistic-suspicious-labels { display: inline-flex; gap: 6px; flex-wrap: wrap; margin-left: 12px; align-items: center; } .statistic-suspicious-label { font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 10px; border: 1px solid; white-space: nowrap; display: inline-flex; align-items: center; gap: 3px; text-transform: uppercase; letter-spacing: 0.3px; transition: all 0.2s ease; cursor: default; } .statistic-suspicious-label:hover { transform: scale(1.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .statistic-suspicious-label::before { content: '⚠'; font-size: 8px; } .statistic-suspicious-label.rat-trader::before { content: '🐭'; } .statistic-suspicious-label.transfer-in::before { content: '⬇'; } .statistic-suspicious-label.bundler::before { content: '📦'; } /* 现代化详情数据样式 */ .statistic-detail-grid-modern { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; padding: 8px 0; } .statistic-detail-item { display: flex; align-items: center; background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 8px 10px; transition: all 0.3s ease; position: relative; overflow: hidden; min-height: 48px; } .statistic-detail-item::before { content: ''; position: absolute; top: 0; left: 0; width: 3px; height: 100%; background: linear-gradient(180deg, #3b82f6, #8b5cf6); opacity: 0; transition: opacity 0.3s ease; } .statistic-detail-item:hover { background: rgba(255, 255, 255, 0.05); border-color: rgba(59, 130, 246, 0.3); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .statistic-detail-item:hover::before { opacity: 1; } .statistic-detail-highlight { background: rgba(59, 130, 246, 0.08) !important; border-color: rgba(59, 130, 246, 0.2) !important; } .statistic-detail-highlight::before { opacity: 1 !important; } .statistic-detail-icon { font-size: 16px; margin-right: 8px; min-width: 20px; text-align: center; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); } .statistic-detail-content { flex: 1; min-width: 0; } .statistic-detail-label { font-size: 10px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; margin-bottom: 2px; line-height: 1; } .statistic-detail-value { font-size: 13px; color: #e2e8f0; font-weight: 600; line-height: 1.2; word-break: break-all; } .statistic-detail-value.profit-positive { color: #10b981; } .statistic-detail-value.profit-negative { color: #ef4444; } .statistic-detail-value.highlight { color: #60a5fa; } .statistic-detail-value.warning { color: #f59e0b; } /* 下载按钮样式 - 与其他数字保持一致 */ .statistic-download-btn { color:rgb(243, 243, 243) !important; font-weight: normal !important; cursor: pointer !important; /* 继承其他数字的基础样式 */ } .statistic-download-btn:hover { background-color: rgba(0, 255, 0, 0.1) !important; border-radius: 3px !important; transform: scale(1.03) !important; } .statistic-download-btn.disabled { color: rgba(135, 135, 135, 0.73) !important; cursor: not-allowed !important; pointer-events: none !important; } .statistic-download-btn.disabled:hover { background-color: transparent !important; transform: none !important; } /* 图片预览模态框样式 */ .image-preview-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 10000; /* backdrop-filter: blur(5px); */ /* 移除性能杀手 */ } .image-preview-content { background: #1a1a1a; border-radius: 12px; padding: 20px; max-width: 90vw; max-height: 90vh; display: flex; flex-direction: column; align-items: center; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); } .image-preview-header { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-bottom: 15px; } .image-preview-title { color: #ffffff; font-size: 18px; font-weight: bold; } .image-preview-close { background: none; border: none; color: #ffffff; font-size: 24px; cursor: pointer; padding: 5px; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; transition: background 0.2s ease; } .image-preview-close:hover { background: rgba(255, 255, 255, 0.1); } .image-preview-img { max-width: 100%; max-height: 60vh; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .image-preview-buttons { display: flex; gap: 12px; } .image-preview-btn { padding: 10px 20px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: all 0.2s ease; color: #ffffff; } .image-preview-btn.copy-btn { background: #10b981; /* 简化为纯色,提升性能 */ } .image-preview-btn.copy-btn:hover { background: #059669; /* 简化为纯色,提升性能 */ transform: translateY(-1px); } .image-preview-btn.download-btn { background: linear-gradient(135deg, #3b82f6, #1d4ed8); } .image-preview-btn.download-btn:hover { background: linear-gradient(135deg, #1d4ed8, #1e40af); transform: translateY(-1px); } /* 现代化提示框样式 */ .modern-toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 20000; background: rgba(0, 0, 0, 0.9); /* backdrop-filter: blur(10px); */ /* 移除性能杀手 */ border-radius: 16px; padding: 0; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1); min-width: 300px; max-width: 400px; /* 移除toast动画,直接显示 */ cursor: pointer; } .modern-toast-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 19999; background: rgba(0, 0, 0, 0.3); /* backdrop-filter: blur(3px); */ /* 移除性能杀手 */ /* animation: overlayFadeIn 0.3s ease forwards; */ /* 移除动画,提升性能 */ } .modern-toast-content { display: flex; align-items: center; padding: 20px 24px; gap: 16px; } .modern-toast-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; flex-shrink: 0; } .modern-toast-icon.success { background: #10b981; /* 简化为纯色,提升性能 */ color: #ffffff; } .modern-toast-icon.error { background: linear-gradient(135deg, #ef4444, #dc2626); color: #ffffff; } .modern-toast-icon.info { background: linear-gradient(135deg, #3b82f6, #1d4ed8); color: #ffffff; } .modern-toast-text { flex: 1; color: #ffffff; font-size: 16px; font-weight: 500; line-height: 1.4; } .modern-toast-close { width: 32px; height: 32px; border-radius: 50%; background: rgba(255, 255, 255, 0.1); border: none; color: #ffffff; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; transition: all 0.2s ease; flex-shrink: 0; } .modern-toast-close:hover { background: rgba(255, 255, 255, 0.2); transform: scale(1.1); } /* 移除所有toast动画关键帧,提升性能 */ `; document.head.appendChild(style); // 存储拦截到的数据 let interceptedData = null; // 存储首次加载的数据 let initialStats = null; // 标记是否是首次加载 let isFirstLoad = true; // 新增存储当前CA地址 let currentCaAddress = null; // 存储首次加载的CA地址 let initialCaAddress = null; // 性能优化:添加缓存机制 let dataCache = { lastDataHash: null, calculatedStats: null, filteredResults: new Map(), eventsInitialized: false }; // 防抖函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 检查当前网络是否为SOL function isSolNetwork() { const url = window.location.href; return url.includes('/sol/') || url.includes('gmgn.ai/sol'); } // 获取可疑地址的具体类型标识 function getSuspiciousTypeLabels(holder) { const labels = []; // 基础可疑标记 if (holder.is_suspicious) { labels.push({ text: '可疑', color: '#dc2626', bgColor: 'rgba(220, 38, 38, 0.15)', borderColor: 'rgba(220, 38, 38, 0.3)' }); } // 检查maker_token_tags if (holder.maker_token_tags) { if (holder.maker_token_tags.includes('rat_trader')) { labels.push({ text: '老鼠仓', color: '#ef4444', bgColor: 'rgba(239, 68, 68, 0.15)', borderColor: 'rgba(239, 68, 68, 0.3)' }); } if (holder.transfer_in) { labels.push({ text: '小鱼钱包', color: '#f87171', bgColor: 'rgba(248, 113, 113, 0.15)', borderColor: 'rgba(248, 113, 113, 0.3)' }); } if (holder.maker_token_tags.includes('bundler')) { labels.push({ text: '捆绑交易', color: '#b91c1c', bgColor: 'rgba(185, 28, 28, 0.15)', borderColor: 'rgba(185, 28, 28, 0.3)' }); } } return labels; } // 生成现代化详情数据HTML function generateDetailItemHTML(icon, label, value, valueClass = '', isHighlight = false) { const highlightClass = isHighlight ? 'statistic-detail-highlight' : ''; return ` <div class="statistic-detail-item ${highlightClass}"> <div class="statistic-detail-icon">${icon}</div> <div class="statistic-detail-content"> <div class="statistic-detail-label">${label}</div> <div class="statistic-detail-value ${valueClass}">${value}</div> </div> </div> `; } // 生成可疑标识HTML function generateSuspiciousLabelsHTML(labels) { if (!labels || labels.length === 0) { return ''; } const labelsHTML = labels.map(label => { const typeClass = label.text === '老鼠仓' ? 'rat-trader' : label.text === '小鱼钱包' ? 'transfer-in' : label.text === '捆绑交易' ? 'bundler' : ''; return `<span class="statistic-suspicious-label ${typeClass}" style="color: ${label.color}; background: ${label.bgColor}; border-color: ${label.borderColor};" title="${label.text}标识"> ${label.text} </span>`; }).join(''); return `<div class="statistic-suspicious-labels">${labelsHTML}</div>`; } // 检查是否为交易所地址 function isExchangeAddress(holder) { const exchangeNames = ['coinbase', 'binance', 'bybit', 'bitget', 'okx', 'kraken', 'coinsquare', 'crypto.com', 'robinhood', 'mexc']; // 检查native_transfer中的name if (holder.native_transfer && holder.native_transfer.name) { const name = holder.native_transfer.name.toLowerCase(); if (exchangeNames.some(exchange => name.includes(exchange))) { return true; } } // 检查其他可能的transfer字段 if (holder.transfer && holder.transfer.name) { const name = holder.transfer.name.toLowerCase(); if (exchangeNames.some(exchange => name.includes(exchange))) { return true; } } return false; } // 获取交易所名称 function getExchangeName(holder) { const exchangeNames = ['coinbase', 'binance', 'bybit', 'bitget', 'okx', 'kraken', 'coinsquare', 'crypto.com', 'robinhood', 'mexc']; let sourceName = ''; if (holder.native_transfer && holder.native_transfer.name) { sourceName = holder.native_transfer.name.toLowerCase(); } else if (holder.transfer && holder.transfer.name) { sourceName = holder.transfer.name.toLowerCase(); } for (let exchange of exchangeNames) { if (sourceName.includes(exchange)) { return exchange.charAt(0).toUpperCase() + exchange.slice(1); } } return 'Unknown'; } // 交易所专用弹框 function createExchangeModal(data, caAddress) { // 移除已存在的弹框 const existingModal = document.querySelector('.statistic-gmgn-modal'); if (existingModal) { existingModal.remove(); } // 按交易所分组数据 const exchangeGroups = {}; data.forEach(holder => { const exchangeName = getExchangeName(holder); if (!exchangeGroups[exchangeName]) { exchangeGroups[exchangeName] = []; } exchangeGroups[exchangeName].push(holder); }); // 计算已卖筹码地址数 const soldAddressCount = data.filter(holder => (holder.sell_amount_percentage || 0) > 0).length; // 计算总持仓占比 const totalHoldingPercentage = data.reduce((sum, holder) => { return sum + (holder.amount_percentage || 0); }, 0); // 创建弹框 const modal = document.createElement('div'); modal.className = 'statistic-gmgn-modal'; // 生成交易所统计数据 const exchangeSummary = Object.keys(exchangeGroups).map(exchange => { return { name: exchange, count: exchangeGroups[exchange].length, addresses: exchangeGroups[exchange] }; }).sort((a, b) => b.count - a.count); modal.innerHTML = ` <div class="statistic-gmgn-modal-content"> <div class="statistic-gmgn-modal-header"> <div class="statistic-gmgn-modal-title">🚀 交易所地址分析 (共${data.length}个地址)</div> <button class="statistic-gmgn-modal-close">×</button> </div> <div class="statistic-gmgn-analysis-summary"> <div class="statistic-gmgn-summary-stats"> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">已卖筹码地址数:</span> <span class="statistic-gmgn-stat-value">${soldAddressCount}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">交易所数:</span> <span class="statistic-gmgn-stat-value">${Object.keys(exchangeGroups).length}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">总持仓占比:</span> <span class="statistic-gmgn-stat-value">${(totalHoldingPercentage * 100).toFixed(2)}%</span> </div> </div> <button id="statistic-export-exchange-btn" class="statistic-gmgn-export-btn" title="导出Excel">导出Excel</button> </div> <div id="statistic-exchange-summary"> <div class="statistic-gmgn-section-title">📱 交易所统计</div> <div class="statistic-exchange-summary-grid"> ${exchangeSummary.map(item => ` <div class="statistic-exchange-summary-item" data-exchange="${item.name}"> <span class="statistic-exchange-name">${item.name}</span> <span class="statistic-exchange-count">${item.count}个地址</span> </div> `).join('')} </div> </div> <div id="statistic-exchange-details"></div> </div> `; document.body.appendChild(modal); // 添加交易所统计样式 if (!document.getElementById('exchange-summary-styles')) { const summaryStyles = document.createElement('style'); summaryStyles.id = 'exchange-summary-styles'; summaryStyles.textContent = ` .statistic-exchange-summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; } .statistic-exchange-summary-item { background-color: #475569; border-radius: 8px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; } .statistic-exchange-summary-item:hover { background-color: #64748b; border-color: #3b82f6; transform: translateY(-2px); } .statistic-exchange-summary-item.active { background-color: #3b82f6; border-color: #1d4ed8; } .statistic-exchange-name { font-weight: 600; color: #e2e8f0; font-size: 14px; } .statistic-exchange-count { color: #10b981; font-weight: 600; font-size: 13px; } .statistic-exchange-details-section { margin-bottom: 20px; } .statistic-exchange-section-header { background-color: #1e293b; padding: 12px 16px; border-radius: 8px 8px 0 0; border-left: 4px solid #3b82f6; margin-bottom: 0; } .statistic-exchange-section-title { font-size: 16px; font-weight: 600; color: #3b82f6; margin: 0; } .statistic-exchange-section-count { font-size: 12px; color: #94a3b8; margin-top: 4px; } `; document.head.appendChild(summaryStyles); } // 绑定交易所统计点击事件 exchangeSummary.forEach(item => { const summaryItem = modal.querySelector(`[data-exchange="${item.name}"]`); if (summaryItem) { summaryItem.addEventListener('click', () => { // 移除所有活跃状态 modal.querySelectorAll('.statistic-exchange-summary-item').forEach(el => { el.classList.remove('active'); }); // 添加当前活跃状态 summaryItem.classList.add('active'); // 显示该交易所的详细信息 displayExchangeDetails(item.addresses, item.name, modal); }); } }); // ESC键关闭处理函数 const escKeyHandler = (e) => { if (e.key === 'Escape') { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }; document.addEventListener('keydown', escKeyHandler); // 绑定导出Excel按钮事件 const exportBtn = modal.querySelector('#statistic-export-exchange-btn'); if (exportBtn) { exportBtn.addEventListener('click', () => { exportExchangeToExcel(exchangeGroups, caAddress); }); } // 绑定关闭按钮事件 modal.querySelector('.statistic-gmgn-modal-close').addEventListener('click', () => { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); }); // 点击模态框外部关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }); // 默认显示第一个交易所的详情 if (exchangeSummary.length > 0) { const firstItem = modal.querySelector(`[data-exchange="${exchangeSummary[0].name}"]`); if (firstItem) { firstItem.click(); } } } // 显示交易所详细信息 function displayExchangeDetails(addresses, exchangeName, modal) { const detailsContainer = modal.querySelector('#statistic-exchange-details'); // 创建全局排名映射 - 基于原始完整数据按持仓比例排序 const globalRankMap = new Map(); if (interceptedData?.data?.list) { const allHolders = [...interceptedData.data.list]; allHolders .sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)) .forEach((holder, index) => { globalRankMap.set(holder.address, index + 1); }); } // 按持仓比例排序 const sortedAddresses = addresses.sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)); detailsContainer.innerHTML = ` <div class="statistic-exchange-details-section"> <div class="statistic-exchange-section-header"> <div class="statistic-exchange-section-title">${exchangeName} 地址详情</div> <div class="statistic-exchange-section-count">共 ${sortedAddresses.length} 个地址</div> </div> ${sortedAddresses.map((holder, index) => { const globalRank = globalRankMap.get(holder.address) || (index + 1); const processedData = { rank: index + 1, rankIndex: globalRank, // 使用全局排名 address: holder.address, balance: formatNumber(holder.balance), usdValue: formatNumber(holder.usd_value), netflowUsd: formatNumber(holder.netflow_usd), netflowClass: (holder.netflow_usd || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profit: formatNumber(holder.profit), profitSign: holder.profit >= 0 ? '+' : '', profitClass: holder.profit >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profitChange: holder.profit_change ? (holder.profit_change * 100).toFixed(1) + '%' : 'N/A', profitChangeClass: (holder.profit_change || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', exchangeName: getExchangeName(holder), transferName: (holder.native_transfer && holder.native_transfer.name) || (holder.transfer && holder.transfer.name) || 'N/A', amountPercentage: holder.amount_percentage ? (holder.amount_percentage * 100).toFixed(2) + '%' : 'N/A', sellPercentage: holder.sell_amount_percentage ? (holder.sell_amount_percentage * 100).toFixed(2) + '%' : '0.00%' // 筹码已卖 }; return ` <div class="statistic-gmgn-result-item"> <div class="statistic-gmgn-result-header"> <div class="statistic-gmgn-result-rank"> <span style="color: #ff6b35; font-weight: bold; background: rgba(255, 107, 53, 0.15); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 107, 53, 0.3); font-size: 12px;">榜${processedData.rankIndex}</span> </div> <div class="statistic-gmgn-result-address" title="点击复制地址" onclick="navigator.clipboard.writeText('${processedData.address}'); this.style.backgroundColor='#16a34a'; this.style.color='white'; setTimeout(() => { this.style.backgroundColor=''; this.style.color=''; }, 1000);">${processedData.address}</div> <a href="https://gmgn.ai/sol/address/${processedData.address}" target="_blank" class="statistic-gmgn-address-jump-btn" title="查看钱包详情">详情</a> </div> <div class="statistic-gmgn-compact-details"> <div class="statistic-gmgn-detail-section"> <div class="statistic-gmgn-section-title">基本信息</div> <div class="statistic-detail-grid-modern"> ${generateDetailItemHTML('💎', '持仓', processedData.balance)} ${generateDetailItemHTML('✨', '持仓占比', processedData.amountPercentage, 'highlight', true)} ${generateDetailItemHTML('📉', '筹码已卖', processedData.sellPercentage, processedData.sellPercentage === '0.00%' ? 'profit-positive' : 'warning')} ${generateDetailItemHTML('💰', '净流入', '$' + processedData.netflowUsd, processedData.netflowClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('📈', '盈亏', processedData.profitSign + '$' + processedData.profit, processedData.profitClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('🚀', '倍数', processedData.profitChange, processedData.profitChangeClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('🏢', '交易所', processedData.exchangeName, 'highlight', true)} ${generateDetailItemHTML('🏷️', '标签', processedData.transferName)} </div> </div> </div> </div> `; }).join('')} </div> `; } // 交易所数据导出函数 function exportExchangeToExcel(exchangeGroups, caAddress) { try { const worksheetData = []; // 添加标题行 worksheetData.push(['交易所', '排名', '地址', '持仓数量', '持仓比例', '筹码已卖', 'USD价值', '净流入USD', '盈亏USD', '盈亏倍数', '标签名称']); // 按交易所排序添加数据 Object.keys(exchangeGroups).forEach(exchangeName => { const addresses = exchangeGroups[exchangeName].sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)); addresses.forEach((holder, index) => { const row = [ exchangeName, index + 1, holder.address, formatNumber(holder.balance), holder.amount_percentage ? (holder.amount_percentage * 100).toFixed(2) + '%' : 'N/A', holder.sell_amount_percentage ? (holder.sell_amount_percentage * 100).toFixed(2) + '%' : '0.00%', formatNumber(holder.usd_value), formatNumber(holder.netflow_usd), (holder.profit >= 0 ? '+' : '') + formatNumber(holder.profit), holder.profit_change ? (holder.profit_change * 100).toFixed(1) + '%' : 'N/A', (holder.native_transfer && holder.native_transfer.name) || (holder.transfer && holder.transfer.name) || 'N/A' ]; worksheetData.push(row); }); }); // 创建工作簿 const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet(worksheetData); // 设置列宽 const colWidths = [ {wch: 12}, // 交易所 {wch: 6}, // 排名 {wch: 45}, // 地址 {wch: 15}, // 持仓数量 {wch: 10}, // 持仓比例 {wch: 10}, // 已卖比例 {wch: 15}, // USD价值 {wch: 15}, // 净流入 {wch: 15}, // 盈亏 {wch: 12}, // 倍数 {wch: 25} // 标签名称 ]; ws['!cols'] = colWidths; // 添加工作表到工作簿 XLSX.utils.book_append_sheet(wb, ws, '交易所地址'); // 生成文件名 const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const fileName = `交易所地址_${caAddress ? caAddress.slice(0, 8) : 'data'}_${timestamp}.xlsx`; // 下载文件 XLSX.writeFile(wb, fileName); // 显示成功提示 const exportBtn = document.querySelector('#statistic-export-exchange-btn'); if (exportBtn) { const originalText = exportBtn.textContent; exportBtn.textContent = '✅ 导出成功'; exportBtn.style.backgroundColor = '#059669'; setTimeout(() => { exportBtn.textContent = originalText; exportBtn.style.backgroundColor = ''; }, 2000); } } catch (error) { console.error('Excel导出失败:', error); showModernToast('导出失败,请检查浏览器控制台了解详情', 'error'); } } // 优化后的弹框管理函数 - 添加分页支持 function createModal(title, data, caAddress, showSolBalance = false) { // 移除已存在的弹框 const existingModal = document.querySelector('.statistic-gmgn-modal'); if (existingModal) { existingModal.remove(); } // 性能优化:数据量限制 const ITEMS_PER_PAGE = 50; const isLargeDataset = data.length > ITEMS_PER_PAGE; let currentPage = 1; let totalPages = Math.ceil(data.length / ITEMS_PER_PAGE); // 1. 数据预处理 - 首先获取全局排名 if (!interceptedData?.data?.list) { console.error('无法获取原始数据进行全局排名'); return; } // 创建全局排名映射 - 基于原始完整数据按持仓比例排序 const globalRankMap = new Map(); const allHolders = [...interceptedData.data.list]; allHolders .sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)) .forEach((holder, index) => { globalRankMap.set(holder.address, index + 1); }); // 2. 计算已卖筹码地址数 const soldAddressCount = data.filter(holder => (holder.sell_amount_percentage || 0) > 0).length; // 计算总持仓占比 const totalHoldingPercentage = data.reduce((sum, holder) => { return sum + (holder.amount_percentage || 0); }, 0); // 3. 处理所有数据并排序 const allProcessedData = data .sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)) // 按持仓比例排序 .map((holder, index) => { const globalRank = globalRankMap.get(holder.address) || (index + 1); const baseData = { rank: index + 1, // 在当前数据集中的排名(用于显示序号) rankIndex: globalRank, // 在全局数据中的排名(用于显示"榜X") address: holder.address, balance: formatNumber(holder.balance), usdValue: formatNumber(holder.usd_value), netflowUsd: formatNumber(holder.netflow_usd), netflowClass: (holder.netflow_usd || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profit: formatNumber(holder.profit), profitSign: holder.profit >= 0 ? '+' : '', profitClass: holder.profit >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profitChange: holder.profit_change ? (holder.profit_change * 100).toFixed(1) + '%' : 'N/A', profitChangeClass: (holder.profit_change || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', amountPercentage: holder.amount_percentage ? (holder.amount_percentage * 100).toFixed(2) + '%' : 'N/A', sellPercentage: holder.sell_amount_percentage ? (holder.sell_amount_percentage * 100).toFixed(2) + '%' : '0.00%', // 筹码已卖 // 添加可疑类型标识 suspiciousLabels: getSuspiciousTypeLabels(holder), // 保留原始数据用于检测 originalHolder: holder }; // 只有在需要显示SOL余额时才添加 if (showSolBalance) { baseData.solBalance = holder.native_balance ? ((holder.native_balance / 1000000000).toFixed(2) + ' SOL') : 'N/A'; } return baseData; }); // 分页处理:获取当前页数据 function getCurrentPageData(page = 1) { const start = (page - 1) * ITEMS_PER_PAGE; const end = start + ITEMS_PER_PAGE; return allProcessedData.slice(start, end); } const processedData = getCurrentPageData(currentPage); // 2. 创建弹框基础结构 - 使用token_holding_temp.js的DOM结构 const modal = document.createElement('div'); modal.className = 'statistic-gmgn-modal'; modal.innerHTML = ` <div class="statistic-gmgn-modal-content"> <div class="statistic-gmgn-modal-header"> <div class="statistic-gmgn-modal-title">💎 ${title} (${allProcessedData.length}个地址)</div> <button class="statistic-gmgn-modal-close">×</button> </div> ${isLargeDataset ? ` <div class="statistic-gmgn-pagination-info"> <span class="statistic-pagination-text">⚡ 性能优化:分页显示 | 第${currentPage}页,共${totalPages}页 | 每页${ITEMS_PER_PAGE}条</span> </div> ` : ''} <div class="statistic-gmgn-analysis-summary"> <div class="statistic-gmgn-summary-stats"> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">已卖筹码地址数:</span> <span class="statistic-gmgn-stat-value">${soldAddressCount}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">总数量:</span> <span class="statistic-gmgn-stat-value">${allProcessedData.length}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">总持仓占比:</span> <span class="statistic-gmgn-stat-value">${(totalHoldingPercentage * 100).toFixed(2)}%</span> </div> </div> <button id="statistic-export-excel-btn" class="statistic-gmgn-export-btn" title="导出Excel">导出Excel</button> </div> <div id="statistic-gmgn-results-list"></div> ${isLargeDataset ? ` <div class="statistic-gmgn-pagination-controls"> <button id="statistic-prev-page" class="statistic-pagination-btn" ${currentPage === 1 ? 'disabled' : ''}>← 上一页</button> <span class="statistic-pagination-current">第 ${currentPage} 页 / 共 ${totalPages} 页</span> <button id="statistic-next-page" class="statistic-pagination-btn" ${currentPage === totalPages ? 'disabled' : ''}>下一页 →</button> </div> ` : ''} </div> `; // 3. 插入DOM document.body.appendChild(modal); // 4. 填充结果列表 - 参考token_holding_temp.js的方式 const resultsList = document.getElementById('statistic-gmgn-results-list'); processedData.forEach((holder, index) => { const item = document.createElement('div'); item.className = 'statistic-gmgn-result-item'; item.innerHTML = ` <div class="statistic-gmgn-result-header"> <div class="statistic-gmgn-result-rank"> <span style="color: #ff6b35; font-weight: bold; background: rgba(255, 107, 53, 0.15); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 107, 53, 0.3); font-size: 12px;">榜${holder.rankIndex}</span> </div> <div class="statistic-gmgn-result-address" title="点击复制地址">${holder.address}</div> <a href="https://gmgn.ai/sol/address/${holder.address}" target="_blank" class="statistic-gmgn-address-jump-btn" title="查看钱包详情">详情</a> </div> <div class="statistic-gmgn-compact-details"> <div class="statistic-gmgn-detail-section"> <div class="statistic-gmgn-section-title"> 基本信息 ${generateSuspiciousLabelsHTML(holder.suspiciousLabels)} </div> <div class="statistic-detail-grid-modern"> ${generateDetailItemHTML('💎', '持仓', holder.balance)} ${generateDetailItemHTML('✨', '持仓占比', holder.amountPercentage, 'highlight', true)} ${generateDetailItemHTML('📉', '筹码已卖', holder.sellPercentage, holder.sellPercentage === '0.00%' ? 'profit-positive' : 'warning')} ${generateDetailItemHTML('💰', '净流入', '$' + holder.netflowUsd, holder.netflowClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('📈', '盈亏', holder.profitSign + '$' + holder.profit, holder.profitClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('🚀', '倍数', holder.profitChange, holder.profitChangeClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${holder.solBalance ? generateDetailItemHTML('⭐', 'SOL餘額', holder.solBalance, 'highlight') : ''} </div> </div> </div> `; // 添加地址复制功能 const addressElement = item.querySelector('.statistic-gmgn-result-address'); addressElement.addEventListener('click', () => { navigator.clipboard.writeText(holder.address).then(() => { addressElement.style.backgroundColor = '#16a34a'; addressElement.style.color = 'white'; setTimeout(() => { addressElement.style.backgroundColor = ''; addressElement.style.color = ''; }, 1000); }); }); resultsList.appendChild(item); }); // ESC键关闭处理函数 const escKeyHandler = (e) => { if (e.key === 'Escape') { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }; document.addEventListener('keydown', escKeyHandler); // 5. 绑定导出Excel按钮事件 - 导出完整数据而非分页数据 const exportBtn = modal.querySelector('#statistic-export-excel-btn'); if (exportBtn) { exportBtn.addEventListener('click', () => { exportToExcel(allProcessedData, title, caAddress, showSolBalance); }); } // 6. 分页控制逻辑 if (isLargeDataset) { // 渲染指定页面的数据 function renderPage(page) { currentPage = page; const currentPageData = getCurrentPageData(page); // 清空当前列表 const resultsList = document.getElementById('statistic-gmgn-results-list'); resultsList.innerHTML = ''; // 重新渲染当前页数据 currentPageData.forEach((holder, index) => { const item = document.createElement('div'); item.className = 'statistic-gmgn-result-item'; item.innerHTML = ` <div class="statistic-gmgn-result-header"> <div class="statistic-gmgn-result-rank"> <span style="color: #ff6b35; font-weight: bold; background: rgba(255, 107, 53, 0.15); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 107, 53, 0.3); font-size: 12px;">榜${holder.rankIndex}</span> </div> <div class="statistic-gmgn-result-address" title="点击复制地址">${holder.address}</div> <a href="https://gmgn.ai/sol/address/${holder.address}" target="_blank" class="statistic-gmgn-address-jump-btn" title="查看钱包详情">详情</a> </div> <div class="statistic-gmgn-compact-details"> <div class="statistic-gmgn-detail-section"> <div class="statistic-gmgn-section-title"> 基本信息 ${generateSuspiciousLabelsHTML(holder.suspiciousLabels)} </div> <div class="statistic-detail-grid-modern"> ${generateDetailItemHTML('💎', '持仓', holder.balance)} ${generateDetailItemHTML('✨', '持仓占比', holder.amountPercentage, 'highlight', true)} ${generateDetailItemHTML('📉', '筹码已卖', holder.sellPercentage, holder.sellPercentage === '0.00%' ? 'profit-positive' : 'warning')} ${generateDetailItemHTML('💰', '净流入', '$' + holder.netflowUsd, holder.netflowClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('📈', '盈亏', holder.profitSign + '$' + holder.profit, holder.profitClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('🚀', '倍数', holder.profitChange, holder.profitChangeClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${holder.solBalance ? generateDetailItemHTML('⭐', 'SOL餘額', holder.solBalance, 'highlight') : ''} </div> </div> </div> `; // 添加地址复制功能 const addressElement = item.querySelector('.statistic-gmgn-result-address'); addressElement.addEventListener('click', () => { navigator.clipboard.writeText(holder.address).then(() => { addressElement.style.backgroundColor = '#16a34a'; addressElement.style.color = 'white'; setTimeout(() => { addressElement.style.backgroundColor = ''; addressElement.style.color = ''; }, 1000); }); }); resultsList.appendChild(item); }); // 更新分页按钮状态 const prevBtn = modal.querySelector('#statistic-prev-page'); const nextBtn = modal.querySelector('#statistic-next-page'); const currentSpan = modal.querySelector('.statistic-pagination-current'); if (prevBtn) { prevBtn.disabled = (page === 1); } if (nextBtn) { nextBtn.disabled = (page === totalPages); } if (currentSpan) { currentSpan.textContent = `第 ${page} 页 / 共 ${totalPages} 页`; } } // 绑定分页按钮事件 const prevBtn = modal.querySelector('#statistic-prev-page'); const nextBtn = modal.querySelector('#statistic-next-page'); if (prevBtn) { prevBtn.addEventListener('click', () => { if (currentPage > 1) { renderPage(currentPage - 1); } }); } if (nextBtn) { nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { renderPage(currentPage + 1); } }); } } // 7. 绑定关闭按钮事件 modal.querySelector('.statistic-gmgn-modal-close').addEventListener('click', () => { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); }); // 点击模态框外部关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }); } // 数字格式化函数 function formatNumber(num) { if (num === null || num === undefined) return 'N/A'; // 處理負數:保留負號,對絕對值進行格式化 const isNegative = num < 0; const absNum = Math.abs(num); let formatted; if (absNum >= 1000000000) { formatted = (absNum / 1000000000).toFixed(2) + 'B'; } else if (absNum >= 1000000) { formatted = (absNum / 1000000).toFixed(2) + 'M'; } else if (absNum >= 1000) { formatted = (absNum / 1000).toFixed(2) + 'K'; } else { formatted = absNum.toFixed(2); } return isNegative ? '-' + formatted : formatted; } // Excel导出功能 function exportToExcel(data, title, caAddress, showSolBalance) { try { // 创建工作表数据 const worksheetData = []; // 添加标题行 const headers = ['排名', '地址', '持仓数量', '持仓占比', '筹码已卖', 'USD价值', '净流入USD', '盈亏USD', '盈亏倍数']; if (showSolBalance) { headers.push('SOL餘額'); } worksheetData.push(headers); // 添加数据行 data.forEach((holder, index) => { const row = [ holder.rank, holder.address, holder.balance, holder.amountPercentage, holder.sellPercentage, holder.usdValue, holder.netflowUsd, (holder.profitSign || '') + holder.profit, holder.profitChange ]; if (showSolBalance) { row.push(holder.solBalance || 'N/A'); } worksheetData.push(row); }); // 创建工作簿 const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet(worksheetData); // 设置列宽 const colWidths = [ {wch: 6}, // 排名 {wch: 45}, // 地址 {wch: 15}, // 持仓数量 {wch: 10}, // 持仓比例 {wch: 10}, // 已卖比例 {wch: 15}, // USD价值 {wch: 15}, // 净流入 {wch: 15}, // 盈亏 {wch: 12} // 倍数 ]; if (showSolBalance) { colWidths.push({wch: 12}); // SOL餘額 } ws['!cols'] = colWidths; // 添加工作表到工作簿 XLSX.utils.book_append_sheet(wb, ws, title); // 生成文件名 const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const fileName = `${title}_${caAddress ? caAddress.slice(0, 8) : 'data'}_${timestamp}.xlsx`; // 下载文件 XLSX.writeFile(wb, fileName); // 显示成功提示 const exportBtn = document.querySelector('#statistic-export-excel-btn'); if (exportBtn) { const originalText = exportBtn.textContent; exportBtn.textContent = '✅ 导出成功'; exportBtn.style.backgroundColor = '#059669'; setTimeout(() => { exportBtn.textContent = originalText; exportBtn.style.backgroundColor = ''; }, 2000); } } catch (error) { console.error('Excel导出失败:', error); showModernToast('导出失败,请检查浏览器控制台了解详情', 'error'); } } // 根据类型获取对应的地址数据(优化版本) function getAddressByType(type) { if (!interceptedData?.data?.list) return []; // 检查缓存 const currentHash = getDataHash(interceptedData); const cacheKey = `${type}_${currentHash}`; if (dataCache.filteredResults.has(cacheKey)) { console.log('[性能优化] 使用缓存的过滤结果:', type); return dataCache.filteredResults.get(cacheKey); } console.log('[性能优化] 重新过滤数据:', type); const currentTime = Math.floor(Date.now() / 1000); const sevenDaysInSeconds = 7 * 24 * 60 * 60; const holders = interceptedData.data.list; let result; switch(type) { case 'fullPosition': result = holders.filter(h => h.sell_amount_percentage === 0 && (!h.token_transfer_out || !h.token_transfer_out.address) ); break; case 'profitable': result = holders.filter(h => h.profit > 0); break; case 'losing': result = holders.filter(h => h.profit < 0); break; case 'active24h': result = holders.filter(h => h.last_active_timestamp > currentTime - 86400); break; case 'diamondHands': result = holders.filter(h => h.maker_token_tags?.includes('diamond_hands')); break; case 'newAddress': result = holders.filter(h => h.tags?.includes('fresh_wallet')); break; case 'holdingLessThan7Days': result = holders.filter(h => h.start_holding_at && (currentTime - h.start_holding_at) < sevenDaysInSeconds ); break; case 'highProfit': result = holders.filter(h => h.profit_change > 5); break; case 'suspicious': result = holders.filter(h => h.is_suspicious || h.transfer_in || (h.maker_token_tags && ( h.maker_token_tags.includes('rat_trader') || h.maker_token_tags.includes('bundler') )) ); break; case 'lowSolBalance': result = holders.filter(h => h.native_balance && (h.native_balance / 1000000000) < 1 ); break; case 'tokenTransferIn': result = holders.filter(h => h.token_transfer_in && h.token_transfer_in.address && h.token_transfer_in.address.trim() !== '' ); break; case 'exchangeAddresses': result = holders.filter(h => isExchangeAddress(h)); break; default: result = []; } // 缓存结果 dataCache.filteredResults.set(cacheKey, result); console.log('[性能优化] 过滤结果已缓存:', type, 'count:', result.length); return result; } // 获取类型对应的中文标题 function getTypeTitle(type) { const titles = { 'fullPosition': '满仓地址', 'profitable': '盈利地址', 'losing': '亏损地址', 'active24h': '24小时活跃地址', 'diamondHands': '钻石手地址', 'newAddress': '新地址', 'holdingLessThan7Days': '持仓小于7天的地址', 'highProfit': '5倍以上盈利地址', 'suspicious': '可疑地址', 'lowSolBalance': 'SOL餘額不足1的地址', 'tokenTransferIn': '代币转入地址', 'exchangeAddresses': '交易所地址' }; return titles[type] || '未知类型'; } // 1. 拦截 fetch 请求 const originalFetch = window.fetch; window.fetch = function(url, options) { if (isTargetApi(url)) { console.log('[拦截] fetch 请求:', url); return originalFetch.apply(this, arguments) .then(response => { if (response.ok) { processResponse(response.clone()); } return response; }); } return originalFetch.apply(this, arguments); }; // 2. 拦截 XMLHttpRequest const originalXHR = window.XMLHttpRequest; window.XMLHttpRequest = function() { const xhr = new originalXHR(); const originalOpen = xhr.open; xhr.open = function(method, url) { if (isTargetApi(url)) { console.log('[拦截] XHR 请求:', url); const originalOnload = xhr.onload; xhr.onload = function() { if (xhr.readyState === 4 && xhr.status === 200) { processResponse(xhr.responseText); } originalOnload?.apply(this, arguments); }; } return originalOpen.apply(this, arguments); }; return xhr; }; function isTargetApi(url) { if (typeof url !== 'string') return false; // 检查是否是token_holders API且包含limit参数 const isTokenHoldersApi = /vas\/api\/v1\/token_holders\/(sol|eth|base|bsc|tron)(\/|$|\?)/i.test(url); const hasLimitParam = /[?&]limit=/i.test(url); const isTarget = isTokenHoldersApi && hasLimitParam; if (isTarget) { // 从URL中提取CA地址 const match = url.match(/vas\/api\/v1\/token_holders\/sol\/([^/?]+)/i); console.log('匹配的ca:',match) console.log('包含limit参数的URL:', url) if (match && match[1]) { currentCaAddress = match[1]; } } return isTarget; } function processResponseInternal(response) { console.log('开始处理响应数据'); try { const dataPromise = typeof response === 'string' ? Promise.resolve(JSON.parse(response)) : response.json(); dataPromise.then(data => { interceptedData = data; console.log('[成功] 拦截到数据量:', data.data?.list?.length); console.log('[成功] 拦截到数据:',data); const currentStats = calculateStats(); if (isFirstLoad) { // 首次加载,记录初始数据和CA地址 initialStats = currentStats; initialCaAddress = currentCaAddress; isFirstLoad = false; updateStatsDisplay(currentStats, true); } else { // 非首次加载,比较CA地址 const isSameCa = currentCaAddress === initialCaAddress; updateStatsDisplay(currentStats, !isSameCa); // 如果CA地址不同,更新初始数据为当前数据,并重置下载按钮状态 if (!isSameCa) { initialStats = currentStats; initialCaAddress = currentCaAddress; // 重置下载按钮状态 resetDownloadButtonState(); console.log('检测到CA地址变更,已重置下载按钮状态'); } } }).catch(e => console.error('解析失败:', e)); } catch (e) { console.error('处理响应错误:', e); } } // 防抖版本的processResponse const processResponse = debounce(processResponseInternal, 100); // 计算数据哈希值用于缓存 function getDataHash(data) { return JSON.stringify({ length: data?.data?.list?.length || 0, timestamp: data?.data?.list?.[0]?.last_active_timestamp || 0, caAddress: currentCaAddress }); } // 3. 计算所有统计指标(优化版本) function calculateStats() { if (!interceptedData?.data?.list) return null; // 检查缓存 const currentHash = getDataHash(interceptedData); if (dataCache.lastDataHash === currentHash && dataCache.calculatedStats) { console.log('[性能优化] 使用缓存的统计数据'); return dataCache.calculatedStats; } console.log('[性能优化] 重新计算统计数据'); const currentTime = Math.floor(Date.now() / 1000); const sevenDaysInSeconds = 7 * 24 * 60 * 60; // 7天的秒数 const holders = interceptedData.data.list; const stats = { fullPosition: 0, // 全仓 profitable: 0, // 盈利 losing: 0, // 亏损 active24h: 0, // 24h活跃 diamondHands: 0, // 钻石手 newAddress: 0, // 新地址 highProfit: 0, // 10x盈利 suspicious: 0, // 新增:可疑地址 holdingLessThan7Days: 0, // 新增:持仓小于7天 lowSolBalance: 0, // 新增:SOL餘額小於1的地址 tokenTransferIn: 0, // 新增:代币转入地址数 exchangeAddresses: 0 // 新增:交易所地址数 }; holders.forEach(holder => { // 满判断条件:1.没有卖出;2.没有出货地址 if (holder.sell_amount_percentage === 0 && (!holder.token_transfer_out || !holder.token_transfer_out.address)) { stats.fullPosition++; } if (holder.profit > 0) stats.profitable++; if (holder.profit < 0) stats.losing++; if (holder.last_active_timestamp > currentTime - 86400) stats.active24h++; if (holder.maker_token_tags?.includes('diamond_hands')) stats.diamondHands++; if (holder.tags?.includes('fresh_wallet')) stats.newAddress++; if (holder.profit_change > 5) stats.highProfit++; // 增强版可疑地址检测 if ( holder.is_suspicious || holder.transfer_in || (holder.maker_token_tags && ( holder.maker_token_tags.includes('rat_trader') || holder.maker_token_tags.includes('bundler') )) ) { stats.suspicious++; } // 新增7天持仓统计 if (holder.start_holding_at && (currentTime - holder.start_holding_at) < sevenDaysInSeconds) { stats.holdingLessThan7Days++; } // 新增低SOL餘額統計(小於1 SOL) if (holder.native_balance && (holder.native_balance / 1000000000) < 1) { stats.lowSolBalance++; } // 新增代币转入地址统计 if (holder.token_transfer_in && holder.token_transfer_in.address && holder.token_transfer_in.address.trim() !== '') { stats.tokenTransferIn++; } // 新增交易所地址统计 if (isExchangeAddress(holder)) { stats.exchangeAddresses++; } }); // 缓存计算结果 dataCache.lastDataHash = currentHash; dataCache.calculatedStats = stats; dataCache.filteredResults.clear(); // 清空过滤缓存 console.log('[性能优化] 统计数据已缓存'); return stats; } // 1. 持久化容器监听 const observer = new MutationObserver(() => { const targetContainer = document.querySelector('.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full'); if (targetContainer && !targetContainer.querySelector('#statistic-gmgn-stats-item')) { injectStatsItem(targetContainer); } }); function injectStatsItem(container) { if (container.querySelector('#statistic-gmgn-stats-item')) return; const isSol = isSolNetwork(); const statsItem = document.createElement('div'); statsItem.id = 'statistic-gmgn-stats-item'; statsItem.className = 'statistic-gmgn-stats-container'; const headerClass = isSol ? 'statistic-gmgn-stats-header sol-network' : 'statistic-gmgn-stats-header'; const dataClass = isSol ? 'statistic-gmgn-stats-data sol-network' : 'statistic-gmgn-stats-data'; statsItem.innerHTML = ` <div class="${headerClass}"> <span title="持有代币且未卖出任何数量的地址(排除转移代币卖出的地址)">满仓</span> <span title="当前持仓价值高于买入成本的地址">盈利</span> <span title="当前持仓价值低于买入成本的地址">亏损</span> <span title="过去24小时内有交易活动的地址">活跃</span> <span title="长期持有且很少卖出的地址">钻石</span> <span title="新钱包">新址</span> <span title="持仓时间小于7天的地址">7天</span> <span title="盈利超过5倍的地址">5X</span> <span title="标记为可疑或异常行为的地址">可疑</span> <span title="有代币转入记录的地址">转入</span> <span title="与交易所相关的地址">交易所</span> ${isSol ? '<span title="SOL餘額小於1的地址">低SOL</span>' : ''} <span title="下载统计数据图片">图片</span> </div> <div class="${dataClass}"> <span id="fullPosition">-</span> <span id="profitable">-</span> <span id="losing">-</span> <span id="active24h">-</span> <span id="diamondHands">-</span> <span id="newAddress">-</span> <span id="holdingLessThan7Days">-</span> <span id="highProfit">-</span> <span id="suspicious">-</span> <span id="tokenTransferIn">-</span> <span id="exchangeAddresses">-</span> ${isSol ? '<span id="lowSolBalance">-</span>' : ''} <span id="statistic-download-image-btn" class="statistic-download-btn clickable" title="下载统计数据图片">下载</span> </div> `; container.insertAdjacentElement('afterbegin', statsItem); } function updateStatsDisplayInternal(currentStats, forceNoArrows) { if (!currentStats) return; // 确保DOM已存在 if (!document.getElementById('statistic-gmgn-stats-item')) { injectStatsItem(); } // 优化的事件监听器绑定(只绑定一次) if (!dataCache.eventsInitialized) { console.log('[性能优化] 初始化事件监听器'); const baseClickableTypes = ['fullPosition', 'profitable', 'losing', 'active24h', 'diamondHands', 'newAddress', 'holdingLessThan7Days', 'highProfit', 'suspicious', 'tokenTransferIn', 'exchangeAddresses']; const clickableTypes = isSolNetwork() ? [...baseClickableTypes, 'lowSolBalance'] : baseClickableTypes; clickableTypes.forEach(id => { const element = document.getElementById(id); if (element && !element.hasAttribute('data-event-bound')) { element.classList.add('clickable'); element.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const addresses = getAddressByType(id); // 交易所地址使用专用弹框 if (id === 'exchangeAddresses') { createExchangeModal(addresses, currentCaAddress); } else { const title = getTypeTitle(id); const showSolBalance = id === 'lowSolBalance'; createModal(title, addresses, currentCaAddress, showSolBalance); } }; element.setAttribute('data-event-bound', 'true'); console.log('[性能优化] 事件监听器已绑定:', id); } }); dataCache.eventsInitialized = true; } const updateStatElement = (id, value, hasChanged, isIncrease) => { const element = document.getElementById(id); if (!element) return; element.innerHTML = `<strong style="color: ${id === 'profitable' ? '#2E8B57' : (id === 'losing' || id === 'suspicious' ? '#FF1493' : id === 'holdingLessThan7Days' ? '#00E5EE' : id === 'lowSolBalance' ? '#FFA500' : '#e9ecef')}">${value}</strong>`; // 只有当不是强制不显示箭头且确实有变化时才显示箭头 if (!forceNoArrows && hasChanged) { const arrow = document.createElement('span'); arrow.className = isIncrease ? 'statistic-up-arrow' : 'statistic-down-arrow'; arrow.textContent = isIncrease ? '▲' : '▼'; // 移除旧的箭头(如果有) const oldArrow = element.querySelector('.statistic-up-arrow, .statistic-down-arrow'); if (oldArrow) oldArrow.remove(); element.appendChild(arrow); } else { // 没有变化或强制不显示箭头,移除箭头(如果有) const oldArrow = element.querySelector('.statistic-up-arrow, .statistic-down-arrow'); if (oldArrow) oldArrow.remove(); } // 事件监听器已在初始化时绑定,无需重复绑定 }; // 绑定下载图片按钮事件 const downloadBtn = document.getElementById('statistic-download-image-btn'); if (downloadBtn && !downloadBtn.hasAttribute('data-event-bound')) { downloadBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // 检查是否正在处理中 if (isDownloadInProgress) { console.log('下载正在进行中,请稍候...'); return; } handleDownloadImage(); }); downloadBtn.setAttribute('data-event-bound', 'true'); } // 更新各个统计指标 // 新增7天持仓统计更新 updateStatElement('holdingLessThan7Days', currentStats.holdingLessThan7Days, initialStats && currentStats.holdingLessThan7Days !== initialStats.holdingLessThan7Days, initialStats && currentStats.holdingLessThan7Days > initialStats.holdingLessThan7Days); updateStatElement('fullPosition', currentStats.fullPosition, initialStats && currentStats.fullPosition !== initialStats.fullPosition, initialStats && currentStats.fullPosition > initialStats.fullPosition); updateStatElement('profitable', currentStats.profitable, initialStats && currentStats.profitable !== initialStats.profitable, initialStats && currentStats.profitable > initialStats.profitable); updateStatElement('losing', currentStats.losing, currentStats.losing !== initialStats.losing, currentStats.losing > initialStats.losing); updateStatElement('active24h', currentStats.active24h, currentStats.active24h !== initialStats.active24h, currentStats.active24h > initialStats.active24h); updateStatElement('diamondHands', currentStats.diamondHands, currentStats.diamondHands !== initialStats.diamondHands, currentStats.diamondHands > initialStats.diamondHands); updateStatElement('newAddress', currentStats.newAddress, currentStats.newAddress !== initialStats.newAddress, currentStats.newAddress > initialStats.newAddress); updateStatElement('highProfit', currentStats.highProfit, currentStats.highProfit !== initialStats.highProfit, currentStats.highProfit > initialStats.highProfit); updateStatElement('suspicious', currentStats.suspicious, currentStats.suspicious !== initialStats.suspicious, currentStats.suspicious > initialStats.suspicious); updateStatElement('tokenTransferIn', currentStats.tokenTransferIn, initialStats && currentStats.tokenTransferIn !== initialStats.tokenTransferIn, initialStats && currentStats.tokenTransferIn > initialStats.tokenTransferIn); updateStatElement('exchangeAddresses', currentStats.exchangeAddresses, initialStats && currentStats.exchangeAddresses !== initialStats.exchangeAddresses, initialStats && currentStats.exchangeAddresses > initialStats.exchangeAddresses); // 只在SOL网络时更新低SOL余额统计 if (isSolNetwork()) { updateStatElement('lowSolBalance', currentStats.lowSolBalance, initialStats && currentStats.lowSolBalance !== initialStats.lowSolBalance, initialStats && currentStats.lowSolBalance > initialStats.lowSolBalance); } } // 防抖版本的updateStatsDisplay const updateStatsDisplay = debounce(updateStatsDisplayInternal, 200); // 数据收集函数 - 收集基础统计数据和详细持有者信息 function collectStatsData() { if (!interceptedData?.data?.list || !currentCaAddress) { console.error('数据不完整,无法生成图片'); return null; } const currentStats = calculateStats(); if (!currentStats) { console.error('无法计算统计数据'); return null; } // 基础统计数据 const basicStats = { fullPosition: { label: '满仓', value: currentStats.fullPosition, type: 'fullPosition' }, profitable: { label: '盈利', value: currentStats.profitable, type: 'profitable' }, losing: { label: '亏损', value: currentStats.losing, type: 'losing' }, active24h: { label: '活跃', value: currentStats.active24h, type: 'active24h' }, diamondHands: { label: '钻石', value: currentStats.diamondHands, type: 'diamondHands' }, newAddress: { label: '新址', value: currentStats.newAddress, type: 'newAddress' }, holdingLessThan7Days: { label: '7天', value: currentStats.holdingLessThan7Days, type: 'holdingLessThan7Days' }, highProfit: { label: '5X', value: currentStats.highProfit, type: 'highProfit' }, suspicious: { label: '可疑', value: currentStats.suspicious, type: 'suspicious' }, tokenTransferIn: { label: '转入', value: currentStats.tokenTransferIn, type: 'tokenTransferIn' }, exchangeAddresses: { label: '交易所', value: currentStats.exchangeAddresses, type: 'exchangeAddresses' } }; // 如果是SOL网络,添加低余额统计 if (isSolNetwork()) { basicStats.lowSolBalance = { label: '低SOL', value: currentStats.lowSolBalance, type: 'lowSolBalance' }; } // 收集每个统计类型的汇总数据(包括值为0的项目) const detailedData = {}; for (const [key, stat] of Object.entries(basicStats)) { const addresses = getAddressByType(stat.type); if (addresses && addresses.length > 0) { // 计算汇总信息 const soldChipsCount = addresses.filter(holder => (holder.sell_amount_percentage || 0) > 0).length; const totalHoldingPercentage = addresses.reduce((sum, holder) => sum + (holder.amount_percentage || 0), 0); detailedData[key] = { label: stat.label, totalCount: addresses.length, soldChipsCount: soldChipsCount, totalHoldingPercentage: (totalHoldingPercentage * 100).toFixed(2) + '%' }; } else { // 即使没有地址数据,也创建空的详细数据 detailedData[key] = { label: stat.label, totalCount: 0, soldChipsCount: 0, totalHoldingPercentage: '0.00%' }; } } return { caAddress: currentCaAddress, timestamp: new Date(), basicStats: basicStats, detailedData: detailedData }; } // 绘制圆角矩形辅助函数 function drawRoundedRect(ctx, x, y, width, height, radius, strokeColor = null, strokeWidth = 0, fillOnly = false) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.arcTo(x + width, y, x + width, y + height, radius); ctx.arcTo(x + width, y + height, x, y + height, radius); ctx.arcTo(x, y + height, x, y, radius); ctx.arcTo(x, y, x + width, y, radius); ctx.closePath(); if (!fillOnly) { ctx.fill(); } if (strokeColor && strokeWidth > 0) { ctx.strokeStyle = strokeColor; ctx.lineWidth = strokeWidth; ctx.stroke(); } } // 图片生成函数 - 现代化风格 function generateStatsImage(data) { if (!data) { console.error('无数据可生成图片'); return null; } const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // 设置画布尺寸 - 现代化尺寸 canvas.width = 1200; canvas.height = 1400; // 增加高度以适应现代化布局 // 创建现代渐变背景 const bgGradient = ctx.createLinearGradient(0, 0, 0, canvas.height); bgGradient.addColorStop(0, '#0f172a'); bgGradient.addColorStop(0.3, '#1e293b'); bgGradient.addColorStop(0.7, '#334155'); bgGradient.addColorStop(1, '#1e293b'); ctx.fillStyle = bgGradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // 绘制现代化圆角边框 const borderRadius = 20; const borderPadding = 30; drawRoundedRect(ctx, borderPadding, borderPadding, canvas.width - borderPadding * 2, canvas.height - borderPadding * 2, borderRadius, '#3b82f6', 3); // 绘制标题区域背景 const titleBg = ctx.createLinearGradient(0, 50, 0, 150); titleBg.addColorStop(0, 'rgba(59, 130, 246, 0.2)'); titleBg.addColorStop(1, 'rgba(59, 130, 246, 0.05)'); ctx.fillStyle = titleBg; drawRoundedRect(ctx, 60, 60, canvas.width - 120, 120, 15); // 绘制现代化标题 ctx.font = 'bold 36px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#ffffff'; ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; ctx.shadowBlur = 4; ctx.shadowOffsetY = 2; const title = 'GMGN 前排统计分析'; ctx.fillText(title, canvas.width / 2, 110); // 清除阴影 ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; ctx.shadowOffsetY = 0; // 绘制CA地址和时间 - 现代化样式 ctx.font = '18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#22d3ee'; const formatTime = data.timestamp.getFullYear() + '-' + String(data.timestamp.getMonth() + 1).padStart(2, '0') + '-' + String(data.timestamp.getDate()).padStart(2, '0') + ' ' + String(data.timestamp.getHours()).padStart(2, '0') + ':' + String(data.timestamp.getMinutes()).padStart(2, '0') + ':' + String(data.timestamp.getSeconds()).padStart(2, '0'); ctx.fillText(`CA: ${data.caAddress}`, canvas.width / 2, 140); ctx.fillStyle = '#fbbf24'; ctx.fillText(`时间: ${formatTime}`, canvas.width / 2, 165); // 绘制基础统计数据(第一层)- 现代化风格 ctx.font = 'bold 24px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#ffffff'; ctx.textAlign = 'left'; ctx.fillText('基础统计数据', 80, 220); let yPos = 260; const statsPerRow = 3; // 每行3个 const statWidth = 350; // 增加宽度适应现代化布局 const statHeight = 90; // 增加高度 let currentRow = 0; let currentCol = 0; const baseX = 80; // 左侧边距 for (const [key, stat] of Object.entries(data.basicStats)) { const x = baseX + currentCol * statWidth; const y = yPos + currentRow * statHeight; // 绘制现代化卡片背景渐变 const cardGradient = ctx.createLinearGradient(x, y, x, y + statHeight - 15); cardGradient.addColorStop(0, 'rgba(255, 255, 255, 0.08)'); cardGradient.addColorStop(1, 'rgba(59, 130, 246, 0.12)'); ctx.fillStyle = cardGradient; drawRoundedRect(ctx, x, y, statWidth - 30, statHeight - 15, 12); // 绘制现代化边框 drawRoundedRect(ctx, x, y, statWidth - 30, statHeight - 15, 12, '#3b82f6', 2, true); // 绘制标签 - 现代化字体 ctx.font = '16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.textAlign = 'left'; ctx.fillText(stat.label, x + 20, y + 30); // 绘制数值 - 现代化颜色和字体 ctx.font = 'bold 28px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; const valueColor = key === 'profitable' ? '#22c55e' : (key === 'losing' || key === 'suspicious' ? '#ef4444' : key === 'holdingLessThan7Days' ? '#06b6d4' : key === 'lowSolBalance' ? '#f59e0b' : '#22d3ee'); ctx.fillStyle = valueColor; ctx.fillText(stat.value.toString(), x + 20, y + 65); currentCol++; if (currentCol >= statsPerRow) { currentCol = 0; currentRow++; } } // 绘制详细数据(第二层)- 现代化风格 yPos = 180 + (Math.ceil(Object.keys(data.basicStats).length / statsPerRow) + 1) * statHeight + 50; // 绘制详细分析标题 ctx.font = 'bold 24px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#ffffff'; ctx.textAlign = 'left'; ctx.fillText('详细数据分析', 80, yPos); yPos += 40; // 使用现代化网格布局绘制详细数据分析 const detailStatsPerRow = 3; // 每行3个详细数据单元格 const detailStatWidth = 350; // 与基础统计保持一致 const detailStatHeight = 130; // 增加高度以适应现代化布局 let detailCurrentRow = 0; let detailCurrentCol = 0; for (const [key, detail] of Object.entries(data.detailedData)) { if (yPos + detailCurrentRow * detailStatHeight > canvas.height - 150) break; // 防止超出画布 const x = baseX + detailCurrentCol * detailStatWidth; // 与基础数据对齐 const y = yPos + detailCurrentRow * detailStatHeight; // 绘制现代化卡片背景渐变 const detailCardGradient = ctx.createLinearGradient(x, y, x, y + detailStatHeight - 15); detailCardGradient.addColorStop(0, 'rgba(255, 255, 255, 0.06)'); detailCardGradient.addColorStop(1, 'rgba(16, 185, 129, 0.08)'); ctx.fillStyle = detailCardGradient; drawRoundedRect(ctx, x, y, detailStatWidth - 30, detailStatHeight - 15, 12); // 绘制现代化边框 drawRoundedRect(ctx, x, y, detailStatWidth - 30, detailStatHeight - 15, 12, '#10b981', 2, true); // 绘制分类标题 - 现代化样式 ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; const titleColor = key === 'profitable' ? '#22c55e' : (key === 'losing' || key === 'suspicious' ? '#ef4444' : key === 'holdingLessThan7Days' ? '#06b6d4' : key === 'lowSolBalance' ? '#f59e0b' : '#22d3ee'); ctx.fillStyle = titleColor; ctx.textAlign = 'left'; ctx.fillText(`${detail.label}`, x + 20, y + 30); // 绘制汇总数据 - 现代化样式 ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; // 已卖筹码数 ctx.fillText('已卖筹码数:', x + 20, y + 55); ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = detail.soldChipsCount > 0 ? '#ef4444' : '#22c55e'; ctx.fillText(detail.soldChipsCount.toString(), x + 150, y + 55); // 总地址数 ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.fillText('总地址数:', x + 20, y + 80); ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = titleColor; // 使用与标题相同的颜色 ctx.fillText(detail.totalCount.toString(), x + 150, y + 80); // 持仓占比 ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.fillText('持仓占比:', x + 20, y + 105); ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#60a5fa'; ctx.fillText(detail.totalHoldingPercentage, x + 150, y + 105); detailCurrentCol++; if (detailCurrentCol >= detailStatsPerRow) { detailCurrentCol = 0; detailCurrentRow++; } } return canvas; } // 下载图片函数 function downloadImage(canvas, filename) { if (!canvas) { console.error('无法下载图片:画布为空'); return; } try { // 转换为blob canvas.toBlob(function(blob) { // 创建下载链接 const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; // 触发下载 document.body.appendChild(link); link.click(); document.body.removeChild(link); // 清理URL对象 URL.revokeObjectURL(url); console.log('图片下载成功:', filename); }, 'image/png'); } catch (error) { console.error('下载图片失败:', error); } } // 显示图片预览模态框 function showImagePreview(canvas, filename) { const modal = document.createElement('div'); modal.className = 'image-preview-modal'; const imageUrl = canvas.toDataURL('image/png'); modal.innerHTML = ` <div class="image-preview-content"> <div class="image-preview-header"> <div class="image-preview-title">📷 统计图片预览</div> <button class="image-preview-close">×</button> </div> <img src="${imageUrl}" alt="统计图片" class="image-preview-img"> <div class="image-preview-buttons"> <button class="image-preview-btn copy-btn">📋 复制图片</button> <button class="image-preview-btn download-btn">💾 下载图片</button> </div> </div> `; document.body.appendChild(modal); // 绑定事件 const closeBtn = modal.querySelector('.image-preview-close'); const copyBtn = modal.querySelector('.copy-btn'); const downloadBtn = modal.querySelector('.download-btn'); // 关闭模态框 const closeModal = () => { document.body.removeChild(modal); }; closeBtn.addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); // 复制图片 copyBtn.addEventListener('click', () => { copyImageToClipboard(canvas); }); // 下载图片 downloadBtn.addEventListener('click', () => { downloadImageFromPreview(canvas, filename); closeModal(); }); // ESC键关闭 const escHandler = (e) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } // 复制图片到剪贴板 async function copyImageToClipboard(canvas) { try { // 将canvas转为blob canvas.toBlob(async (blob) => { try { if (navigator.clipboard && window.ClipboardItem) { const item = new ClipboardItem({ 'image/png': blob }); await navigator.clipboard.write([item]); showModernToast('图片已复制到剪贴板!', 'success'); } else { // 兜底方案:创建临时图片元素让用户手动复制 const img = document.createElement('img'); img.src = canvas.toDataURL('image/png'); img.style.position = 'fixed'; img.style.top = '-9999px'; document.body.appendChild(img); // 选择图片 const range = document.createRange(); range.selectNode(img); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); // 尝试复制 const success = document.execCommand('copy'); document.body.removeChild(img); window.getSelection().removeAllRanges(); if (success) { showModernToast('图片已复制到剪贴板!', 'success'); } else { showModernToast('复制失败,请尝试手动下载图片', 'error'); } } } catch (error) { console.error('复制图片失败:', error); showModernToast('复制失败:' + error.message, 'error'); } }, 'image/png'); } catch (error) { console.error('复制图片失败:', error); showModernToast('复制失败:' + error.message, 'error'); } } // 从预览下载图片 function downloadImageFromPreview(canvas, filename) { try { canvas.toBlob((blob) => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); showModernToast('图片下载成功!', 'success'); }, 'image/png'); } catch (error) { console.error('下载图片失败:', error); showModernToast('下载失败:' + error.message, 'error'); } } // 重置按钮状态 function resetDownloadButtonState() { isDownloadInProgress = false; const button = document.getElementById('statistic-download-image-btn'); if (button) { button.classList.remove('disabled'); button.textContent = '下载'; } } // 设置按钮禁用状态 function setDownloadButtonDisabled(disabled) { const button = document.getElementById('statistic-download-image-btn'); if (button) { if (disabled) { button.classList.add('disabled'); button.textContent = '生成中...'; } else { button.classList.remove('disabled'); button.textContent = '下载'; } } } // 主要的下载处理函数 - 现在显示预览而不是直接下载 function handleDownloadImage() { const button = document.getElementById('statistic-download-image-btn'); if (!button) return; // 检查是否已在处理中 if (isDownloadInProgress) { console.log('图片生成正在进行中...'); return; } // 设置处理状态 isDownloadInProgress = true; setDownloadButtonDisabled(true); try { // 收集数据 const data = collectStatsData(); if (!data) { throw new Error('无法收集数据'); } // 更新当前CA地址 currentCAAddress = data.caAddress || ''; // 生成图片 const canvas = generateStatsImage(data); if (!canvas) { throw new Error('无法生成图片'); } // 生成文件名 const timestamp = data.timestamp.getFullYear() + String(data.timestamp.getMonth() + 1).padStart(2, '0') + String(data.timestamp.getDate()).padStart(2, '0') + String(data.timestamp.getHours()).padStart(2, '0'); const filename = `${data.caAddress}_${timestamp}.png`; // 显示预览而不是直接下载 showImagePreview(canvas, filename); } catch (error) { console.error('生成图片失败:', error); showModernToast('生成图片失败:' + error.message, 'error'); } finally { // 恢复按钮状态 isDownloadInProgress = false; setDownloadButtonDisabled(false); } } // 4. 初始化 if (document.readyState === 'complete') { startObserving(); } else { window.addEventListener('DOMContentLoaded', startObserving); } function startObserving() { // 立即检查一次 const initialContainer = document.querySelector('.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full'); if (initialContainer) injectStatsItem(initialContainer); // 持续监听DOM变化 observer.observe(document.body, { childList: true, subtree: true, attributes: false }); } })();// ==UserScript== // @name GMGN 前排统计 // @namespace http://tampermonkey.net/ // @version 4.8 // @description 统计GMGN任意代币前排地址的数据,让数字来说话!新增首次记录和涨跌提醒功能,所有数字可点击查看详情,弹框显示净流入数据,负数红色显示,点击外部关闭 // @match https://gmgn.ai/* // @match https://www.gmgn.ai/* // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @require https://code.jquery.com/jquery-3.6.0.min.js // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // 全局变量 - 跟踪下载按钮状态 let isDownloadInProgress = false; let currentCAAddress = ''; // 现代化提示框函数 function showModernToast(message, type = 'success', duration = 3000) { // 移除现有的提示框 const existingToast = document.querySelector('.modern-toast'); const existingOverlay = document.querySelector('.modern-toast-overlay'); if (existingToast) existingToast.remove(); if (existingOverlay) existingOverlay.remove(); // 创建遮罩层 const overlay = document.createElement('div'); overlay.className = 'modern-toast-overlay'; // 创建提示框 const toast = document.createElement('div'); toast.className = 'modern-toast'; // 根据类型设置图标 let icon, iconClass; switch (type) { case 'success': icon = '✓'; iconClass = 'success'; break; case 'error': icon = '✕'; iconClass = 'error'; break; case 'info': icon = 'ℹ'; iconClass = 'info'; break; default: icon = '✓'; iconClass = 'success'; } toast.innerHTML = ` <div class="modern-toast-content"> <div class="modern-toast-icon ${iconClass}">${icon}</div> <div class="modern-toast-text">${message}</div> <button class="modern-toast-close">×</button> </div> `; // 添加到页面 document.body.appendChild(overlay); document.body.appendChild(toast); // 关闭函数 const closeToast = () => { toast.style.animation = 'toastSlideOut 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards'; overlay.style.animation = 'overlayFadeIn 0.3s ease reverse'; setTimeout(() => { if (toast.parentNode) toast.remove(); if (overlay.parentNode) overlay.remove(); }, 300); }; // 绑定关闭事件 const closeBtn = toast.querySelector('.modern-toast-close'); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); closeToast(); }); // 点击遮罩层关闭 overlay.addEventListener('click', closeToast); // 点击提示框本身也可以关闭 toast.addEventListener('click', closeToast); // 自动关闭 if (duration > 0) { setTimeout(closeToast, duration); } // ESC键关闭 const escHandler = (e) => { if (e.key === 'Escape') { closeToast(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); // 返回关闭函数,允许手动关闭 return closeToast; } // 动态添加 CSS const style = document.createElement('style'); style.textContent = ` .statistic-gmgn-stats-container { background-color: transparent; border-radius: 4px; font-family: Arial, sans-serif; margin-right: 8px; margin-bottom:8px; border: 1px solid #333; /* 精细的右侧和下侧发光效果 */ box-shadow: 2px 2px 4px rgba(0, 119, 255, 0.6), /* 右下外发光(更小的偏移和模糊) */ 1px 1px 2px rgba(0, 119, 255, 0.4), /* 精细的次级发光 */ inset 0 0 3px rgba(0, 119, 255, 0.2); /* 更细腻的内发光 */ padding: 4px 6px; max-width: fit-content; } .statistic-gmgn-stats-header, .statistic-gmgn-stats-data { display: grid; grid-template-columns: repeat(12, 1fr); text-align: center; gap: 6px; font-weight: normal; font-size: 13px; } .statistic-gmgn-stats-header.sol-network, .statistic-gmgn-stats-data.sol-network { grid-template-columns: repeat(13, minmax(auto, 1fr)); gap: 4px; font-size: 12px; } .statistic-gmgn-stats-header span { color: #ccc; font-weight: normal; padding: 1px 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .statistic-gmgn-stats-header.sol-network span { font-size: 11px; padding: 1px; } .statistic-gmgn-stats-data span { color: #00ff00; font-weight: normal; cursor: default; transition: all 0.2s ease; padding: 1px 3px; border-radius: 2px; min-width: 0; white-space: nowrap; } .statistic-gmgn-stats-data span.clickable { cursor: pointer; } .statistic-gmgn-stats-data span.clickable:hover { background-color: rgba(0, 255, 0, 0.1); border-radius: 3px; transform: scale(1.03); } .statistic-gmgn-stats-data.sol-network span { padding: 1px 2px; font-size: 12px; } .statistic-gmgn-stats-data span .statistic-up-arrow, .statistic-up-arrow { color: green !important; margin-left: 2px; font-weight: bold; } .statistic-gmgn-stats-data span .statistic-down-arrow, .statistic-down-arrow { color: red !important; margin-left: 2px; font-weight: bold; } /* 完整弹框CSS样式 - 现代化设计 */ .statistic-gmgn-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); /* 简化为纯色,提升性能 */ /* backdrop-filter: blur(8px); */ /* 移除性能杀手 */ display: flex; align-items: center; justify-content: center; z-index: 1000; /* animation: modalFadeIn 0.3s ease-out; */ /* 移除动画,提升性能 */ } .statistic-gmgn-modal-content { background: #1e293b !important; /* 简化为纯色,提升性能 */ border-radius: 16px !important; width: 85% !important; max-width: 900px !important; max-height: 85vh !important; overflow-y: auto !important; padding: 24px !important; color: white !important; position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) scale(0.95) !important; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05), inset 0 1px 0 0 rgba(255, 255, 255, 0.1) !important; margin: 0 !important; z-index: 100000 !important; box-sizing: border-box !important; min-height: auto !important; min-width: 320px !important; pointer-events: auto !important; /* 移除动画,直接显示 */ backface-visibility: hidden !important; contain: layout style paint !important; /* 优化滚动性能 */ overflow-anchor: none !important; scroll-behavior: smooth !important; -webkit-overflow-scrolling: touch !important; } .statistic-gmgn-modal-header { display: flex !important; justify-content: space-between !important; align-items: center !important; margin-bottom: 24px !important; padding: 16px 20px !important; margin: -24px -24px 24px -24px !important; background: rgba(99, 102, 241, 0.1) !important; /* 简化为纯色,提升性能 */ border-radius: 16px 16px 0 0 !important; border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important; /* backdrop-filter: blur(10px) !important; */ /* 移除性能杀手 */ } .statistic-gmgn-modal-title { font-size: 20px !important; font-weight: 700 !important; color: white !important; margin: 0 !important; color: #ffffff !important; /* 简化文本渐变为纯色,提升性能 */ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important; } .statistic-gmgn-modal-close { background: rgba(148, 163, 184, 0.1) !important; border: 1px solid rgba(148, 163, 184, 0.2) !important; color: #94a3b8 !important; font-size: 18px !important; cursor: pointer !important; padding: 8px !important; line-height: 1 !important; width: 36px !important; height: 36px !important; border-radius: 50% !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: background-color 0.2s ease !important; /* 简化过渡,提升性能 */ } .statistic-gmgn-modal-close:hover { color: #fff !important; background: #ef4444 !important; /* 简化为纯色,提升性能 */ border-color: #ef4444 !important; /* transform: scale(1.1) !important; */ /* 移除复杂变换,提升性能 */ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4) !important; } .statistic-gmgn-result-item { background: rgba(51, 65, 85, 0.6); /* 简化为纯色,提升性能 */ border-radius: 12px; padding: 16px; margin-bottom: 16px; border: 1px solid rgba(255, 255, 255, 0.1); transition: background-color 0.2s ease; /* 简化过渡,提升性能 */ position: relative; overflow: hidden; /* 性能优化 - 硬件加速 */ will-change: transform, opacity; transform: translateZ(0); backface-visibility: hidden; contain: layout style; /* 减少backdrop-filter在大数据量时的性能消耗 */ } .statistic-gmgn-result-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, #3b82f6, #8b5cf6, #06b6d4); opacity: 0; transition: opacity 0.3s ease; transform: translateZ(0); } .statistic-gmgn-result-item:hover { background: rgba(51, 65, 85, 0.8); /* 简化为纯色,提升性能 */ transform: translateY(-2px) translateZ(0); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.1); border-color: rgba(59, 130, 246, 0.3); } .statistic-gmgn-result-item:hover::before { opacity: 1; } .statistic-gmgn-analysis-summary { margin-bottom: 24px; padding: 20px; background: linear-gradient(135deg, rgba(38, 50, 56, 0.6) 0%, rgba(30, 41, 59, 0.8) 100%); border-radius: 12px; display: flex; justify-content: space-between; align-items: center; border: 1px solid rgba(255, 255, 255, 0.1); /* backdrop-filter: blur(10px); */ /* 移除性能杀手 */ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1); } .statistic-gmgn-summary-stats { display: flex; gap: 32px; flex-wrap: wrap; } .statistic-gmgn-stat-item { display: flex; flex-direction: column; align-items: flex-start; padding: 8px 12px; background: rgba(255, 255, 255, 0.05); border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; min-width: 80px; } .statistic-gmgn-stat-item:hover { background: rgba(255, 255, 255, 0.1); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); } .statistic-gmgn-stat-label { color: #94a3b8; font-size: 12px; font-weight: 500; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; } .statistic-gmgn-stat-value { font-weight: 700; font-size: 18px; background: #3b82f6; /* 简化为纯色,提升性能 */ background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); } .statistic-gmgn-result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-wrap: wrap; gap: 8px; } .statistic-gmgn-result-rank { font-size: 14px; color: #94a3b8; font-weight: 600; min-width: 30px; } .statistic-gmgn-result-address { font-weight: 600; word-break: break-all; cursor: pointer; padding: 8px 12px; border-radius: 8px; transition: background-color 0.2s ease; /* 简化过渡,提升性能 */ background: linear-gradient(135deg, rgba(71, 85, 105, 0.6), rgba(51, 65, 85, 0.8)); border: 1px solid rgba(0, 255, 136, 0.3); flex: 1; min-width: 200px; color: #00ff88; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; position: relative; overflow: hidden; } .statistic-gmgn-result-address::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(0, 255, 136, 0.2), transparent); transition: left 0.5s ease; } .statistic-gmgn-result-address:hover { background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), rgba(51, 65, 85, 0.9)); border-color: #00ff88; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 255, 136, 0.3); } .statistic-gmgn-result-address:hover::before { left: 100%; } .statistic-gmgn-detail-section { margin-bottom: 12px; } .statistic-gmgn-section-title { font-size: 13px; font-weight: 600; color: #94a3b8; margin-bottom: 8px; display: flex; align-items: center; flex-wrap: wrap; } .statistic-gmgn-detail-grid { display: grid; grid-template-columns: 80px 1fr 80px 1fr; gap: 4px 8px; align-items: start; font-size: 12px; } .statistic-gmgn-detail-label { color: #94a3b8; font-size: 12px; padding: 2px 0; align-self: start; } .statistic-gmgn-detail-value { font-size: 12px; color: #e2e8f0; padding: 2px 0; word-break: break-word; line-height: 1.4; } .statistic-gmgn-value-highlight { color: #3b82f6; font-weight: 600; } .statistic-gmgn-compact-details .statistic-gmgn-detail-section { margin-bottom: 8px; } .statistic-gmgn-compact-details .statistic-gmgn-detail-section { margin-left: 10px; } .statistic-gmgn-address-jump-btn { background: #10b981; /* 简化为纯色,提升性能 */ color: white; padding: 6px 12px; border-radius: 8px; font-size: 11px; font-weight: 600; margin-left: 12px; cursor: pointer; transition: background-color 0.2s ease; /* 简化过渡,提升性能 */ text-decoration: none; display: inline-flex; align-items: center; gap: 4px; border: 1px solid rgba(16, 185, 129, 0.3); position: relative; overflow: hidden; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2); } .statistic-gmgn-address-jump-btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.4s ease; } .statistic-gmgn-address-jump-btn:hover { background: #059669; /* 简化为纯色,提升性能 */ transform: translateY(-2px) scale(1.05); box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4); border-color: #10b981; } .statistic-gmgn-address-jump-btn:hover::before { left: 100%; } .statistic-gmgn-address-jump-btn:active { transform: translateY(0) scale(1); } .statistic-gmgn-profit-positive { color: #00ff88 !important; } .statistic-gmgn-profit-negative { color: #ff4444 !important; } .statistic-gmgn-empty-message { text-align: center; color: #ccc; padding: 20px; margin: 0; } .statistic-gmgn-stats-info { text-align: center !important; margin-bottom: 15px !important; padding: 10px !important; background: rgba(0, 119, 255, 0.1) !important; border-radius: 8px !important; border: 1px solid rgba(0, 119, 255, 0.3) !important; color: #fff !important; font-size: 14px !important; } .statistic-gmgn-export-btn { background: linear-gradient(135deg, #10b981, #059669) !important; color: white !important; border: 1px solid rgba(16, 185, 129, 0.3) !important; padding: 12px 20px !important; border-radius: 12px !important; font-size: 13px !important; font-weight: 600 !important; cursor: pointer !important; transition: background-color 0.2s ease !important; /* 简化过渡,提升性能 */ display: flex !important; align-items: center !important; gap: 8px !important; position: relative !important; overflow: hidden !important; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2) !important; } .statistic-gmgn-export-btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.5s ease; } .statistic-gmgn-export-btn:hover { background: linear-gradient(135deg, #059669, #047857) !important; transform: translateY(-2px) !important; box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4) !important; border-color: #10b981 !important; } .statistic-gmgn-export-btn:hover::before { left: 100% !important; } .statistic-gmgn-export-btn:active { transform: translateY(0) !important; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3) !important; } /* 移除动画关键帧,直接显示弹出框 */ /* 响应式设计优化 */ @media (max-width: 768px) { .statistic-gmgn-modal-content { width: 95% !important; padding: 16px !important; margin: 10px !important; } .statistic-gmgn-modal-header { padding: 12px 16px !important; margin: -16px -16px 16px -16px !important; } .statistic-gmgn-summary-stats { gap: 16px; flex-wrap: wrap; } .statistic-gmgn-stat-item { min-width: 60px; padding: 6px 8px; } .statistic-gmgn-result-address { font-size: 11px; padding: 6px 8px; } } /* 自定义滚动条 */ .statistic-gmgn-modal-content::-webkit-scrollbar { width: 8px; } .statistic-gmgn-modal-content::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); border-radius: 4px; } .statistic-gmgn-modal-content::-webkit-scrollbar-thumb { background: #3b82f6; /* 简化为纯色,提升性能 */ border-radius: 4px; } .statistic-gmgn-modal-content::-webkit-scrollbar-thumb:hover { background: #2563eb; /* 简化为纯色,提升性能 */ } /* 加载状态动画 */ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .statistic-gmgn-loading { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } /* 分页控制样式 */ .statistic-gmgn-pagination-info { background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(139, 92, 246, 0.1)); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px; padding: 8px 12px; margin-bottom: 16px; text-align: center; } .statistic-pagination-text { color: #3b82f6; font-size: 12px; font-weight: 500; } .statistic-gmgn-pagination-controls { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; margin: 16px -24px -24px -24px; background: linear-gradient(135deg, rgba(30, 41, 59, 0.8), rgba(15, 23, 42, 0.9)); border-radius: 0 0 16px 16px; border-top: 1px solid rgba(255, 255, 255, 0.1); } .statistic-pagination-btn { background: #3b82f6; /* 简化为纯色,提升性能 */ color: white; border: none; padding: 8px 16px; border-radius: 8px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; transform: translateZ(0); } .statistic-pagination-btn:hover:not(:disabled) { background: #2563eb; /* 简化为纯色,提升性能 */ transform: translateY(-1px); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); } .statistic-pagination-btn:disabled { background: rgba(148, 163, 184, 0.3); color: rgba(148, 163, 184, 0.6); cursor: not-allowed; transform: none; } .statistic-pagination-current { color: #e2e8f0; font-size: 13px; font-weight: 500; } /* 可疑地址类型标识样式 */ .statistic-suspicious-labels { display: inline-flex; gap: 6px; flex-wrap: wrap; margin-left: 12px; align-items: center; } .statistic-suspicious-label { font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 10px; border: 1px solid; white-space: nowrap; display: inline-flex; align-items: center; gap: 3px; text-transform: uppercase; letter-spacing: 0.3px; transition: all 0.2s ease; cursor: default; } .statistic-suspicious-label:hover { transform: scale(1.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .statistic-suspicious-label::before { content: '⚠'; font-size: 8px; } .statistic-suspicious-label.rat-trader::before { content: '🐭'; } .statistic-suspicious-label.transfer-in::before { content: '⬇'; } .statistic-suspicious-label.bundler::before { content: '📦'; } /* 现代化详情数据样式 */ .statistic-detail-grid-modern { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; padding: 8px 0; } .statistic-detail-item { display: flex; align-items: center; background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 8px 10px; transition: all 0.3s ease; position: relative; overflow: hidden; min-height: 48px; } .statistic-detail-item::before { content: ''; position: absolute; top: 0; left: 0; width: 3px; height: 100%; background: linear-gradient(180deg, #3b82f6, #8b5cf6); opacity: 0; transition: opacity 0.3s ease; } .statistic-detail-item:hover { background: rgba(255, 255, 255, 0.05); border-color: rgba(59, 130, 246, 0.3); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .statistic-detail-item:hover::before { opacity: 1; } .statistic-detail-highlight { background: rgba(59, 130, 246, 0.08) !important; border-color: rgba(59, 130, 246, 0.2) !important; } .statistic-detail-highlight::before { opacity: 1 !important; } .statistic-detail-icon { font-size: 16px; margin-right: 8px; min-width: 20px; text-align: center; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); } .statistic-detail-content { flex: 1; min-width: 0; } .statistic-detail-label { font-size: 10px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; margin-bottom: 2px; line-height: 1; } .statistic-detail-value { font-size: 13px; color: #e2e8f0; font-weight: 600; line-height: 1.2; word-break: break-all; } .statistic-detail-value.profit-positive { color: #10b981; } .statistic-detail-value.profit-negative { color: #ef4444; } .statistic-detail-value.highlight { color: #60a5fa; } .statistic-detail-value.warning { color: #f59e0b; } /* 下载按钮样式 - 与其他数字保持一致 */ .statistic-download-btn { color:rgb(243, 243, 243) !important; font-weight: normal !important; cursor: pointer !important; /* 继承其他数字的基础样式 */ } .statistic-download-btn:hover { background-color: rgba(0, 255, 0, 0.1) !important; border-radius: 3px !important; transform: scale(1.03) !important; } .statistic-download-btn.disabled { color: rgba(135, 135, 135, 0.73) !important; cursor: not-allowed !important; pointer-events: none !important; } .statistic-download-btn.disabled:hover { background-color: transparent !important; transform: none !important; } /* 图片预览模态框样式 */ .image-preview-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 10000; /* backdrop-filter: blur(5px); */ /* 移除性能杀手 */ } .image-preview-content { background: #1a1a1a; border-radius: 12px; padding: 20px; max-width: 90vw; max-height: 90vh; display: flex; flex-direction: column; align-items: center; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); } .image-preview-header { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-bottom: 15px; } .image-preview-title { color: #ffffff; font-size: 18px; font-weight: bold; } .image-preview-close { background: none; border: none; color: #ffffff; font-size: 24px; cursor: pointer; padding: 5px; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; transition: background 0.2s ease; } .image-preview-close:hover { background: rgba(255, 255, 255, 0.1); } .image-preview-img { max-width: 100%; max-height: 60vh; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .image-preview-buttons { display: flex; gap: 12px; } .image-preview-btn { padding: 10px 20px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: all 0.2s ease; color: #ffffff; } .image-preview-btn.copy-btn { background: #10b981; /* 简化为纯色,提升性能 */ } .image-preview-btn.copy-btn:hover { background: #059669; /* 简化为纯色,提升性能 */ transform: translateY(-1px); } .image-preview-btn.download-btn { background: linear-gradient(135deg, #3b82f6, #1d4ed8); } .image-preview-btn.download-btn:hover { background: linear-gradient(135deg, #1d4ed8, #1e40af); transform: translateY(-1px); } /* 现代化提示框样式 */ .modern-toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 20000; background: rgba(0, 0, 0, 0.9); /* backdrop-filter: blur(10px); */ /* 移除性能杀手 */ border-radius: 16px; padding: 0; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1); min-width: 300px; max-width: 400px; /* 移除toast动画,直接显示 */ cursor: pointer; } .modern-toast-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 19999; background: rgba(0, 0, 0, 0.3); /* backdrop-filter: blur(3px); */ /* 移除性能杀手 */ /* animation: overlayFadeIn 0.3s ease forwards; */ /* 移除动画,提升性能 */ } .modern-toast-content { display: flex; align-items: center; padding: 20px 24px; gap: 16px; } .modern-toast-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; flex-shrink: 0; } .modern-toast-icon.success { background: #10b981; /* 简化为纯色,提升性能 */ color: #ffffff; } .modern-toast-icon.error { background: linear-gradient(135deg, #ef4444, #dc2626); color: #ffffff; } .modern-toast-icon.info { background: linear-gradient(135deg, #3b82f6, #1d4ed8); color: #ffffff; } .modern-toast-text { flex: 1; color: #ffffff; font-size: 16px; font-weight: 500; line-height: 1.4; } .modern-toast-close { width: 32px; height: 32px; border-radius: 50%; background: rgba(255, 255, 255, 0.1); border: none; color: #ffffff; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; transition: all 0.2s ease; flex-shrink: 0; } .modern-toast-close:hover { background: rgba(255, 255, 255, 0.2); transform: scale(1.1); } /* 移除所有toast动画关键帧,提升性能 */ `; document.head.appendChild(style); // 存储拦截到的数据 let interceptedData = null; // 存储首次加载的数据 let initialStats = null; // 标记是否是首次加载 let isFirstLoad = true; // 新增存储当前CA地址 let currentCaAddress = null; // 存储首次加载的CA地址 let initialCaAddress = null; // 性能优化:添加缓存机制 let dataCache = { lastDataHash: null, calculatedStats: null, filteredResults: new Map(), eventsInitialized: false }; // 防抖函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 检查当前网络是否为SOL function isSolNetwork() { const url = window.location.href; return url.includes('/sol/') || url.includes('gmgn.ai/sol'); } // 获取可疑地址的具体类型标识 function getSuspiciousTypeLabels(holder) { const labels = []; // 基础可疑标记 if (holder.is_suspicious) { labels.push({ text: '可疑', color: '#dc2626', bgColor: 'rgba(220, 38, 38, 0.15)', borderColor: 'rgba(220, 38, 38, 0.3)' }); } // 检查maker_token_tags if (holder.maker_token_tags) { if (holder.maker_token_tags.includes('rat_trader')) { labels.push({ text: '老鼠仓', color: '#ef4444', bgColor: 'rgba(239, 68, 68, 0.15)', borderColor: 'rgba(239, 68, 68, 0.3)' }); } if (holder.transfer_in) { labels.push({ text: '小鱼钱包', color: '#f87171', bgColor: 'rgba(248, 113, 113, 0.15)', borderColor: 'rgba(248, 113, 113, 0.3)' }); } if (holder.maker_token_tags.includes('bundler')) { labels.push({ text: '捆绑交易', color: '#b91c1c', bgColor: 'rgba(185, 28, 28, 0.15)', borderColor: 'rgba(185, 28, 28, 0.3)' }); } } return labels; } // 生成现代化详情数据HTML function generateDetailItemHTML(icon, label, value, valueClass = '', isHighlight = false) { const highlightClass = isHighlight ? 'statistic-detail-highlight' : ''; return ` <div class="statistic-detail-item ${highlightClass}"> <div class="statistic-detail-icon">${icon}</div> <div class="statistic-detail-content"> <div class="statistic-detail-label">${label}</div> <div class="statistic-detail-value ${valueClass}">${value}</div> </div> </div> `; } // 生成可疑标识HTML function generateSuspiciousLabelsHTML(labels) { if (!labels || labels.length === 0) { return ''; } const labelsHTML = labels.map(label => { const typeClass = label.text === '老鼠仓' ? 'rat-trader' : label.text === '小鱼钱包' ? 'transfer-in' : label.text === '捆绑交易' ? 'bundler' : ''; return `<span class="statistic-suspicious-label ${typeClass}" style="color: ${label.color}; background: ${label.bgColor}; border-color: ${label.borderColor};" title="${label.text}标识"> ${label.text} </span>`; }).join(''); return `<div class="statistic-suspicious-labels">${labelsHTML}</div>`; } // 检查是否为交易所地址 function isExchangeAddress(holder) { const exchangeNames = ['coinbase', 'binance', 'bybit', 'bitget', 'okx', 'kraken', 'coinsquare', 'crypto.com', 'robinhood', 'mexc']; // 检查native_transfer中的name if (holder.native_transfer && holder.native_transfer.name) { const name = holder.native_transfer.name.toLowerCase(); if (exchangeNames.some(exchange => name.includes(exchange))) { return true; } } // 检查其他可能的transfer字段 if (holder.transfer && holder.transfer.name) { const name = holder.transfer.name.toLowerCase(); if (exchangeNames.some(exchange => name.includes(exchange))) { return true; } } return false; } // 获取交易所名称 function getExchangeName(holder) { const exchangeNames = ['coinbase', 'binance', 'bybit', 'bitget', 'okx', 'kraken', 'coinsquare', 'crypto.com', 'robinhood', 'mexc']; let sourceName = ''; if (holder.native_transfer && holder.native_transfer.name) { sourceName = holder.native_transfer.name.toLowerCase(); } else if (holder.transfer && holder.transfer.name) { sourceName = holder.transfer.name.toLowerCase(); } for (let exchange of exchangeNames) { if (sourceName.includes(exchange)) { return exchange.charAt(0).toUpperCase() + exchange.slice(1); } } return 'Unknown'; } // 交易所专用弹框 function createExchangeModal(data, caAddress) { // 移除已存在的弹框 const existingModal = document.querySelector('.statistic-gmgn-modal'); if (existingModal) { existingModal.remove(); } // 按交易所分组数据 const exchangeGroups = {}; data.forEach(holder => { const exchangeName = getExchangeName(holder); if (!exchangeGroups[exchangeName]) { exchangeGroups[exchangeName] = []; } exchangeGroups[exchangeName].push(holder); }); // 计算已卖筹码地址数 const soldAddressCount = data.filter(holder => (holder.sell_amount_percentage || 0) > 0).length; // 计算总持仓占比 const totalHoldingPercentage = data.reduce((sum, holder) => { return sum + (holder.amount_percentage || 0); }, 0); // 创建弹框 const modal = document.createElement('div'); modal.className = 'statistic-gmgn-modal'; // 生成交易所统计数据 const exchangeSummary = Object.keys(exchangeGroups).map(exchange => { return { name: exchange, count: exchangeGroups[exchange].length, addresses: exchangeGroups[exchange] }; }).sort((a, b) => b.count - a.count); modal.innerHTML = ` <div class="statistic-gmgn-modal-content"> <div class="statistic-gmgn-modal-header"> <div class="statistic-gmgn-modal-title">🚀 交易所地址分析 (共${data.length}个地址)</div> <button class="statistic-gmgn-modal-close">×</button> </div> <div class="statistic-gmgn-analysis-summary"> <div class="statistic-gmgn-summary-stats"> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">已卖筹码地址数:</span> <span class="statistic-gmgn-stat-value">${soldAddressCount}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">交易所数:</span> <span class="statistic-gmgn-stat-value">${Object.keys(exchangeGroups).length}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">总持仓占比:</span> <span class="statistic-gmgn-stat-value">${(totalHoldingPercentage * 100).toFixed(2)}%</span> </div> </div> <button id="statistic-export-exchange-btn" class="statistic-gmgn-export-btn" title="导出Excel">导出Excel</button> </div> <div id="statistic-exchange-summary"> <div class="statistic-gmgn-section-title">📱 交易所统计</div> <div class="statistic-exchange-summary-grid"> ${exchangeSummary.map(item => ` <div class="statistic-exchange-summary-item" data-exchange="${item.name}"> <span class="statistic-exchange-name">${item.name}</span> <span class="statistic-exchange-count">${item.count}个地址</span> </div> `).join('')} </div> </div> <div id="statistic-exchange-details"></div> </div> `; document.body.appendChild(modal); // 添加交易所统计样式 if (!document.getElementById('exchange-summary-styles')) { const summaryStyles = document.createElement('style'); summaryStyles.id = 'exchange-summary-styles'; summaryStyles.textContent = ` .statistic-exchange-summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; } .statistic-exchange-summary-item { background-color: #475569; border-radius: 8px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; } .statistic-exchange-summary-item:hover { background-color: #64748b; border-color: #3b82f6; transform: translateY(-2px); } .statistic-exchange-summary-item.active { background-color: #3b82f6; border-color: #1d4ed8; } .statistic-exchange-name { font-weight: 600; color: #e2e8f0; font-size: 14px; } .statistic-exchange-count { color: #10b981; font-weight: 600; font-size: 13px; } .statistic-exchange-details-section { margin-bottom: 20px; } .statistic-exchange-section-header { background-color: #1e293b; padding: 12px 16px; border-radius: 8px 8px 0 0; border-left: 4px solid #3b82f6; margin-bottom: 0; } .statistic-exchange-section-title { font-size: 16px; font-weight: 600; color: #3b82f6; margin: 0; } .statistic-exchange-section-count { font-size: 12px; color: #94a3b8; margin-top: 4px; } `; document.head.appendChild(summaryStyles); } // 绑定交易所统计点击事件 exchangeSummary.forEach(item => { const summaryItem = modal.querySelector(`[data-exchange="${item.name}"]`); if (summaryItem) { summaryItem.addEventListener('click', () => { // 移除所有活跃状态 modal.querySelectorAll('.statistic-exchange-summary-item').forEach(el => { el.classList.remove('active'); }); // 添加当前活跃状态 summaryItem.classList.add('active'); // 显示该交易所的详细信息 displayExchangeDetails(item.addresses, item.name, modal); }); } }); // ESC键关闭处理函数 const escKeyHandler = (e) => { if (e.key === 'Escape') { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }; document.addEventListener('keydown', escKeyHandler); // 绑定导出Excel按钮事件 const exportBtn = modal.querySelector('#statistic-export-exchange-btn'); if (exportBtn) { exportBtn.addEventListener('click', () => { exportExchangeToExcel(exchangeGroups, caAddress); }); } // 绑定关闭按钮事件 modal.querySelector('.statistic-gmgn-modal-close').addEventListener('click', () => { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); }); // 点击模态框外部关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }); // 默认显示第一个交易所的详情 if (exchangeSummary.length > 0) { const firstItem = modal.querySelector(`[data-exchange="${exchangeSummary[0].name}"]`); if (firstItem) { firstItem.click(); } } } // 显示交易所详细信息 function displayExchangeDetails(addresses, exchangeName, modal) { const detailsContainer = modal.querySelector('#statistic-exchange-details'); // 创建全局排名映射 - 基于原始完整数据按持仓比例排序 const globalRankMap = new Map(); if (interceptedData?.data?.list) { const allHolders = [...interceptedData.data.list]; allHolders .sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)) .forEach((holder, index) => { globalRankMap.set(holder.address, index + 1); }); } // 按持仓比例排序 const sortedAddresses = addresses.sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)); detailsContainer.innerHTML = ` <div class="statistic-exchange-details-section"> <div class="statistic-exchange-section-header"> <div class="statistic-exchange-section-title">${exchangeName} 地址详情</div> <div class="statistic-exchange-section-count">共 ${sortedAddresses.length} 个地址</div> </div> ${sortedAddresses.map((holder, index) => { const globalRank = globalRankMap.get(holder.address) || (index + 1); const processedData = { rank: index + 1, rankIndex: globalRank, // 使用全局排名 address: holder.address, balance: formatNumber(holder.balance), usdValue: formatNumber(holder.usd_value), netflowUsd: formatNumber(holder.netflow_usd), netflowClass: (holder.netflow_usd || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profit: formatNumber(holder.profit), profitSign: holder.profit >= 0 ? '+' : '', profitClass: holder.profit >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profitChange: holder.profit_change ? (holder.profit_change * 100).toFixed(1) + '%' : 'N/A', profitChangeClass: (holder.profit_change || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', exchangeName: getExchangeName(holder), transferName: (holder.native_transfer && holder.native_transfer.name) || (holder.transfer && holder.transfer.name) || 'N/A', amountPercentage: holder.amount_percentage ? (holder.amount_percentage * 100).toFixed(2) + '%' : 'N/A', sellPercentage: holder.sell_amount_percentage ? (holder.sell_amount_percentage * 100).toFixed(2) + '%' : '0.00%' // 筹码已卖 }; return ` <div class="statistic-gmgn-result-item"> <div class="statistic-gmgn-result-header"> <div class="statistic-gmgn-result-rank"> <span style="color: #ff6b35; font-weight: bold; background: rgba(255, 107, 53, 0.15); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 107, 53, 0.3); font-size: 12px;">榜${processedData.rankIndex}</span> </div> <div class="statistic-gmgn-result-address" title="点击复制地址" onclick="navigator.clipboard.writeText('${processedData.address}'); this.style.backgroundColor='#16a34a'; this.style.color='white'; setTimeout(() => { this.style.backgroundColor=''; this.style.color=''; }, 1000);">${processedData.address}</div> <a href="https://gmgn.ai/sol/address/${processedData.address}" target="_blank" class="statistic-gmgn-address-jump-btn" title="查看钱包详情">详情</a> </div> <div class="statistic-gmgn-compact-details"> <div class="statistic-gmgn-detail-section"> <div class="statistic-gmgn-section-title">基本信息</div> <div class="statistic-detail-grid-modern"> ${generateDetailItemHTML('💎', '持仓', processedData.balance)} ${generateDetailItemHTML('✨', '持仓占比', processedData.amountPercentage, 'highlight', true)} ${generateDetailItemHTML('📉', '筹码已卖', processedData.sellPercentage, processedData.sellPercentage === '0.00%' ? 'profit-positive' : 'warning')} ${generateDetailItemHTML('💰', '净流入', '$' + processedData.netflowUsd, processedData.netflowClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('📈', '盈亏', processedData.profitSign + '$' + processedData.profit, processedData.profitClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('🚀', '倍数', processedData.profitChange, processedData.profitChangeClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('🏢', '交易所', processedData.exchangeName, 'highlight', true)} ${generateDetailItemHTML('🏷️', '标签', processedData.transferName)} </div> </div> </div> </div> `; }).join('')} </div> `; } // 交易所数据导出函数 function exportExchangeToExcel(exchangeGroups, caAddress) { try { const worksheetData = []; // 添加标题行 worksheetData.push(['交易所', '排名', '地址', '持仓数量', '持仓比例', '筹码已卖', 'USD价值', '净流入USD', '盈亏USD', '盈亏倍数', '标签名称']); // 按交易所排序添加数据 Object.keys(exchangeGroups).forEach(exchangeName => { const addresses = exchangeGroups[exchangeName].sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)); addresses.forEach((holder, index) => { const row = [ exchangeName, index + 1, holder.address, formatNumber(holder.balance), holder.amount_percentage ? (holder.amount_percentage * 100).toFixed(2) + '%' : 'N/A', holder.sell_amount_percentage ? (holder.sell_amount_percentage * 100).toFixed(2) + '%' : '0.00%', formatNumber(holder.usd_value), formatNumber(holder.netflow_usd), (holder.profit >= 0 ? '+' : '') + formatNumber(holder.profit), holder.profit_change ? (holder.profit_change * 100).toFixed(1) + '%' : 'N/A', (holder.native_transfer && holder.native_transfer.name) || (holder.transfer && holder.transfer.name) || 'N/A' ]; worksheetData.push(row); }); }); // 创建工作簿 const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet(worksheetData); // 设置列宽 const colWidths = [ {wch: 12}, // 交易所 {wch: 6}, // 排名 {wch: 45}, // 地址 {wch: 15}, // 持仓数量 {wch: 10}, // 持仓比例 {wch: 10}, // 已卖比例 {wch: 15}, // USD价值 {wch: 15}, // 净流入 {wch: 15}, // 盈亏 {wch: 12}, // 倍数 {wch: 25} // 标签名称 ]; ws['!cols'] = colWidths; // 添加工作表到工作簿 XLSX.utils.book_append_sheet(wb, ws, '交易所地址'); // 生成文件名 const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const fileName = `交易所地址_${caAddress ? caAddress.slice(0, 8) : 'data'}_${timestamp}.xlsx`; // 下载文件 XLSX.writeFile(wb, fileName); // 显示成功提示 const exportBtn = document.querySelector('#statistic-export-exchange-btn'); if (exportBtn) { const originalText = exportBtn.textContent; exportBtn.textContent = '✅ 导出成功'; exportBtn.style.backgroundColor = '#059669'; setTimeout(() => { exportBtn.textContent = originalText; exportBtn.style.backgroundColor = ''; }, 2000); } } catch (error) { console.error('Excel导出失败:', error); showModernToast('导出失败,请检查浏览器控制台了解详情', 'error'); } } // 优化后的弹框管理函数 - 添加分页支持 function createModal(title, data, caAddress, showSolBalance = false) { // 移除已存在的弹框 const existingModal = document.querySelector('.statistic-gmgn-modal'); if (existingModal) { existingModal.remove(); } // 性能优化:数据量限制 const ITEMS_PER_PAGE = 50; const isLargeDataset = data.length > ITEMS_PER_PAGE; let currentPage = 1; let totalPages = Math.ceil(data.length / ITEMS_PER_PAGE); // 1. 数据预处理 - 首先获取全局排名 if (!interceptedData?.data?.list) { console.error('无法获取原始数据进行全局排名'); return; } // 创建全局排名映射 - 基于原始完整数据按持仓比例排序 const globalRankMap = new Map(); const allHolders = [...interceptedData.data.list]; allHolders .sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)) .forEach((holder, index) => { globalRankMap.set(holder.address, index + 1); }); // 2. 计算已卖筹码地址数 const soldAddressCount = data.filter(holder => (holder.sell_amount_percentage || 0) > 0).length; // 计算总持仓占比 const totalHoldingPercentage = data.reduce((sum, holder) => { return sum + (holder.amount_percentage || 0); }, 0); // 3. 处理所有数据并排序 const allProcessedData = data .sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)) // 按持仓比例排序 .map((holder, index) => { const globalRank = globalRankMap.get(holder.address) || (index + 1); const baseData = { rank: index + 1, // 在当前数据集中的排名(用于显示序号) rankIndex: globalRank, // 在全局数据中的排名(用于显示"榜X") address: holder.address, balance: formatNumber(holder.balance), usdValue: formatNumber(holder.usd_value), netflowUsd: formatNumber(holder.netflow_usd), netflowClass: (holder.netflow_usd || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profit: formatNumber(holder.profit), profitSign: holder.profit >= 0 ? '+' : '', profitClass: holder.profit >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profitChange: holder.profit_change ? (holder.profit_change * 100).toFixed(1) + '%' : 'N/A', profitChangeClass: (holder.profit_change || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', amountPercentage: holder.amount_percentage ? (holder.amount_percentage * 100).toFixed(2) + '%' : 'N/A', sellPercentage: holder.sell_amount_percentage ? (holder.sell_amount_percentage * 100).toFixed(2) + '%' : '0.00%', // 筹码已卖 // 添加可疑类型标识 suspiciousLabels: getSuspiciousTypeLabels(holder), // 保留原始数据用于检测 originalHolder: holder }; // 只有在需要显示SOL余额时才添加 if (showSolBalance) { baseData.solBalance = holder.native_balance ? ((holder.native_balance / 1000000000).toFixed(2) + ' SOL') : 'N/A'; } return baseData; }); // 分页处理:获取当前页数据 function getCurrentPageData(page = 1) { const start = (page - 1) * ITEMS_PER_PAGE; const end = start + ITEMS_PER_PAGE; return allProcessedData.slice(start, end); } const processedData = getCurrentPageData(currentPage); // 2. 创建弹框基础结构 - 使用token_holding_temp.js的DOM结构 const modal = document.createElement('div'); modal.className = 'statistic-gmgn-modal'; modal.innerHTML = ` <div class="statistic-gmgn-modal-content"> <div class="statistic-gmgn-modal-header"> <div class="statistic-gmgn-modal-title">💎 ${title} (${allProcessedData.length}个地址)</div> <button class="statistic-gmgn-modal-close">×</button> </div> ${isLargeDataset ? ` <div class="statistic-gmgn-pagination-info"> <span class="statistic-pagination-text">⚡ 性能优化:分页显示 | 第${currentPage}页,共${totalPages}页 | 每页${ITEMS_PER_PAGE}条</span> </div> ` : ''} <div class="statistic-gmgn-analysis-summary"> <div class="statistic-gmgn-summary-stats"> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">已卖筹码地址数:</span> <span class="statistic-gmgn-stat-value">${soldAddressCount}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">总数量:</span> <span class="statistic-gmgn-stat-value">${allProcessedData.length}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">总持仓占比:</span> <span class="statistic-gmgn-stat-value">${(totalHoldingPercentage * 100).toFixed(2)}%</span> </div> </div> <button id="statistic-export-excel-btn" class="statistic-gmgn-export-btn" title="导出Excel">导出Excel</button> </div> <div id="statistic-gmgn-results-list"></div> ${isLargeDataset ? ` <div class="statistic-gmgn-pagination-controls"> <button id="statistic-prev-page" class="statistic-pagination-btn" ${currentPage === 1 ? 'disabled' : ''}>← 上一页</button> <span class="statistic-pagination-current">第 ${currentPage} 页 / 共 ${totalPages} 页</span> <button id="statistic-next-page" class="statistic-pagination-btn" ${currentPage === totalPages ? 'disabled' : ''}>下一页 →</button> </div> ` : ''} </div> `; // 3. 插入DOM document.body.appendChild(modal); // 4. 填充结果列表 - 参考token_holding_temp.js的方式 const resultsList = document.getElementById('statistic-gmgn-results-list'); processedData.forEach((holder, index) => { const item = document.createElement('div'); item.className = 'statistic-gmgn-result-item'; item.innerHTML = ` <div class="statistic-gmgn-result-header"> <div class="statistic-gmgn-result-rank"> <span style="color: #ff6b35; font-weight: bold; background: rgba(255, 107, 53, 0.15); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 107, 53, 0.3); font-size: 12px;">榜${holder.rankIndex}</span> </div> <div class="statistic-gmgn-result-address" title="点击复制地址">${holder.address}</div> <a href="https://gmgn.ai/sol/address/${holder.address}" target="_blank" class="statistic-gmgn-address-jump-btn" title="查看钱包详情">详情</a> </div> <div class="statistic-gmgn-compact-details"> <div class="statistic-gmgn-detail-section"> <div class="statistic-gmgn-section-title"> 基本信息 ${generateSuspiciousLabelsHTML(holder.suspiciousLabels)} </div> <div class="statistic-detail-grid-modern"> ${generateDetailItemHTML('💎', '持仓', holder.balance)} ${generateDetailItemHTML('✨', '持仓占比', holder.amountPercentage, 'highlight', true)} ${generateDetailItemHTML('📉', '筹码已卖', holder.sellPercentage, holder.sellPercentage === '0.00%' ? 'profit-positive' : 'warning')} ${generateDetailItemHTML('💰', '净流入', '$' + holder.netflowUsd, holder.netflowClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('📈', '盈亏', holder.profitSign + '$' + holder.profit, holder.profitClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('🚀', '倍数', holder.profitChange, holder.profitChangeClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${holder.solBalance ? generateDetailItemHTML('⭐', 'SOL餘額', holder.solBalance, 'highlight') : ''} </div> </div> </div> `; // 添加地址复制功能 const addressElement = item.querySelector('.statistic-gmgn-result-address'); addressElement.addEventListener('click', () => { navigator.clipboard.writeText(holder.address).then(() => { addressElement.style.backgroundColor = '#16a34a'; addressElement.style.color = 'white'; setTimeout(() => { addressElement.style.backgroundColor = ''; addressElement.style.color = ''; }, 1000); }); }); resultsList.appendChild(item); }); // ESC键关闭处理函数 const escKeyHandler = (e) => { if (e.key === 'Escape') { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }; document.addEventListener('keydown', escKeyHandler); // 5. 绑定导出Excel按钮事件 - 导出完整数据而非分页数据 const exportBtn = modal.querySelector('#statistic-export-excel-btn'); if (exportBtn) { exportBtn.addEventListener('click', () => { exportToExcel(allProcessedData, title, caAddress, showSolBalance); }); } // 6. 分页控制逻辑 if (isLargeDataset) { // 渲染指定页面的数据 function renderPage(page) { currentPage = page; const currentPageData = getCurrentPageData(page); // 清空当前列表 const resultsList = document.getElementById('statistic-gmgn-results-list'); resultsList.innerHTML = ''; // 重新渲染当前页数据 currentPageData.forEach((holder, index) => { const item = document.createElement('div'); item.className = 'statistic-gmgn-result-item'; item.innerHTML = ` <div class="statistic-gmgn-result-header"> <div class="statistic-gmgn-result-rank"> <span style="color: #ff6b35; font-weight: bold; background: rgba(255, 107, 53, 0.15); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 107, 53, 0.3); font-size: 12px;">榜${holder.rankIndex}</span> </div> <div class="statistic-gmgn-result-address" title="点击复制地址">${holder.address}</div> <a href="https://gmgn.ai/sol/address/${holder.address}" target="_blank" class="statistic-gmgn-address-jump-btn" title="查看钱包详情">详情</a> </div> <div class="statistic-gmgn-compact-details"> <div class="statistic-gmgn-detail-section"> <div class="statistic-gmgn-section-title"> 基本信息 ${generateSuspiciousLabelsHTML(holder.suspiciousLabels)} </div> <div class="statistic-detail-grid-modern"> ${generateDetailItemHTML('💎', '持仓', holder.balance)} ${generateDetailItemHTML('✨', '持仓占比', holder.amountPercentage, 'highlight', true)} ${generateDetailItemHTML('📉', '筹码已卖', holder.sellPercentage, holder.sellPercentage === '0.00%' ? 'profit-positive' : 'warning')} ${generateDetailItemHTML('💰', '净流入', '$' + holder.netflowUsd, holder.netflowClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('📈', '盈亏', holder.profitSign + '$' + holder.profit, holder.profitClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${generateDetailItemHTML('🚀', '倍数', holder.profitChange, holder.profitChangeClass.includes('positive') ? 'profit-positive' : 'profit-negative')} ${holder.solBalance ? generateDetailItemHTML('⭐', 'SOL餘額', holder.solBalance, 'highlight') : ''} </div> </div> </div> `; // 添加地址复制功能 const addressElement = item.querySelector('.statistic-gmgn-result-address'); addressElement.addEventListener('click', () => { navigator.clipboard.writeText(holder.address).then(() => { addressElement.style.backgroundColor = '#16a34a'; addressElement.style.color = 'white'; setTimeout(() => { addressElement.style.backgroundColor = ''; addressElement.style.color = ''; }, 1000); }); }); resultsList.appendChild(item); }); // 更新分页按钮状态 const prevBtn = modal.querySelector('#statistic-prev-page'); const nextBtn = modal.querySelector('#statistic-next-page'); const currentSpan = modal.querySelector('.statistic-pagination-current'); if (prevBtn) { prevBtn.disabled = (page === 1); } if (nextBtn) { nextBtn.disabled = (page === totalPages); } if (currentSpan) { currentSpan.textContent = `第 ${page} 页 / 共 ${totalPages} 页`; } } // 绑定分页按钮事件 const prevBtn = modal.querySelector('#statistic-prev-page'); const nextBtn = modal.querySelector('#statistic-next-page'); if (prevBtn) { prevBtn.addEventListener('click', () => { if (currentPage > 1) { renderPage(currentPage - 1); } }); } if (nextBtn) { nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { renderPage(currentPage + 1); } }); } } // 7. 绑定关闭按钮事件 modal.querySelector('.statistic-gmgn-modal-close').addEventListener('click', () => { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); }); // 点击模态框外部关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }); } // 数字格式化函数 function formatNumber(num) { if (num === null || num === undefined) return 'N/A'; // 處理負數:保留負號,對絕對值進行格式化 const isNegative = num < 0; const absNum = Math.abs(num); let formatted; if (absNum >= 1000000000) { formatted = (absNum / 1000000000).toFixed(2) + 'B'; } else if (absNum >= 1000000) { formatted = (absNum / 1000000).toFixed(2) + 'M'; } else if (absNum >= 1000) { formatted = (absNum / 1000).toFixed(2) + 'K'; } else { formatted = absNum.toFixed(2); } return isNegative ? '-' + formatted : formatted; } // Excel导出功能 function exportToExcel(data, title, caAddress, showSolBalance) { try { // 创建工作表数据 const worksheetData = []; // 添加标题行 const headers = ['排名', '地址', '持仓数量', '持仓占比', '筹码已卖', 'USD价值', '净流入USD', '盈亏USD', '盈亏倍数']; if (showSolBalance) { headers.push('SOL餘額'); } worksheetData.push(headers); // 添加数据行 data.forEach((holder, index) => { const row = [ holder.rank, holder.address, holder.balance, holder.amountPercentage, holder.sellPercentage, holder.usdValue, holder.netflowUsd, (holder.profitSign || '') + holder.profit, holder.profitChange ]; if (showSolBalance) { row.push(holder.solBalance || 'N/A'); } worksheetData.push(row); }); // 创建工作簿 const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet(worksheetData); // 设置列宽 const colWidths = [ {wch: 6}, // 排名 {wch: 45}, // 地址 {wch: 15}, // 持仓数量 {wch: 10}, // 持仓比例 {wch: 10}, // 已卖比例 {wch: 15}, // USD价值 {wch: 15}, // 净流入 {wch: 15}, // 盈亏 {wch: 12} // 倍数 ]; if (showSolBalance) { colWidths.push({wch: 12}); // SOL餘額 } ws['!cols'] = colWidths; // 添加工作表到工作簿 XLSX.utils.book_append_sheet(wb, ws, title); // 生成文件名 const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const fileName = `${title}_${caAddress ? caAddress.slice(0, 8) : 'data'}_${timestamp}.xlsx`; // 下载文件 XLSX.writeFile(wb, fileName); // 显示成功提示 const exportBtn = document.querySelector('#statistic-export-excel-btn'); if (exportBtn) { const originalText = exportBtn.textContent; exportBtn.textContent = '✅ 导出成功'; exportBtn.style.backgroundColor = '#059669'; setTimeout(() => { exportBtn.textContent = originalText; exportBtn.style.backgroundColor = ''; }, 2000); } } catch (error) { console.error('Excel导出失败:', error); showModernToast('导出失败,请检查浏览器控制台了解详情', 'error'); } } // 根据类型获取对应的地址数据(优化版本) function getAddressByType(type) { if (!interceptedData?.data?.list) return []; // 检查缓存 const currentHash = getDataHash(interceptedData); const cacheKey = `${type}_${currentHash}`; if (dataCache.filteredResults.has(cacheKey)) { console.log('[性能优化] 使用缓存的过滤结果:', type); return dataCache.filteredResults.get(cacheKey); } console.log('[性能优化] 重新过滤数据:', type); const currentTime = Math.floor(Date.now() / 1000); const sevenDaysInSeconds = 7 * 24 * 60 * 60; const holders = interceptedData.data.list; let result; switch(type) { case 'fullPosition': result = holders.filter(h => h.sell_amount_percentage === 0 && (!h.token_transfer_out || !h.token_transfer_out.address) ); break; case 'profitable': result = holders.filter(h => h.profit > 0); break; case 'losing': result = holders.filter(h => h.profit < 0); break; case 'active24h': result = holders.filter(h => h.last_active_timestamp > currentTime - 86400); break; case 'diamondHands': result = holders.filter(h => h.maker_token_tags?.includes('diamond_hands')); break; case 'newAddress': result = holders.filter(h => h.tags?.includes('fresh_wallet')); break; case 'holdingLessThan7Days': result = holders.filter(h => h.start_holding_at && (currentTime - h.start_holding_at) < sevenDaysInSeconds ); break; case 'highProfit': result = holders.filter(h => h.profit_change > 5); break; case 'suspicious': result = holders.filter(h => h.is_suspicious || h.transfer_in || (h.maker_token_tags && ( h.maker_token_tags.includes('rat_trader') || h.maker_token_tags.includes('bundler') )) ); break; case 'lowSolBalance': result = holders.filter(h => h.native_balance && (h.native_balance / 1000000000) < 1 ); break; case 'tokenTransferIn': result = holders.filter(h => h.token_transfer_in && h.token_transfer_in.address && h.token_transfer_in.address.trim() !== '' ); break; case 'exchangeAddresses': result = holders.filter(h => isExchangeAddress(h)); break; default: result = []; } // 缓存结果 dataCache.filteredResults.set(cacheKey, result); console.log('[性能优化] 过滤结果已缓存:', type, 'count:', result.length); return result; } // 获取类型对应的中文标题 function getTypeTitle(type) { const titles = { 'fullPosition': '满仓地址', 'profitable': '盈利地址', 'losing': '亏损地址', 'active24h': '24小时活跃地址', 'diamondHands': '钻石手地址', 'newAddress': '新地址', 'holdingLessThan7Days': '持仓小于7天的地址', 'highProfit': '5倍以上盈利地址', 'suspicious': '可疑地址', 'lowSolBalance': 'SOL餘額不足1的地址', 'tokenTransferIn': '代币转入地址', 'exchangeAddresses': '交易所地址' }; return titles[type] || '未知类型'; } // 1. 拦截 fetch 请求 const originalFetch = window.fetch; window.fetch = function(url, options) { if (isTargetApi(url)) { console.log('[拦截] fetch 请求:', url); return originalFetch.apply(this, arguments) .then(response => { if (response.ok) { processResponse(response.clone()); } return response; }); } return originalFetch.apply(this, arguments); }; // 2. 拦截 XMLHttpRequest const originalXHR = window.XMLHttpRequest; window.XMLHttpRequest = function() { const xhr = new originalXHR(); const originalOpen = xhr.open; xhr.open = function(method, url) { if (isTargetApi(url)) { console.log('[拦截] XHR 请求:', url); const originalOnload = xhr.onload; xhr.onload = function() { if (xhr.readyState === 4 && xhr.status === 200) { processResponse(xhr.responseText); } originalOnload?.apply(this, arguments); }; } return originalOpen.apply(this, arguments); }; return xhr; }; function isTargetApi(url) { if (typeof url !== 'string') return false; // 检查是否是token_holders API且包含limit参数 const isTokenHoldersApi = /vas\/api\/v1\/token_holders\/(sol|eth|base|bsc|tron)(\/|$|\?)/i.test(url); const hasLimitParam = /[?&]limit=/i.test(url); const isTarget = isTokenHoldersApi && hasLimitParam; if (isTarget) { // 从URL中提取CA地址 const match = url.match(/vas\/api\/v1\/token_holders\/sol\/([^/?]+)/i); console.log('匹配的ca:',match) console.log('包含limit参数的URL:', url) if (match && match[1]) { currentCaAddress = match[1]; } } return isTarget; } function processResponseInternal(response) { console.log('开始处理响应数据'); try { const dataPromise = typeof response === 'string' ? Promise.resolve(JSON.parse(response)) : response.json(); dataPromise.then(data => { interceptedData = data; console.log('[成功] 拦截到数据量:', data.data?.list?.length); console.log('[成功] 拦截到数据:',data); const currentStats = calculateStats(); if (isFirstLoad) { // 首次加载,记录初始数据和CA地址 initialStats = currentStats; initialCaAddress = currentCaAddress; isFirstLoad = false; updateStatsDisplay(currentStats, true); } else { // 非首次加载,比较CA地址 const isSameCa = currentCaAddress === initialCaAddress; updateStatsDisplay(currentStats, !isSameCa); // 如果CA地址不同,更新初始数据为当前数据,并重置下载按钮状态 if (!isSameCa) { initialStats = currentStats; initialCaAddress = currentCaAddress; // 重置下载按钮状态 resetDownloadButtonState(); console.log('检测到CA地址变更,已重置下载按钮状态'); } } }).catch(e => console.error('解析失败:', e)); } catch (e) { console.error('处理响应错误:', e); } } // 防抖版本的processResponse const processResponse = debounce(processResponseInternal, 100); // 计算数据哈希值用于缓存 function getDataHash(data) { return JSON.stringify({ length: data?.data?.list?.length || 0, timestamp: data?.data?.list?.[0]?.last_active_timestamp || 0, caAddress: currentCaAddress }); } // 3. 计算所有统计指标(优化版本) function calculateStats() { if (!interceptedData?.data?.list) return null; // 检查缓存 const currentHash = getDataHash(interceptedData); if (dataCache.lastDataHash === currentHash && dataCache.calculatedStats) { console.log('[性能优化] 使用缓存的统计数据'); return dataCache.calculatedStats; } console.log('[性能优化] 重新计算统计数据'); const currentTime = Math.floor(Date.now() / 1000); const sevenDaysInSeconds = 7 * 24 * 60 * 60; // 7天的秒数 const holders = interceptedData.data.list; const stats = { fullPosition: 0, // 全仓 profitable: 0, // 盈利 losing: 0, // 亏损 active24h: 0, // 24h活跃 diamondHands: 0, // 钻石手 newAddress: 0, // 新地址 highProfit: 0, // 10x盈利 suspicious: 0, // 新增:可疑地址 holdingLessThan7Days: 0, // 新增:持仓小于7天 lowSolBalance: 0, // 新增:SOL餘額小於1的地址 tokenTransferIn: 0, // 新增:代币转入地址数 exchangeAddresses: 0 // 新增:交易所地址数 }; holders.forEach(holder => { // 满判断条件:1.没有卖出;2.没有出货地址 if (holder.sell_amount_percentage === 0 && (!holder.token_transfer_out || !holder.token_transfer_out.address)) { stats.fullPosition++; } if (holder.profit > 0) stats.profitable++; if (holder.profit < 0) stats.losing++; if (holder.last_active_timestamp > currentTime - 86400) stats.active24h++; if (holder.maker_token_tags?.includes('diamond_hands')) stats.diamondHands++; if (holder.tags?.includes('fresh_wallet')) stats.newAddress++; if (holder.profit_change > 5) stats.highProfit++; // 增强版可疑地址检测 if ( holder.is_suspicious || holder.transfer_in || (holder.maker_token_tags && ( holder.maker_token_tags.includes('rat_trader') || holder.maker_token_tags.includes('bundler') )) ) { stats.suspicious++; } // 新增7天持仓统计 if (holder.start_holding_at && (currentTime - holder.start_holding_at) < sevenDaysInSeconds) { stats.holdingLessThan7Days++; } // 新增低SOL餘額統計(小於1 SOL) if (holder.native_balance && (holder.native_balance / 1000000000) < 1) { stats.lowSolBalance++; } // 新增代币转入地址统计 if (holder.token_transfer_in && holder.token_transfer_in.address && holder.token_transfer_in.address.trim() !== '') { stats.tokenTransferIn++; } // 新增交易所地址统计 if (isExchangeAddress(holder)) { stats.exchangeAddresses++; } }); // 缓存计算结果 dataCache.lastDataHash = currentHash; dataCache.calculatedStats = stats; dataCache.filteredResults.clear(); // 清空过滤缓存 console.log('[性能优化] 统计数据已缓存'); return stats; } // 1. 持久化容器监听 const observer = new MutationObserver(() => { const targetContainer = document.querySelector('.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full'); if (targetContainer && !targetContainer.querySelector('#statistic-gmgn-stats-item')) { injectStatsItem(targetContainer); } }); function injectStatsItem(container) { if (container.querySelector('#statistic-gmgn-stats-item')) return; const isSol = isSolNetwork(); const statsItem = document.createElement('div'); statsItem.id = 'statistic-gmgn-stats-item'; statsItem.className = 'statistic-gmgn-stats-container'; const headerClass = isSol ? 'statistic-gmgn-stats-header sol-network' : 'statistic-gmgn-stats-header'; const dataClass = isSol ? 'statistic-gmgn-stats-data sol-network' : 'statistic-gmgn-stats-data'; statsItem.innerHTML = ` <div class="${headerClass}"> <span title="持有代币且未卖出任何数量的地址(排除转移代币卖出的地址)">满仓</span> <span title="当前持仓价值高于买入成本的地址">盈利</span> <span title="当前持仓价值低于买入成本的地址">亏损</span> <span title="过去24小时内有交易活动的地址">活跃</span> <span title="长期持有且很少卖出的地址">钻石</span> <span title="新钱包">新址</span> <span title="持仓时间小于7天的地址">7天</span> <span title="盈利超过5倍的地址">5X</span> <span title="标记为可疑或异常行为的地址">可疑</span> <span title="有代币转入记录的地址">转入</span> <span title="与交易所相关的地址">交易所</span> ${isSol ? '<span title="SOL餘額小於1的地址">低SOL</span>' : ''} <span title="下载统计数据图片">图片</span> </div> <div class="${dataClass}"> <span id="fullPosition">-</span> <span id="profitable">-</span> <span id="losing">-</span> <span id="active24h">-</span> <span id="diamondHands">-</span> <span id="newAddress">-</span> <span id="holdingLessThan7Days">-</span> <span id="highProfit">-</span> <span id="suspicious">-</span> <span id="tokenTransferIn">-</span> <span id="exchangeAddresses">-</span> ${isSol ? '<span id="lowSolBalance">-</span>' : ''} <span id="statistic-download-image-btn" class="statistic-download-btn clickable" title="下载统计数据图片">下载</span> </div> `; container.insertAdjacentElement('afterbegin', statsItem); } function updateStatsDisplayInternal(currentStats, forceNoArrows) { if (!currentStats) return; // 确保DOM已存在 if (!document.getElementById('statistic-gmgn-stats-item')) { injectStatsItem(); } // 优化的事件监听器绑定(只绑定一次) if (!dataCache.eventsInitialized) { console.log('[性能优化] 初始化事件监听器'); const baseClickableTypes = ['fullPosition', 'profitable', 'losing', 'active24h', 'diamondHands', 'newAddress', 'holdingLessThan7Days', 'highProfit', 'suspicious', 'tokenTransferIn', 'exchangeAddresses']; const clickableTypes = isSolNetwork() ? [...baseClickableTypes, 'lowSolBalance'] : baseClickableTypes; clickableTypes.forEach(id => { const element = document.getElementById(id); if (element && !element.hasAttribute('data-event-bound')) { element.classList.add('clickable'); element.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const addresses = getAddressByType(id); // 交易所地址使用专用弹框 if (id === 'exchangeAddresses') { createExchangeModal(addresses, currentCaAddress); } else { const title = getTypeTitle(id); const showSolBalance = id === 'lowSolBalance'; createModal(title, addresses, currentCaAddress, showSolBalance); } }; element.setAttribute('data-event-bound', 'true'); console.log('[性能优化] 事件监听器已绑定:', id); } }); dataCache.eventsInitialized = true; } const updateStatElement = (id, value, hasChanged, isIncrease) => { const element = document.getElementById(id); if (!element) return; element.innerHTML = `<strong style="color: ${id === 'profitable' ? '#2E8B57' : (id === 'losing' || id === 'suspicious' ? '#FF1493' : id === 'holdingLessThan7Days' ? '#00E5EE' : id === 'lowSolBalance' ? '#FFA500' : '#e9ecef')}">${value}</strong>`; // 只有当不是强制不显示箭头且确实有变化时才显示箭头 if (!forceNoArrows && hasChanged) { const arrow = document.createElement('span'); arrow.className = isIncrease ? 'statistic-up-arrow' : 'statistic-down-arrow'; arrow.textContent = isIncrease ? '▲' : '▼'; // 移除旧的箭头(如果有) const oldArrow = element.querySelector('.statistic-up-arrow, .statistic-down-arrow'); if (oldArrow) oldArrow.remove(); element.appendChild(arrow); } else { // 没有变化或强制不显示箭头,移除箭头(如果有) const oldArrow = element.querySelector('.statistic-up-arrow, .statistic-down-arrow'); if (oldArrow) oldArrow.remove(); } // 事件监听器已在初始化时绑定,无需重复绑定 }; // 绑定下载图片按钮事件 const downloadBtn = document.getElementById('statistic-download-image-btn'); if (downloadBtn && !downloadBtn.hasAttribute('data-event-bound')) { downloadBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // 检查是否正在处理中 if (isDownloadInProgress) { console.log('下载正在进行中,请稍候...'); return; } handleDownloadImage(); }); downloadBtn.setAttribute('data-event-bound', 'true'); } // 更新各个统计指标 // 新增7天持仓统计更新 updateStatElement('holdingLessThan7Days', currentStats.holdingLessThan7Days, initialStats && currentStats.holdingLessThan7Days !== initialStats.holdingLessThan7Days, initialStats && currentStats.holdingLessThan7Days > initialStats.holdingLessThan7Days); updateStatElement('fullPosition', currentStats.fullPosition, initialStats && currentStats.fullPosition !== initialStats.fullPosition, initialStats && currentStats.fullPosition > initialStats.fullPosition); updateStatElement('profitable', currentStats.profitable, initialStats && currentStats.profitable !== initialStats.profitable, initialStats && currentStats.profitable > initialStats.profitable); updateStatElement('losing', currentStats.losing, currentStats.losing !== initialStats.losing, currentStats.losing > initialStats.losing); updateStatElement('active24h', currentStats.active24h, currentStats.active24h !== initialStats.active24h, currentStats.active24h > initialStats.active24h); updateStatElement('diamondHands', currentStats.diamondHands, currentStats.diamondHands !== initialStats.diamondHands, currentStats.diamondHands > initialStats.diamondHands); updateStatElement('newAddress', currentStats.newAddress, currentStats.newAddress !== initialStats.newAddress, currentStats.newAddress > initialStats.newAddress); updateStatElement('highProfit', currentStats.highProfit, currentStats.highProfit !== initialStats.highProfit, currentStats.highProfit > initialStats.highProfit); updateStatElement('suspicious', currentStats.suspicious, currentStats.suspicious !== initialStats.suspicious, currentStats.suspicious > initialStats.suspicious); updateStatElement('tokenTransferIn', currentStats.tokenTransferIn, initialStats && currentStats.tokenTransferIn !== initialStats.tokenTransferIn, initialStats && currentStats.tokenTransferIn > initialStats.tokenTransferIn); updateStatElement('exchangeAddresses', currentStats.exchangeAddresses, initialStats && currentStats.exchangeAddresses !== initialStats.exchangeAddresses, initialStats && currentStats.exchangeAddresses > initialStats.exchangeAddresses); // 只在SOL网络时更新低SOL余额统计 if (isSolNetwork()) { updateStatElement('lowSolBalance', currentStats.lowSolBalance, initialStats && currentStats.lowSolBalance !== initialStats.lowSolBalance, initialStats && currentStats.lowSolBalance > initialStats.lowSolBalance); } } // 防抖版本的updateStatsDisplay const updateStatsDisplay = debounce(updateStatsDisplayInternal, 200); // 数据收集函数 - 收集基础统计数据和详细持有者信息 function collectStatsData() { if (!interceptedData?.data?.list || !currentCaAddress) { console.error('数据不完整,无法生成图片'); return null; } const currentStats = calculateStats(); if (!currentStats) { console.error('无法计算统计数据'); return null; } // 基础统计数据 const basicStats = { fullPosition: { label: '满仓', value: currentStats.fullPosition, type: 'fullPosition' }, profitable: { label: '盈利', value: currentStats.profitable, type: 'profitable' }, losing: { label: '亏损', value: currentStats.losing, type: 'losing' }, active24h: { label: '活跃', value: currentStats.active24h, type: 'active24h' }, diamondHands: { label: '钻石', value: currentStats.diamondHands, type: 'diamondHands' }, newAddress: { label: '新址', value: currentStats.newAddress, type: 'newAddress' }, holdingLessThan7Days: { label: '7天', value: currentStats.holdingLessThan7Days, type: 'holdingLessThan7Days' }, highProfit: { label: '5X', value: currentStats.highProfit, type: 'highProfit' }, suspicious: { label: '可疑', value: currentStats.suspicious, type: 'suspicious' }, tokenTransferIn: { label: '转入', value: currentStats.tokenTransferIn, type: 'tokenTransferIn' }, exchangeAddresses: { label: '交易所', value: currentStats.exchangeAddresses, type: 'exchangeAddresses' } }; // 如果是SOL网络,添加低余额统计 if (isSolNetwork()) { basicStats.lowSolBalance = { label: '低SOL', value: currentStats.lowSolBalance, type: 'lowSolBalance' }; } // 收集每个统计类型的汇总数据(包括值为0的项目) const detailedData = {}; for (const [key, stat] of Object.entries(basicStats)) { const addresses = getAddressByType(stat.type); if (addresses && addresses.length > 0) { // 计算汇总信息 const soldChipsCount = addresses.filter(holder => (holder.sell_amount_percentage || 0) > 0).length; const totalHoldingPercentage = addresses.reduce((sum, holder) => sum + (holder.amount_percentage || 0), 0); detailedData[key] = { label: stat.label, totalCount: addresses.length, soldChipsCount: soldChipsCount, totalHoldingPercentage: (totalHoldingPercentage * 100).toFixed(2) + '%' }; } else { // 即使没有地址数据,也创建空的详细数据 detailedData[key] = { label: stat.label, totalCount: 0, soldChipsCount: 0, totalHoldingPercentage: '0.00%' }; } } return { caAddress: currentCaAddress, timestamp: new Date(), basicStats: basicStats, detailedData: detailedData }; } // 绘制圆角矩形辅助函数 function drawRoundedRect(ctx, x, y, width, height, radius, strokeColor = null, strokeWidth = 0, fillOnly = false) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.arcTo(x + width, y, x + width, y + height, radius); ctx.arcTo(x + width, y + height, x, y + height, radius); ctx.arcTo(x, y + height, x, y, radius); ctx.arcTo(x, y, x + width, y, radius); ctx.closePath(); if (!fillOnly) { ctx.fill(); } if (strokeColor && strokeWidth > 0) { ctx.strokeStyle = strokeColor; ctx.lineWidth = strokeWidth; ctx.stroke(); } } // 图片生成函数 - 现代化风格 function generateStatsImage(data) { if (!data) { console.error('无数据可生成图片'); return null; } const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // 设置画布尺寸 - 现代化尺寸 canvas.width = 1200; canvas.height = 1400; // 增加高度以适应现代化布局 // 创建现代渐变背景 const bgGradient = ctx.createLinearGradient(0, 0, 0, canvas.height); bgGradient.addColorStop(0, '#0f172a'); bgGradient.addColorStop(0.3, '#1e293b'); bgGradient.addColorStop(0.7, '#334155'); bgGradient.addColorStop(1, '#1e293b'); ctx.fillStyle = bgGradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // 绘制现代化圆角边框 const borderRadius = 20; const borderPadding = 30; drawRoundedRect(ctx, borderPadding, borderPadding, canvas.width - borderPadding * 2, canvas.height - borderPadding * 2, borderRadius, '#3b82f6', 3); // 绘制标题区域背景 const titleBg = ctx.createLinearGradient(0, 50, 0, 150); titleBg.addColorStop(0, 'rgba(59, 130, 246, 0.2)'); titleBg.addColorStop(1, 'rgba(59, 130, 246, 0.05)'); ctx.fillStyle = titleBg; drawRoundedRect(ctx, 60, 60, canvas.width - 120, 120, 15); // 绘制现代化标题 ctx.font = 'bold 36px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#ffffff'; ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; ctx.shadowBlur = 4; ctx.shadowOffsetY = 2; const title = 'GMGN 前排统计分析'; ctx.fillText(title, canvas.width / 2, 110); // 清除阴影 ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; ctx.shadowOffsetY = 0; // 绘制CA地址和时间 - 现代化样式 ctx.font = '18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#22d3ee'; const formatTime = data.timestamp.getFullYear() + '-' + String(data.timestamp.getMonth() + 1).padStart(2, '0') + '-' + String(data.timestamp.getDate()).padStart(2, '0') + ' ' + String(data.timestamp.getHours()).padStart(2, '0') + ':' + String(data.timestamp.getMinutes()).padStart(2, '0') + ':' + String(data.timestamp.getSeconds()).padStart(2, '0'); ctx.fillText(`CA: ${data.caAddress}`, canvas.width / 2, 140); ctx.fillStyle = '#fbbf24'; ctx.fillText(`时间: ${formatTime}`, canvas.width / 2, 165); // 绘制基础统计数据(第一层)- 现代化风格 ctx.font = 'bold 24px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#ffffff'; ctx.textAlign = 'left'; ctx.fillText('基础统计数据', 80, 220); let yPos = 260; const statsPerRow = 3; // 每行3个 const statWidth = 350; // 增加宽度适应现代化布局 const statHeight = 90; // 增加高度 let currentRow = 0; let currentCol = 0; const baseX = 80; // 左侧边距 for (const [key, stat] of Object.entries(data.basicStats)) { const x = baseX + currentCol * statWidth; const y = yPos + currentRow * statHeight; // 绘制现代化卡片背景渐变 const cardGradient = ctx.createLinearGradient(x, y, x, y + statHeight - 15); cardGradient.addColorStop(0, 'rgba(255, 255, 255, 0.08)'); cardGradient.addColorStop(1, 'rgba(59, 130, 246, 0.12)'); ctx.fillStyle = cardGradient; drawRoundedRect(ctx, x, y, statWidth - 30, statHeight - 15, 12); // 绘制现代化边框 drawRoundedRect(ctx, x, y, statWidth - 30, statHeight - 15, 12, '#3b82f6', 2, true); // 绘制标签 - 现代化字体 ctx.font = '16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.textAlign = 'left'; ctx.fillText(stat.label, x + 20, y + 30); // 绘制数值 - 现代化颜色和字体 ctx.font = 'bold 28px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; const valueColor = key === 'profitable' ? '#22c55e' : (key === 'losing' || key === 'suspicious' ? '#ef4444' : key === 'holdingLessThan7Days' ? '#06b6d4' : key === 'lowSolBalance' ? '#f59e0b' : '#22d3ee'); ctx.fillStyle = valueColor; ctx.fillText(stat.value.toString(), x + 20, y + 65); currentCol++; if (currentCol >= statsPerRow) { currentCol = 0; currentRow++; } } // 绘制详细数据(第二层)- 现代化风格 yPos = 180 + (Math.ceil(Object.keys(data.basicStats).length / statsPerRow) + 1) * statHeight + 50; // 绘制详细分析标题 ctx.font = 'bold 24px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#ffffff'; ctx.textAlign = 'left'; ctx.fillText('详细数据分析', 80, yPos); yPos += 40; // 使用现代化网格布局绘制详细数据分析 const detailStatsPerRow = 3; // 每行3个详细数据单元格 const detailStatWidth = 350; // 与基础统计保持一致 const detailStatHeight = 130; // 增加高度以适应现代化布局 let detailCurrentRow = 0; let detailCurrentCol = 0; for (const [key, detail] of Object.entries(data.detailedData)) { if (yPos + detailCurrentRow * detailStatHeight > canvas.height - 150) break; // 防止超出画布 const x = baseX + detailCurrentCol * detailStatWidth; // 与基础数据对齐 const y = yPos + detailCurrentRow * detailStatHeight; // 绘制现代化卡片背景渐变 const detailCardGradient = ctx.createLinearGradient(x, y, x, y + detailStatHeight - 15); detailCardGradient.addColorStop(0, 'rgba(255, 255, 255, 0.06)'); detailCardGradient.addColorStop(1, 'rgba(16, 185, 129, 0.08)'); ctx.fillStyle = detailCardGradient; drawRoundedRect(ctx, x, y, detailStatWidth - 30, detailStatHeight - 15, 12); // 绘制现代化边框 drawRoundedRect(ctx, x, y, detailStatWidth - 30, detailStatHeight - 15, 12, '#10b981', 2, true); // 绘制分类标题 - 现代化样式 ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; const titleColor = key === 'profitable' ? '#22c55e' : (key === 'losing' || key === 'suspicious' ? '#ef4444' : key === 'holdingLessThan7Days' ? '#06b6d4' : key === 'lowSolBalance' ? '#f59e0b' : '#22d3ee'); ctx.fillStyle = titleColor; ctx.textAlign = 'left'; ctx.fillText(`${detail.label}`, x + 20, y + 30); // 绘制汇总数据 - 现代化样式 ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; // 已卖筹码数 ctx.fillText('已卖筹码数:', x + 20, y + 55); ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = detail.soldChipsCount > 0 ? '#ef4444' : '#22c55e'; ctx.fillText(detail.soldChipsCount.toString(), x + 150, y + 55); // 总地址数 ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.fillText('总地址数:', x + 20, y + 80); ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = titleColor; // 使用与标题相同的颜色 ctx.fillText(detail.totalCount.toString(), x + 150, y + 80); // 持仓占比 ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.fillText('持仓占比:', x + 20, y + 105); ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = '#60a5fa'; ctx.fillText(detail.totalHoldingPercentage, x + 150, y + 105); detailCurrentCol++; if (detailCurrentCol >= detailStatsPerRow) { detailCurrentCol = 0; detailCurrentRow++; } } return canvas; } // 下载图片函数 function downloadImage(canvas, filename) { if (!canvas) { console.error('无法下载图片:画布为空'); return; } try { // 转换为blob canvas.toBlob(function(blob) { // 创建下载链接 const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; // 触发下载 document.body.appendChild(link); link.click(); document.body.removeChild(link); // 清理URL对象 URL.revokeObjectURL(url); console.log('图片下载成功:', filename); }, 'image/png'); } catch (error) { console.error('下载图片失败:', error); } } // 显示图片预览模态框 function showImagePreview(canvas, filename) { const modal = document.createElement('div'); modal.className = 'image-preview-modal'; const imageUrl = canvas.toDataURL('image/png'); modal.innerHTML = ` <div class="image-preview-content"> <div class="image-preview-header"> <div class="image-preview-title">📷 统计图片预览</div> <button class="image-preview-close">×</button> </div> <img src="${imageUrl}" alt="统计图片" class="image-preview-img"> <div class="image-preview-buttons"> <button class="image-preview-btn copy-btn">📋 复制图片</button> <button class="image-preview-btn download-btn">💾 下载图片</button> </div> </div> `; document.body.appendChild(modal); // 绑定事件 const closeBtn = modal.querySelector('.image-preview-close'); const copyBtn = modal.querySelector('.copy-btn'); const downloadBtn = modal.querySelector('.download-btn'); // 关闭模态框 const closeModal = () => { document.body.removeChild(modal); }; closeBtn.addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); // 复制图片 copyBtn.addEventListener('click', () => { copyImageToClipboard(canvas); }); // 下载图片 downloadBtn.addEventListener('click', () => { downloadImageFromPreview(canvas, filename); closeModal(); }); // ESC键关闭 const escHandler = (e) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } // 复制图片到剪贴板 async function copyImageToClipboard(canvas) { try { // 将canvas转为blob canvas.toBlob(async (blob) => { try { if (navigator.clipboard && window.ClipboardItem) { const item = new ClipboardItem({ 'image/png': blob }); await navigator.clipboard.write([item]); showModernToast('图片已复制到剪贴板!', 'success'); } else { // 兜底方案:创建临时图片元素让用户手动复制 const img = document.createElement('img'); img.src = canvas.toDataURL('image/png'); img.style.position = 'fixed'; img.style.top = '-9999px'; document.body.appendChild(img); // 选择图片 const range = document.createRange(); range.selectNode(img); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); // 尝试复制 const success = document.execCommand('copy'); document.body.removeChild(img); window.getSelection().removeAllRanges(); if (success) { showModernToast('图片已复制到剪贴板!', 'success'); } else { showModernToast('复制失败,请尝试手动下载图片', 'error'); } } } catch (error) { console.error('复制图片失败:', error); showModernToast('复制失败:' + error.message, 'error'); } }, 'image/png'); } catch (error) { console.error('复制图片失败:', error); showModernToast('复制失败:' + error.message, 'error'); } } // 从预览下载图片 function downloadImageFromPreview(canvas, filename) { try { canvas.toBlob((blob) => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); showModernToast('图片下载成功!', 'success'); }, 'image/png'); } catch (error) { console.error('下载图片失败:', error); showModernToast('下载失败:' + error.message, 'error'); } } // 重置按钮状态 function resetDownloadButtonState() { isDownloadInProgress = false; const button = document.getElementById('statistic-download-image-btn'); if (button) { button.classList.remove('disabled'); button.textContent = '下载'; } } // 设置按钮禁用状态 function setDownloadButtonDisabled(disabled) { const button = document.getElementById('statistic-download-image-btn'); if (button) { if (disabled) { button.classList.add('disabled'); button.textContent = '生成中...'; } else { button.classList.remove('disabled'); button.textContent = '下载'; } } } // 主要的下载处理函数 - 现在显示预览而不是直接下载 function handleDownloadImage() { const button = document.getElementById('statistic-download-image-btn'); if (!button) return; // 检查是否已在处理中 if (isDownloadInProgress) { console.log('图片生成正在进行中...'); return; } // 设置处理状态 isDownloadInProgress = true; setDownloadButtonDisabled(true); try { // 收集数据 const data = collectStatsData(); if (!data) { throw new Error('无法收集数据'); } // 更新当前CA地址 currentCAAddress = data.caAddress || ''; // 生成图片 const canvas = generateStatsImage(data); if (!canvas) { throw new Error('无法生成图片'); } // 生成文件名 const timestamp = data.timestamp.getFullYear() + String(data.timestamp.getMonth() + 1).padStart(2, '0') + String(data.timestamp.getDate()).padStart(2, '0') + String(data.timestamp.getHours()).padStart(2, '0'); const filename = `${data.caAddress}_${timestamp}.png`; // 显示预览而不是直接下载 showImagePreview(canvas, filename); } catch (error) { console.error('生成图片失败:', error); showModernToast('生成图片失败:' + error.message, 'error'); } finally { // 恢复按钮状态 isDownloadInProgress = false; setDownloadButtonDisabled(false); } } // 4. 初始化 if (document.readyState === 'complete') { startObserving(); } else { window.addEventListener('DOMContentLoaded', startObserving); } function startObserving() { // 立即检查一次 const initialContainer = document.querySelector('.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full'); if (initialContainer) injectStatsItem(initialContainer); // 持续监听DOM变化 observer.observe(document.body, { childList: true, subtree: true, attributes: false }); } })();