fix(fan): 仅在无风扇转动时汇总显示启动中

多风扇机型上任一风扇起步会隐藏其他风扇的实际转速,改为没有风扇在转且存在起步窗口时才显示启动中。
This commit is contained in:
ysicing authored and 缘生 committed 2026-10-04 21:52:14 +08:00
1 parent 9707abba1d
commit d279f5083f
5 files changed
+30 -7

No files matched your search

@@ -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 {
@@ -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)
}
}
@@ -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)
}
@@ -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 控制中")
}
@@ -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)
}
}