/* OpenStats 官网 · 应用界面还原
菜单栏读数、详情弹窗、主窗口与刘海岛都是 HTML 重建,数据是本页随机游走生成的模拟值,
不读取访客电脑的任何信息。风格、字号与布局对应应用内的 MenuBarRenderer、详情弹窗与主窗口。 */
(() => {
"use strict";
// ── 模拟数据 ──────────────────────────────────────────────
const HISTORY = 30;
const PROBES = 120;
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
const walk = (v, step, lo, hi) => clamp(v + (Math.random() - 0.5) * step, lo, hi);
const fill = (n, f) => Array.from({ length: n }, f);
const ITEM_ORDER = ["cpu", "mem", "net", "gpu", "temp"];
const S = {
cpu: 0.24, cpuHistory: fill(HISTORY, () => 0.15 + Math.random() * 0.2),
cores: fill(18, (_, i) => (i < 6 ? 0.1 : 0.35) + Math.random() * 0.2),
gpu: 0.12, gpuHistory: fill(HISTORY, () => Math.random() * 0.25),
mem: 0.68, memHistory: fill(HISTORY, () => 0.68),
up: 12 * 1024, down: 1.4 * 1024 * 1024,
upHistory: fill(HISTORY, () => Math.random() * 40 * 1024),
downHistory: fill(HISTORY, () => Math.random() * 1.6 * 1024 * 1024),
probes: fill(36, () => 58 + Math.random() * 12),
cpuTemp: 52, gpuTemp: 47, memTemp: 45, batteryTemp: 31, palmTemp: 30,
fans: [1640, 1760], fanMode: "auto",
keepAwake: false, awakeMode: "display", lidClosed: false, duration: 0,
items: readList("os-items", ["cpu", "mem", "net"]),
popover: null,
page: "overview", windowOpen: false, sort: "cpu",
dns: "auto", dnsManual: false, dnsNote: "",
style: read("os-style", "stacked"),
cleaned: false, cleaning: false, confirming: false,
};
const FAN_RANGE = [[1350, 5349], [1350, 5777]];
const PROCESSES = [
["Safari", 1], ["Xcode", 1], ["WindowServer", 3], ["音乐", 4], ["访达", 1], ["OpenStats", 1],
["终端", 3], ["照片", 5], ["备忘录", 5], ["邮件", 2], ["Spotlight", 6], ["系统设置", 6],
].map(([name, color]) => ({ name, color, cpu: Math.random() * 0.3, mem: (80 + Math.random() * 900) * 1024 * 1024 }));
const NET_PROCESSES = [["Safari", 1], ["音乐", 4], ["邮件", 2], ["终端", 3], ["照片", 5]]
.map(([name, color]) => ({ name, color, down: Math.random() * 400 * 1024, up: Math.random() * 60 * 1024 }));
const CLEAN_RULES = [
{ id: "caches", cat: "系统", title: "应用缓存", detail: "10 项 · 8 项使用中", size: 3.5e9, on: true },
{ id: "logs", cat: "系统", title: "应用日志与崩溃报告", detail: "10 项", size: 1.9e6, on: true },
{ id: "chrome", cat: "浏览器", title: "Chrome 缓存", detail: "Chrome 正在运行", size: 0, blocked: true },
{ id: "safari", cat: "浏览器", title: "Safari 缓存", detail: "1 项", size: 812e6, on: true },
{ id: "derived", cat: "开发者", title: "Xcode 编译缓存", detail: "6 项", size: 6.2e9, on: true },
{ id: "npm", cat: "开发者", title: "npm 缓存", detail: "1 项", size: 11.8e9, on: true },
{ id: "installers", cat: "下载", title: "安装包", detail: "3 项 · 移到废纸篓", size: 1.2e9, on: false },
{ id: "trash", cat: "废纸篓", title: "清空废纸篓", detail: "永久删除", size: 640e6, on: false },
];
const DNS_PRESETS = [
["auto", "自动", []], ["cloudflare", "Cloudflare", ["1.1.1.1", "1.0.0.1"]], ["google", "Google", ["8.8.8.8", "8.8.4.4"]],
["tencent", "腾讯 DNSPod", ["119.29.29.29", "182.254.116.116"], "腾讯"], ["aliyun", "阿里云", ["223.5.5.5", "223.6.6.6"]],
];
function read(key, fallback) {
try { return localStorage.getItem(key) || fallback; } catch { return fallback; }
}
function readList(key, fallback) {
const value = read(key, "");
const list = value.split(",").filter((k) => ITEM_ORDER.includes(k));
return value ? list : fallback;
}
function write(key, value) {
try { localStorage.setItem(key, value); } catch { /* 隐私模式下不可用,忽略 */ }
}
function push(history, value, size = HISTORY) {
history.push(value);
if (history.length > size) history.shift();
}
function tick() {
S.cores = S.cores.map((v, i) => walk(v, 0.18, 0.02, i < 6 ? 0.55 : 0.9));
S.cpu = S.cores.reduce((a, b) => a + b, 0) / S.cores.length;
S.gpu = walk(S.gpu, 0.12, 0, 0.6);
S.mem = walk(S.mem, 0.01, 0.6, 0.78);
S.up = clamp(S.up * (0.6 + Math.random() * 0.8) + Math.random() * 8000, 800, 4 * 1024 * 1024);
S.down = clamp(S.down * (0.6 + Math.random() * 0.8) + Math.random() * 60000, 2000, 60 * 1024 * 1024);
S.cpuTemp = walk(S.cpuTemp, 2, 42, 78);
S.gpuTemp = walk(S.gpuTemp, 1.5, 40, 70);
push(S.cpuHistory, S.cpu); push(S.gpuHistory, S.gpu); push(S.memHistory, S.mem);
push(S.upHistory, S.up); push(S.downHistory, S.down);
// 探测:偶尔一次偏慢或超时
const roll = Math.random();
push(S.probes, roll < 0.02 ? null : roll < 0.06 ? 220 + Math.random() * 80 : 58 + Math.random() * 12, PROBES);
const targets = { auto: [0.07 + S.cpu * 0.3, 0.08 + S.cpu * 0.3], cool: [0.6, 0.6], max: [1, 1], custom: [0.45, 0.45] }[S.fanMode];
S.fans = S.fans.map((rpm, i) => {
const [lo, hi] = FAN_RANGE[i];
const target = lo + (hi - lo) * targets[i];
return Math.round(rpm + (target - rpm) * 0.35);
});
PROCESSES.forEach((p) => { p.cpu = clamp(p.cpu + (Math.random() - 0.5) * 0.2, 0, 1.2); });
NET_PROCESSES.forEach((p) => {
p.down = clamp(p.down * (0.5 + Math.random()), 0, 8 * 1024 * 1024);
p.up = clamp(p.up * (0.5 + Math.random()), 0, 1024 * 1024);
});
}
// ── 格式化 ────────────────────────────────────────────────
const pct = (v) => `${Math.round(clamp(v, 0, 1) * 100)}%`;
function rate(bps) {
const units = ["B/s", "KB/s", "MB/s", "GB/s"];
let value = Math.max(0, bps), i = 0;
while (value >= 999.5 && i < units.length - 1) { value /= 1024; i++; }
const text = i >= 2 && value < 9.95 ? value.toFixed(1) : String(Math.round(value));
return `${text} ${units[i]}`;
}
function bytes(n, decimal = true) {
const step = decimal ? 1000 : 1024;
const units = ["B", "KB", "MB", "GB", "TB"];
let value = n, i = 0;
while (value >= step && i < units.length - 1) { value /= step; i++; }
return i === 0 ? `${Math.round(value)} B` : `${value >= 100 ? Math.round(value) : value.toFixed(1)} ${units[i]}`;
}
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
const splitRate = (bps) => { const [n, u] = rate(bps).split(" "); return `${n}${u}`; };
// ── SVG 图形 ──────────────────────────────────────────────
function barsSVG(values, { w = 190, h = 40, count = 20, cls = "fill-primary" } = {}) {
const recent = values.slice(-count);
const gap = 2, bw = (w - gap * (count - 1)) / count;
let out = "";
for (let i = 0; i < count; i++) {
const v = recent[i - (count - recent.length)];
const x = i * (bw + gap);
if (v === undefined) { out += ``; continue; }
const bh = Math.max(2, h * clamp(v, 0, 1));
out += ``;
}
return ``;
}
function points(values, w, top, span, max, invert = false) {
const step = w / (HISTORY - 1);
return values.map((v, i) => {
const x = w - (values.length - 1 - i) * step;
const offset = span * clamp(v / max, 0, 1);
return [x, invert ? top + offset : top + span - offset];
});
}
const pathOf = (pts) => pts.map(([x, y], i) => `${i ? "L" : "M"}${x.toFixed(1)},${y.toFixed(1)}`).join("");
function lineSVG(values, { w = 190, h = 40, max = 1, cls = "primary" } = {}) {
const d = pathOf(points(values, w, 1, h - 2, max));
return ``;
}
function dualSVG(up, down, { w = 190, h = 40 } = {}) {
const max = Math.max(...up, ...down, 64 * 1024) * 1.1;
const du = pathOf(points(up, w, 1, h - 2, max)), dd = pathOf(points(down, w, 1, h - 2, max));
return ``;
}
/** 上下镜像:上半上传、下半下载,各自按峰值缩放 */
function mirroredSVG(up, down, { w = 296, h = 72 } = {}) {
const mid = h / 2;
const du = pathOf(points(up, w, 1, mid - 1, Math.max(...up, 64 * 1024)));
const dd = pathOf(points(down, w, mid, mid - 1, Math.max(...down, 64 * 1024), true));
const first = w - (up.length - 1) * (w / (HISTORY - 1));
return ``;
}
function ringSVG(fraction, size, width, cls) {
const r = (size - width) / 2, c = 2 * Math.PI * r;
return ``;
}
// ── 图标(线性 SVG,不使用 emoji) ──────────────────────────
const ICON = {
cpu: '',
mem: '',
net: '',
gpu: '',
temp: '',
dash: '',
list: '',
cup: '',
laptop: '',
fan: '',
clean: '',
window: '',
gear: '',
moon: '',
power: '',
refresh: '',
warn: '',
check: '',
apple: '',
};
// ── 菜单栏读数:八种风格 ────────────────────────────────────
const STYLES = [
["stacked", "双行文字", "小标签在上、数值在下,最紧凑"],
["inline", "单行文字", "标签与数值同一行,最易读"],
["icon", "图标", "用系统图标代替文字标签"],
["ring", "圆环", "圆环表示当前占用比例"],
["pie", "饼图", "饼图表示当前占用比例"],
["history", "柱状历史", "10 根柱子是最近 10 次采样(约 20 秒)的变化"],
["meter", "电量条", "竖向电量条表示当前占用比例"],
["dot", "状态圆点", "绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上)"],
];
const MINI = {
cpu: '',
mem: '',
gpu: '',
temp: '',
};
function shape(style, value, history) {
const f = clamp(value, 0, 1);
switch (style) {
case "ring": {
const r = 5.5, c = 2 * Math.PI * r;
return ``;
}
case "pie": {
const a = f * 2 * Math.PI, x = 6.5 + 6.5 * Math.sin(a), y = 6.5 - 6.5 * Math.cos(a);
const wedge = f >= 0.999 ? ''
: ``;
return ``;
}
case "history": {
const recent = history.slice(-10);
const bars = recent.map((v, i) => { const h = Math.max(1, 13 * clamp(v, 0, 1)); return ``; }).join("");
return ``;
}
case "meter": {
const h = Math.max(3.4, 13 * f);
return ``;
}
case "dot": {
const cls = f >= 0.85 ? "fill-error" : f >= 0.6 ? "fill-warning" : "fill-success";
return ``;
}
default: return "";
}
}
function metricHTML(key, label, value, history, style, text = pct(value)) {
const stacked = `${label}${text}`;
if (style === "stacked") return `${stacked}`;
if (style === "inline") return `${label}${text}`;
if (style === "icon") return `${MINI[key]}${text}`;
if (key === "temp") return `${stacked}`;
return `${shape(style, value, history)}${stacked}`;
}
function itemHTML(key, style) {
switch (key) {
case "cpu": return metricHTML("cpu", "CPU", S.cpu, S.cpuHistory, style);
case "mem": return metricHTML("mem", "RAM", S.mem, S.memHistory, style);
case "gpu": return metricHTML("gpu", "GPU", S.gpu, S.gpuHistory, style);
case "temp": return metricHTML("temp", "TEMP", 0, [], style, `${Math.round(S.cpuTemp)}°`);
case "net": return `${rate(S.up)}${rate(S.down)}`;
default: return "";
}
}
// ── 共用片段 ──────────────────────────────────────────────
const card = (head, body, extra = "") => `
`;
const row = (label, value) => `${label}${value}
`;
const copyable = (text) => ``;
const tempTone = (t) => (t >= 80 ? "op-chip--warning" : "op-chip--primary");
const level = (v) => (v < 0.3 ? "低负载" : v < 0.7 ? "中等负载" : "高负载");
const loadRing = (v) => (v >= 0.85 ? "ring-error" : v >= 0.6 ? "ring-warning" : "ring-primary");
function procRows(list, cols) {
return `${list.map((p) => `
${esc(p.name)}${cols.map((c) => `${c(p)}`).join("")}
`).join("")}
`;
}
const cpuText = (p) => `${(p.cpu * 100).toFixed(1)}%`;
const memText = (p) => bytes(p.mem, false);
// ── 各指标内容(弹窗与主窗口共用,wide 为主窗口) ─────────────
const CONTENT = {
cpu(wide) {
const cores = S.cores.slice(12).concat(S.cores.slice(0, 12));
const coreBars = cores.map((v, i) => `${i === 6 ? '' : ""}`).join("");
const hero = `${ringSVG(S.cpu, 56, 6, loadRing(S.cpu))}
${pct(S.cpu)}
用户 ${Math.round(S.cpu * 72)}%系统 ${Math.round(S.cpu * 28)}%空闲 ${pct(1 - S.cpu)}
${Math.round(S.cpuTemp)}°C `;
const history = card(`负载历史 最近 60 秒`, barsSVG(S.cpuHistory, { count: wide ? 30 : 24, w: wide ? 600 : 296, h: wide ? 96 : 48 }));
const coresCard = card("核心负载", `${coreBars}
超级核 · 6性能核 · 12
`);
const info = card("处理器信息", row("处理器", "Apple M5 Max") + row("核心", "18 核(6 超级核 + 12 性能核)") + row("负载平均", `${(S.cpu * 18).toFixed(2)} · ${(S.cpu * 16).toFixed(2)} · ${(S.cpu * 15).toFixed(2)}`) + row("已运行", "3 天 4 小时"));
const procs = card(`高占用进程 CPU`, procRows([...PROCESSES].sort((a, b) => b.cpu - a.cpu).slice(0, 5), [cpuText]));
return wide ? `${hero}${history}${coresCard}${info}
${procs}` : hero + history + coresCard + info + procs;
},
mem(wide) {
const used = S.mem * 64;
const hero = `${ringSVG(S.mem, 56, 6, "ring-success")}
${pct(S.mem)}
${used.toFixed(1)} GB/ 64.0 GB内存压力 正常
`;
const history = card(`使用历史 最近 60 秒`, lineSVG(S.memHistory, { w: wide ? 600 : 296, h: wide ? 96 : 48 }));
const app = used * 0.78, wired = used * 0.14, comp = used * 0.08;
const parts = [["bg-primary", "App 内存", app], ["bg-secondary", "联动内存", wired], ["bg-warning", "被压缩", comp], ["bg-muted", "缓存文件", 19.2], ["bg-track", "交换空间", 0.01]];
const stack = `
`;
const breakdown = card("内存构成", stack + parts.map(([cls, t, gb]) => `${t}${gb < 1 ? "12.8 MB" : `${gb.toFixed(1)} GB`}
`).join("")
+ ``);
const procs = card(`高占用进程 内存`, procRows([...PROCESSES].sort((a, b) => b.mem - a.mem).slice(0, 5), [memText]));
return wide ? `${hero}${history}${breakdown}${procs}
` : hero + history + breakdown + procs;
},
net(wide) {
const hero = ``;
const history = card(`流量历史 最近 60 秒`,
`峰值 ↑ ${rate(Math.max(...S.upHistory, 64 * 1024))}${mirroredSVG(S.upHistory, S.downHistory, { w: wide ? 600 : 296, h: wide ? 120 : 72 })}峰值 ↓ ${rate(Math.max(...S.downHistory, 64 * 1024))}
`);
const count = wide ? 120 : 60;
const recent = S.probes.slice(-count);
const cells = fill(count - recent.length, () => '').join("")
+ recent.map((v) => `= 200 ? "is-slow" : "is-ok"}">`).join("");
const ok = recent.filter((v) => v != null);
const jitter = ok.slice(-20).reduce((a, v, i, arr) => (i ? a + Math.abs(v - arr[i - 1]) : a), 0) / Math.max(1, Math.min(ok.length, 20) - 1);
const loss = recent.length ? recent.filter((v) => v == null).length / recent.length : 0;
const last = [...recent].reverse().find((v) => v != null);
const probe = card(`连接探测 1.1.1.1 · 每 2 秒`,
`${cells}
延迟${last ? Math.round(last) : "—"} ms
抖动${jitter.toFixed(1)} ms
丢包${Math.round(loss * 100)}%
`);
const iface = card("接口", row("接口", "Wi‑Fi(en0)") + row("状态", '已连接')
+ row("物理地址", copyable("a4:83:e7:12:34:56")) + row("信号强度", "-42 dBm · 极好") + row("传输速率", "1200 Mbps")
+ row("开机后下载", "18.4 GB") + row("开机后上传", "2.1 GB"));
const addresses = card(`IP 地址 `,
row("本地 IPv4", copyable("192.168.1.20")) + row("本地 IPv6", copyable("2001:db8::20")) + row("路由器", copyable("192.168.1.1"))
+ row("公网 IPv4", copyable("203.0.113.24")) + row("公网 IPv6", copyable("2001:db8:85a3::8a2e"))
+ row("归属地", '
中国 · Shanghai')
+ row("ASN", copyable("AS64500")) + row("网络运营方", "EXAMPLE NETWORK"));
const current = DNS_PRESETS.find(([id]) => id === S.dns);
const servers = S.dns === "auto" ? "192.168.1.1" : current[2].join("
");
const chips = DNS_PRESETS.map(([id, title, , short]) => ``).join("")
+ ``;
const manual = S.dnsManual ? `` : "";
const dns = card(`DNS Wi‑Fi`,
row("正在使用", servers) + row("配置方式", S.dns === "auto" ? "自动(由路由器分配)" : current[1])
+ `${chips}
${manual}
${S.dnsNote ? `${esc(S.dnsNote)}
` : ""}`);
const procs = card(`高占用进程 下载 · 上传`,
procRows([...NET_PROCESSES].sort((a, b) => b.down + b.up - a.down - a.up), [(p) => rate(p.down), (p) => rate(p.up)]));
return wide
? `${hero}${history}${probe}
${iface}${addresses}
${dns}${procs}
`
: hero + history + probe + iface + addresses + dns + procs;
},
gpu(wide) {
const hero = `${ringSVG(S.gpu, 56, 6, loadRing(S.gpu))}
${pct(S.gpu)}
Apple M5 Max
${Math.round(S.gpuTemp)}°C `;
return hero + card(`使用历史 最近 60 秒`, lineSVG(S.gpuHistory, { cls: "secondary", w: wide ? 600 : 296, h: wide ? 96 : 48 }))
+ card("显卡信息", row("型号", "Apple M5 Max") + row("核心数", "40") + row("最高温度", `${Math.round(S.gpuTemp)}°C`));
},
temp(wide) {
if (wide) return thermalPage();
const temps = [["CPU", S.cpuTemp], ["GPU", S.gpuTemp], ["内存", S.memTemp], ["电池", S.batteryTemp], ["掌托", S.palmTemp]]
.map(([t, v]) => `${t}${Math.round(v)}°C
`).join("");
const fans = S.fans.map((rpm, i) => `风扇 ${i + 1}${rpm.toLocaleString("en-US")} RPM
`).join("");
const modes = [["auto", "自动"], ["cool", "降温"], ["max", "强冷"]].map(([m, t]) => ``).join("");
return `${Math.round(S.cpuTemp)}°C
${Math.max(...S.fans).toLocaleString("en-US")}RPM
`
+ card(`温度 最高`, temps)
+ card(`风扇 ${S.fanMode === "auto" ? "由 macOS 调节" : "OpenStats 控制中"}`, `${fans}${modes}
`);
},
};
const ITEM_META = {
cpu: ["CPU", ICON.cpu], mem: ["内存", ICON.mem], net: ["网络", ICON.net], gpu: ["GPU", ICON.gpu], temp: ["温度", ICON.temp],
};
// ── 菜单栏与详情弹窗 ───────────────────────────────────────
const itemsBox = document.getElementById("mbItems");
const popover = document.getElementById("osPopover");
/** 按钮保持不变,只替换里面的读数:刷新时整个按钮被替换会让恰好落在刷新瞬间的点击丢失 */
function renderMenubar() {
if (!itemsBox) return;
const items = ITEM_ORDER.filter((k) => S.items.includes(k));
const keys = items.length ? items : ["window"];
const current = [...itemsBox.children].map((b) => b.dataset.item || "window");
if (current.join() !== keys.join()) {
itemsBox.innerHTML = keys.map((key) => key === "window"
? ``
: ``).join("");
}
[...itemsBox.children].forEach((button, index) => {
const key = keys[index];
if (key === "window") { button.innerHTML = ICON.dash; return; }
const awake = index === 0 && S.keepAwake ? `${ICON.cup}` : "";
button.innerHTML = awake + itemHTML(key, S.style);
button.setAttribute("aria-expanded", String(S.popover === key));
});
}
function renderPopover() {
if (!S.popover) { popover.hidden = true; return; }
const [title, icon] = ITEM_META[S.popover];
const previous = popover.querySelector(".op-popbody");
const scroll = previous && previous.dataset.item === S.popover ? previous.scrollTop : 0;
popover.innerHTML = `${icon}${title}
${CONTENT[S.popover](false)}
`;
popover.hidden = false;
popover.querySelector(".op-popbody").scrollTop = scroll;
placePopover();
}
function placePopover() {
const anchor = itemsBox.querySelector(`[data-item="${S.popover}"]`);
if (!anchor) return;
const margin = 8;
const width = Math.min(320, window.innerWidth - margin * 2);
const rect = anchor.getBoundingClientRect();
const left = clamp(rect.left + rect.width / 2 - width / 2, margin, window.innerWidth - width - margin);
popover.style.left = `${left}px`;
popover.style.width = `${width}px`;
}
function openPopover(key) {
S.popover = S.popover === key ? null : key;
renderMenubar();
renderPopover();
}
function closePopover() {
if (!S.popover) return;
S.popover = null;
renderMenubar();
renderPopover();
}
// ── 主窗口 ────────────────────────────────────────────────
const appWindow = document.getElementById("osWindow");
const PAGES = [
["监控", [["overview", "仪表盘", ICON.dash], ["cpu", "CPU", ICON.cpu], ["gpu", "GPU", ICON.gpu], ["mem", "内存", ICON.mem], ["net", "网络", ICON.net], ["temp", "温度与风扇", ICON.fan]]],
["工具", [["processes", "进程", ICON.list], ["keepAwake", "防休眠", ICON.cup], ["cleaner", "清理", ICON.clean]]],
];
const pageTitle = (id) => PAGES.flatMap(([, list]) => list).find(([p]) => p === id)[1];
function overviewPage() {
const maxFan = Math.max(...S.fans);
const fanPct = Math.round((S.fans[0] / FAN_RANGE[0][1] + S.fans[1] / FAN_RANGE[1][1]) / 2 * 100);
const cores = S.cores.slice(12).concat(S.cores.slice(0, 12));
const coreBars = cores.map((v, i) => `${i === 6 ? '' : ""}`).join("");
const fanModes = [["auto", "自动"], ["cool", "降温"], ["max", "强冷"]].map(([m, t]) =>
``).join("");
return `
${ICON.apple} M5 Max64 GBmacOS 27.0
已运行 3 天 4 小时MacBook Pro 16 英寸
${card(`CPU
${Math.round(S.cpuTemp)}°C`,
`
${Math.round(S.cpu * 100)}%
${barsSVG(S.cpuHistory)}`)}
${card(`GPU
${Math.round(S.gpuTemp)}°C`,
`
${Math.round(S.gpu * 100)}%
${lineSVG(S.gpuHistory, { cls: "secondary" })}`)}
${card(`内存
压力 正常`,
`
${Math.round(S.mem * 100)}%
${lineSVG(S.memHistory)}`)}
${card(`磁盘
2 TB`,
`
16%
`)}
${card(`网络
Wi‑Fi`,
`
${splitRate(S.up + S.down)}
${dualSVG(S.upHistory, S.downHistory)}`)}
${card(`风扇
转速 ${fanPct}%`,
`
${maxFan.toLocaleString("en-US")}RPM
${fanModes}
`)}
${card(`核心负载
用户 ${Math.round(S.cpu * 72)}%系统 ${Math.round(S.cpu * 28)}%`,
`
${coreBars}
超级核 · 6性能核 · 12
`)}
${card(`电池
健康 100%`,
`
${ringSVG(1, 56, 6, "ring-success")}
`)}
${card(`高占用进程 CPU · 内存`, procRows([...PROCESSES].sort((a, b) => b.cpu - a.cpu).slice(0, 5), [cpuText, memText]))}
${card("快捷开关", `
`)}
`;
}
function processesPage() {
const list = [...PROCESSES].sort((a, b) => (S.sort === "cpu" ? b.cpu - a.cpu : b.mem - a.mem));
return `
${card(`进程 占用最高的 ${list.length} 个`, procRows(list, [cpuText, memText]))}
`;
}
function thermalPage() {
const temps = [["CPU", S.cpuTemp], ["GPU", S.gpuTemp], ["内存", S.memTemp], ["电池", S.batteryTemp], ["掌托", S.palmTemp]]
.map(([t, v]) => ``).join("");
const fans = S.fans.map((rpm, i) => {
const [lo, hi] = FAN_RANGE[i];
return ``;
}).join("");
const modes = [["auto", "自动"], ["cool", "降温"], ["max", "强冷"], ["custom", "自定义"]]
.map(([m, t]) => ``).join("");
return `
${card(`温度
41 个传感器`, temps)}
${card(`风扇
${S.fanMode === "auto" ? "系统自动" : "OpenStats 控制"}`,
`${fans}
${modes}
`)}
`;
}
function keepAwakePage() {
const modes = [["display", "屏幕保持常亮", "屏幕和系统都不会因闲置而关闭或休眠"], ["system", "仅系统不休眠", "屏幕可按设置关闭,后台任务继续运行"]]
.map(([m, t, d]) => ``).join("");
const durations = [[15, "15 分钟"], [60, "1 小时"], [120, "2 小时"], [300, "5 小时"], [0, "不限"]]
.map(([m, t]) => ``).join("");
return `
模式
${modes}
持续时间
${durations}
合盖后继续运行
${ICON.warn}合盖运行时散热变差,请勿放入包中。使用电池且电量低于 20% 时会自动关闭。
`;
}
function cleanerPage() {
const selected = CLEAN_RULES.filter((r) => r.on && !r.blocked && !S.cleaned);
const total = selected.reduce((a, r) => a + r.size, 0);
const cats = [...new Set(CLEAN_RULES.map((r) => r.cat))];
const groups = cats.map((cat) => card(cat, CLEAN_RULES.filter((r) => r.cat === cat).map((r) => `
${r.title}${S.cleaned && r.on ? "无需清理" : r.detail}
${r.blocked || (S.cleaned && r.on) ? "" : bytes(r.size)}
`).join(""))).join("");
let action;
if (S.cleaned) {
action = `${ICON.check}已释放 ${bytes(CLEAN_RULES.filter((r) => r.on && !r.blocked).reduce((a, r) => a + r.size, 0))}(网页演示,没有删除任何文件)
`;
} else if (S.confirming) {
action = `${ICON.warn}将清理 ${selected.length} 类项目,共 ${bytes(total)}。缓存与日志直接删除,下载内容移到废纸篓。
`;
} else {
action = `
`;
}
return `${card(`磁盘清理 扫描于 刚刚`, `
${bytes(S.cleaned ? 0 : CLEAN_RULES.filter((r) => !r.blocked).reduce((a, r) => a + r.size, 0))}可清理
${action}`)}
${groups}
`;
}
const WINDOW_PAGES = {
overview: overviewPage, processes: processesPage, keepAwake: keepAwakePage, cleaner: cleanerPage,
cpu: () => CONTENT.cpu(true), gpu: () => CONTENT.gpu(true), mem: () => CONTENT.mem(true), net: () => CONTENT.net(true), temp: () => CONTENT.temp(true),
};
function renderWindow() {
if (!S.windowOpen) { appWindow.hidden = true; return; }
const scroller = appWindow.querySelector(".ow-content");
const scroll = scroller ? scroller.scrollTop : 0;
const nav = PAGES.map(([group, list]) => `${group}
` + list.map(([id, title, icon]) =>
``).join("")).join("");
const toggle = ITEM_META[S.page]
? `在菜单栏显示`
: "";
appWindow.innerHTML = `
${WINDOW_PAGES[S.page]()}
`;
appWindow.hidden = false;
appWindow.querySelector(".ow-content").scrollTop = scroll;
}
function openWindow(page) {
if (page) S.page = page;
S.windowOpen = true;
closePopover();
renderWindow();
document.getElementById("dockOpenStats")?.classList.add("dock__item--running");
}
// ── 交互 ──────────────────────────────────────────────────
function handle(target, event) {
const d = target.dataset;
if (d.copy) {
navigator.clipboard?.writeText(d.copy).catch(() => {});
target.textContent = "已拷贝";
target.classList.add("is-copied");
setTimeout(() => { target.textContent = d.copy; target.classList.remove("is-copied"); }, 1200);
return false;
}
if (d.item) { event.stopPropagation(); openPopover(d.item); return false; }
if (d.page) S.page = d.page;
if (d.fan) S.fanMode = d.fan;
if (d.sort) S.sort = d.sort;
if (d.awakeMode) S.awakeMode = d.awakeMode;
if (d.duration !== undefined) S.duration = Number(d.duration);
if (d.dns) { S.dns = d.dns; S.dnsManual = false; S.dnsNote = "网页演示:没有修改你电脑的 DNS。"; }
if (d.rule) { const r = CLEAN_RULES.find((x) => x.id === d.rule); r.on = !r.on; S.confirming = false; }
switch (d.action) {
case "theme": window.OpenStatsSite?.toggleTheme(); break;
case "settings": closePopover(); document.getElementById("styles").scrollIntoView(); return false;
case "window": openWindow(S.popover === "temp" ? "temp" : S.popover || null); return false;
case "window-close": S.windowOpen = false; renderWindow(); return false;
case "menubar-item": {
S.items = S.items.includes(S.page) ? S.items.filter((k) => k !== S.page) : [...S.items, S.page];
write("os-items", S.items.join(","));
break;
}
case "dns-manual": S.dnsManual = !S.dnsManual; break;
case "flush": S.dnsNote = "DNS 缓存已刷新(网页演示)"; break;
case "lookup": S.dnsNote = ""; break;
case "purge": target.textContent = "已释放 1.2 GB(网页演示)"; target.disabled = true; return false;
case "awake": S.keepAwake = !S.keepAwake; if (!S.keepAwake) S.lidClosed = false; break;
case "lid": S.lidClosed = !S.lidClosed; if (S.lidClosed) S.keepAwake = true; break;
case "clean": S.confirming = true; break;
case "cancel": S.confirming = false; break;
case "confirm": S.confirming = false; S.cleaning = true; setTimeout(() => { S.cleaning = false; S.cleaned = true; renderWindow(); }, 900); break;
case "rescan": S.cleaned = false; S.confirming = false; break;
}
return true;
}
document.addEventListener("click", (event) => {
const target = event.target.closest("[data-item], [data-page], [data-fan], [data-sort], [data-awake-mode], [data-duration], [data-dns], [data-rule], [data-action], [data-copy]");
const inPopover = popover.contains(event.target);
const inWindow = appWindow.contains(event.target);
const inBar = itemsBox?.contains(event.target);
if (target && (inPopover || inWindow || inBar)) {
if (inPopover || inWindow) event.stopPropagation();
if (handle(target, event)) { renderMenubar(); renderPopover(); renderWindow(); }
return;
}
if (S.popover && !inPopover) closePopover();
});
document.addEventListener("submit", (event) => {
const form = event.target.closest('[data-form="dns"]');
if (!form) return;
event.preventDefault();
const input = form.elements.servers;
const parts = input.value.split(/[\s,,]+/).filter(Boolean);
const ipv4 = /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
const valid = parts.length > 0 && parts.length <= 6 && parts.every((p) => ipv4.test(p) || /^[0-9a-f:]+$/i.test(p) && p.includes(":"));
S.dnsNote = valid ? `网页演示:会把 DNS 设为 ${parts.join("、")},没有修改你的电脑。` : "地址格式不正确,只接受 IPv4 / IPv6 地址。";
if (valid) S.dnsManual = false;
renderPopover();
renderWindow();
});
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePopover(); });
window.addEventListener("resize", () => { if (S.popover) placePopover(); });
document.getElementById("dockOpenStats")?.addEventListener("click", (e) => { e.stopPropagation(); openWindow(); });
document.getElementById("heroOpenWindow")?.addEventListener("click", (e) => { e.stopPropagation(); openWindow("net"); });
// 按住窗口顶栏或侧边栏顶部拖动(仅桌面宽度)
appWindow.addEventListener("pointerdown", (event) => {
const handleBar = event.target.closest(".ow-drag");
if (!handleBar || event.target.closest("button") || window.innerWidth < 900) return;
const start = { x: event.clientX, y: event.clientY, left: appWindow.offsetLeft, top: appWindow.offsetTop };
const move = (e) => {
appWindow.style.left = `${start.left + e.clientX - start.x}px`;
appWindow.style.top = `${Math.max(28, start.top + e.clientY - start.y)}px`;
appWindow.style.transform = "none";
};
const up = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up);
});
// ── 刘海岛、风格选择 ───────────────────────────────────────
const bind = (key, fn) => document.querySelectorAll(`[data-bind="${key}"]`).forEach(fn);
function renderIsland() {
bind("island-cpu", (el) => { el.textContent = `CPU ${pct(S.cpu)} · ${Math.round(S.cpuTemp)}°`; });
const tiles = { cpu: [S.cpu, pct(S.cpu)], gpu: [S.gpu, pct(S.gpu)], mem: [S.mem, pct(S.mem)], temp: [S.cpuTemp / 110, `${Math.round(S.cpuTemp)}°`] };
for (const [k, [f, text]] of Object.entries(tiles)) {
bind(`island-${k}-value`, (el) => { el.textContent = text; });
bind(`island-${k}-bar`, (el) => { el.style.width = `${Math.round(clamp(f, 0, 1) * 100)}%`; });
}
bind("island-up", (el) => { el.textContent = rate(S.up); });
bind("island-down", (el) => { el.textContent = rate(S.down); });
bind("island-fan", (el) => { el.textContent = `风扇 ${Math.max(...S.fans).toLocaleString("en-US")} RPM`; });
}
const picker = document.getElementById("stylePicker");
if (picker) {
picker.innerHTML = STYLES.map(([id, title]) =>
``).join("");
}
function renderStyles() {
if (!picker) return;
picker.querySelectorAll("[data-style]").forEach((chip) => chip.setAttribute("aria-checked", String(chip.dataset.style === S.style)));
const preview = ["cpu", "mem", "gpu"].map((k) => `${itemHTML(k, S.style)}`).join("");
document.getElementById("stylePreviewLight").innerHTML = preview;
document.getElementById("stylePreviewDark").innerHTML = preview;
document.getElementById("styleCaption").textContent = STYLES.find(([id]) => id === S.style)[2];
}
picker?.addEventListener("click", (e) => {
const chip = e.target.closest("[data-style]");
if (!chip) return;
S.style = chip.dataset.style;
write("os-style", S.style);
renderStyles();
renderMenubar();
});
const island = document.getElementById("island");
const setIsland = (open) => {
island.classList.toggle("is-open", open);
island.setAttribute("aria-expanded", String(open));
island.querySelector(".island__expanded").setAttribute("aria-hidden", String(!open));
};
island.addEventListener("mouseenter", () => setIsland(true));
island.addEventListener("mouseleave", () => setIsland(false));
island.addEventListener("click", (e) => { e.stopPropagation(); setIsland(!island.classList.contains("is-open")); });
// ── 循环 ──────────────────────────────────────────────────
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const editing = (el) => el.contains(document.activeElement) && document.activeElement.matches("input");
// 按下后 1 秒内不重绘:触屏没有悬停状态,按下与抬起之间换掉按钮会让点击落空
let holdUntil = 0;
for (const el of [popover, appWindow]) {
el.addEventListener("pointerdown", () => { holdUntil = Date.now() + 1000; }, true);
}
const busy = (el) => Date.now() < holdUntil || el.matches(":hover") || editing(el);
function frame() {
tick();
renderMenubar();
renderIsland();
renderStyles();
if (S.popover && !busy(popover)) renderPopover();
if (S.windowOpen && !busy(appWindow)) renderWindow();
}
frame();
if (!reduced) setInterval(frame, 1500);
})();