diff --git a/web/css/app.css b/web/css/app.css
index 6d3a688..dce584c 100644
--- a/web/css/app.css
+++ b/web/css/app.css
@@ -2,6 +2,7 @@
body.light{--bg:#f6f7f9;--bg-alt:#ffffff;--border:#e2e8f0;--text:#1e293b;--text-dim:#64748b;--accent:#2563eb;--accent-glow:#3b82f6;--shadow:0 4px 20px -4px rgba(0,0,0,.08);--logo-start:#2563eb;--logo-end:#1d4ed8;--logo-accent-grad-start:#1d4ed8;--logo-accent-grad-end:#60a5fa}
*{box-sizing:border-box}
html,body{height:100%;margin:0;padding:0;font-family:var(--font);background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
+html.light{background:var(--bg)}
body,button{font-size:14px;line-height:1.35}
a{color:var(--accent);text-decoration:none}
a:hover{color:var(--accent-glow)}
@@ -27,9 +28,15 @@ table.data th,table.data td{font-variant-numeric:tabular-nums}
table.data thead th:nth-child(7),table.data tbody td:nth-child(7),
table.data thead th:nth-child(8),table.data tbody td:nth-child(8),
table.data thead th:nth-child(9),table.data tbody td:nth-child(9){
- width:170px;min-width:170px;max-width:170px;/* 基于最大示例 1111.12GB|1123.12GB 预留空间 */
+ width:132px;min-width:132px;max-width:132px;/* 适配示例 111.1GB|222.2GB */
font-variant-numeric:tabular-nums;letter-spacing:.3px;
}
+/* CPU / 内存 / 硬盘 列:居中 + 固定宽度 与仪表盘一致 */
+table.data thead th:nth-child(10),table.data tbody td:nth-child(10),
+table.data thead th:nth-child(11),table.data tbody td:nth-child(11),
+table.data thead th:nth-child(12),table.data tbody td:nth-child(12){
+ width:70px;min-width:70px;max-width:70px;text-align:center;padding-left:.25rem;padding-right:.25rem;
+}
table.data tbody tr:last-child td{border-bottom:none}
table.data tbody tr:hover{background:rgba(255,255,255,.04)}
.badge{display:inline-block;padding:2px 6px;font-size:11px;border-radius:12px;font-weight:500;line-height:1.2;background:var(--bg);border:1px solid var(--border);color:var(--text-dim)}
@@ -63,6 +70,30 @@ table.data tbody tr:hover{background:rgba(255,255,255,.04)}
/* spark lines */
.spark{width:82px;height:28px;display:flex;align-items:center;justify-content:center}
.spark canvas{width:100%;height:100%;display:block}
+/* 仪表盘样式 (替换 spark) */
+/* 全圆旧样式(保留以便回退) */
+.gauge{--p:0;--col:var(--accent);width:74px;height:74px;position:relative;display:grid;place-items:center;font-size:11px;font-family:ui-monospace,monospace;font-weight:600;color:var(--text);}
+.gauge:before{content:"";position:absolute;inset:0;border-radius:50%;background:conic-gradient(var(--col) calc(var(--p)*1turn),rgba(255,255,255,0.06) 0);mask:radial-gradient(circle at 50% 50%,transparent 58%,#000 59%);-webkit-mask:radial-gradient(circle at 50% 50%,transparent 58%,#000 59%);border:1px solid var(--border);box-shadow:0 2px 6px -2px rgba(0,0,0,.4),0 0 0 1px rgba(255,255,255,.05);}
+.gauge span{position:relative;z-index:1}
+
+/* 半圆仪表盘 */
+.gauge-half{--p:0;width:60px;height:34px;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;font-family:ui-monospace,monospace;font-size:10px;font-weight:600;gap:0;color:var(--text);}
+.gauge-half svg{width:100%;height:28px;overflow:visible;}
+.gauge-half path{fill:none;stroke-linecap:round;}
+/* 基础颜色 (暗色/亮色自动过渡) */
+.gauge-half path.track{stroke:color-mix(in srgb,var(--text-dim) 18%,transparent);stroke-width:6;}
+.gauge-half path.arc{stroke:var(--gauge-base,#3b82f6);stroke-width:8;stroke-dasharray:126;stroke-dashoffset:calc(126*(1 - var(--p)));transition:stroke-dashoffset .8s cubic-bezier(.4,0,.2,1),stroke .35s;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45));}
+.gauge-half[data-type=mem] path.arc{--gauge-base:#10b981}
+.gauge-half[data-type=hdd] path.arc{--gauge-base:#f59e0b}
+/* 阈值颜色:>=50% 警告黄,>=90% 危险红 */
+.gauge-half[data-warn] path.arc{stroke:var(--warn)}
+.gauge-half[data-bad] path.arc{stroke:var(--danger)}
+/* 指针:以中心(50,50)为原点旋转;半圆角度范围 180deg -> 从 180deg (左) 到 0deg(右) */
+.gauge-half span{line-height:1;position:relative;top:-6px;font-size:12px;}
+/* 亮色模式细化对比度 */
+body.light .gauge-half path.track{stroke:color-mix(in srgb,var(--text-dim) 28%,transparent)}
+body.light .gauge-half path.arc{filter:none}
+body.light .gauge-half .needle{background:linear-gradient(var(--text),var(--text-dim))}
/* status pill */
.pill{display:inline-block;padding:2px 12px;font-size:12px;font-weight:600;border-radius:999px;letter-spacing:.5px;min-width:54px;text-align:center;line-height:1.3;border:0;box-shadow:0 2px 4px -1px rgba(0,0,0,.4),0 0 0 1px rgba(255,255,255,.04);transition:var(--trans);color:#fff}
@@ -72,8 +103,8 @@ table.data tbody tr:hover{background:rgba(255,255,255,.04)}
.pill.off:hover{filter:brightness(1.1)}
/* buckets CU/CT/CM (simple version) */
-.buckets{display:flex;align-items:flex-end;gap:6px;min-width:110px}
-.bucket{position:relative;width:24px;height:28px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);border-radius:7px;padding:3px 3px 14px;box-sizing:border-box;display:flex;justify-content:flex-end}
+.buckets{display:flex;align-items:flex-end;gap:8px;min-width:140px}
+.bucket{position:relative;width:26px;height:34px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);border-radius:8px;padding:4px 4px 16px;box-sizing:border-box;display:flex;justify-content:flex-end}
.bucket span{display:block;width:100%;background:var(--accent);border-radius:4px 4px 6px 6px;height:var(--h);align-self:flex-end;transition:height .8s cubic-bezier(.4,0,.2,1),background .3s}
.bucket[data-lv=warn] span{background:var(--warn)}
.bucket[data-lv=bad] span{background:var(--danger)}
@@ -82,6 +113,10 @@ table.data tbody tr:hover{background:rgba(255,255,255,.04)}
/* 居中联通电信移动列 */
table.data thead th:last-child, table.data tbody td:last-child { text-align:center; }
+/* 放大第13列宽度以容纳更宽水桶 */
+table.data thead th:nth-child(13),table.data tbody td:nth-child(13){
+ width:170px;min-width:170px;max-width:170px;
+}
.buckets{justify-content:center}
/* 响应式隐藏非关键列,保持可读性 */
diff --git a/web/js/app.js b/web/js/app.js
index 0f773d2..f838532 100644
--- a/web/js/app.js
+++ b/web/js/app.js
@@ -91,13 +91,13 @@ function renderServers(){
${s.type||'-'} |
${s.location||'-'} |
${s.uptime||'-'} |
- ${s.load_1==-1?'–':s.load_1?.toFixed(2)} |
+ ${s.load_1==-1?'–':Math.max(0,(s.load_1||0)).toFixed(2)} |
${monthIn} | ${monthOut} |
${netNow} |
${netTotal} |
- ${online?``:'-'} |
- ${online?``:'-'} |
- ${online?``:'-'} |
+ ${online?gaugeHTML('cpu', s.cpu||0):'-'} |
+ ${online?gaugeHTML('mem', memPct):'-'} |
+ ${online?gaugeHTML('hdd', hddPct):'-'} |
${pingBuckets} |
`;
});
@@ -112,8 +112,22 @@ function renderServers(){
});
});
- drawSparks();
+ // 仪表盘无需 drawSparks
}
+// 生成仪表盘 (圆形 conic-gradient)
+function gaugeHTML(type,val){
+ const pct = Math.max(0,Math.min(100,val));
+ const p = (pct/100).toFixed(3);
+ const warnAttr = pct>=90? 'data-bad' : (pct>=50? 'data-warn' : '');
+ return `
+
+
${pct.toFixed(0)}%
+
`;
+}
+function labelOf(t){ return t==='cpu'?'CPU': t==='mem'?'内存':'硬盘'; }
function renderServersCards(){
const wrap = document.getElementById('serversCards');
if(!wrap) return;
@@ -239,7 +253,7 @@ function bindTheme(){
const mql = window.matchMedia('(prefers-color-scheme: light)');
const saved = localStorage.getItem('theme'); // 'light' | 'dark' | null (auto)
- const apply = (isLight)=>{ document.body.classList.toggle('light', isLight); };
+ const apply = (isLight)=>{ document.body.classList.toggle('light', isLight); document.documentElement.classList.toggle('light', isLight); };
if(!saved){
// 自动跟随系统
@@ -287,9 +301,9 @@ function openDetail(i){
- ● 联通
- ● 电信
- ● 移动
+ ● 联通 (${num(s.time_10010)}ms)
+ ● 电信 (${num(s.time_189)}ms)
+ ● 移动 (${num(s.time_10086)}ms)
(~${S.hist[key]?S.hist[key].cu.length:0} 条)
`;
@@ -308,7 +322,6 @@ function openDetail(i){
}
function ioBar(label,bytesVal,role){ const peak=150*1000*1000; const pct=Math.min(100,(bytesVal/peak)*100); const lvl = bytesVal>100*1000*1000?'bad': bytesVal>50*1000*1000?'warn':''; return barHTML(label,pct, (bytes(bytesVal)+'/s'), role, true).replace('位置${s.location||'-'}
TCP/UDP/进/线${procLine}
@@ -356,6 +369,10 @@ function drawLatencyChart(key){
const W = canvas.clientWidth; const H = canvas.height; canvas.width = W; // 适配宽度
ctx.clearRect(0,0,W,H);
const padL=40, padR=10, padT=10, padB=18;
+ const isLight = document.body.classList.contains('light');
+ const axisColor = isLight? 'rgba(0,0,0,0.22)' : 'rgba(255,255,255,0.18)';
+ const gridColor = isLight? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.10)';
+ const textColor = isLight? 'var(--text-dim)' : 'rgba(226,232,240,0.85)';
const series = [ {arr:data.cu,color:'#3b82f6'}, {arr:data.ct,color:'#10b981'}, {arr:data.cm,color:'#f59e0b'} ];
const allVals = series.flatMap(s=>s.arr);
if(!allVals.length){ ctx.fillStyle='var(--text-dim)'; ctx.font='12px system-ui'; ctx.fillText('暂无数据', W/2-30, H/2); return; }
@@ -364,11 +381,11 @@ function drawLatencyChart(key){
const range = Math.max(1, max-min);
const n = Math.max(...series.map(s=>s.arr.length));
const xStep = (W - padL - padR) / Math.max(1,n-1);
- // 网格与轴
- ctx.strokeStyle='rgba(255,255,255,0.08)'; ctx.lineWidth=1;
+ // 网格与轴 (增强暗色对比)
+ ctx.strokeStyle=axisColor; ctx.lineWidth=1.1;
ctx.beginPath(); ctx.moveTo(padL,padT); ctx.lineTo(padL,H-padB); ctx.lineTo(W-padR,H-padB); ctx.stroke();
- ctx.fillStyle='var(--text-dim)'; ctx.font='10px system-ui';
- const yMarks=4; for(let i=0;i<=yMarks;i++){ const y = padT + (H-padT-padB)*i/yMarks; const val = (max - range*i/yMarks).toFixed(0)+'ms'; ctx.fillText(val,4,y+3); ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke(); }
+ ctx.fillStyle=textColor; ctx.font='10px system-ui';
+ const yMarks=4; for(let i=0;i<=yMarks;i++){ const y = padT + (H-padT-padB)*i/yMarks; const val = (max - range*i/yMarks).toFixed(0)+'ms'; ctx.fillText(val,4,y+3); ctx.strokeStyle=gridColor; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke(); }
// 绘制线
series.forEach(s=>{
if(s.arr.length<2) return;
@@ -429,10 +446,14 @@ function drawLoadChart(key){
const padL=38,padR=8,padT=8,padB=16;
const max=Math.max(...all); const min=Math.min(...all); const range=Math.max(0.5,max-min);
const n = Math.max(l1.length,l5.length,l15.length); const xStep=(W-padL-padR)/Math.max(1,n-1);
- // 轴 & 网格
- ctx.strokeStyle='rgba(255,255,255,0.08)'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(padL,padT); ctx.lineTo(padL,H-padB); ctx.lineTo(W-padR,H-padB); ctx.stroke();
- ctx.fillStyle='var(--text-dim)'; ctx.font='10px system-ui';
- const yMarks=4; for(let i=0;i<=yMarks;i++){ const y=padT+(H-padT-padB)*i/yMarks; const val=(max - range*i/yMarks).toFixed(2); ctx.fillText(val,4,y+3); ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke(); }
+ const isLight = document.body.classList.contains('light');
+ const axisColor = isLight? 'rgba(0,0,0,0.22)' : 'rgba(255,255,255,0.18)';
+ const gridColor = isLight? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.10)';
+ const textColor = isLight? 'var(--text-dim)' : 'rgba(226,232,240,0.85)';
+ // 轴 & 网格 (增强暗色对比)
+ ctx.strokeStyle=axisColor; ctx.lineWidth=1.1; ctx.beginPath(); ctx.moveTo(padL,padT); ctx.lineTo(padL,H-padB); ctx.lineTo(W-padR,H-padB); ctx.stroke();
+ ctx.fillStyle=textColor; ctx.font='10px system-ui';
+ const yMarks=4; for(let i=0;i<=yMarks;i++){ const y=padT+(H-padT-padB)*i/yMarks; const val=(max - range*i/yMarks).toFixed(2); ctx.fillText(val,4,y+3); ctx.strokeStyle=gridColor; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke(); }
const series=[{arr:l1,color:'#8b5cf6',fill:true},{arr:l5,color:'#10b981'},{arr:l15,color:'#f59e0b'}];
// 面积先画 load1
series.forEach(s=>{
@@ -469,6 +490,10 @@ function updateDetailMetrics(key){
function updIO(role,val){ const wrap=document.querySelector(`#detailContent .bar-wrap[data-role="${role}"]`); if(!wrap) return; const peak=150*1000*1000; const pct=Math.min(100,(val/peak)*100); const bar=wrap.querySelector('.bar span'); if(bar) bar.style.setProperty('--p',(pct/100).toFixed(3)); const lbl=wrap.querySelector('.bar-label span:last-child'); if(lbl) lbl.textContent= bytes(val)+'/s'; const box=wrap.querySelector('.bar'); if(box){ box.removeAttribute('data-warn'); box.removeAttribute('data-bad'); if(val>100*1000*1000) box.setAttribute('data-bad',''); else if(val>50*1000*1000) box.setAttribute('data-warn',''); } }
updIO('io-read', ioRead);
updIO('io-write', ioWrite);
+ // 延迟动态刷新 (若存在)
+ const cuEl=document.getElementById('lat-cu'); if(cuEl) cuEl.textContent = num(s.time_10010)+'ms';
+ const ctEl=document.getElementById('lat-ct'); if(ctEl) ctEl.textContent = num(s.time_189)+'ms';
+ const cmEl=document.getElementById('lat-cm'); if(cmEl) cmEl.textContent = num(s.time_10086)+'ms';
}
function startDetailAutoUpdate(){ stopDetailAutoUpdate(); S._detailTimer = setInterval(()=>{ if(S._openDetailKey) updateDetailMetrics(S._openDetailKey); }, 1000); }
function stopDetailAutoUpdate(){ if(S._detailTimer){ clearInterval(S._detailTimer); S._detailTimer=null; } }