import SwiftUI struct InstanceFormView: View { @Environment(\.dismiss) private var dismiss @ObservedObject var model: FleetViewModel let instance: Instance? @State private var name = "" @State private var originText = "" @State private var tags: [String] = [] @State private var tagDraft = "" @State private var password = "" @State private var selectedGroupId: UUID? @State private var validationMessage: String? @State private var isSaving = false init(model: FleetViewModel, instance: Instance? = nil) { self.model = model self.instance = instance _name = State(initialValue: instance?.name ?? "") _originText = State(initialValue: instance?.origin.absoluteString ?? "") _tags = State(initialValue: instance?.tags ?? []) _selectedGroupId = State(initialValue: instance?.groupId) } private var canSave: Bool { !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !originText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } var body: some View { NavigationStack { ScrollView { VStack(spacing: 12) { AICard { VStack(alignment: .leading, spacing: 14) { field("名称", text: $name, placeholder: "Home Node") field("地址", text: $originText, placeholder: "http://192.168.1.10:3000") tagEditor groupPicker field( "实例密码", text: $password, placeholder: instance == nil ? "可选,仅保存在本机" : "留空则不修改密码", secure: true ) } } if let validationMessage { AIStatusBanner( validationMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle") } } .padding(16) .padding(.bottom, 8) } .aiBottomActionBar { AIActionRow { Button("取消") { dismiss() } .buttonStyle(AIButtonStyle(kind: .secondary, size: .regular, fillsWidth: true)) Button { save() } label: { HStack(spacing: 8) { if isSaving { ProgressView() .controlSize(.small) } Text(isSaving ? "保存中" : "保存实例") } } .buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true)) .disabled(!canSave || isSaving) } } .background(AITheme.canvas.ignoresSafeArea()) .navigationTitle(instance == nil ? "添加实例" : "编辑实例") #if os(iOS) .scrollDismissesKeyboard(.interactively) #endif } } private var groupPicker: some View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { AIFieldLabel("分组") Spacer() Button { model.showingGroupManager = true } label: { Label("管理", systemImage: "folder.badge.gearshape") } .buttonStyle(AIButtonStyle(kind: .secondary, size: .toolbarInline, fixedWidth: 72)) } Picker("分组", selection: $selectedGroupId) { Text("未分组").tag(UUID?.none) ForEach(model.groups) { group in Text(group.name).tag(UUID?.some(group.id)) } } .pickerStyle(.menu) .aiSelectSurface() } .accessibilityElement(children: .contain) } private var tagEditor: some View { VStack(alignment: .leading, spacing: 6) { AIFieldLabel("标签") TagEditorView(tags: $tags, suggestions: model.allTags) } .accessibilityElement(children: .contain) } private func field( _ title: String, text: Binding, placeholder: String, secure: Bool = false ) -> some View { VStack(alignment: .leading, spacing: 6) { AIFieldLabel(title) if secure { SecureField(placeholder, text: text) .textFieldStyle(AIFieldStyle()) .textContentType(.password) .accessibilityLabel(title) } else { TextField(placeholder, text: text) .textFieldStyle(AIFieldStyle()) .accessibilityLabel(title) .submitLabel(.done) #if os(iOS) .keyboardType(title == "地址" ? .URL : .default) .textInputAutocapitalization(.never) #endif .autocorrectionDisabled() } } } private func save() { guard !isSaving else { return } isSaving = true let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines) guard let origin = URL(string: originText.trimmingCharacters(in: .whitespacesAndNewlines)), origin.scheme == "http" || origin.scheme == "https", origin.host != nil else { validationMessage = "请输入有效的 HTTP 或 HTTPS 根地址。" isSaving = false return } let input = InstanceInput( name: normalizedName, origin: origin, tags: tags, groupId: selectedGroupId, password: password.isEmpty ? nil : password ) Task { do { if let instance { try await model.update(instance.id, input) } else { try await model.add(input) } } catch { validationMessage = "保存失败,请检查地址是否重复。" } isSaving = false } } }