import SwiftUI struct InstanceGroupEditorView: View { @Environment(\.dismiss) private var dismiss @ObservedObject var model: FleetViewModel let targetIDs: [UUID] let onSaved: () -> Void @State private var selectedGroupID: UUID? @State private var isCreatingGroup = false @State private var newGroupName = "" @State private var isWorking = false @State private var errorMessage: String? private var selectedCount: Int { targetIDs.count } var body: some View { NavigationStack { ScrollView { LazyVStack(spacing: 12) { if let errorMessage { AIStatusBanner( errorMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle" ) } AICard(padding: 16) { VStack(alignment: .leading, spacing: 12) { AISectionTitle( "批量归类", systemImage: "folder.badge.gearshape", detail: selectedCount == 0 ? "全部实例" : "\(selectedCount) 个实例" ) Text("选择一个分组后保存,所选实例会立即移入该分组。") .font(.footnote) .foregroundStyle(AITheme.muted) .frame(maxWidth: .infinity, alignment: .leading) } } if isCreatingGroup { AICard(padding: 16) { VStack(alignment: .leading, spacing: 10) { AIFieldLabel("新分组名称") TextField("例如:家里、外场、测试机", text: $newGroupName) .textFieldStyle(AIFieldStyle()) .submitLabel(.done) } } } else { AICard(padding: 8) { VStack(spacing: 2) { groupRow( title: "未分组", subtitle: "移出当前分组", systemImage: "square.dashed", value: nil, isSelected: selectedGroupID == nil, selectionTrait: selectedGroupID == nil ) ForEach(model.groups) { group in Divider() .overlay(AITheme.line.opacity(0.55)) groupRow( title: group.name, subtitle: "\(instanceCount(group.id)) 个实例", systemImage: "folder", value: group.id, isSelected: selectedGroupID == group.id, selectionTrait: selectedGroupID == group.id ) } } } } } .padding(16) } .background(AITheme.canvas.ignoresSafeArea()) .navigationTitle("实例分组") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .cancellationAction) { Button("取消") { dismiss() } } ToolbarItem(placement: .primaryAction) { Button("新建") { selectedGroupID = nil isCreatingGroup = true errorMessage = nil } .disabled(isWorking) } } .aiBottomActionBar { AIActionRow { Button { Task { await save() } } label: { Label( selectedGroupID == nil ? "移出分组" : "移入分组", systemImage: "tray.and.arrow.down" ) } .buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true)) .disabled(isWorking || (selectedGroupID == nil && !isCreatingGroup)) } } } } private func groupRow( title: String, subtitle: String, systemImage: String, value: UUID?, isSelected: Bool, selectionTrait: Bool ) -> some View { Button { selectedGroupID = value isCreatingGroup = false newGroupName = "" errorMessage = nil } label: { HStack(spacing: 12) { Image(systemName: systemImage) .font(.subheadline.weight(.semibold)) .foregroundStyle(isSelected ? AITheme.primary : AITheme.muted) .frame(width: 32, height: 32) .background((isSelected ? AITheme.primary : AITheme.muted).opacity(0.10)) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .accessibilityHidden(true) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.subheadline.weight(.semibold)) .foregroundStyle(AITheme.ink) .lineLimit(1) Text(subtitle) .font(.caption) .foregroundStyle(AITheme.muted) .lineLimit(1) } Spacer(minLength: 8) Image( systemName: isSelected ? "checkmark.circle.fill" : "circle.dashed" ) .font(.system(size: 18)) .foregroundStyle(isSelected ? AITheme.primary : AITheme.muted) } .padding(.horizontal, 12) .padding(.vertical, 8) .frame(minHeight: 54) .background(isSelected ? AITheme.primary.opacity(0.08) : Color.clear) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .contentShape(Rectangle()) .accessibilityElement(children: .combine) .accessibilityLabel("\(title),\(subtitle)") .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : [.isButton]) } .buttonStyle(.plain) } private func instanceCount(_ id: UUID) -> Int { model.rows.filter { $0.instance.groupId == id }.count } private func save() async { guard !isWorking else { return } isWorking = true errorMessage = nil do { if isCreatingGroup { let name = newGroupName.trimmingCharacters(in: .whitespacesAndNewlines) let group = try await model.createGroup(named: name) selectedGroupID = group.id isCreatingGroup = false newGroupName = "" } try await model.assign( groupID: selectedGroupID, to: targetIDs.isEmpty ? model.visibleRows.map(\.id) : targetIDs ) onSaved() dismiss() } catch { errorMessage = isCreatingGroup ? "创建分组失败,名称不能为空且不能重复。" : "保存分组失败,请检查节点后重试。" } isWorking = false } }