优化界面布局
This commit is contained in:
@@ -181,13 +181,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<span class="stat-card-label">实时带宽 (↑/↓)</span>
|
||||
<span class="stat-card-label">实时带宽 (MB/s ↑/↓)</span>
|
||||
<div class="stat-card-value-group">
|
||||
<span class="stat-card-value" id="totalBandwidthTx">0 B/s</span>
|
||||
<span class="stat-card-value" id="totalBandwidthTx">0.00</span>
|
||||
<span class="stat-card-separator">/</span>
|
||||
<span class="stat-card-value" id="totalBandwidthRx">0 B/s</span>
|
||||
<span class="stat-card-value" id="totalBandwidthRx">0.00</span>
|
||||
</div>
|
||||
<span class="stat-card-sub" id="bandwidthDetail">实时期末统计</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -206,10 +205,10 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot legend-rx"></span>接收 (RX)</span>
|
||||
<span class="legend-item"><span class="legend-dot legend-tx"></span>发送 (TX)</span>
|
||||
<span class="legend-item" id="legendRx" style="cursor: pointer;" title="点击切换 接收 (RX) 显示/隐藏"><span class="legend-dot legend-rx"></span>接收 (RX)</span>
|
||||
<span class="legend-item" id="legendTx" style="cursor: pointer;" title="点击切换 发送 (TX) 显示/隐藏"><span class="legend-dot legend-tx"></span>发送 (TX)</span>
|
||||
<span class="legend-item disabled" id="legendP95" style="cursor: pointer;" title="点击切换 P95 线显示/隐藏">
|
||||
<span class="legend-dot legend-p95"></span>95计费 (上行)
|
||||
<span class="legend-dot legend-p95"></span>95计费 (<span id="p95LabelText">上行</span>)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -417,6 +416,15 @@
|
||||
<option value="0">不显示</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 15px;">
|
||||
<label for="p95TypeSelect">95带宽计费统计类型</label>
|
||||
<select id="p95TypeSelect"
|
||||
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary);">
|
||||
<option value="tx">仅统计上行 (TX)</option>
|
||||
<option value="rx">仅统计下行 (RX)</option>
|
||||
<option value="both">统计上行+下行 (Sum)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end;">
|
||||
<button class="btn btn-add" id="btnSaveSiteSettings">保存设置</button>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user