feat: WS实时推送、网速双线图、按钮MD3修复、页面加载器修复 (v0.6.0)
后端: - 新增 /api/system/ws WebSocket端点 替代HTTP轮询 (每2s推送系统+框架数据) - 修复 uptime始终为0 (sm.start_time时间戳) - 网络采集新增TX上行+实时网速(delta法) - 进程内存采集 (/proc/self/status VmRSS) - 抑制aiohttp内部WS帧日志防刷爆日志文件 前端: - 仪表盘: HTTP轮询→WS连接+指数退避自动重连 - 实时日志: 修复onclose重连bug+批量渲染30fps+500行上限防卡死 - 网速卡片: DualLineChart双线图(下行实线/上行虚线) - Y轴零点偏移-5% 防止零网速贴底 - 所有按钮修复MD3组合类(btn+btn-tonal+btn-sm) - 页面加载器: 改动态script元素执行 修复onclick全局作用域问题 - emoji图标→Material Design SVG图标 - CSS去残留</style>+新增.nav-item svg约束 - 登录页输入框+标签左对齐+按钮MD3样式 - 多个版本号显示修复(去双重v前缀) - chart.js/app.js/HTML页面统一加版本号防浏览器缓存 - .gitignore新增docs/目录 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -55,21 +55,30 @@ async function loadPage(pageName) {
|
||||
|
||||
content.innerHTML = cleanHtml;
|
||||
|
||||
// Execute inline scripts
|
||||
// Execute inline scripts via dynamic <script> tag so function
|
||||
// declarations become globally accessible (onclick handlers need them)
|
||||
for(const code of scripts) {
|
||||
try { (new Function(code)).call(window); } catch(e) { console.error('Inline script error:', e); }
|
||||
try {
|
||||
const s = document.createElement('script');
|
||||
s.textContent = code;
|
||||
document.head.appendChild(s);
|
||||
document.head.removeChild(s);
|
||||
} catch(e) { console.error('Inline script error:', e); }
|
||||
}
|
||||
|
||||
// Load external JS module (optional — skip if 404)
|
||||
// Load external JS module the same way (optional — skip if 404)
|
||||
try {
|
||||
const jsResp = await fetch(`./static/pages/${pageName}.js?t=${Date.now()}`);
|
||||
if(jsResp.ok) {
|
||||
const jsCode = await jsResp.text();
|
||||
(new Function(jsCode)).call(window);
|
||||
const s = document.createElement('script');
|
||||
s.textContent = jsCode;
|
||||
document.head.appendChild(s);
|
||||
document.head.removeChild(s);
|
||||
const moduleName = pageName.charAt(0).toUpperCase() + pageName.slice(1) + 'Module';
|
||||
if(window[moduleName]?.init) window[moduleName].init();
|
||||
}
|
||||
} catch(e) { /* JS module optional */ }
|
||||
} catch(e) { console.error('Page JS error:', pageName, e); }
|
||||
|
||||
bar.style.width = '100%';
|
||||
setTimeout(() => bar.classList.remove('active'), 200);
|
||||
|
||||
+102
-23
@@ -5,13 +5,13 @@ class MiniChart {
|
||||
this.color = color;
|
||||
this.data = new Array(60).fill(0); // 60秒历史
|
||||
this.maxVal = 100;
|
||||
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
}
|
||||
|
||||
resize() {
|
||||
const rect = this.canvas.parentElement.getBoundingClientRect();
|
||||
var rect = this.canvas.parentElement.getBoundingClientRect();
|
||||
this.canvas.width = rect.width - 24;
|
||||
this.canvas.height = 60;
|
||||
this.draw();
|
||||
@@ -19,33 +19,112 @@ class MiniChart {
|
||||
|
||||
update(val) {
|
||||
this.data.push(val);
|
||||
if(this.data.length > 60) this.data.shift();
|
||||
this.maxVal = Math.max(...this.data, 100);
|
||||
if (this.data.length > 60) this.data.shift();
|
||||
this.maxVal = Math.max.apply(null, this.data.concat([100]));
|
||||
this.draw();
|
||||
}
|
||||
|
||||
draw() {
|
||||
if(!this.ctx) return;
|
||||
const { width, height } = this.canvas;
|
||||
this.ctx.clearRect(0, 0, width, height);
|
||||
|
||||
this.ctx.strokeStyle = this.color;
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
|
||||
this.data.forEach((v, i) => {
|
||||
const x = (i / 59) * width;
|
||||
const y = height - (v / this.maxVal) * (height - 10);
|
||||
if(i === 0) this.ctx.moveTo(x, y);
|
||||
else this.ctx.lineTo(x, y);
|
||||
if (!this.ctx) return;
|
||||
var ctx = this.ctx;
|
||||
var w = this.canvas.width;
|
||||
var h = this.canvas.height;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
ctx.strokeStyle = this.color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
var self = this;
|
||||
this.data.forEach(function(v, i) {
|
||||
var x = (i / 59) * w;
|
||||
var y = h - (v / self.maxVal) * (h - 10);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
this.ctx.stroke();
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// 填充渐变
|
||||
this.ctx.lineTo(width, height);
|
||||
this.ctx.lineTo(0, height);
|
||||
this.ctx.fillStyle = this.color + '20';
|
||||
this.ctx.fill();
|
||||
ctx.lineTo(w, h);
|
||||
ctx.lineTo(0, h);
|
||||
ctx.fillStyle = this.color + '20';
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Dual-line chart (e.g. download + upload speed) ── */
|
||||
class DualLineChart {
|
||||
constructor(canvasId, colorA, colorB) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.colorA = colorA || '#bb9af7'; // solid line (download)
|
||||
this.colorB = colorB || '#c0a8f0'; // dashed line (upload)
|
||||
this.dataA = new Array(60).fill(0);
|
||||
this.dataB = new Array(60).fill(0);
|
||||
this.maxVal = 100;
|
||||
this.applyResize();
|
||||
window.addEventListener('resize', () => this.applyResize());
|
||||
}
|
||||
|
||||
applyResize() {
|
||||
var rect = this.canvas.parentElement.getBoundingClientRect();
|
||||
this.canvas.width = rect.width - 24;
|
||||
this.canvas.height = 60;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
update(valA, valB) {
|
||||
this.dataA.push(valA);
|
||||
this.dataB.push(valB);
|
||||
if (this.dataA.length > 60) { this.dataA.shift(); this.dataB.shift(); }
|
||||
var all = this.dataA.concat(this.dataB).concat([100]);
|
||||
this.maxVal = Math.max.apply(null, all);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!this.ctx) return;
|
||||
var ctx = this.ctx;
|
||||
var w = this.canvas.width;
|
||||
var h = this.canvas.height;
|
||||
|
||||
var self = this;
|
||||
|
||||
// Y-axis: range from -5% to 100%, so zero never hugs the bottom
|
||||
var pad = Math.max(10, self.maxVal * 0.05);
|
||||
var range = self.maxVal + pad; // e.g. 0..100 becomes -5..100
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// ── Line A: solid (download) ──
|
||||
ctx.strokeStyle = this.colorA;
|
||||
ctx.lineWidth = 1.8;
|
||||
ctx.setLineDash([]);
|
||||
ctx.beginPath();
|
||||
this.dataA.forEach(function(v, i) {
|
||||
var x = (i / 59) * w;
|
||||
var y = h - ((v + pad) / range) * (h - 8);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
|
||||
// ── Line B: dashed (upload) ──
|
||||
ctx.strokeStyle = this.colorB;
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.beginPath();
|
||||
this.dataB.forEach(function(v, i) {
|
||||
var x = (i / 59) * w;
|
||||
var y = h - ((v + pad) / range) * (h - 8);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
window.MiniChart = MiniChart;
|
||||
window.DualLineChart = DualLineChart;
|
||||
|
||||
Reference in New Issue
Block a user