diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..32fe78b --- /dev/null +++ b/web/app.js @@ -0,0 +1,68 @@ +/* OpenStats 官网 · 页面外壳:主题切换、菜单栏下拉菜单与时钟 */ +(() => { + "use strict"; + + const root = document.documentElement; + const THEME_KEY = "os-theme"; + const media = window.matchMedia("(prefers-color-scheme: dark)"); + + function saved() { + try { return localStorage.getItem(THEME_KEY); } catch { return null; } + } + function applyTheme(theme, persist) { + root.dataset.theme = theme; + if (!persist) return; + try { localStorage.setItem(THEME_KEY, theme); } catch { /* 隐私模式下不可用,忽略 */ } + } + + applyTheme(saved() || (media.matches ? "dark" : "light"), false); + // 没有手动选过时跟随系统 + media.addEventListener("change", (e) => { if (!saved()) applyTheme(e.matches ? "dark" : "light", false); }); + + window.OpenStatsSite = { + toggleTheme() { applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true); }, + }; + document.getElementById("themeToggle")?.addEventListener("click", (e) => { + e.stopPropagation(); + window.OpenStatsSite.toggleTheme(); + }); + + // ── 菜单栏下拉菜单:点击展开,展开后悬停切换,点外部或 Esc 收起 ── + const menus = [...document.querySelectorAll(".mb-menu")]; + let openMenu = null; + + function setMenu(menu) { + for (const m of menus) { + const open = m === menu; + m.classList.toggle("is-open", open); + m.querySelector(".mb-title")?.setAttribute("aria-expanded", String(open)); + } + openMenu = menu; + } + + for (const menu of menus) { + const title = menu.querySelector(".mb-title"); + title?.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + setMenu(openMenu === menu ? null : menu); + }); + menu.addEventListener("mouseenter", () => { if (openMenu && openMenu !== menu) setMenu(menu); }); + menu.querySelector(".mb-drop")?.addEventListener("click", () => setMenu(null)); + } + document.addEventListener("click", (e) => { if (openMenu && !openMenu.contains(e.target)) setMenu(null); }); + document.addEventListener("keydown", (e) => { if (e.key === "Escape") setMenu(null); }); + + // ── 时钟:按访客本地时间显示 ── + const clock = document.getElementById("menubarClock"); + const format = new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric", weekday: "short", hour: "2-digit", minute: "2-digit", hour12: false }); + function renderClock() { + if (!clock) return; + const now = new Date(); + const parts = Object.fromEntries(format.formatToParts(now).map((p) => [p.type, p.value])); + clock.textContent = `${parts.month}月${parts.day}日 ${parts.weekday} ${parts.hour}:${parts.minute}`; + clock.dateTime = now.toISOString(); + } + renderClock(); + setInterval(renderClock, 15000); +})(); diff --git a/web/assets/apple-touch-icon.png b/web/assets/apple-touch-icon.png new file mode 100644 index 0000000..381527b Binary files /dev/null and b/web/assets/apple-touch-icon.png differ diff --git a/web/assets/cleaner.png b/web/assets/cleaner.png new file mode 100644 index 0000000..09f9434 Binary files /dev/null and b/web/assets/cleaner.png differ diff --git a/web/assets/favicon-32.png b/web/assets/favicon-32.png new file mode 100644 index 0000000..b16ea0b Binary files /dev/null and b/web/assets/favicon-32.png differ diff --git a/web/assets/flags/cn.svg b/web/assets/flags/cn.svg new file mode 100644 index 0000000..10d3489 --- /dev/null +++ b/web/assets/flags/cn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/assets/icon.png b/web/assets/icon.png new file mode 100644 index 0000000..1ae4185 Binary files /dev/null and b/web/assets/icon.png differ diff --git a/web/assets/popover-network-dark.png b/web/assets/popover-network-dark.png new file mode 100644 index 0000000..3070f7d Binary files /dev/null and b/web/assets/popover-network-dark.png differ diff --git a/web/assets/popover-network-light.png b/web/assets/popover-network-light.png new file mode 100644 index 0000000..1f02aee Binary files /dev/null and b/web/assets/popover-network-light.png differ diff --git a/web/assets/settings-menubar.png b/web/assets/settings-menubar.png new file mode 100644 index 0000000..b72daa2 Binary files /dev/null and b/web/assets/settings-menubar.png differ diff --git a/web/assets/thermal.png b/web/assets/thermal.png new file mode 100644 index 0000000..0f645de Binary files /dev/null and b/web/assets/thermal.png differ diff --git a/web/assets/window-dark.png b/web/assets/window-dark.png new file mode 100644 index 0000000..d7dad6a Binary files /dev/null and b/web/assets/window-dark.png differ diff --git a/web/assets/window-light.png b/web/assets/window-light.png new file mode 100644 index 0000000..10a338a Binary files /dev/null and b/web/assets/window-light.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..b458996 --- /dev/null +++ b/web/index.html @@ -0,0 +1,410 @@ + + + + + +OpenStats — Mac 的状态,抬眼就看见 + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+ + 欢迎使用 OpenStats +
+
+ OpenStats 图标 +

Mac 的状态,抬眼就看见

+

CPU、GPU、内存、网络与温度常驻菜单栏,还能调风扇、防休眠、一键清理。所有数据在本机读取,没有账号与统计上报。

+ +

macOS 14 及以上 · Apple Silicon · MIT 开源

+

点右上角菜单栏里的 CPU、内存或网速,看看每一项的详情 ↗

+ +
+
+ + + + + + + + + 继续了解 ↓ +
+ +
+
+
+
+

每个指标一个图标,点开就是详情

+

原生 Swift 编写,只显示菜单栏时约 0.3% CPU、16 MB 内存。

+
+
    +
  • +

    八种菜单栏风格

    +

    双行文字、圆环、饼图、柱状历史……整体选一种统一套用,也能给单个指标单独指定。

    +
  • +
  • +

    单项详情,按需显示

    +

    CPU、内存、网络、GPU、温度各有一个窄弹窗,显示哪些区块由你勾选;另有侧边栏主窗口看全部。

    +
  • +
  • +

    网络详情与 DNS

    +

    本地与公网 IPv4 / IPv6、归属地与 ASN、连接探测格子,一键刷新 DNS 或切换 Cloudflare、Google、腾讯、阿里云。

    +
  • +
  • +

    风扇调速与防休眠

    +

    自动、降温、强冷、自定义风扇;屏幕常亮、合上屏幕继续运行,过热或电量过低时自动恢复。

    +
  • +
  • +

    安全的清理

    +

    缓存、日志、浏览器与开发者缓存先扫描预览再清理,钥匙串、Cookie 与历史记录一律不碰。

    +
  • +
  • +

    数据留在本机

    +

    指标来自本机内核、IOKit 与 SMC,没有账号、没有统计上报;公网 IP 查询与连接探测都可关闭。

    +
  • +
+
+
+ +
+
+
+

菜单栏风格,由你挑

+

点一下下面的风格,页面顶部菜单栏里的 OpenStats 会立即换成这个样子。

+
+
+
+
+
+

+
+
+
+ +
+
+
+

网络,一眼看到底

+
    +
  • 上下镜像的流量历史,上传绿、下载蓝
  • +
  • 连接探测格子:每 1 / 2 / 5 秒 ping 一次,延迟、抖动、丢包一目了然
  • +
  • 本地与公网 IPv4 / IPv6、路由器、Wi‑Fi 信号,点一下就拷贝
  • +
  • 归属地显示国旗、城市、ASN 与网络运营方
  • +
  • 一键刷新 DNS 缓存,一键切换 Cloudflare、Google、腾讯 DNSPod、阿里云,或手动填写
  • +
  • 各进程的上传与下载速率
  • +
+

公网 IP 查询与连接探测都可以在设置里关闭。修改 DNS 需要管理员权限。

+
+
+ OpenStats 网络详情弹窗 + OpenStats 网络详情弹窗,深色 +
+
+
+ +
+
+
+

白底蓝,黑底蓝,一套外观

+

主窗口、设置和弹窗共用同一底色,侧边栏与标题栏不分栏着色;窗口宽高都能拖动调整。

+
+
+
OpenStats 主窗口仪表盘,浅色
浅色
+
OpenStats 主窗口仪表盘,深色
深色
+
+
+
+ +
+
+
+

热了就吹,要跑就别睡

+ + + + + + + + +
风扇模式作用
自动交还 macOS 温控
降温最低到最高转速之间的 60%
强冷最高转速
自定义滑块自由设定
+

合盖后继续运行适合下载、渲染和远程连接。使用电池且电量低于下限时会自动关闭,避免在包里过热。

+

风扇与合盖运行需要安装一次辅助工具。它只提供固定的几个操作,不执行任意命令,应用退出时自动恢复设置。

+
+
OpenStats 散热页面
+
+
+ +
+
+
+

先看清再清理

+
    +
  • 应用缓存、日志与崩溃报告
  • +
  • Chrome、Edge、Brave、Arc、Firefox、Safari 缓存
  • +
  • Xcode 编译缓存、模拟器与 npm 缓存
  • +
  • 未完成的下载、安装包、废纸篓
  • +
  • 一键刷新 DNS 缓存、释放内存
  • +
+

只处理白名单目录;正在运行的应用会跳过,浏览器需先退出;删除前每一项再校验一次,所有操作写入日志。

+
+
OpenStats 清理页面
+
+
+ +
+
+
+

数据不出这台 Mac

+

没有账号,没有统计上报。只有两项可关闭的网络功能会联网。

+
+
    +
  • 指标来源内核接口(host_processor_info、host_statistics64、sysctl)、IOKit 与 SMC
  • +
  • 账号不需要,也没有
  • +
  • 统计上报没有
  • +
  • 公网 IP打开网络详情时向 ipinfo.io 查询一次地址、归属地与 ASN,10 分钟内不重复,可关闭
  • +
  • 连接探测按设定间隔 ping 你选择的目标,只在显示网络项时运行,可关闭
  • +
  • 偏好设置保存在本机,不含个人信息
  • +
  • 源代码MIT 开源,全部公开
  • +
+
+
+ +
+
+ +

下载 OpenStats

+

使用 Developer ID 签名并经过 Apple 公证,打开时不会遇到安全拦截。

+ +

macOS 14 Sonoma 及以上 · 简体中文界面

+
+
+ +
+
+
+

更新日志

+

每天改了什么,都记在这里。

+
+ + +
+
+
未发布
+

新增 23 · 样式 5 · 修复 6

+
    +
  • 新增菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。
  • +
  • 新增菜单栏风格:双行文字、单行文字、图标、圆环、饼图、柱状历史、电量条、状态圆点八种,整体选择一种统一套用,也可以给个别指标单独指定。设置里每种风格都有用示例数据画出的预览和说明,柱状历史注明是最近 10 次采样的变化。
  • +
  • 新增网速有双行圆点、双行箭头、单行三种样式,绿色上传、蓝色下载,带完整单位。
  • +
  • 新增鼠标悬停在菜单栏图标上显示完整读数。
  • +
  • 新增菜单栏布局:每项独立(每个指标一个图标,点击弹出该项的窄详情)或合并为一个(点击打开主窗口)。每个详情里显示哪些区块可以在设置中逐个勾选。
  • +
  • 新增详情弹窗:CPU(负载历史、各核心、处理器信息、高占用进程)、内存(使用历史、内存构成与释放内存、高占用进程)、GPU、温度与风扇(各组温度、风扇转速与快捷模式)。
  • +
  • 新增网络详情:上下镜像的流量历史;连接探测格子(每 1 / 2 / 5 秒 ping 一次 Cloudflare、Google、阿里云、腾讯或路由器,显示延迟、抖动、丢包);接口、物理地址、Wi-Fi 信号与速率、VPN / 代理;本地 IPv4 / IPv6、路由器、公网 IPv4 / IPv6,点击即可拷贝;归属地显示 SVG 国旗、城市、ASN 与网络运营方;各进程上传下载速率。
  • +
  • 新增DNS:显示正在使用的 DNS 与配置方式,一键刷新 DNS 缓存,一键切换为自动、Cloudflare、Google、腾讯 DNSPod、阿里云,或手动填写地址(逐个校验为 IPv4 / IPv6)。流量经过 VPN / 代理时提示 DNS 可能被接管。
  • +
+ 还有 26 项 → +
+
+ +

查看完整更新日志 →

+
+
+ +
+
+

常见问题

+
+ 会不会很耗电? +

只显示菜单栏时只采集菜单栏用到的指标,M5 Max 实测约 0.3% CPU、16 MB 内存;锁屏、屏幕休眠和系统睡眠时暂停采样与探测。

+
+
+ 辅助工具是做什么的,安全吗? +

调风扇、合盖不睡眠、刷新 DNS、修改 DNS 服务器和释放内存需要系统权限,由一个通过 SMAppService 注册的辅助工具完成。它只提供这几个固定操作,不执行任意命令,DNS 地址会再次校验;只接受同一开发团队签名的 OpenStats 调用,应用退出时自动恢复风扇与睡眠设置。

+
+
+ 会联网吗? +

只有两项可关闭的功能会:打开网络详情时向 ipinfo.io 查询一次公网地址、归属地与 ASN(10 分钟内不重复);连接探测按你设定的间隔 ping 你选择的目标。没有账号,没有统计上报。

+
+
+ 为什么不上 App Store? +

App Store 的沙盒不允许访问 SMC、读取其他应用的缓存和安装特权辅助工具,而这正是大部分功能。

+
+
+ 清理会不会删掉重要文件? +

只清理白名单目录中的缓存与日志,钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;清理前可以展开查看每一项,下载目录中的内容会先移到废纸篓。

+
+
+ 支持 Intel Mac 吗? +

目前在 Apple Silicon 上开发和测试,Intel 机型的风扇与温度读取尚未验证。

+
+
+
+
+ + + + + + + diff --git a/web/replica.css b/web/replica.css new file mode 100644 index 0000000..ebcba62 --- /dev/null +++ b/web/replica.css @@ -0,0 +1,638 @@ +/* OpenStats 官网 · 首屏的 Mac 桌面与应用界面还原 + 面板配色、圆角、间距与应用内 DS token 一致。 + 菜单栏里的读数刻意使用 7–11px 字号:这是在还原原生菜单栏的真实尺寸,属于有意的例外。 */ + +:root { + --mb-height: 28px; + --mb-text: 14px; + --mbs-label: 7px; + --mbs-value: 10px; + --mbs-inline: 11px; + --mbs-network: 9px; + --wall-sky: #d6e4fb; + --wall-1: #b5cdf5; + --wall-2: #8fb3ee; + --wall-3: #6696e3; + --wall-4: #3f76d6; + --menubar-bg: rgba(248, 249, 250, 0.72); + --menubar-fg: #111827; + --drop-bg: #ffffff; + --drop-border: rgba(0, 0, 0, 0.1); + --window-bg: #ffffff; + --window-bar: #f3f4f6; + --dock-bg: rgba(255, 255, 255, 0.55); + --dock-border: rgba(255, 255, 255, 0.7); + + /* 应用界面:白底蓝色;卡片为浅灰实色,不加边框 */ + --p-bg: #ffffff; + --p-surface: #f3f4f6; + --p-elevated: #ffffff; + --p-hover: rgba(0, 0, 0, 0.05); + --p-border: rgba(0, 0, 0, 0.08); + --p-track: rgba(0, 0, 0, 0.06); + --p-text: #111827; + --p-text-2: #6b7280; + --p-text-3: #9ca3af; + --p-primary: #2563eb; + --p-secondary: #0d9488; + --p-success: #16a34a; + --p-warning: #d97706; + --p-error: #dc2626; + --p-up: #16a34a; + --p-down: #2563eb; +} + +:root[data-theme="dark"] { + --wall-sky: #0d1526; + --wall-1: #13213f; + --wall-2: #182c55; + --wall-3: #1e386c; + --wall-4: #264587; + --menubar-bg: rgba(17, 19, 21, 0.62); + --menubar-fg: #f3f4f6; + --drop-bg: #25282d; + --drop-border: rgba(255, 255, 255, 0.1); + --window-bg: #1d1f23; + --window-bar: #25282d; + --dock-bg: rgba(29, 31, 35, 0.6); + --dock-border: rgba(255, 255, 255, 0.12); + + --p-bg: #0b0b0d; + --p-surface: #17181b; + --p-elevated: #26282d; + --p-hover: rgba(255, 255, 255, 0.07); + --p-border: rgba(255, 255, 255, 0.09); + --p-track: rgba(255, 255, 255, 0.1); + --p-text: #f3f4f6; + --p-text-2: #9ca3af; + --p-text-3: #6b7280; + --p-primary: #3b82f6; + --p-secondary: #2dd4bf; + --p-success: #22c55e; + --p-warning: #f59e0b; + --p-error: #f87171; + --p-up: #22c55e; + --p-down: #3b82f6; +} + +/* ── 桌面 ─────────────────────────────────────────────── */ + +.desktop { + position: relative; + min-height: max(100vh, 760px); + overflow: hidden; + isolation: isolate; +} + +/* 壁纸:纯色层叠山丘,不使用渐变与位图 */ +.desktop__wall { + position: absolute; + inset: 0; + z-index: -1; + background: var(--wall-sky); +} +.desktop__wall::before, +.desktop__wall::after { + content: ""; + position: absolute; + left: -10%; + right: -10%; + border-radius: 50% 50% 0 0; +} +.desktop__wall::before { bottom: -35%; height: 80%; background: var(--wall-2); box-shadow: -420px -60px 0 40px var(--wall-1); } +.desktop__wall::after { bottom: -55%; height: 75%; background: var(--wall-4); box-shadow: 380px -90px 0 0 var(--wall-3); } + +/* ── 菜单栏 ───────────────────────────────────────────── */ + +.menubar { + position: relative; + z-index: 20; + display: flex; + align-items: center; + gap: var(--space-1); + height: var(--mb-height); + padding: 0 var(--space-3); + background: var(--menubar-bg); + color: var(--menubar-fg); + font-size: var(--mb-text); +} + +.menubar__apple { width: 14px; height: 17px; margin: 0 var(--space-3) 0 var(--space-1); } + +.mb-menu { position: relative; } + +.mb-title { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 var(--space-2); + border-radius: var(--radius-sm); + color: inherit; +} +.mb-title:hover, .mb-menu.is-open .mb-title { color: inherit; background: rgba(127, 127, 127, 0.22); } +.mb-title--app { font-weight: 600; } + +.mb-drop { + position: absolute; + top: calc(100% + var(--space-1)); + left: 0; + min-width: 220px; + padding: var(--space-1); + background: var(--drop-bg); + border: 1px solid var(--drop-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-2); + display: none; +} +.mb-menu.is-open .mb-drop { display: block; } +.mb-drop a { + display: flex; + justify-content: space-between; + padding: var(--space-1) var(--space-3); + border-radius: var(--radius-sm); + color: var(--menubar-fg); + font-size: var(--text-sm); +} +.mb-drop a:hover { background: var(--color-primary); color: #fff; } +.mb-drop hr { border: 0; border-top: 1px solid var(--drop-border); margin: var(--space-1) var(--space-2); } +.mb-drop__ext { opacity: 0.6; } + +.menubar__right { margin-left: auto; display: flex; align-items: center; gap: var(--space-3); } +.menubar__clock { font-variant-numeric: tabular-nums; } + +.mb-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 22px; + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: inherit; + cursor: pointer; +} +.mb-icon:hover { background: rgba(127, 127, 127, 0.22); } +.mb-icon__sun { display: none; } +:root[data-theme="dark"] .mb-icon__moon { display: none; } +:root[data-theme="dark"] .mb-icon__sun { display: block; } + +/* OpenStats 菜单栏读数 */ +.mb-stats { + display: inline-flex; + align-items: center; + gap: var(--space-3); + height: 24px; + padding: 0 var(--space-2); + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.mb-stats:hover, .mb-stats[aria-expanded="true"] { background: rgba(127, 127, 127, 0.22); } +.mb-items { display: flex; align-items: center; gap: var(--space-1); } + +.mbs { display: inline-flex; align-items: center; gap: var(--space-1); line-height: 1; } +.mbs__stack { display: inline-flex; flex-direction: column; align-items: flex-end; gap: 1px; } +.mbs__label { font-size: var(--mbs-label); font-weight: 500; opacity: 0.65; letter-spacing: 0.02em; } +.mbs__value { font-size: var(--mbs-value); font-weight: 500; font-variant-numeric: tabular-nums; min-width: 3.1ch; text-align: right; } +.mbs__inline-label { font-size: var(--mbs-value); opacity: 0.65; } +.mbs__inline-value { font-size: var(--mbs-inline); font-weight: 500; font-variant-numeric: tabular-nums; min-width: 3.1ch; text-align: right; } +.mbs__net { display: inline-flex; flex-direction: column; gap: 1px; font-size: var(--mbs-network); font-variant-numeric: tabular-nums; } +.mbs__net span { display: inline-flex; align-items: center; justify-content: space-between; gap: var(--space-1); min-width: 7.4ch; } +.mbs__net i { width: 5px; height: 5px; border-radius: 50%; } +.mbs__net .up { background: var(--p-up); } +.mbs__net .down { background: var(--p-down); } +.mbs svg { display: block; } + +/* ── 刘海岛 ───────────────────────────────────────────── */ + +.island { + position: absolute; + z-index: 25; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 200px; + height: 32px; + padding: 0 var(--space-4); + border: 0; + border-radius: 0 0 var(--radius-lg) var(--radius-lg); + background: #000; + color: #f3f4f6; + font: inherit; + cursor: pointer; + overflow: hidden; + transition: width 0.28s ease, height 0.28s ease, border-radius 0.28s ease; +} +.island__compact { display: flex; align-items: center; justify-content: space-between; height: 32px; font-size: var(--text-xs); } +.island__dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; } +.island__mini { font-variant-numeric: tabular-nums; } +.island__expanded { display: none; } + +.island.is-open { width: 380px; height: 176px; border-radius: 0 0 22px 22px; cursor: default; } +.island.is-open .island__compact { display: none; } +.island.is-open .island__expanded { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-3) 0; text-align: left; } +.island__row { display: flex; align-items: center; justify-content: space-between; } +.island__brand { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); font-weight: 600; } +.island__brand img { border-radius: var(--radius-sm); } +.island__health { font-size: var(--text-xs); color: #22c55e; } +.island__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-3); } +.island__tile { display: flex; flex-direction: column; gap: var(--space-1); } +.island__label { font-size: var(--text-xs); color: #9ca3af; } +.island__value { font-size: var(--text-lg); font-weight: 600; font-variant-numeric: tabular-nums; } +.island__bar { height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.14); overflow: hidden; } +.island__bar i { display: block; height: 100%; width: 0; background: #3b82f6; transition: width 0.6s ease; } +.island__foot { display: flex; justify-content: space-between; font-size: var(--text-xs); color: #9ca3af; font-variant-numeric: tabular-nums; } +.island__foot b { color: #f3f4f6; font-weight: 500; } +.dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: var(--space-1); } +.dot--up { background: #22c55e; } +.dot--down { background: #3b82f6; } + +/* ── 桌面图标 ─────────────────────────────────────────── */ + +.desk-icons { + position: absolute; + z-index: 5; + top: calc(var(--mb-height) + var(--space-6)); + left: var(--space-6); + display: grid; + gap: var(--space-4); +} +.desk-icon { display: flex; flex-direction: column; align-items: center; gap: var(--space-1); width: 88px; padding: var(--space-2); border-radius: var(--radius-md); color: #fff; } +.desk-icon:hover { color: #fff; background: rgba(0, 0, 0, 0.16); } +.desk-icon__name { font-size: var(--text-xs); font-weight: 500; text-align: center; padding: 0 var(--space-1); border-radius: var(--radius-sm); background: rgba(0, 0, 0, 0.32); } +.desk-icon__art { width: 48px; height: 56px; display: flex; align-items: center; justify-content: center; } +.desk-icon__art--dmg img { border-radius: var(--radius-md); box-shadow: var(--shadow-1); } +.desk-icon__art--doc { background: #fff; border-radius: var(--radius-sm); box-shadow: var(--shadow-1); color: #2563eb; font-size: var(--text-xs); font-weight: 700; } +.desk-icon__art--folder { width: 56px; height: 44px; margin-top: var(--space-2); background: #60a5fa; border-radius: var(--radius-sm) var(--radius-md) var(--radius-md) var(--radius-md); position: relative; box-shadow: var(--shadow-1); } +.desk-icon__art--folder::before { content: ""; position: absolute; top: -6px; left: 0; width: 22px; height: 8px; background: #60a5fa; border-radius: var(--radius-sm) var(--radius-sm) 0 0; } + +/* ── 欢迎窗口 ─────────────────────────────────────────── */ + +.window { + position: absolute; + z-index: 10; + background: var(--window-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-2); + overflow: hidden; +} +.window__bar { position: relative; display: flex; align-items: center; justify-content: center; height: 36px; background: var(--window-bar); border-bottom: 1px solid var(--color-border); } +.window__lights { position: absolute; left: var(--space-3); display: flex; gap: var(--space-2); } +.window__lights i { width: 12px; height: 12px; border-radius: 50%; background: #ff5f57; } +.window__lights i:nth-child(2) { background: #febc2e; } +.window__lights i:nth-child(3) { background: #28c840; } +.window__title { font-size: var(--text-xs); font-weight: 600; color: var(--color-text-secondary); } + +.hero { top: 18%; left: 50%; transform: translateX(-72%); width: min(520px, calc(100vw - 2 * var(--space-6))); } +.hero__body { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4); padding: var(--space-8); } +.hero__icon { border-radius: var(--radius-lg); } +.hero__title { font-size: var(--text-2xl); } +.hero__lead { color: var(--color-text-secondary); } +.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); } +.hero__meta { font-size: var(--text-xs); color: var(--color-text-tertiary); } +.hero__hint { font-size: var(--text-xs); color: var(--color-primary); } +.hero__link { margin-top: calc(-1 * var(--space-3)); padding: 0; border: 0; background: none; color: var(--color-text-secondary); font: inherit; font-size: var(--text-xs); text-decoration: underline; text-underline-offset: 2px; cursor: pointer; } +.hero__link:hover { color: var(--color-primary); } + +/* ── 程序坞 ───────────────────────────────────────────── */ + +.dock { + position: absolute; + z-index: 15; + left: 50%; + bottom: var(--space-3); + transform: translateX(-50%); + display: flex; + align-items: flex-end; + gap: var(--space-2); + padding: var(--space-2); + background: var(--dock-bg); + border: 1px solid var(--dock-border); + border-radius: var(--radius-xl); +} +.dock__item { + position: relative; + display: block; + width: 52px; + height: 52px; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + transition: transform 0.18s ease; + transform-origin: bottom center; +} +.dock__item:hover { transform: scale(1.22) translateY(-4px); } +.dock__item.dock__item--running::after { content: ""; position: absolute; bottom: -6px; left: 50%; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; background: var(--menubar-fg); opacity: 0.7; } +.dock__art { display: block; width: 52px; height: 52px; border-radius: var(--radius-lg); } +img.dock__art { border-radius: var(--radius-lg); } +.dock__art--finder { background: #3b82f6; position: relative; } +.dock__art--finder::before { content: ""; position: absolute; inset: 0 50% 0 0; background: #93c5fd; border-radius: var(--radius-lg) 0 0 var(--radius-lg); } +.dock__art--fan { background: #0d9488; position: relative; } +.dock__art--fan::before { content: ""; position: absolute; inset: 14px; border: 4px solid #fff; border-radius: 50%; border-left-color: transparent; } +.dock__art--clean { background: #16a34a; position: relative; } +.dock__art--clean::before { content: ""; position: absolute; left: 18px; right: 18px; top: 14px; bottom: 12px; background: #fff; border-radius: 2px 2px var(--radius-sm) var(--radius-sm); } +.dock__art--terminal { background: #1f2937; position: relative; } +.dock__art--terminal::before { content: ">_"; position: absolute; left: 10px; top: 12px; color: #22c55e; font: 600 var(--text-base) var(--font-mono); } +.dock__art--download { background: #e5e7eb; position: relative; } +.dock__art--download::before { content: "↓"; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #2563eb; font-size: var(--text-xl); font-weight: 700; } +.dock__divider { width: 1px; height: 44px; margin: 0 var(--space-1) var(--space-1); background: var(--dock-border); } + +.scroll-cue { position: absolute; right: var(--space-6); bottom: var(--space-6); z-index: 15; font-size: var(--text-xs); color: #fff; padding: var(--space-1) var(--space-3); border-radius: var(--radius-md); background: rgba(0, 0, 0, 0.32); } +.scroll-cue:hover { color: #fff; background: rgba(0, 0, 0, 0.48); } + +/* ── 菜单栏详情弹窗与主窗口(对应应用内的 PopoverRootView / MainWindowView) ── */ + +.os-popover, +.os-window { + position: absolute; + z-index: 30; + color: var(--p-text); + background: var(--p-bg); + border: 1px solid var(--p-border); + box-shadow: var(--shadow-2); + font-size: var(--text-sm); +} +.os-popover[hidden], .os-window[hidden] { display: none; } + +.os-popover { + top: calc(var(--mb-height) + var(--space-1)); + width: 320px; + max-height: calc(100vh - var(--mb-height) - var(--space-4)); + display: flex; + flex-direction: column; + border-radius: var(--radius-xl); + overflow: hidden; +} +.op-pophead { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); color: var(--p-text-2); } +.op-pophead b { color: var(--p-text); font-size: var(--text-base); font-weight: 600; margin-right: auto; } +.op-popbody { min-height: 0; padding: 0 var(--space-3) var(--space-3); display: grid; gap: var(--space-3); align-content: start; overflow-y: auto; overscroll-behavior: contain; } + +.os-window { + z-index: 28; + top: calc(var(--mb-height) + var(--space-8)); + left: 50%; + transform: translateX(-50%); + width: min(960px, calc(100vw - var(--space-12))); + height: min(640px, calc(100vh - var(--mb-height) - 140px)); + display: flex; + border-radius: var(--radius-lg); + overflow: hidden; + resize: both; + min-width: 640px; + min-height: 420px; +} + +/* 侧边栏与页面在同一张底色上,不分栏着色,也没有分隔线 */ +.ow-sidebar { flex: 0 0 192px; display: flex; flex-direction: column; padding: 0 var(--space-3) var(--space-3); } +.ow-lights { height: 40px; display: flex; align-items: center; gap: var(--space-2); padding-left: var(--space-1); cursor: grab; } +.ow-lights i, .ow-lights button { width: 12px; height: 12px; padding: 0; border: 0; border-radius: 50%; background: #febc2e; } +.ow-lights button { background: #ff5f57; cursor: pointer; } +.ow-lights i:last-child { background: #28c840; } +.ow-navlist { display: grid; gap: var(--space-1); align-content: start; margin-top: var(--space-2); } +.ow-group { padding: var(--space-2) var(--space-2) 0; font-size: var(--text-xs); font-weight: 500; color: var(--p-text-3); } +.ow-nav { display: flex; align-items: center; gap: var(--space-2); height: 32px; padding: 0 var(--space-2); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--p-text); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; } +.ow-nav:hover { background: var(--p-hover); } +.ow-nav[aria-current="page"] { background: var(--p-primary); color: #fff; font-weight: 600; } +.ow-tools { margin-top: auto; display: flex; gap: var(--space-1); } +.ow-tools .op-icon:last-child { margin-left: auto; } + +.ow-main { flex: 1; min-width: 0; display: flex; flex-direction: column; } +.ow-header { flex: none; height: 40px; display: flex; align-items: center; gap: var(--space-3); padding: 0 var(--space-4); cursor: grab; } +.ow-header h2 { margin-right: auto; font-size: var(--text-base); } +.ow-toggle-label { font-size: var(--text-xs); color: var(--p-text-2); } +.ow-content { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--space-4) var(--space-4) var(--space-3); display: grid; gap: var(--space-3); align-content: start; } + +/* ── 卡片与控件 ── */ + +.op-row { display: grid; gap: var(--space-3); align-items: stretch; } +.op-row--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.op-row--2-1 { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); } +.op-row--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; } + +.op-card { padding: var(--space-3); background: var(--p-surface); border-radius: var(--radius-lg); display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; } +.op-card__head { display: flex; align-items: center; gap: var(--space-1); min-height: 24px; font-size: var(--text-xs); font-weight: 600; color: var(--p-text-2); } +.op-card__head .op-chip, .op-card__note, .op-mini { margin-left: auto; } +.op-card__note { font-weight: 400; color: var(--p-text-3); } +.op-chip { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); font-weight: 500; padding: 0 var(--space-2); border-radius: var(--radius-sm); background: var(--p-track); color: var(--p-text-2); white-space: nowrap; font-variant-numeric: tabular-nums; } +.op-chip--primary { background: color-mix(in srgb, var(--p-primary) 14%, transparent); color: var(--p-primary); } +.op-chip--success { background: color-mix(in srgb, var(--p-success) 14%, transparent); color: var(--p-success); } +.op-chip--warning { background: color-mix(in srgb, var(--p-warning) 14%, transparent); color: var(--p-warning); } +.op-badge { display: inline-flex; align-items: center; gap: var(--space-1); padding: 0 var(--space-2); border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: 500; white-space: nowrap; } +.op-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; } +.op-badge--success { color: var(--p-success); background: color-mix(in srgb, var(--p-success) 12%, transparent); } +.op-value { font-size: var(--text-xl); font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.25; white-space: nowrap; } +.op-value small { font-size: var(--text-xs); font-weight: 500; color: var(--p-text-2); margin-left: 2px; } +.op-foot { font-size: var(--text-xs); color: var(--p-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; } +.op-chart { height: 40px; width: 100%; display: block; } +.op-chart--tall { height: 72px; } +.ow-content .op-chart { height: 48px; } +.ow-content .op-card > .op-chart, .ow-content .op-mirror .op-chart { height: 96px; } + +.op-icon { width: 32px; height: 32px; flex: none; display: flex; align-items: center; justify-content: center; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--p-text-2); cursor: pointer; } +.op-icon:hover { background: var(--p-hover); color: var(--p-primary); } +.op-mini { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--p-text-2); cursor: pointer; } +.op-mini:hover { background: var(--p-hover); color: var(--p-text); } + +.op-info { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); font-size: var(--text-xs); } +.op-info > span { color: var(--p-text-2); white-space: nowrap; display: inline-flex; align-items: center; gap: var(--space-2); } +.op-info > b { font-weight: 500; text-align: right; font-variant-numeric: tabular-nums; min-width: 0; overflow: hidden; text-overflow: ellipsis; } +.op-copy { max-width: 100%; padding: 0; border: 0; background: none; color: inherit; font: inherit; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; } +.op-copy:hover { color: var(--p-primary); } +.op-copy.is-copied { color: var(--p-success); } +.op-flag { display: inline-flex; align-items: center; gap: var(--space-2); } +.op-flag img { border-radius: 2px; box-shadow: 0 0 0 1px var(--p-border); } +.op-swatch { width: 8px; height: 8px; border-radius: 2px; } + +.op-hero { display: flex; align-items: center; gap: var(--space-3); position: relative; } +.op-hero__ring { position: absolute; left: 0; width: 56px; text-align: center; font-size: var(--text-sm); font-weight: 600; font-variant-numeric: tabular-nums; } +.op-hero__legend { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); margin-right: auto; } +.op-hero__legend .op-legend { margin-left: 0; } +.op-hero__legend .op-value { font-size: var(--text-lg); } +.op-rates { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); } +.op-rates > div + div { padding-left: var(--space-3); border-left: 1px solid var(--p-border); } +.op-rates .op-legend { margin-left: 0; } + +.op-mirror { display: grid; gap: var(--space-1); font-size: var(--text-xs); color: var(--p-text-3); font-variant-numeric: tabular-nums; } +.op-probe { display: grid; grid-template-columns: repeat(20, minmax(0, 1fr)); gap: 2px; } +.op-probe--wide { grid-template-columns: repeat(40, minmax(0, 1fr)); } +.op-probe i { aspect-ratio: 1; border-radius: 2px; background: var(--p-track); } +.op-probe i.is-ok { background: var(--p-success); } +.op-probe i.is-slow { background: var(--p-warning); } +.op-probe i.is-fail { background: var(--p-error); } +.op-stats { display: grid; grid-template-columns: repeat(3, 1fr); } +.op-stats small { display: block; font-size: var(--text-xs); color: var(--p-text-3); } +.op-stats b { font-weight: 600; font-variant-numeric: tabular-nums; } + +.op-chipgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-1); } +.op-chipbtn { height: 24px; padding: 0 var(--space-1); border: 1px solid var(--p-border); border-radius: var(--radius-sm); background: transparent; color: var(--p-text-2); font: inherit; font-size: var(--text-xs); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; } +.op-chipbtn:hover { background: var(--p-hover); } +.op-chipbtn[aria-pressed="true"] { border-color: color-mix(in srgb, var(--p-primary) 40%, transparent); background: color-mix(in srgb, var(--p-primary) 12%, transparent); color: var(--p-primary); font-weight: 600; } +.op-field { display: flex; gap: var(--space-2); } +.op-field input { flex: 1; min-width: 0; height: 32px; padding: 0 var(--space-2); border: 1px solid var(--color-neutral-300); border-radius: var(--radius-md); background: var(--p-elevated); color: var(--p-text); font: inherit; font-size: var(--text-xs); } +:root[data-theme="dark"] .op-field input { border-color: #3f434a; } +.op-field input:focus { outline: none; border-color: var(--p-primary); } +.op-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); } +.op-note { font-size: var(--text-xs); color: var(--p-success); } + +.op-health { display: flex; align-items: center; gap: var(--space-3); padding: 0 var(--space-1); } +.op-health__score { font-size: var(--text-2xl); font-weight: 600; font-variant-numeric: tabular-nums; } +.op-health__text { font-size: var(--text-base); color: var(--p-text-2); font-weight: 500; } +.op-badges { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: 0 var(--space-1); } + +.op-track { height: 8px; border-radius: var(--radius-sm); background: var(--p-track); overflow: hidden; } +.op-track--thin { height: 4px; margin-top: calc(-1 * var(--space-1)); } +.op-track i { display: block; height: 100%; border-radius: var(--radius-sm); background: var(--p-primary); transition: width 0.5s ease; } +.op-stack { display: flex; gap: 1px; height: 12px; border-radius: var(--radius-sm); background: var(--p-track); overflow: hidden; } +.op-stack i { display: block; height: 100%; } +.op-cores { display: flex; gap: 2px; height: 64px; align-items: flex-end; } +.op-cores i { flex: 1; height: 100%; border-radius: 2px; background: var(--p-track); position: relative; overflow: hidden; } +.op-cores i b { position: absolute; left: 0; right: 0; bottom: 0; background: var(--p-primary); transition: height 0.5s ease; } +.op-cores .gap { flex: 0 0 var(--space-3); background: none; } +.op-core-labels { display: flex; font-size: var(--text-xs); color: var(--p-text-3); } +.op-core-labels span:first-child { flex: 6 6 0; } +.op-core-labels span:last-child { flex: 12 12 0; margin-left: var(--space-3); } + +.op-legend { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); color: var(--p-text-2); margin-left: var(--space-2); white-space: nowrap; } +.op-legend i { width: 8px; height: 8px; border-radius: 50%; } +.op-legend b { color: var(--p-text); font-weight: 600; } +.op-battery { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); } + +.op-procs { display: grid; gap: var(--space-2); } +.op-proc { display: grid; align-items: center; gap: var(--space-2); font-size: var(--text-xs); font-variant-numeric: tabular-nums; } +.op-proc--1 { grid-template-columns: 16px minmax(0, 1fr) 64px; } +.op-proc--2 { grid-template-columns: 16px minmax(0, 1fr) 64px 64px; } +.op-proc__icon { width: 16px; height: 16px; border-radius: var(--radius-sm); } +.op-proc__name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.op-proc__num { text-align: right; color: var(--p-text-2); } + +.op-quick { display: flex; align-items: center; gap: var(--space-2); height: 32px; padding: 0 var(--space-2); border: 0; border-radius: var(--radius-md); background: var(--p-track); color: var(--p-text); font: inherit; font-size: var(--text-sm); font-weight: 500; cursor: pointer; } +.op-quick span:last-child { margin-left: auto; font-size: var(--text-xs); color: var(--p-text-3); font-weight: 400; } +.op-quick[aria-pressed="true"] { background: color-mix(in srgb, var(--p-primary) 12%, transparent); color: var(--p-primary); } +.op-quick[aria-pressed="true"] span:last-child { color: var(--p-primary); } + +.op-list-row { display: flex; align-items: center; gap: var(--space-3); } +.op-list-row__title { width: 48px; font-weight: 500; } +.op-list-row .op-track { flex: 1; } +.op-list-row__value { width: 56px; text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; } +.op-list-row .op-foot { margin-right: auto; } + +.op-toggle { position: relative; width: 40px; height: 24px; border: 0; border-radius: 12px; background: var(--color-neutral-300); cursor: pointer; flex: none; } +:root[data-theme="dark"] .op-toggle { background: #3f434a; } +.op-toggle::after { content: ""; position: absolute; top: 4px; left: 4px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: var(--shadow-1); transition: transform 0.15s ease; } +:root[data-theme="dark"] .op-toggle[aria-checked="true"], .op-toggle[aria-checked="true"] { background: var(--p-primary); } +.op-toggle[aria-checked="true"]::after { transform: translateX(16px); } + +.op-seg { display: flex; padding: var(--space-1); background: var(--p-track); border-radius: var(--radius-md); } +.op-seg--narrow { width: 200px; } +.op-seg button { flex: 1; height: 24px; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--p-text-2); font: inherit; font-size: var(--text-xs); cursor: pointer; } +.op-seg button[aria-pressed="true"] { background: var(--p-elevated); color: var(--p-text); font-weight: 600; box-shadow: var(--shadow-1); } + +.op-radio { display: flex; gap: var(--space-3); align-items: flex-start; padding: var(--space-2) 0; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; } +.op-radio i { width: 16px; height: 16px; margin-top: 2px; border-radius: 50%; border: 1.5px solid var(--color-neutral-300); flex: none; } +.op-radio[aria-checked="true"] i { border-color: var(--p-primary); box-shadow: inset 0 0 0 3px var(--p-surface), inset 0 0 0 8px var(--p-primary); } +.op-radio b { display: block; font-weight: 500; } +.op-radio small { display: block; font-size: var(--text-xs); color: var(--p-text-2); } + +.op-banner { display: flex; gap: var(--space-2); padding: var(--space-3); border-radius: var(--radius-md); font-size: var(--text-xs); background: color-mix(in srgb, var(--p-warning) 10%, transparent); } +.op-banner svg { flex: none; color: var(--p-warning); } +.op-banner--success { background: color-mix(in srgb, var(--p-success) 10%, transparent); } +.op-banner--success svg { color: var(--p-success); } + +.op-btn { align-self: flex-start; height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-md); border: 1px solid var(--p-border); background: var(--p-elevated); color: var(--p-text); font: inherit; font-size: var(--text-sm); font-weight: 500; cursor: pointer; } +.op-btn:hover { background: var(--p-hover); } +.op-btn:disabled { opacity: 0.6; cursor: default; } +.op-btn--primary { border-color: transparent; background: var(--p-primary); color: #fff; } +.op-btn--primary:hover { background: var(--color-primary-hover); } + +.op-check { width: 16px; height: 16px; border-radius: var(--radius-sm); border: 1.5px solid var(--color-neutral-300); background: transparent; cursor: pointer; flex: none; padding: 0; position: relative; } +.op-check[aria-checked="true"] { border-color: var(--p-primary); background: var(--p-primary); } +.op-check[aria-checked="true"]::after { content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg); } + +.op-clean-row { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) 0; border-top: 1px solid var(--p-border); } +.op-card__head + .op-clean-row { border-top: 0; } +.op-clean-row b { display: block; font-weight: 500; } +.op-clean-row small { display: block; font-size: var(--text-xs); color: var(--p-text-3); } +.op-clean-row .size { margin-left: auto; font-weight: 500; font-variant-numeric: tabular-nums; } + +/* ── 图表与状态色(SVG 与 JS 生成的元素使用) ── */ + +.fill-primary { fill: var(--p-primary); } +.fill-warning { fill: var(--p-warning); } +.fill-success { fill: var(--p-success); } +.fill-error { fill: var(--p-error); } +.fill-track { fill: var(--p-track); } +.stroke-white { fill: none; stroke: #fff; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } +.area-primary { fill: var(--p-primary); fill-opacity: 0.14; } +.stroke-primary { fill: none; stroke: var(--p-primary); stroke-width: 1.5; vector-effect: non-scaling-stroke; } +.area-secondary { fill: var(--p-secondary); fill-opacity: 0.14; } +.stroke-secondary { fill: none; stroke: var(--p-secondary); stroke-width: 1.5; vector-effect: non-scaling-stroke; } +.area-up { fill: var(--p-up); fill-opacity: 0.12; } +.stroke-up { fill: none; stroke: var(--p-up); stroke-width: 1.5; vector-effect: non-scaling-stroke; } +.area-down { fill: var(--p-down); fill-opacity: 0.12; } +.stroke-down { fill: none; stroke: var(--p-down); stroke-width: 1.5; vector-effect: non-scaling-stroke; } +.ring-track { stroke: var(--p-track); } +.ring-primary { stroke: var(--p-primary); } +.ring-success { stroke: var(--p-success); } +.ring-warning { stroke: var(--p-warning); } +.ring-error { stroke: var(--p-error); } +.bg-primary { background: var(--p-primary); } +.bg-secondary, .op-track i.bg-secondary { background: var(--p-secondary); } +.bg-warning, .op-track i.bg-warning { background: var(--p-warning); } +.bg-up { background: var(--p-up); } +.bg-down { background: var(--p-down); } +.bg-track { background: var(--p-track); } +.bg-muted { background: var(--color-neutral-300); } +.up-text { color: var(--p-up); } +.down-text { color: var(--p-down); } +.warning-text { color: var(--p-warning); } +.op-chart--center { display: flex; align-items: center; } +.op-chart--center .op-track { flex: 1; height: 12px; } +.op-quick svg { flex: none; } + +/* 进程图标色块(示意,不复刻真实应用图标) */ +.proc-c1 { background: #2563eb; } +.proc-c2 { background: #0d9488; } +.proc-c3 { background: #374151; } +.proc-c4 { background: #dc2626; } +.proc-c5 { background: #d97706; } +.proc-c6 { background: #6b7280; } + +.mb-stats--static { cursor: default; background: transparent; } +.mb-stats--static:hover { background: transparent; } + +/* ── 窄屏 ── */ + +@media (max-width: 1100px) { + .hero { transform: translateX(-50%); top: 16%; } + .desk-icons { display: none; } + .os-window { min-width: 0; } + .ow-sidebar { flex-basis: 64px; padding: 0 var(--space-2) var(--space-2); } + .ow-nav span, .ow-group, .ow-tools .op-icon:not(:first-child) { display: none; } + .ow-nav { justify-content: center; } + .op-row--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 760px) { + .mb-menu:not(:first-of-type), .menubar__extra, .menubar__clock { display: none; } + .menubar { white-space: nowrap; padding: 0 var(--space-2); } + .menubar__right { gap: var(--space-2); } + .island { display: none; } + .hero { top: calc(var(--mb-height) + var(--space-6)); } + .hero__body { padding: var(--space-6); } + .hero__title { font-size: var(--text-xl); } + .dock__item, .dock__art { width: 40px; height: 40px; } + .dock__art--terminal::before { font-size: var(--text-xs); left: 8px; top: 10px; } + .dock__art--fan::before { inset: 10px; border-width: 3px; } + .dock__art--clean::before { left: 13px; right: 13px; top: 10px; bottom: 9px; } + .scroll-cue { display: none; } + .os-window { top: calc(var(--mb-height) + var(--space-2)); width: calc(100vw - var(--space-4)); height: calc(100vh - var(--mb-height) - 96px); resize: none; } + .op-row--3, .op-row--2, .op-row--2-1 { grid-template-columns: 1fr; } + .op-probe--wide { grid-template-columns: repeat(20, minmax(0, 1fr)); } + .op-probe--wide i:nth-child(-n+60) { display: none; } +} diff --git a/web/replica.js b/web/replica.js new file mode 100644 index 0000000..274257b --- /dev/null +++ b/web/replica.js @@ -0,0 +1,768 @@ +/* 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); +})(); diff --git a/web/styles.css b/web/styles.css new file mode 100644 index 0000000..b4c91d7 --- /dev/null +++ b/web/styles.css @@ -0,0 +1,272 @@ +/* OpenStats 官网 · 页面基础与介绍区 + 所有颜色、字号、间距、圆角与阴影都从下面的 token 取值。 */ + +:root { + color-scheme: light dark; + + /* 品牌与语义色 */ + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-secondary: #0d9488; + --color-success: #16a34a; + --color-warning: #d97706; + --color-error: #dc2626; + + /* 中性色 */ + --color-neutral-50: #f8f9fa; + --color-neutral-100: #f3f4f6; + --color-neutral-200: #e5e7eb; + --color-neutral-300: #d1d5db; + --color-neutral-400: #9ca3af; + --color-neutral-500: #6b7280; + --color-neutral-700: #374151; + --color-neutral-800: #1f2937; + --color-neutral-900: #111827; + + --color-bg: #f8f9fa; + --color-bg-alt: #ffffff; + --color-surface: #ffffff; + --color-border: #e5e7eb; + --color-text: #111827; + --color-text-secondary: #6b7280; + --color-text-tertiary: #9ca3af; + --color-on-primary: #ffffff; + + /* 字号 */ + --text-xs: 12px; + --text-sm: 14px; + --text-base: 16px; + --text-lg: 20px; + --text-xl: 24px; + --text-2xl: 32px; + + /* 间距(4px 栅格) */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-6: 24px; + --space-8: 32px; + --space-12: 48px; + --space-16: 64px; + + /* 圆角:控件 < 卡片 < 窗口 */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + + /* 阴影(全站只用两级) */ + --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-2: 0 4px 12px rgba(0, 0, 0, 0.1); + + --shell: 1120px; + --narrow: 760px; + --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Helvetica Neue", Arial, sans-serif; + --font-mono: "SF Mono", ui-monospace, Menlo, monospace; +} + +:root[data-theme="dark"] { + --color-primary: #3b82f6; + --color-primary-hover: #2563eb; + --color-secondary: #2dd4bf; + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-error: #f87171; + --color-bg: #111315; + --color-bg-alt: #16181c; + --color-surface: #1d1f23; + --color-border: #2c2f35; + --color-text: #f3f4f6; + --color-text-secondary: #9ca3af; + --color-text-tertiary: #6b7280; + --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-2: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +*, *::before, *::after { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + font-family: var(--font); + font-size: var(--text-base); + font-weight: 400; + line-height: 1.5; + color: var(--color-text); + background: var(--color-bg); + -webkit-font-smoothing: antialiased; +} + +img { max-width: 100%; display: block; } + +a { color: var(--color-primary); text-decoration: none; } +a:hover { color: var(--color-primary-hover); } + +h1, h2, h3 { font-weight: 600; line-height: 1.25; margin: 0; } + +p { margin: 0; } + +:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } +} + +/* ── 按钮 ─────────────────────────────────────────────── */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + height: 40px; + padding: 0 var(--space-4); + border-radius: var(--radius-md); + font-size: var(--text-sm); + font-weight: 600; + border: 1px solid transparent; + transition: background-color 0.15s ease, border-color 0.15s ease; +} + +.btn--primary { background: var(--color-primary); color: var(--color-on-primary); } +.btn--primary:hover { background: var(--color-primary-hover); color: var(--color-on-primary); } + +.btn--secondary { background: var(--color-surface); color: var(--color-text); border-color: var(--color-border); } +.btn--secondary:hover { background: var(--color-neutral-100); color: var(--color-text); } +:root[data-theme="dark"] .btn--secondary:hover { background: #25282d; } + +/* ── 介绍区 ───────────────────────────────────────────── */ + +.shell { max-width: var(--shell); margin: 0 auto; padding: 0 var(--space-6); } +.narrow { max-width: var(--narrow); } + +.band { padding: var(--space-16) 0; } +.band--alt { background: var(--color-bg-alt); border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); } + +.section-head { margin-bottom: var(--space-8); } +.section-head h2 { font-size: var(--text-2xl); } +.section-head p { margin-top: var(--space-2); color: var(--color-text-secondary); font-size: var(--text-base); } + +.feature-grid { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-4); +} + +.feature { + padding: var(--space-6); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); +} +.feature h3 { font-size: var(--text-base); } +.feature p { margin-top: var(--space-2); font-size: var(--text-sm); color: var(--color-text-secondary); } + +/* 风格选择 */ +.style-picker { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-6); } + +.style-chip { + height: 36px; + padding: 0 var(--space-4); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text); + font: inherit; + font-size: var(--text-sm); + cursor: pointer; +} +.style-chip:hover { border-color: var(--color-neutral-400); } +.style-chip[aria-checked="true"] { border-color: var(--color-primary); color: var(--color-primary); font-weight: 600; } + +.style-preview { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); } +.style-preview__bar { + height: 64px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-lg); +} +.style-preview__bar--light { background: var(--color-neutral-200); color: #000; } +.style-preview__bar--dark { background: var(--color-neutral-800); color: #fff; } +.style-preview__caption { grid-column: 1 / -1; font-size: var(--text-sm); color: var(--color-text-secondary); } + +/* 截图 */ +.shots { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6); align-items: start; } +.shot { margin: 0; } +.shot img { border-radius: var(--radius-lg); box-shadow: var(--shadow-2); } +.shot--popover { max-width: 320px; justify-self: center; } +.shot--popover img { border-radius: var(--radius-xl); } +.shot--dark { display: none; } +:root[data-theme="dark"] .shot--light { display: none; } +:root[data-theme="dark"] .shot--dark { display: block; } +.shot figcaption { margin-top: var(--space-3); text-align: center; font-size: var(--text-sm); color: var(--color-text-secondary); } + +.split { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-12); align-items: center; } +.split--reverse .split__text { order: 2; } +.split__text h2 { font-size: var(--text-2xl); margin-bottom: var(--space-6); } +.split__text p { margin-top: var(--space-4); color: var(--color-text-secondary); } +.split__text .note { font-size: var(--text-sm); color: var(--color-text-tertiary); } + +.mode-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); } +.mode-table th, .mode-table td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--color-border); } +.mode-table th { color: var(--color-text-secondary); font-weight: 600; } + +.check-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); } +.check-list li { position: relative; padding-left: var(--space-6); } +.check-list li::before { content: "✓"; position: absolute; left: 0; color: var(--color-success); font-weight: 600; } + +.fact-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface); } +.fact-list li { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-4); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--color-border); font-size: var(--text-sm); } +.fact-list li:first-child { border-top: 0; } +.fact-list b { font-weight: 600; } +.fact-list span { color: var(--color-text-secondary); } + +.download { text-align: center; display: flex; flex-direction: column; align-items: center; gap: var(--space-4); } +.download h2 { font-size: var(--text-2xl); } +.download p { color: var(--color-text-secondary); } + +/* 更新日志(由 Scripts/sync_changelog.py 生成) */ +.log { display: grid; gap: var(--space-4); } +.log__day { padding: var(--space-6); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); } +.log__head { display: flex; align-items: center; gap: var(--space-3); } +.log__head time { font-weight: 600; } +.log__tag { font-size: var(--text-xs); padding: 0 var(--space-2); border-radius: var(--radius-sm); background: var(--color-neutral-100); color: var(--color-text-secondary); } +:root[data-theme="dark"] .log__tag { background: #25282d; } +.log__counts { margin-top: var(--space-1); font-size: var(--text-xs); color: var(--color-text-tertiary); } +.log__list { margin: var(--space-3) 0 0; padding-left: var(--space-4); font-size: var(--text-sm); color: var(--color-text-secondary); display: grid; gap: var(--space-2); } +.log__kind { display: inline-block; margin-right: var(--space-2); font-size: var(--text-xs); font-weight: 600; color: var(--color-primary); } +.log__list code { font-family: var(--font-mono); font-size: var(--text-xs); } +.log__more { display: inline-block; margin-top: var(--space-3); font-size: var(--text-sm); } +.log__all { margin-top: var(--space-6); font-size: var(--text-sm); } + +.faq { border-bottom: 1px solid var(--color-border); padding: var(--space-4) 0; } +.faq summary { cursor: pointer; font-weight: 600; list-style: none; } +.faq summary::-webkit-details-marker { display: none; } +.faq summary::after { content: "+"; float: right; color: var(--color-text-tertiary); } +.faq[open] summary::after { content: "−"; } +.faq p { margin-top: var(--space-3); color: var(--color-text-secondary); font-size: var(--text-sm); } + +.footer { padding: var(--space-8) 0; border-top: 1px solid var(--color-border); } +.footer__inner { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); } +.footer__brand { display: inline-flex; align-items: center; gap: var(--space-2); font-weight: 600; } +.footer__fine { font-size: var(--text-xs); color: var(--color-text-tertiary); } + +@media (max-width: 900px) { + .feature-grid { grid-template-columns: 1fr 1fr; } + .split, .shots { grid-template-columns: 1fr; } + .split--reverse .split__text { order: 0; } +} + +@media (max-width: 600px) { + .band { padding: var(--space-12) 0; } + .feature-grid, .style-preview { grid-template-columns: 1fr; } + .section-head h2, .split__text h2, .download h2 { font-size: var(--text-xl); } + .fact-list li { grid-template-columns: 1fr; gap: var(--space-1); } +}