From 958d566870a61b6af854c7176d9ab96be525fe27 Mon Sep 17 00:00:00 2001 From: gentpan Date: Sun, 13 Sep 2026 06:18:50 +0500 Subject: [PATCH] =?UTF-8?q?=E9=87=8A=E6=94=BE=E5=86=85=E5=AD=98=E7=A7=BB?= =?UTF-8?q?=E5=88=B0=E6=A0=87=E9=A2=98=E6=A0=8F=EF=BC=9B=E4=B8=BB=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E4=B8=8E=E8=AE=BE=E7=BD=AE=E7=AA=97=E5=8F=A3=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E6=9B=B4=E5=A4=A7=E4=B8=94=E5=8F=AF=E8=B0=83=E6=95=B4?= =?UTF-8?q?=EF=BC=9B=E6=BB=9A=E5=8A=A8=E6=9D=A1=E5=8F=AA=E5=9C=A8=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E6=97=B6=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 ++ .../OpenStatsUI/AppKit/AppController.swift | 11 +++-- .../AppKit/MainWindowController.swift | 10 +++-- .../OpenStatsUI/Design/Components.swift | 3 +- .../OpenStatsUI/Design/PanelContainer.swift | 44 +++++++++++++++++++ .../Sources/OpenStatsUI/Design/Tokens.swift | 4 ++ .../OpenStatsUI/Panel/MainWindowView.swift | 1 + .../OpenStatsUI/Popover/MetricPopovers.swift | 35 +++++++++------ .../OpenStatsUI/Popover/PopoverRootView.swift | 1 + .../OpenStatsUI/Settings/SettingsView.swift | 8 ++-- README.md | 6 ++- README.zh-CN.md | 6 ++- web/index.html | 4 +- 13 files changed, 106 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b68f7..e3d3660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ #### 样式 +- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。 +- 主窗口默认 1072×720、设置窗口默认 920×720,两者都可自由调整大小;滚动条改为浮层样式,只在滚动时出现。 + - 菜单栏字号缩小并统一:两行布局为 7pt 标签加 10pt 数值,单行布局 11pt,网速两行 9pt;每种风格内部只用一套字号,指标之间留出更宽的间距。 - 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。 - 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。 diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift index d6ba016..e90c2ef 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift @@ -92,8 +92,11 @@ public final class AppController: NSObject, NSApplicationDelegate, NSWindowDeleg private func showSettings() { menuBar.dismissPopovers() if settingsWindow == nil { - let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: DS.Size.settingsWidth, height: DS.Size.settingsHeight), - styleMask: [.titled, .closable, .miniaturizable], + let visible = NSScreen.main?.visibleFrame.size ?? DS.Size.settingsDefaultSize + let window = NSWindow(contentRect: NSRect(x: 0, y: 0, + width: min(DS.Size.settingsDefaultSize.width, visible.width - DS.Space.s12), + height: min(DS.Size.settingsDefaultSize.height, visible.height - DS.Space.s12)), + styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false) window.title = "OpenStats 设置" @@ -101,7 +104,9 @@ public final class AppController: NSObject, NSApplicationDelegate, NSWindowDeleg window.isReleasedWhenClosed = false window.delegate = self window.contentView = NSHostingView(rootView: SettingsView().environment(model)) - window.center() + window.contentMinSize = NSSize(width: DS.Size.settingsWidth, height: DS.Size.settingsHeight) + window.setFrameAutosaveName("OpenStatsSettingsWindow") + if window.frame.width < DS.Size.settingsDefaultSize.width { window.center() } settingsWindow = window } NSApp.activate() diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainWindowController.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainWindowController.swift index 429c08a..494abff 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainWindowController.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainWindowController.swift @@ -30,7 +30,8 @@ final class MainWindowController: NSObject, NSWindowDelegate { private func makeWindow() -> NSWindow { let width = DS.Size.settingsSidebar + DS.Size.panelWidth - let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: width, height: DS.Size.panelMinHeight * 2), + let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: DS.Size.settingsSidebar + DS.Size.windowDefaultContentWidth, + height: DS.Size.panelMinHeight * 2), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false) @@ -39,7 +40,8 @@ final class MainWindowController: NSObject, NSWindowDelegate { window.isReleasedWhenClosed = false window.delegate = self window.contentMinSize = NSSize(width: width, height: DS.Size.panelMinHeight * 1.5) - window.setFrameAutosaveName("OpenStatsMainWindow") + // 默认尺寸调大后换一个保存名,旧的小窗口尺寸不再沿用 + window.setFrameAutosaveName("OpenStatsMainWindow.v2") window.contentView = NSHostingView(rootView: MainWindowView().environment(model)) return window } @@ -52,7 +54,9 @@ final class MainWindowController: NSObject, NSWindowDelegate { } let measuring = NSHostingView(rootView: MainWindowView().environment(model).environment(\.isSnapshot, true)) let height = min(measuring.fittingSize.height, visible.height - DS.Space.s12) - let size = NSSize(width: window.frame.width, height: max(height, DS.Size.panelMinHeight * 1.5)) + // 至少 720pt 高(屏幕放得下时),页面较短也不会开成一个小窗口 + let size = NSSize(width: min(window.frame.width, visible.width - DS.Space.s12), + height: min(max(height, DS.Size.settingsDefaultSize.height), visible.height - DS.Space.s12)) return NSRect(x: visible.midX - size.width / 2, y: visible.midY - size.height / 2, width: size.width, height: size.height) } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Components.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Components.swift index 68cb8f1..fac2702 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Components.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Components.swift @@ -496,8 +496,7 @@ struct PageScroll: View { stack } else { // 内容放得下时不回弹,避免点击时整页轻微抖动 - ScrollView { stack } - .scrollIndicators(.never) + ScrollView { stack.overlayScrollers() } .scrollBounceBehavior(.basedOnSize) } } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/PanelContainer.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/PanelContainer.swift index 959028c..b38eafc 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/PanelContainer.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/PanelContainer.swift @@ -32,3 +32,47 @@ struct WindowDragArea: NSViewRepresentable { func makeNSView(context: Context) -> DragView { DragView() } func updateNSView(_ nsView: DragView, context: Context) {} } + +/// 滚动条只在滚动时出现:把所在的 NSScrollView 固定为浮层样式。 +/// 系统设置为“始终显示滚动条”或接了鼠标时,SwiftUI 默认会一直显示一条占位的滚动条 +struct OverlayScrollers: NSViewRepresentable { + final class Probe: NSView { + private var observer: NSObjectProtocol? + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + guard window != nil else { + if let observer { NotificationCenter.default.removeObserver(observer) } + observer = nil + return + } + apply() + guard observer == nil else { return } + // 系统切换滚动条样式时会重置,收到通知后重新设置 + observer = NotificationCenter.default.addObserver(forName: NSScroller.preferredScrollerStyleDidChangeNotification, + object: nil, queue: .main) { [weak self] _ in + MainActor.assumeIsolated { self?.apply() } + } + } + + func apply() { + DispatchQueue.main.async { [weak self] in + MainActor.assumeIsolated { + guard let scrollView = self?.enclosingScrollView else { return } + scrollView.scrollerStyle = .overlay + scrollView.autohidesScrollers = true + } + } + } + } + + func makeNSView(context: Context) -> Probe { Probe() } + func updateNSView(_ nsView: Probe, context: Context) { nsView.apply() } +} + +extension View { + /// 放在 ScrollView 的内容里:滚动条改为浮层,只在滚动时显示 + func overlayScrollers() -> some View { + background(OverlayScrollers().frame(width: 0, height: 0)) + } +} diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Tokens.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Tokens.swift index 24e1246..8400fca 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Tokens.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Tokens.swift @@ -132,8 +132,12 @@ extension DS { static let windowHeader: CGFloat = 40 /// 窄控件的固定宽度(分段控件等) static let tileWidth: CGFloat = (panelWidth - Space.s3 * 4) / 3 + /// 设置窗口的最小尺寸;默认打开时更大,可自由调整 static let settingsWidth: CGFloat = 720 static let settingsHeight: CGFloat = 520 + static let settingsDefaultSize = CGSize(width: 920, height: 720) + /// 主窗口默认内容区宽度(侧边栏另计) + static let windowDefaultContentWidth: CGFloat = 880 static let settingsSidebar: CGFloat = 192 static let valueColumn: CGFloat = 64 static let labelColumn: CGFloat = 48 diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift index 2e0c477..277457d 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift @@ -112,6 +112,7 @@ private struct PageHeader: View { if model.keepAwake.isActive { Chip(text: "防休眠已开启", icon: "cup.and.saucer.fill", tone: .primary) } + if tab == .memory { PurgeMemoryButton() } if let item = tab.menuBarItem { Text("在菜单栏显示").dsFont(.xs).foregroundStyle(DS.Palette.textSecondary) DSToggle(isOn: Binding(get: { settings.isEnabled(item) }, set: { settings.setEnabled(item, $0) }), diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/MetricPopovers.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/MetricPopovers.swift index f04bea1..bd4a07b 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/MetricPopovers.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/MetricPopovers.swift @@ -193,7 +193,6 @@ struct MemoryPopover: View { } } } - PurgeMemoryRow() } case .memoryCompression: SectionCard(title: section.title) { @@ -397,25 +396,35 @@ private struct ExplainableRow: View { } } -private struct PurgeMemoryRow: View { +/// 标题栏里的“释放内存”:执行中显示进度,完成后短暂显示结果再恢复 +struct PurgeMemoryButton: View { @Environment(AppModel.self) private var model + @State private var result: MaintenanceController.Outcome? var body: some View { let maintenance = model.maintenance - HStack(spacing: DS.Space.s2) { - Button(maintenance.running == .purgeMemory ? "正在释放…" : "释放内存") { - Task { await maintenance.run(.purgeMemory) } + let running = maintenance.running == .purgeMemory + Button { + Task { + await maintenance.run(.purgeMemory) + result = maintenance.outcomes[.purgeMemory] + try? await Task.sleep(for: .seconds(3)) + result = nil } - .buttonStyle(DSButtonStyle(kind: .secondary)) - .disabled(maintenance.running != nil) - if let outcome = maintenance.outcomes[.purgeMemory] { - Text(outcome.text) - .dsFont(.xs) - .foregroundStyle(outcome.isError ? DS.Palette.error : DS.Palette.textSecondary) - .lineLimit(2) + } label: { + HStack(spacing: DS.Space.s1) { + if running { + ProgressView().controlSize(.mini) + } else { + Image(systemName: result == nil ? "wind" : result!.isError ? "exclamationmark.triangle" : "checkmark") + } + Text(running ? "正在释放…" : result?.text ?? "释放内存").lineLimit(1) } - Spacer(minLength: 0) + .foregroundStyle(result?.isError == true ? DS.Palette.error : result != nil ? DS.Palette.success : DS.Palette.textPrimary) } + .buttonStyle(DSButtonStyle(kind: .secondary)) + .disabled(maintenance.running != nil) + .help("清理可回收的缓存内存(需要管理员授权或辅助工具)") } } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/PopoverRootView.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/PopoverRootView.swift index d499a31..68f0281 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/PopoverRootView.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/PopoverRootView.swift @@ -49,6 +49,7 @@ private struct PopoverHeader: View { .dsFont(.base, weight: .semibold) .foregroundStyle(DS.Palette.textPrimary) Spacer(minLength: DS.Space.s2) + if item == .memory { PurgeMemoryButton() } IconButton(systemName: "macwindow", help: "打开 OpenStats 主窗口") { model.openMainWindow(nil) } IconButton(systemName: "gearshape", help: "设置") { model.openSettings() } } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift index 320ca37..1e425b2 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift @@ -81,7 +81,9 @@ public struct SettingsView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - .frame(width: DS.Size.settingsWidth, height: isSnapshot ? nil : DS.Size.settingsHeight) + .frame(width: isSnapshot ? DS.Size.settingsWidth : nil) + .frame(minWidth: DS.Size.settingsWidth, maxWidth: .infinity, + minHeight: isSnapshot ? nil : DS.Size.settingsHeight, maxHeight: isSnapshot ? nil : .infinity) .fixedSize(horizontal: false, vertical: isSnapshot) .background(DS.Palette.background) .ignoresSafeArea() @@ -107,8 +109,8 @@ private struct SettingsPage: View { if isSnapshot { stack } else { - ScrollView { stack } - .scrollIndicators(.automatic) + ScrollView { stack.overlayScrollers() } + .scrollBounceBehavior(.basedOnSize) } } } diff --git a/README.md b/README.md index 1e3847a..629ef62 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,10 @@ Developed and tested on Apple Silicon. The interface is in Simplified Chinese. -Not released yet · **38** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese) +Not released yet · **40** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
-2026-09-13 · Unreleased · 27 added · 5 style · 6 fixed +2026-09-13 · Unreleased · 27 added · 7 style · 6 fixed **Added** @@ -83,6 +83,8 @@ Not released yet · **38** changes in development · [full changelog](CHANGELOG. **Style** +- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。 +- 主窗口默认 1072×720、设置窗口默认 920×720,两者都可自由调整大小;滚动条改为浮层样式,只在滚动时出现。 - 菜单栏字号缩小并统一:两行布局为 7pt 标签加 10pt 数值,单行布局 11pt,网速两行 9pt;每种风格内部只用一套字号,指标之间留出更宽的间距。 - 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。 - 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。 diff --git a/README.zh-CN.md b/README.zh-CN.md index 07924c1..254282e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -44,10 +44,10 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/ -尚未发布正式版本 · 开发中 **38** 项改动尚未发布 · [完整更新日志](CHANGELOG.md) +尚未发布正式版本 · 开发中 **40** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
-2026-09-13 · 未发布 · 新增 27 · 样式 5 · 修复 6 +2026-09-13 · 未发布 · 新增 27 · 样式 7 · 修复 6 **新增** @@ -81,6 +81,8 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/ **样式** +- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。 +- 主窗口默认 1072×720、设置窗口默认 920×720,两者都可自由调整大小;滚动条改为浮层样式,只在滚动时出现。 - 菜单栏字号缩小并统一:两行布局为 7pt 标签加 10pt 数值,单行布局 11pt,网速两行 9pt;每种风格内部只用一套字号,指标之间留出更宽的间距。 - 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。 - 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。 diff --git a/web/index.html b/web/index.html index 47a86c3..a2c520d 100644 --- a/web/index.html +++ b/web/index.html @@ -347,7 +347,7 @@
未发布
-

新增 27 · 样式 5 · 修复 6

+

新增 27 · 样式 7 · 修复 6

  • 新增菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。
  • 新增菜单栏风格:双行文字、单行文字、图标、圆环、饼图、柱状历史、电量条、状态圆点八种,整体选择一种统一套用,也可以给个别指标单独指定。设置里每种风格都有用示例数据画出的预览和说明,柱状历史注明是最近 10 次采样的变化。
  • @@ -358,7 +358,7 @@
  • 新增网络详情:上下镜像的流量历史;连接探测格子(每 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 可能被接管。
- 还有 30 项 → + 还有 32 项 →