下载python
https://www.python.org/ftp/python/3.12.5/python-3.12.5-amd64.exe
指定安装路径
.\python.exe /quiet InstallAllUsers=1 PrependPath=1 Include_pip=1 TargetDir="C:\Python312"
安装plances
pip install glances==3.* psutil bottle
启动
glances -w
必应时安装VC++运行库
https://aka.ms/vs/17/release/vc_redist.x64.exe
页脚注入
<!--
模块名称:底部服务器资源监控组件
功能概述:
1. 周期性从 Glances API 获取 CPU / 内存 / 网络实时使用数据
2. 响应式显示(PC 与移动端不同布局;移动端只显示首个网卡并允许横向滚动)
3. 状态指示灯基于 CPU 与内存使用率阈值显示健康状况(正常/警告/严重),并在数据陈旧时加发光
4. 失败容错:网络失败后继续展示上次成功数据,标记“延迟”;连续多次失败显示“错误”
5. DOM 更新做差异控制,减少不必要重绘
使用说明:
直接嵌入页面底部;如跨域请为 MONITOR_BASE 部署反向代理。
-->
<style>
:root{
--monitor-font-size:12px; /* 基础字体大小(PC) */
--monitor-line-height:1.3; /* 指标行高 */
}
@media (max-width:600px){
:root{ --monitor-font-size:11px; } /* 中屏字体缩小 */
}
@media (max-width:360px){
:root{ --monitor-font-size:10px; } /* 小屏再缩小 */
}
/* 第一行指标容器(PC 默认用 2 行截断策略) */
footer .monitor-metrics-wrap{
font:var(--monitor-font-size) monospace;
line-height:var(--monitor-line-height);
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:2; /* 最多显示 2 行(移动端会覆盖) */
overflow:hidden;
word-break:break-all;
white-space:normal;
max-width:100%;
color:#555;
margin-bottom:14px;
}
/* 状态指示灯(颜色通过附加类控制) */
.monitor-status-dot{
width:10px;height:10px;border-radius:50%;
background:#aaa;
display:inline-block;
position:relative;top:1px;
margin-right:6px;
flex:0 0 10px; /* 在 flex 中固定宽度避免被压缩 */
}
.monitor-status-ok{background:#2ecc71!important;} /* 正常 */
.monitor-status-warn{background:#f1c40f!important;} /* 警告 */
.monitor-status-bad{background:#e74c3c!important;} /* 严重 */
.monitor-status-stale{box-shadow:0 0 4px 1px #ff9800;} /* 数据陈旧高亮边缘 */
/* 徽章(显示“延迟”或“错误”) */
.monitor-badge{
background:#eee;color:#666;
font-size:10px;
padding:0 4px;
border-radius:3px;
margin-right:6px;
display:inline-block;
position:relative;top:-1px;
flex:0 0 auto;
}
/* 第二行:标题与时间 */
.monitor-footer-line{
display:flex;
justify-content:space-between;
align-items:center;
flex-wrap:wrap;
gap:6px;
line-height:1.2;
font:10px monospace;
color:#888;
}
@media (max-width:600px){
.monitor-footer-line{font-size:9.5px;}
}
@media (max-width:360px){
.monitor-footer-line{font-size:9px;}
}
/* 分隔符与网络项基础样式 */
.mm-sep{margin:0 4px;color:#888;flex:0 0 auto;}
.mm-net-item{white-space:nowrap;}
/* 移动端:改为单行横向滚动,防止挤掉状态灯 */
@media (max-width:600px){
footer .monitor-metrics-wrap{
display:flex;
-webkit-line-clamp:unset; /* 取消多行截断 */
-webkit-box-orient:unset;
white-space:nowrap;
overflow-x:auto;
overflow-y:hidden;
align-items:center;
gap:6px;
word-break:normal;
scrollbar-width:none; /* Firefox 隐藏横向滚动条 */
}
footer .monitor-metrics-wrap::-webkit-scrollbar{display:none;} /* WebKit 隐藏滚动条 */
.mm-sep{margin:0 3px;}
.mm-item{flex:1 1 auto;min-width:0;} /* 指标可压缩 */
[data-f="cpu"],
[data-f="memUsed"],
[data-f="memTotal"],
[data-f="memPct"]{
display:inline-block;
min-width:auto;
}
#mm-net{flex:1 1 auto;min-width:0;}
}
@media (max-width:360px){
footer .monitor-metrics-wrap{ /* 预留扩展 */ }
}
</style>
<footer>
<!-- 指标区 -->
<div class="monitor-metrics-wrap" id="monitor-metrics-wrap">
<span id="monitor-status-dot" class="monitor-status-dot"></span>
<span id="monitor-badge" class="monitor-badge" style="display:none"></span>
<span class="mm-item">CPU <span data-f="cpu">--%</span></span>
<span class="mm-sep">|</span>
<span class="mm-item">内存 <span data-f="memUsed">0.00G</span>/<span data-f="memTotal">0.00G</span> (<span data-f="memPct">0%</span>)</span>
<span class="mm-sep">|</span>
<span id="mm-net">
<span class="mm-net-item" data-net="_none">网络 无活动网卡</span>
</span>
</div>
<!-- 标签 + 更新时间 -->
<div class="monitor-footer-line">
<span id="monitor-label">服务器资源占用</span>
<span id="monitor-ts">更新时间 --:--:--</span>
</div>
</footer>
<script>
/* ------------------ 可配置参数 ------------------ */
const MONITOR_BASE='https://111.173.104.39:61/api/3'; // Glances API 根路径
const MONITOR_REFRESH_MS=5000; // 刷新间隔
const MONITOR_MAX_FAIL=5; // 连续失败阈值
const MONITOR_NET_MAX=8; // PC 端最多显示网卡数
/* ------------------ 运行时状态 ------------------ */
let monitorLastGood=null; // 最近一次成功的数据缓存
let monitorFailCount=0; // 连续失败次数
/* 媒体断点判断(与 CSS 保持一致) */
function isNarrow(){return window.innerWidth<=600;}
/* 百分比格式化(short 预留简化模式) */
function mPct(n,short=false){
if(!isFinite(n))return'N/A';
return short?n.toFixed(0)+'%':n.toFixed(1)+'%';
}
/* 速率格式化:默认返回 "X.XKB/s";short 模式用整数 K 或 X.XM */
function mKbps(v,short=false){
if(!isFinite(v))return short?'0':'0KB/s';
const kb=v/1024;
if(short){
if(kb>=1024)return(kb/1024).toFixed(1)+'M';
return kb.toFixed(0)+'K';
}
return kb.toFixed(1)+'KB/s';
}
/* 兼容多种字段名的网卡名提取 */
function mIface(n){return n?.interface||n?.interface_name||n?.name||'';}
/* 兼容不同版本内存字段 */
function mSafeMem(mem){
const used=mem.used??mem.active??mem.mem_used??0;
const total=mem.total??mem.mem_total??(used||1);
return{used,total};
}
/* 确保状态灯存在(极端情况下被移除时重建) */
function ensureDot(){
let dot=document.getElementById('monitor-status-dot');
if(!dot){
const wrap=document.getElementById('monitor-metrics-wrap');
if(!wrap) return null;
dot=document.createElement('span');
dot.id='monitor-status-dot';
dot.className='monitor-status-dot';
wrap.insertBefore(dot,wrap.firstChild);
}
return dot;
}
/* 根据 CPU/内存占用决定状态灯颜色;stale 时增加外发光 */
function mSetHealth(cpuPct,memPct,stale){
const dot=ensureDot();
if(!dot) return;
dot.className='monitor-status-dot';
if(stale)dot.classList.add('monitor-status-stale');
if(cpuPct<60&&memPct<70)dot.classList.add('monitor-status-ok');
else if(cpuPct<85&&memPct<85)dot.classList.add('monitor-status-warn');
else dot.classList.add('monitor-status-bad');
}
/* 按需更新某字段文本 */
function setField(k,val){
const el=document.querySelector(`[data-f="${k}"]`);
if(el&&el.textContent!==val)el.textContent=val;
}
/* 渲染 CPU 与内存,对内存返回数值百分比用于状态灯逻辑 */
function renderMem(cpu,mem){
const narrow=isNarrow();
const usedRaw=mem.used/1024/1024/1024;
const totalRaw=mem.total/1024/1024/1024;
const usedG=usedRaw.toFixed(narrow?1:2)+'G';
const totalG=totalRaw.toFixed(narrow?1:2)+'G';
const pct=(mem.used*100/mem.total);
setField('cpu',mPct(cpu,narrow));
setField('memUsed',usedG);
setField('memTotal',totalG);
setField('memPct',mPct(pct,narrow));
return pct;
}
/* 渲染网络:
- 移动端仅显示首个网卡
- 差异检测后再整体替换,减少重排
*/
function renderNet(list){
const holder=document.getElementById('mm-net');
if(!holder)return;
const narrow=isNarrow();
if(narrow&&list.length>1)list=list.slice(0,1);
if(!list.length){
const existing=holder.querySelector('[data-net="_none"]');
if(!existing){
holder.textContent='';
const sp=document.createElement('span');
sp.className='mm-net-item';
sp.setAttribute('data-net','_none');
sp.textContent='网络 无活动网卡';
holder.appendChild(sp);
}
return;
}
const oldMap=new Map();
holder.childNodes.forEach(n=>{
if(n.nodeType===1&&n.hasAttribute('data-net')){
oldMap.set(n.getAttribute('data-net'),n);
}
});
const newNodes=[];
list.forEach((n,i)=>{
const key=n.name;
let span=oldMap.get(key);
if(span)oldMap.delete(key);
else{
span=document.createElement('span');
span.className='mm-net-item';
span.setAttribute('data-net',key);
}
/* 移动端:使用斜杠 rx/tx;PC:使用 ↓ / ↑ */
const txt=narrow
? `${n.name} ${mKbps(n.rx,true)}/${mKbps(n.tx,true)}`
: `${n.name} ↓${mKbps(n.rx)} ↑${mKbps(n.tx)}`;
if(span.textContent!==txt)span.textContent=txt;
if(i>0){
const sep=document.createElement('span');
sep.className='mm-sep';
sep.textContent='|';
newNodes.push(sep);
}
newNodes.push(span);
});
let changed=holder.childNodes.length!==newNodes.length;
if(!changed){
for(let i=0;i<newNodes.length;i++){
if(holder.childNodes[i].textContent!==newNodes[i].textContent){changed=true;break;}
}
}
if(changed){
holder.textContent='';
newNodes.forEach(n=>holder.appendChild(n));
}
}
/* 总渲染:指标、网络、徽章、时间、状态灯 */
function mRender(data,{stale=false,errorMsg=null}={}){
if(!data)return;
const badge=document.getElementById('monitor-badge');
const ts=document.getElementById('monitor-ts');
const memPct=renderMem(data.cpu,data.mem);
renderNet(data.net);
if(errorMsg&&monitorFailCount>=MONITOR_MAX_FAIL){
badge.style.display='inline-block';badge.textContent='错误';
}else if(stale){
badge.style.display='inline-block';badge.textContent='延迟';
}else{
badge.style.display='none';
}
ts.textContent=(stale?'上次成功 ':'更新时间 ')+new Date(data.ts).toLocaleTimeString();
mSetHealth(data.cpu,memPct,stale);
}
/* 带超时的 fetch 封装,避免长时间挂起 */
async function mFetch(path,timeout=3000){
const ctrl=new AbortController();
const t=setTimeout(()=>ctrl.abort(),timeout);
try{
const r=await fetch(MONITOR_BASE+path,{cache:'no-store',signal:ctrl.signal});
if(!r.ok)throw new Error('HTTP '+r.status);
return await r.json();
}finally{
clearTimeout(t);
}
}
/* 主更新流程:
1. 并发请求 CPU/内存/网络
2. 兼容字段提取
3. 构造快照并重置失败计数
4. 失败:使用上次成功数据(stale)或显示占位
*/
async function mUpdate(){
try{
const [cpu,memRaw,netRaw]=await Promise.all([
mFetch('/cpu'),
mFetch('/mem'),
mFetch('/network')
]);
const cpuTotal=cpu.total??cpu.current??0;
const m=mSafeMem(memRaw);
let netList=Array.isArray(netRaw)?netRaw:(netRaw?Object.values(netRaw):[]);
netList=netList
.filter(n=>{
const nm=mIface(n).toLowerCase();
return nm&&!nm.includes('loopback'); // 排除 loopback
})
.slice(0,MONITOR_NET_MAX)
.map(n=>{
const name=mIface(n)||'if';
const rx=n.rx_per_sec??n.rx??0;
const tx=n.tx_per_sec??n.tx??0;
return {name,rx,tx};
});
monitorLastGood={
cpu:cpuTotal,
mem:{used:m.used,total:m.total},
net:netList,
ts:Date.now()
};
monitorFailCount=0;
mRender(monitorLastGood);
}catch(e){
monitorFailCount++;
if(monitorLastGood){
mRender(monitorLastGood,{stale:true,errorMsg:e.message});
}else{
setField('cpu','...');
}
console.warn('[monitor]',e.message);
}
}
/* 启动与轮询 */
mUpdate();
setInterval(mUpdate,MONITOR_REFRESH_MS);
/* 视口尺寸变化时重新渲染(保证格式与缩写自适应) */
window.addEventListener('resize',()=>{
if(monitorLastGood)mRender(monitorLastGood);
});
</script>