213 lines
7.7 KiB
Swift
213 lines
7.7 KiB
Swift
import SwiftUI
|
|
|
|
struct FleetView: View {
|
|
@StateObject private var model: FleetViewModel
|
|
@State private var showsDeleteDialog = false
|
|
|
|
init(model: FleetViewModel? = nil) {
|
|
_model = StateObject(wrappedValue: model ?? DefaultContainer.makeFleetModel())
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
AITheme.canvas.ignoresSafeArea()
|
|
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 {
|
|
AIEmptyState(
|
|
systemImage: "server.rack",
|
|
title: "暂无实例",
|
|
message: "添加 SimAdmin 实例后即可开始管理。"
|
|
)
|
|
}
|
|
}
|
|
ForEach(model.rows) { row in
|
|
if model.selectionMode {
|
|
instanceCard(row)
|
|
} else {
|
|
NavigationLink {
|
|
InstanceDetailView(instance: row.instance)
|
|
} label: {
|
|
instanceCard(row)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
.padding(16)
|
|
}
|
|
}
|
|
.navigationTitle("节点总览")
|
|
.aiBottomActionBar {
|
|
if !model.selectionMode {
|
|
HStack(spacing: 10) {
|
|
Button {
|
|
model.selectionMode = true
|
|
} label: {
|
|
Label("批量选择", systemImage: "checklist")
|
|
}
|
|
.buttonStyle(AIButtonStyle(kind: .secondary, size: .regular, fillsWidth: true))
|
|
.frame(maxWidth: .infinity)
|
|
|
|
Button {
|
|
model.showingForm = true
|
|
} label: {
|
|
Label("添加实例", systemImage: "plus")
|
|
}
|
|
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
}
|
|
.confirmationDialog(
|
|
"删除已选实例?登录信息也会一并清除。",
|
|
isPresented: $showsDeleteDialog,
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("删除 \(model.selectedIDs.count) 个实例", role: .destructive) {
|
|
Task { await model.deleteSelected() }
|
|
}
|
|
Button("取消", role: .cancel) {}
|
|
}
|
|
.refreshable {
|
|
await model.refresh()
|
|
}
|
|
.task {
|
|
await model.refresh()
|
|
}
|
|
.sheet(isPresented: $model.showingForm) {
|
|
InstanceFormView(model: model)
|
|
}
|
|
}
|
|
|
|
private var searchField: some View {
|
|
AISearchField("搜索实例或地址", text: $model.searchText) {
|
|
Task { await model.refresh() }
|
|
}
|
|
}
|
|
|
|
private var selectionBar: some View {
|
|
AICard {
|
|
HStack(spacing: 10) {
|
|
Button {
|
|
if model.selectedIDs.count == model.rows.count {
|
|
model.selectedIDs.removeAll()
|
|
} else {
|
|
model.selectedIDs = Set(model.rows.map(\.id))
|
|
}
|
|
} label: {
|
|
Label(
|
|
model.selectedIDs.count == model.rows.count ? "取消全选" : "全选",
|
|
systemImage: "checkmark.circle"
|
|
)
|
|
}
|
|
.buttonStyle(
|
|
AIButtonStyle(
|
|
kind: .secondary,
|
|
size: .compact,
|
|
fixedWidth: AITheme.footerSecondaryButtonWidth
|
|
)
|
|
)
|
|
|
|
Spacer()
|
|
|
|
Text("已选 \(model.selectedIDs.count)")
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundStyle(AITheme.muted)
|
|
|
|
Button {
|
|
showsDeleteDialog = true
|
|
} label: {
|
|
Label("删除", systemImage: "trash")
|
|
}
|
|
.buttonStyle(
|
|
AIButtonStyle(
|
|
kind: .danger,
|
|
size: .compact,
|
|
fixedWidth: AITheme.footerSecondaryButtonWidth
|
|
)
|
|
)
|
|
.disabled(model.selectedIDs.isEmpty)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func instanceCard(_ row: FleetViewModel.Row) -> some View {
|
|
AICard {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
if model.selectionMode {
|
|
Button {
|
|
toggle(row.id)
|
|
} label: {
|
|
Image(systemName: model.selectedIDs.contains(row.id)
|
|
? "checkmark.circle.fill" : "circle")
|
|
.font(.system(size: 20))
|
|
.foregroundStyle(model.selectedIDs.contains(row.id) ? AITheme.primary : AITheme.muted)
|
|
}
|
|
.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) {
|
|
Text(row.instance.name)
|
|
.font(.headline)
|
|
.foregroundStyle(AITheme.ink)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
.layoutPriority(1)
|
|
Spacer()
|
|
AITag(row.status.label, tint: statusColor(row.status))
|
|
}
|
|
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
|
|
AITag(tag)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func statusColor(_ status: ConnectionStatus) -> Color {
|
|
switch status {
|
|
case .online: return AITheme.success
|
|
case .offline: return AITheme.danger
|
|
case .authRequired: return AITheme.warning
|
|
case .degraded: return AITheme.warning
|
|
case .unknown: return AITheme.muted
|
|
}
|
|
}
|
|
|
|
private func toggle(_ id: UUID) {
|
|
if model.selectedIDs.contains(id) {
|
|
model.selectedIDs.remove(id)
|
|
} else {
|
|
model.selectedIDs.insert(id)
|
|
}
|
|
}
|
|
}
|