From d279f5083fcfdce95b84a7c55d4788f7dca6f3e7 Mon Sep 17 00:00:00 2001 From: ysicing Date: Sun, 4 Oct 2026 14:17:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(fan):=20=E4=BB=85=E5=9C=A8=E6=97=A0?= =?UTF-8?q?=E9=A3=8E=E6=89=87=E8=BD=AC=E5=8A=A8=E6=97=B6=E6=B1=87=E6=80=BB?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=90=AF=E5=8A=A8=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 多风扇机型上任一风扇起步会隐藏其他风扇的实际转速,改为没有风扇在转且存在起步窗口时才显示启动中。 --- Packages/XStatsKit/Sources/SMC/FanControl.swift | 7 +++++++ .../Sources/XStatsUI/Panel/OverviewPage.swift | 4 ++-- .../XStatsUI/Popover/CombinedPopover.swift | 4 ++-- .../XStatsUI/Popover/MetricPopovers.swift | 6 +++--- .../Tests/MetricsTests/FanStartupTests.swift | 16 ++++++++++++++++ 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Packages/XStatsKit/Sources/SMC/FanControl.swift b/Packages/XStatsKit/Sources/SMC/FanControl.swift index 00821b8..2d91651 100644 --- a/Packages/XStatsKit/Sources/SMC/FanControl.swift +++ b/Packages/XStatsKit/Sources/SMC/FanControl.swift @@ -36,6 +36,13 @@ public struct FanState: Sendable, Equatable, Identifiable { } } +extension Array where Element == FanState { + /// 汇总值是否显示“启动中”:仅当没有任何风扇在转、且至少一个处于起步窗口 + public var isStartingUp: Bool { + !contains { $0.current > 0 } && contains(where: \.isStarting) + } +} + /// 部分机型没有起步状态键,只能以手动目标转速推断。两种来源均最多提示 10 秒, /// 超时后保留真实的 0 RPM,直到风扇转动或起步条件消失才允许开始新的窗口。 struct FanStartupTracker { diff --git a/Packages/XStatsKit/Sources/XStatsUI/Panel/OverviewPage.swift b/Packages/XStatsKit/Sources/XStatsUI/Panel/OverviewPage.swift index f633043..67efd10 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Panel/OverviewPage.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Panel/OverviewPage.swift @@ -297,11 +297,11 @@ private struct FanTile: View { .foregroundStyle(DS.Palette.textSecondary) HStack(alignment: .firstTextBaseline, spacing: DS.Space.s1 / 2) { - Text(verbatim: fans.contains(where: \.isStarting) ? tr("启动中…") : fastest.map { Int($0.current).formatted() } ?? "—") + Text(verbatim: fans.isStartingUp ? tr("启动中…") : fastest.map { Int($0.current).formatted() } ?? "—") .dsFont(.xl, weight: .semibold) .monospacedDigit() .foregroundStyle(DS.Palette.textPrimary) - if !fans.contains(where: \.isStarting) { + if !fans.isStartingUp { Text(verbatim: "RPM").dsFont(.xs, weight: .medium).foregroundStyle(DS.Palette.textSecondary) } } diff --git a/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift b/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift index 0f629de..d25f279 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift @@ -295,8 +295,8 @@ struct OverviewReading { case .fan: let fans = store.sensors?.fans ?? [] if let fastest = store.fastestFan { - value = fans.contains(where: \.isStarting) ? tr("启动中…") : Int(fastest.current).formatted() - unit = fans.contains(where: \.isStarting) ? nil : "RPM" + value = fans.isStartingUp ? tr("启动中…") : Int(fastest.current).formatted() + unit = fans.isStartingUp ? nil : "RPM" let average = fans.map { $0.maximum > 0 ? $0.current / $0.maximum : 0 }.reduce(0, +) / Double(max(1, fans.count)) chart = .level(average, DS.Palette.primary) } diff --git a/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift b/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift index 9a49810..5bd38a3 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift @@ -720,8 +720,8 @@ struct ThermalPopover: View { if model.store.supportsFans { VStack(alignment: .leading, spacing: DS.Space.s1) { Text(tr("风扇")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary) - HeroValue(value: fans.contains(where: \.isStarting) ? tr("启动中…") : store.fastestFan.map { Int($0.current).formatted() } ?? "—", - unit: fans.isEmpty || fans.contains(where: \.isStarting) ? nil : "RPM") + HeroValue(value: fans.isStartingUp ? tr("启动中…") : store.fastestFan.map { Int($0.current).formatted() } ?? "—", + unit: fans.isEmpty || fans.isStartingUp ? nil : "RPM") } .frame(maxWidth: .infinity, alignment: .leading) } @@ -782,7 +782,7 @@ struct ThermalPopover: View { @MainActor func fanStatusText(fans: [FanState], mode: FanController.Mode) -> String { if fans.isEmpty { return tr("未检测到风扇") } - if fans.contains(where: \.isStarting) { return tr("正在启动风扇…") } + if fans.isStartingUp { return tr("正在启动风扇…") } guard fans.contains(where: \.isManual) else { return tr("由 macOS 调节") } return mode == .automatic ? tr("其他程序手动控制") : tr("XStats 控制中") } diff --git a/Packages/XStatsKit/Tests/MetricsTests/FanStartupTests.swift b/Packages/XStatsKit/Tests/MetricsTests/FanStartupTests.swift index 9beeb4a..915a5ce 100644 --- a/Packages/XStatsKit/Tests/MetricsTests/FanStartupTests.swift +++ b/Packages/XStatsKit/Tests/MetricsTests/FanStartupTests.swift @@ -71,3 +71,19 @@ struct FanStartupTests { #expect(!afterRepeatedFailure) } } + +struct FanStartupSummaryTests { + private func fan(_ id: Int, current: Double, starting: Bool = false) -> FanState { + FanState(id: id, current: current, minimum: 1000, maximum: 5000, target: 2000, isManual: true, isStarting: starting) + } + + @Test func spinningFanKeepsSummaryWhileAnotherStarts() { + #expect(![fan(0, current: 2400), fan(1, current: 0, starting: true)].isStartingUp) + } + + @Test func summaryStartsOnlyWhenNoFanSpins() { + #expect([fan(0, current: 0), fan(1, current: 0, starting: true)].isStartingUp) + #expect(![fan(0, current: 0), fan(1, current: 0)].isStartingUp) + #expect(![FanState]().isStartingUp) + } +}