import SwiftUI struct ContentView: View { @ObservedObject var monitor: SystemMonitor @ObservedObject private var config = Configuration.shared weak var windowController: FloatingWindowController? init(monitor: SystemMonitor, windowController: FloatingWindowController? = nil) { self.monitor = monitor self.windowController = windowController } var body: some View { VStack(spacing: 0) { // ── 顶部固定区域:图标 + Monitor + 时间 + 关闭按钮 ── headerBar Divider() .background(Color.white.opacity(0.1)) // ── 可滚动内容 ── ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 14) { ForEach(Array(moduleRows.enumerated()), id: \.offset) { _, row in row } } .padding(.top, 6) .padding(.horizontal, 16) .padding(.bottom, 16) } } .frame(minWidth: 240, idealWidth: 280, maxWidth: 400, minHeight: 200, idealHeight: 480, maxHeight: 700) .onAppear { monitor.interval = config.refreshInterval monitor.topN = config.processCount applyOpacity() } .onChange(of: config.processCount) { _, new in monitor.topN = new } .onChange(of: config.opacity) { _, _ in applyOpacity() } .onReceive(NotificationCenter.default.publisher(for: .refreshIntervalChanged)) { notification in if let interval = notification.object as? Double { monitor.interval = interval } } } // MARK: - 顶部栏(精简版) private var headerBar: some View { HStack(spacing: 6) { Image(systemName: "gauge.with.dots.needle.33percent") .font(.system(size: 13, weight: .semibold)) .foregroundColor(.accentColor) Text("Monitor") .font(.system(size: config.fontSize, weight: .semibold)) .foregroundColor(.primary.opacity(0.85)) Spacer() Text(timeString) .font(.system(size: 10, design: .monospaced)) .foregroundColor(.secondary.opacity(0.5)) Button(action: { windowController?.hide() }) { Image(systemName: "xmark") .font(.system(size: 11, weight: .medium)) .foregroundColor(.secondary.opacity(0.5)) } .buttonStyle(.plain) .help("隐藏窗口(点击菜单栏图标重新打开)") } .padding(.horizontal, 16) .padding(.vertical, 10) } // MARK: - 各指标 private var cpuGauge: some View { StyledGauge( title: "CPU", icon: "cpu", value: monitor.info.cpuUsage, unit: "%", detail: "\(monitor.info.cpuCores) 核心 · \(monitor.info.cpuModel)", style: config.cpuStyle, fontSize: config.fontSize, animateGraphic: config.gaugeAnimationEnabled, animateText: config.textAnimationEnabled ) } private var gpuGauge: some View { StyledGauge( title: "GPU", icon: "display", value: monitor.info.gpuUsage ?? 0, unit: "%", detail: monitor.info.gpuModel + (monitor.info.gpuUsage == nil ? " (N/A)" : ""), style: config.gpuStyle, fontSize: config.fontSize, animateGraphic: config.gaugeAnimationEnabled, animateText: config.textAnimationEnabled ) } private var memoryGauge: some View { StyledGauge( title: "内存", icon: "memorychip", value: monitor.info.memoryUsagePercent, unit: "%", detail: formatBytes(monitor.info.memoryUsed) + " / " + formatBytes(monitor.info.memoryTotal), style: config.memoryStyle, fontSize: config.fontSize, animateGraphic: config.gaugeAnimationEnabled, animateText: config.textAnimationEnabled ) } private var swapGauge: some View { StyledGauge( title: "Swap", icon: "arrow.left.arrow.right", value: monitor.info.swapUsagePercent, unit: "%", detail: formatBytes(monitor.info.swapUsed) + " / " + formatBytes(monitor.info.swapTotal), style: config.swapStyle, fontSize: config.fontSize, animateGraphic: config.gaugeAnimationEnabled, animateText: config.textAnimationEnabled ) } private var cpuTemperatureGauge: some View { let temp = monitor.info.cpuTemperature let hasData = temp != nil let v = temp ?? 0 return StyledGauge( title: "CPU 温度", icon: "thermometer.medium", value: v, unit: "°C", detail: hasData ? "" : "N/A", style: config.cpuTemperatureStyle, fontSize: config.fontSize, maxValue: 120, animateGraphic: config.gaugeAnimationEnabled, animateText: config.textAnimationEnabled ) .opacity(hasData ? 1.0 : 0.5) } private var gpuTemperatureGauge: some View { let temp = monitor.info.gpuTemperature let hasData = temp != nil let v = temp ?? 0 return StyledGauge( title: "GPU 温度", icon: "thermometer.low", value: v, unit: "°C", detail: hasData ? "" : "N/A", style: config.gpuTemperatureStyle, fontSize: config.fontSize, maxValue: 120, animateGraphic: config.gaugeAnimationEnabled, animateText: config.textAnimationEnabled ) .opacity(hasData ? 1.0 : 0.5) } private var processesSection: some View { VStack(spacing: 12) { ProcessListView( processes: monitor.info.topCPUProcesses, title: "CPU 消耗 Top", icon: "bolt.horizontal", isMemory: false, fontSize: config.fontSize ) ProcessListView( processes: monitor.info.topMemoryProcesses, title: "内存消耗 Top", icon: "tray.full", isMemory: true, fontSize: config.fontSize ) } } // MARK: - 布局引擎(左/右/全宽排列) /// 根据行号 + 列位生成布局 private var moduleRows: [AnyView] { struct Item { let name: String let row: Int let position: ModulePosition let view: AnyView } var items: [Item] = [] if config.showCPU { items.append(Item(name: "CPU", row: config.cpuRow, position: config.cpuPos, view: AnyView(cpuGauge))) } if config.showGPU { items.append(Item(name: "GPU", row: config.gpuRow, position: config.gpuPos, view: AnyView(gpuGauge.opacity(monitor.info.gpuUsage == nil ? 0.5 : 1.0)))) } if config.showMemory { items.append(Item(name: "内存", row: config.memoryRow, position: config.memoryPos, view: AnyView(memoryGauge))) } if config.showSwap { items.append(Item(name: "Swap", row: config.swapRow, position: config.swapPos, view: AnyView(swapGauge))) } if config.showCPUTemperature { items.append(Item(name: "CPU温度", row: config.cpuTemperatureRow, position: config.cpuTemperaturePos, view: AnyView(cpuTemperatureGauge))) } if config.showGPUTemperature { items.append(Item(name: "GPU温度", row: config.gpuTemperatureRow, position: config.gpuTemperaturePos, view: AnyView(gpuTemperatureGauge))) } if config.showTopProcesses { items.append(Item(name: "进程", row: config.processesRow, position: config.processesPos, view: AnyView(processesSection))) } // 按行号排序 items.sort { $0.row < $1.row } // 同行 left+right 配对,full 独占 var rows: [AnyView] = [] var i = 0 while i < items.count { let item = items[i] switch item.position { case .full: rows.append(item.view) i += 1 case .left: // 找同行下一个 right if let j = (i+1.. String { if bytes >= 1_073_741_824 { return String(format: "%.1f GB", Double(bytes) / 1_073_741_824) } else if bytes >= 1_048_576 { return String(format: "%.0f MB", Double(bytes) / 1_048_576) } else if bytes >= 1_024 { return String(format: "%.0f KB", Double(bytes) / 1_024) } else { return "\(bytes) B" } } private func applyOpacity() { DispatchQueue.main.async { windowController?.applyOpacity(config.opacity) } } }