一、项目概述
一个单文件、零依赖、零网络请求的本机状态监控页面。用纯 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和 SVGpoints属性,不重建节点。 -
趋势数组定长 :
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>
六、效果截图
