使用Claude优化动画

This commit is contained in:
CN-JS-HuiBai
2026-04-06 02:40:28 +08:00
parent 225ec71ac3
commit 96de50285f

View File

@@ -125,7 +125,7 @@
let siteThemeQuery = null; // For media query cleanup let siteThemeQuery = null; // For media query cleanup
let siteThemeHandler = null; let siteThemeHandler = null;
let backgroundIntervals = []; // To track setIntervals let backgroundIntervals = []; // To track setIntervals
// Load sort state from localStorage or use default // Load sort state from localStorage or use default
let currentSort = { column: 'up', direction: 'desc' }; let currentSort = { column: 'up', direction: 'desc' };
try { try {
@@ -136,7 +136,7 @@
} catch (e) { } catch (e) {
console.warn('Failed to load sort state', e); console.warn('Failed to load sort state', e);
} }
let myMap2D = null; let myMap2D = null;
let editingRouteId = null; let editingRouteId = null;
@@ -157,7 +157,7 @@
// Event listeners // Event listeners
dom.btnSettings.addEventListener('click', openSettings); dom.btnSettings.addEventListener('click', openSettings);
dom.modalClose.addEventListener('click', closeSettings); dom.modalClose.addEventListener('click', closeSettings);
// Toggle server source option based on type // Toggle server source option based on type
if (dom.sourceType) { if (dom.sourceType) {
dom.sourceType.addEventListener('change', () => { dom.sourceType.addEventListener('change', () => {
@@ -172,7 +172,7 @@
} }
if (dom.btnCancelEditRoute) { if (dom.btnCancelEditRoute) {
dom.btnCancelEditRoute.onclick = cancelEditRoute; dom.btnCancelEditRoute.onclick = cancelEditRoute;
} }
dom.settingsModal.addEventListener('click', (e) => { dom.settingsModal.addEventListener('click', (e) => {
@@ -201,7 +201,7 @@
// Site settings // Site settings
dom.btnSaveSiteSettings.addEventListener('click', saveSiteSettings); dom.btnSaveSiteSettings.addEventListener('click', saveSiteSettings);
dom.btnAddRoute.addEventListener('click', addLatencyRoute); dom.btnAddRoute.addEventListener('click', addLatencyRoute);
// Auth password change // Auth password change
if (dom.btnChangePassword) { if (dom.btnChangePassword) {
dom.btnChangePassword.addEventListener('click', saveChangePassword); dom.btnChangePassword.addEventListener('click', saveChangePassword);
@@ -251,7 +251,7 @@
offset: 1 offset: 1
} }
], { ], {
duration: 400, duration: 600,
easing: 'cubic-bezier(0.16, 1, 0.3, 1)', easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
fill: 'none' fill: 'none'
}); });
@@ -305,7 +305,7 @@
offset: 1 offset: 1
} }
], { ], {
duration: 350, duration: 500,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)', easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'forwards' // Hold final frame until we remove class fill: 'forwards' // Hold final frame until we remove class
}); });
@@ -660,7 +660,7 @@
try { try {
const response = await fetch('/api/metrics/latency'); const response = await fetch('/api/metrics/latency');
const data = await response.json(); const data = await response.json();
currentLatencies = data.routes || []; currentLatencies = data.routes || [];
if (allServersData.length > 0) { if (allServersData.length > 0) {
updateMap2D(allServersData); updateMap2D(allServersData);
} }
@@ -672,74 +672,74 @@
// ---- Global 2D Map ---- // ---- Global 2D Map ----
async function initMap2D() { async function initMap2D() {
if (!dom.globeContainer) return; if (!dom.globeContainer) return;
if (typeof echarts === 'undefined') { if (typeof echarts === 'undefined') {
console.warn('[Map2D] ECharts library not loaded.'); console.warn('[Map2D] ECharts library not loaded.');
dom.globeContainer.innerHTML = `<div class="chart-empty">地图库加载失败</div>`; dom.globeContainer.innerHTML = `<div class="chart-empty">地图库加载失败</div>`;
return; return;
} }
try { try {
// Fetch map data // Fetch map data
const resp = await fetch('https://cdn.jsdelivr.net/npm/echarts@4.9.0/map/json/world.json'); const resp = await fetch('https://cdn.jsdelivr.net/npm/echarts@4.9.0/map/json/world.json');
const worldJSON = await resp.json(); const worldJSON = await resp.json();
// Transform to Pacific-centered correctly
const transformCoords = (coords) => {
if (!Array.isArray(coords) || coords.length === 0) return;
const first = coords[0];
if (!Array.isArray(first)) return;
if (typeof first[0] === 'number') { // Ring // Transform to Pacific-centered correctly
let sum = 0; const transformCoords = (coords) => {
coords.forEach(pt => sum += pt[0]); if (!Array.isArray(coords) || coords.length === 0) return;
let avg = sum / coords.length; const first = coords[0];
if (avg < -20) { if (!Array.isArray(first)) return;
coords.forEach(pt => pt[0] += 360);
}
} else {
coords.forEach(transformCoords);
}
};
if (worldJSON && worldJSON.features) { if (typeof first[0] === 'number') { // Ring
worldJSON.features.forEach(feature => { let sum = 0;
if (feature.geometry && feature.geometry.coordinates) { coords.forEach(pt => sum += pt[0]);
transformCoords(feature.geometry.coordinates); let avg = sum / coords.length;
} if (avg < -20) {
}); coords.forEach(pt => pt[0] += 360);
}
} else {
coords.forEach(transformCoords);
} }
};
echarts.registerMap('world', worldJSON);
if (myMap2D) { if (worldJSON && worldJSON.features) {
myMap2D.dispose(); worldJSON.features.forEach(feature => {
if (mapResizeHandler) { if (feature.geometry && feature.geometry.coordinates) {
window.removeEventListener('resize', mapResizeHandler); transformCoords(feature.geometry.coordinates);
} }
});
}
echarts.registerMap('world', worldJSON);
if (myMap2D) {
myMap2D.dispose();
if (mapResizeHandler) {
window.removeEventListener('resize', mapResizeHandler);
} }
}
myMap2D = echarts.init(dom.globeContainer); myMap2D = echarts.init(dom.globeContainer);
const isLight = document.documentElement.classList.contains('light-theme');
// Helper to transform app coordinates to shifted map coordinates
const shiftLng = (lng) => (lng < -20 ? lng + 360 : lng);
const option = { const isLight = document.documentElement.classList.contains('light-theme');
backgroundColor: 'transparent',
tooltip: { // Helper to transform app coordinates to shifted map coordinates
show: true, const shiftLng = (lng) => (lng < -20 ? lng + 360 : lng);
trigger: 'item',
confine: true, const option = {
transitionDuration: 0, backgroundColor: 'transparent',
backgroundColor: 'rgba(10, 14, 26, 0.9)', tooltip: {
borderColor: 'var(--accent-indigo)', show: true,
textStyle: { color: '#fff', fontSize: 12 }, trigger: 'item',
formatter: (params) => { confine: true,
const d = params.data; transitionDuration: 0,
if (!d) return ''; backgroundColor: 'rgba(10, 14, 26, 0.9)',
return ` borderColor: 'var(--accent-indigo)',
textStyle: { color: '#fff', fontSize: 12 },
formatter: (params) => {
const d = params.data;
if (!d) return '';
return `
<div style="padding: 4px;"> <div style="padding: 4px;">
<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="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="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>
@@ -748,53 +748,53 @@
</div> </div>
</div> </div>
`; `;
} }
}, },
geo: { geo: {
map: 'world', map: 'world',
roam: true, roam: true,
center: [165, 20], // Centered in Pacific center: [165, 20], // Centered in Pacific
zoom: 1.1, zoom: 1.1,
aspectScale: 0.85, aspectScale: 0.85,
emphasis: { emphasis: {
label: { show: false }, label: { show: false },
itemStyle: { areaColor: isLight ? '#f0f2f5' : '#2d334d' } itemStyle: { areaColor: isLight ? '#f0f2f5' : '#2d334d' }
}, },
itemStyle: { itemStyle: {
areaColor: isLight ? '#f4f6fa' : '#1a1d2e', areaColor: isLight ? '#f4f6fa' : '#1a1d2e',
borderColor: isLight ? '#cbd5e1' : '#2d334d', borderColor: isLight ? '#cbd5e1' : '#2d334d',
borderWidth: 1 borderWidth: 1
}, },
select: { select: {
itemStyle: { areaColor: isLight ? '#f4f8ff' : '#2d334d' } itemStyle: { areaColor: isLight ? '#f4f8ff' : '#2d334d' }
} }
}, },
series: [{ series: [{
type: 'scatter', type: 'scatter',
coordinateSystem: 'geo', coordinateSystem: 'geo',
geoIndex: 0, geoIndex: 0,
symbolSize: 5, symbolSize: 5,
itemStyle: { itemStyle: {
color: '#06b6d4', color: '#06b6d4',
shadowBlur: 3, shadowBlur: 3,
shadowColor: 'rgba(6, 182, 212, 0.5)' shadowColor: 'rgba(6, 182, 212, 0.5)'
}, },
data: [] data: []
}] }]
}; };
myMap2D.setOption(option); myMap2D.setOption(option);
mapResizeHandler = debounce(() => { mapResizeHandler = debounce(() => {
if (myMap2D) myMap2D.resize(); if (myMap2D) myMap2D.resize();
}, 100); }, 100);
window.addEventListener('resize', mapResizeHandler); window.addEventListener('resize', mapResizeHandler);
if (allServersData.length > 0) { if (allServersData.length > 0) {
updateMap2D(allServersData); updateMap2D(allServersData);
} }
} catch (err) { } catch (err) {
console.error('[Map2D] Initialization failed:', err); console.error('[Map2D] Initialization failed:', err);
} }
} }
@@ -802,15 +802,15 @@
if (!myMap2D) return; if (!myMap2D) return;
const isLight = theme === 'light'; const isLight = theme === 'light';
myMap2D.setOption({ myMap2D.setOption({
geo: { geo: {
itemStyle: { itemStyle: {
areaColor: isLight ? '#f4f6fa' : '#1a1d2e', areaColor: isLight ? '#f4f6fa' : '#1a1d2e',
borderColor: isLight ? '#cbd5e1' : '#2d334d' borderColor: isLight ? '#cbd5e1' : '#2d334d'
}, },
emphasis: { emphasis: {
itemStyle: { areaColor: isLight ? '#f4f8ff' : '#2d334d' } itemStyle: { areaColor: isLight ? '#f4f8ff' : '#2d334d' }
}
} }
}
}); });
} }
function updateMap2D(servers) { function updateMap2D(servers) {
@@ -820,175 +820,175 @@
const shiftLng = (lng) => (lng < -20 ? lng + 360 : lng); const shiftLng = (lng) => (lng < -20 ? lng + 360 : lng);
const geoData = servers const geoData = servers
.filter(s => s.lat && s.lng) .filter(s => s.lat && s.lng)
.map(s => ({ .map(s => ({
name: s.job, name: s.job,
value: [shiftLng(s.lng), s.lat], value: [shiftLng(s.lng), s.lat],
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
})); }));
// Combine all series // Combine all series
const finalSeries = [ const finalSeries = [
{ {
type: 'scatter', type: 'scatter',
coordinateSystem: 'geo', coordinateSystem: 'geo',
geoIndex: 0, geoIndex: 0,
symbolSize: 6, symbolSize: 6,
itemStyle: { itemStyle: {
color: '#06b6d4', color: '#06b6d4',
shadowBlur: 3, shadowBlur: 3,
shadowColor: 'rgba(6, 182, 212, 0.5)' shadowColor: 'rgba(6, 182, 212, 0.5)'
}, },
data: geoData, data: geoData,
zlevel: 1 zlevel: 1
} }
]; ];
// Add latency routes if configured // Add latency routes if configured
if (currentLatencies && currentLatencies.length > 0) { if (currentLatencies && currentLatencies.length > 0) {
const countryCoords = { const countryCoords = {
'china': [116.4074, 39.9042], 'china': [116.4074, 39.9042],
'beijing': [116.4074, 39.9042], 'beijing': [116.4074, 39.9042],
'shanghai': [121.4737, 31.2304], 'shanghai': [121.4737, 31.2304],
'hong kong': [114.1694, 22.3193], 'hong kong': [114.1694, 22.3193],
'taiwan': [120.9605, 23.6978], 'taiwan': [120.9605, 23.6978],
'united states': [-95.7129, 37.0902], 'united states': [-95.7129, 37.0902],
'us seattle': [-122.3321, 47.6062], 'us seattle': [-122.3321, 47.6062],
'seattle': [-122.3321, 47.6062], 'seattle': [-122.3321, 47.6062],
'us chicago': [-87.6298, 41.8781], 'us chicago': [-87.6298, 41.8781],
'chicago': [-87.6298, 41.8781], 'chicago': [-87.6298, 41.8781],
'news york': [-74.0060, 40.7128], 'news york': [-74.0060, 40.7128],
'new york corp': [-74.0060, 40.7128], 'new york corp': [-74.0060, 40.7128],
'new york': [-74.0060, 40.7128], 'new york': [-74.0060, 40.7128],
'san francisco': [-122.4194, 37.7749], 'san francisco': [-122.4194, 37.7749],
'los angeles': [-118.2437, 34.0522], 'los angeles': [-118.2437, 34.0522],
'japan': [138.2529, 36.2048], 'japan': [138.2529, 36.2048],
'tokyo': [139.6917, 35.6895], 'tokyo': [139.6917, 35.6895],
'singapore': [103.8198, 1.3521], 'singapore': [103.8198, 1.3521],
'germany': [10.4515, 51.1657], 'germany': [10.4515, 51.1657],
'frankfurt': [8.6821, 50.1109], 'frankfurt': [8.6821, 50.1109],
'united kingdom': [-3.436, 55.3781], 'united kingdom': [-3.436, 55.3781],
'london': [-0.1276, 51.5074], 'london': [-0.1276, 51.5074],
'france': [2.2137, 46.2276], 'france': [2.2137, 46.2276],
'paris': [2.3522, 48.8566], 'paris': [2.3522, 48.8566],
'south korea': [127.7669, 35.9078], 'south korea': [127.7669, 35.9078],
'korea': [127.7669, 35.9078], 'korea': [127.7669, 35.9078],
'seoul': [126.9780, 37.5665] 'seoul': [126.9780, 37.5665]
}; };
const getCoords = (name) => { const getCoords = (name) => {
const lowerName = (name || '').toLowerCase().trim(); const lowerName = (name || '').toLowerCase().trim();
if (countryCoords[lowerName]) return countryCoords[lowerName]; if (countryCoords[lowerName]) return countryCoords[lowerName];
const s = servers.find(sv =>
(sv.countryName || '').toLowerCase() === lowerName ||
(sv.country || '').toLowerCase() === lowerName ||
(sv.city || '').toLowerCase() === lowerName
);
if (s && s.lng && s.lat) return [shiftLng(s.lng), s.lat];
return null;
};
const getShiftedCoords = (name) => { const s = servers.find(sv =>
const c = countryCoords[(name || '').toLowerCase().trim()]; (sv.countryName || '').toLowerCase() === lowerName ||
if (c) return [shiftLng(c[0]), c[1]]; (sv.country || '').toLowerCase() === lowerName ||
const raw = getCoords(name); (sv.city || '').toLowerCase() === lowerName
if (raw) return [shiftLng(raw[0]), raw[1]]; );
return null; if (s && s.lng && s.lat) return [shiftLng(s.lng), s.lat];
}; return null;
};
// Group latency routes by path to handle overlap visually const getShiftedCoords = (name) => {
const routeGroups = {}; const c = countryCoords[(name || '').toLowerCase().trim()];
currentLatencies.forEach(route => { if (c) return [shiftLng(c[0]), c[1]];
const start = getShiftedCoords(route.source); const raw = getCoords(name);
const end = getShiftedCoords(route.dest); if (raw) return [shiftLng(raw[0]), raw[1]];
if (start && end) { return null;
// Canonical points for grouping (independent of direction) };
// Sort by longitude then latitude
const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
const key = `${pts[0][0].toFixed(4)},${pts[0][1].toFixed(4)}_${pts[1][0].toFixed(4)},${pts[1][1].toFixed(4)}`;
if (!routeGroups[key]) routeGroups[key] = [];
routeGroups[key].push({ route, start, end });
}
});
Object.keys(routeGroups).forEach(key => { // Group latency routes by path to handle overlap visually
const routes = routeGroups[key]; const routeGroups = {};
const count = routes.length; currentLatencies.forEach(route => {
const start = getShiftedCoords(route.source);
routes.forEach((item, i) => { const end = getShiftedCoords(route.dest);
const { route, start, end } = item; if (start && end) {
// Canonical points for grouping (independent of direction)
// Identify if the route is in the "canonical" direction // Sort by longitude then latitude
const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
const isForward = (start === pts[0]); const key = `${pts[0][0].toFixed(4)},${pts[0][1].toFixed(4)}_${pts[1][0].toFixed(4)},${pts[1][1].toFixed(4)}`;
if (!routeGroups[key]) routeGroups[key] = [];
// Calculate curveness: ensure all lines are slightly curved routeGroups[key].push({ route, start, end });
let finalCurve = 0; }
if (count === 1) { });
finalCurve = 0.15; // Default slight curve for single lines
} else {
// Spread overlapping lines: 0.15, -0.15, 0.3, -0.3...
// This creates an "eye" or "fan" effect where no line is straight
const magnitude = 0.15 + Math.floor(i / 2) * 0.15;
const spread = (i % 2 === 0) ? magnitude : -magnitude;
// Adjust sign based on direction so they occupy unique visual slots
finalCurve = isForward ? spread : -spread;
}
finalSeries.push({ Object.keys(routeGroups).forEach(key => {
type: 'lines', const routes = routeGroups[key];
coordinateSystem: 'geo', const count = routes.length;
zlevel: 2,
latencyLine: true, routes.forEach((item, i) => {
effect: { const { route, start, end } = item;
show: true,
period: 4, // Identify if the route is in the "canonical" direction
trailLength: 0.1, const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
color: 'rgba(99, 102, 241, 0.8)', const isForward = (start === pts[0]);
symbol: 'arrow',
symbolSize: 6 // Calculate curveness: ensure all lines are slightly curved
}, let finalCurve = 0;
lineStyle: { if (count === 1) {
color: 'rgba(99, 102, 241, 0.3)', finalCurve = 0.15; // Default slight curve for single lines
width: 2, } else {
curveness: finalCurve // Spread overlapping lines: 0.15, -0.15, 0.3, -0.3...
}, // This creates an "eye" or "fan" effect where no line is straight
tooltip: { const magnitude = 0.15 + Math.floor(i / 2) * 0.15;
formatter: () => { const spread = (i % 2 === 0) ? magnitude : -magnitude;
const latVal = (route.latency !== null && route.latency !== undefined) ? `${route.latency.toFixed(2)} ms` : '测量中...'; // Adjust sign based on direction so they occupy unique visual slots
return ` finalCurve = isForward ? spread : -spread;
}
finalSeries.push({
type: 'lines',
coordinateSystem: 'geo',
zlevel: 2,
latencyLine: true,
effect: {
show: true,
period: 4,
trailLength: 0.1,
color: 'rgba(99, 102, 241, 0.8)',
symbol: 'arrow',
symbolSize: 6
},
lineStyle: {
color: 'rgba(99, 102, 241, 0.3)',
width: 2,
curveness: finalCurve
},
tooltip: {
formatter: () => {
const latVal = (route.latency !== null && route.latency !== undefined) ? `${route.latency.toFixed(2)} ms` : '测量中...';
return `
<div style="padding: 4px;"> <div style="padding: 4px;">
<div style="font-weight: 700;">${route.source}${route.dest}</div> <div style="font-weight: 700;">${route.source}${route.dest}</div>
<div style="font-size: 0.75rem; color: var(--accent-indigo); margin-top: 4px;">延时: ${latVal}</div> <div style="font-size: 0.75rem; color: var(--accent-indigo); margin-top: 4px;">延时: ${latVal}</div>
</div> </div>
`; `;
} }
}, },
data: [{ data: [{
fromName: route.source, fromName: route.source,
toName: route.dest, toName: route.dest,
coords: [start, end] coords: [start, end]
}] }]
}); });
});
}); });
});
} }
myMap2D.setOption({ myMap2D.setOption({
series: finalSeries series: finalSeries
}); });
// 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;
} }
} }
@@ -1023,7 +1023,7 @@
// Update server table // Update server table
renderFilteredServers(); renderFilteredServers();
// Update globe // Update globe
updateMap2D(data.servers || []); updateMap2D(data.servers || []);
@@ -1044,8 +1044,8 @@
// Apply search filter // Apply search filter
const searchTerm = (dom.serverSearchFilter?.value || '').toLowerCase().trim(); const searchTerm = (dom.serverSearchFilter?.value || '').toLowerCase().trim();
if (searchTerm) { if (searchTerm) {
filtered = filtered.filter(s => filtered = filtered.filter(s =>
(s.job || '').toLowerCase().includes(searchTerm) || (s.job || '').toLowerCase().includes(searchTerm) ||
(s.instance || '').toLowerCase().includes(searchTerm) (s.instance || '').toLowerCase().includes(searchTerm)
); );
} }
@@ -1282,11 +1282,11 @@
async function openServerDetail(instance, job, source) { async function openServerDetail(instance, job, source) {
// Cleanup old charts if any were still present from a previous open (safety) // Cleanup old charts if any were still present from a previous open (safety)
if (currentServerDetail.charts) { if (currentServerDetail.charts) {
Object.values(currentServerDetail.charts).forEach(chart => { Object.values(currentServerDetail.charts).forEach(chart => {
if (chart && chart.destroy) chart.destroy(); if (chart && chart.destroy) chart.destroy();
}); });
} }
currentServerDetail = { instance, job, source, charts: {} }; currentServerDetail = { instance, job, source, charts: {} };
dom.serverDetailTitle.textContent = `${job}`; dom.serverDetailTitle.textContent = `${job}`;
dom.serverDetailModal.classList.add('active'); dom.serverDetailModal.classList.add('active');
@@ -1333,7 +1333,7 @@
// Disk Total // Disk Total
if (dom.detailDiskTotal) { if (dom.detailDiskTotal) {
dom.detailDiskTotal.textContent = formatBytes(data.totalDiskSize || 0); dom.detailDiskTotal.textContent = formatBytes(data.totalDiskSize || 0);
} }
// Define metrics to show // Define metrics to show
@@ -1416,21 +1416,21 @@
// Handle partitions integration: Move the expandable partition section UNDER the Disk Usage metric // Handle partitions integration: Move the expandable partition section UNDER the Disk Usage metric
if (data.partitions && data.partitions.length > 0) { if (data.partitions && data.partitions.length > 0) {
dom.detailPartitionsContainer.style.display = 'block'; dom.detailPartitionsContainer.style.display = 'block';
dom.partitionSummary.textContent = `${data.partitions.length} 个本地分区`; dom.partitionSummary.textContent = `${data.partitions.length} 个本地分区`;
// Find the disk metric item and insert the partition container after it
const diskMetricItem = document.getElementById('metric-rootFsUsedPct');
if (diskMetricItem) {
diskMetricItem.after(dom.detailPartitionsContainer);
}
dom.partitionHeader.onclick = (e) => { // Find the disk metric item and insert the partition container after it
e.stopPropagation(); const diskMetricItem = document.getElementById('metric-rootFsUsedPct');
dom.detailPartitionsContainer.classList.toggle('active'); if (diskMetricItem) {
}; diskMetricItem.after(dom.detailPartitionsContainer);
}
dom.detailPartitionsList.innerHTML = data.partitions.map(p => ` dom.partitionHeader.onclick = (e) => {
e.stopPropagation();
dom.detailPartitionsContainer.classList.toggle('active');
};
dom.detailPartitionsList.innerHTML = data.partitions.map(p => `
<div class="partition-row"> <div class="partition-row">
<div class="partition-info"> <div class="partition-info">
<span class="partition-mount">${escapeHtml(p.mountpoint)}</span> <span class="partition-mount">${escapeHtml(p.mountpoint)}</span>
@@ -1442,7 +1442,7 @@
</div> </div>
`).join(''); `).join('');
} else { } else {
dom.detailPartitionsContainer.style.display = 'none'; dom.detailPartitionsContainer.style.display = 'none';
} }
} }
@@ -1507,20 +1507,20 @@
const data = await res.json(); const data = await res.json();
if (metricKey === 'networkTrend' && data.stats) { if (metricKey === 'networkTrend' && data.stats) {
const stats = data.stats; const stats = data.stats;
const summaryDiv = document.getElementById(`summary-${metricKey}`); const summaryDiv = document.getElementById(`summary-${metricKey}`);
if (summaryDiv) { if (summaryDiv) {
summaryDiv.style.display = 'flex'; summaryDiv.style.display = 'flex';
const rxEl = document.getElementById(`stat-${metricKey}-rx`); const rxEl = document.getElementById(`stat-${metricKey}-rx`);
const txEl = document.getElementById(`stat-${metricKey}-tx`); const txEl = document.getElementById(`stat-${metricKey}-tx`);
const p95El = document.getElementById(`stat-${metricKey}-p95`); const p95El = document.getElementById(`stat-${metricKey}-p95`);
const totalEl = document.getElementById(`stat-${metricKey}-total`); const totalEl = document.getElementById(`stat-${metricKey}-total`);
if (rxEl) rxEl.textContent = formatBytes(stats.rxTotal); if (rxEl) rxEl.textContent = formatBytes(stats.rxTotal);
if (txEl) txEl.textContent = formatBytes(stats.txTotal); if (txEl) txEl.textContent = formatBytes(stats.txTotal);
if (p95El) p95El.textContent = formatBandwidth(stats.p95); if (p95El) p95El.textContent = formatBandwidth(stats.p95);
if (totalEl) totalEl.textContent = formatBytes(stats.total); if (totalEl) totalEl.textContent = formatBytes(stats.total);
} }
} }
chart.setData(data); chart.setData(data);
@@ -1582,7 +1582,7 @@
hideMessage(); hideMessage();
hideSiteMessage(); hideSiteMessage();
hideChangePasswordMessage(); hideChangePasswordMessage();
// Reset password fields // Reset password fields
if (dom.oldPasswordInput) dom.oldPasswordInput.value = ''; if (dom.oldPasswordInput) dom.oldPasswordInput.value = '';
if (dom.newPasswordInput) dom.newPasswordInput.value = ''; if (dom.newPasswordInput) dom.newPasswordInput.value = '';
@@ -1617,7 +1617,7 @@
if (networkChart) { if (networkChart) {
networkChart.showP95 = !!settings.show_95_bandwidth; networkChart.showP95 = !!settings.show_95_bandwidth;
networkChart.p95Type = settings.p95_type || 'tx'; networkChart.p95Type = settings.p95_type || 'tx';
if (dom.legendP95) { if (dom.legendP95) {
dom.legendP95.classList.toggle('disabled', !networkChart.showP95); dom.legendP95.classList.toggle('disabled', !networkChart.showP95);
} }
@@ -1639,9 +1639,9 @@
// Listen for system theme changes if set to auto (cleanup existing listener first) // Listen for system theme changes if set to auto (cleanup existing listener first)
if (siteThemeQuery && siteThemeHandler) { if (siteThemeQuery && siteThemeHandler) {
siteThemeQuery.removeEventListener('change', siteThemeHandler); siteThemeQuery.removeEventListener('change', siteThemeHandler);
} }
siteThemeQuery = window.matchMedia('(prefers-color-scheme: light)'); siteThemeQuery = window.matchMedia('(prefers-color-scheme: light)');
siteThemeHandler = () => { siteThemeHandler = () => {
const currentSavedTheme = localStorage.getItem('theme'); const currentSavedTheme = localStorage.getItem('theme');
@@ -1685,22 +1685,22 @@
</svg> </svg>
`; `;
} }
// P95 setting // P95 setting
if (settings.show_95_bandwidth !== undefined || settings.p95_type !== undefined) { if (settings.show_95_bandwidth !== undefined || settings.p95_type !== undefined) {
if (networkChart) { if (networkChart) {
if (settings.show_95_bandwidth !== undefined) { if (settings.show_95_bandwidth !== undefined) {
networkChart.showP95 = !!settings.show_95_bandwidth; networkChart.showP95 = !!settings.show_95_bandwidth;
if (dom.legendP95) { if (dom.legendP95) {
dom.legendP95.classList.toggle('disabled', !networkChart.showP95); dom.legendP95.classList.toggle('disabled', !networkChart.showP95);
} }
} }
if (settings.p95_type !== undefined) { if (settings.p95_type !== undefined) {
networkChart.p95Type = settings.p95_type; networkChart.p95Type = settings.p95_type;
if (dom.p95LabelText) { if (dom.p95LabelText) {
const types = { tx: '上行', rx: '下行', both: '上行+下行' }; const types = { tx: '上行', rx: '下行', both: '上行+下行' };
dom.p95LabelText.textContent = types[settings.p95_type] || '上行'; dom.p95LabelText.textContent = types[settings.p95_type] || '上行';
} }
} }
networkChart.draw(); networkChart.draw();
} }
@@ -1785,30 +1785,30 @@
`).join(''); `).join('');
} }
window.editRoute = function(id, source_id, source, dest, target) { window.editRoute = function (id, source_id, source, dest, target) {
editingRouteId = id; editingRouteId = id;
dom.routeSourceSelect.value = source_id; dom.routeSourceSelect.value = source_id;
dom.routeSourceInput.value = source; dom.routeSourceInput.value = source;
dom.routeDestInput.value = dest; dom.routeDestInput.value = dest;
dom.routeTargetInput.value = target; dom.routeTargetInput.value = target;
dom.btnAddRoute.textContent = '保存修改'; dom.btnAddRoute.textContent = '保存修改';
dom.btnCancelEditRoute.style.display = 'block'; dom.btnCancelEditRoute.style.display = 'block';
// Select the tab just in case (though it's already there) // Select the tab just in case (though it's already there)
const tab = Array.from(dom.modalTabs).find(t => t.dataset.tab === 'routes'); const tab = Array.from(dom.modalTabs).find(t => t.dataset.tab === 'routes');
if (tab) tab.click(); if (tab) tab.click();
}; };
function cancelEditRoute() { function cancelEditRoute() {
editingRouteId = null; editingRouteId = null;
dom.routeSourceSelect.value = ""; dom.routeSourceSelect.value = "";
dom.routeSourceInput.value = ""; dom.routeSourceInput.value = "";
dom.routeDestInput.value = ""; dom.routeDestInput.value = "";
dom.routeTargetInput.value = ""; dom.routeTargetInput.value = "";
dom.btnAddRoute.textContent = '添加线路'; dom.btnAddRoute.textContent = '添加线路';
dom.btnCancelEditRoute.style.display = 'none'; dom.btnCancelEditRoute.style.display = 'none';
} }
async function addLatencyRoute() { async function addLatencyRoute() {
@@ -1853,7 +1853,7 @@
} }
} }
window.deleteLatencyRoute = async function(id) { window.deleteLatencyRoute = async function (id) {
if (!user) { if (!user) {
showSiteMessage('请登录后再操作', 'error'); showSiteMessage('请登录后再操作', 'error');
openLoginModal(); openLoginModal();
@@ -1973,10 +1973,10 @@
} }
if (dom.routeSourceSelect) { if (dom.routeSourceSelect) {
const currentVal = dom.routeSourceSelect.value; const currentVal = dom.routeSourceSelect.value;
dom.routeSourceSelect.innerHTML = '<option value="">-- 选择数据源 --</option>' + dom.routeSourceSelect.innerHTML = '<option value="">-- 选择数据源 --</option>' +
sources.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join(''); sources.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
dom.routeSourceSelect.value = currentVal; dom.routeSourceSelect.value = currentVal;
} }
} }