From 0829f425cb8cecb1211ca4151671cbbf989195dc Mon Sep 17 00:00:00 2001 From: gentpan Date: Sun, 13 Sep 2026 06:45:57 +0500 Subject: [PATCH] =?UTF-8?q?=E4=BE=A7=E8=BE=B9=E6=A0=8F=E9=80=89=E4=B8=AD?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=BB=91=E5=8A=A8=E7=9A=84=E8=93=9D=E8=89=B2?= =?UTF-8?q?=E5=85=89=E6=9D=A1=EF=BC=9A=E7=BB=86=E7=BA=BF=E5=BA=95=E8=BD=A8?= =?UTF-8?q?=E3=80=81=E5=8F=91=E5=85=89=E6=8C=87=E7=A4=BA=E4=B8=8E=E6=B8=90?= =?UTF-8?q?=E6=B7=A1=E9=AB=98=E4=BA=AE=EF=BC=8C=E5=88=87=E6=8D=A2=E5=B8=A6?= =?UTF-8?q?=E5=9B=9E=E5=BC=B9=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + .../OpenStatsUI/Design/Components.swift | 75 +++++++++++++++++-- .../OpenStatsUI/Panel/MainWindowView.swift | 1 + .../OpenStatsUI/Settings/SettingsView.swift | 1 + README.md | 5 +- README.zh-CN.md | 5 +- web/index.html | 4 +- 7 files changed, 80 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c60786..67a97bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ #### 样式 +- 主窗口与设置窗口的侧边栏选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。 - 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。 - 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。 - 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。 diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Components.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Components.swift index fac2702..1376532 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Components.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Components.swift @@ -211,6 +211,7 @@ struct IconButton: View { // MARK: - 侧边栏 +/// 侧边栏的一项。选中态不填色:由容器上的 `sidebarGlider()` 画左侧滑动的蓝色光条与淡淡的高亮 struct SidebarButton: View { let title: String let symbol: String @@ -227,20 +228,82 @@ struct SidebarButton: View { Text(title).dsFont(.sm, weight: isSelected ? .semibold : .regular) Spacer() } - .foregroundStyle(isSelected ? DS.Palette.onPrimary : DS.Palette.textPrimary) - .padding(.horizontal, DS.Space.s2) + .foregroundStyle(isSelected ? DS.Palette.primary : hovering ? DS.Palette.textPrimary : DS.Palette.textSecondary) + .padding(.leading, DS.Space.s3) + .padding(.trailing, DS.Space.s2) .frame(height: DS.Size.controlHeight) - .background(background, in: RoundedRectangle(cornerRadius: DS.Radius.md)) .contentShape(Rectangle()) + .animation(DS.Motion.quick, value: isSelected) + .animation(DS.Motion.quick, value: hovering) } .buttonStyle(.plain) .onHover { hovering = $0 } .accessibilityAddTraits(isSelected ? .isSelected : []) + .anchorPreference(key: SidebarRowsKey.self, value: .bounds) { [SidebarRow(isSelected: isSelected, bounds: $0)] } } +} - private var background: Color { - if isSelected { return DS.Palette.primary } - return hovering ? DS.Palette.surfaceHover : .clear +struct SidebarRow { + let isSelected: Bool + let bounds: Anchor +} + +struct SidebarRowsKey: PreferenceKey { + static let defaultValue: [SidebarRow] = [] + static func reduce(value: inout [SidebarRow], nextValue: () -> [SidebarRow]) { value += nextValue() } +} + +extension View { + /// 放在包含 SidebarButton 的容器上:左侧一条两端渐隐的细线,选中项位置有一段发光的蓝色光条, + /// 切换时带回弹地滑过去,光条右侧拖出一片渐淡的蓝色高亮 + func sidebarGlider() -> some View { + overlayPreferenceValue(SidebarRowsKey.self) { rows in + GeometryReader { proxy in + SidebarGlider(frames: rows.map { proxy[$0.bounds] }, + selected: rows.first(where: \.isSelected).map { proxy[$0.bounds] }) + } + .allowsHitTesting(false) + } + } +} + +private struct SidebarGlider: View { + let frames: [CGRect] + let selected: CGRect? + + private static let barWidth: CGFloat = 2 + private static let glowWidth = DS.Space.s1 + DS.Space.s1 / 2 + + var body: some View { + if let first = frames.min(by: { $0.minY < $1.minY }), let last = frames.max(by: { $0.maxY < $1.maxY }) { + ZStack(alignment: .topLeading) { + // 底轨:整列的细线,上下两端渐隐 + Rectangle() + .fill(LinearGradient(colors: [.clear, DS.Palette.border, DS.Palette.border, .clear], startPoint: .top, endPoint: .bottom)) + .frame(width: DS.Size.stroke, height: last.maxY - first.minY) + .offset(x: first.minX, y: first.minY) + + if let selected { + Rectangle() + .fill(LinearGradient(colors: [DS.Palette.primary.opacity(0.14), DS.Palette.primary.opacity(0)], + startPoint: .leading, endPoint: .trailing)) + .frame(width: selected.width, height: selected.height) + .offset(x: selected.minX, y: selected.minY) + Capsule() + .fill(DS.Palette.primary) + .frame(width: Self.glowWidth, height: selected.height * 0.6) + .blur(radius: DS.Space.s2) + .offset(x: selected.minX - Self.glowWidth / 2, y: selected.midY - selected.height * 0.3) + Rectangle() + .fill(LinearGradient(colors: [DS.Palette.primary.opacity(0), DS.Palette.primary, DS.Palette.primary.opacity(0)], + startPoint: .top, endPoint: .bottom)) + .frame(width: Self.barWidth, height: selected.height) + .offset(x: selected.minX - Self.barWidth / 2, y: selected.minY) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .animation(.spring(response: 0.45, dampingFraction: 0.6), value: selected) + } } } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift index 32bdd51..1b13a73 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift @@ -77,6 +77,7 @@ private struct MainSidebar: View { IconButton(systemName: "power", help: "退出 OpenStats") { model.quit() } } } + .sidebarGlider() .padding(.horizontal, DS.Space.s3) // 顶部留出窗口红绿灯按钮的位置 .padding(.top, DS.Size.windowHeader + DS.Space.s2) diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift index bd7ae20..47c89be 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift @@ -52,6 +52,7 @@ public struct SettingsView: View { } Spacer() } + .sidebarGlider() .padding(.horizontal, DS.Space.s3) .padding(.top, DS.Size.windowHeader + DS.Space.s2) .padding(.bottom, DS.Space.s3) diff --git a/README.md b/README.md index 8b66e20..1f50c9f 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 · **45** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese) +Not released yet · **46** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
-2026-09-13 · Unreleased · 30 added · 9 style · 6 fixed +2026-09-13 · Unreleased · 30 added · 10 style · 6 fixed **Added** @@ -86,6 +86,7 @@ Not released yet · **45** changes in development · [full changelog](CHANGELOG. **Style** +- 主窗口与设置窗口的侧边栏选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。 - 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。 - 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。 - 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。 diff --git a/README.zh-CN.md b/README.zh-CN.md index 87d7c5e..8b2a1fa 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -44,10 +44,10 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/ -尚未发布正式版本 · 开发中 **45** 项改动尚未发布 · [完整更新日志](CHANGELOG.md) +尚未发布正式版本 · 开发中 **46** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
-2026-09-13 · 未发布 · 新增 30 · 样式 9 · 修复 6 +2026-09-13 · 未发布 · 新增 30 · 样式 10 · 修复 6 **新增** @@ -84,6 +84,7 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/ **样式** +- 主窗口与设置窗口的侧边栏选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。 - 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。 - 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。 - 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。 diff --git a/web/index.html b/web/index.html index 5ebb5da..e313b3e 100644 --- a/web/index.html +++ b/web/index.html @@ -348,7 +348,7 @@
未发布
-

新增 30 · 样式 9 · 修复 6

+

新增 30 · 样式 10 · 修复 6

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