Files
multi-simadmin/apps/ios/Sources/MultiSimAdmin/Features/Fleet/InstanceGroupEditorView.swift
T
chick 3607eddc9f release(ios): 发布 0.2.1 并重构图标与界面细节
- 应用图标改用与 Web favicon 同源的扁平标记,金色 SIM 卡体加三格递增信号,29pt 下仍可读
- 标签统一为胶囊形状,提高深色主题下的描边权重,避免标签在暗色下消失
- 消息卡片与详情分区卡片收紧行距,单屏信息密度明显提升
- 详情头部指标精简为 4 行,数据模块改为单条读数
- 批量选择勾选框移出名称区域,工具栏控件统一为等高 34pt
- 分组编辑器改为点选即生效,移除说明文字与常驻禁用按钮
2026-09-14 16:30:39 +08:00

227 lines
7.1 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 isCreatingGroup = false
@State private var newGroupName = ""
@State private var isWorking = false
@State private var errorMessage: String?
private var targets: [UUID] {
targetIDs.isEmpty ? model.visibleRows.map(\.id) : targetIDs
}
private var targetGroupIDs: [UUID?] {
let owner = Dictionary(model.rows.map { ($0.id, $0.instance.groupId) }, uniquingKeysWith: { a, _ in a })
return targets.compactMap { owner[$0] }
}
/// 目标实例当前是否同属一个分组;nil 表示 mixed,不显示对勾。
private var uniformGroup: UUID?? {
guard let first = targetGroupIDs.first else { return nil }
return targetGroupIDs.allSatisfy { $0 == first } ? .some(first) : nil
}
private var trimmedName: String {
newGroupName.trimmingCharacters(in: .whitespacesAndNewlines)
}
var body: some View {
NavigationStack {
ScrollView {
LazyVStack(spacing: 12) {
if let errorMessage {
AIStatusBanner(
errorMessage,
tint: AITheme.danger,
systemImage: "exclamationmark.triangle"
)
}
AISectionTitle(
"移动到",
systemImage: "folder",
detail: "\(targets.count) 台实例"
)
if isCreatingGroup {
AICard(padding: 16) {
VStack(alignment: .leading, spacing: 12) {
AIFieldLabel("新分组名称")
TextField("例如:家里、外场、测试机", text: $newGroupName)
.textFieldStyle(AIFieldStyle())
.submitLabel(.done)
.onSubmit { Task { await createAndAssign() } }
AIActionRow(spacing: 8) {
Button("取消") {
isCreatingGroup = false
newGroupName = ""
errorMessage = nil
}
.buttonStyle(AIButtonStyle(kind: .secondary, size: .regular, fillsWidth: true))
.disabled(isWorking)
Button {
Task { await createAndAssign() }
} label: {
Label("创建并移入", systemImage: "folder.badge.plus")
}
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
.disabled(trimmedName.isEmpty || isWorking)
}
}
}
}
AICard(padding: 8) {
VStack(spacing: 2) {
groupRow(
title: "未分组",
subtitle: "不归属任何分组",
systemImage: "square.dashed",
groupID: nil
)
ForEach(model.groups) { group in
Divider()
.overlay(AITheme.line.opacity(0.55))
groupRow(
title: group.name,
subtitle: "\(instanceCount(group.id)) 台在此",
systemImage: "folder",
groupID: group.id
)
}
}
}
.opacity(isWorking ? 0.55 : 1)
}
.padding(16)
}
.background(AITheme.canvas.ignoresSafeArea())
.navigationTitle("实例分组")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") { dismiss() }
}
ToolbarItem(placement: .primaryAction) {
AIIconButton("新建分组", systemImage: "plus") {
errorMessage = nil
isCreatingGroup = true
}
.disabled(isWorking)
}
}
}
}
private func groupRow(
title: String,
subtitle: String,
systemImage: String,
groupID: UUID?
) -> some View {
Button {
guard uniformGroup != groupID else {
dismiss()
return
}
Task { await apply(groupID: groupID) }
} label: {
HStack(spacing: 12) {
Image(systemName: systemImage)
.font(.subheadline.weight(.semibold))
.foregroundStyle(isCurrent(groupID) ? AITheme.primary : AITheme.muted)
.frame(width: 32, height: 32)
.background((isCurrent(groupID) ? AITheme.primary : AITheme.muted).opacity(0.10))
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.overlay {
// 深色下 10% 色洗几乎看不出边界,补一圈同色淡描边保住方块轮廓。
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder((isCurrent(groupID) ? AITheme.primary : AITheme.muted).opacity(0.20))
}
.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)
Group {
if isWorking {
EmptyView()
} else if isCurrent(groupID) {
Image(systemName: "checkmark")
.font(.system(size: 14, weight: .bold))
.foregroundStyle(AITheme.primary)
}
}
.frame(width: 20, alignment: .trailing)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.frame(minHeight: 54)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.contentShape(Rectangle())
.accessibilityElement(children: .combine)
.accessibilityLabel(isCurrent(groupID) ? "\(title),当前分组" : "移动到\(title)")
.accessibilityAddTraits(.isButton)
}
.buttonStyle(.plain)
.disabled(isWorking)
}
private func isCurrent(_ groupID: UUID?) -> Bool {
uniformGroup == groupID && targetGroupIDs.isEmpty == false
}
private func instanceCount(_ id: UUID) -> Int {
model.rows.filter { $0.instance.groupId == id }.count
}
private func apply(groupID: UUID?) async {
guard !isWorking else { return }
isWorking = true
errorMessage = nil
do {
try await model.assign(groupID: groupID, to: targets)
onSaved()
dismiss()
} catch {
errorMessage = "分组保存失败,请重试。"
isWorking = false
}
}
private func createAndAssign() async {
guard !isWorking, trimmedName.isEmpty == false else { return }
isWorking = true
errorMessage = nil
do {
let group = try await model.createGroup(named: trimmedName)
try await model.assign(groupID: group.id, to: targets)
onSaved()
dismiss()
} catch {
errorMessage = "创建分组失败,名称不能为空且不能重复。"
isWorking = false
}
}
}