fix(ios): improve fleet overview and app identity

- 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
This commit is contained in:
chick
2026-09-13 00:46:26 +08:00
parent 409345cba3
commit 2ff23d1fb0
23 changed files with 3600 additions and 269 deletions
@@ -0,0 +1,205 @@
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
}
}