// ==UserScript== // @name GMGN 前排统计测试版 // @namespace http://tampermonkey.net/ // @version 5.3 // @description 【优化版】统计GMGN代币前排地址数据 | 修复拦截冲突 | 解决内存泄漏 | 性能优化 | 涨跌提醒 | 详情弹窗 | Excel导出 // @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 GM_xmlhttpRequest // @run-at document-start // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/552368/GMGN%20%E5%89%8D%E6%8E%92%E7%BB%9F%E8%AE%A1%E6%B5%8B%E8%AF%95%E7%89%88.user.js // @updateURL https://update.greasyfork.icu/scripts/552368/GMGN%20%E5%89%8D%E6%8E%92%E7%BB%9F%E8%AE%A1%E6%B5%8B%E8%AF%95%E7%89%88.meta.js // ==/UserScript== (function () { 'use strict'; // ===== 全局状态管理 (优化版) ===== const GlobalState = { // 下载状态 isDownloadInProgress: false, // 当前代币CA地址 currentCAAddress: '', initialCAAddress: '', // 拦截的数据 interceptedData: null, initialStats: null, isFirstLoad: true, // 数据缓存 (带过期机制) cache: { lastDataHash: null, calculatedStats: null, filteredResults: new Map(), eventsInitialized: false, cacheTime: 0, maxCacheAge: 60000 // 缓存60秒 }, // Observer 管理 observer: null, // 请求队列管理 (解决竞态) requestQueue: { pending: 0, lastRequestId: 0 }, // ===== 新增:加仓功能相关状态 ===== // K线数据 klineData: null, // 回调60%的时间戳(毫秒) rebound60Timestamp: null, // ATH信息 athInfo: { price: null, timestamp: null }, // API参数(从token_holders请求中提取) apiParams: null, // 加仓地址缓存 addedPositionAddresses: [], // 加仓数据加载状态 addedPositionLoading: false, // 加仓分析是否已完成 addedPositionCompleted: false, // K线数据是否已获取 klineDataFetched: false, // 当前链 currentChain: 'sol', // 清理所有状态 reset() { this.interceptedData = null; this.initialStats = null; this.isFirstLoad = true; this.cache.lastDataHash = null; this.cache.calculatedStats = null; this.cache.filteredResults.clear(); this.cache.cacheTime = 0; this.requestQueue.pending = 0; this.requestQueue.lastRequestId = 0; // 清理加仓相关数据 this.klineData = null; this.rebound60Timestamp = null; this.athInfo = { price: null, timestamp: null }; this.addedPositionAddresses = []; this.addedPositionLoading = false; this.addedPositionCompleted = false; this.klineDataFetched = false; this.currentChain = 'sol'; }, // 清理缓存 clearCache() { this.cache.lastDataHash = null; this.cache.calculatedStats = null; this.cache.filteredResults.clear(); this.cache.cacheTime = 0; console.log('[缓存管理] 缓存已清理'); }, // 检查缓存是否过期 isCacheExpired() { return Date.now() - this.cache.cacheTime > this.cache.maxCacheAge; } }; // 现代化提示框函数 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 = `
${icon}
${message}
`; // 添加到页面 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(13, 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(14, 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); // ===== 旧全局变量 (已废弃 - 使用GlobalState代替) ===== // 保留这些定义仅用于兼容性,实际使用GlobalState let interceptedData = null; let initialStats = null; let isFirstLoad = true; let currentCaAddress = null; let initialCaAddress = null; let dataCache = { lastDataHash: null, calculatedStats: null, filteredResults: new Map(), eventsInitialized: false }; // 同步旧变量到GlobalState (兼容层) Object.defineProperty(window, '__gmgn_interceptedData', { get() { return GlobalState.interceptedData; }, set(val) { GlobalState.interceptedData = val; interceptedData = val; } }); Object.defineProperty(window, '__gmgn_currentCA', { get() { return GlobalState.currentCAAddress; }, set(val) { GlobalState.currentCAAddress = val; currentCaAddress = val; } }); // 防抖函数 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 `
${icon}
${label}
${value}
`; } // 生成可疑标识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 ` ${label.text} `; }).join(''); return `
${labelsHTML}
`; } // 检查是否为交易所地址 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 = `
🚀 交易所地址分析 (共${data.length}个地址)
已卖筹码地址数: ${soldAddressCount}
交易所数: ${Object.keys(exchangeGroups).length}
总持仓占比: ${(totalHoldingPercentage * 100).toFixed(2)}%
📱 交易所统计
${exchangeSummary.map(item => `
${item.name} ${item.count}个地址
`).join('')}
`; 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 = [...GlobalState.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 = `
${exchangeName} 地址详情
共 ${sortedAddresses.length} 个地址
${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 `
榜${processedData.rankIndex}
${processedData.address}
详情
基本信息
${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)}
`; }).join('')}
`; } // 交易所数据导出函数 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 (!GlobalState.interceptedData?.data?.list) { console.error('无法获取原始数据进行全局排名'); return; } // 创建全局排名映射 - 基于原始完整数据按持仓比例排序 const globalRankMap = new Map(); const allHolders = [...GlobalState.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 = `
💎 ${title} (${allProcessedData.length}个地址)
${isLargeDataset ? `
⚡ 性能优化:分页显示 | 第${currentPage}页,共${totalPages}页 | 每页${ITEMS_PER_PAGE}条
` : ''}
已卖筹码地址数: ${soldAddressCount}
总数量: ${allProcessedData.length}
总持仓占比: ${(totalHoldingPercentage * 100).toFixed(2)}%
${isLargeDataset ? `
第 ${currentPage} 页 / 共 ${totalPages} 页
` : ''}
`; // 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 = `
榜${holder.rankIndex}
${holder.address}
详情
基本信息 ${generateSuspiciousLabelsHTML(holder.suspiciousLabels)}
${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') : ''}
`; // 添加地址复制功能 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 = `
榜${holder.rankIndex}
${holder.address}
详情
基本信息 ${generateSuspiciousLabelsHTML(holder.suspiciousLabels)}
${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') : ''}
`; // 添加地址复制功能 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 (!GlobalState.interceptedData?.data?.list) return []; // 检查缓存 const currentHash = getDataHash(interceptedData); const cacheKey = `${type}_${currentHash}`; if (GlobalState.cache.filteredResults.has(cacheKey)) { console.log('[性能优化] 使用缓存的过滤结果:', type); return GlobalState.cache.filteredResults.get(cacheKey); } console.log('[性能优化] 重新过滤数据:', type); const currentTime = Math.floor(Date.now() / 1000); const sevenDaysInSeconds = 7 * 24 * 60 * 60; const holders = GlobalState.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; case 'addedPosition': // 返回加仓地址列表 result = GlobalState.addedPositionAddresses; break; default: result = []; } // 缓存结果 GlobalState.cache.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': '交易所地址', 'addedPosition': '回调60%后加仓地址' }; return titles[type] || '未知类型'; } // ===== API拦截器 (增强版 - 避免冲突) ===== // 使用Proxy包装而不是直接覆盖,避免与其他插件冲突 const ApiInterceptor = { initialized: false, originalFetch: window.fetch, xhrPrototypeOpen: XMLHttpRequest.prototype.open, // 安全的Fetch拦截 (使用链式调用保留其他拦截器) hookFetch() { if (!window.fetch) return; const self = this; const prevFetch = window.fetch; window.fetch = function (...args) { const [url] = args; try { if (self.isTargetApi(url)) { return prevFetch.apply(this, args).then(response => { if (response && response.ok) { // 异步处理,不阻塞主流程,传递URL self.processResponse(response.clone(), url).catch(err => { console.error('[拦截器] 处理响应失败:', err); }); } return response; }).catch(err => { console.error('[拦截器] Fetch失败:', err); throw err; }); } } catch (err) { console.error('[拦截器] Fetch拦截异常:', err); } return prevFetch.apply(this, args); }; }, // 安全的XHR拦截 (使用open钩子获取URL) hookXHR() { const self = this; const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, ...rest) { // 保存URL到实例,供后续使用 this.__interceptor_url = url; this.__interceptor_method = method; // 只对目标API添加监听 if (self.isTargetApi(url)) { // 使用一次性监听器,避免重复绑定 const loadHandler = function () { // 确保请求已完成且成功 if (this.readyState === 4 && this.status === 200) { try { // 验证 responseText 不为空 if (this.responseText && this.responseText.trim()) { // 传递URL到processResponse self.processResponse(this.responseText, this.__interceptor_url).catch(err => { console.error('[拦截器] XHR响应处理失败:', err); }); } else { console.warn('[拦截器] XHR响应为空,跳过处理'); } } catch (err) { console.error('[拦截器] XHR异常:', err); } // 清理 this.removeEventListener('readystatechange', loadHandler); } }; this.addEventListener('readystatechange', loadHandler); } return originalOpen.apply(this, [method, url, ...rest]); }; }, // 初始化拦截器 init() { if (this.initialized) { console.warn('[拦截器] 已初始化,跳过重复初始化'); return; } try { this.hookFetch(); this.hookXHR(); this.initialized = true; console.log('[拦截器] API拦截器初始化成功'); } catch (err) { console.error('[拦截器] 初始化失败:', err); } }, // 判断是否为目标API isTargetApi(url) { if (typeof url !== 'string') return false; try { // 检查是否是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 isHoldersTarget = isTokenHoldersApi && hasLimitParam; if (isHoldersTarget) { // 修复: 支持所有链的CA地址提取 const match = url.match(/vas\/api\/v1\/token_holders\/(sol|eth|base|bsc|tron)\/([^/?]+)/i); if (match && match[2]) { const newCA = match[2]; const newChain = match[1].toLowerCase(); // 检测CA变化,重置状态 if (GlobalState.currentCAAddress && GlobalState.currentCAAddress !== newCA) { console.log('[加仓功能] 检测到CA变化:', GlobalState.currentCAAddress, '->', newCA); GlobalState.reset(); } GlobalState.currentCAAddress = newCA; GlobalState.currentChain = newChain; console.log('[加仓功能] 提取CA地址:', newCA, '链:', newChain); // 提取API参数 this.extractApiParams(url); // 新增:提取到API参数后,主动获取K线数据 console.log('[加仓功能] 准备主动获取K线数据...'); setTimeout(() => { this.fetchKlineData(); }, 500); // 延迟500ms确保参数已设置 } } // 注意:移除了K线API的拦截逻辑,改为主动获取 return isHoldersTarget; } catch (err) { console.error('[拦截器] URL判断异常:', err); return false; } }, // 新增:提取API参数 extractApiParams(url) { try { const urlObj = new URL(url, 'https://gmgn.ai'); const params = new URLSearchParams(urlObj.search); const apiParams = { device_id: params.get('device_id'), fp_did: params.get('fp_did'), client_id: params.get('client_id'), from_app: params.get('from_app'), app_ver: params.get('app_ver'), tz_name: params.get('tz_name'), tz_offset: params.get('tz_offset'), app_lang: params.get('app_lang'), os: params.get('os') }; GlobalState.apiParams = apiParams; console.log('[加仓功能] API参数已提取:', apiParams); } catch (err) { console.error('[加仓功能] 提取API参数失败:', err); } }, // 新增:主动获取K线数据 async fetchKlineData() { try { const { currentCAAddress, currentChain, apiParams, klineDataFetched } = GlobalState; // 检查是否已获取过K线数据 if (klineDataFetched) { console.log('[K线获取] K线数据已获取,跳过重复请求'); return; } // 检查必要参数 if (!currentCAAddress) { console.warn('[K线获取] 缺少代币地址'); return; } if (!currentChain) { console.warn('[K线获取] 缺少链网络信息'); return; } if (!apiParams) { console.warn('[K线获取] 缺少API参数,等待参数提取...'); return; } console.log('[K线获取] ========== 开始获取K线数据 =========='); console.log('[K线获取] 代币地址:', currentCAAddress); console.log('[K线获取] 链网络:', currentChain); console.log('[K线获取] API参数:', apiParams); // 构建URL参数 const params = new URLSearchParams({ ...apiParams, resolution: '1h', from: '0', to: Date.now().toString(), limit: '500' }); // 构建完整URL const url = `https://gmgn.ai/api/v1/token_mcap_candles/${currentChain}/${currentCAAddress}?${params.toString()}`; console.log('[K线获取] 请求URL:', url); // 发起请求 const response = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Accept': 'application/json', 'Referer': 'https://gmgn.ai/', 'User-Agent': navigator.userAgent } }); console.log('[K线获取] 响应状态:', response.status, response.statusText); if (!response.ok) { console.error('[K线获取] 请求失败,HTTP状态:', response.status); return; } const data = await response.json(); console.log('[K线获取] ✅ 成功获取K线数据'); console.log('[K线获取] 数据结构:', { hasData: !!data.data, hasList: !!data.data?.list, listLength: data.data?.list?.length || 0 }); // 处理K线数据 if (data && data.data && data.data.list) { console.log('[K线获取] 开始处理K线数据...'); this.processKlineData(data); // 标记K线数据已获取,避免重复请求 GlobalState.klineDataFetched = true; console.log('[K线获取] 已标记K线数据为已获取状态'); } else { console.warn('[K线获取] K线数据格式异常'); } console.log('[K线获取] ========== K线数据获取完成 =========='); } catch (err) { console.error('[K线获取] ❌ 获取K线数据失败:', err); console.error('[K线获取] 错误堆栈:', err.stack); } }, // 新增:处理K线数据 processKlineData(data) { try { console.log('[K线处理] ========== 开始处理K线数据 =========='); if (!data || !data.data || !data.data.list || data.data.list.length === 0) { console.warn('[K线处理] K线数据为空'); return; } const klineList = data.data.list; console.log(`[K线处理] K线数据条数: ${klineList.length}`); console.log('[K线处理] 第一条K线:', klineList[0]); console.log('[K线处理] 最后一条K线:', klineList[klineList.length - 1]); // 存储K线数据 GlobalState.klineData = data; // 计算ATH (历史最高价) let athPrice = 0; let athTimestamp = 0; klineList.forEach(candle => { const high = parseFloat(candle.high); if (high > athPrice) { athPrice = high; athTimestamp = candle.time; } }); GlobalState.athInfo = { price: athPrice, timestamp: athTimestamp }; console.log(`[K线处理] ATH价格: ${athPrice}, 时间: ${new Date(athTimestamp).toLocaleString()}`); // 检测回调60% this.detectRebound60(klineList, athPrice, athTimestamp); console.log('[K线处理] ========== K线数据处理完成 =========='); } catch (err) { console.error('[K线处理] ❌ 处理K线数据失败:', err); console.error('[K线处理] 错误堆栈:', err.stack); } }, // 新增:检测回调60% detectRebound60(klineList, athPrice, athTimestamp) { try { console.log('[加仓功能] 开始检测回调60%...'); // 只检查ATH之后的蜡烛图,且排除ATH所在的那根K线(避免单根K线内的波动) const candlesAfterATH = klineList.filter(c => c.time > athTimestamp); if (candlesAfterATH.length === 0) { console.log('[加仓功能] ATH是最后一根K线,无后续数据'); GlobalState.rebound60Timestamp = null; return; } for (let candle of candlesAfterATH) { const low = parseFloat(candle.low); const reboundPercent = ((athPrice - low) / athPrice) * 100; // 打印每根K线的回调情况(前几根) if (candlesAfterATH.indexOf(candle) < 5) { console.log(`[加仓功能] 时间: ${new Date(candle.time).toLocaleString()}, 最低价: ${low}, 回调: ${reboundPercent.toFixed(2)}%`); } if (reboundPercent >= 60) { GlobalState.rebound60Timestamp = candle.time; console.log(`[加仓功能] ✅ 发现回调60%! 时间: ${new Date(candle.time).toLocaleString()}`); console.log(`[加仓功能] ATH价格: ${athPrice} (${new Date(athTimestamp).toLocaleString()}), 最低价: ${low}, 回调: ${reboundPercent.toFixed(2)}%`); // 延迟触发加仓分析,等待持有者数据加载 console.log('[加仓功能] 等待持有者数据加载...'); setTimeout(() => { this.analyzeAddedPositions(); }, 2000); // 延迟2秒 return; } } console.log('[加仓功能] 未检测到回调60%'); GlobalState.rebound60Timestamp = null; } catch (err) { console.error('[加仓功能] 检测回调60%失败:', err); } }, // 新增:分析加仓地址 async analyzeAddedPositions(retryCount = 0) { const maxRetries = 3; console.log('[加仓功能] 开始分析TOP20加仓情况...'); // 检查是否已完成分析 if (GlobalState.addedPositionCompleted) { console.log('[加仓功能] 已完成分析,跳过重复请求'); return; } // 检查必要条件 if (!GlobalState.rebound60Timestamp) { console.warn('[加仓功能] 未检测到回调60%,无法分析加仓'); GlobalState.addedPositionCompleted = true; // 标记为完成(无需分析) updateStatsDisplay(calculateStats(), false); return; } if (!GlobalState.interceptedData || !GlobalState.interceptedData.data || !GlobalState.interceptedData.data.list) { if (retryCount < maxRetries) { console.warn(`[加仓功能] 持有者数据不可用,${retryCount + 1}/${maxRetries} 次重试,2秒后重试...`); setTimeout(() => this.analyzeAddedPositions(retryCount + 1), 2000); return; } else { console.error('[加仓功能] 持有者数据不可用,已达到最大重试次数'); GlobalState.addedPositionCompleted = true; // 标记为完成(失败) updateStatsDisplay(calculateStats(), false); return; } } if (!GlobalState.apiParams) { console.warn('[加仓功能] API参数未提取,无法查询交易历史'); GlobalState.addedPositionCompleted = true; // 标记为完成(失败) updateStatsDisplay(calculateStats(), false); return; } if (GlobalState.addedPositionLoading) { console.log('[加仓功能] 正在加载中,跳过重复请求'); return; } console.log('[加仓功能] ✅ 所有数据已就绪,开始分析...'); GlobalState.addedPositionLoading = true; // 立即更新UI显示"加载中"状态 updateStatsDisplay(calculateStats(), false); try { const holders = GlobalState.interceptedData.data.list; // 获取TOP20持有者 const top20 = holders .sort((a, b) => b.amount_percentage - a.amount_percentage) .slice(0, 20); console.log(`[加仓功能] TOP20持有者地址:`, top20.map(h => h.address)); // 分批查询,避免限流 (每批5个) const batchSize = 5; const addedAddresses = []; for (let i = 0; i < top20.length; i += batchSize) { const batch = top20.slice(i, i + batchSize); console.log(`[加仓功能] 处理第 ${Math.floor(i / batchSize) + 1} 批,共 ${batch.length} 个地址`); const batchPromises = batch.map(holder => this.checkIfAddedPosition(holder.address) ); const results = await Promise.all(batchPromises); results.forEach((isAdded, index) => { if (isAdded) { addedAddresses.push(batch[index]); } }); // 批次之间延迟500ms if (i + batchSize < top20.length) { await new Promise(resolve => setTimeout(resolve, 500)); } } GlobalState.addedPositionAddresses = addedAddresses; GlobalState.addedPositionCompleted = true; // 标记为完成 console.log(`[加仓功能] ✅ 加仓分析完成!共 ${addedAddresses.length} 个地址加仓`); console.log(`[加仓功能] 加仓地址:`, addedAddresses.map(h => h.address)); // 更新UI显示 updateStatsDisplay(calculateStats(), false); } catch (err) { console.error('[加仓功能] 分析加仓失败:', err); GlobalState.addedPositionCompleted = true; // 即使失败也标记为完成 updateStatsDisplay(calculateStats(), false); } finally { GlobalState.addedPositionLoading = false; } }, // 新增:检查单个地址是否加仓 async checkIfAddedPosition(walletAddress) { try { const { apiParams, currentCAAddress, rebound60Timestamp, currentChain } = GlobalState; // 根据链选择API端点和构建URL let url; if (currentChain === 'sol') { // SOL链使用 vas API const params = new URLSearchParams({ type: 'buy', wallet: walletAddress, token: currentCAAddress, limit: '50', ...apiParams }); // 添加type=sell params.append('type', 'sell'); url = `https://gmgn.ai/vas/api/v1/wallet_activity/sol?${params.toString()}`; } else { // BSC/ETH/BASE/TRON链使用 defi API const params = new URLSearchParams({ wallet: walletAddress, token: currentCAAddress, limit: '50', ...apiParams }); url = `https://gmgn.ai/defi/quotation/v1/wallet_token_activity/${currentChain}?${params.toString()}`; } console.log(`[加仓功能] 查询交易历史: ${walletAddress.substring(0, 8)}... [${currentChain}]`); const response = await fetch(url, { credentials: 'include', headers: { 'Accept': 'application/json' } }); if (!response.ok) { console.warn(`[加仓功能] 请求失败: ${response.status}`); return false; } const data = await response.json(); // 根据链处理不同的响应结构 let activities; if (currentChain === 'sol') { // SOL: data.data.activities if (!data || !data.data || !data.data.activities) { console.warn(`[加仓功能] 无效的响应数据 (SOL)`); return false; } activities = data.data.activities; } else { // BSC/ETH/BASE/TRON: data.data.activities (根据用户提供的BSC API结构) if (!data || !data.data || !data.data.activities) { console.warn(`[加仓功能] 无效的响应数据 (${currentChain})`); return false; } activities = data.data.activities; } // 过滤出基准时间戳之后的交易 const rebound60TimestampSeconds = Math.floor(rebound60Timestamp / 1000); const activitiesAfterRebound = activities.filter( act => act.timestamp >= rebound60TimestampSeconds ); if (activitiesAfterRebound.length === 0) { console.log(`[加仓功能] ${walletAddress.substring(0, 8)}... 回调后无交易`); return false; } // 计算净买入 (兼容不同链的字段名) let totalBuy = 0; let totalSell = 0; activitiesAfterRebound.forEach(act => { // 兼容不同字段名: token_amount (SOL) 或 amount (其他链) const amount = parseFloat(act.token_amount || act.amount || 0); // 兼容不同字段名: event_type (SOL) 或 type (其他链) const eventType = act.event_type || act.type; if (eventType === 'buy') { totalBuy += amount; } else if (eventType === 'sell') { totalSell += amount; } }); const netBuy = totalBuy - totalSell; const isAdded = netBuy > 0; console.log(`[加仓功能] ${walletAddress.substring(0, 8)}... 买入: ${totalBuy.toFixed(2)}, 卖出: ${totalSell.toFixed(2)}, 净买: ${netBuy.toFixed(2)}, 加仓: ${isAdded ? '✅' : '❌'}`); return isAdded; } catch (err) { console.error(`[加仓功能] 检查地址失败:`, err); return false; } }, // 处理响应 (带请求队列管理) async processResponse(response, url = '') { const requestId = ++GlobalState.requestQueue.lastRequestId; GlobalState.requestQueue.pending++; console.log(`[请求队列] 新请求 ID:${requestId}, 待处理:${GlobalState.requestQueue.pending}`); try { let data; if (typeof response === 'string') { // 验证字符串不为空 if (!response || !response.trim()) { console.warn('[拦截器] 响应字符串为空'); return; } try { data = JSON.parse(response); } catch (parseError) { console.error('[拦截器] JSON解析失败:', parseError, '响应内容:', response.substring(0, 200)); return; } } else if (response.json) { data = await response.json(); } else { throw new Error('无效的响应格式'); } // 只处理最新的请求 if (requestId !== GlobalState.requestQueue.lastRequestId) { console.log(`[请求队列] 丢弃过期请求 ID:${requestId}`); return; } // 注意:K线数据已改为主动获取,不再从拦截器处理 // 处理持有者数据 GlobalState.interceptedData = data; const currentStats = calculateStats(); if (currentStats) { if (GlobalState.isFirstLoad) { GlobalState.initialStats = currentStats; GlobalState.initialCAAddress = GlobalState.currentCAAddress; GlobalState.isFirstLoad = false; updateStatsDisplay(currentStats, true); } else { const isSameCA = GlobalState.currentCAAddress === GlobalState.initialCAAddress; updateStatsDisplay(currentStats, !isSameCA); if (!isSameCA) { GlobalState.initialStats = currentStats; GlobalState.initialCAAddress = GlobalState.currentCAAddress; GlobalState.isDownloadInProgress = false; console.log('[状态管理] CA变更,状态已更新'); } } } // 新增:持有者数据加载完成后,检查是否需要触发加仓分析 if (GlobalState.rebound60Timestamp && !GlobalState.addedPositionLoading && !GlobalState.addedPositionCompleted) { console.log('[加仓功能] 持有者数据已就绪,触发加仓分析...'); this.analyzeAddedPositions(); } } catch (err) { console.error('[拦截器] 处理响应错误:', err); } finally { GlobalState.requestQueue.pending--; console.log(`[请求队列] 请求完成 ID:${requestId}, 剩余:${GlobalState.requestQueue.pending}`); } } }; // 兼容旧代码的辅助函数 (已废弃,保留以防引用) function isTargetApi(url) { return ApiInterceptor.isTargetApi(url); } // 计算数据哈希值用于缓存 function getDataHash(data) { return JSON.stringify({ length: data?.data?.list?.length || 0, timestamp: data?.data?.list?.[0]?.last_active_timestamp || 0, caAddress: GlobalState.currentCAAddress }); } // 3. 计算所有统计指标(优化版本 + 缓存过期检查) function calculateStats() { if (!GlobalState.interceptedData?.data?.list) return null; // 检查缓存有效性 const currentHash = getDataHash(GlobalState.interceptedData); if (GlobalState.cache.lastDataHash === currentHash && GlobalState.cache.calculatedStats && !GlobalState.isCacheExpired()) { // 缓存命中,但需要更新addedPosition(因为它独立于持有者数据) const cachedStats = GlobalState.cache.calculatedStats; cachedStats.addedPosition = GlobalState.addedPositionAddresses.length; return cachedStats; } const currentTime = Math.floor(Date.now() / 1000); const sevenDaysInSeconds = 7 * 24 * 60 * 60; // 7天的秒数 const holders = GlobalState.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, // 新增:交易所地址数 addedPosition: 0 // 新增:回调60%后加仓的地址数 }; 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++; } }); // 新增:统计加仓地址数量 stats.addedPosition = GlobalState.addedPositionAddresses.length; // 缓存计算结果 GlobalState.cache.lastDataHash = currentHash; GlobalState.cache.calculatedStats = stats; GlobalState.cache.filteredResults.clear(); // 清空过滤缓存 GlobalState.cache.cacheTime = Date.now(); // 记录缓存时间 return stats; } // ===== DOM监听器管理 (内存泄漏修复) ===== const DOMManager = { observer: null, isObserving: false, targetSelector: '.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full', // 启动监听 startObserving() { if (this.isObserving) { console.warn('[DOM管理] 已在监听中'); return; } this.observer = new MutationObserver(() => { const targetContainer = document.querySelector(this.targetSelector); if (targetContainer && !targetContainer.querySelector('#statistic-gmgn-stats-item')) { injectStatsItem(targetContainer); } }); // 立即检查一次 const initialContainer = document.querySelector(this.targetSelector); if (initialContainer) { injectStatsItem(initialContainer); } // 开始监听 this.observer.observe(document.body, { childList: true, subtree: true, attributes: false }); this.isObserving = true; console.log('[DOM管理] 监听器已启动'); }, // 停止监听 stopObserving() { if (this.observer) { this.observer.disconnect(); this.observer = null; this.isObserving = false; console.log('[DOM管理] 监听器已停止'); } }, // 清理所有DOM元素 cleanup() { const statsItem = document.getElementById('statistic-gmgn-stats-item'); if (statsItem) { statsItem.remove(); console.log('[DOM管理] 统计面板已移除'); } const modals = document.querySelectorAll('.statistic-gmgn-modal'); modals.forEach(modal => modal.remove()); console.log('[DOM管理] 弹窗已清理'); } }; 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 = `
满仓 盈利 亏损 活跃 钻石 新址 7天 5X 可疑 转入 交易所 ${isSol ? '低SOL' : ''} 加仓 图片
- - - - - - - - - - - ${isSol ? '-' : ''} - 下载
`; container.insertAdjacentElement('afterbegin', statsItem); } function updateStatsDisplayInternal(currentStats, forceNoArrows) { if (!currentStats) return; // 确保DOM已存在 const existingElement = document.getElementById('statistic-gmgn-stats-item'); if (!existingElement) { const targetContainer = document.querySelector('.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full'); if (targetContainer) { injectStatsItem(targetContainer); } else { return; } } // 优化的事件监听器绑定(只绑定一次) if (!dataCache.eventsInitialized) { const baseClickableTypes = ['fullPosition', 'profitable', 'losing', 'active24h', 'diamondHands', 'newAddress', 'holdingLessThan7Days', 'highProfit', 'suspicious', 'tokenTransferIn', 'exchangeAddresses', 'addedPosition']; 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, GlobalState.currentCAAddress); } else { const title = getTypeTitle(id); const showSolBalance = id === 'lowSolBalance'; createModal(title, addresses, GlobalState.currentCAAddress, showSolBalance); } }; element.setAttribute('data-event-bound', 'true'); } }); dataCache.eventsInitialized = true; } const updateStatElement = (id, value, hasChanged, isIncrease) => { const element = document.getElementById(id); if (!element) return; // 颜色方案:重点关注盈利、亏损、可疑、7天、5X、新址 let color = '#FFFFFF'; // 默认白色 if (id === 'profitable') color = '#10B981'; // 盈利:绿色 else if (id === 'losing') color = '#EF4444'; // 亏损:红色 else if (id === 'suspicious') color = '#F59E0B'; // 可疑:橙色 else if (id === 'holdingLessThan7Days') color = '#3B82F6'; // 7天:蓝色 else if (id === 'highProfit') color = '#8B5CF6'; // 5X:紫色 else if (id === 'newAddress') color = '#06B6D4'; // 新址:青色 element.innerHTML = `${value}`; // 只有当不是强制不显示箭头且确实有变化时才显示箭头 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 (GlobalState.isDownloadInProgress) 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, initialStats && currentStats.losing !== initialStats.losing, initialStats && currentStats.losing > initialStats.losing); updateStatElement('active24h', currentStats.active24h, initialStats && currentStats.active24h !== initialStats.active24h, initialStats && currentStats.active24h > initialStats.active24h); updateStatElement('diamondHands', currentStats.diamondHands, initialStats && currentStats.diamondHands !== initialStats.diamondHands, initialStats && currentStats.diamondHands > initialStats.diamondHands); updateStatElement('newAddress', currentStats.newAddress, initialStats && currentStats.newAddress !== initialStats.newAddress, initialStats && currentStats.newAddress > initialStats.newAddress); updateStatElement('highProfit', currentStats.highProfit, initialStats && currentStats.highProfit !== initialStats.highProfit, initialStats && currentStats.highProfit > initialStats.highProfit); updateStatElement('suspicious', currentStats.suspicious, initialStats && currentStats.suspicious !== initialStats.suspicious, initialStats && 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); } // 新增:更新加仓地址统计 - 根据加载状态显示不同内容 const addedPositionElement = document.getElementById('addedPosition'); if (addedPositionElement) { if (GlobalState.addedPositionLoading) { // 正在加载中 addedPositionElement.textContent = '...'; addedPositionElement.style.color = '#ffff00'; // 黄色表示加载中 } else if (GlobalState.addedPositionCompleted) { // 已完成,显示数据 updateStatElement('addedPosition', currentStats.addedPosition, initialStats && currentStats.addedPosition !== initialStats.addedPosition, initialStats && currentStats.addedPosition > initialStats.addedPosition); } else { // 未开始 addedPositionElement.textContent = '-'; } } } // 防抖版本的updateStatsDisplay const updateStatsDisplay = debounce(updateStatsDisplayInternal, 200); // 数据收集函数 - 收集基础统计数据和详细持有者信息 function collectStatsData() { if (!GlobalState.interceptedData?.data?.list || !GlobalState.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: GlobalState.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); // 绘制描述文字 - 小字批注 const descriptions = { 'fullPosition': '未卖出且\n未转出', 'profitable': '当前盈利\n的地址', 'losing': '当前亏损\n的地址', 'active24h': '24小时内\n活跃', 'diamondHands': '钻石手\n标记', 'newAddress': '新钱包\n地址', 'holdingLessThan7Days': '持仓不足\n7天', 'highProfit': '5倍以上\n收益', 'suspicious': '可疑交易\n行为', 'tokenTransferIn': '代币转入\n地址', 'exchangeAddresses': '交易所\n关联', 'lowSolBalance': 'SOL余额\n<1' }; const description = descriptions[key] || '数据统计'; const descLines = description.split('\n'); ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.textAlign = 'right'; // 绘制多行描述文字 descLines.forEach((line, index) => { ctx.fillText(line, x + statWidth - 50, y + 35 + (index * 16)); }); 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 = `
📷 统计图片预览
统计图片
`; 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() { GlobalState.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 (GlobalState.isDownloadInProgress) return; // 设置处理状态 GlobalState.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 { // 恢复按钮状态 GlobalState.isDownloadInProgress = false; setDownloadButtonDisabled(false); } } // ===== 插件生命周期管理 ===== const PluginLifecycle = { initialized: false, // 初始化插件 init() { if (this.initialized) { console.warn('[生命周期] 插件已初始化'); return; } try { // 1. 初始化API拦截器(如果还未初始化) if (!ApiInterceptor.initialized) { ApiInterceptor.init(); } // 2. 启动DOM监听 DOMManager.startObserving(); // 3. 监听页面卸载,清理资源 window.addEventListener('beforeunload', () => { this.cleanup(); }); // 4. 监听页面可见性变化,优化性能 document.addEventListener('visibilitychange', () => { if (document.hidden) { console.log('[性能] 页面隐藏,暂停部分功能'); } else { console.log('[性能] 页面可见,恢复功能'); } }); this.initialized = true; console.log('[生命周期] 插件初始化完成'); } catch (err) { console.error('[生命周期] 初始化失败:', err); } }, // 清理所有资源 cleanup() { console.log('[生命周期] 开始清理资源...'); try { // 停止DOM监听 DOMManager.stopObserving(); // 清理DOM元素 DOMManager.cleanup(); // 清理状态 GlobalState.reset(); console.log('[生命周期] 资源清理完成'); } catch (err) { console.error('[生命周期] 清理失败:', err); } }, // 重启插件 restart() { console.log('[生命周期] 重启插件...'); this.cleanup(); this.initialized = false; setTimeout(() => this.init(), 100); } }; // 暴露到window供调试使用 window.GMGN_Stats_Plugin = { state: GlobalState, api: ApiInterceptor, dom: DOMManager, lifecycle: PluginLifecycle, // 调试方法 debug: { getState: () => GlobalState, getCache: () => GlobalState.cache, clearCache: () => GlobalState.clearCache(), restart: () => PluginLifecycle.restart() } }; // ===== 启动插件 ===== // 立即初始化拦截器(必须在任何API请求之前) try { ApiInterceptor.init(); console.log('[生命周期] 拦截器已提前初始化'); } catch (err) { console.error('[生命周期] 拦截器提前初始化失败:', err); } // DOM相关的初始化等待DOM加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => PluginLifecycle.init()); } else { // DOM已加载,立即初始化 PluginLifecycle.init(); } console.log('[GMGN统计插件] 脚本加载完成 v4.9-优化版'); })();