fix some bug

master
cppla 2025-08-15 13:33:39 +08:00
parent 3c8ebcf710
commit 73b3d16ff6
3 changed files with 96 additions and 28 deletions

View File

@ -25,18 +25,65 @@ table.data thead th{position:sticky;top:0;background:var(--bg-alt);font-weight:5
table.data tbody td{padding:.55rem .75rem;border-bottom:1px solid var(--border);font-size:13px;vertical-align:middle;white-space:nowrap}
/* 防止数值变化导致列抖动:为月流量(7)/当前网络(8)/总流量(9)设置固定宽度并使用等宽数字 */
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),
/* 月流量(2) 左对齐;当前网络(8)/总流量(9) 居中 */
table.data thead th:nth-child(2),table.data tbody td:nth-child(2){
/* 月流量列:向左贴近协议(减小左 padding同时加大右 padding 拉开与节点距离 */
width:128px;min-width:128px;max-width:128px;font-variant-numeric:tabular-nums;letter-spacing:.3px;text-align:center;padding:0 1.05rem 0 .15rem;
}
/* 节点列加宽,避免被月流量胶囊视觉挤压 */
table.data thead th:nth-child(3),table.data tbody td:nth-child(3){
width:160px;min-width:160px;max-width:160px;
}
/* 协议列继续收紧右侧 padding 与固定宽度 */
table.data thead th:nth-child(1),table.data tbody td:nth-child(1){
padding-right:.14rem;width:78px;min-width:78px;max-width:78px; /* 扩大协议列并恢复适度间距 */
}
/* 让双色胶囊更靠近协议列 */
table.data tbody td:nth-child(2) .caps-traffic.duo{margin-left:-6px;} /* 向协议方向微移,视觉更靠近;右侧 padding 增大避免靠近节点 */
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:132px;min-width:132px;max-width:132px;/* 适配示例 111.1GB|222.2GB */
font-variant-numeric:tabular-nums;letter-spacing:.3px;
width:132px;min-width:132px;max-width:132px;font-variant-numeric:tabular-nums;letter-spacing:.3px;text-align:center;
/* 进一步拉开与 CPU/内存/硬盘 组的视觉距离 */
padding-right:1.95rem;
}
/* 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;
width:70px;min-width:70px;max-width:70px;text-align:center;padding-left:1.1rem;padding-right:0; /* 继续右移并贴近右侧列 */
}
/* 月流量胶囊 */
.caps-traffic{display:inline-flex;align-items:center;gap:6px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);padding:3px 12px 3px 10px;border-radius:999px;font-size:12px;line-height:1;font-weight:500;position:relative;box-shadow:0 2px 4px -2px rgba(0,0,0,.35),0 0 0 1px rgba(255,255,255,.03);}
.caps-traffic:before{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(circle at 20% 20%,rgba(255,255,255,.06),transparent 70%);pointer-events:none;}
.caps-traffic .io{display:inline-flex;align-items:center;gap:2px;font-variant-numeric:tabular-nums;letter-spacing:.3px;}
.caps-traffic .io.in{color:var(--ok);}
.caps-traffic .io.out{color:var(--accent);}
.caps-traffic .sep{opacity:.4;font-size:11px;display:none;}
.caps-traffic.sm{padding:2px 8px 2px 7px;font-size:11px;gap:5px;}
.caps-traffic.sm .io{font-size:11px;}
/* 双色胶囊:左红右黄 */
.caps-traffic.duo{background:none;border:0;gap:0;padding:0;box-shadow:none;position:relative;border-radius:999px;overflow:hidden;font-size:12px;}
/* 宽度按内容自适应(不再拉伸占满列),每半边仅为其文本 + padding可容纳最大 111.1MB */
.caps-traffic.duo .half{flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:2px 4px;font-variant-numeric:tabular-nums;font-weight:600;line-height:1.25; /* 与 .pill 保持一致高度 */ letter-spacing:.25px;color:#fff;font-size:12px;white-space:nowrap;}
/*
normal (): (#fff) (accent)
heavy (>=500GB ): (warn) (danger)
*/
/* normal 初始:淡绿色(入) + 淡蓝色(出) */
.caps-traffic.duo.normal .half.in{background:#d1fae5;color:#065f46;} /* emerald-100 / text-emerald-800 */
body.light .caps-traffic.duo.normal .half.in{background:#d1fae5;color:#065f46;}
.caps-traffic.duo.normal .half.out{background:#bfdbfe;color:#1e3a8a;} /* blue-200 / text-blue-900 */
body.light .caps-traffic.duo.normal .half.out{background:#bfdbfe;color:#1e3a8a;}
.caps-traffic.duo.heavy .half.in{background:var(--warn);color:#111;}
body.light .caps-traffic.duo.heavy .half.in{background:var(--warn);color:#111;}
.caps-traffic.duo.heavy .half.out{background:var(--danger);color:#fff;}
body.light .caps-traffic.duo.heavy .half.out{color:#fff;}
/* 半之间分隔线 */
.caps-traffic.duo .half + .half{border-left:1px solid rgba(0,0,0,.18);}
body.light .caps-traffic.duo .half + .half{border-left:1px solid rgba(0,0,0,.08);}
.caps-traffic.duo.sm .half{padding:1px 4px;font-size:10px;min-width:0;}
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)}
@ -66,6 +113,10 @@ table.data tbody tr:hover{background:rgba(255,255,255,.04)}
.kv{display:flex;justify-content:space-between;gap:1rem;padding:.5rem .75rem;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);border-radius:10px}
.kv span{white-space:nowrap}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
/* 资源使用百分比色彩标签 */
/* 回退:移除资源使用百分比彩色标签样式 */
/* 详情弹窗三列信息行 */
/* spark lines */
.spark{width:82px;height:28px;display:flex;align-items:center;justify-content:center}
@ -96,7 +147,7 @@ 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}
.pill{display:inline-block;padding:2px 8px;font-size:12px;font-weight:600;border-radius:999px;letter-spacing:.45px;min-width:48px;text-align:center;line-height:1.25;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}
.pill.on{background:var(--ok)}
.pill.off{background:var(--danger)}
.pill.on:hover{filter:brightness(1.1)}
@ -115,17 +166,19 @@ body.light .gauge-half .needle{background:linear-gradient(var(--text),var(--text
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;
width:150px;min-width:150px;max-width:150px;padding-left:0;padding-right:.55rem; /* 去除左 padding 进一步贴近 */
}
/* 调整“总流量”表头第9列padding 使标题文字居中,不受正文额外右 padding 影响 */
table.data thead th:nth-child(9){padding-left:.75rem;padding-right:.75rem;}
.buckets{justify-content:center}
/* 响应式隐藏非关键列,保持可读性 */
@media (max-width:1100px){
table.data{min-width:100%;}
table.data thead th:nth-child(3),
table.data tbody td:nth-child(3), /* 虚拟化 */
table.data thead th:nth-child(7),
table.data tbody td:nth-child(7), /* 月流量 */
table.data tbody td:nth-child(3), /* 节点 */
table.data thead th:nth-child(4),
table.data tbody td:nth-child(4), /* 虚拟化 */
table.data thead th:nth-child(8),
table.data tbody td:nth-child(8) /* 当前网络 */ {display:none}
}

View File

@ -41,12 +41,12 @@
<thead>
<tr>
<th>协议</th>
<th>月流量 ↓|↑</th>
<th>节点</th>
<th>虚拟化</th>
<th>位置</th>
<th>在线</th>
<th>负载</th>
<th>月流量 ↓|↑</th>
<th>当前网络 ↓|↑</th>
<th>总流量 ↓|↑</th>
<th>CPU</th>

View File

@ -75,9 +75,14 @@ function renderServers(){
const cpuCls = clsBy(s.cpu);
const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0; const memCls = clsBy(memPct);
const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0; const hddCls = clsBy(hddPct);
const monthIn = bytes(s.network_in - s.last_network_in);
const monthOut = bytes(s.network_out - s.last_network_out);
const netNow = bytes(s.network_rx)+' | '+bytes(s.network_tx);
const monthInBytes = (s.network_in - s.last_network_in) || 0;
const monthOutBytes = (s.network_out - s.last_network_out) || 0;
const monthIn = bytes(monthInBytes);
const monthOut = bytes(monthOutBytes);
const HEAVY_THRESHOLD = 500 * 1000 * 1000 * 1000; // 500GB
const heavy = monthInBytes >= HEAVY_THRESHOLD || monthOutBytes >= HEAVY_THRESHOLD;
const trafficCls = heavy ? 'caps-traffic duo heavy' : 'caps-traffic duo normal';
const netNow = bytes(s.network_rx) + ' | ' + bytes(s.network_tx);
const netTotal = bytes(s.network_in)+' | '+bytes(s.network_out);
const p1 = (s.ping_10010||0); const p2 = (s.ping_189||0); const p3 = (s.ping_10086||0);
function bucket(p){ const v = Math.max(0, Math.min(100, p)); const level = v>=20?'bad':(v>=10?'warn':'ok'); return `<div class=\"bucket\" data-lv=\"${level}\"><span style=\"--h:${v}%\"></span><label>${v.toFixed(0)}%</label></div>`; }
@ -87,12 +92,12 @@ function renderServers(){
const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 );
html += `<tr data-idx="${idx}" data-online="${online?1:0}" class="row-server${highLoad?' high-load':''}" style="cursor:${rowCursor};${online?'':'opacity:.65;'}">
<td>${statusPill}</td>
<td><span class="${trafficCls}" title="本月下行 | 上行 (≥500GB 触发红黄)"><span class="half in">${monthIn}</span><span class="half out">${monthOut}</span></span></td>
<td>${s.name||'-'}</td>
<td>${s.type||'-'}</td>
<td>${s.location||'-'}</td>
<td>${s.uptime||'-'}</td>
<td>${s.load_1==-1?'':Math.max(0,(s.load_1||0)).toFixed(2)}</td>
<td>${monthIn} | ${monthOut}</td>
<td>${netNow}</td>
<td>${netTotal}</td>
<td>${online?gaugeHTML('cpu', s.cpu||0):'-'}</td>
@ -140,8 +145,14 @@ function renderServersCards(){
const pill = `<span class="status-pill ${online?'on':'off'}">${proto}</span>`;
const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0;
const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0;
const monthIn = bytes(s.network_in - s.last_network_in);
const monthOut = bytes(s.network_out - s.last_network_out);
// 月流量(移动端)并应用 500GB 阈值配色逻辑
const monthInBytes = (s.network_in - s.last_network_in) || 0;
const monthOutBytes = (s.network_out - s.last_network_out) || 0;
const monthIn = bytes(monthInBytes);
const monthOut = bytes(monthOutBytes);
const HEAVY_THRESHOLD = 500 * 1000 * 1000 * 1000; // 500GB
const heavy = monthInBytes >= HEAVY_THRESHOLD || monthOutBytes >= HEAVY_THRESHOLD;
const trafficCls = heavy ? 'caps-traffic duo heavy sm' : 'caps-traffic duo normal sm';
const netNow = bytes(s.network_rx)+' | '+bytes(s.network_tx);
const netTotal = bytes(s.network_in)+' | '+bytes(s.network_out);
const p1 = (s.ping_10010||0); const p2=(s.ping_189||0); const p3=(s.ping_10086||0);
@ -149,7 +160,7 @@ function renderServersCards(){
const buckets = `<div class=\"buckets\">${bucket(p1)}${bucket(p2)}${bucket(p3)}</div>`;
const key = s.name || s.location || 'node';
const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 );
html += `<div class=\"card${online?'':' offline'}${highLoad?' high-load':''}\" data-idx=\"${idx}\" data-online=\"${online?1:0}\">\n <button class=\"expand-btn\" aria-label=\"展开\">▼</button>\n <div class=\"card-header\">\n <div class=\"card-title\">${s.name||'-'} <span class=\"tag\">${s.location||'-'}</span></div>\n ${pill}\n </div>\n <div class=\"kvlist\">\n <div><span class=\"key\">负载</span><span>${s.load_1==-1?'':s.load_1?.toFixed(2)}</span></div>\n <div><span class=\"key\">在线</span><span>${s.uptime||'-'}</span></div>\n <div><span class=\"key\">月流量</span><span>${monthIn}/${monthOut}</span></div>\n <div><span class=\"key\">网络</span><span>${netNow}</span></div>\n <div><span class=\"key\">总流量</span><span>${netTotal}</span></div>\n <div><span class=\"key\">CPU</span><span>${s.cpu||0}%</span></div>\n <div><span class=\"key\">内存</span><span>${memPct.toFixed(0)}%</span></div>\n <div><span class=\"key\">硬盘</span><span>${hddPct.toFixed(0)}%</span></div>\n </div>\n ${buckets}\n <div class=\"expand-area\">\n <div style=\"font-size:.65rem;opacity:.7;margin-top:.3rem\">${online?'点击卡片可查看详情':'离线,不可查看详情'}</div>\n </div>\n </div>`;
html += `<div class=\"card${online?'':' offline'}${highLoad?' high-load':''}\" data-idx=\"${idx}\" data-online=\"${online?1:0}\">\n <button class=\"expand-btn\" aria-label=\"展开\">▼</button>\n <div class=\"card-header\">\n <div class=\"card-title\">${s.name||'-'} <span class=\"tag\">${s.location||'-'}</span></div>\n ${pill}\n </div>\n <div class=\"kvlist\">\n <div><span class=\"key\">负载</span><span>${s.load_1==-1?'':s.load_1?.toFixed(2)}</span></div>\n <div><span class=\"key\">在线</span><span>${s.uptime||'-'}</span></div>\n <div><span class=\"key\">月流量</span><span><span class=\"${trafficCls}\" title=\"本月下行 | 上行 (≥500GB 触发红黄)\"><span class=\"half in\">${monthIn}</span><span class=\"half out\">${monthOut}</span></span></span></div>\n <div><span class=\"key\">网络</span><span>${netNow}</span></div>\n <div><span class=\"key\">总流量</span><span>${netTotal}</span></div>\n <div><span class=\"key\">CPU</span><span>${s.cpu||0}%</span></div>\n <div><span class=\"key\">内存</span><span>${memPct.toFixed(0)}%</span></div>\n <div><span class=\"key\">硬盘</span><span>${hddPct.toFixed(0)}%</span></div>\n </div>\n ${buckets}\n <div class=\"expand-area\">\n <div style=\"font-size:.65rem;opacity:.7;margin-top:.3rem\">${online?'点击卡片可查看详情':'离线,不可查看详情'}</div>\n </div>\n </div>`;
});
wrap.innerHTML = html || '<div class="muted" style="font-size:.75rem;text-align:center;padding:1rem;">无数据</div>';
wrap.querySelectorAll('.card').forEach(card=>{
@ -321,8 +332,22 @@ function openDetail(i){
return `<div class="bar-wrap" data-role="${role}"><div class="bar-label"><span>${label}</span><span>${text}</span></div><div class="bar${ioCls}" ${lvl?`data-${lvl}`:''}><span style="--p:${(valPct/100).toFixed(3)}"></span></div></div>`;
}
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('<div class="bar io"', `<div class="bar io" ${lvl?`data-${lvl}`:''}`); }
// 资源行(移除百分比显示,仅显示 已用 / 总量)
const memLine = s.memory_total? bytes(s.memory_used)+' / '+bytes(s.memory_total):'-';
const swapLine = s.swap_total? bytes(s.swap_used)+' / '+bytes(s.swap_total):'-';
const diskLine = s.hdd_total? bytes(s.hdd_used)+' / '+bytes(s.hdd_total):'-';
const ioReadLine = ioRead? bytes(ioRead)+'/s':'-';
const ioWriteLine = ioWrite? bytes(ioWrite)+'/s':'-';
// 阈值着色 ( >80% / >100MB/s )
const memColor = memPct>80? ' style="color:var(--danger)"':'';
const swapColor = swapPct>80? ' style="color:var(--danger)"':'';
const hddColor = hddPct>80? ' style="color:var(--danger)"':'';
const readColor = ioRead>100*1000*1000? ' style="color:var(--danger)"':'';
const writeColor = ioWrite>100*1000*1000? ' style="color:var(--danger)"':'';
box.innerHTML = `
<div class="kv"><span>TCP/UDP//线</span><span class="mono" id="detail-proc">${procLine}</span></div>
<div class="kv"><span>内存 / 虚存</span><span class="mono"><span${memColor}>${memLine}</span> | <span${swapColor}>${swapLine}</span></span></div>
<div class="kv"><span>硬盘 / 读写</span><span class="mono"><span${hddColor}>${diskLine}</span> | <span${readColor}>${ioReadLine}</span> / <span${writeColor}>${ioWriteLine}</span></span></div>
<div style="display:flex;flex-direction:column;gap:.35rem;">
<canvas id="loadChart" height="120" style="width:100%;border:1px solid var(--border);border-radius:10px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));"></canvas>
<div class="mono" style="font-size:11px;display:flex;gap:.9rem;flex-wrap:wrap;align-items:center;opacity:.8;">
@ -332,15 +357,7 @@ function openDetail(i){
<span style="opacity:.6">(~${(S.loadHist[key]?S.loadHist[key].l1.length:0)} )</span>
</div>
</div>
<div class="row-bars" style="margin-top:.25rem;">
${offline?'<div class="bar-wrap" data-role="mem"><div class="bar-label"><span>内存</span><span>-</span></div><div class="bar"><span style="--p:0"></span></div></div>':barHTML('内存', memPct, memPct.toFixed(1)+'%','mem')}
${offline?'<div class="bar-wrap" data-role="swap"><div class="bar-label"><span>虚存</span><span>-</span></div><div class="bar"><span style="--p:0"></span></div></div>':barHTML('虚存', swapPct, s.swap_total? swapPct.toFixed(1)+'%':'-','swap')}
</div>
<div class="row-bars" style="margin-top:.4rem;">
${offline?'<div class="bar-wrap" data-role="disk"><div class="bar-label"><span>硬盘</span><span>-</span></div><div class="bar"><span style="--p:0"></span></div></div>':barHTML('硬盘', hddPct, hddPct.toFixed(1)+'%','disk')}
${offline?'<div class="bar-wrap" data-role="io-read"><div class="bar-label"><span>读速</span><span>-</span></div><div class="bar io"><span style="--p:0"></span></div></div>':ioBar('读速', ioRead,'io-read')}
${offline?'<div class="bar-wrap" data-role="io-write"><div class="bar-label"><span>写速</span><span>-</span></div><div class="bar io"><span style="--p:0"></span></div></div>':ioBar('写速', ioWrite,'io-write')}
</div>
<!-- 进度条移除//虚存以文本形式显示于上方合并行 -->
${latencyBlock}
`;
modal.style.display='flex';
@ -484,9 +501,7 @@ function updateDetailMetrics(key){
const procLine = `${num(s.tcp_count)} / ${num(s.udp_count)} / ${num(s.process_count)} / ${num(s.thread_count)}`;
const procEl = document.getElementById('detail-proc'); if(procEl) procEl.textContent = procLine;
function upd(role,pct,text){ const wrap=document.querySelector(`#detailContent .bar-wrap[data-role="${role}"]`); if(!wrap) return; const valSpan=wrap.querySelector('.bar-label span:last-child'); if(valSpan) valSpan.textContent=text; const bar=wrap.querySelector('.bar span'); if(bar) bar.style.setProperty('--p',(pct/100).toFixed(3)); const box=wrap.querySelector('.bar'); if(box){ box.removeAttribute('data-warn'); box.removeAttribute('data-bad'); if(pct>=90) box.setAttribute('data-bad',''); else if(pct>=80) box.setAttribute('data-warn',''); } }
upd('mem', memPct, memPct.toFixed(1)+'%');
if(document.querySelector('.bar-wrap[data-role="swap"]')) upd('swap', swapPct, s.swap_total? swapPct.toFixed(1)+'%':'-');
upd('disk', hddPct, hddPct.toFixed(1)+'%');
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);