From 58544ecc0dc91aa5d03d441e260b81b3b9f76e7a Mon Sep 17 00:00:00 2001 From: ysicing Date: Thu, 1 Oct 2026 11:02:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(review):=20=E6=94=B6=E6=95=9B=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E5=88=86=E7=BB=84=E9=94=AE=E5=B9=B6=E8=A1=A5=E5=BC=BA?= =?UTF-8?q?=E6=97=A5=E5=8E=86=E6=B5=8B=E9=87=8F=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ProcessUsage.rowGroupID,进程表与磁盘排行共用同一分组标识 - 弹窗额度读数只读取 AI 项目 - 合并预览栅格化的两处降级分支 - 日历测量测试改为方向性断言,去掉固定等待 --- .../XStatsUI/AppKit/MenuBarRenderer.swift | 16 +++---- .../XStatsUI/Panel/ProcessesPage.swift | 9 +++- .../XStatsUI/Popover/CombinedPopover.swift | 2 +- .../Sources/XStatsUI/State/MetricsStore.swift | 6 +-- .../CalendarEnhancementTests.swift | 43 +++++++++++-------- .../Tests/XStatsUITests/XStatsUITests.swift | 1 + 6 files changed, 43 insertions(+), 34 deletions(-) diff --git a/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift b/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift index 032006e..eb3e9c0 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift @@ -325,16 +325,14 @@ enum MenuBarRenderer { if let appearance { appearance.performAsCurrentDrawingAppearance(content) } else { content() } } let scale = backingScale ?? NSScreen.main?.backingScaleFactor ?? 2 - guard let rep = NSBitmapImageRep(bitmapDataPlanes: nil, - pixelsWide: Int(ceil(image.size.width * scale)), - pixelsHigh: Int(ceil(image.size.height * scale)), - bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, - colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0) else { - return NSImage(size: image.size, flipped: false) { draw($0); return true } - } + let rep = NSBitmapImageRep(bitmapDataPlanes: nil, + pixelsWide: Int(ceil(image.size.width * scale)), + pixelsHigh: Int(ceil(image.size.height * scale)), + bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, + colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0) // 逻辑尺寸决定上下文的点到像素缩放,必须在创建上下文前设置。 - rep.size = image.size - guard let context = NSGraphicsContext(bitmapImageRep: rep) else { + rep?.size = image.size + guard let rep, let context = NSGraphicsContext(bitmapImageRep: rep) else { return NSImage(size: image.size, flipped: false) { draw($0); return true } } NSGraphicsContext.saveGraphicsState() diff --git a/Packages/XStatsKit/Sources/XStatsUI/Panel/ProcessesPage.swift b/Packages/XStatsKit/Sources/XStatsUI/Panel/ProcessesPage.swift index 2596966..e39d2d1 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Panel/ProcessesPage.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Panel/ProcessesPage.swift @@ -205,6 +205,11 @@ struct ProcessesPage: View { // MARK: - 行模型 +extension ProcessUsage { + /// 进程表的行标识:同一应用的进程合并为一行,其余按 PID 区分;磁盘排行沿用同一标识 + var rowGroupID: String { appBundlePath.map { "app-\($0)" } ?? "pid-\(pid)" } +} + struct ProcessRowModel: Identifiable { let id: String let title: String @@ -245,7 +250,7 @@ struct ProcessRowModel: Identifiable { private init(group: [ProcessUsage], key: String, title: String) { let representative = group.max { $0.memory < $1.memory } ?? group[0] - id = "app-\(key)" + id = key self.title = title subtitle = group.count > 1 ? tr("\(group.count) 个进程") : "PID \(representative.pid)" bundlePath = representative.appBundlePath @@ -271,7 +276,7 @@ struct ProcessRowModel: Identifiable { static func grouped(_ processes: [ProcessUsage]) -> [ProcessRowModel] { var buckets: [String: [ProcessUsage]] = [:] for process in processes { - buckets[process.appBundlePath ?? "pid-\(process.pid)", default: []].append(process) + buckets[process.rowGroupID, default: []].append(process) } return buckets.map { key, group in if let bundle = group[0].appBundlePath { diff --git a/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift b/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift index 4a481c0..5c1a679 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Popover/CombinedPopover.swift @@ -312,7 +312,7 @@ private struct OverviewReading { } case .aiUsage: if !model.settings.aiUsageShowsLocalUsage { - if let quota = MenuBarReading(model: model).aiQuotas.min(by: { $0.remainingPercent < $1.remainingPercent }) { + if let quota = MenuBarReading(model: model, items: [.aiUsage]).aiQuotas.min(by: { $0.remainingPercent < $1.remainingPercent }) { value = "\(quota.remainingPercent)" unit = "%" detail = quota.sourceName + " · " + tr("订阅额度") diff --git a/Packages/XStatsKit/Sources/XStatsUI/State/MetricsStore.swift b/Packages/XStatsKit/Sources/XStatsUI/State/MetricsStore.swift index 0f9b1cc..89e94c7 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/State/MetricsStore.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/State/MetricsStore.swift @@ -109,11 +109,11 @@ public final class MetricsStore { if let processes = snapshot.processes { self.processes = processes // 排行只需要磁盘计数,不必构建带名称、文案和其他指标的完整界面行。 - // 分组键与 ProcessRowModel 保持一致;同组有读取计数时才采用写入合计, - // 保留混合权限进程和缺失计数的既有语义,并分别累加读写以保持浮点运算顺序。 + // 同组有读取计数时才采用写入合计,保留混合权限进程和缺失计数的既有语义, + // 并分别累加读写以保持浮点运算顺序。 var totals: [String: (read: Double?, write: Double)] = [:] for process in processes { - let id = process.appBundlePath.map { "app-\($0)" } ?? "pid-\(process.pid)" + let id = process.rowGroupID var total = totals[id] ?? (nil, 0) if let read = process.diskRead { total.read = (total.read ?? 0) + read } total.write += process.diskWrite ?? 0 diff --git a/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift b/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift index 4a412fe..14de6d5 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift @@ -12,32 +12,37 @@ import Testing struct CalendarEnhancementTests { let zone = TimeZone(identifier: "Asia/Shanghai")! - @Test(arguments: [false, true], [false, true]) - func detachedCalendarMeasurementIncludesEnabledContent(showsDayDetails: Bool, showsHolidayOverview: Bool) async throws { + @Test(arguments: [false, true]) + func detachedCalendarMeasurementIncludesEnabledContent(showsDayDetails: Bool) throws { let name = "CalendarMeasurementTests.\(UUID())" let defaults = try #require(UserDefaults(suiteName: name)) defer { defaults.removePersistentDomain(forName: name) } let settings = AppSettings(defaults: defaults, calendarLocale: Locale(identifier: "zh_CN")) settings.calendarFeatures = [.lunar, .holidays, .festivals, .seasonalInfo] - settings.calendarPreferences.showHolidayOverview = showsHolidayOverview let model = AppModel(settings: settings, historyURL: nil, aiUsageProviders: [], aiQuotaProviders: []) let date = try #require(ISO8601DateFormatter().date(from: "2026-09-23T04:00:00Z")) - let hosting = NSHostingView(rootView: CalendarPopover(referenceDate: date, showsDayDetails: showsDayDetails, - firstWeekday: settings.calendarFirstWeekday) - .environment(model) - .environment(\.isSnapshot, true)) - // StatusPanel 在挂载前测量;启用的卡片必须已计入高度,不能等 onAppear 后才补齐。 - let measured = hosting.fittingSize - #expect(hosting.window == nil) - let window = NSWindow(contentRect: NSRect(origin: CGPoint(x: -10_000, y: -10_000), size: measured), - styleMask: .borderless, backing: .buffered, defer: false) - window.isReleasedWhenClosed = false - defer { window.close() } - window.contentView = hosting - window.orderFrontRegardless() - hosting.layoutSubtreeIfNeeded() - try await Task.sleep(for: .milliseconds(150)) - #expect(abs(hosting.fittingSize.height - measured.height) < 1) + // StatusPanel 在挂载前测量,此时 onAppear 尚未运行;启用的卡片必须已计入这次测量的高度。 + let measuredHeight = { (showsHolidayOverview: Bool) -> CGFloat in + settings.calendarPreferences.showHolidayOverview = showsHolidayOverview + let hosting = NSHostingView(rootView: CalendarPopover(referenceDate: date, showsDayDetails: showsDayDetails, + firstWeekday: settings.calendarFirstWeekday) + .environment(model) + .environment(\.isSnapshot, true)) + return hosting.fittingSize.height + } + let withoutOverview = measuredHeight(false) + let withOverview = measuredHeight(true) + if showsDayDetails { + // 详情页不显示假期概览;黄历宜忌须已计入,弹窗不能比单独的黄历卡片还矮。 + #expect(withOverview == withoutOverview) + let day = try #require(CalendarEngine.today(at: date)) + let almanac = NSHostingView(rootView: CalendarAlmanacView(day: day, almanac: CalendarEngine.almanac(for: day), + features: settings.calendarFeatures) + .frame(width: CalendarPopoverSizing.standard.width)) + #expect(withOverview > almanac.fittingSize.height) + } else { + #expect(withOverview > withoutOverview) + } } @Test func calendarLayoutDoesNotFeedOverflowBackIntoWindowSize() async throws { diff --git a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift index 90b7b66..92b16d7 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift @@ -227,6 +227,7 @@ private func isolatedDefaults() -> UserDefaults { let center = try #require(rep.colorAt(x: rep.pixelsWide / 2, y: rep.pixelsHigh / 2)?.usingColorSpace(.deviceRGB)) #expect(center.alphaComponent > 0.99) #expect((center.redComponent > 0.5) == dark) + // 源图铺满整个画布,四角也应被着色;缩放错位时角落会留空或越界。 for (x, y) in [(0, 0), (rep.pixelsWide - 1, 0), (0, rep.pixelsHigh - 1), (rep.pixelsWide - 1, rep.pixelsHigh - 1)] { let corner = try #require(rep.colorAt(x: x, y: y)?.usingColorSpace(.deviceRGB)) #expect(corner.alphaComponent > 0.99)