From 464c3193d10a47745dffef650ae8db439b13de0c Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Sun, 5 Apr 2026 18:13:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=95=8C=E9=9D=A2=E5=B8=83?= =?UTF-8?q?=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.html | 22 ++++++++++----- public/js/app.js | 54 ++++++++++++++++++++++++++++++------ public/js/chart.js | 35 ++++++++++++++++------- public/js/utils.js | 9 ++++++ server/db-integrity-check.js | 6 ++++ server/index.js | 19 +++++++------ 6 files changed, 111 insertions(+), 34 deletions(-) diff --git a/public/index.html b/public/index.html index f8704aa..1fec209 100644 --- a/public/index.html +++ b/public/index.html @@ -181,13 +181,12 @@
- 实时带宽 (↑/↓) + 实时带宽 (MB/s ↑/↓)
- 0 B/s + 0.00 / - 0 B/s + 0.00
- 实时期末统计
@@ -206,10 +205,10 @@
- 接收 (RX) - 发送 (TX) + 接收 (RX) + 发送 (TX) - 95计费 (上行) + 95计费 (上行)
@@ -417,6 +416,15 @@ +
+ + +
diff --git a/public/js/app.js b/public/js/app.js index 149389c..048c445 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -21,7 +21,6 @@ diskDetail: document.getElementById('diskDetail'), totalBandwidthTx: document.getElementById('totalBandwidthTx'), totalBandwidthRx: document.getElementById('totalBandwidthRx'), - bandwidthDetail: document.getElementById('bandwidthDetail'), traffic24hRx: document.getElementById('traffic24hRx'), traffic24hTx: document.getElementById('traffic24hTx'), traffic24hTotal: document.getElementById('traffic24hTotal'), @@ -62,6 +61,10 @@ loginError: document.getElementById('loginError'), footerTime: document.getElementById('footerTime'), legendP95: document.getElementById('legendP95'), + legendRx: document.getElementById('legendRx'), + legendTx: document.getElementById('legendTx'), + p95LabelText: document.getElementById('p95LabelText'), + p95TypeSelect: document.getElementById('p95TypeSelect'), // Server Details Modal serverDetailModal: document.getElementById('serverDetailModal'), serverDetailClose: document.getElementById('serverDetailClose'), @@ -225,6 +228,23 @@ }); } + // RX/TX Legend Toggle + if (dom.legendRx) { + dom.legendRx.addEventListener('click', () => { + networkChart.showRx = !networkChart.showRx; + dom.legendRx.classList.toggle('disabled', !networkChart.showRx); + networkChart.draw(); + }); + } + + if (dom.legendTx) { + dom.legendTx.addEventListener('click', () => { + networkChart.showTx = !networkChart.showTx; + dom.legendTx.classList.toggle('disabled', !networkChart.showTx); + networkChart.draw(); + }); + } + // Source filter listener if (dom.sourceFilter) { dom.sourceFilter.addEventListener('change', () => { @@ -269,6 +289,7 @@ dom.logoUrlInput.value = window.SITE_SETTINGS.logo_url || ''; dom.defaultThemeInput.value = window.SITE_SETTINGS.default_theme || 'dark'; dom.show95BandwidthInput.value = window.SITE_SETTINGS.show_95_bandwidth ? "1" : "0"; + dom.p95TypeSelect.value = window.SITE_SETTINGS.p95_type || 'tx'; } loadSiteSettings(); @@ -571,9 +592,8 @@ dom.diskDetail.textContent = `${formatBytes(data.disk.used)}/${formatBytes(data.disk.total)}`; // Bandwidth - dom.totalBandwidthTx.textContent = formatBandwidth(data.network.tx || 0); - dom.totalBandwidthRx.textContent = formatBandwidth(data.network.rx || 0); - dom.bandwidthDetail.textContent = `当前实时数据聚合`; + dom.totalBandwidthTx.textContent = toMBps(data.network.tx || 0); + dom.totalBandwidthRx.textContent = toMBps(data.network.rx || 0); // 24h traffic dom.traffic24hRx.textContent = formatBytes(data.traffic24h.rx); @@ -1111,9 +1131,15 @@ dom.show95BandwidthInput.value = settings.show_95_bandwidth ? "1" : "0"; if (networkChart) { networkChart.showP95 = !!settings.show_95_bandwidth; + networkChart.p95Type = settings.p95_type || 'tx'; + if (dom.legendP95) { dom.legendP95.classList.toggle('disabled', !networkChart.showP95); } + if (dom.p95LabelText) { + const types = { tx: '上行', rx: '下行', both: '上行+下行' }; + dom.p95LabelText.textContent = types[networkChart.p95Type] || '上行'; + } networkChart.draw(); } } @@ -1170,11 +1196,20 @@ } // P95 setting - if (settings.show_95_bandwidth !== undefined) { + if (settings.show_95_bandwidth !== undefined || settings.p95_type !== undefined) { if (networkChart) { - networkChart.showP95 = !!settings.show_95_bandwidth; - if (dom.legendP95) { - dom.legendP95.classList.toggle('disabled', !networkChart.showP95); + if (settings.show_95_bandwidth !== undefined) { + networkChart.showP95 = !!settings.show_95_bandwidth; + if (dom.legendP95) { + dom.legendP95.classList.toggle('disabled', !networkChart.showP95); + } + } + if (settings.p95_type !== undefined) { + networkChart.p95Type = settings.p95_type; + if (dom.p95LabelText) { + const types = { tx: '上行', rx: '下行', both: '上行+下行' }; + dom.p95LabelText.textContent = types[settings.p95_type] || '上行'; + } } networkChart.draw(); } @@ -1193,7 +1228,8 @@ title: dom.siteTitleInput.value.trim(), logo_url: dom.logoUrlInput.value.trim(), default_theme: dom.defaultThemeInput.value, - show_95_bandwidth: dom.show95BandwidthInput.value === "1" ? 1 : 0 + show_95_bandwidth: dom.show95BandwidthInput.value === "1" ? 1 : 0, + p95_type: dom.p95TypeSelect.value }; dom.btnSaveSiteSettings.disabled = true; diff --git a/public/js/chart.js b/public/js/chart.js index c4f56fa..08d09bf 100644 --- a/public/js/chart.js +++ b/public/js/chart.js @@ -10,6 +10,9 @@ class AreaChart { this.animProgress = 0; this.animFrame = null; this.showP95 = false; + this.showRx = true; + this.showTx = true; + this.p95Type = 'tx'; // 'tx', 'rx', 'both' this.dpr = window.devicePixelRatio || 1; this.padding = { top: 20, right: 16, bottom: 32, left: 56 }; @@ -49,8 +52,15 @@ class AreaChart { } // Calculate P95 (95th percentile) - // Updated: Only count Upstream (TX) as requested - const combined = data.tx.map(t => t || 0); + let combined = []; + if (this.p95Type === 'tx') { + combined = data.tx.map(t => t || 0); + } else if (this.p95Type === 'rx') { + combined = data.rx.map(r => r || 0); + } else { + combined = data.tx.map((t, i) => (t || 0) + (data.rx[i] || 0)); + } + if (combined.length > 0) { const sorted = [...combined].sort((a, b) => a - b); const p95Idx = Math.floor(sorted.length * 0.95); @@ -103,9 +113,10 @@ class AreaChart { } // Find max raw value - let maxDataVal = 0; + let maxDataVal = 1024; // Minimum 1KB/s scale for (let i = 0; i < rx.length; i++) { - maxDataVal = Math.max(maxDataVal, rx[i] || 0, tx[i] || 0); + if (this.showRx) maxDataVal = Math.max(maxDataVal, rx[i] || 0); + if (this.showTx) maxDataVal = Math.max(maxDataVal, tx[i] || 0); } // Determine consistent unit based on max data value @@ -177,14 +188,18 @@ class AreaChart { ctx.fillText(formatTime(timestamps[len - 1]), getX(len - 1), h - 8); // Draw TX area - this.drawArea(ctx, tx, getX, getY, chartH, p, - 'rgba(99, 102, 241, 0.25)', 'rgba(99, 102, 241, 0.02)', - '#6366f1', len); + if (this.showTx) { + this.drawArea(ctx, tx, getX, getY, chartH, p, + 'rgba(99, 102, 241, 0.25)', 'rgba(99, 102, 241, 0.02)', + '#6366f1', len); + } // Draw RX area (on top) - this.drawArea(ctx, rx, getX, getY, chartH, p, - 'rgba(6, 182, 212, 0.25)', 'rgba(6, 182, 212, 0.02)', - '#06b6d4', len); + if (this.showRx) { + this.drawArea(ctx, rx, getX, getY, chartH, p, + 'rgba(6, 182, 212, 0.25)', 'rgba(6, 182, 212, 0.02)', + '#06b6d4', len); + } // Draw P95 line if (this.showP95 && this.p95 && this.animProgress === 1) { diff --git a/public/js/utils.js b/public/js/utils.js index 9d0fdc1..42e48f0 100644 --- a/public/js/utils.js +++ b/public/js/utils.js @@ -28,6 +28,15 @@ function formatBandwidth(bytesPerSec, decimals = 2) { return value.toFixed(decimals) + ' ' + sizes[i]; } +/** + * Convert bytes per second to MB/s (numeric string) + */ +function toMBps(bytesPerSec, decimals = 2) { + if (!bytesPerSec || bytesPerSec === 0) return '0.00'; + const mbps = bytesPerSec / (1024 * 1024); + return mbps.toFixed(decimals); +} + /** * Format percentage */ diff --git a/server/db-integrity-check.js b/server/db-integrity-check.js index 81c6a6d..c95202e 100644 --- a/server/db-integrity-check.js +++ b/server/db-integrity-check.js @@ -45,6 +45,11 @@ async function checkAndFixDatabase() { await db.query("ALTER TABLE site_settings ADD COLUMN show_95_bandwidth TINYINT(1) DEFAULT 0 AFTER default_theme"); console.log(`[Database Integrity] ✅ Column 'show_95_bandwidth' added.`); } + if (!columnNames.includes('p95_type')) { + console.log(`[Database Integrity] ⚠️ Missing column 'p95_type' in 'site_settings'. Adding it...`); + await db.query("ALTER TABLE site_settings ADD COLUMN p95_type VARCHAR(20) DEFAULT 'tx' AFTER show_95_bandwidth"); + console.log(`[Database Integrity] ✅ Column 'p95_type' added.`); + } } catch (err) { console.error('[Database Integrity] ❌ Error checking integrity:', err.message); } @@ -85,6 +90,7 @@ async function createTable(tableName) { logo_url TEXT, default_theme VARCHAR(20) DEFAULT 'dark', show_95_bandwidth TINYINT(1) DEFAULT 0, + p95_type VARCHAR(20) DEFAULT 'tx', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); diff --git a/server/index.js b/server/index.js index 576f57a..73468af 100644 --- a/server/index.js +++ b/server/index.js @@ -217,12 +217,13 @@ app.post('/api/setup/init', async (req, res) => { logo_url TEXT, default_theme VARCHAR(20) DEFAULT 'dark', show_95_bandwidth TINYINT(1) DEFAULT 0, + p95_type VARCHAR(20) DEFAULT 'tx', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await connection.query(` - INSERT IGNORE INTO site_settings (id, page_name, title, default_theme, show_95_bandwidth) - VALUES (1, '数据可视化展示大屏', '数据可视化展示大屏', 'dark', 0) + INSERT IGNORE INTO site_settings (id, page_name, title, default_theme, show_95_bandwidth, p95_type) + VALUES (1, '数据可视化展示大屏', '数据可视化展示大屏', 'dark', 0, 'tx') `); await connection.end(); @@ -489,7 +490,8 @@ app.get('/api/settings', async (req, res) => { page_name: '数据可视化展示大屏', title: '数据可视化展示大屏', logo_url: null, - show_95_bandwidth: 0 + show_95_bandwidth: 0, + p95_type: 'tx' }); } res.json(rows[0]); @@ -501,18 +503,19 @@ app.get('/api/settings', async (req, res) => { // Update site settings app.post('/api/settings', requireAuth, async (req, res) => { - const { page_name, title, logo_url, default_theme, show_95_bandwidth } = req.body; + const { page_name, title, logo_url, default_theme, show_95_bandwidth, p95_type } = req.body; try { await db.query( - `INSERT INTO site_settings (id, page_name, title, logo_url, default_theme, show_95_bandwidth) - VALUES (1, ?, ?, ?, ?, ?) + `INSERT INTO site_settings (id, page_name, title, logo_url, default_theme, show_95_bandwidth, p95_type) + VALUES (1, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE page_name = VALUES(page_name), title = VALUES(title), logo_url = VALUES(logo_url), default_theme = VALUES(default_theme), - show_95_bandwidth = VALUES(show_95_bandwidth)`, - [page_name, title, logo_url, default_theme, show_95_bandwidth ? 1 : 0] + show_95_bandwidth = VALUES(show_95_bandwidth), + p95_type = VALUES(p95_type)`, + [page_name, title, logo_url, default_theme, show_95_bandwidth ? 1 : 0, p95_type || 'tx'] ); res.json({ success: true }); } catch (err) {