// 简洁现代前端 - 仅使用原生 JS const S = { updated:0, servers:[], ssl:[], error:false, hist:{}, metricHist:{}, loadHist:{} };// hist latency; metricHist: {key:{cpu:[],mem:[],hdd:[]}}; loadHist: {key:[]} const els = { notice: ()=>document.getElementById('notice'), last: ()=>document.getElementById('lastUpdate'), serversBody: ()=>document.getElementById('serversBody'), monitorsBody: ()=>document.getElementById('monitorsBody'), sslBody: ()=>document.getElementById('sslBody') }; function bytes(v){ if(v===0) return '0B'; if(!v) return '-'; const k=1000; const u=['B','KB','MB','GB','TB','PB']; const i=Math.floor(Math.log(v)/Math.log(k)); return (v/Math.pow(k,i)).toFixed(i?1:0)+u[i]; } function pct(v){ return (v||0).toFixed(0)+'%'; } function clsBy(v){ return v>=90?'danger':v>=80?'warn':'ok'; } function humanAgo(ts){ if(!ts) return '-'; const s=Math.floor((Date.now()/1000 - ts)); const m=Math.floor(s/60); return m>0? m+' 分钟前':'几秒前'; } function num(v){ return (typeof v==='number' && !isNaN(v)) ? v : '-'; } async function fetchData(){ try { const r = await fetch('json/stats.json?_='+Date.now()); if(!r.ok) throw new Error(r.status); const j = await r.json(); if(j.reload) location.reload(); S.updated = j.updated; S.servers = j.servers||[]; S.ssl = j.sslcerts||[]; S.error=false; // 更新延迟历史 (按节点名聚合) S.servers.forEach(s=>{ const key = s.name || s.location || 'node'; if(!S.hist[key]) S.hist[key] = {cu:[],ct:[],cm:[]}; const H = S.hist[key]; // 使用 time_ 字段 (ms) 若不存在则跳过 if(typeof s.time_10010 === 'number') H.cu.push(s.time_10010); if(typeof s.time_189 === 'number') H.ct.push(s.time_189); if(typeof s.time_10086 === 'number') H.cm.push(s.time_10086); const MAX=120; // 保留约 120*4s ≈ 8 分钟 ['cu','ct','cm'].forEach(k=>{ if(H[k].length>MAX) H[k].splice(0,H[k].length-MAX); }); // 指标历史 (仅在线时记录) if(!S.metricHist[key]) S.metricHist[key] = {cpu:[],mem:[],hdd:[]}; const MH = S.metricHist[key]; if(s.online4||s.online6){ 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; MH.cpu.push(s.cpu||0); MH.mem.push(memPct||0); MH.hdd.push(hddPct||0); const MAXM=120; ['cpu','mem','hdd'].forEach(k=>{ if(MH[k].length>MAXM) MH[k].splice(0,MH[k].length-MAXM); }); } // 负载历史 (记录 load_1 / load_5 / load_15) if(!S.loadHist[key]) S.loadHist[key] = {l1:[],l5:[],l15:[]}; const LH = S.loadHist[key]; const pushLoad = (arr,val)=>{ if(typeof val === 'number' && val >= 0){ arr.push(val); if(arr.length>120) arr.splice(0,arr.length-120); } }; pushLoad(LH.l1, s.load_1); pushLoad(LH.l5, s.load_5); pushLoad(LH.l15, s.load_15); }); render(); }catch(e){ S.error=true; els.notice().textContent = '数据获取失败'; console.error(e); } } function render(){ els.notice().style.display='none'; renderServers(); renderServersCards(); renderMonitors(); renderMonitorsCards(); renderSSL(); renderSSLCards(); updateTime(); } function renderServers(){ const tbody = els.serversBody(); let html=''; S.servers.forEach((s,idx)=>{ const online = s.online4||s.online6; const proto = online ? (s.online4 && s.online6? '双栈': s.online4? 'IPv4':'IPv6') : '离线'; const statusPill = online ? `${proto}` : `${proto}`; 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 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 `
`; } const pingBuckets = `
${bucket(p1)}${bucket(p2)}${bucket(p3)}
`; const key = s.name || s.location || 'node'; const rowCursor = online? 'pointer':'default'; const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 ); html += ` ${statusPill} ${s.name||'-'} ${s.type||'-'} ${s.location||'-'} ${s.uptime||'-'} ${s.load_1==-1?'–':Math.max(0,(s.load_1||0)).toFixed(2)} ${monthIn} | ${monthOut} ${netNow} ${netTotal} ${online?gaugeHTML('cpu', s.cpu||0):'-'} ${online?gaugeHTML('mem', memPct):'-'} ${online?gaugeHTML('hdd', hddPct):'-'} ${pingBuckets} `; }); tbody.innerHTML = html || `无数据`; // 绑定行点击 tbody.querySelectorAll('tr.row-server').forEach(tr=>{ tr.addEventListener('click',()=>{ if(tr.getAttribute('data-online')!=='1') return; // 离线不弹出 const i = parseInt(tr.getAttribute('data-idx')); openDetail(i); }); }); // 仪表盘无需 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; // 仅在窄屏时显示 (和 CSS 一致判断, 可稍放宽避免闪烁) if(window.innerWidth>700){ wrap.innerHTML=''; return; } let html=''; S.servers.forEach((s,idx)=>{ const online = s.online4||s.online6; const proto = online ? (s.online4 && s.online6? '双栈': s.online4? 'IPv4':'IPv6') : '离线'; const pill = `${proto}`; 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); 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 `
`; } const buckets = `
${bucket(p1)}${bucket(p2)}${bucket(p3)}
`; const key = s.name || s.location || 'node'; const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 ); html += `
\n \n
\n
${s.name||'-'} ${s.location||'-'}
\n ${pill}\n
\n
\n
负载${s.load_1==-1?'–':s.load_1?.toFixed(2)}
\n
在线${s.uptime||'-'}
\n
月流量${monthIn}/${monthOut}
\n
网络${netNow}
\n
总流量${netTotal}
\n
CPU${s.cpu||0}%
\n
内存${memPct.toFixed(0)}%
\n
硬盘${hddPct.toFixed(0)}%
\n
\n ${buckets}\n
\n
${online?'点击卡片可查看详情':'离线,不可查看详情'}
\n
\n
`; }); wrap.innerHTML = html || '
无数据
'; wrap.querySelectorAll('.card').forEach(card=>{ const idx = parseInt(card.getAttribute('data-idx')); card.addEventListener('click', e=>{ if(e.target.classList.contains('expand-btn')){ card.classList.toggle('expanded'); e.stopPropagation(); return;} if(card.getAttribute('data-online')!=='1') return; // 离线不弹 openDetail(idx); }); }); } function renderMonitors(){ const tbody = els.monitorsBody(); let html=''; S.servers.forEach(s=>{ html += ` ${(s.online4||s.online6)?'在线':'离线'} ${s.name||'-'} ${s.location||'-'} ${s.custom||'-'} `; }); tbody.innerHTML = html || `无数据`; } // 服务卡片 (移动端) function renderMonitorsCards(){ const wrap = document.getElementById('monitorsCards'); if(!wrap) return; if(window.innerWidth>700){ wrap.innerHTML=''; return; } let html=''; S.servers.forEach(s=>{ const online = (s.online4||s.online6)?'在线':'离线'; const pill = `${online}`; html += `
${s.name||'-'} ${s.location||'-'}
${pill}
监测内容${s.custom||'-'}
协议${online}
`; }); wrap.innerHTML = html || '
无数据
'; } function renderSSL(){ const tbody = els.sslBody(); let html=''; S.ssl.forEach(c=>{ const cls = c.expire_days<=0? 'err': c.expire_days<=7? 'warn':'ok'; const status = c.expire_days<=0? '已过期': c.expire_days<=7? '将到期':'正常'; const dt = c.expire_ts? new Date(c.expire_ts*1000).toISOString().replace('T',' ').replace(/\.\d+Z/,''):'-'; html += ` ${c.name||'-'} ${(c.domain||'').replace(/^https?:\/\//,'')} ${c.port||443} ${c.expire_days??'-'} ${dt} ${status} `; }); tbody.innerHTML = html || `无证书数据`; } // 证书卡片 (移动端) function renderSSLCards(){ const wrap = document.getElementById('sslCards'); if(!wrap) return; if(window.innerWidth>700){ wrap.innerHTML=''; return; } let html=''; S.ssl.forEach(c=>{ const cls = c.expire_days<=0? 'err': c.expire_days<=7? 'warn':'ok'; const status = c.expire_days<=0? '已过期': c.expire_days<=7? '将到期':'正常'; const dt = c.expire_ts? new Date(c.expire_ts*1000).toISOString().replace('T',' ').replace(/\.\d+Z/,''):'-'; html += `
${c.name||'-'}
${status}
域名${(c.domain||'').replace(/^https?:\/\//,'')}
端口${c.port||443}
剩余(天)${c.expire_days??'-'}
到期${dt.split(' ')[0]||dt}
`; }); wrap.innerHTML = html || '
无证书数据
'; } function updateTime(){ const el = els.last(); if(S.updated){ el.textContent = '最后更新: '+ humanAgo(S.updated); } } function bindTabs(){ document.getElementById('navTabs').addEventListener('click',e=>{ if(e.target.tagName!=='BUTTON') return; const tab=e.target.dataset.tab; document.querySelectorAll('.nav button').forEach(b=>b.classList.toggle('active',b===e.target)); document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active', p.id==='panel-'+tab)); }); } function bindTheme(){ const btn = document.getElementById('themeToggle'); 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); document.documentElement.classList.toggle('light', isLight); }; if(!saved){ // 自动跟随系统 apply(mql.matches); // 监听系统偏好变化(仅在未手动选择时) mql.addEventListener('change', e=>{ if(!localStorage.getItem('theme')) apply(e.matches); }); } else { apply(saved==='light'); } btn.addEventListener('click',()=>{ // 用户手动切换后即固定,不再自动 const toLight = !document.body.classList.contains('light'); apply(toLight); localStorage.setItem('theme', toLight?'light':'dark'); }); } bindTabs(); bindTheme(); fetchData(); setInterval(fetchData, 1000); setInterval(updateTime, 60000); // 详情弹窗逻辑 function openDetail(i){ const s = S.servers[i]; if(!s) return; const box = document.getElementById('detailContent'); const modal = document.getElementById('detailModal'); document.getElementById('detailTitle').textContent = s.name + ' 详情'; const offline = !(s.online4||s.online6); const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0; const swapPct = s.swap_total? (s.swap_used/s.swap_total*100):0; const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0; const ioRead = (typeof s.io_read==='number')? s.io_read:0; const ioWrite = (typeof s.io_write==='number')? s.io_write:0; const procLine = `${num(s.tcp_count)} / ${num(s.udp_count)} / ${num(s.process_count)} / ${num(s.thread_count)}`; // 保留延迟数据用于图表,但不再展示当前延迟文字行 const latText = offline ? '离线' : `CU/CT/CM: ${num(s.time_10010)}ms (${(s.ping_10010||0).toFixed(0)}%) / ${num(s.time_189)}ms (${(s.ping_189||0).toFixed(0)}%) / ${num(s.time_10086)}ms (${(s.ping_10086||0).toFixed(0)}%)`; const key = s.name || s.location || 'node'; let latencyBlock = ''; if(!offline){ latencyBlock = `
● 联通 (${num(s.time_10010)}ms) ● 电信 (${num(s.time_189)}ms) ● 移动 (${num(s.time_10086)}ms) (~${S.hist[key]?S.hist[key].cu.length:0} 条)
`; } else { latencyBlock = `
离线,无联通/电信/移动延迟数据
`; } function barHTML(label,valPct,text,role,io){ const lvl = valPct>=90?'bad':(valPct>=80?'warn':''); const ioCls = io? ' io':''; return `
${label}${text}
`; } 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('
TCP/UDP/进/线${procLine}
● load1 ● load5 ● load15 (~${(S.loadHist[key]?S.loadHist[key].l1.length:0)} 条)
${offline?'
内存-
':barHTML('内存', memPct, memPct.toFixed(1)+'%','mem')} ${offline?'
虚存-
':barHTML('虚存', swapPct, s.swap_total? swapPct.toFixed(1)+'%':'-','swap')}
${offline?'
硬盘-
':barHTML('硬盘', hddPct, hddPct.toFixed(1)+'%','disk')} ${offline?'
读速-
':ioBar('读速', ioRead,'io-read')} ${offline?'
写速-
':ioBar('写速', ioWrite,'io-write')}
${latencyBlock} `; modal.style.display='flex'; document.addEventListener('keydown', escCloseOnce); if(!offline){ drawLatencyChart(key); drawLoadChart(key); S._openDetailKey = key; // 记录当前弹窗对应节点 startDetailAutoUpdate(); } else { S._openDetailKey = null; stopDetailAutoUpdate(); } } function escCloseOnce(e){ if(e.key==='Escape'){ closeDetail(); } } function closeDetail(){ const m=document.getElementById('detailModal'); m.style.display='none'; document.removeEventListener('keydown', escCloseOnce); stopDetailAutoUpdate(); } document.getElementById('detailClose').addEventListener('click', closeDetail); document.getElementById('detailModal').addEventListener('click', e=>{ if(e.target.id==='detailModal') closeDetail(); }); // 绘制三网延迟折线图 (简易实现) function drawLatencyChart(key){ const data = S.hist[key]; const canvas = document.getElementById('latChart'); if(!canvas || !data) return; const ctx = canvas.getContext('2d'); 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; } const max = Math.max(...allVals); const min = Math.min(...allVals); 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=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(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; ctx.strokeStyle = s.color; ctx.lineWidth=1.6; ctx.beginPath(); s.arr.forEach((v,i)=>{ const x = padL + xStep*i; const y = padT + (H-padT-padB)*(1-(v-min)/range); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }); ctx.stroke(); }); } // 在每次 render 后若弹窗打开则重绘最新图 const _oldRender = render; render = function(){ _oldRender(); if(S._openDetailKey){ drawLatencyChart(S._openDetailKey); drawLoadChart(S._openDetailKey); } }; window.addEventListener('resize', ()=>{ if(S._openDetailKey){ drawLatencyChart(S._openDetailKey); drawLoadChart(S._openDetailKey); } renderServersCards(); renderMonitorsCards(); renderSSLCards(); }); // 绘制小型折线 (sparklines) function drawSparks(){ const els = document.querySelectorAll('.spark'); els.forEach(div=>{ // 若已有canvas跳过重建 let canvas = div.querySelector('canvas'); if(!canvas){ canvas = document.createElement('canvas'); div.appendChild(canvas); } const key = div.getAttribute('data-key'); const metric = div.getAttribute('data-metric'); const hist = (S.metricHist[key] && S.metricHist[key][metric])? S.metricHist[key][metric]:[]; const W = 80, H = 26; canvas.width=W; canvas.height=H; const ctx=canvas.getContext('2d'); ctx.clearRect(0,0,W,H); div.classList.add('spark-ready'); if(hist.length<2){ ctx.fillStyle='var(--text-dim)'; ctx.font='10px system-ui'; ctx.fillText('-', W/2-3, H/2+3); return; } // 硬盘与 CPU/内存保持一致的折线显示(去掉低波动迷你条特殊样式) const max = Math.max(...hist); const min = Math.min(...hist); const range = Math.max(1,max-min); const step = W/(hist.length-1); // 线颜色 let color = '#3b82f6'; if(metric==='mem') color='#10b981'; else if(metric==='hdd') color='#f59e0b'; ctx.strokeStyle=color; ctx.lineWidth=1.3; ctx.beginPath(); hist.forEach((v,i)=>{ const x=i*step; const y=H - ( (v-min)/range )* (H-4) -2; if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }); ctx.stroke(); // 当前值点 const last = hist[hist.length-1]; const lx = W-1; const ly = H - ((last-min)/range)*(H-4)-2; ctx.fillStyle=color; ctx.beginPath(); ctx.arc(lx,ly,2,0,Math.PI*2); ctx.fill(); }); } // 负载折线图 (load1 历史) function drawLoadChart(key){ const L = S.loadHist[key]; const canvas = document.getElementById('loadChart'); if(!canvas) return; const ctx = canvas.getContext('2d'); if(!L){ ctx.clearRect(0,0,canvas.width,canvas.height); return; } const l1=L.l1||[], l5=L.l5||[], l15=L.l15||[]; const canvasW = canvas.clientWidth; const H = canvas.height; canvas.width = canvasW; const W=canvasW; ctx.clearRect(0,0,W,H); if(l1.length<2){ ctx.fillStyle='var(--text-dim)'; ctx.font='12px system-ui'; ctx.fillText('暂无负载数据', W/2-42, H/2); return; } const all = [...l1,...l5,...l15]; 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); 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=>{ if(s.arr.length<2) return; ctx.beginPath(); ctx.lineWidth=1.5; ctx.strokeStyle=s.color; s.arr.forEach((v,i)=>{ const x=padL+xStep*i; const y=padT+(H-padT-padB)*(1-(v-min)/range); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }); ctx.stroke(); if(s.fill){ const lastX = padL + xStep*(s.arr.length-1); ctx.lineTo(lastX,H-padB); ctx.lineTo(padL,H-padB); ctx.closePath(); const grd = ctx.createLinearGradient(0,padT,0,H-padB); grd.addColorStop(0,'rgba(139,92,246,0.25)'); grd.addColorStop(1,'rgba(139,92,246,0)'); ctx.fillStyle=grd; ctx.fill(); } }); } //# sourceMappingURL=app.js.map // ====== 详情动态刷新 ====== function findServerByKey(key){ return S.servers.find(x=> (x.name||x.location||'node')===key); } function updateDetailMetrics(key){ const s = findServerByKey(key); if(!s) return; if(!(s.online4||s.online6)) return; // 离线不更新 const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0; const swapPct = s.swap_total? (s.swap_used/s.swap_total*100):0; const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0; const ioRead = (typeof s.io_read==='number')? s.io_read:0; const ioWrite = (typeof s.io_write==='number')? s.io_write:0; 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); // 延迟动态刷新 (若存在) 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; } }