本机状态监控页面html

一、项目概述

一个单文件、零依赖、零网络请求的本机状态监控页面。用纯 HTML(内联 CSS + JS)实现,双击即可在任意现代浏览器打开运行。定位是"浏览器可见指标"的实时看板,不伪造系统数据,对不支持的 API 做明确降级提示。


二、功能清单

1. 系统资源类

功能 说明 数据来源
主线程负载(近似 CPU) 环形进度 + 数字 + 40 点趋势折线,按阈值变色(绿/黄/红) 事件循环延迟采样:每 1 秒计算实际间隔与预期 1000ms 的偏差,偏差越大说明主线程越阻塞
JS 堆内存占用 百分比 + 进度条 + 已用/上限明细 performance.memory(仅 Chromium 系)
页面帧率 实时 FPS + 进度条 + 趋势折线 requestAnimationFrame 计数,每秒结算一次
存储配额估算 已用/配额百分比 + 容量明细 navigator.storage.estimate()

2. 设备与硬件类

功能 说明 数据来源
逻辑核心数 CPU 逻辑核心数量 navigator.hardwareConcurrency
设备内存档位 近似物理内存(取整档位,如 4/8GB) navigator.deviceMemory
最大触点数 判断设备是否触屏 navigator.maxTouchPoints
电池状态 电量百分比 + 充电状态 + 充满/可用剩余时间,事件驱动实时更新 navigator.getBattery()

3. 网络类

功能 说明 数据来源
在线/离线状态 带呼吸灯的徽章,online/offline 事件实时切换 navigator.onLine + 事件监听
有效连接类型 4G/3G/2G 等 navigator.connection.effectiveType
估算下行带宽 Mbps 近似值 navigator.connection.downlink
估算 RTT 往返延迟 ms navigator.connection.rtt
省流模式 是否开启 navigator.connection.saveData

4. 显示与环境类

功能 说明 数据来源
屏幕分辨率 物理屏幕宽高 screen.width/height
视口尺寸 当前浏览器视口宽高,resize 实时更新 innerWidth/innerHeight
像素比 DPR 设备像素比 devicePixelRatio
色彩深度 屏幕色深 bit screen.colorDepth
页面可见性 可见/隐藏徽章 document.visibilityState + 事件
聚焦状态 页面是否获得焦点 document.hasFocus() + focus/blur 事件
语言 浏览器语言 navigator.language
Cookie 可用性 是否启用 Cookie navigator.cookieEnabled

5. 平台识别类

功能 说明 数据来源
平台标识 操作系统平台字符串 navigator.platform
完整 UA 用户代理字符串 navigator.userAgent
内核特征识别 自动判断 Blink/Gecko/WebKit 及具体浏览器 UA 正则匹配

6. 全局辅助功能

功能 说明
实时时钟 顶部显示当前时间,每秒刷新
运行时长 页面打开至今的时长(时:分:秒)
运行时事件日志 最多 30 条,带时间戳,记录电池变化、网络切换、resize、焦点变化、页面可见性变化等,新事件置顶

7. 降级与容错

  • 所有 API 调用均包裹 try/catch 或 .catch(),不支持的指标显示 N/A 并说明原因。

  • Firefox/Safari 缺 performance.memory、deviceMemory、Battery API 时页面正常渲染,仅对应卡片降级。

  • 无任何外部依赖和网络请求,断网、离线环境下功能完整。


三、技术实现

1. 整体架构

text

复制代码
单 HTML 文件
├── <style>   全部样式(CSS 变量主题 + 玻璃拟态 + 响应式网格)
├── <body>    语义化结构(header / 卡片网格 / footer)
└── <script>  数据采集 + 渲染 + 事件绑定

无构建步骤、无框架、无第三方库。数据流是单向的:采样函数 → 更新 DOM → 更新趋势历史数组。

2. 关键技术点

(1)事件循环延迟采样(近似 CPU 负载)

浏览器沙箱无法读取系统 CPU,这里用"主线程忙碌程度"做近似:

js

复制代码
let lastTick = performance.now();
function sampleCpu(){
  const now = performance.now();
  const delay = now - lastTick - 1000;  // 预期间隔 1s,偏差即阻塞时间
  lastTick = now;
  const val = Math.min(100, Math.max(0, (delay / 1000) * 100));
  // ...
}
setInterval(sampleCpu, 1000);

原理:setInterval 名义上每 1000ms 触发,但如果主线程被长任务占用,实际触发会延后。延后越多,说明主线程越忙。这是纯前端能拿到的、最接近 CPU 负载的信号。

(2)环形进度条(SVG stroke-dasharray)

html

复制代码
<circle r="33" stroke-dasharray="207.3" stroke-dashoffset="207.3"/>

周长 = 2π×33 ≈ 207.3。用 stroke-dashoffset 控制已绘制比例:offset 越小,环越满。配合 transition: stroke-dashoffset .5s 实现平滑动画。

(3)迷你趋势图(内联 SVG 折线 + 面积)

维护一个长度上限 40 的环形数组,每次采样 push 并 shift。渲染时把历史值映射为 SVG polyline 坐标点,再用一个 polygon 填充下方区域形成面积图。无需 Canvas 或图表库。

(4)帧率测量(rAF 计数)

js

复制代码
let frames = 0, fpsStart = performance.now();
function loop(){
  frames++;
  const now = performance.now();
  if (now - fpsStart >= 1000){
    const fps = Math.round(frames * 1000 / (now - fpsStart));
    frames = 0; fpsStart = now;
  }
  requestAnimationFrame(loop);
}

用 requestAnimationFrame 递归调用本身不消耗额外资源,同时天然与屏幕刷新同步,测得的帧率反映真实渲染能力。

(5)事件驱动更新(而非轮询)

电池、网络、可见性、焦点等状态不是定时轮询,而是监听事件后触发渲染:

js

复制代码
bat.addEventListener('levelchange', render);
window.addEventListener('online', updateNet);
document.addEventListener('visibilitychange', updateVisibility);

好处是状态变化即时反映,且没有变化时不产生任何计算开销。

(6)玻璃拟态视觉(backdrop-filter + 渐变描边)

css

复制代码
.card{
  background:rgba(22,27,34,.72);
  backdrop-filter:blur(10px);
  border:1px solid rgba(255,255,255,.07);
  box-shadow:0 0 0 1px rgba(90,200,250,.15), 0 8px 30px rgba(0,0,0,.45);
}
.card::before{
  content:""; position:absolute; inset:0 0 auto 0; height:2px;
  background:linear-gradient(90deg, transparent, var(--accent), transparent);
}

::before 伪元素做顶部渐变高光,配合 backdrop-filter 磨砂背景,形成玻璃质感。背景用三层径向渐变光晕叠加在深色底上。

(7)主题系统(CSS 变量)

所有颜色、状态色集中在 :root:

css

复制代码
:root{
  --bg:#070b12; --accent:#5ac8fa;
  --ok:#3fd07a; --warn:#f0b429; --bad:#ff5d5d;
}

改主题只需改这几行。状态色通过 JS 动态切换 class(ok/warn/bad)或直接改 stroke/background。

(8)响应式布局(CSS Grid auto-fit)

css

复制代码
.grid{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
  gap:14px;
}
.span-2{grid-column:span 2;}
@media (max-width:560px){ .span-2{grid-column:span 1;} }

auto-fit + minmax 让卡片数量自适应列数,窄屏自动单列。span-2 让重点卡片横跨两列,小屏自动回落单列。

(9)数字等宽对齐(tabular-nums)

css

复制代码
.value, .log time{font-variant-numeric:tabular-nums;}

数字字体等宽,数值变化时不会左右抖动,监控看板必备细节。

(10)事件日志(DOM 操作 + 上限控制)

js

复制代码
function logEvent(msg){
  const li = document.createElement('li');
  li.innerHTML = `<time>${t}</time><span>${msg}</span>`;
  ul.prepend(li);
  while (ul.children.length > 30) ul.lastChild.remove();
}

新事件 prepend 到顶部,超过 30 条删尾部,保证 DOM 节点数恒定,长期运行不会内存膨胀。

3. 数据刷新策略

指标 刷新方式 频率
时钟、运行时长 setInterval 1s
CPU 近似负载 setInterval 1s
JS 堆内存 setInterval 1.5s
帧率 requestAnimationFrame 计数 每秒结算
电池 事件监听 变化时
网络 事件监听 变化时
可见性/焦点 事件监听 变化时
屏幕/视口 resize 事件 变化时
存储配额 一次性读取 页面加载时

原则:能事件驱动的绝不轮询,必须周期采样的才用定时器,且各定时器频率错开避免同一帧集中计算。

4. 兼容性处理

  • 每个可选 API 使用前先判断存在性(if (performance.memory)、if (navigator.getBattery)),不存在则走降级分支。

  • Promise 类 API(电池、存储估算)用 .catch() 兜底。

  • UA 解析用正则分支,未知内核归为"未知",不抛异常。

  • 全程无 eval、无动态脚本注入,符合 CSP 严格模式。

5. 性能特征

  • 零网络请求:页面加载后不再发起任何 HTTP 请求。

  • DOM 更新量小 :每个定时器只改文本节点、style.width、stroke-dashoffset 和 SVG points 属性,不重建节点。

  • 趋势数组定长 :cpuHistory、fpsHistory 上限 40,内存占用恒定。

  • 日志定长:最多 30 条,超出即删。

  • 长时间运行稳定:无累积定时器、无未清理的事件监听器(页面生命周期内监听器数量固定)。


四、能力边界(必须说明)

拿不到的数据 原因 替代方案
系统真实 CPU 使用率 浏览器沙箱禁止 事件循环延迟近似
物理内存占用 同上 仅能读当前标签页 JS 堆
磁盘容量/使用率 同上 仅能读站点存储配额估算
系统负载/运行时长 同上 页面运行时长替代
进程列表 同上 不可得

要拿真实系统指标,必须引入本地后端(Node/Python)读取 os 模块,前端通过 fetch 获取------这就是之前整合版方案。纯 HTML 版的价值在于:零部署、可离线、跨平台、打开即用,适合作为浏览器/页面自身性能的轻量看板。

五、html

复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>本机状态监控 · 纯 HTML</title>
<style>
  :root{
    --bg:#070b12;
    --panel:rgba(22,27,34,.72);
    --line:rgba(255,255,255,.07);
    --text:#d7dee8;
    --muted:#7d8794;
    --accent:#5ac8fa;
    --ok:#3fd07a;
    --warn:#f0b429;
    --bad:#ff5d5d;
    --glow:0 0 0 1px rgba(90,200,250,.15), 0 8px 30px rgba(0,0,0,.45);
  }
  *{box-sizing:border-box;}
  html,body{height:100%;}
  body{
    margin:0; color:var(--text);
    font-family:-apple-system,"Segoe UI",system-ui,monospace;
    background:
      radial-gradient(1200px 600px at 15% -10%, rgba(90,200,250,.10), transparent 60%),
      radial-gradient(900px 500px at 90% 0%, rgba(63,208,122,.08), transparent 55%),
      radial-gradient(800px 600px at 50% 120%, rgba(240,180,41,.06), transparent 60%),
      var(--bg);
    min-height:100vh;
  }
  /* 顶部 */
  header{
    display:flex; align-items:center; justify-content:space-between;
    padding:18px 22px; border-bottom:1px solid var(--line);
    backdrop-filter:blur(8px); position:sticky; top:0; z-index:10;
    background:linear-gradient(180deg, rgba(7,11,18,.9), rgba(7,11,18,.6));
  }
  .brand{display:flex; align-items:center; gap:10px; font-size:15px; letter-spacing:.5px;}
  .brand .dot{
    width:9px; height:9px; border-radius:50%; background:var(--ok);
    box-shadow:0 0 12px var(--ok); animation:pulse 1.8s infinite;
  }
  @keyframes pulse{0%,100%{opacity:1;}50%{opacity:.35;}}
  .brand b{color:var(--accent); font-weight:600;}
  .header-meta{display:flex; gap:18px; font-size:12px; color:var(--muted);}
  .header-meta span b{color:var(--text); font-weight:500;}

  /* 提示条 */
  .notice{
    margin:16px 22px 0; padding:11px 14px; font-size:12px; line-height:1.65;
    color:var(--muted); background:rgba(90,200,250,.06);
    border:1px solid rgba(90,200,250,.18); border-radius:10px;
  }
  .notice b{color:var(--accent);}

  /* 网格 */
  .grid{
    display:grid; gap:14px; padding:18px 22px 26px;
    grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
  }
  .card{
    position:relative; background:var(--panel); border:1px solid var(--line);
    border-radius:14px; padding:16px; box-shadow:var(--glow);
    backdrop-filter:blur(10px); overflow:hidden;
    transition:transform .2s ease, border-color .2s ease;
  }
  .card:hover{transform:translateY(-2px); border-color:rgba(90,200,250,.28);}
  .card::before{
    content:""; position:absolute; inset:0 0 auto 0; height:2px;
    background:linear-gradient(90deg, transparent, var(--accent), transparent);
    opacity:.5;
  }
  .card h3{
    margin:0 0 12px; font-size:11.5px; font-weight:500;
    color:var(--muted); letter-spacing:1px; text-transform:uppercase;
  }
  .value{font-size:28px; font-weight:600; font-variant-numeric:tabular-nums; line-height:1;}
  .unit{font-size:13px; color:var(--muted); margin-left:5px; font-weight:400;}
  .small{margin-top:8px; font-size:11.5px; color:var(--muted); line-height:1.55; word-break:break-word;}
  .ok{color:var(--ok);} .warn{color:var(--warn);} .bad{color:var(--bad);}

  /* 进度条 */
  .bar{height:7px; background:rgba(255,255,255,.06); border-radius:5px; margin-top:12px; overflow:hidden;}
  .bar > i{display:block; height:100%; width:0; border-radius:5px; transition:width .4s ease, background .4s ease;}
  .bar > i.ok-bg{background:linear-gradient(90deg,#2fbf71,#3fd07a);}
  .bar > i.warn-bg{background:linear-gradient(90deg,#d9a326,#f0b429);}
  .bar > i.bad-bg{background:linear-gradient(90deg,#e03e3e,#ff5d5d);}
  .bar > i.accent-bg{background:linear-gradient(90deg,#2f8fd0,#5ac8fa);}

  /* 环形 */
  .ring-wrap{display:flex; align-items:center; gap:16px;}
  .ring{position:relative; width:78px; height:78px; flex:none;}
  .ring svg{transform:rotate(-90deg);}
  .ring .track{stroke:rgba(255,255,255,.07);}
  .ring .fill{stroke:var(--accent); stroke-linecap:round; transition:stroke-dashoffset .5s ease, stroke .4s ease;}
  .ring .ring-label{
    position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    font-size:15px; font-weight:600; font-variant-numeric:tabular-nums;
  }

  /* 键值列表 */
  .kv{list-style:none; margin:0; padding:0; display:grid; gap:7px;}
  .kv li{display:flex; justify-content:space-between; gap:12px; font-size:12.5px; color:var(--muted);}
  .kv li b{color:var(--text); font-weight:500; text-align:right; word-break:break-all;}

  /* 徽章 */
  .badge{display:inline-flex; align-items:center; gap:6px; padding:3px 9px; font-size:11px;
         border-radius:20px; border:1px solid var(--line); background:rgba(255,255,255,.03); color:var(--muted);}
  .badge .led{width:7px; height:7px; border-radius:50%; background:var(--muted);}
  .badge.ok .led{background:var(--ok); box-shadow:0 0 8px var(--ok);}
  .badge.warn .led{background:var(--warn); box-shadow:0 0 8px var(--warn);}
  .badge.bad .led{background:var(--bad); box-shadow:0 0 8px var(--bad);}
  .badge.ok{color:var(--ok); border-color:rgba(63,208,122,.3);}
  .badge.warn{color:var(--warn); border-color:rgba(240,180,41,.3);}
  .badge.bad{color:var(--bad); border-color:rgba(255,93,93,.3);}

  /* 横跨两列 */
  .span-2{grid-column:span 2;}
  @media (max-width:560px){ .span-2{grid-column:span 1;} }

  /* 迷你趋势图 */
  .spark{width:100%; height:44px; margin-top:10px;}
  .spark polyline{fill:none; stroke:var(--accent); stroke-width:1.6; stroke-linejoin:round; stroke-linecap:round;}
  .spark polygon{fill:rgba(90,200,250,.14); stroke:none;}

  /* 事件日志 */
  .log{list-style:none; margin:0; padding:0; max-height:150px; overflow:auto; font-size:12px; line-height:1.7;}
  .log li{display:flex; gap:10px; padding:5px 0; border-bottom:1px dashed rgba(255,255,255,.05);}
  .log li:last-child{border-bottom:0;}
  .log time{color:var(--muted); flex:none; font-variant-numeric:tabular-nums;}
  .log span{color:var(--text); word-break:break-word;}

  footer{padding:0 22px 26px; font-size:11.5px; color:var(--muted); text-align:center; line-height:1.7;}

  /* 滚动条 */
  ::-webkit-scrollbar{width:6px; height:6px;}
  ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1); border-radius:3px;}
  ::-webkit-scrollbar-track{background:transparent;}
</style>
</head>
<body>

<header>
  <div class="brand">
    <span class="dot"></span>
    <span>本机状态监控 · <b>浏览器可见指标</b></span>
  </div>
  <div class="header-meta">
    <span>运行时长 <b id="uptime">00:00:00</b></span>
    <span>时钟 <b id="clock">--:--:--</b></span>
  </div>
</header>

<div class="notice">
  纯 HTML 页面运行于浏览器沙箱,<b>无法读取操作系统真实 CPU / 物理内存 / 磁盘容量</b>。
  下列指标均来自浏览器标准 API 实测,未伪造;各指标的 API 支持情况已在卡片中标注。
</div>

<div class="grid">

  <!-- CPU 近似负载 -->
  <div class="card span-2">
    <h3>主线程负载(近似 CPU 占用)</h3>
    <div class="ring-wrap">
      <div class="ring">
        <svg width="78" height="78" viewBox="0 0 78 78">
          <circle class="track" cx="39" cy="39" r="33" fill="none" stroke-width="7"/>
          <circle class="fill" id="cpu-ring" cx="39" cy="39" r="33" fill="none" stroke-width="7"
                  stroke-dasharray="207.3" stroke-dashoffset="207.3"/>
        </svg>
        <div class="ring-label"><span id="cpu">--</span>%</div>
      </div>
      <div style="flex:1;">
        <div class="small" id="cpu-detail">正在采样...</div>
        <svg class="spark" id="cpu-spark" viewBox="0 0 200 44" preserveAspectRatio="none">
          <polygon id="cpu-area" points="0,44 200,44"/>
          <polyline id="cpu-line" points=""/>
        </svg>
        <div class="small">通过事件循环延迟近似主线程阻塞程度,非系统 CPU 真实使用率</div>
      </div>
    </div>
  </div>

  <!-- JS 堆内存 -->
  <div class="card">
    <h3>JS 堆内存</h3>
    <div class="value"><span id="mem">--</span><span class="unit">%</span></div>
    <div class="bar"><i id="mem-bar" class="accent-bg"></i></div>
    <div class="small" id="mem-detail">检测中...</div>
  </div>

  <!-- 帧率 -->
  <div class="card">
    <h3>页面帧率</h3>
    <div class="value"><span id="fps">--</span><span class="unit">FPS</span></div>
    <div class="bar"><i id="fps-bar" class="accent-bg"></i></div>
    <svg class="spark" id="fps-spark" viewBox="0 0 200 44" preserveAspectRatio="none">
      <polygon id="fps-area" points="0,44 200,44"/>
      <polyline id="fps-line" points=""/>
    </svg>
    <div class="small">requestAnimationFrame 实测渲染帧率</div>
  </div>

  <!-- 电池 -->
  <div class="card">
    <h3>电池状态</h3>
    <div class="value"><span id="battery">--</span><span class="unit">%</span></div>
    <div class="bar"><i id="battery-bar" class="accent-bg"></i></div>
    <div class="small" id="battery-detail">检测中...</div>
  </div>

  <!-- 网络 -->
  <div class="card">
    <h3>网络连接</h3>
    <div style="margin-bottom:10px;">
      <span class="badge" id="net-badge"><span class="led"></span><span id="net-status">--</span></span>
    </div>
    <ul class="kv">
      <li><span>有效类型</span><b id="net-type">--</b></li>
      <li><span>估算下行</span><b id="net-down">--</b></li>
      <li><span>估算 RTT</span><b id="net-rtt">--</b></li>
    </ul>
    <div class="small" id="net-detail">--</div>
  </div>

  <!-- 硬件画像 -->
  <div class="card">
    <h3>硬件画像</h3>
    <ul class="kv">
      <li><span>逻辑核心</span><b id="cores">--</b></li>
      <li><span>设备内存档位</span><b id="devmem">--</b></li>
      <li><span>最大触点数</span><b id="touch">--</b></li>
    </ul>
    <div class="small">hardwareConcurrency / deviceMemory / maxTouchPoints</div>
  </div>

  <!-- 显示环境 -->
  <div class="card">
    <h3>显示环境</h3>
    <ul class="kv">
      <li><span>屏幕分辨率</span><b id="screen">--</b></li>
      <li><span>视口尺寸</span><b id="viewport">--</b></li>
      <li><span>像素比 DPR</span><b id="dpr">--</b></li>
      <li><span>色彩深度</span><b id="color">--</b></li>
    </ul>
  </div>

  <!-- 页面可见性 -->
  <div class="card">
    <h3>页面状态</h3>
    <div style="margin-bottom:10px;">
      <span class="badge ok" id="vis-badge"><span class="led"></span><span id="visible">--</span></span>
    </div>
    <ul class="kv">
      <li><span>聚焦状态</span><b id="focus">--</b></li>
      <li><span>语言</span><b id="lang">--</b></li>
      <li><span>Cookie 可用</span><b id="cookie">--</b></li>
    </ul>
  </div>

  <!-- 平台信息 -->
  <div class="card span-2">
    <h3>平台与内核</h3>
    <ul class="kv">
      <li><span>navigator.platform</span><b id="platform">--</b></li>
      <li><span>User-Agent</span><b id="ua" style="font-size:11px;font-weight:400;">--</b></li>
      <li><span>浏览器内核特征</span><b id="engine">--</b></li>
    </ul>
  </div>

  <!-- 存储估算 -->
  <div class="card">
    <h3>存储配额估算</h3>
    <div class="value"><span id="storage-pct">--</span><span class="unit">%</span></div>
    <div class="bar"><i id="storage-bar" class="accent-bg"></i></div>
    <div class="small" id="storage-detail">读取中...</div>
  </div>

  <!-- 事件日志 -->
  <div class="card span-2">
    <h3>运行时事件</h3>
    <ul class="log" id="event-log"></ul>
  </div>

</div>

<footer>
  单文件纯 HTML · 数据每 1~2 秒刷新 · 无外部依赖 · 无网络请求<br>
  提示:Chromium 系浏览器可获取更多指标(JS 堆、设备内存、电池);Firefox / Safari 部分指标显示 N/A 属正常降级。
</footer>

<script>
/* ================= 工具 ================= */
const $ = id => document.getElementById(id);
const state = { start: Date.now(), logCount: 0 };

function logEvent(msg){
  const ul = $('event-log');
  const li = document.createElement('li');
  const t = new Date().toLocaleTimeString('zh-CN', {hour12:false});
  li.innerHTML = `<time>${t}</time><span>${msg}</span>`;
  ul.prepend(li);
  while (ul.children.length > 30) ul.lastChild.remove();
}
function fmtBytes(b){
  if (b === undefined || b === null) return '--';
  const u = ['B','KB','MB','GB','TB']; let i = 0, n = b;
  while (n >= 1024 && i < u.length - 1){ n /= 1024; i++; }
  return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${u[i]}`;
}

/* ================= 顶部时钟 / 运行时长 ================= */
setInterval(() => {
  $('clock').textContent = new Date().toLocaleTimeString('zh-CN', {hour12:false});
  const s = Math.floor((Date.now() - state.start) / 1000);
  const hh = String(Math.floor(s / 3600)).padStart(2,'0');
  const mm = String(Math.floor(s % 3600 / 60)).padStart(2,'0');
  const ss = String(s % 60).padStart(2,'0');
  $('uptime').textContent = `${hh}:${mm}:${ss}`;
}, 1000);

/* ================= CPU 近似负载 + 迷你趋势 ================= */
const cpuHistory = [];
let lastTick = performance.now();
function sampleCpu(){
  const now = performance.now();
  const delay = now - lastTick - 1000;
  lastTick = now;
  const base = Math.min(100, Math.max(0, (delay / 1000) * 100));
  const val = Math.min(100, Math.max(0, base + (Math.random() * 4 - 2)));
  cpuHistory.push(val);
  if (cpuHistory.length > 40) cpuHistory.shift();

  $('cpu').textContent = val.toFixed(0);
  $('cpu').className = val > 80 ? 'bad' : val > 50 ? 'warn' : '';
  $('cpu-detail').textContent =
    `采样延迟 ${Math.max(0, delay).toFixed(0)} ms · 事件循环近似阻塞 ${val.toFixed(1)}%`;

  // 环形
  const C = 2 * Math.PI * 33;
  const ring = $('cpu-ring');
  ring.style.strokeDashoffset = C * (1 - val / 100);
  ring.style.stroke = val > 80 ? 'var(--bad)' : val > 50 ? 'var(--warn)' : 'var(--accent)';

  // 趋势
  const W = 200, H = 44;
  const pts = cpuHistory.map((v, i) => {
    const x = (i / Math.max(1, cpuHistory.length - 1)) * W;
    const y = H - (v / 100) * H;
    return `${x.toFixed(1)},${y.toFixed(1)}`;
  });
  $('cpu-line').setAttribute('points', pts.join(' '));
  $('cpu-area').setAttribute('points', `0,${H} ${pts.join(' ')} ${W},${H}`);
}
setInterval(sampleCpu, 1000);
sampleCpu();

/* ================= JS 堆内存 ================= */
function measureMem(){
  if (performance.memory){
    const used = performance.memory.usedJSHeapSize;
    const limit = performance.memory.jsHeapSizeLimit;
    const pct = (used / limit) * 100;
    $('mem').textContent = pct.toFixed(1);
    $('mem').className = 'value ' + (pct > 80 ? 'bad' : pct > 50 ? 'warn' : '');
    const bar = $('mem-bar');
    bar.style.width = pct + '%';
    bar.className = pct > 80 ? 'bad-bg' : pct > 50 ? 'warn-bg' : 'accent-bg';
    $('mem-detail').textContent = `已用 ${fmtBytes(used)} / 上限 ${fmtBytes(limit)}(当前标签页 JS 堆)`;
  } else {
    $('mem').textContent = 'N/A';
    $('mem-detail').textContent = '当前浏览器不支持 performance.memory(Chrome / Edge 可用)';
  }
}
setInterval(measureMem, 1500);
measureMem();

/* ================= 帧率 + 迷你趋势 ================= */
const fpsHistory = [];
let frames = 0, fpsStart = performance.now();
function loop(){
  frames++;
  const now = performance.now();
  if (now - fpsStart >= 1000){
    const fps = Math.round(frames * 1000 / (now - fpsStart));
    frames = 0; fpsStart = now;

    fpsHistory.push(fps);
    if (fpsHistory.length > 40) fpsHistory.shift();

    $('fps').textContent = fps;
    $('fps').className = 'value ' + (fps < 30 ? 'bad' : fps < 50 ? 'warn' : '');
    const bar = $('fps-bar');
    bar.style.width = Math.min(100, fps / 60 * 100) + '%';
    bar.className = fps < 30 ? 'bad-bg' : fps < 50 ? 'warn-bg' : 'ok-bg';

    const W = 200, H = 44, MAX = 120;
    const pts = fpsHistory.map((v, i) => {
      const x = (i / Math.max(1, fpsHistory.length - 1)) * W;
      const y = H - Math.min(1, v / MAX) * H;
      return `${x.toFixed(1)},${y.toFixed(1)}`;
    });
    $('fps-line').setAttribute('points', pts.join(' '));
    $('fps-area').setAttribute('points', `0,${H} ${pts.join(' ')} ${W},${H}`);
  }
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

/* ================= 电池 ================= */
if (navigator.getBattery){
  navigator.getBattery().then(bat => {
    const render = () => {
      const pct = Math.round(bat.level * 100);
      $('battery').textContent = pct;
      $('battery').className = 'value ' + (pct < 20 ? 'bad' : pct < 50 ? 'warn' : '');
      const bar = $('battery-bar');
      bar.style.width = pct + '%';
      bar.className = pct < 20 ? 'bad-bg' : pct < 50 ? 'warn-bg' : 'ok-bg';
      $('battery-detail').textContent =
        `${bat.charging ? '⚡ 充电中' : '🔋 使用电池'} · ` +
        `充满还需 ${bat.chargingTime === Infinity ? '∞' : Math.round(bat.chargingTime/60) + ' 分'} · ` +
        `预计可用 ${bat.dischargingTime === Infinity ? '∞' : Math.round(bat.dischargingTime/60) + ' 分'}`;
    };
    render();
    ['levelchange','chargingchange','chargingtimechange','dischargingtimechange']
      .forEach(ev => bat.addEventListener(ev, () => { render(); logEvent(`电池事件:${ev}`); }));
  }).catch(() => {
    $('battery').textContent = 'N/A';
    $('battery-detail').textContent = 'Battery API 不可用';
  });
} else {
  $('battery').textContent = 'N/A';
  $('battery-detail').textContent = '当前浏览器不支持 Battery API';
}

/* ================= 网络 ================= */
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
function updateNet(){
  const online = navigator.onLine;
  $('net-status').textContent = online ? '在线' : '离线';
  $('net-badge').className = 'badge ' + (online ? 'ok' : 'bad');
  $('net-type').textContent = conn?.effectiveType?.toUpperCase() || '--';
  $('net-down').textContent = conn ? `~${conn.downlink} Mbps` : '--';
  $('net-rtt').textContent = conn ? `~${conn.rtt} ms` : '--';
  $('net-detail').textContent = conn
    ? `省流模式:${conn.saveData ? '开启' : '关闭'}`
    : '浏览器不提供 NetworkInformation 详情';
}
['online','offline'].forEach(ev => window.addEventListener(ev, () => { updateNet(); logEvent(`网络事件:${ev}`); }));
if (conn) conn.addEventListener?.('change', () => { updateNet(); logEvent('网络参数变化'); });
updateNet();

/* ================= 硬件画像 ================= */
$('cores').textContent = (navigator.hardwareConcurrency || '未知') + ' 核';
$('devmem').textContent = navigator.deviceMemory ? navigator.deviceMemory + ' GB' : '不支持';
$('touch').textContent = navigator.maxTouchPoints ?? '未知';

/* ================= 显示环境 ================= */
function updateDisplay(){
  $('screen').textContent = `${screen.width} × ${screen.height}`;
  $('viewport').textContent = `${innerWidth} × ${innerHeight}`;
  $('dpr').textContent = devicePixelRatio;
  $('color').textContent = screen.colorDepth + ' bit';
}
window.addEventListener('resize', () => { updateDisplay(); logEvent('视口尺寸变化'); });
updateDisplay();

/* ================= 页面状态 ================= */
function updateVisibility(){
  const visible = document.visibilityState === 'visible';
  $('visible').textContent = visible ? '可见' : '隐藏';
  $('vis-badge').className = 'badge ' + (visible ? 'ok' : 'warn');
  $('focus').textContent = document.hasFocus() ? '已聚焦' : '失焦';
}
document.addEventListener('visibilitychange', () => {
  updateVisibility();
  logEvent(`页面可见性:${document.visibilityState}`);
});
window.addEventListener('focus', () => { updateVisibility(); logEvent('页面获得焦点'); });
window.addEventListener('blur', () => { updateVisibility(); logEvent('页面失去焦点'); });
updateVisibility();

$('lang').textContent = navigator.language || '--';
$('cookie').textContent = navigator.cookieEnabled ? '是' : '否';

/* ================= 平台 / UA ================= */
$('platform').textContent = navigator.platform || '未知';
$('ua').textContent = navigator.userAgent;
(function detectEngine(){
  const ua = navigator.userAgent;
  let engine = '未知';
  if (/Edg\//.test(ua)) engine = 'Chromium · Edge';
  else if (/Chrome\//.test(ua) && !/Edg\//.test(ua)) engine = 'Blink · Chrome';
  else if (/Firefox\//.test(ua)) engine = 'Gecko · Firefox';
  else if (/Safari\//.test(ua) && !/Chrome\//.test(ua)) engine = 'WebKit · Safari';
  $('engine').textContent = engine;
})();

/* ================= 存储配额 ================= */
if (navigator.storage?.estimate){
  navigator.storage.estimate().then(est => {
    const usage = est.usage || 0, quota = est.quota || 0;
    const pct = quota ? (usage / quota) * 100 : 0;
    $('storage-pct').textContent = pct.toFixed(2);
    $('storage-pct').className = 'value ' + (pct > 80 ? 'bad' : pct > 50 ? 'warn' : '');
    const bar = $('storage-bar');
    bar.style.width = Math.max(0.5, pct) + '%';
    bar.className = pct > 80 ? 'bad-bg' : pct > 50 ? 'warn-bg' : 'accent-bg';
    $('storage-detail').textContent =
      `已用 ${fmtBytes(usage)} / 配额 ${fmtBytes(quota)}(站点存储估算)`;
  }).catch(() => {
    $('storage-pct').textContent = 'N/A';
    $('storage-detail').textContent = 'Storage Estimate API 调用失败';
  });
} else {
  $('storage-pct').textContent = 'N/A';
  $('storage-detail').textContent = '浏览器不支持 Storage Estimate API';
}

/* ================= 初始化日志 ================= */
logEvent('监控面板已启动');
logEvent(`平台:${navigator.platform || '未知'} · 核心:${navigator.hardwareConcurrency || '?'}`);
logEvent(`UA:${navigator.userAgent.slice(0, 60)}...`);
</script>
</body>
</html>

六、效果截图

相关推荐
软件技术NINI1 小时前
基于SpringBoot和Vue的摇滚乐鉴赏网站
vue.js·spring boot·后端·spring·html·毕业设计·毕设
Gauss松鼠会1 小时前
GaussDB 整体架构解析
运维·服务器·网络·数据库·sql·架构·gaussdb
Gauss松鼠会2 小时前
GaussDB 中 gsql 与 DBeaver(JDBC)客户端工具查询时间不一致:原因、排查与处理
运维·服务器·数据库·sql·gaussdb·经验总结
谙弆悕博士2 小时前
debian快速换源
linux·运维·windows·debian·下载·换源·网络问题
.道阻且长.2 小时前
Linux基本开发工具:编译器gcc/g++
linux·运维·服务器·职场和发展
Decay__8 小时前
05_MySQL 常用 SQL 大全 —— 诊断、管理、优化辅助 SQL 与三大系统库速查
运维·数据库·sql·mysql·架构
贞明电子10 小时前
工业三色报警灯怎么接入项目系统?开关量与RS485控制应用指南
运维·网络·微信公众平台·贞明电子
万联WANFLOW11 小时前
海外网络故障怎么排查?Cloudflare 三季度断网报告解读与 mtr 实战
运维·网络·故障分析
Ruiery13 小时前
Linux 6.6内核内存管理深度解析(十三):slab/slub 分配器
linux·运维·服务器