您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
获取GMGN.ai前100持仓者的MemeRadar标注信息
// ==UserScript== // @name GMGN.ai 前排标注查询工具 // @namespace http://tampermonkey.net/ // @version 2.0 // @description 获取GMGN.ai前100持仓者的MemeRadar标注信息 // @author 专业油猴脚本开发者 // @match https://gmgn.ai/* // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @connect plugin.chaininsight.vip // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @license MIT // ==/UserScript== (function() { 'use strict'; console.log('[标注查询] MemeRadar标注查询工具已启动'); // 全局变量 let currentCA = ''; // 当前代币合约地址 let currentChain = ''; // 当前链网络 let topHolders = []; // 前排持仓者地址列表 let tagData = []; // 标注数据 let isDataReady = false; // 数据是否就绪 let isFetchingTags = false; // 是否正在获取标注 let hasInterceptedTags = false; // 是否已拦截到标注数据 let hasInterceptedHolders = false; // 是否已拦截到持仓者数据 let interceptedCA = ''; // 已拦截的CA地址 // 链网络映射 const chainMapping = { 'sol': 'Solana', 'eth': 'Ethereum', 'base': 'Base', 'bsc': 'bsc', // tron 不支持 }; // 立即设置XHR拦截 setupXhrInterception(); // DOM加载完成后初始化UI if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeUI); } else { setTimeout(initializeUI, 100); } /** * 设置XHR请求拦截 */ function setupXhrInterception() { console.log('[请求拦截] 开始设置token_holders请求拦截'); // 避免重复设置 if (window._memeradarInterceptionSetup) { console.log('[请求拦截] 检测到已存在拦截设置,跳过重复设置'); return; } const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._url = url; this._method = method; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { const url = this._url; // 监听token_holders请求 if (url && url.includes('/vas/api/v1/token_holders/')) { // 解析链网络和CA地址 const urlMatch = url.match(/\/token_holders\/([^\/]+)\/([^?]+)/); if (!urlMatch) { console.warn('[请求拦截] ⚠️无法解析token_holders URL:', url); return originalSend.apply(this, arguments); } const chain = urlMatch[1]; const ca = urlMatch[2]; // 检查是否已经拦截过这个CA if (hasInterceptedHolders && interceptedCA === ca) { console.log(`[请求拦截] 📋已拦截过CA ${ca} 的持仓者数据,跳过重复拦截`); return originalSend.apply(this, arguments); } console.log('[请求拦截] 🎯捕获到token_holders请求:', url); console.log(`[数据解析] 链网络: ${chain}, CA地址: ${ca}`); // 检查CA是否变化 if (currentCA && currentCA !== ca) { console.log('[数据重置] 检测到CA地址变化,清除所有数据'); resetAllData(); } currentChain = chain; currentCA = ca; console.log('currentChain', currentChain); console.log('currentCA', currentCA); this.addEventListener('load', function() { if (this.status === 200) { console.log('[请求拦截] ✅token_holders请求成功'); try { const response = JSON.parse(this.responseText); if (response.code === 0 && response.data && response.data.list) { processTokenHolders(response.data.list); // 标记已拦截成功 hasInterceptedHolders = true; interceptedCA = ca; console.log(`[拦截完成] ✅已完成CA ${ca} 的持仓者数据拦截,后续请求将被跳过`); } else { console.warn('[数据处理] ⚠️token_holders返回数据格式异常:', response); } } catch (error) { console.error('[数据处理] ❌解析token_holders响应失败:', error); } } else { console.error('[请求拦截] ❌token_holders请求失败,状态码:', this.status); } }); } // 监听wallet_tags_v2请求 if (url && url.includes('/api/v0/util/query/wallet_tags_v2')) { // 检查是否已经拦截过标注数据(针对当前CA) if (hasInterceptedTags && currentCA) { console.log(`[请求拦截] 📋已拦截过CA ${currentCA} 的标注数据,跳过重复拦截`); return originalSend.apply(this, arguments); } console.log('[请求拦截] 🎯捕获到wallet_tags_v2请求:', url); this.addEventListener('load', function() { if (this.status === 200) { console.log('[请求拦截] ✅wallet_tags_v2请求成功'); try { const response = JSON.parse(this.responseText); console.log('[标注拦截] wallet_tags_v2响应数据:', response); if (response.code === 0 && response.data) { console.log('[标注拦截] ✅成功拦截到标注数据,开始处理'); // 确保有持仓者数据才处理 if (topHolders && topHolders.length > 0) { processInterceptedTagData(response.data); hasInterceptedTags = true; updateButtonState(); console.log('[标注拦截] ✅标注数据处理完成,已更新按钮状态'); } else { console.warn('[标注拦截] ⚠️持仓者数据尚未准备,延迟处理标注数据'); // 保存标注数据,等待持仓者数据准备完成 window._pendingTagData = response.data; } } else { console.warn('[数据处理] ⚠️wallet_tags_v2返回数据格式异常:', response); console.log('[数据处理] 响应码:', response.code, '消息:', response.msg); } } catch (error) { console.error('[数据处理] ❌解析wallet_tags_v2响应失败:', error); } } else { console.error('[请求拦截] ❌wallet_tags_v2请求失败,状态码:', this.status); } }); this.addEventListener('error', function(error) { console.error('[请求拦截] ❌wallet_tags_v2网络请求错误:', error); }); } return originalSend.apply(this, arguments); }; window._memeradarInterceptionSetup = true; console.log('[请求拦截] ✅XHR拦截设置完成'); } /** * 处理持仓者数据 */ function processTokenHolders(holdersList) { console.log(`[数据处理] 开始处理持仓者列表,总数量: ${holdersList.length}`); // 提取前100个地址 topHolders = holdersList.slice(0, 100).map(holder => holder.address); isDataReady = true; console.log(`[数据处理] ✅已提取前${topHolders.length}个持仓者地址`); console.log('[数据处理] 前5个地址示例:', topHolders.slice(0, 5)); // 检查是否有待处理的标注数据 if (window._pendingTagData) { console.log('[数据处理] 🔄发现待处理的标注数据,开始处理'); processInterceptedTagData(window._pendingTagData); hasInterceptedTags = true; window._pendingTagData = null; // 清除待处理数据 console.log('[数据处理] ✅待处理标注数据处理完成'); } // 更新按钮状态 updateButtonState(); } /** * 处理拦截到的标注数据 */ function processInterceptedTagData(responseData) { console.log('[拦截数据] 开始处理拦截到的标注数据'); if (!responseData || !responseData.walletTags) { console.warn('[拦截数据] 响应数据格式异常'); return; } // 创建地址到标注的映射 const tagMap = {}; responseData.walletTags.forEach(wallet => { tagMap[wallet.address] = { count: wallet.count || 0, tags: wallet.tags ? wallet.tags.map(tag => tag.tagName) : [], expertTags: wallet.expertTags ? wallet.expertTags.map(tag => tag.tagName) : [] }; }); // 为所有地址创建完整的标注数据 tagData = topHolders.map(address => { const walletTags = tagMap[address] || { count: 0, tags: [], expertTags: [] }; return { address: address, tagCount: walletTags.count, tags: walletTags.tags, expertTags: walletTags.expertTags }; }); // 按标注数量降序排序 tagData.sort((a, b) => b.tagCount - a.tagCount); console.log(`[拦截数据] ✅处理完成,共${tagData.length}个地址,有标注的地址:${tagData.filter(w => w.tagCount > 0).length}个`); } /** * 重置所有数据 */ function resetAllData() { console.log('[数据重置] 🔄开始重置所有数据和拦截状态'); currentCA = ''; currentChain = ''; topHolders = []; tagData = []; isDataReady = false; isFetchingTags = false; hasInterceptedTags = false; hasInterceptedHolders = false; interceptedCA = ''; // 清除待处理的标注数据 if (window._pendingTagData) { window._pendingTagData = null; } console.log('[数据重置] ✅所有数据和拦截状态已重置,可开始新一轮拦截'); } /** * 初始化UI界面 */ function initializeUI() { console.log('[UI初始化] 开始初始化用户界面'); addStyles(); setupUI(); console.log('[UI初始化] ✅用户界面初始化完成'); } /** * 添加CSS样式 */ function addStyles() { GM_addStyle(` .memeradar-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 6px; padding: 6px 12px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; margin-right: 8px; min-width: 80px; height: 32px; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3); } .memeradar-btn:disabled { background: #94a3b8; cursor: not-allowed; transform: none; box-shadow: none; } .memeradar-btn.fetching { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); animation: pulse 2s infinite; } .memeradar-btn.ready { background: linear-gradient(135deg, #10b981 0%, #059669 100%); } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } .memeradar-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 100000; } .memeradar-modal-content { background-color: #1e293b !important; border-radius: 8px !important; width: 80% !important; max-width: 800px !important; max-height: 80vh !important; overflow-y: auto !important; padding: 20px !important; color: white !important; position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5) !important; margin: 0 !important; z-index: 100000 !important; box-sizing: border-box !important; min-height: auto !important; min-width: 300px !important; pointer-events: auto !important; } .memeradar-modal-header { display: flex !important; justify-content: space-between !important; align-items: center !important; margin-bottom: 16px !important; padding: 0 !important; } .memeradar-modal-title { font-size: 18px !important; font-weight: 600 !important; color: white !important; margin: 0 !important; } .memeradar-modal-subtitle { color: #94a3b8 !important; font-size: 14px !important; } .memeradar-header-actions { display: flex !important; align-items: center !important; gap: 8px !important; } .memeradar-export-btn { background: #3b82f6 !important; color: white !important; border: none !important; padding: 6px 12px !important; border-radius: 4px !important; cursor: pointer !important; font-size: 14px !important; } .memeradar-export-btn:hover { background: #2563eb !important; } .memeradar-modal-close { background: none !important; border: none !important; color: #94a3b8 !important; font-size: 20px !important; cursor: pointer !important; padding: 5px !important; line-height: 1 !important; width: 30px !important; height: 30px !important; display: flex !important; align-items: center !important; justify-content: center !important; } .memeradar-modal-close:hover { color: #ff4444 !important; background-color: rgba(255, 255, 255, 0.1) !important; border-radius: 4px !important; } .memeradar-analysis-summary { margin-bottom: 16px; padding: 12px; background-color: #263238; border-radius: 6px; display: flex; justify-content: space-between; align-items: center; } .memeradar-summary-stats { display: flex; gap: 20px; } .memeradar-stat-item { display: flex; align-items: baseline; } .memeradar-stat-label { color: #94a3b8; margin-right: 5px; } .memeradar-stat-value { font-weight: 600; color: #3b82f6; } .memeradar-result-item { background-color: #334155; border-radius: 6px; padding: 12px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; } .memeradar-wallet-info { flex: 1; } .memeradar-wallet-address { color: #3b82f6; font-family: monospace; font-size: 14px; cursor: pointer; margin-bottom: 4px; } .memeradar-tags { display: flex; flex-wrap: wrap; gap: 4px; } .memeradar-tag { background: rgba(59, 130, 246, 0.2); color: #93c5fd; padding: 2px 6px; border-radius: 3px; font-size: 12px; } .memeradar-expert-tag { background: rgba(249, 115, 22, 0.2); color: #fb923c; padding: 2px 6px; border-radius: 3px; font-size: 12px; } .memeradar-tag-count { color: #10b981; font-weight: 600; margin-left: 12px; min-width: 40px; text-align: center; } .memeradar-modal-close { background: rgba(255, 255, 255, 0.2); border: none; color: white; font-size: 18px; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; } .memeradar-modal-body { padding: 12px; /* 移除滾動條,自適應內容高度 */ overflow: visible; /* 性能優化 */ contain: layout style paint; transform: translateZ(0); } .memeradar-stats { background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 6px; padding: 10px; margin-bottom: 12px; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; } .memeradar-stat-item { text-align: center; } .memeradar-stat-label { color: #94a3b8; font-size: 10px; margin-bottom: 2px; } .memeradar-stat-value { color: #3b82f6; font-size: 16px; font-weight: 600; } .memeradar-export-btn { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none; border-radius: 4px; padding: 4px 8px; font-size: 11px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 3px; height: 28px; box-shadow: 0 1px 3px rgba(16, 185, 129, 0.3); } .memeradar-wallets-table { width: 100%; border-collapse: collapse; font-size: 11px; /* 表格性能優化 */ table-layout: fixed; contain: layout style paint; transform: translateZ(0); /* 進一步性能優化 */ will-change: auto; backface-visibility: hidden; } .memeradar-wallets-table th { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; padding: 8px 6px; text-align: left; font-weight: 600; font-size: 10px; border-bottom: 1px solid rgba(59, 130, 246, 0.3); position: sticky; top: 0; z-index: 10; } .memeradar-wallets-table th:first-child { width: 45%; } .memeradar-wallets-table th:nth-child(2) { width: 10%; text-align: center; } .memeradar-wallets-table th:last-child { width: 45%; } .memeradar-wallets-table td { padding: 6px; border-bottom: 1px solid rgba(100, 116, 139, 0.2); vertical-align: top; background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.9) 100%); } .memeradar-wallets-table tr:nth-child(even) td { background: linear-gradient(135deg, rgba(30, 41, 59, 0.9) 0%, rgba(51, 65, 85, 0.9) 100%); } .memeradar-wallet-address { font-family: 'Courier New', monospace; color: #e2e8f0; font-size: 13px; cursor: pointer; padding: 2px 4px; background: rgba(15, 23, 42, 0.8); border-radius: 3px; word-break: break-all; display: inline-block; width: 100%; } .memeradar-tag-count { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; padding: 2px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; box-shadow: 0 1px 2px rgba(245, 158, 11, 0.3); display: inline-block; } .memeradar-tags-container { display: flex; flex-wrap: wrap; gap: 2px; line-height: 1.3; } .memeradar-tag { background: linear-gradient(135deg, #c084fc 0%, #a855f7 100%); color: white; padding: 2px 6px; border-radius: 10px; font-size: 12px; font-weight: 500; white-space: nowrap; box-shadow: 0 1px 2px rgba(192, 132, 252, 0.3); } .memeradar-expert-tag { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; padding: 2px 6px; border-radius: 10px; font-size: 12px; font-weight: 500; white-space: nowrap; box-shadow: 0 1px 2px rgba(59, 130, 246, 0.3); position: relative; } .memeradar-expert-tag::before { content: "⭐"; margin-right: 2px; } .memeradar-no-tags { color: #94a3b8; font-style: italic; font-size: 10px; } .memeradar-loading { text-align: center; padding: 40px; color: #94a3b8; } .memeradar-error { background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); color: #ef4444; padding: 12px; border-radius: 6px; margin-bottom: 16px; font-size: 14px; } `); } /** * 设置UI界面 */ function setupUI() { const observer = new MutationObserver(() => { const targetContainer = document.querySelector('.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full'); if (targetContainer && !targetContainer.querySelector('#memeradar-btn')) { injectButton(targetContainer); } }); observer.observe(document.body, { childList: true, subtree: true }); } /** * 注入按钮到页面 */ function injectButton(container) { const button = document.createElement('button'); button.id = 'memeradar-btn'; button.className = 'memeradar-btn'; button.textContent = '获取前排标注'; container.insertAdjacentElement('afterbegin', button); button.addEventListener('click', handleButtonClick); console.log('[UI注入] ✅前排标注按钮已注入'); } /** * 处理按钮点击事件 */ async function handleButtonClick() { const button = document.getElementById('memeradar-btn'); if (isFetchingTags) { console.log('[按钮点击] 正在获取标注中,忽略点击'); return; } // 检查数据是否就绪 if (!isDataReady || !topHolders.length) { showErrorModal('数据尚未就绪', '请等待页面加载完成,或刷新页面重试。\n\n可能原因:\n1. 页面数据还在加载中\n2. 网络请求被拦截失败\n3. 当前页面不是代币详情页'); return; } // 检查链网络是否支持 if (!chainMapping[currentChain]) { showErrorModal('不支持的链网络', `当前链网络 "${currentChain}" 暂不支持标注查询。\n\n支持的链网络:\n• Solana (sol)\n• Ethereum (eth)\n• Base (base)\n• BSC (bsc)`); return; } // 如果已有标注数据(无论是拦截的还是API获取的),直接显示 if (tagData.length > 0) { showTagsModal(); return; } // 调试信息:显示当前状态 console.log('[按钮点击] 当前数据状态检查:'); console.log(' hasInterceptedTags:', hasInterceptedTags); console.log(' tagData.length:', tagData.length); console.log(' topHolders.length:', topHolders.length); console.log(' window._pendingTagData:', !!window._pendingTagData); // 如果已拦截到标注数据但还没处理完成,提示用户稍等 if (hasInterceptedTags && tagData.length === 0) { showErrorModal('数据处理中', '已检测到标注数据,正在处理中,请稍候...'); return; } // 开始通过API获取标注数据 isFetchingTags = true; button.className = 'memeradar-btn fetching'; button.textContent = '获取中...'; try { console.log(`[API获取] 开始通过API获取${topHolders.length}个地址的标注信息`); await fetchWalletTags(); button.className = 'memeradar-btn ready'; button.textContent = '查看标注'; console.log('[API获取] ✅标注数据获取完成'); showTagsModal(); } catch (error) { console.error('[API获取] ❌获取标注数据失败:', error); showErrorModal('获取失败', `标注数据获取失败:${error.message}\n\n请检查网络连接或稍后重试。`); button.className = 'memeradar-btn'; button.textContent = '获取前排标注'; } finally { isFetchingTags = false; } } /** * 获取钱包标注数据 */ async function fetchWalletTags() { const chainName = chainMapping[currentChain]; const requestData = { walletAddresses: topHolders, chain: chainName }; console.log(`[API请求] 发送标注查询请求,链网络: ${chainName}, 地址数量: ${topHolders.length}`); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://plugin.chaininsight.vip/api/v0/util/query/wallet_tags_v2', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, data: JSON.stringify(requestData), timeout: 30000, onload: function(response) { console.log(`[API响应] 状态码: ${response.status}`); if (response.status !== 200) { reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); return; } try { const data = JSON.parse(response.responseText); console.log('[API响应] 响应数据:', data); if (data.code !== 0) { reject(new Error(data.msg || `API错误码: ${data.code}`)); return; } processTagData(data.data); resolve(); } catch (error) { console.error('[API响应] JSON解析失败:', error); reject(new Error('响应数据解析失败')); } }, onerror: function(error) { console.error('[API请求] 网络请求失败:', error); reject(new Error('网络请求失败')); }, ontimeout: function() { console.warn('[API请求] 请求超时'); reject(new Error('请求超时')); } }); }); } /** * 处理标注数据 */ function processTagData(responseData) { console.log('[数据处理] 开始处理标注数据'); if (!responseData || !responseData.walletTags) { console.warn('[数据处理] 响应数据格式异常'); tagData = []; return; } // 创建地址到标注的映射 const tagMap = {}; responseData.walletTags.forEach(wallet => { tagMap[wallet.address] = { count: wallet.count || 0, tags: wallet.tags ? wallet.tags.map(tag => tag.tagName) : [], expertTags: wallet.expertTags ? wallet.expertTags.map(tag => tag.tagName) : [] }; }); // 为所有地址创建完整的标注数据 tagData = topHolders.map(address => { const walletTags = tagMap[address] || { count: 0, tags: [], expertTags: [] }; return { address: address, tagCount: walletTags.count, tags: walletTags.tags, expertTags: walletTags.expertTags }; }); // 按标注数量降序排序 tagData.sort((a, b) => b.tagCount - a.tagCount); console.log(`[数据处理] ✅处理完成,共${tagData.length}个地址,有标注的地址:${tagData.filter(w => w.tagCount > 0).length}个`); } /** * 更新按钮状态 */ function updateButtonState() { const button = document.getElementById('memeradar-btn'); if (!button) return; if (!isDataReady) { button.disabled = true; button.textContent = '等待数据...'; button.className = 'memeradar-btn'; } else if (hasInterceptedTags && tagData.length > 0) { // 已拦截到标注数据,可直接查看 button.disabled = false; button.textContent = '查看标注'; button.className = 'memeradar-btn ready'; } else if (tagData.length > 0) { // 已获取标注数据,可查看 button.disabled = false; button.textContent = '查看标注'; button.className = 'memeradar-btn ready'; } else { // 需要获取标注数据 button.disabled = false; button.textContent = '获取前排标注'; button.className = 'memeradar-btn'; } } /** * 显示错误弹窗 */ function showErrorModal(title, message) { const modal = document.createElement('div'); modal.className = 'memeradar-modal'; modal.innerHTML = ` <div class="memeradar-modal-content" style="max-width: 500px;"> <div class="memeradar-modal-header" style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);"> <h3 class="memeradar-modal-title">${title}</h3> <button class="memeradar-modal-close">×</button> </div> <div class="memeradar-modal-body"> <div style="color: #e2e8f0; line-height: 1.6; white-space: pre-line;">${message}</div> </div> </div> `; document.body.appendChild(modal); // 绑定关闭事件 modal.querySelector('.memeradar-modal-close').addEventListener('click', () => { document.body.removeChild(modal); }); modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); } }); } /** * 分批渲染錢包列表(大數據量優化) */ function renderWalletsBatched(walletsList, walletsWithTags) { const BATCH_SIZE = 50; let currentIndex = 0; // 創建錢包行的函數(分批渲染用) function createWalletRowBatch(wallet) { return createWalletRow(wallet); } // 分批渲染函數 function renderBatch() { const fragment = document.createDocumentFragment(); const endIndex = Math.min(currentIndex + BATCH_SIZE, walletsWithTags.length); for (let i = currentIndex; i < endIndex; i++) { const walletRow = createWalletRowBatch(walletsWithTags[i]); fragment.appendChild(walletRow); } walletsList.appendChild(fragment); currentIndex = endIndex; // 如果還有未渲染的項目,繼續下一批 if (currentIndex < walletsWithTags.length) { // 使用 requestAnimationFrame 確保不阻塞主線程 requestAnimationFrame(renderBatch); } else { console.log('[性能優化] 分批渲染完成'); // 渲染完成後添加事件委託 setupWalletListEvents(walletsList); } } // 開始渲染 renderBatch(); } /** * 創建錢包表格行 */ function createWalletRow(wallet) { const resultItem = document.createElement('div'); resultItem.className = 'memeradar-result-item'; // 錢包信息容器 const walletInfo = document.createElement('div'); walletInfo.className = 'memeradar-wallet-info'; // 錢包地址 const walletAddress = document.createElement('div'); walletAddress.className = 'memeradar-wallet-address'; walletAddress.title = '點擊複製地址'; walletAddress.textContent = wallet.address; walletAddress.dataset.address = wallet.address; // 標籤容器 const tagsContainer = document.createElement('div'); tagsContainer.className = 'memeradar-tags'; // 添加專業玩家標籤 if (wallet.expertTags && wallet.expertTags.length > 0) { wallet.expertTags.forEach(tag => { const expertTag = document.createElement('span'); expertTag.className = 'memeradar-expert-tag'; expertTag.textContent = tag; tagsContainer.appendChild(expertTag); }); } // 添加普通標籤 wallet.tags.forEach(tag => { const regularTag = document.createElement('span'); regularTag.className = 'memeradar-tag'; regularTag.textContent = tag; tagsContainer.appendChild(regularTag); }); // 标注數量 const tagCount = document.createElement('div'); tagCount.className = 'memeradar-tag-count'; tagCount.textContent = wallet.tagCount; // 組裝 walletInfo.appendChild(walletAddress); walletInfo.appendChild(tagsContainer); resultItem.appendChild(walletInfo); resultItem.appendChild(tagCount); return resultItem; } /** * 設置錢包列表事件委託 */ function setupWalletListEvents(walletsList) { walletsList.addEventListener('click', (e) => { const addressElement = e.target.closest('.memeradar-wallet-address'); if (addressElement) { const address = addressElement.dataset.address; navigator.clipboard.writeText(address).then(() => { const originalColor = addressElement.style.color; const originalBg = addressElement.style.background; addressElement.style.color = '#10b981'; addressElement.style.background = 'rgba(16, 185, 129, 0.1)'; setTimeout(() => { addressElement.style.color = originalColor; addressElement.style.background = originalBg; }, 1000); }).catch(err => { console.warn('[複製失敗]', err); }); } }); } /** * 显示标注数据弹窗 */ function showTagsModal() { console.log('[界面显示] 显示标注数据弹窗'); // 只显示有标注的钱包(包括有专业玩家标注的) const walletsWithTags = tagData.filter(w => w.tagCount > 0 || (w.expertTags && w.expertTags.length > 0)); const hasTagsCount = walletsWithTags.length; const totalTags = walletsWithTags.reduce((sum, w) => sum + w.tagCount, 0); const expertTaggedAddressCount = walletsWithTags.filter(w => w.expertTags && w.expertTags.length > 0).length; const modal = document.createElement('div'); modal.className = 'memeradar-modal'; modal.innerHTML = ` <div class="memeradar-modal-content"> <div class="memeradar-modal-header"> <h3 class="memeradar-modal-title">前排标注信息<span class="memeradar-modal-subtitle"> - ${currentCA.slice(0, 8)}...${currentCA.slice(-6)}</span></h3> <div class="memeradar-header-actions"> <button class="memeradar-export-btn" id="export-excel-btn">📊 導出Excel</button> <button class="memeradar-modal-close">×</button> </div> </div> <div class="memeradar-analysis-summary"> <div class="memeradar-summary-stats"> <div class="memeradar-stat-item"> <div class="memeradar-stat-label">总地址數:</div> <div class="memeradar-stat-value">${tagData.length}</div> </div> <div class="memeradar-stat-item"> <div class="memeradar-stat-label">有标注地址:</div> <div class="memeradar-stat-value">${hasTagsCount}</div> </div> <div class="memeradar-stat-item"> <div class="memeradar-stat-label">总标注數:</div> <div class="memeradar-stat-value">${totalTags}</div> </div> <div class="memeradar-stat-item"> <div class="memeradar-stat-label">专业玩家标注地址數:</div> <div class="memeradar-stat-value">${expertTaggedAddressCount}</div> </div> </div> </div> ${walletsWithTags.length === 0 ? '<div class="memeradar-loading">📝 暂无标注数据</div>' : '<div id="wallets-list"></div>' } </div> `; document.body.appendChild(modal); // 填充钱包列表 - 只显示有标注的钱包(卡片版本) if (walletsWithTags.length === 0) { return; // 無數據時直接返回 } const walletsList = modal.querySelector('#wallets-list'); // 大數據量時使用分批渲染,提升性能 const BATCH_SIZE = 100; const shouldUseBatchRendering = walletsWithTags.length > BATCH_SIZE; if (shouldUseBatchRendering) { console.log(`[性能優化] 檢測到${walletsWithTags.length}個錢包項目,啟用分批渲染`); renderWalletsBatched(walletsList, walletsWithTags); return; } // 使用DocumentFragment批量操作,避免多次DOM重排 const fragment = document.createDocumentFragment(); // 批量创建所有钱包行 const walletRows = walletsWithTags.map(wallet => { return createWalletRow(wallet); }); // 批量添加到fragment walletRows.forEach(row => fragment.appendChild(row)); // 一次性添加到DOM walletsList.appendChild(fragment); // 設置事件委託 setupWalletListEvents(walletsList); // 绑定导出Excel按钮事件 const exportBtn = modal.querySelector('#export-excel-btn'); if (exportBtn) { exportBtn.addEventListener('click', exportToExcel); } // 绑定关闭事件 const closeModal = () => { if (modal && modal.parentNode) { document.body.removeChild(modal); } }; // 關閉按鈕事件 const closeBtn = modal.querySelector('.memeradar-modal-close'); if (closeBtn) { closeBtn.addEventListener('click', (e) => { e.stopPropagation(); closeModal(); }); } // 背景點擊關閉 modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }); // ESC 鍵關閉 const handleKeyDown = (e) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', handleKeyDown); } }; document.addEventListener('keydown', handleKeyDown); } /** * 导出数据到Excel */ function exportToExcel() { try { console.log('[Excel导出] 开始导出标注数据'); // 只导出有标注的地址(包括专业玩家标注) const walletsWithTags = tagData.filter(wallet => wallet.tagCount > 0 || (wallet.expertTags && wallet.expertTags.length > 0)); console.log(`[Excel导出] 过滤后数据:总地址${tagData.length}个,有标注${walletsWithTags.length}个`); if (walletsWithTags.length === 0) { alert('没有找到有标注的地址,无法导出Excel文件'); return; } // 准备Excel数据 - 只包含有标注的地址 const excelData = walletsWithTags.map((wallet, index) => ({ '排名': index + 1, '钱包地址': wallet.address, '标注数量': wallet.tagCount, '标签列表': wallet.tags.join(', '), '专业玩家打标': (wallet.expertTags && wallet.expertTags.length > 0) ? wallet.expertTags.join(',') : '' })); // 创建工作簿 const wb = XLSX.utils.book_new(); // 创建标注数据工作表 const ws = XLSX.utils.json_to_sheet(excelData); XLSX.utils.book_append_sheet(wb, ws, "标注数据"); // 生成文件名 const now = new Date(); const timeStr = now.toISOString().slice(0, 19).replace(/[:\-T]/g, ''); const caShort = currentCA.slice(0, 8) + '...' + currentCA.slice(-6); const fileName = `${timeStr}-前排标注-${caShort}.xlsx`; // 下载文件 XLSX.writeFile(wb, fileName); console.log(`[Excel导出] ✅Excel文件导出成功: ${fileName},包含${walletsWithTags.length}个有标注地址`); // 显示成功提示 const exportBtn = document.getElementById('export-excel-btn'); if (exportBtn) { const originalText = exportBtn.innerHTML; exportBtn.innerHTML = '✅ 导出成功'; exportBtn.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)'; setTimeout(() => { exportBtn.innerHTML = originalText; exportBtn.style.background = ''; }, 2000); } } catch (error) { console.error('[Excel导出] ❌导出失败:', error); alert('Excel导出失败: ' + error.message); } } // 页面切换监听 - 精确检测CA地址变化 let lastUrl = location.href; let lastCA = ''; function checkCAChange() { const url = location.href; // 提取当前URL中的CA地址 - 支持多链 let urlCA = ''; const caMatch = url.match(/\/(sol|eth|bsc|base|tron)\/([A-Za-z0-9]{32,})/); if (caMatch) { urlCA = caMatch[2]; // CA地址 // 也可以获取链网络: caMatch[1] } // 检查CA是否变化 if (lastCA && lastCA !== urlCA && urlCA) { console.log(`[页面切换] 🔄检测到CA地址变化: ${lastCA} → ${urlCA}`); console.log(`[页面切换] 完整URL变化: ${lastUrl} → ${url}`); resetAllData(); updateButtonState(); lastCA = urlCA; lastUrl = url; return true; } else if (urlCA && !lastCA) { // 首次进入代币页面 console.log(`[页面切换] 🎯首次进入代币页面: ${urlCA}`); lastCA = urlCA; lastUrl = url; return false; } else if (!urlCA && lastCA) { // 离开代币页面 console.log(`[页面切换] 🚪离开代币页面: ${lastCA}`); resetAllData(); updateButtonState(); lastCA = ''; lastUrl = url; return true; } else if (url !== lastUrl) { // URL变化但CA未变化(如参数变化) console.log(`[页面切换] 📝URL变化但CA未变(${urlCA || '无CA'}): ${url}`); lastUrl = url; return false; } return false; } // 监听页面变化 new MutationObserver(() => { checkCAChange(); }).observe(document, { subtree: true, childList: true }); // 监听浏览器前进后退 window.addEventListener('popstate', () => { setTimeout(checkCAChange, 100); // 延迟检查,确保URL已更新 }); // 初始化检查 checkCAChange(); console.log(` 🏷️ MemeRadar前排标注查询工具 v2.3 已启动 📋 v2.3优化更新: • 📈 Excel导出优化 - 只导出有标注的地址,精简数据 • 🎯 智能数据过滤 - 避免导出无价值的空标注数据 • ⚠️ 异常处理增强 - 无标注数据时给出友好提示 📋 v2.2重大优化: • 🎯 智能单次拦截 - token_holders成功后不再重复拦截 • 🔄 精确CA切换检测 - 支持多链地址变化监听 • 🚪 智能页面状态管理 - 进入/离开代币页面自动处理 • 📊 减少不必要拦截 - 大幅提升性能和稳定性 • 🛡️ 防重复请求机制 - 避免资源浪费 📋 核心功能: • 🎯 智能拦截token_holders和wallet_tags_v2 API • 🌐 支持多链网络 (SOL/ETH/BSC/BASE/TRON) • 🏷️ 获取前100持仓者标注信息 • 📊 优雅的数据展示界面 (只显示有标注地址) • 📈 精简的Excel数据导出功能 (仅含有标注地址) • 🔄 精确的CA切换检测和状态重置 🔍 监听状态: 已启用 📍 当前页面: ${window.location.href} `); })();