优化交互
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
153
public/js/app.js
153
public/js/app.js
@@ -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,92 +360,119 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 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-weight: 700; color: #fff; margin-bottom: 4px;">${escapeHtml(d.job)}</div>
|
|
||||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">${escapeHtml(d.city || '')}, ${escapeHtml(d.countryName || d.country || '')}</div>
|
<div style="font-size: 0.75rem; color: var(--text-secondary);">${escapeHtml(d.city || '')}, ${escapeHtml(d.countryName || d.country || '')}</div>
|
||||||
<div style="font-size: 0.75rem; margin-top: 4px;">
|
<div style="font-size: 0.75rem; margin-top: 4px;">
|
||||||
<span style="color: var(--accent-indigo);">BW:</span> ↓${formatBandwidth(d.netRx)} ↑${formatBandwidth(d.netTx)}
|
<span style="color: var(--accent-cyan);">BW:</span> ↓${formatBandwidth(d.netRx)} ↑${formatBandwidth(d.netTx)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`;
|
||||||
|
|
||||||
// Resizing with initial dimensions
|
|
||||||
myGlobe.width(dom.globeContainer.clientWidth).height(dom.globeContainer.clientHeight);
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
if (myGlobe && dom.globeContainer.clientWidth > 0) {
|
|
||||||
myGlobe.width(dom.globeContainer.clientWidth).height(dom.globeContainer.clientHeight);
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
resizeObserver.observe(dom.globeContainer);
|
geo3D: {
|
||||||
|
map: 'world',
|
||||||
|
shading: 'lambert',
|
||||||
|
environment: 'transparent',
|
||||||
|
light: {
|
||||||
|
main: { intensity: 1.2, shadow: true, alpha: 45, beta: 45 },
|
||||||
|
ambient: { intensity: 0.3 }
|
||||||
|
},
|
||||||
|
viewControl: {
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 5,
|
||||||
|
distance: 80,
|
||||||
|
alpha: 40,
|
||||||
|
beta: 0
|
||||||
|
},
|
||||||
|
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: []
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
// Initial view
|
myMap3D.setOption(option);
|
||||||
myGlobe.controls().autoRotate = true;
|
|
||||||
myGlobe.controls().autoRotateSpeed = 1.2; // Slightly faster for visual appeal
|
window.addEventListener('resize', () => myMap3D.resize());
|
||||||
myGlobe.controls().enableZoom = false;
|
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -453,14 +480,6 @@
|
|||||||
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user