Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
记录 OpenStats 应用的功能新增、样式调整和问题修复,按版本与日期倒序排列。
官网、构建与发布脚本等不影响应用本身的改动不在此记录。

## 未发布

### 2026-11-01

#### 新增

- 设置新增「监控大屏」:开启后向局域网提供只读网页仪表盘(默认端口 8790,可选 8791/8888),同一网络里的平板、旧电脑、电视用浏览器打开即可实时查看本机监控——CPU(总占用与各核心)、内存、网络速率、磁盘、负载、电量与进程 TOP,配 5 分钟走势,页面自适应横竖屏,断网自动重连。数据只在局域网内流转,不出外网。

## 0.7.3 · 2026-10-21

### 2026-10-21
Expand Down
9 changes: 9 additions & 0 deletions Packages/OpenStatsKit/Sources/Localization/Translations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1217,5 +1217,14 @@ macOS 报告的热状态:偏高及以上时,系统会降低性能来降温 T
,充电中 , charging
,已从程序坞移除 , removed from the Dock
:{} : {}
监控大屏 Dashboard screen
无法在端口 {} 上监听 Cannot listen on port {}
未知错误 Unknown error
启用监控大屏 Enable dashboard screen
同一局域网里的设备(平板、旧电脑、电视的浏览器)打开下面的地址,即可实时查看本机监控,适合挂一块监控屏 Open the address below in a browser on any device on the same network (tablet, spare computer, TV) for a live view of this Mac — handy as a dedicated monitoring screen
端口 Port
未开启 Off
服务运行中 · 端口 {} Running · port {}
端口 {} 启动失败 Failed to start on port {}
"""#
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public struct MainWindowView: View {
case .startupItems: StartupItemsPage()
case .settingsGeneral: SettingsTabPage { GeneralSettings() }
case .settingsMenuBar: SettingsTabPage { MenuBarSettings() }
case .settingsDashboard: SettingsTabPage { DashboardSettings() }
case .settingsNotifications: SettingsTabPage { NotificationSettings() }
case .settingsAccount: SettingsTabPage { AccountSettings() }
case .settingsHelper: SettingsTabPage { HelperSettings() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Localization
import SwiftUI

/// 设置 · 监控大屏:开关、端口与当前地址
struct DashboardSettings: View {
@Environment(AppModel.self) private var model

var body: some View {
@Bindable var settings = model.settings

SettingsGroup {
GroupRow(showsDivider: false) {
SettingRow(title: tr("启用监控大屏"), subtitle: tr("同一局域网里的设备(平板、旧电脑、电视的浏览器)打开下面的地址,即可实时查看本机监控,适合挂一块监控屏")) {
DSToggle(isOn: Binding(get: { settings.dashboardEnabled },
set: {
settings.dashboardEnabled = $0
model.dashboard.applySettings()
}),
label: tr("启用监控大屏"))
}
}
GroupRow {
SettingRow(title: tr("端口")) {
SegmentedControl(selection: Binding(get: { settings.dashboardPort },
set: {
settings.dashboardPort = $0
model.dashboard.applySettings()
}),
options: AppSettings.dashboardPortOptions.map { ($0, "\($0)") })
.frame(width: DS.Size.sidebarWidth)
}
}
GroupRow {
status
}
}
}

@ViewBuilder private var status: some View {
switch model.dashboard.availability {
case .off:
SettingRow(title: tr("状态")) {
Text(tr("未开启"))
.foregroundStyle(DS.Palette.textSecondary)
}
case .starting(let port), .running(let port):
SettingRow(title: tr("状态")) {
VStack(alignment: .trailing, spacing: DS.Space.s1) {
Text(tr("服务运行中 · 端口 \(port)"))
.foregroundStyle(DS.Palette.success)
if let address = model.dashboard.address {
Text(address).font(.system(.callout, design: .monospaced))
.textSelection(.enabled)
}
}
}
case .failed(let port, let reason):
SettingRow(title: tr("状态"), subtitle: reason) {
Text(tr("端口 \(port) 启动失败"))
.foregroundStyle(DS.Palette.error)
}
}
}
}
10 changes: 7 additions & 3 deletions Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public final class AppModel {
public let history: HistoryRecorder
public let sync: SyncController
let diskTools: DiskToolsController
public let dashboard: DashboardController
@ObservationIgnored public let hub: MetricsHub

public var isMainWindowVisible = false
Expand Down Expand Up @@ -84,6 +85,7 @@ public final class AppModel {
sync = SyncController(settings: settings)
diskTools = DiskToolsController(helper: helper)
speedTest = SpeedTestController(settings: settings)
dashboard = DashboardController(settings: settings, store: store)
}

/// 根据当前可见内容决定采集范围:主窗口看标签页,详情弹窗看是哪一项
Expand All @@ -103,13 +105,15 @@ public final class AppModel {
demand.memory = true
demand.network = true
let showing = { (page: PanelTab, item: MenuBarItem) in (window && tab == page) || popover == item }
// 监控大屏开着时,它的页面数据要一直采
let dashboard = settings.dashboardEnabled
demand.gpu = overview || (window && tab == .system) || menu.contains(.gpu) || showing(.gpu, .gpu)
demand.disk = overview || (window && [.system, .cleaner, .disk].contains(tab)) || menu.contains(.disk) || popover == .disk
demand.disk = overview || (window && [.system, .cleaner, .disk].contains(tab)) || menu.contains(.disk) || popover == .disk || dashboard
demand.diskDetail = showing(.disk, .disk)
demand.battery = overview || (window && tab == .keepAwake) || keepAwake.lidClosedActive
|| menu.contains(.battery) || showing(.battery, .battery)
|| menu.contains(.battery) || showing(.battery, .battery) || dashboard
demand.processes = overview || (window && [.processes, .disk].contains(tab)) || showing(.cpu, .cpu) || showing(.memory, .memory)
|| popover == .disk
|| popover == .disk || dashboard
demand.systemProcesses = window && tab == .processes

var groups = Set<TemperatureGroup>()
Expand Down
20 changes: 18 additions & 2 deletions Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,13 @@ public enum AppearanceMode: String, CaseIterable, Identifiable, Sendable {
/// 主窗口侧边栏的页面
public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case overview, system, history, cpu, gpu, memory, disk, network, thermal, battery, processes, keepAwake, cleaner, uninstaller, startupItems
case settingsGeneral, settingsMenuBar, settingsNotifications, settingsAccount, settingsHelper, settingsAbout
case settingsGeneral, settingsMenuBar, settingsDashboard, settingsNotifications, settingsAccount, settingsHelper, settingsAbout

public var id: String { rawValue }

static let monitors: [PanelTab] = [.overview, .system, .history, .cpu, .gpu, .memory, .disk, .network, .thermal, .battery]
static let tools: [PanelTab] = [.processes, .startupItems, .keepAwake, .cleaner, .uninstaller]
static let settings: [PanelTab] = [.settingsGeneral, .settingsMenuBar, .settingsNotifications, .settingsAccount, .settingsHelper, .settingsAbout]
static let settings: [PanelTab] = [.settingsGeneral, .settingsMenuBar, .settingsDashboard, .settingsNotifications, .settingsAccount, .settingsHelper, .settingsAbout]

var isSettings: Bool { Self.settings.contains(self) }

Expand All @@ -313,6 +313,7 @@ public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case .startupItems: tr("启动项")
case .settingsGeneral: tr("通用")
case .settingsMenuBar: tr("菜单栏")
case .settingsDashboard: tr("监控大屏")
case .settingsNotifications: tr("通知")
case .settingsAccount: tr("账号与同步")
case .settingsHelper: tr("辅助工具")
Expand All @@ -339,6 +340,7 @@ public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case .startupItems: "power"
case .settingsGeneral: "gearshape"
case .settingsMenuBar: "menubar.rectangle"
case .settingsDashboard: "tv"
case .settingsNotifications: "bell.badge"
case .settingsAccount: "person.crop.circle"
case .settingsHelper: "lock.shield"
Expand Down Expand Up @@ -523,6 +525,14 @@ public final class AppSettings {
public var cleanPrefersTrash: Bool {
didSet { defaults.set(cleanPrefersTrash, forKey: Keys.cleanPrefersTrash) }
}
/// 局域网监控大屏:向同网段设备提供只读网页仪表盘
public var dashboardEnabled: Bool {
didSet { defaults.set(dashboardEnabled, forKey: Keys.dashboardEnabled) }
}
/// 监控大屏的 HTTP 端口
public var dashboardPort: Int {
didSet { defaults.set(dashboardPort, forKey: Keys.dashboardPort) }
}

public static let refreshOptions = [1, 2, 3, 5]
public static let batteryFloorOptions = [10, 20, 30, 40]
Expand All @@ -531,6 +541,7 @@ public final class AppSettings {
public static let alertTemperatureOptions = [85, 90, 95, 100]
public static let alertLoadOptions = [70, 80, 90]
public static let cpuChartOptions = [60, 180, 300]
public static let dashboardPortOptions = [8790, 8791, 8888]

public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
Expand Down Expand Up @@ -585,6 +596,9 @@ public final class AppSettings {
? defaults.integer(forKey: Keys.alertCPULoad) : 80
cpuChartSeconds = Self.cpuChartOptions.contains(defaults.integer(forKey: Keys.cpuChartSeconds))
? defaults.integer(forKey: Keys.cpuChartSeconds) : 60
dashboardEnabled = defaults.object(forKey: Keys.dashboardEnabled) as? Bool ?? false
dashboardPort = Self.dashboardPortOptions.contains(defaults.integer(forKey: Keys.dashboardPort))
? defaults.integer(forKey: Keys.dashboardPort) : 8790
}

/// 合并显示用自定义顺序;每项独立的位置由 macOS 的 ⌘ 拖动与 autosaveName 记住。
Expand Down Expand Up @@ -680,5 +694,7 @@ public final class AppSettings {
static let alertCPUTemperature = "alertCPUTemperature"
static let alertCPULoad = "alertCPULoad"
static let cpuChartSeconds = "cpuChartSeconds"
static let dashboardEnabled = "dashboardEnabled"
static let dashboardPort = "dashboardPort"
}
}
Loading