Files
cftest/index.html
2025-12-13 22:43:52 +08:00

983 lines
35 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloudflare优选IP智能扫描工具</title>
<style>
:root {
--bg-primary: #f4f7f6;
--bg-secondary: #fff;
--text-primary: #333;
--text-secondary: #555;
--text-muted: #777;
--border-color: #ddd;
--accent-color: #3498db;
--accent-hover: #2980b9;
--success-color: #27ae60;
--danger-color: #e74c3c;
--warning-color: #ffc107;
--card-gradient-1: #667eea;
--card-gradient-2: #764ba2;
--table-header: #34495e;
--table-hover: #e8f4f8;
--table-stripe: #f9f9f9;
--info-bg: #e8f4f8;
--info-border: #3498db;
--warning-bg: #fff3cd;
--warning-border: #ffc107;
--log-bg: #f9f9f9;
--progress-bg: #ecf0f1;
--shadow: rgba(0,0,0,0.2);
}
[data-theme="dark"] {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--text-primary: #e4e4e4;
--text-secondary: #b8b8b8;
--text-muted: #888;
--border-color: #2d3748;
--accent-color: #4a9eff;
--accent-hover: #3182ce;
--success-color: #48bb78;
--danger-color: #f56565;
--warning-color: #ed8936;
--card-gradient-1: #4a5568;
--card-gradient-2: #2d3748;
--table-header: #2d3748;
--table-hover: #2a4365;
--table-stripe: #1a2332;
--info-bg: #1e3a5f;
--info-border: #4a9eff;
--warning-bg: #3d2817;
--warning-border: #ed8936;
--log-bg: #1a2332;
--progress-bg: #2d3748;
--shadow: rgba(0,0,0,0.5);
}
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
background-color: var(--bg-primary);
color: var(--text-primary);
max-width: 1000px;
margin: 20px auto;
padding: 20px;
}
h1 {
color: var(--text-primary);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
background: var(--accent-color);
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 8px var(--shadow);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.theme-toggle:hover {
background: var(--accent-hover);
transform: scale(1.1);
}
.container { display: flex; flex-direction: column; gap: 20px; }
.section {
padding: 20px;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--bg-secondary);
}
.section-title {
font-size: 1.3em;
font-weight: bold;
color: var(--text-primary);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--accent-color);
}
label {
font-weight: bold;
margin-right: 10px;
color: var(--text-primary);
}
textarea {
width: 100%;
height: 200px;
padding: 10px;
border-radius: 5px;
border: 1px solid var(--border-color);
font-size: 14px;
font-family: monospace;
box-sizing: border-box;
resize: vertical;
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
margin-top: 15px;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.sample-mode {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
background: var(--log-bg);
border-radius: 5px;
border: 1px solid var(--border-color);
}
.sample-mode label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-weight: normal;
}
.sample-mode input[type="radio"] {
cursor: pointer;
}
button {
padding: 12px 24px;
font-size: 16px;
font-weight: bold;
color: white;
background-color: var(--accent-color);
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
button:hover {
background-color: var(--accent-hover);
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow);
}
button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
transform: none;
opacity: 0.6;
}
button.danger {
background-color: var(--danger-color);
}
button.danger:hover {
background-color: #c0392b;
}
button.success {
background-color: var(--success-color);
}
select, input[type="number"] {
padding: 10px;
font-size: 15px;
border-radius: 5px;
border: 1px solid var(--border-color);
background-color: var(--bg-secondary);
color: var(--text-primary);
}
input[type="number"] {
width: 80px;
}
#progressContainer {
display: none;
margin-top: 15px;
}
#progressBar {
width: 100%;
height: 30px;
background-color: var(--progress-bg);
border-radius: 15px;
overflow: hidden;
position: relative;
}
#progressFill {
height: 100%;
background: linear-gradient(90deg, var(--accent-color), var(--success-color));
width: 0%;
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
#progressText {
margin-top: 8px;
text-align: center;
color: var(--text-secondary);
font-size: 14px;
}
#output {
padding: 20px;
border: 1px solid var(--border-color);
border-radius: 5px;
background-color: var(--bg-secondary);
min-height: 100px;
max-height: 500px;
overflow-y: auto;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
th, td {
padding: 12px;
border: 1px solid var(--border-color);
text-align: left;
}
th {
background-color: var(--table-header);
color: white;
font-weight: bold;
position: sticky;
top: 0;
}
tr:nth-child(even) { background-color: var(--table-stripe); }
tr:hover { background-color: var(--table-hover); }
.latency-good { color: var(--success-color); font-weight: bold; }
.latency-medium { color: var(--warning-color); font-weight: bold; }
.latency-bad { color: var(--danger-color); font-weight: bold; }
.no-result {
font-style: italic;
color: var(--text-muted);
text-align: center;
padding: 40px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-card {
padding: 15px;
background: linear-gradient(135deg, var(--card-gradient-1) 0%, var(--card-gradient-2) 100%);
color: white;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 2em;
font-weight: bold;
margin: 10px 0;
}
.stat-label {
font-size: 0.9em;
opacity: 0.9;
}
footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
font-size: 0.9em;
color: var(--text-muted);
}
.info-box {
background-color: var(--info-bg);
border-left: 4px solid var(--info-border);
padding: 12px;
margin: 10px 0;
border-radius: 4px;
font-size: 0.95em;
color: var(--text-primary);
}
.warning-box {
background-color: var(--warning-bg);
border-left: 4px solid var(--warning-border);
padding: 12px;
margin: 10px 0;
border-radius: 4px;
font-size: 0.95em;
color: var(--text-primary);
}
.log-item {
padding: 5px;
font-size: 0.9em;
border-bottom: 1px solid var(--border-color);
color: var(--text-secondary);
}
.log-success { color: var(--success-color); }
.log-fail { color: var(--danger-color); }
.log-info { color: var(--accent-color); }
#testLog {
max-height: 200px;
overflow-y: auto;
margin-top: 15px;
display: none;
background: var(--log-bg);
padding: 10px;
border-radius: 5px;
border: 1px solid var(--border-color);
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--progress-bg);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--accent-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-hover);
}
</style>
</head>
<body>
<button class="theme-toggle" onclick="toggleTheme()" title="切换主题">
<span id="themeIcon">🌙</span>
</button>
<h1>🚀 Cloudflare优选IP智能扫描工具</h1>
<div class="container">
<!-- 阶段1: IP范围输入 -->
<div class="section">
<div class="section-title">📋 阶段1: 输入IP范围 (CIDR格式)</div>
<div class="warning-box">
⚠️ <strong>测试原理:</strong>使用HTTP协议测试IP连通性和延迟避免HTTPS证书问题
</div>
<div class="info-box">
💡 支持CIDR格式如 104.16.0.0/12或单个IP每行一个。系统会根据采样模式进行测试。
</div>
<textarea id="ipRangeInput" placeholder="173.245.48.0/20
103.21.244.0/22
104.16.0.0/12
172.64.0.0/13
..."></textarea>
<div class="controls">
<div class="sample-mode">
<strong>采样模式:</strong>
<label>
<input type="radio" name="sampleMode" value="sample" checked>
智能采样(推荐)- 每个IP段采样
<input type="number" id="sampleSize" value="5" min="1" max="100" style="width: 60px; margin-left: 5px;">
个IP
</label>
<label>
<input type="radio" name="sampleMode" value="full">
完整扫描 - 测试CIDR范围内所有IP 大段IP可能耗时很长
</label>
</div>
</div>
<div class="controls">
<div class="control-group">
<label for="latencyThreshold">延迟阈值(ms):</label>
<input type="number" id="latencyThreshold" value="500" min="50" max="2000">
</div>
<div class="control-group">
<label for="concurrency">并发数:</label>
<input type="number" id="concurrency" value="5" min="1" max="20">
</div>
<button onclick="startScan()">🔍 开始智能扫描</button>
<button class="danger" onclick="stopScan()" id="stopBtn" disabled>⏹ 停止扫描</button>
</div>
<div id="progressContainer">
<div id="progressBar">
<div id="progressFill">0%</div>
</div>
<div id="progressText">准备开始...</div>
</div>
<div id="testLog">
<strong>测试日志:</strong>
<div id="logContent"></div>
</div>
</div>
<!-- 阶段2: 扫描结果 -->
<div class="section">
<div class="section-title">📊 阶段2: 扫描结果统计</div>
<div class="stats" id="statsContainer">
<div class="stat-card">
<div class="stat-label">总测试IP</div>
<div class="stat-value" id="statTotal">0</div>
</div>
<div class="stat-card">
<div class="stat-label">有效IP</div>
<div class="stat-value" id="statValid">0</div>
</div>
<div class="stat-card">
<div class="stat-label">平均延迟</div>
<div class="stat-value" id="statAvgLatency">-</div>
</div>
<div class="stat-card">
<div class="stat-label">最低延迟</div>
<div class="stat-value" id="statMinLatency">-</div>
</div>
</div>
</div>
<!-- 阶段3: 结果分析 -->
<div class="section">
<div class="section-title">🎯 阶段3: 结果分析与筛选</div>
<div class="controls">
<div class="control-group">
<label for="locationFilter">地区筛选:</label>
<select id="locationFilter" disabled>
<option value="">等待扫描完成...</option>
</select>
</div>
<button onclick="renderResults()" id="refreshBtn" disabled>🔄 刷新结果</button>
</div>
<div id="output">
<p class="no-result">请先输入IP范围并开始扫描</p>
</div>
</div>
<!-- 阶段4: 导出 -->
<div class="section">
<div class="section-title">💾 阶段4: 导出优选IP</div>
<div class="controls">
<div class="control-group">
<input type="radio" id="exportAll" name="exportMode" value="all" checked>
<label for="exportAll">全部</label>
</div>
<div class="control-group">
<input type="radio" id="exportTop" name="exportMode" value="top">
<label for="exportTop"></label>
<input type="number" id="topCount" value="10" min="1" disabled>
<label></label>
</div>
<div class="control-group">
<input type="radio" id="exportLatency" name="exportMode" value="latency">
<label for="exportLatency">延迟低于</label>
<input type="number" id="exportLatencyValue" value="200" min="1" disabled>
<label>ms</label>
</div>
<button class="success" onclick="exportData()" id="exportBtn" disabled>📋 复制到剪贴板</button>
</div>
</div>
</div>
<footer>
Copyright &copy; <span id="year"></span> All Rights Reserved.
</footer>
<script>
let allParsedIps = [];
let isScanning = false;
let shouldStop = false;
let locationCache = {};
// 主题切换
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
const icon = document.getElementById('themeIcon');
icon.textContent = newTheme === 'dark' ? '☀️' : '🌙';
localStorage.setItem('theme', newTheme);
}
// 初始化主题
function initTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
document.getElementById('themeIcon').textContent = savedTheme === 'dark' ? '☀️' : '🌙';
}
// 添加日志
function addLog(message, type = 'info') {
const logContent = document.getElementById('logContent');
const logItem = document.createElement('div');
logItem.className = `log-item log-${type}`;
logItem.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logContent.appendChild(logItem);
logContent.scrollTop = logContent.scrollHeight;
}
// CIDR转IP列表支持完整扫描和采样
function cidrToIPs(cidr, sampleSize = null) {
const [ip, bits] = cidr.split('/');
const mask = bits ? parseInt(bits) : 32;
const ipParts = ip.split('.').map(Number);
const ipNum = (ipParts[0] << 24) + (ipParts[1] << 16) + (ipParts[2] << 8) + ipParts[3];
const hostBits = 32 - mask;
const totalHosts = Math.pow(2, hostBits);
const ips = [];
// 如果是完整扫描模式或总数小于采样数
if (sampleSize === null || totalHosts <= sampleSize) {
// 返回所有IP
for (let i = 0; i < totalHosts; i++) {
const newIpNum = ipNum + i;
const newIp = [
(newIpNum >>> 24) & 255,
(newIpNum >>> 16) & 255,
(newIpNum >>> 8) & 255,
newIpNum & 255
].join('.');
ips.push(newIp);
}
return ips;
}
// 采样模式
const actualSampleSize = Math.min(sampleSize, totalHosts);
const step = Math.max(1, Math.floor(totalHosts / actualSampleSize));
for (let i = 0; i < actualSampleSize; i++) {
const offset = i * step;
const newIpNum = ipNum + offset;
const newIp = [
(newIpNum >>> 24) & 255,
(newIpNum >>> 16) & 255,
(newIpNum >>> 8) & 255,
newIpNum & 255
].join('.');
ips.push(newIp);
}
return ips;
}
// 使用HTTP图片加载测试
async function testIPWithImage(ip, timeout = 5000) {
return new Promise((resolve) => {
const start = performance.now();
const img = new Image();
let resolved = false;
const timeoutId = setTimeout(() => {
if (!resolved) {
resolved = true;
img.src = '';
resolve({ ip, latency: 9999, success: false, error: 'timeout' });
}
}, timeout);
img.onload = () => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
const latency = Math.round(performance.now() - start);
resolve({ ip, latency, success: true });
}
};
img.onerror = () => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
const latency = Math.round(performance.now() - start);
if (latency < timeout && latency > 10) {
resolve({ ip, latency, success: true });
} else {
resolve({ ip, latency: 9999, success: false, error: 'error' });
}
}
};
img.src = `http://${ip}/cdn-cgi/trace?t=${Date.now()}`;
});
}
// 测试单个IP
async function testIP(ip, timeout = 3000) {
const result = await testIPWithImage(ip, timeout);
if (result.success) {
result.location = '获取中...';
addLog(`${ip} - ${result.latency}ms`, 'success');
} else {
addLog(`${ip} - 失败`, 'fail');
}
return result;
}
// 批量获取IP位置信息
async function batchGetLocations(ips) {
addLog(`开始获取 ${ips.length} 个IP的地理位置...`, 'info');
for (let i = 0; i < ips.length; i++) {
const ip = ips[i];
if (locationCache[ip]) {
allParsedIps[i].location = locationCache[ip];
continue;
}
try {
const response = await fetch(`http://ip-api.com/json/${ip}?fields=country,city,isp,status`, {
signal: AbortSignal.timeout(3000)
});
const data = await response.json();
let location = '未知';
if (data.status === 'success') {
location = `${data.country || '未知'}-${data.city || '未知'}`;
}
locationCache[ip] = location;
allParsedIps[i].location = location;
// 随机延迟 1200-1800ms符合API限流要求且更自然
if (i < ips.length - 1) {
const randomDelay = 1200 + Math.random() * 600;
await new Promise(resolve => setTimeout(resolve, randomDelay));
}
} catch (error) {
allParsedIps[i].location = '未知';
locationCache[ip] = '未知';
}
if ((i + 1) % 5 === 0 || i === ips.length - 1) {
addLog(`位置获取进度: ${i + 1}/${ips.length}`, 'info');
}
}
addLog('位置信息获取完成!', 'success');
}
// 并发控制(添加随机延迟)
async function testIPsConcurrently(ips, concurrency, latencyThreshold, onProgress) {
const results = [];
let completed = 0;
let validCount = 0;
for (let i = 0; i < ips.length; i += concurrency) {
if (shouldStop) break;
const batch = ips.slice(i, i + concurrency);
const batchResults = await Promise.all(batch.map(ip => testIP(ip)));
const validResults = batchResults.filter(r => r.success && r.latency < latencyThreshold);
results.push(...validResults);
validCount += validResults.length;
completed += batch.length;
onProgress(completed, ips.length, validCount);
// 随机延迟 150-350ms
const randomDelay = 150 + Math.random() * 200;
await new Promise(resolve => setTimeout(resolve, randomDelay));
}
return results;
}
// 开始扫描
async function startScan() {
const input = document.getElementById('ipRangeInput').value.trim();
if (!input) {
alert('请输入IP范围');
return;
}
// 获取采样模式
const sampleMode = document.querySelector('input[name="sampleMode"]:checked').value;
const sampleSize = sampleMode === 'sample' ? parseInt(document.getElementById('sampleSize').value) : null;
isScanning = true;
shouldStop = false;
allParsedIps = [];
document.getElementById('stopBtn').disabled = false;
document.getElementById('progressContainer').style.display = 'block';
document.getElementById('testLog').style.display = 'block';
document.getElementById('logContent').innerHTML = '';
document.querySelector('button[onclick="startScan()"]').disabled = true;
const latencyThreshold = parseInt(document.getElementById('latencyThreshold').value);
const concurrency = parseInt(document.getElementById('concurrency').value);
const modeText = sampleMode === 'full' ? '完整扫描' : `智能采样(${sampleSize}个/段)`;
addLog(`开始扫描 [${modeText}], 延迟阈值: ${latencyThreshold}ms, 并发: ${concurrency}`, 'info');
// 解析IP范围
const lines = input.split('\n').filter(l => l.trim());
let allIPs = [];
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.includes('/')) {
const ips = cidrToIPs(trimmed, sampleSize);
allIPs.push(...ips);
addLog(`解析 ${trimmed}: 生成 ${ips.length} 个IP ${sampleMode === 'full' ? '(完整)' : '(采样)'}`, 'info');
} else if (/^\d+\.\d+\.\d+\.\d+$/.test(trimmed)) {
allIPs.push(trimmed);
}
}
if (allIPs.length > 1000 && sampleMode === 'full') {
const confirm = window.confirm(`检测到需要测试 ${allIPs.length} 个IP这可能需要很长时间。\n\n是否继续?`);
if (!confirm) {
isScanning = false;
document.getElementById('stopBtn').disabled = true;
document.querySelector('button[onclick="startScan()"]').disabled = false;
return;
}
}
addLog(`总共需要测试 ${allIPs.length} 个IP`, 'info');
updateProgress(0, allIPs.length, 0);
// 开始测试
allParsedIps = await testIPsConcurrently(
allIPs,
concurrency,
latencyThreshold,
updateProgress
);
if (allParsedIps.length === 0) {
isScanning = false;
document.getElementById('stopBtn').disabled = true;
document.querySelector('button[onclick="startScan()"]').disabled = false;
addLog(`扫描完成但未找到有效IP`, 'fail');
document.getElementById('output').innerHTML = '<p class="no-result">未找到符合条件的IP请尝试<br>1. 增加延迟阈值到800-1000ms<br>2. 增加采样数量<br>3. 检查网络连接</p>';
return;
}
// 批量获取位置信息
addLog(`找到 ${allParsedIps.length} 个有效IP开始获取位置信息...`, 'info');
const validIPs = allParsedIps.map(item => item.ip);
await batchGetLocations(validIPs);
// 扫描完成
isScanning = false;
document.getElementById('stopBtn').disabled = true;
document.querySelector('button[onclick="startScan()"]').disabled = false;
addLog(`扫描完成!找到 ${allParsedIps.length} 个有效IP`, 'success');
// 更新统计
updateStats();
// 填充地区选择器
const uniqueLocations = [...new Set(allParsedIps.map(item => item.location))].sort();
const locationFilter = document.getElementById('locationFilter');
locationFilter.disabled = false;
locationFilter.innerHTML = '<option value="ALL">全部地区</option>';
uniqueLocations.forEach(loc => {
locationFilter.appendChild(new Option(loc, loc));
});
document.getElementById('refreshBtn').disabled = false;
document.getElementById('exportBtn').disabled = false;
renderResults();
}
// 停止扫描
function stopScan() {
shouldStop = true;
addLog('用户停止扫描', 'info');
document.getElementById('progressText').textContent = '正在停止...';
}
// 更新进度
function updateProgress(completed, total, validCount) {
const percent = Math.round((completed / total) * 100);
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressFill').textContent = percent + '%';
document.getElementById('progressText').textContent =
`已测试: ${completed}/${total} | 有效IP: ${validCount}`;
// 实时更新统计
document.getElementById('statTotal').textContent = completed;
document.getElementById('statValid').textContent = validCount;
}
// 更新统计
function updateStats() {
const total = allParsedIps.length;
const latencies = allParsedIps.map(ip => ip.latency);
const avgLatency = total > 0 ? Math.round(latencies.reduce((a, b) => a + b, 0) / total) : 0;
const minLatency = total > 0 ? Math.min(...latencies) : 0;
document.getElementById('statTotal').textContent = total;
document.getElementById('statValid').textContent = total;
document.getElementById('statAvgLatency').textContent = avgLatency + 'ms';
document.getElementById('statMinLatency').textContent = minLatency + 'ms';
}
// 渲染结果
function renderResults() {
const selectedLocation = document.getElementById('locationFilter').value;
let ipsToShow = selectedLocation === 'ALL'
? [...allParsedIps]
: allParsedIps.filter(item => item.location === selectedLocation);
ipsToShow.sort((a, b) => a.latency - b.latency);
const outputDiv = document.getElementById('output');
if (ipsToShow.length === 0) {
outputDiv.innerHTML = '<p class="no-result">没有符合条件的数据</p>';
return;
}
let tableHTML = '<table><thead><tr><th>排名</th><th>IP地址</th><th>位置</th><th>延迟 (ms)</th></tr></thead><tbody>';
ipsToShow.forEach((item, index) => {
const latencyClass = item.latency < 100 ? 'latency-good' :
item.latency < 200 ? 'latency-medium' : 'latency-bad';
tableHTML += `<tr>
<td>${index + 1}</td>
<td><strong>${item.ip}</strong></td>
<td>${item.location}</td>
<td class="${latencyClass}">${item.latency}</td>
</tr>`;
});
tableHTML += '</tbody></table>';
outputDiv.innerHTML = tableHTML;
}
// 导出数据
function exportData() {
const selectedLocation = document.getElementById('locationFilter').value;
let ipsToExport = selectedLocation === 'ALL'
? [...allParsedIps]
: allParsedIps.filter(item => item.location === selectedLocation);
ipsToExport.sort((a, b) => a.latency - b.latency);
const mode = document.querySelector('input[name="exportMode"]:checked').value;
if (mode === 'top') {
const count = parseInt(document.getElementById('topCount').value);
ipsToExport = ipsToExport.slice(0, count);
} else if (mode === 'latency') {
const threshold = parseInt(document.getElementById('exportLatencyValue').value);
ipsToExport = ipsToExport.filter(item => item.latency < threshold);
}
if (ipsToExport.length === 0) {
alert('没有符合导出条件的IP');
return;
}
const ipListText = ipsToExport.map(item => item.ip).join('\n');
navigator.clipboard.writeText(ipListText).then(() => {
const btn = document.getElementById('exportBtn');
const originalText = btn.textContent;
btn.textContent = '✅ 已复制 ' + ipsToExport.length + ' 个IP!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}).catch(() => {
alert('复制失败,请手动复制:\n\n' + ipListText);
});
}
// 事件监听
document.getElementById('locationFilter').addEventListener('change', renderResults);
const exportRadios = document.querySelectorAll('input[name="exportMode"]');
exportRadios.forEach(radio => {
radio.addEventListener('change', () => {
document.getElementById('topCount').disabled = radio.value !== 'top';
document.getElementById('exportLatencyValue').disabled = radio.value !== 'latency';
});
});
// 采样模式切换
const sampleModeRadios = document.querySelectorAll('input[name="sampleMode"]');
sampleModeRadios.forEach(radio => {
radio.addEventListener('change', () => {
document.getElementById('sampleSize').disabled = radio.value !== 'sample';
});
});
// 初始化
document.getElementById('year').textContent = new Date().getFullYear();
initTheme();
</script>
</body>
</html>