您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
监听gmgn.ai的持仓者API,提供追踪者数据分析和可视化
// ==UserScript== // @name GMGN追踪者统计分析 // @namespace http://tampermonkey.net/ // @version 1.1.0 // @description 监听gmgn.ai的持仓者API,提供追踪者数据分析和可视化 // @author Assistant // @match https://gmgn.ai/* // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // 全局数据存储 let followerData = null; let currentToken = null; let currentUrl = window.location.href; // 工具函数:格式化数字为K/M/B单位 function formatNumber(num) { if (num === 0) return '$0'; const absNum = Math.abs(num); const sign = num < 0 ? '-' : ''; if (absNum >= 1e9) { return sign + '$' + (absNum / 1e9).toFixed(1) + 'B'; } else if (absNum >= 1e6) { return sign + '$' + (absNum / 1e6).toFixed(1) + 'M'; } else if (absNum >= 1e3) { return sign + '$' + (absNum / 1e3).toFixed(1) + 'K'; } return sign + '$' + absNum.toFixed(2); } // 工具函数:格式化百分比 function formatPercentage(num) { return (num * 100).toFixed(3) + '%'; } // 数据管理器 class FollowerDataManager { constructor() { this.data = null; this.stats = null; } setData(data) { this.data = data; this.calculateStats(); this.updateButtonState(); } calculateStats() { if (!this.data || !this.data.list) { this.stats = null; return; } const holders = this.data.list; this.stats = { totalAddresses: holders.length, // 所有持有过的地址数 totalHolders: holders.filter(h => h.amount_percentage >= 0.00001).length, // 0.001%以上认为未清仓 profitableHolders: holders.filter(h => h.profit > 0).length, lossHolders: holders.filter(h => h.profit < 0).length, totalHoldingPercentage: holders.reduce((sum, h) => sum + h.amount_percentage, 0), // 所有追踪者数据(包括已清仓的) chartData: holders .sort((a, b) => b.amount_percentage - a.amount_percentage) .map(h => ({ address: h.address, percentage: h.amount_percentage, profit: h.profit, profitRate: (h.profit_change || 0) * 100, netflow: h.netflow_usd, sellAmount: h.sell_amount_cur || 0, balance: h.balance, walletTag: h.wallet_tag_v2 || '', hasHolding: h.amount_percentage >= 0.00001, avgCost: h.avg_cost || 0, avgSold: h.avg_sold || 0 })), // 只有持仓的地址用于饼图 pieChartData: holders .filter(h => h.amount_percentage >= 0.00001) .sort((a, b) => b.amount_percentage - a.amount_percentage) .map(h => ({ address: h.address, percentage: h.amount_percentage, profit: h.profit, profitRate: (h.profit_change || 0) * 100, netflow: h.netflow_usd, sellAmount: h.sell_amount_cur || 0, balance: h.balance, walletTag: h.wallet_tag_v2 || '', avgCost: h.avg_cost || 0, avgSold: h.avg_sold || 0 })) }; } clearData() { this.data = null; this.stats = null; this.updateButtonState(); } updateButtonState() { const button = document.querySelector('#follower-stats-button'); if (button) { if (this.stats && this.stats.totalHolders > 0) { button.style.opacity = '1'; button.style.pointerEvents = 'auto'; button.title = `点击查看 ${this.stats.totalHolders} 个追踪者数据分析`; } else { button.style.opacity = '0.5'; button.style.pointerEvents = 'none'; button.title = '暂无追踪者数据'; } } } getStats() { return this.stats; } } const dataManager = new FollowerDataManager(); // API拦截器 function setupAPIInterception() { // 拦截fetch请求 const originalFetch = window.fetch; window.fetch = async function(...args) { const response = await originalFetch.apply(this, args); const url = args[0]; if (typeof url === 'string' && url.includes('/vas/api/v1/token_holders/') && url.includes('following=true')) { try { const clone = response.clone(); const data = await clone.json(); if (data.code === 0 && data.data && data.data.list) { dataManager.setData(data.data); // 提取代币地址 const tokenMatch = url.match(/token_holders\/sol\/([^?]+)/); if (tokenMatch) { currentToken = tokenMatch[1]; } } } catch (e) { console.log('解析追踪者数据失败:', e); } } return response; }; // 拦截XMLHttpRequest const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url, ...args) { this._url = url; return originalOpen.apply(this, [method, url, ...args]); }; XMLHttpRequest.prototype.send = function(...args) { if (this._url && this._url.includes('/vas/api/v1/token_holders/') && this._url.includes('following=true')) { const originalOnReadyStateChange = this.onreadystatechange; this.onreadystatechange = function() { if (this.readyState === 4 && this.status === 200) { try { const data = JSON.parse(this.responseText); if (data.code === 0 && data.data && data.data.list) { dataManager.setData(data.data); const tokenMatch = this._url.match(/token_holders\/sol\/([^?]+)/); if (tokenMatch) { currentToken = tokenMatch[1]; } } } catch (e) { console.log('解析追踪者数据失败:', e); } } if (originalOnReadyStateChange) { return originalOnReadyStateChange.apply(this, arguments); } }; } return originalSend.apply(this, args); }; } // 创建按钮 function createFollowerStatsButton() { const button = document.createElement('div'); button.id = 'follower-stats-button'; button.className = 'h-[28px] flex items-center text-[12px] font-medium cursor-pointer bg-btn-secondary p-6px rounded-6px gap-2px text-text-200 hover:text-text-100'; button.style.opacity = '0.5'; button.style.pointerEvents = 'none'; button.title = '暂无追踪者数据'; button.innerHTML = ` <svg width="12px" height="12px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"> <path d="M8 2C4.691 2 2 4.691 2 8s2.691 6 6 6 6-2.691 6-6-2.691-6-6-6zm0 10.5c-2.481 0-4.5-2.019-4.5-4.5S5.519 3.5 8 3.5 12.5 5.519 12.5 8 10.481 12.5 8 12.5z"/> <path d="M8 5.5c-1.381 0-2.5 1.119-2.5 2.5S6.619 10.5 8 10.5 10.5 9.381 10.5 8 9.381 5.5 8 5.5zm0 3.5c-0.551 0-1-0.449-1-1s0.449-1 1-1 1 0.449 1 1-0.449 1-1 1z"/> </svg> 追踪者分析 `; button.addEventListener('click', () => { const stats = dataManager.getStats(); if (stats && stats.totalHolders > 0) { showStatsModal(stats); } }); return button; } // 甜甜圈图组件 function createPieChart(data, containerId) { const container = document.getElementById(containerId); if (!container) return; const size = 280; const center = size / 2; const outerRadius = size * 0.4; const innerRadius = size * 0.25; // 内圆半径,创建空心效果 // 计算总和用于百分比计算 const total = data.reduce((sum, item) => sum + item.percentage, 0); let currentAngle = -90; // 从顶部开始 const segments = []; // 只显示前10个最大的持仓者,其他合并为"其他" const displayData = data.slice(0, 10); const otherData = data.slice(10); if (otherData.length > 0) { const otherSum = otherData.reduce((sum, item) => sum + item.percentage, 0); displayData.push({ address: 'Others', percentage: otherSum, profit: otherData.reduce((sum, item) => sum + item.profit, 0), profitRate: 0, netflow: otherData.reduce((sum, item) => sum + item.netflow, 0), sellAmount: otherData.reduce((sum, item) => sum + item.sellAmount, 0), isOther: true }); } // 生成颜色 const colors = [ '#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff', '#5f27cd', '#00d2d3', '#ff9f43', '#c7ecee' ]; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', size); svg.setAttribute('height', size); svg.style.cursor = 'pointer'; displayData.forEach((item, index) => { const percentage = item.percentage / total; const angle = percentage * 360; if (angle < 0.1) return; // 跳过太小的片段 const startAngle = currentAngle * Math.PI / 180; const endAngle = (currentAngle + angle) * Math.PI / 180; // 外圆弧点 const outerX1 = center + outerRadius * Math.cos(startAngle); const outerY1 = center + outerRadius * Math.sin(startAngle); const outerX2 = center + outerRadius * Math.cos(endAngle); const outerY2 = center + outerRadius * Math.sin(endAngle); // 内圆弧点 const innerX1 = center + innerRadius * Math.cos(startAngle); const innerY1 = center + innerRadius * Math.sin(startAngle); const innerX2 = center + innerRadius * Math.cos(endAngle); const innerY2 = center + innerRadius * Math.sin(endAngle); const largeArcFlag = angle > 180 ? 1 : 0; const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); // 创建甜甜圈形状的路径 const pathData = [ `M ${outerX1} ${outerY1}`, `A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${outerX2} ${outerY2}`, `L ${innerX2} ${innerY2}`, `A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${innerX1} ${innerY1}`, 'Z' ].join(' '); path.setAttribute('d', pathData); path.setAttribute('fill', colors[index % colors.length]); path.setAttribute('stroke', '#1a1a1a'); path.setAttribute('stroke-width', '2'); path.style.transition = 'all 0.3s ease'; path.style.transformOrigin = `${center}px ${center}px`; // 添加悬停效果 path.addEventListener('mouseenter', (e) => { path.style.transform = 'scale(1.05)'; path.style.filter = 'brightness(1.1)'; showTooltip(e, item); }); path.addEventListener('mouseleave', () => { path.style.transform = 'scale(1)'; path.style.filter = 'brightness(1)'; hideTooltip(); }); path.addEventListener('mousemove', (e) => { updateTooltipPosition(e); }); svg.appendChild(path); currentAngle += angle; }); // 中心文字 const centerText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); centerText.setAttribute('x', center); centerText.setAttribute('y', center - 8); centerText.setAttribute('text-anchor', 'middle'); centerText.setAttribute('dominant-baseline', 'middle'); centerText.setAttribute('fill', '#fff'); centerText.setAttribute('font-size', '14'); centerText.setAttribute('font-weight', 'bold'); centerText.textContent = '总持仓占比'; const centerValue = document.createElementNS('http://www.w3.org/2000/svg', 'text'); centerValue.setAttribute('x', center); centerValue.setAttribute('y', center + 12); centerValue.setAttribute('text-anchor', 'middle'); centerValue.setAttribute('dominant-baseline', 'middle'); centerValue.setAttribute('fill', '#4ecdc4'); centerValue.setAttribute('font-size', '16'); centerValue.setAttribute('font-weight', 'bold'); centerValue.textContent = formatPercentage(total); svg.appendChild(centerText); svg.appendChild(centerValue); container.innerHTML = ''; container.appendChild(svg); } // 工具提示 let tooltip = null; function showTooltip(event, data) { hideTooltip(); tooltip = document.createElement('div'); tooltip.className = 'follower-tooltip'; tooltip.innerHTML = ` <div class="tooltip-header"> ${data.isOther ? '其他持仓者' : `${data.address.substring(0, 8)}...`} ${data.walletTag ? `<span class="wallet-tag">${data.walletTag}</span>` : ''} </div> <div class="tooltip-row"> <span>持仓占比:</span> <span class="tooltip-value">${formatPercentage(data.percentage)}</span> </div> <div class="tooltip-row"> <span>总利润:</span> <span class="tooltip-value ${data.profit >= 0 ? 'profit-positive' : 'profit-negative'}">${formatNumber(data.profit)}</span> </div> <div class="tooltip-row"> <span>利润率:</span> <span class="tooltip-value ${data.profitRate >= 0 ? 'profit-positive' : 'profit-negative'}">${data.profitRate.toFixed(2)}%</span> </div> <div class="tooltip-row"> <span>净流入:</span> <span class="tooltip-value">${formatNumber(data.netflow)}</span> </div> <div class="tooltip-row"> <span>总卖出:</span> <span class="tooltip-value">${formatNumber(data.sellAmount)}</span> </div> `; document.body.appendChild(tooltip); updateTooltipPosition(event); } function updateTooltipPosition(event) { if (!tooltip) return; const rect = tooltip.getBoundingClientRect(); let x = event.clientX + 10; let y = event.clientY + 10; // 确保工具提示不会超出视口 if (x + rect.width > window.innerWidth) { x = event.clientX - rect.width - 10; } if (y + rect.height > window.innerHeight) { y = event.clientY - rect.height - 10; } tooltip.style.left = x + 'px'; tooltip.style.top = y + 'px'; } function hideTooltip() { if (tooltip) { tooltip.remove(); tooltip = null; } } // 复制地址功能 function copyToClipboard(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(() => { showNotification('地址已复制到剪贴板'); }).catch(() => { fallbackCopy(text); }); } else { fallbackCopy(text); } } function fallbackCopy(text) { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.opacity = '0'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); showNotification('地址已复制到剪贴板'); } catch (err) { showNotification('复制失败,请手动复制'); } document.body.removeChild(textArea); } // 显示通知 function showNotification(message) { const notification = document.createElement('div'); notification.className = 'follower-notification'; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.classList.add('show'); }, 10); setTimeout(() => { notification.classList.remove('show'); setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); }, 2000); } // 创建用户列表项 function createUserListItem(user, index) { const profitClass = user.profit >= 0 ? 'profit-positive' : 'profit-negative'; const itemClass = user.hasHolding ? '' : ' no-holding'; return ` <div class="user-list-item${itemClass}" data-index="${index}"> <div class="address-column"> <div class="address-display" onclick="copyAddressWithAnimation(this, '${user.address}')" title="点击复制地址: ${user.address}"> ${user.address.substring(0, 6)}...${user.address.substring(user.address.length - 4)} </div> </div> <div class="price-column"> <div class="price-buy" title="平均买价">${user.avgCost && user.avgCost > 0 ? '$' + user.avgCost.toFixed(6) : '-'}</div> <div class="price-sell" title="平均卖价">${user.avgSold && user.avgSold > 0 ? '$' + user.avgSold.toFixed(6) : '-'}</div> </div> <div class="profit-column"> <div class="profit-amount ${profitClass}">${formatNumber(user.profit)}</div> <div class="profit-rate ${profitClass}">${user.profitRate.toFixed(2)}%</div> </div> <div class="challenge-column"> <button class="challenge-btn" onclick="window.open('https://gmgn.ai/sol/address/${user.address}', '_blank')" title="跳转到该用户"> <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"> <path d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/> <path d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/> </svg> </button> </div> </div> `; } // 模态框 function showStatsModal(stats) { // 创建用户列表HTML const userListHTML = stats.chartData.map((user, index) => createUserListItem(user, index)).join(''); // 创建模态框 const modal = document.createElement('div'); modal.className = 'follower-modal-overlay'; modal.innerHTML = ` <div class="follower-modal"> <div class="modal-header"> <h2>追踪者数据分析</h2> <button class="modal-close">×</button> </div> <div class="modal-content"> <div class="stats-summary"> <div class="stat-item"> <div class="stat-value">${stats.totalAddresses}</div> <div class="stat-label">总地址数</div> </div> <div class="stat-item"> <div class="stat-value">${stats.totalHolders}</div> <div class="stat-label">未清仓地址</div> </div> <div class="stat-item profit"> <div class="stat-value">${stats.profitableHolders}</div> <div class="stat-label">盈利地址</div> </div> <div class="stat-item loss"> <div class="stat-value">${stats.lossHolders}</div> <div class="stat-label">亏损地址</div> </div> </div> <div class="chart-container"> <div class="chart-title">持仓分布图</div> <div class="chart-content"> <div class="user-list-container"> <div class="user-list-header"> <div class="header-address">地址</div> <div class="header-price">平均买价/平均卖价</div> <div class="header-profit">收益</div> <div class="header-challenge">跳转</div> </div> <div class="user-list" id="user-list"> ${userListHTML} </div> </div> <div class="chart-right"> <div id="pie-chart-container"></div> </div> </div> </div> </div> </div> `; // 添加关闭事件 const closeBtn = modal.querySelector('.modal-close'); closeBtn.addEventListener('click', () => { modal.remove(); }); modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); // 将复制函数挂载到window上,供内联事件使用 window.copyToClipboard = copyToClipboard; document.body.appendChild(modal); // 添加地址点击复制功能 window.copyAddressWithAnimation = function(element, address) { // 复制地址 copyToClipboard(address); // 添加点击动画 element.classList.add('address-clicked'); setTimeout(() => { element.classList.remove('address-clicked'); }, 300); }; // 创建饼图(使用有持仓的数据) setTimeout(() => { if (stats.pieChartData && stats.pieChartData.length > 0) { createPieChart(stats.pieChartData, 'pie-chart-container'); } else { console.log('没有有效的饼图数据:', stats); } }, 100); } // 添加样式 function addStyles() { const style = document.createElement('style'); style.textContent = ` /* 模态框样式 */ .follower-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 10000; backdrop-filter: blur(5px); animation: fadeIn 0.3s ease; } .follower-modal { background: #1a1a1a; border-radius: 12px; width: 90%; max-width: 1000px; max-height: 90vh; overflow: hidden; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); animation: slideIn 0.3s ease; border: 1px solid #333; } .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid #333; background: #222; } .modal-header h2 { color: #fff; margin: 0; font-size: 18px; font-weight: 600; } .modal-close { background: none; border: none; color: #999; font-size: 24px; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: all 0.2s ease; } .modal-close:hover { background: #333; color: #fff; } .modal-content { padding: 24px; overflow-y: auto; max-height: calc(90vh - 80px); } .stats-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 32px; } .stat-item { text-align: center; padding: 16px; background: #2a2a2a; border-radius: 8px; border: 1px solid #333; min-width: 0; } .stat-item.profit { border-color: #4ecdc4; background: linear-gradient(135deg, #2a2a2a, #1f3a3a); } .stat-item.loss { border-color: #ff6b6b; background: linear-gradient(135deg, #2a2a2a, #3a1f1f); } .stat-value { font-size: 24px; font-weight: bold; color: #fff; margin-bottom: 4px; } .stat-item.profit .stat-value { color: #4ecdc4; } .stat-item.loss .stat-value { color: #ff6b6b; } .stat-label { font-size: 12px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; } .chart-container { text-align: center; } .chart-title { font-size: 16px; font-weight: 600; color: #fff; margin-bottom: 20px; } .chart-content { display: flex; gap: 20px; align-items: flex-start; height: 400px; } .user-list-container { flex: 0 0 50%; min-width: 0; height: 100%; display: flex; flex-direction: column; } .user-list-header { display: grid; grid-template-columns: 1fr 100px 90px 50px; gap: 6px; padding: 8px 12px; background: #2a2a2a; border-radius: 8px 8px 0 0; border: 1px solid #333; font-size: 11px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.5px; flex-shrink: 0; } .header-address { text-align: left; } .header-price { text-align: center; } .header-profit { text-align: center; } .header-challenge { text-align: center; } .user-list { flex: 1; overflow-y: auto; border: 1px solid #333; border-top: none; border-radius: 0 0 8px 8px; background: #1e1e1e; min-height: 0; } .user-list::-webkit-scrollbar { width: 6px; } .user-list::-webkit-scrollbar-track { background: #2a2a2a; } .user-list::-webkit-scrollbar-thumb { background: #4ecdc4; border-radius: 3px; } .user-list::-webkit-scrollbar-thumb:hover { background: #45b7d1; } .user-list-item { display: grid; grid-template-columns: 1fr 100px 90px 50px; gap: 6px; padding: 8px 12px; border-bottom: 1px solid #333; align-items: center; transition: all 0.2s ease; } .user-list-item.no-holding { opacity: 0.4; background: #1a1a1a; } .user-list-item.no-holding:hover { opacity: 0.6; background: #202020; } .user-list-item:hover { background: #252525; } .user-list-item:last-child { border-bottom: none; } .challenge-column { display: flex; justify-content: center; } .challenge-btn { width: 28px; height: 28px; border: none; border-radius: 4px; background: linear-gradient(135deg, #4ecdc4, #45b7d1); color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; box-shadow: 0 1px 4px rgba(76, 205, 196, 0.3); } .challenge-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(76, 205, 196, 0.4); } .challenge-btn:active { transform: translateY(0); } .price-column { display: flex; flex-direction: column; align-items: center; gap: 2px; } .price-buy { font-size: 10px; color: #4ecdc4; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; } .price-sell { font-size: 10px; color: #ff9f43; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; } .user-list-item.no-holding .price-buy, .user-list-item.no-holding .price-sell { opacity: 0.6; } .profit-column { display: flex; flex-direction: column; align-items: center; gap: 4px; } .profit-amount { font-size: 14px; font-weight: 600; } .profit-rate { font-size: 12px; opacity: 0.8; } .address-column { display: flex; align-items: center; justify-content: flex-start; } .address-display { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 11px; color: #fff; background: #333; padding: 6px 10px; border-radius: 4px; border: 1px solid #444; cursor: pointer; transition: all 0.2s ease; user-select: none; } .address-display:hover { background: #4ecdc4; color: #000; transform: translateY(-1px); } .address-display.address-clicked { animation: addressClickAnimation 0.3s ease; } @keyframes addressClickAnimation { 0% { transform: scale(1); background: #4ecdc4; } 50% { transform: scale(1.1); background: #45b7d1; } 100% { transform: scale(1); background: #4ecdc4; } } .chart-right { flex: 0 0 50%; display: flex; justify-content: center; align-items: center; height: 100%; } #pie-chart-container { display: flex; justify-content: center; align-items: center; height: 100%; width: 100%; } /* 通知样式 */ .follower-notification { position: fixed; top: 20px; right: 20px; background: #4ecdc4; color: #000; padding: 12px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; box-shadow: 0 4px 12px rgba(76, 205, 196, 0.3); z-index: 10002; opacity: 0; transform: translateX(100%); transition: all 0.3s ease; } .follower-notification.show { opacity: 1; transform: translateX(0); } /* 工具提示样式 */ .follower-tooltip { position: fixed; background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 12px; z-index: 10001; max-width: 280px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); pointer-events: none; animation: tooltipIn 0.2s ease; } .tooltip-header { color: #fff; font-weight: 600; margin-bottom: 8px; font-size: 14px; display: flex; align-items: center; gap: 8px; } .wallet-tag { background: #4ecdc4; color: #000; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: bold; text-transform: uppercase; } .tooltip-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; font-size: 12px; } .tooltip-row span:first-child { color: #999; } .tooltip-value { color: #fff; font-weight: 500; } .profit-positive { color: #4ecdc4 !important; } .profit-negative { color: #ff6b6b !important; } /* 动画 */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideIn { from { opacity: 0; transform: translateY(-20px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes tooltipIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } /* 响应式设计 */ @media (max-width: 768px) { .follower-modal { width: 95%; margin: 20px; max-width: none; } .stats-summary { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 24px; } .stat-item { min-width: auto; padding: 12px; } .modal-content { padding: 16px; } .chart-content { flex-direction: column; gap: 16px; height: auto; } .chart-right { flex: none; } .user-list-container { flex: none; height: 250px; } .user-list-header { grid-template-columns: 1fr 70px 60px 35px; gap: 3px; padding: 6px 8px; font-size: 9px; } .user-list-item { grid-template-columns: 1fr 70px 60px 35px; gap: 3px; padding: 6px 8px; } .challenge-btn { width: 24px; height: 24px; } .address-display { font-size: 9px; padding: 4px 6px; } .profit-amount { font-size: 12px; } .profit-rate { font-size: 10px; } #pie-chart-container svg { width: 250px; height: 250px; } .follower-notification { top: 10px; right: 10px; left: 10px; transform: translateY(-100%); } .follower-notification.show { transform: translateY(0); } } /* 按钮悬停效果增强 */ #follower-stats-button { transition: all 0.2s ease; position: relative; overflow: hidden; } #follower-stats-button:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(76, 205, 196, 0.2); } #follower-stats-button::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); transition: left 0.5s ease; } #follower-stats-button:hover::before { left: 100%; } `; document.head.appendChild(style); } // 监控URL变化 function monitorUrlChange() { const observer = new MutationObserver(() => { if (window.location.href !== currentUrl) { currentUrl = window.location.href; dataManager.clearData(); currentToken = null; } }); observer.observe(document.body, { childList: true, subtree: true }); // 监听popstate事件 window.addEventListener('popstate', () => { if (window.location.href !== currentUrl) { currentUrl = window.location.href; dataManager.clearData(); currentToken = null; } }); } // 插入按钮到页面 function insertButton() { const buttonContainer = document.querySelector('.flex.absolute.top-0.right-0.gap-8px.pl-4px'); if (buttonContainer && !document.querySelector('#follower-stats-button')) { const button = createFollowerStatsButton(); buttonContainer.insertBefore(button, buttonContainer.firstChild); } } // 初始化 function init() { addStyles(); setupAPIInterception(); monitorUrlChange(); // 等待页面加载后插入按钮 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(insertButton, 1000); }); } else { setTimeout(insertButton, 1000); } // 定期检查按钮是否存在,如果不存在则重新插入 setInterval(() => { if (document.querySelector('.flex.absolute.top-0.right-0.gap-8px.pl-4px') && !document.querySelector('#follower-stats-button')) { insertButton(); } }, 2000); } // 启动脚本 init(); console.log('GMGN追踪者统计分析脚本已加载'); })();