Compare commits
2 Commits
f065efab26
...
427890d6fb
| Author | SHA1 | Date | |
|---|---|---|---|
| 427890d6fb | |||
| be1150e67c |
@@ -1166,70 +1166,114 @@ function renderClusterCards(clusters) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Step 4: Map (static background + grid clustering) ── */
|
||||
/* ── Simplified continent outlines as Leaflet polygons ──── */
|
||||
const CONTINENTS = [
|
||||
// North America
|
||||
[[75,-168],[72,-140],[70,-130],[65,-125],[62,-115],[58,-108],[55,-100],[50,-95],[45,-90],[40,-85],[35,-82],[30,-80],[25,-78],[20,-80],[15,-83],[10,-84],[8,-80],[9,-77],[12,-72],[15,-66],[18,-60],[22,-60],[28,-58],[32,-60],[35,-55],[34,-45],[35,-40],[42,-35],[48,-30],[52,-25],[55,-20],[58,-15],[62,-12],[68,-10],[72,-15],[75,-25],[78,-35],[80,-50],[82,-60],[83,-70],[84,-80],[82,-90],[80,-100],[78,-120],[76,-140],[75,-168]],
|
||||
// Greenland
|
||||
[[83,-50],[82,-25],[80,-18],[78,-15],[76,-18],[75,-25],[76,-35],[78,-45],[80,-55],[83,-60],[84,-55],[83,-50]],
|
||||
// South America
|
||||
[[12,-72],[10,-65],[8,-55],[5,-48],[0,-45],[-5,-40],[-8,-35],[-10,-30],[-12,-25],[-15,-20],[-18,-18],[-22,-20],[-25,-25],[-28,-30],[-32,-35],[-35,-38],[-38,-40],[-40,-45],[-45,-48],[-50,-50],[-54,-52],[-55,-48],[-54,-42],[-52,-38],[-48,-35],[-42,-32],[-38,-30],[-35,-28],[-30,-25],[-25,-22],[-20,-20],[-15,-23],[-10,-30],[-5,-38],[0,-44],[5,-50],[8,-58],[10,-65],[12,-72]],
|
||||
// Europe
|
||||
[[71,-25],[70,-10],[68,5],[65,12],[62,15],[58,15],[55,12],[52,8],[48,5],[45,0],[43,-5],[44,-8],[48,-10],[52,-12],[55,-15],[57,-20],[58,-25],[60,-30],[62,-35],[65,-30],[68,-25],[70,-20],[71,-25]],
|
||||
// UK/Ireland
|
||||
[[59,-8],[58,-2],[56,0],[54,-3],[52,-6],[51,-10],[53,-12],[55,-10],[57,-8],[59,-8]],
|
||||
// Scandinavia
|
||||
[[71,25],[70,18],[68,15],[65,14],[62,12],[60,8],[58,5],[56,8],[55,12],[56,18],[58,25],[60,30],[62,32],[65,30],[68,28],[70,25],[71,25]],
|
||||
// Africa
|
||||
[[37,-8],[35,-2],[33,5],[30,10],[28,15],[25,20],[22,28],[18,35],[15,38],[12,40],[8,42],[3,43],[-2,42],[-5,38],[-8,35],[-12,32],[-15,28],[-18,25],[-20,20],[-22,18],[-24,15],[-27,12],[-30,8],[-32,5],[-35,0],[-33,-5],[-28,-8],[-22,-10],[-18,-8],[-14,-5],[-10,-3],[-5,0],[0,-2],[5,-4],[10,-5],[15,-7],[20,-10],[25,-12],[30,-10],[35,-8],[37,-8]],
|
||||
// Asia
|
||||
[[72,55],[70,80],[68,110],[65,130],[62,145],[58,155],[55,160],[50,165],[45,168],[40,170],[35,172],[30,170],[28,165],[25,160],[22,155],[18,150],[15,145],[12,140],[10,135],[8,130],[5,125],[2,120],[0,115],[-2,110],[-5,105],[-8,100],[-10,105],[-12,110],[-15,115],[-18,120],[-20,118],[-25,112],[-28,108],[-32,105],[-35,100],[-38,95],[-40,90],[-42,85],[-45,80],[-48,75],[-50,70],[-55,65],[-58,60],[-60,55],[-58,50],[-55,45],[-52,42],[-50,38],[-48,35],[-45,38],[-42,42],[-40,48],[-35,52],[-30,55],[-25,52],[-22,48],[-18,45],[-15,48],[-12,52],[-8,55],[-5,52],[-2,48],[0,45],[3,42],[5,38],[8,35],[10,38],[12,42],[15,48],[20,55],[25,62],[30,68],[35,72],[40,75],[45,78],[50,80],[55,78],[60,75],[65,70],[68,65],[70,60],[72,55]],
|
||||
// Japan
|
||||
[[45,142],[42,140],[38,138],[35,136],[32,135],[30,137],[28,140],[30,143],[33,145],[35,143],[38,142],[42,144],[45,143],[45,142]],
|
||||
// SE Asia islands
|
||||
[[8,115],[-2,110],[-6,105],[-8,108],[-5,112],[-2,115],[0,118],[2,120],[5,118],[8,115]],
|
||||
[[0,125],[-5,128],[-8,130],[-6,135],[-3,138],[0,140],[3,138],[5,135],[3,130],[0,125]],
|
||||
// Australia
|
||||
[[-12,130],[-15,128],[-20,126],[-25,128],[-30,132],[-35,138],[-38,142],[-40,148],[-38,152],[-35,155],[-30,153],[-25,148],[-20,145],[-18,140],[-15,138],[-12,135],[-10,132],[-12,130]],
|
||||
// New Zealand
|
||||
[[-35,172],[-38,170],[-42,172],[-45,174],[-47,172],[-45,168],[-42,170],[-38,172],[-35,172]],
|
||||
// Antarctica (hint)
|
||||
[[-65,-170],[-65,-120],[-68,-60],[-70,0],[-72,60],[-70,120],[-65,170],[-75,170],[-78,120],[-80,60],[-80,0],[-78,-60],[-75,-120],[-72,-170],[-65,-170]],
|
||||
];
|
||||
|
||||
const WORLD_MAP_SVG = 'data:image/svg+xml,' + encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="-180 -90 360 180" width="2048" height="1024">
|
||||
<rect x="-180" y="-90" width="360" height="180" fill="#f0f2f5"/>
|
||||
<!-- Simplified continent outlines as paths -->
|
||||
<g fill="#e2e6ea" stroke="#c0c5cc" stroke-width="0.3">
|
||||
<!-- North America -->
|
||||
<path d="M-168,-20 L-130,-20 L-120,-10 L-100,-10 L-90,0 L-80,0 L-70,15 L-80,30 L-90,30 L-100,25 L-120,30 L-130,25 L-140,15 L-150,10 L-168,5 Z"/>
|
||||
<!-- South America -->
|
||||
<path d="M-80,10 L-70,10 L-60,0 L-50,-5 L-40,0 L-35,10 L-40,20 L-50,30 L-55,35 L-60,30 L-70,25 L-80,20 Z"/>
|
||||
<!-- Europe -->
|
||||
<path d="M-10,35 L0,30 L10,35 L20,40 L30,45 L35,50 L40,55 L30,60 L20,65 L10,70 L0,72 L-10,68 L-10,55 L-5,45 L-10,40 Z"/>
|
||||
<!-- Africa -->
|
||||
<path d="M-5,32 L5,30 L15,25 L25,20 L35,10 L45,5 L50,15 L45,25 L35,30 L30,35 L25,35 L20,40 L10,42 L0,40 L-5,38 Z"/>
|
||||
<!-- Asia -->
|
||||
<path d="M30,35 L50,30 L70,25 L90,20 L110,15 L130,10 L140,20 L145,30 L140,40 L130,50 L120,60 L110,65 L100,70 L80,72 L60,75 L50,73 L40,70 L30,68 L20,60 L25,50 L30,45 Z"/>
|
||||
<!-- Australia -->
|
||||
<path d="M115,20 L125,15 L140,15 L150,20 L155,30 L145,40 L135,42 L125,38 L118,30 L115,25 Z"/>
|
||||
<!-- East Asia islands -->
|
||||
<path d="M130,28 L135,25 L140,28 L142,32 L138,36 L133,35 L130,32 Z"/>
|
||||
<!-- Southeast Asia islands -->
|
||||
<path d="M100,20 L105,15 L115,12 L120,15 L118,20 L110,22 L105,25 L100,23 Z"/>
|
||||
<!-- Greenland -->
|
||||
<path d="M-50,15 L-40,5 L-30,5 L-25,15 L-30,25 L-40,28 L-50,25 Z"/>
|
||||
<!-- Antarctica hint -->
|
||||
<path d="M-170,55 L-120,52 L-60,55 L0,58 L60,55 L120,52 L170,55 L170,65 L120,68 L60,70 L0,72 L-60,70 L-120,68 L-170,65 Z" fill="#e8eaed"/>
|
||||
</g>
|
||||
<!-- Grid lines -->
|
||||
<g stroke="#d5d9de" stroke-width="0.15" stroke-dasharray="2,4">
|
||||
<line x1="-180" y1="-60" x2="180" y2="-60"/><line x1="-180" y1="-30" x2="180" y2="-30"/>
|
||||
<line x1="-180" y1="0" x2="180" y2="0"/><line x1="-180" y1="30" x2="180" y2="30"/>
|
||||
<line x1="-180" y1="60" x2="180" y2="60"/><line x1="-90" y1="-90" x2="-90" y2="90"/>
|
||||
<line x1="0" y1="-90" x2="0" y2="90"/><line x1="90" y1="-90" x2="90" y2="90"/>
|
||||
</g>
|
||||
</svg>`);
|
||||
// Subnet color palette — one color per /24 prefix
|
||||
const SUBNET_COLORS = [
|
||||
'#4361ee','#43aa8b','#f72585','#f4a261','#e76f51','#264653','#2a9d8f',
|
||||
'#e9c46a','#8ecae6','#219ebc','#023047','#ffb703','#fb8500','#6a4c93',
|
||||
'#ff6b6b','#4ecdc4','#45b7d1','#96ceb4','#d4a5a5','#a445b2',
|
||||
'#e17055','#00b894','#0984e3','#6c5ce7','#fd79a8','#00cec9','#e66767',
|
||||
'#3dc1d3','#e77f67','#778beb','#cf6a87','#63cdda','#ea8685','#596275',
|
||||
];
|
||||
|
||||
function getSubnetColor(ip) {
|
||||
// Hash the /24 subnet to a color
|
||||
const parts = ip.split('.');
|
||||
if (parts.length >= 3) {
|
||||
const hash = (parseInt(parts[0]) * 31 + parseInt(parts[1]) * 17 + parseInt(parts[2]) * 7) % SUBNET_COLORS.length;
|
||||
return SUBNET_COLORS[Math.abs(hash)];
|
||||
}
|
||||
return SUBNET_COLORS[0];
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
const container = document.getElementById('saMap');
|
||||
if (!container) return;
|
||||
|
||||
// Ensure container has dimensions
|
||||
container.style.height = '550px';
|
||||
container.style.width = '100%';
|
||||
|
||||
if (SA.map) {
|
||||
container.style.height = '550px';
|
||||
setTimeout(() => { SA.map.invalidateSize(); renderMapData(); }, 200);
|
||||
SA.map.invalidateSize();
|
||||
renderMapData();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
container.style.height = '550px';
|
||||
SA.map = L.map('saMap', {
|
||||
center: [20, 0], zoom: 2, minZoom: 2, maxZoom: 12,
|
||||
worldCopyJump: true, preferCanvas: true,
|
||||
attributionControl: false, zoomControl: true,
|
||||
});
|
||||
|
||||
// Static world map image — no tile requests
|
||||
L.imageOverlay(WORLD_MAP_SVG, [[-90, -180], [90, 180]], {opacity: 1}).addTo(SA.map);
|
||||
// Wait for container to be visible (step switching from display:none)
|
||||
function tryInit(attempts) {
|
||||
if (container.offsetHeight > 0 || attempts <= 0) {
|
||||
SA.map = L.map('saMap', {
|
||||
center: [20, 0], zoom: 2, minZoom: 2, maxZoom: 14,
|
||||
worldCopyJump: true, preferCanvas: true,
|
||||
attributionControl: false, zoomControl: true,
|
||||
});
|
||||
|
||||
SA.map.setMaxBounds([[-90, -180], [90, 180]]);
|
||||
SA.map.on('zoomend moveend', () => renderMapData());
|
||||
setTimeout(() => SA.map.invalidateSize(), 300);
|
||||
renderMapData();
|
||||
}, 250);
|
||||
// Draw continents
|
||||
try {
|
||||
CONTINENTS.forEach(ring => {
|
||||
L.polygon(ring, {
|
||||
color: '#b0b8c4', weight: 0.8, opacity: 0.7,
|
||||
fillColor: '#e8ecf0', fillOpacity: 1,
|
||||
interactive: false,
|
||||
}).addTo(SA.map);
|
||||
});
|
||||
|
||||
// Grid lines
|
||||
for (let lat = -60; lat <= 60; lat += 30) {
|
||||
L.polyline([[lat, -180], [lat, 180]], {
|
||||
color: '#dde1e6', weight: 0.5, interactive: false,
|
||||
}).addTo(SA.map);
|
||||
}
|
||||
for (let lon = -150; lon <= 150; lon += 30) {
|
||||
L.polyline([[-90, lon], [90, lon]], {
|
||||
color: '#dde1e6', weight: 0.5, interactive: false,
|
||||
}).addTo(SA.map);
|
||||
}
|
||||
} catch(e) { console.error('Map draw error:', e); }
|
||||
|
||||
SA.map.setMaxBounds([[-85, -180], [85, 180]]);
|
||||
SA.map.on('zoomend moveend', () => renderMapData());
|
||||
SA.map.invalidateSize();
|
||||
renderMapData();
|
||||
} else {
|
||||
setTimeout(() => tryInit(attempts - 1), 150);
|
||||
}
|
||||
}
|
||||
setTimeout(() => tryInit(10), 100);
|
||||
}
|
||||
|
||||
// ── Grid-based marker clustering ──────────────────────────
|
||||
// ── Render all IP nodes (colored by subnet) ───────────────
|
||||
function renderMapData() {
|
||||
if (!SA.map) return;
|
||||
if (SA.pointLayer) SA.map.removeLayer(SA.pointLayer);
|
||||
@@ -1241,86 +1285,41 @@ function renderMapData() {
|
||||
|
||||
const bounds = SA.map.getBounds();
|
||||
const zoom = SA.map.getZoom();
|
||||
const markers = [];
|
||||
const centerMarkers = [];
|
||||
|
||||
// Grid cell size in degrees (adapts to zoom level)
|
||||
const cellSize = Math.max(0.05, 60 / Math.pow(2, zoom));
|
||||
const grid = {}; // key: "latBin_lonBin" → {nodes, clusterIds}
|
||||
// Per-subnet color cache
|
||||
const subnetColorCache = {};
|
||||
|
||||
nodes.forEach(n => {
|
||||
if (n.lat == null || n.lon == null) return;
|
||||
if (!bounds.contains([n.lat, n.lon])) return;
|
||||
// Skip hidden clusters
|
||||
if (SA.hiddenClusters.has(n.cluster_id)) return;
|
||||
|
||||
const latBin = Math.floor(n.lat / cellSize);
|
||||
const lonBin = Math.floor(n.lon / cellSize);
|
||||
const key = `${latBin}_${lonBin}`;
|
||||
if (!grid[key]) grid[key] = {nodes: [], clusterIds: new Set(), lat: 0, lon: 0};
|
||||
grid[key].nodes.push(n);
|
||||
grid[key].clusterIds.add(n.cluster_id);
|
||||
grid[key].lat += n.lat;
|
||||
grid[key].lon += n.lon;
|
||||
// Color by /24 subnet
|
||||
if (!subnetColorCache[n.ip]) subnetColorCache[n.ip] = getSubnetColor(n.ip);
|
||||
const color = subnetColorCache[n.ip];
|
||||
|
||||
const circle = L.circleMarker([n.lat, n.lon], {
|
||||
radius: 4, fillColor: color, color: '#fff', weight: 0.5,
|
||||
opacity: 0.85, fillOpacity: 0.75,
|
||||
interactive: true, bubblingMouseEvents: false,
|
||||
});
|
||||
circle.bindTooltip(buildNodeTooltip(n), {sticky: true, direction: 'top'});
|
||||
circle.on('click', function(e) {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
loadNodeDetail(n.ip);
|
||||
});
|
||||
markers.push(circle);
|
||||
});
|
||||
|
||||
const markers = [];
|
||||
const centerMarkers = [];
|
||||
// Community center markers (larger dots for multi-node communities)
|
||||
const colors = generateColors(20);
|
||||
const clusterColorMap = {};
|
||||
Object.values(clusters).sort((a, b) => b.size - a.size).forEach((c, i) => {
|
||||
clusterColorMap[c.label] = c.size <= 1 ? '#b0b0b0' : colors[i % colors.length];
|
||||
clusterColorMap[c.label] = colors[i % colors.length];
|
||||
});
|
||||
|
||||
Object.entries(grid).forEach(([key, cell]) => {
|
||||
const count = cell.nodes.length;
|
||||
const avgLat = cell.lat / count;
|
||||
const avgLon = cell.lon / count;
|
||||
|
||||
if (count === 1) {
|
||||
// Single node in cell — render directly
|
||||
const n = cell.nodes[0];
|
||||
const color = clusterColorMap[n.cluster_id] || '#4361ee';
|
||||
const circle = L.circleMarker([n.lat, n.lon], {
|
||||
radius: 4, fillColor: color, color: '#fff', weight: 0.5,
|
||||
opacity: 0.85, fillOpacity: 0.7,
|
||||
});
|
||||
circle.bindTooltip(buildNodeTooltip(n), {sticky: true});
|
||||
circle.on('click', () => loadNodeDetail(n.ip));
|
||||
markers.push(circle);
|
||||
} else if (count <= 10) {
|
||||
// Few nodes — render all with slight offset
|
||||
cell.nodes.forEach((n, i) => {
|
||||
const color = clusterColorMap[n.cluster_id] || '#4361ee';
|
||||
const circle = L.circleMarker([n.lat, n.lon], {
|
||||
radius: 4, fillColor: color, color: '#fff', weight: 0.5,
|
||||
opacity: 0.75, fillOpacity: 0.6,
|
||||
});
|
||||
circle.bindTooltip(buildNodeTooltip(n), {sticky: true});
|
||||
circle.on('click', () => loadNodeDetail(n.ip));
|
||||
markers.push(circle);
|
||||
});
|
||||
} else {
|
||||
// Many nodes — show cluster marker
|
||||
const dominantCluster = [...cell.clusterIds].sort((a, b) => {
|
||||
const ca = cell.nodes.filter(n => n.cluster_id === a).length;
|
||||
const cb = cell.nodes.filter(n => n.cluster_id === b).length;
|
||||
return cb - ca;
|
||||
})[0];
|
||||
const color = clusterColorMap[dominantCluster] || '#4361ee';
|
||||
|
||||
const icon = L.divIcon({
|
||||
html: `<div style="background:${color};width:${Math.min(28, 14 + count/3)}px;height:${Math.min(28, 14 + count/3)}px;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,0.3);display:flex;align-items:center;justify-content:center;color:#fff;font-size:${Math.min(11, 8 + count/10)}px;font-weight:700;">${count}</div>`,
|
||||
className: '', iconSize: [28, 28], iconAnchor: [14, 14],
|
||||
});
|
||||
const marker = L.marker([avgLat, avgLon], {icon});
|
||||
marker.bindTooltip(`<b>${count} 个节点</b><br>${cell.clusterIds.size} 个社区<br>点击放大查看`, {direction: 'top'});
|
||||
marker.on('click', () => {
|
||||
SA.map.setView([avgLat, avgLon], Math.min(zoom + 3, 12));
|
||||
});
|
||||
markers.push(marker);
|
||||
}
|
||||
});
|
||||
|
||||
// Cluster center markers (only for multi-node clusters)
|
||||
Object.entries(clusters).forEach(([cid, cinfo]) => {
|
||||
const cidInt = parseInt(cid);
|
||||
if (cinfo.size <= 1 || SA.hiddenClusters.has(cidInt)) return;
|
||||
@@ -1328,48 +1327,62 @@ function renderMapData() {
|
||||
if (!bounds.contains([cinfo.center_lat, cinfo.center_lon])) return;
|
||||
|
||||
const color = clusterColorMap[cidInt] || '#4361ee';
|
||||
const icon = L.divIcon({
|
||||
html: `<div style="background:${color};width:20px;height:20px;border-radius:50%;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;color:#fff;font-size:9px;font-weight:700;">${cidInt}</div>`,
|
||||
className: '', iconSize: [20, 20], iconAnchor: [10, 10],
|
||||
const circle = L.circleMarker([cinfo.center_lat, cinfo.center_lon], {
|
||||
radius: 8, fillColor: color, color: '#fff', weight: 2,
|
||||
opacity: 0.95, fillOpacity: 0.8,
|
||||
interactive: true, bubblingMouseEvents: false,
|
||||
});
|
||||
const marker = L.marker([cinfo.center_lat, cinfo.center_lon], {icon});
|
||||
marker.bindTooltip(`<b>社区 ${cidInt} 中心</b><br>${cinfo.size} IP · ${cinfo.subnet || ''}`, {direction: 'top'});
|
||||
marker.on('click', () => loadClusterDetail(cidInt));
|
||||
centerMarkers.push(marker);
|
||||
circle.bindTooltip(
|
||||
`<div class="sa-point-tooltip"><b>社区 ${cidInt}</b><br>${cinfo.size} 个IP · ${cinfo.subnet || ''}<br>点击查看详情</div>`,
|
||||
{direction: 'top'}
|
||||
);
|
||||
circle.on('click', function(e) {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
loadClusterDetail(cidInt);
|
||||
});
|
||||
centerMarkers.push(circle);
|
||||
});
|
||||
|
||||
SA.pointLayer = L.layerGroup(markers).addTo(SA.map);
|
||||
SA.centerLayer = L.layerGroup(centerMarkers).addTo(SA.map);
|
||||
renderLegend(colors);
|
||||
renderLegend();
|
||||
}
|
||||
|
||||
function buildNodeTooltip(n) {
|
||||
const serverLabel = n.is_server ? ' 🖥' : '';
|
||||
const parts = n.ip ? n.ip.split('.') : [];
|
||||
const subnet = parts.length >= 3 ? parts[0] + '.' + parts[1] + '.' + parts[2] + '.0/24' : '';
|
||||
return `<div class="sa-point-tooltip">
|
||||
<b>${escapeHtml(n.ip)}</b>${serverLabel}<br>
|
||||
${n.city ? `<span>${escapeHtml(n.city)}</span><br>` : ''}
|
||||
${n.isp ? `<span style="color:#888;">${escapeHtml(n.isp)}</span><br>` : ''}
|
||||
<span style="color:#4361ee;">点击查看详情</span></div>`;
|
||||
${subnet ? '<span style="color:#888;">子网: ' + subnet + '</span><br>' : ''}
|
||||
${n.city ? '<span>' + escapeHtml(n.city) + '</span>, ' : ''}${escapeHtml(n.country || '')}<br>
|
||||
${n.isp ? '<span style="color:#666;">' + escapeHtml(n.isp) + '</span><br>' : ''}
|
||||
<span style="color:#4361ee;">🖱 点击查看详情</span></div>`;
|
||||
}
|
||||
|
||||
function renderLegend(colors) {
|
||||
function renderLegend() {
|
||||
const el = document.getElementById('saLegendItems');
|
||||
const clusters = SA.clusterMap || {};
|
||||
const sorted = Object.values(clusters)
|
||||
.sort((a, b) => b.size - a.size);
|
||||
const sorted = Object.values(clusters).sort((a, b) => b.size - a.size);
|
||||
if (!sorted.length) { el.innerHTML = ''; return; }
|
||||
|
||||
const html = [];
|
||||
sorted.forEach((c, idx) => {
|
||||
const color = c.size <= 1 ? '#b0b0b0' : colors[idx % colors.length];
|
||||
const colors = generateColors(sorted.length);
|
||||
// Show top 20 communities + subnet info
|
||||
const top = sorted.slice(0, 20);
|
||||
const html = top.map((c, idx) => {
|
||||
const color = colors[idx % colors.length];
|
||||
const hidden = SA.hiddenClusters.has(c.label);
|
||||
const label = c.size <= 1 ? '独立节点' : '社区 ' + c.label;
|
||||
html.push(`<div class="sa-legend-item${hidden ? ' hidden' : ''}"
|
||||
return `<div class="sa-legend-item${hidden ? ' hidden' : ''}"
|
||||
onclick="toggleCluster(${c.label})">
|
||||
<span class="color-dot" style="background:${color}"></span>
|
||||
<span>${label}</span>
|
||||
<span style="color:#999;font-size:0.8rem;">${c.size} IP</span>
|
||||
</div>`);
|
||||
<span style="color:#999;font-size:0.75rem;">${c.size} IP · ${c.subnet || ''}</span>
|
||||
</div>`;
|
||||
});
|
||||
if (sorted.length > 20) {
|
||||
html.push(`<div style="font-size:0.75rem;color:#999;padding:0.3rem;">... 还有 ${sorted.length - 20} 个社区</div>`);
|
||||
}
|
||||
el.innerHTML = html.join('');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user