Unify bottom actions, selection controls, navigation structure, spacing, and theme colors across iOS pages. Replace the app icon with a higher-contrast native-style asset and include its source SVG.
126 lines
3.9 KiB
Swift
126 lines
3.9 KiB
Swift
import SwiftUI
|
|
|
|
struct InstanceFormView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@ObservedObject var model: FleetViewModel
|
|
@State private var name = ""
|
|
@State private var originText = ""
|
|
@State private var tagsText = ""
|
|
@State private var password = ""
|
|
@State private var validationMessage: String?
|
|
@State private var isSaving = false
|
|
|
|
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")
|
|
field("标签", text: $tagsText, placeholder: "home, primary")
|
|
field("实例密码", text: $password, placeholder: "可选,保存在本机沙盒", 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("添加实例")
|
|
#if os(iOS)
|
|
.scrollDismissesKeyboard(.interactively)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private func field(
|
|
_ title: String,
|
|
text: Binding<String>,
|
|
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(title == "地址" ? .next : .done)
|
|
#if os(iOS)
|
|
.keyboardType(title == "地址" ? .URL : .default)
|
|
.textInputAutocapitalization(title == "地址" || title == "标签" ? .never : nil)
|
|
#endif
|
|
.autocorrectionDisabled(title == "地址" || title == "标签")
|
|
}
|
|
}
|
|
}
|
|
|
|
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 tags =
|
|
tagsText
|
|
.split(separator: ",")
|
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
.filter { !$0.isEmpty }
|
|
let input = InstanceInput(
|
|
name: normalizedName,
|
|
origin: origin,
|
|
tags: tags,
|
|
password: password.isEmpty ? nil : password
|
|
)
|
|
Task {
|
|
do {
|
|
try await model.add(input)
|
|
} catch {
|
|
validationMessage = "保存失败,请检查地址是否重复。"
|
|
}
|
|
isSaving = false
|
|
}
|
|
}
|
|
}
|