- Show stable group health in a fixed 2x2 metric grid - Add link-chain state to fleet cards and parse hardware/control/SIM/cellular - Replace the wrapped icon with a full-bleed app icon - Bump the IPA build number
206 lines
6.3 KiB
Swift
206 lines
6.3 KiB
Swift
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
|
||
}
|
||
}
|