feat(ios): unify list cards and loading states

This commit is contained in:
chick
2026-09-10 01:35:12 +08:00
parent 995206ac23
commit c841748971
32 changed files with 1297 additions and 607 deletions
@@ -14,23 +14,22 @@ struct FleetView: View {
ScrollView {
LazyVStack(spacing: 12) {
searchField
if let errorMessage = model.errorMessage {
AIStatusBanner(errorMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle")
}
if model.selectionMode {
selectionBar
}
if model.isLoading && model.rows.isEmpty {
AILoadingCard("正在同步实例")
}
if model.rows.isEmpty && !model.isLoading {
AICard {
VStack(spacing: 10) {
Image(systemName: "server.rack")
.font(.system(size: 28))
.foregroundStyle(AITheme.muted)
Text("暂无实例")
.font(.headline)
Text("添加 SimAdmin 实例后即可开始管理。")
.font(.subheadline)
.foregroundStyle(AITheme.muted)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
AIEmptyState(
systemImage: "server.rack",
title: "暂无实例",
message: "添加 SimAdmin 实例后即可开始管理。"
)
}
}
ForEach(model.rows) { row in
@@ -50,22 +49,23 @@ struct FleetView: View {
}
}
.navigationTitle("节点总览")
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button {
model.selectionMode.toggle()
model.selectedIDs.removeAll()
} label: {
Label(model.selectionMode ? "完成" : "批量选择", systemImage: model.selectionMode ? "xmark" : "checklist")
}
.buttonStyle(AIButtonStyle(kind: .secondary, size: .regular))
.aiBottomActionBar {
if !model.selectionMode {
HStack(spacing: 10) {
Button {
model.selectionMode = true
} label: {
Label("批量选择", systemImage: "checklist")
}
.buttonStyle(AIButtonStyle(kind: .secondary, size: .regular, fillsWidth: true))
Button {
model.showingForm = true
} label: {
Label("添加实例", systemImage: "plus")
Button {
model.showingForm = true
} label: {
Label("添加实例", systemImage: "plus")
}
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
}
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular))
}
}
.confirmationDialog(
@@ -110,7 +110,13 @@ struct FleetView: View {
systemImage: "checkmark.circle"
)
}
.buttonStyle(AIButtonStyle(kind: .secondary, size: .compact))
.buttonStyle(
AIButtonStyle(
kind: .secondary,
size: .compact,
fixedWidth: AITheme.footerSecondaryButtonWidth
)
)
Spacer()
@@ -123,7 +129,13 @@ struct FleetView: View {
} label: {
Label("删除", systemImage: "trash")
}
.buttonStyle(AIButtonStyle(kind: .danger, size: .compact))
.buttonStyle(
AIButtonStyle(
kind: .danger,
size: .compact,
fixedWidth: AITheme.footerSecondaryButtonWidth
)
)
.disabled(model.selectedIDs.isEmpty)
}
}
@@ -143,6 +155,8 @@ struct FleetView: View {
}
.buttonStyle(.plain)
.frame(width: 32, height: 32)
.accessibilityLabel(model.selectedIDs.contains(row.id) ? "取消选择" : "选择")
.accessibilityAddTraits(model.selectedIDs.contains(row.id) ? .isSelected : [])
}
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
@@ -155,6 +169,8 @@ struct FleetView: View {
Text(row.instance.origin.absoluteString)
.font(.subheadline)
.foregroundStyle(AITheme.muted)
.lineLimit(1)
.truncationMode(.middle)
if !row.instance.tags.isEmpty {
HStack(spacing: 6) {
ForEach(row.instance.tags, id: \.self) { tag in
@@ -25,32 +25,31 @@ struct InstanceFormView: View {
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)
field("实例密码", text: $password, placeholder: "可选,保存在本机沙盒", secure: true)
}
}
if let validationMessage {
AICard {
Label(validationMessage, systemImage: "exclamationmark.triangle")
.font(.subheadline)
.foregroundStyle(AITheme.danger)
}
AIStatusBanner(validationMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle")
}
}
.padding(16)
}
}
.navigationTitle("添加实例")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
.aiBottomActionBar {
HStack(spacing: 10) {
Button("取消") { dismiss() }
.buttonStyle(AIButtonStyle(kind: .secondary, size: .regular))
}
ToolbarItem(placement: .confirmationAction) {
Button("保存") {
save()
}
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular))
.disabled(!canSave)
.buttonStyle(
AIButtonStyle(
kind: .secondary,
size: .regular,
fixedWidth: AITheme.footerSecondaryButtonWidth
)
)
Button("保存实例") { save() }
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
.disabled(!canSave)
}
}
}
@@ -73,6 +72,11 @@ struct InstanceFormView: View {
} else {
TextField(placeholder, text: text)
.textFieldStyle(AIFieldStyle())
#if os(iOS)
.keyboardType(title == "地址" ? .URL : .default)
.textInputAutocapitalization(title == "地址" || title == "标签" ? .never : nil)
#endif
.autocorrectionDisabled(title == "地址" || title == "标签")
}
}
}