// ==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}
`);
})();