import SwiftUI struct FleetView: View { @StateObject private var model: FleetViewModel @State private var showsDeleteDialog = false init(model: FleetViewModel? = nil) { _model = StateObject(wrappedValue: model ?? DefaultContainer.makeFleetModel()) } var body: some View { ScrollView { LazyVStack(spacing: 12) { searchField if let errorMessage = model.errorMessage { AIStatusBanner( errorMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle" ) } if model.selectionMode { selectionBar } if model.isLoading && model.rows.isEmpty { AILoadingCard("正在同步实例数据") } if model.rows.isEmpty && !model.isLoading { AICard { AIEmptyState( systemImage: "server.rack", title: "暂无实例", message: "添加 SimAdmin 实例后即可开始管理。" ) } } ForEach(model.rows) { row in if model.selectionMode { instanceCard(row) } else { NavigationLink { InstanceDetailView(instance: row.instance) } label: { instanceCard(row) } .buttonStyle(.plain) } } } .padding(16) .padding(.bottom, 8) } .aiBottomActionBar { if !model.selectionMode { AIActionRow { Button { model.selectionMode = true } label: { Label("批量选择", systemImage: "checklist") } .buttonStyle(AIButtonStyle(kind: .secondary, size: .regular, fillsWidth: true)) Button { model.showingForm = true } label: { Label("添加实例", systemImage: "plus") } .buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true)) } } else { AIActionRow { Button { model.selectionMode = false model.selectedIDs.removeAll() } label: { Label("取消选择", systemImage: "xmark.circle") } .buttonStyle(AIButtonStyle(kind: .secondary, size: .regular, fillsWidth: true)) Button { showsDeleteDialog = true } label: { Label( model.selectedIDs.isEmpty ? "删除" : "删除 \(model.selectedIDs.count)", systemImage: "trash" ) } .buttonStyle(AIButtonStyle(kind: .danger, size: .regular, fillsWidth: true)) .disabled(model.selectedIDs.isEmpty) } } } .background(AITheme.canvas.ignoresSafeArea()) .navigationTitle("节点总览") .confirmationDialog( "删除已选实例?登录信息也会一并清除。", isPresented: $showsDeleteDialog, titleVisibility: .visible ) { Button("删除 \(model.selectedIDs.count) 个实例", role: .destructive) { Task { await model.deleteSelected() } } Button("取消", role: .cancel) {} } .refreshable { await model.refresh() } .task { await model.refresh() } .sheet(isPresented: $model.showingForm) { InstanceFormView(model: model) } } private var searchField: some View { AISearchField("搜索实例或地址", text: $model.searchText) { Task { await model.refresh() } } } private var selectionBar: some View { AICard { HStack(spacing: 10) { Button { if model.selectedIDs.count == model.rows.count { model.selectedIDs.removeAll() } else { model.selectedIDs = Set(model.rows.map(\.id)) } } label: { Label( model.selectedIDs.count == model.rows.count ? "取消全选" : "全选", systemImage: "checkmark.circle" ) } .buttonStyle( AIButtonStyle( kind: .secondary, size: .regular, fixedWidth: 96 ) ) Spacer() Text("已选 \(model.selectedIDs.count)") .font(.subheadline.weight(.semibold)) .foregroundStyle(AITheme.muted) } } } private func instanceCard(_ row: FleetViewModel.Row) -> some View { AICard { HStack(alignment: .top, spacing: 12) { if model.selectionMode { Button { toggle(row.id) } label: { Image( systemName: model.selectedIDs.contains(row.id) ? "checkmark.circle.fill" : "circle" ) .font(.system(size: 20)) .foregroundStyle(model.selectedIDs.contains(row.id) ? AITheme.primary : AITheme.muted) } .buttonStyle(.plain) .frame(width: 44, height: 44) .accessibilityLabel(model.selectedIDs.contains(row.id) ? "取消选择" : "选择") .accessibilityAddTraits(model.selectedIDs.contains(row.id) ? .isSelected : []) } VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { Text(row.instance.name) .font(.headline) .foregroundStyle(AITheme.ink) .lineLimit(1) .truncationMode(.middle) .layoutPriority(1) Spacer() AITag(row.status.label, tint: statusColor(row.status)) } Text(row.instance.origin.absoluteString) .font(.subheadline) .foregroundStyle(AITheme.muted) .lineLimit(1) .truncationMode(.middle) if !row.instance.tags.isEmpty { HStack(spacing: 6) { ForEach(row.instance.tags, id: \.self) { tag in AITag(tag) } } } } } } } private func statusColor(_ status: ConnectionStatus) -> Color { switch status { case .online: return AITheme.success case .offline: return AITheme.danger case .authRequired: return AITheme.warning case .degraded: return AITheme.warning case .unknown: return AITheme.muted } } private func toggle(_ id: UUID) { if model.selectedIDs.contains(id) { model.selectedIDs.remove(id) } else { model.selectedIDs.insert(id) } } }