81ebbc3e45
- animateNumber(): 900ms ease-out 缓动, 存储前值于 data-val - CPU%、MEM%、进程内存 三个指标使用跳动动画 - 与 Sentinel 卡片动画风格统一 Co-Authored-By: Claude <noreply@anthropic.com>
201 lines
7.7 KiB
JavaScript
201 lines
7.7 KiB
JavaScript
/* ── 数字跳动动画 ── */
|
|
function animateNumber(el, newVal, decimals, suffix) {
|
|
var oldVal = parseFloat(el.getAttribute('data-val') || newVal);
|
|
if (oldVal === newVal) { el.textContent = newVal.toFixed(decimals) + suffix; return; }
|
|
el.setAttribute('data-val', newVal);
|
|
var start = performance.now();
|
|
var duration = 900;
|
|
function step(ts) {
|
|
var p = Math.min((ts - start) / duration, 1);
|
|
var eased = 1 - Math.pow(1 - p, 3); // ease-out
|
|
el.textContent = (oldVal + (newVal - oldVal) * eased).toFixed(decimals) + suffix;
|
|
if (p < 1) requestAnimationFrame(step);
|
|
}
|
|
requestAnimationFrame(step);
|
|
}
|
|
|
|
window.DashboardModule = {
|
|
ws: null,
|
|
reconnectTimer: null,
|
|
reconnectDelay: 1000,
|
|
charts: {},
|
|
|
|
init: function() {
|
|
var self = this;
|
|
|
|
// 初始化图表
|
|
self.charts = {
|
|
mem: new MiniChart('chart-mem', '#9ece6a'),
|
|
cpu: new MiniChart('chart-cpu', '#7aa2f7'),
|
|
net: new MiniChart('chart-net', '#e0af68'),
|
|
speed: new DualLineChart('chart-speed', '#bb9af7', '#c0a8f0'),
|
|
proc_mem: new MiniChart('chart-proc-mem', '#f7768e')
|
|
};
|
|
|
|
// 静态信息 (HTTP 一次)
|
|
fetchSystemStaticInfo();
|
|
|
|
// WebSocket 实时推送
|
|
self._connect();
|
|
},
|
|
|
|
_connect: function() {
|
|
var self = this;
|
|
if (self.ws) {
|
|
self.ws.onclose = null;
|
|
self.ws.close();
|
|
self.ws = null;
|
|
}
|
|
|
|
var base = window.location.pathname.split("/").slice(0, 2).join("/");
|
|
var ws = new WebSocket("ws://" + location.host + base + "/api/system/ws");
|
|
self.ws = ws;
|
|
|
|
ws.onopen = function() {
|
|
self.reconnectDelay = 1000;
|
|
};
|
|
|
|
ws.onmessage = function(e) {
|
|
try {
|
|
var d = JSON.parse(e.data);
|
|
if (d.type === "sys") {
|
|
self._updateUI(d.system, d.framework);
|
|
}
|
|
} catch(ex) {}
|
|
};
|
|
|
|
ws.onclose = function() {
|
|
self.ws = null;
|
|
self.reconnectTimer = setTimeout(function() {
|
|
self.reconnectDelay = Math.min(self.reconnectDelay * 1.5, 15000);
|
|
self._connect();
|
|
}, self.reconnectDelay);
|
|
};
|
|
|
|
ws.onerror = function() { ws.close(); };
|
|
},
|
|
|
|
_updateUI: function(sys, fw) {
|
|
// ── 框架卡片 ──
|
|
if (fw) {
|
|
var el;
|
|
el = document.getElementById('d-uptime'); if (el) el.textContent = formatUptime(fw.uptime || 0);
|
|
el = document.getElementById('d-plugins'); if (el) el.textContent = fw.plugins || 0;
|
|
el = document.getElementById('d-ver-badge'); if (el) el.textContent = fw.version || '?';
|
|
}
|
|
if (!sys) return;
|
|
var self = this;
|
|
|
|
// ── 内存 ──
|
|
if (sys.memory) {
|
|
var m = sys.memory.percent || 0;
|
|
var el = document.getElementById('d-mem'); if (el) animateNumber(el, m, 1, '%');
|
|
el = document.getElementById('d-mem-detail');
|
|
if (el) el.textContent = (sys.memory.used_gb || 0).toFixed(1) + ' / ' + (sys.memory.total_gb || 0).toFixed(1) + ' GB';
|
|
self.charts.mem.update(m);
|
|
}
|
|
|
|
// ── CPU ──
|
|
if (sys.cpu) {
|
|
var cpuVal = sys.cpu.percent;
|
|
if (cpuVal === null || cpuVal === undefined) {
|
|
var load = (sys.cpu.load_avg && sys.cpu.load_avg[0]) ? sys.cpu.load_avg[0] : 0;
|
|
cpuVal = Math.min(100, (load / (sys.cpu.cores || 1)) * 100);
|
|
}
|
|
var el = document.getElementById('d-cpu'); if (el) animateNumber(el, cpuVal, 0, '%');
|
|
self.charts.cpu.update(cpuVal);
|
|
|
|
if (sys.cpu.load_avg) {
|
|
el = document.getElementById('info-load'); if (el) el.textContent = sys.cpu.load_avg[2].toFixed(2);
|
|
}
|
|
}
|
|
|
|
// ── 网络累计流量 (RX + TX) ──
|
|
if (sys.network) {
|
|
var el;
|
|
el = document.getElementById('d-net-rx'); if (el) el.textContent = formatTraffic(sys.network.rx_mb);
|
|
el = document.getElementById('d-net-tx'); if (el) el.textContent = formatTraffic(sys.network.tx_mb);
|
|
// Chart shows RX trend
|
|
self.charts.net.update(sys.network.rx_mb || 0);
|
|
}
|
|
|
|
// ── 实时网速 ──
|
|
if (sys.net_speed) {
|
|
var el;
|
|
el = document.getElementById('d-speed-rx');
|
|
if (el) el.textContent = formatSpeed(sys.net_speed.rx_bytes_sec);
|
|
el = document.getElementById('d-speed-tx');
|
|
if (el) el.textContent = formatSpeed(sys.net_speed.tx_bytes_sec);
|
|
// Chart: RX solid line, TX dashed line
|
|
self.charts.speed.update(sys.net_speed.rx_bytes_sec || 0, sys.net_speed.tx_bytes_sec || 0);
|
|
}
|
|
|
|
// ── 进程内存 ──
|
|
if (sys.process && sys.process.memory_mb !== undefined) {
|
|
var pm = sys.process.memory_mb || 0;
|
|
var el = document.getElementById('d-proc-mem'); if (el) animateNumber(el, pm, 1, ' MB');
|
|
self.charts.proc_mem.update(pm);
|
|
}
|
|
if (sys.process && sys.process.pid !== undefined) {
|
|
var el = document.getElementById('info-pid'); if (el) el.textContent = sys.process.pid;
|
|
}
|
|
},
|
|
|
|
destroy: function() {
|
|
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
|
|
if (this.ws) { this.ws.onclose = null; this.ws.close(); this.ws = null; }
|
|
this.charts = {};
|
|
}
|
|
};
|
|
|
|
/* ── 静态信息 ── */
|
|
async function fetchSystemStaticInfo() {
|
|
try {
|
|
var sys = await fetch('./api/system', {credentials:'include'}).then(function(r){return r.json();});
|
|
var el;
|
|
if (sys.platform) {
|
|
el = document.getElementById('info-os'); if (el) el.textContent = sys.platform.system || '--';
|
|
el = document.getElementById('info-arch'); if (el) el.textContent = sys.platform.machine || '--';
|
|
el = document.getElementById('info-env'); if (el) el.textContent = sys.platform.env || 'Standard';
|
|
}
|
|
if (sys.cpu) {
|
|
var c = sys.cpu.cores || 0;
|
|
el = document.getElementById('info-cores'); if (el) el.textContent = c + ' / ' + c;
|
|
}
|
|
if (sys.memory) {
|
|
el = document.getElementById('info-mem-total'); if (el) el.textContent = sys.memory.total_gb + ' GB';
|
|
}
|
|
// 框架信息 (部分来自 API)
|
|
try {
|
|
var fw = await fetch('./api/framework', {credentials:'include'}).then(function(r){return r.json();});
|
|
el = document.getElementById('info-fw-ver'); if (el) el.textContent = fw.version || '?';
|
|
} catch(e) {}
|
|
var host = window.location.hostname + (window.location.port ? ':' + window.location.port : '');
|
|
el = document.getElementById('info-addr'); if (el) el.textContent = host;
|
|
} catch(e) {}
|
|
}
|
|
|
|
/* ── 格式化 ── */
|
|
function formatUptime(seconds) {
|
|
var h = Math.floor(seconds / 3600);
|
|
var m = Math.floor((seconds % 3600) / 60);
|
|
var s = Math.floor(seconds % 60);
|
|
if (h > 0) return h + 'h ' + m + 'm';
|
|
if (m > 0) return m + 'm ' + s + 's';
|
|
return s + 's';
|
|
}
|
|
|
|
function formatTraffic(mb) {
|
|
if (mb === undefined || mb === null) return '--';
|
|
if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB';
|
|
if (mb >= 1) return mb.toFixed(1) + ' MB';
|
|
return (mb * 1024).toFixed(0) + ' KB';
|
|
}
|
|
|
|
function formatSpeed(bytesPerSec) {
|
|
if (bytesPerSec === undefined || bytesPerSec === null || bytesPerSec < 0) return '0 B/s';
|
|
if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(1) + ' MB/s';
|
|
if (bytesPerSec >= 1024) return (bytesPerSec / 1024).toFixed(0) + ' KB/s';
|
|
return bytesPerSec.toFixed(0) + ' B/s';
|
|
}
|