- 消息卡片删除按钮移出正文行,改用 overlay 悬在右侧,44pt 点击区不再把行撑高 - 批量标签胶囊由等宽栅格改为流式布局,新增 AIFlowLayout 并让节点页复用同一实现 - 设置页应用信息补「构建」一行,版本号全程不带括号 - 添加实例与发送消息两张表单表统一行内标题,去掉大标题留下的空白带 - 节点空态文案改为「节点未返回设备信息」,与量表条实际返回的数据不矛盾 - 图标石墨底、面板阴影与插槽统一压暖 3~4 级,全图 0 蓝色像素
151 lines
4.6 KiB
Swift
151 lines
4.6 KiB
Swift
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 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 ?? [])
|
|
}
|
|
|
|
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
|
|
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)
|
|
// 表单表统一用行内标题:大标题会在卡片上方留出一整条空白,
|
|
// 而「批量标签」等其它表已经是行内样式。
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.scrollDismissesKeyboard(.interactively)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
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<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(.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,
|
|
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
|
|
}
|
|
}
|
|
}
|