优化交互

This commit is contained in:
CN-JS-HuiBai
2026-04-05 01:32:42 +08:00
parent bea8ed607e
commit 672ea11598
2 changed files with 96 additions and 76 deletions

View File

@@ -12,7 +12,8 @@
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"> rel="stylesheet">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<script src="//unpkg.com/globe.gl"></script> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts-gl@2.0.9/dist/echarts-gl.min.js"></script>
<script> <script>
// Prevent theme flicker // Prevent theme flicker
(function () { (function () {
@@ -244,7 +245,7 @@
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /> <path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg> </svg>
全球节点分布 全球服务器分布 (3D Map)
</h2> </h2>
</div> </div>
<div class="globe-body" id="globeContainer"></div> <div class="globe-body" id="globeContainer"></div>

View File

@@ -92,7 +92,7 @@
let currentSourceFilter = 'all'; let currentSourceFilter = 'all';
let currentPage = 1; let currentPage = 1;
let pageSize = 20; let pageSize = 20;
let myGlobe = null; let myMap3D = null;
// ---- Initialize ---- // ---- Initialize ----
function init() { function init() {
@@ -105,8 +105,8 @@
// Network chart // Network chart
networkChart = new AreaChart(dom.networkCanvas); networkChart = new AreaChart(dom.networkCanvas);
// Initial globe // Initial map
initGlobe(); initMap3D();
// Event listeners // Event listeners
dom.btnSettings.addEventListener('click', openSettings); dom.btnSettings.addEventListener('click', openSettings);
@@ -360,107 +360,126 @@
} }
} }
// ---- Global Globe ---- // ---- Global 3D Map ----
function initGlobe() { async function initMap3D() {
if (!dom.globeContainer) return; if (!dom.globeContainer) return;
if (typeof Globe !== 'function') { if (typeof echarts === 'undefined') {
console.warn('[Globe] Globe.gl library not loaded. 3D visualization will be disabled.'); console.warn('[Map3D] ECharts library not loaded.');
dom.globeContainer.innerHTML = ` dom.globeContainer.innerHTML = `<div class="chart-empty">地图库加载失败</div>`;
<div style="height: 100%; display: flex; align-items: center; justify-content: center; color: var(--text-muted); font-size: 0.8rem; text-align: center; padding: 20px;">
Globe.gl 库加载失败<br>请检查网络连接或刷新页面
</div>
`;
return; return;
} }
try { try {
const width = dom.globeContainer.clientWidth || 400; // Fetch map data
const height = dom.globeContainer.clientHeight || 280; const resp = await fetch('https://cdn.jsdelivr.net/npm/echarts@4.9.0/map/json/world.json');
const worldJSON = await resp.json();
echarts.registerMap('world', worldJSON);
myGlobe = Globe() myMap3D = echarts.init(dom.globeContainer);
(dom.globeContainer)
.width(width) const option = {
.height(height) backgroundColor: 'transparent',
.globeImageUrl('//unpkg.com/three-globe/example/img/earth-blue-marble.jpg') tooltip: {
.bumpImageUrl('//unpkg.com/three-globe/example/img/earth-topology.png') show: true,
.backgroundColor('rgba(0,0,0,0)') trigger: 'item',
.showAtmosphere(true) backgroundColor: 'rgba(10, 14, 26, 0.9)',
.atmosphereColor('#6366f1') borderColor: 'var(--accent-indigo)',
.atmosphereDaylightAlpha(0.1) textStyle: { color: '#fff', fontSize: 12 },
.pointsData([]) formatter: (params) => {
.pointColor(() => '#06b6d4') const d = params.data;
.pointAltitude(0.05) if (!d) return '';
.pointRadius(0.8) return `
.pointsMerge(true) <div style="padding: 4px;">
.pointLabel(d => ` <div style="font-weight: 700; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 4px; padding-bottom: 4px;">${escapeHtml(d.job)}</div>
<div style="background: rgba(10, 14, 26, 0.9); padding: 8px 12px; border: 1px solid var(--accent-indigo); border-radius: 8px; backdrop-filter: blur(8px);"> <div style="font-size: 0.75rem; color: var(--text-secondary);">${escapeHtml(d.city || '')}, ${escapeHtml(d.countryName || d.country || '')}</div>
<div style="font-weight: 700; color: #fff; margin-bottom: 4px;">${escapeHtml(d.job)}</div> <div style="font-size: 0.75rem; margin-top: 4px;">
<div style="font-size: 0.75rem; color: var(--text-secondary);">${escapeHtml(d.city || '')}, ${escapeHtml(d.countryName || d.country || '')}</div> <span style="color: var(--accent-cyan);">BW:</span> ↓${formatBandwidth(d.netRx)} ${formatBandwidth(d.netTx)}
<div style="font-size: 0.75rem; margin-top: 4px;"> </div>
<span style="color: var(--accent-indigo);">BW:</span> ↓${formatBandwidth(d.netRx)}${formatBandwidth(d.netTx)} </div>
</div> `;
</div> }
`); },
geo3D: {
// Resizing with initial dimensions map: 'world',
myGlobe.width(dom.globeContainer.clientWidth).height(dom.globeContainer.clientHeight); shading: 'lambert',
environment: 'transparent',
const resizeObserver = new ResizeObserver(() => { light: {
if (myGlobe && dom.globeContainer.clientWidth > 0) { main: { intensity: 1.2, shadow: true, alpha: 45, beta: 45 },
myGlobe.width(dom.globeContainer.clientWidth).height(dom.globeContainer.clientHeight); ambient: { intensity: 0.3 }
} },
}); viewControl: {
resizeObserver.observe(dom.globeContainer); autoRotate: true,
autoRotateSpeed: 5,
// Initial view distance: 80,
myGlobe.controls().autoRotate = true; alpha: 40,
myGlobe.controls().autoRotateSpeed = 1.2; // Slightly faster for visual appeal beta: 0
myGlobe.controls().enableZoom = false; },
itemStyle: {
color: '#1a1d2d',
opacity: 1,
borderWidth: 0.8,
borderColor: '#2d334d'
},
emphasis: {
itemStyle: { color: '#2d334d' }
},
regions: []
},
series: [{
type: 'scatter3D',
coordinateSystem: 'geo3D',
symbol: 'circle',
symbolSize: 8,
itemStyle: {
color: '#06b6d4',
opacity: 0.8,
borderWidth: 0.5,
borderColor: '#fff'
},
label: { show: false },
data: []
}]
};
myMap3D.setOption(option);
window.addEventListener('resize', () => myMap3D.resize());
// Initial data sync if available
if (allServersData.length > 0) { if (allServersData.length > 0) {
updateGlobe(allServersData); updateMap3D(allServersData);
} }
} catch (err) { } catch (err) {
console.error('[Globe] Initialization failed:', err); console.error('[Map3D] Initialization failed:', err);
} }
} }
function updateGlobe(servers) { function updateMap3D(servers) {
if (!myGlobe) return; if (!myMap3D) return;
// Filter servers with lat/lng
const geoData = servers const geoData = servers
.filter(s => s.lat && s.lng) .filter(s => s.lat && s.lng)
.map(s => ({ .map(s => ({
lat: s.lat, name: s.job,
lng: s.lng, value: [s.lng, s.lat, 2], // 2 is altitude
job: s.job, job: s.job,
city: s.city, city: s.city,
country: s.country, country: s.country,
countryName: s.countryName, countryName: s.countryName,
netRx: s.netRx, netRx: s.netRx,
netTx: s.netTx, netTx: s.netTx
size: Math.max(0.2, Math.min(1.5, (s.netRx + s.netTx) / 1024 / 1024 / 5)) // Scale by bandwidth (MB/s)
})); }));
myGlobe.pointsData(geoData); myMap3D.setOption({
series: [{ data: geoData }]
});
// Update footer stats // Update footer stats
if (dom.globeTotalNodes) dom.globeTotalNodes.textContent = geoData.length; if (dom.globeTotalNodes) dom.globeTotalNodes.textContent = geoData.length;
if (dom.globeTotalRegions) { if (dom.globeTotalRegions) {
const regions = new Set(geoData.map(d => d.country)).size; const regions = new Set(geoData.map(d => d.country)).size;
dom.globeTotalRegions.textContent = regions; dom.globeTotalRegions.textContent = regions;
} }
// Also add arcs for "traffic flow" if we have multiple servers
// For now, let's just show rings or pulses for active traffic
myGlobe.ringsData(geoData.filter(d => (d.netRx + d.netTx) > 1024 * 1024)); // Pulse for servers > 1MB/s
myGlobe.ringColor(() => '#6366f1');
myGlobe.ringMaxRadius(3);
myGlobe.ringPropagationSpeed(1);
myGlobe.ringRepeatPeriod(1000);
} }
// ---- Update Dashboard ---- // ---- Update Dashboard ----
@@ -496,7 +515,7 @@
renderFilteredServers(); renderFilteredServers();
// Update globe // Update globe
updateGlobe(data.servers || []); updateMap3D(data.servers || []);
// Flash animation // Flash animation
if (previousMetrics) { if (previousMetrics) {