// 简洁现代前端 - 仅使用原生 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') }; // (清理) 已移除 bytes / humanAuto 等未使用的通用进位函数 // 最小单位 MB: function humanMinMBFromKB(kb){ if(kb==null||isNaN(kb)) return '-'; // 输入单位: KB let mb = kb/1000; const units=['MB','GB','TB','PB']; let i=0; while(mb>=1000 && i=100? mb.toFixed(0): mb.toFixed(1); return out+units[i]; } function humanMinMBFromMB(mbVal){ if(mbVal==null||isNaN(mbVal)) return '-'; // 输入单位: MB let v=mbVal; const units=['MB','GB','TB','PB']; let i=0; while(v>=1000 && i=100? v.toFixed(0): v.toFixed(1); return out+units[i]; } function humanMinMBFromB(bytes){ if(bytes==null||isNaN(bytes)) return '-'; // 输入单位: B let mb = bytes/1000/1000; const units=['MB','GB','TB','PB']; let i=0; while(mb>=1000 && i=100? mb.toFixed(0): mb.toFixed(1); return out+units[i]; } function humanRateMinMBFromB(bytes){ if(bytes==null||isNaN(bytes)) return '-'; if(bytes<=0) return '0.0MB'; return humanMinMBFromB(bytes); } function humanMinKBFromB(bytes){ if(bytes==null||isNaN(bytes)) return '-'; // 输入单位: B; 最小单位 KB let kb = bytes/1000; const units=['KB','MB','GB','TB','PB']; let i=0; while(kb>=1000 && i=100? kb.toFixed(0): kb.toFixed(1); return out+units[i]; } // (清理) pct / clsBy 已不再使用 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 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 monthInBytes = (s.network_in - s.last_network_in) || 0; // 原始: B const monthOutBytes = (s.network_out - s.last_network_out) || 0; const monthIn = humanMinMBFromB(monthInBytes); // 最小单位 MB const monthOut = humanMinMBFromB(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 = humanMinKBFromB(s.network_rx) + ' | ' + humanMinKBFromB(s.network_tx); // 最小单位 KB const netTotal = humanMinMBFromB(s.network_in)+' | '+humanMinMBFromB(s.network_out); // 最小单位 MB 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} ${monthIn}${monthOut} ${s.name||'-'} ${s.type||'-'} ${s.location||'-'} ${s.uptime||'-'} ${s.load_1==-1?'–':Math.max(0,(s.load_1||0)).toFixed(2)} ${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); }); }); // 仪表盘无需历史 spark 小图 } // 生成仪表盘 (圆形 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; // 月流量(移动端)并应用 500GB 阈值配色逻辑 const monthInBytes = (s.network_in - s.last_network_in) || 0; // B const monthOutBytes = (s.network_out - s.last_network_out) || 0; const monthIn = humanMinMBFromB(monthInBytes); const monthOut = humanMinMBFromB(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 = humanMinKBFromB(s.network_rx)+' | '+humanMinKBFromB(s.network_tx); const netTotal = humanMinMBFromB(s.network_in)+' | '+humanMinMBFromB(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 = `
离线,无联通/电信/移动延迟数据
`; } // 旧进度条函数 barHTML/ioBar 已弃用 // 资源行(移除百分比显示,仅显示 已用 / 总量) // 资源行(单独拆分 span 便于后续动态刷新) // 单位来源:memory/swap: KB; hdd: MB; io: B (速率) -> 统一最小单位显示为 MB,并向上进位 (MB/GB/TB) const memUsed = s.memory_total!=null? humanMinMBFromKB(s.memory_used||0):'-'; const memTotal = s.memory_total!=null? humanMinMBFromKB(s.memory_total):'-'; const swapUsed = s.swap_total!=null? humanMinMBFromKB(s.swap_used||0):'-'; const swapTotal = s.swap_total!=null? humanMinMBFromKB(s.swap_total):'-'; const hddUsed = s.hdd_total!=null? humanMinMBFromMB(s.hdd_used||0):'-'; const hddTotal = s.hdd_total!=null? humanMinMBFromMB(s.hdd_total):'-'; const ioReadLine = (ioRead!=null)? humanRateMinMBFromB(ioRead):'-'; const ioWriteLine = (ioWrite!=null)? humanRateMinMBFromB(ioWrite):'-'; const memColor = memPct>80? ' style="color:var(--danger)"':''; // 已用/总量显示为单一块,所以对已用着色 const swapColor = swapPct>80? ' style="color:var(--danger)"':''; const hddColor = hddPct>80? ' style="color:var(--danger)"':''; // IO 阈值:>100MB (原始单位 B) -> >100*1000*1000 B const readColor = ioRead>100*1000*1000? ' style="color:var(--danger)"':''; const writeColor = ioWrite>100*1000*1000? ' style="color:var(--danger)"':''; box.innerHTML = `
TCP/UDP/进/线${procLine}
内存 / 虚存 ${memUsed} / ${memTotal} | ${swapUsed} / ${swapTotal}
硬盘 / 读写 ${hddUsed} / ${hddTotal} | ${ioReadLine} / ${ioWriteLine}
● load1 ● load5 ● load15 (~${(S.loadHist[key]?S.loadHist[key].l1.length:0)} 条)
${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(); }); // (清理) drawSparks 已移除 // 负载折线图 (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; // 修正:纵轴下限不小于 0,且当真实 range <0.5 时向上扩展 max 而不是向下产生负刻度 const rawMax = all.length? Math.max(...all):0; const rawMin = all.length? Math.min(...all):0; const min = 0; // 我们只显示 >=0 let max = Math.max(rawMax,0); let range = max - min; const MIN_RANGE = 0.5; if(range < MIN_RANGE){ max = MIN_RANGE; range = MIN_RANGE; } 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); // top -> bottom const labelVal = (Math.abs(val) < 0.005 ? 0 : val).toFixed(2); ctx.fillText(labelVal,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 vClamped = Math.max(0, v); const x=padL+xStep*i; const y=padT+(H-padT-padB)*(1-(vClamped-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 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; 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'; // 延迟动态刷新 (若存在) const cuE1=document.getElementById('lat-cu'); if(cuE1) cuE1.textContent = num(s.time_10010)+'ms'; const ctE1=document.getElementById('lat-ct'); if(ctE1) ctE1.textContent = num(s.time_189)+'ms'; const cmE1=document.getElementById('lat-cm'); if(cmE1) cmE1.textContent = num(s.time_10086)+'ms'; // 资源动态刷新 const memLineEl = document.getElementById('mem-line'); if(memLineEl){ const pct = s.memory_total? (s.memory_used/s.memory_total*100):0; document.getElementById('mem-used').textContent = s.memory_total!=null? humanMinMBFromKB(s.memory_used||0):'-'; document.getElementById('mem-total').textContent = s.memory_total!=null? humanMinMBFromKB(s.memory_total):'-'; if(pct>80) memLineEl.style.color='var(--danger)'; else memLineEl.style.color=''; } const swapLineEl = document.getElementById('swap-line'); if(swapLineEl){ const pct = s.swap_total? (s.swap_used/s.swap_total*100):0; document.getElementById('swap-used').textContent = s.swap_total!=null? humanMinMBFromKB(s.swap_used||0):'-'; document.getElementById('swap-total').textContent = s.swap_total!=null? humanMinMBFromKB(s.swap_total):'-'; if(pct>80) swapLineEl.style.color='var(--danger)'; else swapLineEl.style.color=''; } const diskLineEl = document.getElementById('disk-line'); if(diskLineEl){ const pct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0; document.getElementById('hdd-used').textContent = s.hdd_total!=null? humanMinMBFromMB(s.hdd_used||0):'-'; document.getElementById('hdd-total').textContent = s.hdd_total!=null? humanMinMBFromMB(s.hdd_total):'-'; if(pct>80) diskLineEl.style.color='var(--danger)'; else diskLineEl.style.color=''; } const ioReadEl = document.getElementById('io-read'); if(ioReadEl){ const v = (typeof s.io_read==='number')? s.io_read:0; ioReadEl.textContent = humanRateMinMBFromB(v); ioReadEl.style.color = v>100*1000*1000? 'var(--danger)':''; } const ioWriteEl = document.getElementById('io-write'); if(ioWriteEl){ const v = (typeof s.io_write==='number')? s.io_write:0; ioWriteEl.textContent = humanRateMinMBFromB(v); ioWriteEl.style.color = v>100*1000*1000? 'var(--danger)':''; } } function startDetailAutoUpdate(){ stopDetailAutoUpdate(); S._detailTimer = setInterval(()=>{ if(S._openDetailKey) updateDetailMetrics(S._openDetailKey); }, 1000); } function stopDetailAutoUpdate(){ if(S._detailTimer){ clearInterval(S._detailTimer); S._detailTimer=null; } }