Greasy Fork

Greasy Fork is available in English.

GMGN追踪者统计分析

监听gmgn.ai的持仓者API,提供追踪者数据分析和可视化

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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">&times;</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追踪者统计分析脚本已加载');
})();