您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
新一代复盘助手,更精美的UI、更高效的复盘
// ==UserScript== // @name 图寻复盘助手 // @namespace http://tampermonkey.net/ // @version 0.8 // @description 新一代复盘助手,更精美的UI、更高效的复盘 // @author zbmin // @match https://tuxun.fun/* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // 调试模式开关 const DEBUG_MODE = true; // 百度地图API密钥 // 获取方式:https://lbsyun.baidu.com/apiconsole/key,完成实名认证后,创建应用,选择应用类型浏览器端,IP白名单填写*,完成后复制AK const BAIDU_AK = '请在此处输入你的百度开发者AK'; // 日志函数 function log(...args) { if (DEBUG_MODE) { console.log('[图寻小助手-Log]', ...args); } } function error(...args) { if (DEBUG_MODE) { console.error('[图寻小助手-Err]', ...args); } } // 保存原始的fetch函数 const originalFetch = window.fetch; // 保存原始的XMLHttpRequest const OriginalXHR = window.XMLHttpRequest; // 创建调试面板元素 function createDebugPanel() { log('创建调试面板...'); const panel = document.createElement('div'); panel.id = 'tuxun-debug-panel'; panel.className = 'fixed top-[30px] left-4 w-[50%] max-w-[1200px] bg-white/95 rounded-lg shadow-xl z-50 overflow-hidden border border-gray-200 transition-all duration-300 hidden'; // 面板头部 const header = document.createElement('div'); header.className = 'bg-gray-800 text-white px-4 py-2 flex justify-between items-center'; header.innerHTML = ` <div class="flex items-center flex-wrap gap-2"> <h3 class="font-semibold mr-3">图寻复盘助手 v0.8</h3> <span id="tuxun-status" class="text-xs bg-gray-600 text-white px-2 py-0.5 rounded-full">等待请求</span> <span id="tuxun-map-type" class="text-xs bg-gray-600 text-white px-2 py-0.5 rounded-full ml-2">未检测</span> </div> <div class="flex space-x-2"> <button id="tuxun-close-btn" class="text-white hover:text-gray-300 transition-colors"> <i class="fa fa-times"></i> </button> </div> `; // 面板内容区域 - 分为左右两栏 const content = document.createElement('div'); content.id = 'tuxun-debug-content'; content.className = 'flex flex-col md:flex-row max-h-[70vh] overflow-hidden'; // 左侧:原始响应 const rawSection = document.createElement('div'); rawSection.id = 'tuxun-raw-section'; rawSection.className = 'md:w-1/2 p-4 overflow-y-auto bg-gray-50 border-r border-gray-200'; rawSection.innerHTML = ` <div class="flex items-center justify-between mb-2"> <h4 class="font-medium text-gray-800">原始响应</h4> <span id="tuxun-raw-size" class="text-xs text-gray-500">0 KB</span> </div> <pre id="tuxun-raw-response" class="bg-gray-800 text-gray-100 p-3 rounded overflow-x-auto text-xs">等待响应...</pre> `; // 右侧:解析后信息 const parsedSection = document.createElement('div'); parsedSection.id = 'tuxun-parsed-section'; parsedSection.className = 'md:w-1/2 p-4 overflow-y-auto bg-gray-50'; parsedSection.innerHTML = ` <div class="flex items-center justify-between mb-2"> <h4 class="font-medium text-gray-800">解析后信息</h4> <span id="tuxun-parsed-time" class="text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full"> 未捕获请求 </span> </div> <div id="tuxun-parsed-content"> <p class="text-gray-500 italic">等待街景...</p> </div> `; content.appendChild(rawSection); content.appendChild(parsedSection); // 面板底部 const footer = document.createElement('div'); footer.className = 'bg-gray-100 px-4 py-2 flex justify-between items-center border-t border-gray-200'; footer.innerHTML = ` <div class="flex items-center space-x-3"> <button id="tuxun-copy-raw-btn" class="text-sm bg-gray-600 hover:bg-gray-700 text-white px-3 py-1 rounded transition-colors"> <i class="fa fa-copy mr-1"></i> 复制原始 </button> <button id="tuxun-copy-parsed-btn" class="text-sm bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded transition-colors"> <i class="fa fa-copy mr-1"></i> 复制解析 </button> </div> <div class="flex items-center space-x-2"> <span class="text-xs text-gray-500">调试模式: <span id="tuxun-debug-mode">开启</span></span> <button id="tuxun-refresh-btn" class="text-sm bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded transition-colors"> <i class="fa fa-refresh mr-1"></i> 重置 </button> </div> `; panel.appendChild(header); panel.appendChild(content); panel.appendChild(footer); document.body.appendChild(panel); // 添加事件监听器 document.getElementById('tuxun-close-btn').addEventListener('click', closePanel); document.getElementById('tuxun-copy-raw-btn').addEventListener('click', () => copyToClipboard('tuxun-raw-response', '原始响应')); document.getElementById('tuxun-copy-parsed-btn').addEventListener('click', () => copyToClipboard('tuxun-parsed-content', '解析后信息')); document.getElementById('tuxun-refresh-btn').addEventListener('click', resetDebugger); log('调试面板创建完成'); return panel; } // 创建重新打开按钮 function createReopenButton() { const button = document.createElement('button'); button.id = 'tuxun-reopen-btn'; button.className = 'fixed top-4 left-4 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded shadow-lg z-50 transition-all duration-300 hover:scale-110'; button.innerHTML = '<i class="fa fa-bug mr-1"></i> 复盘工具'; button.addEventListener('click', openPanel); document.body.appendChild(button); return button; } // 打开面板 function openPanel() { const panel = document.getElementById('tuxun-debug-panel'); const reopenBtn = document.getElementById('tuxun-reopen-btn'); if (panel) { panel.classList.remove('hidden'); reopenBtn.classList.add('hidden'); log('面板已重新打开'); //showNotification('调试面板已重新打开'); } } // 关闭面板 function closePanel() { const panel = document.getElementById('tuxun-debug-panel'); const reopenBtn = document.getElementById('tuxun-reopen-btn'); if (panel) { panel.classList.add('hidden'); reopenBtn.classList.remove('hidden'); log('面板已关闭'); } } // 复制内容到剪贴板 function copyToClipboard(elementId, type) { const element = document.getElementById(elementId); const text = element.textContent || element.innerText; navigator.clipboard.writeText(text) .then(() => { showNotification(`${type}已复制到剪贴板`); log(`${type}复制成功`); }) .catch(err => { showNotification(`复制${type}失败: ${err.message}`, true); error(`复制${type}失败`, err); }); } // 显示通知 function showNotification(message, isError = false) { const notification = document.createElement('div'); notification.className = `fixed top-4 left-1/2 transform -translate-x-1/2 px-4 py-2 rounded shadow-lg z-50 transition-all duration-300 transform translate-y-10 opacity-0 ${ isError ? 'bg-red-500 text-white' : 'bg-green-500 text-white' }`; notification.textContent = message; document.body.appendChild(notification); // 显示通知 setTimeout(() => { notification.classList.remove('translate-y-10', 'opacity-0'); }, 10); // 隐藏通知 setTimeout(() => { notification.classList.add('translate-y-10', 'opacity-0'); setTimeout(() => notification.remove(), 200); }, 3000); } // 格式化JSON响应为HTML function formatJsonResponse(json) { return JSON.stringify(json, null, 2) .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(match) { let cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="json-' + cls + '">' + match + '</span>'; }); } // 创建解析后的响应内容 function createParsedContent(responseData, mapType) { const parsedContent = document.getElementById('tuxun-parsed-content'); parsedContent.innerHTML = ''; // 地图类型标识 let mapTypeBadge; let mapTypeColor; if (mapType === 'baidu') { mapTypeBadge = '百度街景'; mapTypeColor = 'bg-blue-100 text-blue-800'; } else if (mapType === 'tencent') { mapTypeBadge = '腾讯街景'; mapTypeColor = 'bg-green-100 text-green-800'; } else if (mapType === 'google') { mapTypeBadge = '谷歌街景'; mapTypeColor = 'bg-red-100 text-red-800'; } const mapTypeBadgeHtml = `<span class="inline-block px-2 py-0.5 ${mapTypeColor} text-xs rounded-full mb-2">${mapTypeBadge}</span>`; // 添加基本信息卡片 const infoCard = document.createElement('div'); infoCard.className = 'bg-white rounded-lg shadow-sm p-4 mb-4 border border-gray-200'; let lat, lng; // 百度和腾讯街景信息解析 if (mapType === 'baidu' || mapType === 'tencent') { lat = responseData.data.lat; lng = responseData.data.lng; infoCard.innerHTML = ` ${mapTypeBadgeHtml} <h5 class="font-medium text-gray-800 mb-3">基本信息</h5> <div class="grid grid-cols-2 gap-3"> <div class="bg-gray-50 p-2 rounded"> <span class="text-xs text-gray-500 block">Pano ID</span> <div class="font-medium text-gray-800 break-all"><span class="cursor-pointer hover:underline" onclick="navigator.clipboard.writeText('${responseData.data.pano}').then(() => showNotification('Pano ID已复制')).catch(err => showNotification('复制失败', true))">${responseData.data.pano}</span></div> </div> <div class="bg-gray-50 p-2 rounded"> <span class="text-xs text-gray-500 block">坐标 (WGS84)</span> <span class="font-medium text-gray-800">${lat.toFixed(6)}, ${lng.toFixed(6)}</span> </div> <div class="bg-gray-50 p-2 rounded"> <span class="text-xs text-gray-500 block">坐标 (BD09)</span> <span class="font-medium text-gray-800">${responseData.data.bd09Lat.toFixed(6)}, ${responseData.data.bd09Lng.toFixed(6)}</span> </div> <div class="bg-gray-50 p-2 rounded"> <span class="text-xs text-gray-500 block">初始方向</span> <span class="font-medium text-gray-800">${responseData.data.centerHeading}°</span> </div> </div> `; } // 谷歌街景信息解析 else if (mapType === 'google') { // 解析谷歌街景数据 const panoId = responseData[1][0][1][1]; const location = responseData[1][0][5][1][2]; lat = location[2]; lng = location[3]; const heading = responseData[1][0][5][1][4]; const address = responseData[1][0][3][2][0][0]; const copyright = responseData[1][0][4][0][0][0]; infoCard.innerHTML = ` ${mapTypeBadgeHtml} <h5 class="font-medium text-gray-800 mb-3">基本信息</h5> <div class="grid grid-cols-2 gap-3"> <div class="bg-gray-50 p-2 rounded"> <span class="text-xs text-gray-500 block">Pano ID</span> <div class="font-medium text-gray-800 break-all"><span class="cursor-pointer hover:underline" onclick="navigator.clipboard.writeText('${panoId}').then(() => showNotification('Pano ID已复制')).catch(err => showNotification('复制失败', true))">${panoId}</span></div> </div> <div class="bg-gray-50 p-2 rounded"> <span class="text-xs text-gray-500 block">坐标</span> <span class="font-medium text-gray-800">${lat.toFixed(6)}, ${lng.toFixed(6)}</span> </div> <div class="bg-gray-50 p-2 rounded"> <span class="text-xs text-gray-500 block">初始方向</span> <span class="font-medium text-gray-800">${heading}°</span> </div> <div class="bg-gray-50 p-2 rounded"> <span class="text-xs text-gray-500 block">地址</span> <span class="font-medium text-gray-800">${address}</span> </div> </div> <div class="mt-3 bg-gray-50 p-2 rounded"> <span class="text-xs text-gray-500 block">版权信息</span> <span class="font-medium text-gray-800">${copyright}</span> </div> `; } parsedContent.appendChild(infoCard); // 百度和腾讯街景相邻街景信息 if (mapType === 'baidu' || mapType === 'tencent') { // 添加相邻街景信息 const linksSection = document.createElement('div'); linksSection.className = 'bg-white rounded-lg shadow-sm p-4 mb-4 border border-gray-200'; linksSection.innerHTML = ` <div class="flex items-center justify-between mb-3"> <h5 class="font-medium text-gray-800">相邻街景 (${responseData.data.links.length})</h5> <span class="text-xs text-gray-500">点击复制Pano ID</span> </div> <div id="tuxun-links-list" class="space-y-2"> ${responseData.data.links.map((link, index) => ` <div class="bg-gray-50 p-3 rounded-lg border border-gray-200 hover:border-blue-300 transition-colors cursor-pointer" data-pano="${link.pano}"> <div class="flex justify-between"> <span class="font-medium text-gray-800">街景 ${index + 1}</span> <span class="text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full"> ${link.heading.toFixed(2)}° </span> </div> <div class="mt-1 grid grid-cols-2 gap-2"> <div> <span class="text-xs text-gray-500 block">Pano ID</span> <div class="text-sm text-gray-800 break-all">${link.pano}</div> </div> <div> <span class="text-xs text-gray-500 block">目标方向</span> <span class="text-sm text-gray-800">${link.centerHeading}°</span> </div> </div> </div> `).join('')} </div> `; parsedContent.appendChild(linksSection); // 为每个相邻街景添加点击事件 document.querySelectorAll('#tuxun-links-list > div').forEach(link => { link.addEventListener('click', () => { const pano = link.getAttribute('data-pano'); navigator.clipboard.writeText(pano) .then(() => showNotification(`已复制Pano ID: ${pano}`)) .catch(err => showNotification('复制失败', true)); }); }); } // 添加位置解析信息 addLocationInfoSection(lat, lng, mapType); // 添加响应结构分析 const structureSection = document.createElement('div'); structureSection.className = 'bg-white rounded-lg shadow-sm p-4 border border-gray-200'; if (mapType === 'baidu' || mapType === 'tencent') { structureSection.innerHTML = ` <h5 class="font-medium text-gray-800 mb-3">响应结构分析</h5> <div class="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div class="bg-blue-50 p-3 rounded-lg border border-blue-200"> <div class="text-blue-600 text-2xl font-bold">${Object.keys(responseData).length}</div> <div class="text-xs text-blue-700 mt-1">顶层字段</div> </div> <div class="bg-green-50 p-3 rounded-lg border border-green-200"> <div class="text-green-600 text-2xl font-bold">${Object.keys(responseData.data).length}</div> <div class="text-xs text-green-700 mt-1">数据字段</div> </div> <div class="bg-yellow-50 p-3 rounded-lg border border-yellow-200"> <div class="text-yellow-600 text-2xl font-bold">${responseData.data.links.length}</div> <div class="text-xs text-yellow-700 mt-1">相邻节点</div> </div> <div class="bg-purple-50 p-3 rounded-lg border border-purple-200"> <div class="text-purple-600 text-2xl font-bold">${JSON.stringify(responseData).length}</div> <div class="text-xs text-purple-700 mt-1">字符长度</div> </div> </div> `; } else if (mapType === 'google') { structureSection.innerHTML = ` <h5 class="font-medium text-gray-800 mb-3">响应结构分析</h5> <div class="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div class="bg-blue-50 p-3 rounded-lg border border-blue-200"> <div class="text-blue-600 text-2xl font-bold">${responseData.length}</div> <div class="text-xs text-blue-700 mt-1">顶层元素</div> </div> <div class="bg-green-50 p-3 rounded-lg border border-green-200"> <div class="text-green-600 text-2xl font-bold">${responseData[1][0].length}</div> <div class="text-xs text-green-700 mt-1">数据字段</div> </div> <div class="bg-yellow-50 p-3 rounded-lg border border-yellow-200"> <div class="text-yellow-600 text-2xl font-bold">${responseData[1][0][5][1].length}</div> <div class="text-xs text-yellow-700 mt-1">位置信息</div> </div> <div class="bg-purple-50 p-3 rounded-lg border border-purple-200"> <div class="text-purple-600 text-2xl font-bold">${JSON.stringify(responseData).length}</div> <div class="text-xs text-purple-700 mt-1">字符长度</div> </div> </div> `; } parsedContent.appendChild(structureSection); log('解析后内容已生成'); } // 添加位置信息区域 function addLocationInfoSection(lat, lng, mapType) { const parsedContent = document.getElementById('tuxun-parsed-content'); // 创建位置信息卡片 const locationCard = document.createElement('div'); locationCard.className = 'bg-white rounded-lg shadow-sm p-4 mt-4 border border-gray-200'; locationCard.innerHTML = ` <div class="flex items-center justify-between mb-3"> <h5 class="font-medium text-gray-800">位置解析</h5> <div class="flex items-center space-x-2"> <span id="tuxun-location-status" class="text-xs bg-gray-600 text-white px-2 py-0.5 rounded-full"> 等待解析 </span> <button id="tuxun-refresh-location-btn" class="text-xs text-blue-500 hover:text-blue-700"> <i class="fa fa-refresh mr-1"></i> 重新解析 </button> </div> </div> <div id="tuxun-location-info"> <p class="text-gray-500 italic">正在使用百度地图API解析位置信息...</p> </div> `; parsedContent.appendChild(locationCard); // 添加重新解析按钮事件 document.getElementById('tuxun-refresh-location-btn').addEventListener('click', () => { const locationInfo = document.getElementById('tuxun-location-info'); const locationStatus = document.getElementById('tuxun-location-status'); locationInfo.innerHTML = '<p class="text-gray-500 italic">正在重新解析位置信息...</p>'; locationStatus.textContent = '解析中'; locationStatus.className = 'text-xs bg-blue-500 text-white px-2 py-0.5 rounded-full'; fetchLocationInfo(lat, lng); }); // 立即开始解析位置 fetchLocationInfo(lat, lng); } // 新增:加载百度地图JS API并解析位置 function fetchLocationInfo(lat, lng) { const locationInfo = document.getElementById('tuxun-location-info'); const locationStatus = document.getElementById('tuxun-location-status'); // 显示加载状态 locationInfo.innerHTML = '<p class="text-gray-500 italic">正在加载地图API并解析位置...</p>'; locationStatus.textContent = '加载中'; locationStatus.className = 'text-xs bg-blue-500 text-white px-2 py-0.5 rounded-full'; // 检查百度地图API是否已加载 if (window.BMap) { doGeocode(lat, lng); return; } // 首次加载百度地图API const script = document.createElement('script'); script.src = `https://api.map.baidu.com/api?v=3.0&ak=${BAIDU_AK}&callback=initBaiduMap`; script.type = 'text/javascript'; document.head.appendChild(script); // 定义全局回调函数 window.initBaiduMap = function() { doGeocode(lat, lng); }; // 超时处理 setTimeout(() => { if (!window.BMap) { locationInfo.innerHTML = '<p class="text-red-500">百度地图API加载超时,请重试</p>'; locationStatus.textContent = '加载失败'; locationStatus.className = 'text-xs bg-red-500 text-white px-2 py-0.5 rounded-full'; } }, 5000); } // 新增:执行逆地理编码 function doGeocode(lat, lng) { const locationInfo = document.getElementById('tuxun-location-info'); const locationStatus = document.getElementById('tuxun-location-status'); try { // 创建坐标点(WGS84转百度坐标) const point = new BMap.Point(lng, lat); const convertor = new BMap.Convertor(); const points = [point]; // 坐标转换(WGS84 -> BD09) convertor.translate(points, 1, 5, (data) => { if (data.status === 0) { const bdPoint = data.points[0]; // 创建逆地理编码器 const geoc = new BMap.Geocoder(); geoc.getLocation(bdPoint, (rs) => { const address = rs.addressComponents; const formattedAddress = rs.formattedAddress; // 显示解析结果 locationInfo.innerHTML = ` <div class="space-y-3"> <div class="bg-gray-50 p-3 rounded-lg"> <span class="text-xs text-gray-500 block">详细地址</span> <div class="font-medium text-gray-800">${formattedAddress}</div> </div> <div class="grid grid-cols-2 gap-3"> <div class="bg-gray-50 p-3 rounded-lg"> <span class="text-xs text-gray-500 block">省/市/区</span> <div class="font-medium text-gray-800">${address.province} ${address.city} ${address.district}</div> </div> <div class="bg-gray-50 p-3 rounded-lg"> <span class="text-xs text-gray-500 block">街道信息</span> <div class="font-medium text-gray-800">${address.street} ${address.streetNumber}</div> </div> </div> <div class="bg-blue-50 p-3 rounded-lg border border-blue-200"> <span class="text-xs text-blue-700 block">查看地图</span> <a href="https://map.baidu.com/search/${lat},${lng}" target="_blank" class="font-medium text-blue-600 hover:underline"> <i class="fa fa-map-o mr-1"></i> 在百度地图中查看 </a> </div> </div> `; locationStatus.textContent = '解析成功'; locationStatus.className = 'text-xs bg-green-500 text-white px-2 py-0.5 rounded-full'; showNotification('百度API地点解析成功'); }); } else { throw new Error('坐标转换失败,错误码:' + data.status); } }); } catch (error) { locationInfo.innerHTML = ` <div class="bg-red-50 p-3 rounded-lg border border-red-200"> <div class="font-medium text-red-800">解析失败</div> <div class="text-sm text-red-600 mt-1">${error.message}</div> </div> `; locationStatus.textContent = '解析失败'; locationStatus.className = 'text-xs bg-red-500 text-white px-2 py-0.5 rounded-full'; error('位置解析失败:', error); showNotification('位置解析失败: ' + error.message, true); } } // 注入Tailwind CSS function injectTailwindCSS() { log('注入Tailwind CSS和Font Awesome...'); const tailwindScript = document.createElement('script'); tailwindScript.src = 'https://cdn.tailwindcss.com'; document.head.appendChild(tailwindScript); const fontAwesome = document.createElement('link'); fontAwesome.href = 'https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css'; fontAwesome.rel = 'stylesheet'; document.head.appendChild(fontAwesome); // 配置Tailwind自定义颜色 tailwindScript.onload = function() { const style = document.createElement('style'); style.textContent = ` @layer utilities { .content-auto { content-visibility: auto; } .json-key { color: #e53e3e; } .json-string { color: #48bb78; } .json-number { color: #3182ce; } .json-boolean { color: #f6ad55; } .json-null { color: #b7791f; } } `; document.head.appendChild(style); log('Tailwind CSS配置完成'); }; } // 重置调试器 function resetDebugger() { log('重置调试器...'); const rawResponse = document.getElementById('tuxun-raw-response'); const parsedContent = document.getElementById('tuxun-parsed-content'); const status = document.getElementById('tuxun-status'); const parsedTime = document.getElementById('tuxun-parsed-time'); const rawSize = document.getElementById('tuxun-raw-size'); const mapType = document.getElementById('tuxun-map-type'); const locationStatus = document.getElementById('tuxun-location-status'); rawResponse.textContent = '等待响应...'; parsedContent.innerHTML = '<p class="text-gray-500 italic">等待getPanoInfo、getQQPanoInfo或谷歌街景请求...</p>'; status.textContent = '等待请求'; status.className = 'text-xs bg-gray-600 text-white px-2 py-0.5 rounded-full'; parsedTime.textContent = '未捕获请求'; rawSize.textContent = '0 KB'; mapType.textContent = '未检测'; mapType.className = 'text-xs bg-gray-600 text-white px-2 py-0.5 rounded-full ml-2'; if (locationStatus) { locationStatus.textContent = '等待解析'; locationStatus.className = 'text-xs bg-gray-600 text-white px-2 py-0.5 rounded-full'; } // 隐藏面板 const panel = document.getElementById('tuxun-debug-panel'); const reopenBtn = document.getElementById('tuxun-reopen-btn'); if (panel) { panel.classList.add('hidden'); reopenBtn.classList.remove('hidden'); } showNotification('调试器已重置'); log('调试器重置完成'); } // 初始化调试面板 injectTailwindCSS(); const debugPanel = createDebugPanel(); const reopenBtn = createReopenButton(); // 重写fetch函数以拦截请求 window.fetch = async function(input, init) { const url = typeof input === 'string' ? input : input.url; // 记录所有fetch请求(调试用) log('拦截到fetch请求:', url); // 检查是否是百度街景请求 const baiduPanoRegex = /https:\/\/tuxun\.fun\/api\/v0\/tuxun\/mapProxy\/getPanoInfo/; // 检查是否是腾讯街景请求 const tencentPanoRegex = /https:\/\/tuxun\.fun\/api\/v0\/tuxun\/mapProxy\/getQQPanoInfo/; // 检查是否是谷歌街景请求 const googlePanoRegex = /https:\/\/tile\.chao-fan\.com\/\$rpc\/google\.internal\.maps\.mapsjs\.v1\.MapsJsInternalService\/GetMetadata/; if (baiduPanoRegex.test(url)) { log('检测到百度街景getPanoInfo请求!'); handlePanoRequest(url, 'baidu'); } else if (tencentPanoRegex.test(url)) { log('检测到腾讯街景getQQPanoInfo请求!'); handlePanoRequest(url, 'tencent'); } else if (googlePanoRegex.test(url)) { log('检测到谷歌街景请求!'); handlePanoRequest(url, 'google'); } // 非目标请求,直接执行 return originalFetch(input, init); }; // 处理街景请求 async function handlePanoRequest(url, mapType) { // 更新地图类型显示 const mapTypeElement = document.getElementById('tuxun-map-type'); mapTypeElement.textContent = mapType === 'baidu' ? '百度街景' : mapType === 'tencent' ? '腾讯街景' : '谷歌街景'; mapTypeElement.className = `text-xs ${mapType === 'baidu' ? 'bg-blue-500' : mapType === 'tencent' ? 'bg-green-500' : 'bg-red-500'} text-white px-2 py-0.5 rounded-full ml-2`; // 记录请求参数 const urlObj = new URL(url); let panoId = ''; if (mapType === 'baidu' || mapType === 'tencent') { panoId = urlObj.searchParams.get('pano'); } else if (mapType === 'google') { // 谷歌街景的panoId在请求体中,这里无法直接获取 panoId = '需从响应中解析'; } log(`${mapType === 'baidu' ? '百度' : mapType === 'tencent' ? '腾讯' : '谷歌'}街景请求的Pano ID:`, panoId); // 更新状态 const status = document.getElementById('tuxun-status'); status.textContent = '正在请求'; status.className = 'text-xs bg-yellow-500 text-white px-2 py-0.5 rounded-full'; // 显示面板 const panel = document.getElementById('tuxun-debug-panel'); const reopenBtn = document.getElementById('tuxun-reopen-btn'); if (panel) { panel.classList.remove('hidden'); reopenBtn.classList.add('hidden'); } try { // 记录请求开始时间 const startTime = performance.now(); log('请求开始时间:', new Date().toLocaleString()); // 创建对应的请求对象 let response; if (mapType === 'baidu') { response = await originalFetch(url); } else if (mapType === 'tencent') { // 腾讯街景请求需要添加Referer response = await originalFetch(url, { headers: { 'Referer': 'https://tuxun.fun/' } }); } else if (mapType === 'google') { // 谷歌街景请求 response = await originalFetch(url, { headers: { 'Referer': 'https://tuxun.fun/' } }); } const responseClone = response.clone(); // 记录请求完成时间 const endTime = performance.now(); const duration = (endTime - startTime).toFixed(2); log(`请求完成,耗时: ${duration}ms`); // 记录响应状态 log('响应状态:', response.status); // 更新状态 status.textContent = '解析中'; status.className = 'text-xs bg-blue-500 text-white px-2 py-0.5 rounded-full'; // 解析响应JSON const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { const jsonData = await responseClone.json(); // 记录响应内容 log(`${mapType === 'baidu' ? '百度' : mapType === 'tencent' ? '腾讯' : '谷歌'}街景响应JSON:`, jsonData); // 计算响应大小 const responseSize = (JSON.stringify(jsonData).length / 1024).toFixed(2); // 在面板中显示原始响应 const rawResponse = document.getElementById('tuxun-raw-response'); rawResponse.textContent = JSON.stringify(jsonData, null, 2); // 更新响应时间和大小 document.getElementById('tuxun-parsed-time').textContent = new Date().toLocaleString(); document.getElementById('tuxun-raw-size').textContent = `${responseSize} KB`; // 显示解析后内容 createParsedContent(jsonData, mapType); // 更新状态 status.textContent = '已捕获'; status.className = 'text-xs bg-green-500 text-white px-2 py-0.5 rounded-full'; showNotification(`成功捕获${mapType === 'baidu' ? '百度' : mapType === 'tencent' ? '腾讯' : '谷歌'}街景响应`); log('响应已成功显示在面板中'); } else { const responseText = await responseClone.text(); const rawResponse = document.getElementById('tuxun-raw-response'); rawResponse.textContent = `非JSON响应 (${contentType}):\n\n${responseText.substring(0, 500)}...`; error('响应不是JSON格式:', contentType); showNotification('响应不是JSON格式', true); // 更新状态 status.textContent = '格式错误'; status.className = 'text-xs bg-red-500 text-white px-2 py-0.5 rounded-full'; } return response; } catch (error) { const rawResponse = document.getElementById('tuxun-raw-response'); rawResponse.textContent = `请求出错:\n\n${error.message}`; error('拦截请求出错:', error); showNotification('请求处理失败: ' + error.message, true); // 更新状态 status.textContent = '请求失败'; status.className = 'text-xs bg-red-500 text-white px-2 py-0.5 rounded-full'; throw error; } } // 重写XMLHttpRequest以拦截请求 window.XMLHttpRequest = function() { const xhr = new OriginalXHR(); // 保存原始的open和send方法 const originalOpen = xhr.open; const originalSend = xhr.send; // 重写open方法,记录请求信息 xhr.open = function(method, url) { // 记录所有XHR请求 log('拦截到XHR请求:', url); // 保存请求URL用于后续检查 this._url = url; return originalOpen.apply(this, arguments); }; // 重写send方法,在响应完成后处理 xhr.send = function() { // 监听load事件,获取响应 xhr.addEventListener('load', () => { const url = this._url; // 检查是否是百度街景请求 const baiduPanoRegex = /https:\/\/tuxun\.fun\/api\/v0\/tuxun\/mapProxy\/getPanoInfo/; // 检查是否是腾讯街景请求 const tencentPanoRegex = /https:\/\/tuxun\.fun\/api\/v0\/tuxun\/mapProxy\/getQQPanoInfo/; // 检查是否是谷歌街景请求 const googlePanoRegex = /https:\/\/tile\.chao-fan\.com\/\$rpc\/google\.internal\.maps\.mapsjs\.v1\.MapsJsInternalService\/GetMetadata/; if (baiduPanoRegex.test(url)) { log('检测到百度街景getPanoInfo XHR请求!'); handleXHRPanoResponse(xhr, 'baidu'); } else if (tencentPanoRegex.test(url)) { log('检测到腾讯街景getQQPanoInfo XHR请求!'); handleXHRPanoResponse(xhr, 'tencent'); } else if (googlePanoRegex.test(url)) { log('检测到谷歌街景XHR请求!'); handleXHRPanoResponse(xhr, 'google'); } }); return originalSend.apply(this, arguments); }; return xhr; }; // 处理XHR街景响应 function handleXHRPanoResponse(xhr, mapType) { // 更新地图类型显示 const mapTypeElement = document.getElementById('tuxun-map-type'); mapTypeElement.textContent = mapType === 'baidu' ? '百度街景' : mapType === 'tencent' ? '腾讯街景' : '谷歌街景'; mapTypeElement.className = `text-xs ${mapType === 'baidu' ? 'bg-blue-500' : mapType === 'tencent' ? 'bg-green-500' : 'bg-red-500'} text-white px-2 py-0.5 rounded-full ml-2`; // 记录请求参数 const urlObj = new URL(xhr._url); let panoId = ''; if (mapType === 'baidu' || mapType === 'tencent') { panoId = urlObj.searchParams.get('pano'); } else if (mapType === 'google') { // 谷歌街景的panoId在请求体中,这里无法直接获取 panoId = '需从响应中解析'; } log(`${mapType === 'baidu' ? '百度' : mapType === 'tencent' ? '腾讯' : '谷歌'}街景XHR请求的Pano ID:`, panoId); // 更新状态 const status = document.getElementById('tuxun-status'); status.textContent = '正在请求'; status.className = 'text-xs bg-yellow-500 text-white px-2 py-0.5 rounded-full'; // 显示面板 //const panel = document.getElementById('tuxun-debug-panel'); //const reopenBtn = document.getElementById('tuxun-reopen-btn'); //if (panel) { // panel.classList.remove('hidden'); // reopenBtn.classList.add('hidden'); //} try { // 尝试解析响应 const responseText = xhr.responseText; // 先检查是否是JSON格式 let jsonData; try { jsonData = JSON.parse(responseText); // 记录响应内容 log(`${mapType === 'baidu' ? '百度' : mapType === 'tencent' ? '腾讯' : '谷歌'}街景XHR响应JSON:`, jsonData); // 计算响应大小 const responseSize = (responseText.length / 1024).toFixed(2); // 在面板中显示原始响应 const rawResponse = document.getElementById('tuxun-raw-response'); rawResponse.textContent = JSON.stringify(jsonData, null, 2); // 更新响应时间和大小 document.getElementById('tuxun-parsed-time').textContent = new Date().toLocaleString(); document.getElementById('tuxun-raw-size').textContent = `${responseSize} KB`; // 显示解析后内容 createParsedContent(jsonData, mapType); // 更新状态 status.textContent = '已捕获'; status.className = 'text-xs bg-green-500 text-white px-2 py-0.5 rounded-full'; showNotification(`成功捕获${mapType === 'baidu' ? '百度' : mapType === 'tencent' ? '腾讯' : '谷歌'}街景XHR响应`); log('XHR响应已成功显示在面板中'); } catch (parseError) { // 不是JSON格式 const rawResponse = document.getElementById('tuxun-raw-response'); rawResponse.textContent = `非JSON响应:\n\n${responseText.substring(0, 500)}...`; error('XHR响应不是JSON格式:', parseError); // showNotification('XHR响应不是JSON格式', true); // 更新状态 status.textContent = '格式错误'; status.className = 'text-xs bg-red-500 text-white px-2 py-0.5 rounded-full'; } } catch (error) { const rawResponse = document.getElementById('tuxun-raw-response'); rawResponse.textContent = `XHR请求出错:\n\n${error.message}`; error('处理XHR请求出错:', error); showNotification('XHR请求处理失败: ' + error.message, true); // 更新状态 status.textContent = '请求失败'; status.className = 'text-xs bg-red-500 text-white px-2 py-0.5 rounded-full'; } } })();