import SwiftUI struct LogsView: View { private enum Tab: Hashable { case runtime case connections case diagnostics } @StateObject private var model: LogsViewModel @State private var tab: Tab = .runtime @State private var showsPruneDialog = false init(model: LogsViewModel? = nil) { _model = StateObject(wrappedValue: model ?? DefaultContainer.makeLogsModel()) } var body: some View { ZStack { AITheme.canvas.ignoresSafeArea() ScrollView { LazyVStack(spacing: 12) { if let errorMessage = model.errorMessage { AIStatusBanner(errorMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle") } if model.snapshot == nil && model.isLoading { AILoadingCard("正在同步日志中心") } else { stats Picker("视图", selection: $tab) { Text("运行日志").tag(Tab.runtime) Text("连接日志").tag(Tab.connections) Text("设备诊断").tag(Tab.diagnostics) } .pickerStyle(.segmented) filterBar switch tab { case .runtime: runtimeSection case .connections: connectionSection case .diagnostics: diagnosticSection } } } .padding(16) } } .navigationTitle("日志中心") .toolbar { ToolbarItemGroup(placement: .primaryAction) { AIToolbarButton("刷新", systemImage: "arrow.clockwise") { Task { await model.refresh() } } .disabled(model.isLoading) } } .confirmationDialog( "清理 7 天前的连接日志?操作不可恢复。", isPresented: $showsPruneDialog, titleVisibility: .visible ) { Button("清理连接日志", role: .destructive) { Task { _ = await model.pruneConnections(olderThanDays: 7) } } Button("取消", role: .cancel) {} } .refreshable { await model.refresh() } .task { await model.refresh() } } private var stats: some View { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { AIStatCard(title: "日志", value: "\(model.snapshot?.runtime.count ?? 0)", tint: AITheme.primary) AIStatCard(title: "在线", value: "\(onlineCount)", tint: AITheme.success) AIStatCard(title: "离线", value: "\(model.offlineCount)", tint: AITheme.danger) } } private var filterBar: some View { AICard { VStack(spacing: 10) { AISearchField("搜索日志、错误码或实例", text: $model.searchText) Picker("实例", selection: $model.selectedInstanceID) { Text("全部实例").tag(Optional.none) ForEach(model.instances) { instance in Text(instance.name).tag(Optional(instance.id)) } } .pickerStyle(.menu) .aiSelectSurface() Group { if tab == .runtime { HStack(spacing: 10) { Picker("来源", selection: $model.selectedSource) { Text("全部来源").tag(Optional.none) ForEach(RuntimeLogSource.allCases, id: \.self) { source in Text(source.label).tag(Optional(source)) } } .pickerStyle(.menu) .aiSelectSurface() Picker("级别", selection: $model.selectedLevel) { Text("全部级别").tag(Optional.none) ForEach(RuntimeLogLevel.allCases, id: \.self) { level in Text(level.label).tag(Optional(level)) } } .pickerStyle(.menu) .aiSelectSurface() } .frame(maxWidth: .infinity) } else if tab == .connections { Picker("结果", selection: $model.selectedOutcome) { Text("全部结果").tag(Optional.none) ForEach(ConnectionOutcome.allCases, id: \.self) { outcome in Text(outcome.label).tag(Optional(outcome)) } } .pickerStyle(.menu) .aiSelectSurface() } } .transition(.opacity) } .animation(.easeInOut(duration: 0.18), value: tab) } } private var runtimeSection: some View { AICard { VStack(alignment: .leading, spacing: 10) { AISectionTitle("统一时间线", systemImage: "clock", detail: "\(model.filteredRuntime.count) 条") if model.filteredRuntime.isEmpty { AIEmptyState(systemImage: "clock", title: "暂无运行日志", message: "下拉刷新后会汇总实例日志。") } else { ForEach(model.filteredRuntime) { entry in AIListCard( title: entry.message, subtitle: "\(entry.instanceName ?? "未知实例") · \(entry.occurredAt.formatted())", status: entry.level.label, statusTint: levelTint(entry.level), details: { var values = [entry.source.label] if let duration = entry.durationMs { values.append("耗时 \(duration) ms") } return values }() ) } } } } } private var connectionSection: some View { AICard { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .center, spacing: 10) { AISectionTitle( "连接探测记录", systemImage: "dot.radiowaves.left.and.right", detail: "\(model.filteredConnections.count) 条" ) compactActionButton("清理", systemImage: "trash") { showsPruneDialog = true } } if model.filteredConnections.isEmpty { AIEmptyState(systemImage: "dot.radiowaves.left.and.right", title: "暂无连接日志", message: "连接状态探测后会在这里记录。") } else { ForEach(model.filteredConnections) { entry in AIListCard( title: entry.instanceName, subtitle: "\(entry.observedAt.formatted()) · \(entry.state)", status: entry.outcome.label, statusTint: outcomeTint(entry.outcome), details: [ "HTTP \(entry.httpStatus.map(String.init) ?? "--") · \(entry.durationMs) ms", entry.errorCode ?? "无错误码" ] ) } } } } } private var diagnosticSection: some View { AICard { VStack(alignment: .leading, spacing: 10) { AISectionTitle("设备诊断", systemImage: "iphone.gen3", detail: "\(model.filteredDiagnostics.count) 台") if model.filteredDiagnostics.isEmpty { AIEmptyState(systemImage: "iphone.gen3", title: "暂无设备诊断", message: "已配置实例的设备状态会显示在这里。") } else { ForEach(model.filteredDiagnostics) { diagnostic in AIListCard( title: diagnostic.name, status: diagnostic.healthStatus ?? "未知", statusTint: diagnostic.state == .online ? AITheme.success : AITheme.warning, details: [ diagnostic.origin, "版本 \(diagnostic.version ?? "未知") · \(diagnostic.platform ?? "未知平台")" ] ) { LazyVGrid( columns: [GridItem(.adaptive(minimum: 82), spacing: 6)], alignment: .leading, spacing: 6 ) { ForEach(diagnostic.featureStatus) { feature in AITag(feature.name, tint: featureTint(feature.state)) } if let connection = diagnostic.connection, connection.total > 1 { AITag( "延迟 \(connection.averageDurationMs) ms", tint: connection.averageDurationMs <= 1500 ? AITheme.success : AITheme.warning ) } } } } } } } } private var onlineCount: Int { (model.snapshot?.diagnostics.filter { $0.state == .online }.count) ?? 0 } private func compactActionButton( _ title: String, systemImage: String, action: @escaping () -> Void ) -> some View { Button(action: action) { Label(title, systemImage: systemImage) } .buttonStyle( AIButtonStyle( kind: .secondary, size: .compact, fixedWidth: AITheme.listActionButtonWidth ) ) } private func levelTint(_ level: RuntimeLogLevel) -> Color { switch level { case .info: return AITheme.success case .warning: return AITheme.warning case .error: return AITheme.danger } } private func outcomeTint(_ outcome: ConnectionOutcome) -> Color { switch outcome { case .success: return AITheme.success case .stale: return AITheme.warning case .failed: return AITheme.danger case .unsupported: return AITheme.muted } } private func featureTint(_ state: FeatureStatus.State) -> Color { switch state { case .ready: return AITheme.success case .requiresLogin: return AITheme.warning case .unavailable: return AITheme.muted } } }