/* 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 `${out}`; } 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 `${wedge}`; } 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 `${bars}`; } 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 = "") => `
${head}
${body}
`; 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 = `
${splitRate(S.down)}
下载
${splitRate(S.up)}
上传
`; 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 Max40 核图形处理器
${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 `
CPU 最高
${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 `
100各项指标正常
${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)}
${level(S.cpu)} · 负载 ${(S.cpu * 18).toFixed(1)}/18
`)} ${card(`GPU ${Math.round(S.gpuTemp)}°C`, `
${Math.round(S.gpu * 100)}%
${lineSVG(S.gpuHistory, { cls: "secondary" })}
${level(S.gpu)} · 40 核
`)} ${card(`内存 压力 正常`, `
${Math.round(S.mem * 100)}%
${lineSVG(S.memHistory)}
${(S.mem * 64).toFixed(1)} GB / 64.0 GB
`)}
${card(`磁盘 2 TB`, `
16%
可用 1.7 TB
`)} ${card(`网络 Wi‑Fi`, `
${splitRate(S.up + S.down)}
${dualSVG(S.upHistory, S.downHistory)}
↑ ${rate(S.up)} ↓ ${rate(S.down)}
`)} ${card(`风扇 转速 ${fanPct}%`, `
${maxFan.toLocaleString("en-US")}RPM
${S.fanMode === "auto" ? "由 macOS 调节" : "OpenStats 控制中"}
${fanModes}
`)}
${card(`核心负载 用户 ${Math.round(S.cpu * 72)}%系统 ${Math.round(S.cpu * 28)}%`, `
${coreBars}
超级核 · 6性能核 · 12
`)} ${card(`电池 健康 100%`, `
100%
已充满 · 电源适配器
循环 28 次
${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]))}
仅列出当前用户有权限读取的进程,CPU 以单核满载为 100%。
`; } function 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) => { const [lo, hi] = FAN_RANGE[i]; return `
${i ? "右侧" : "左侧"}
${rpm}
`; }).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 `
防休眠 ${S.keepAwake ? "已开启 · " + (S.duration ? `${S.duration} 分钟后恢复` : "不限时") : "未开启,Mac 按系统设置休眠"}
模式
${modes}
持续时间
${durations}
合盖后继续运行
合上屏幕时 Mac 不进入睡眠
${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))}可清理
Macintosh HD 可用 1.7 TB · 共 2.0 TB
${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 = `

${pageTitle(S.page)}

${toggle}
${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); })();