- AITag 拆为两类:不传状态色时是中性归类胶囊(tile 底、发丝描边、墨字,可带 10pt 图标), 传状态色时才用 14% 色雾与 30% 描边。颜色只服务状态语义。 - 详情页标签改用 AITag(tag, systemImage: "tag"),与节点卡片同一枚形状;头部 VStack 间距 14 -> 12,读数格 minHeight 62 -> 56,加载完成后不再明显小于下方卡片。 - 消息中心「收件 / 发件」由绿/墨配色改回中性胶囊配 arrow.down.left / arrow.up.right, 绿色重新只表示健康状态。 - 批量标签表在无勾选时给出「先在节点列表勾选要归类的节点」,不再留空白行。 - 文档:0.2.2 增量记录与校验值更新为 0e4ffb74…;版本号后不再带括号, 0.2.0/0.2.1/0.2.2 与 README、现行说明统一改写为「0.2.x 构建 N」。
175 lines
5.7 KiB
Swift
175 lines
5.7 KiB
Swift
import SwiftUI
|
|
|
|
struct MessagesView: View {
|
|
@StateObject private var model: MessagesViewModel
|
|
@State private var pendingDeleteMessage: SMSMessage?
|
|
|
|
init(model: MessagesViewModel? = nil) {
|
|
_model = StateObject(wrappedValue: model ?? DefaultContainer.makeMessagesModel())
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
LazyVStack(spacing: 12) {
|
|
filterBar
|
|
if model.isLoading && model.filteredRows.isEmpty {
|
|
AILoadingCard("正在同步消息数据")
|
|
} else if model.filteredRows.isEmpty {
|
|
AICard {
|
|
if let errorMessage = model.errorMessage {
|
|
AIEmptyState(
|
|
systemImage: "arrow.triangle.2.circlepath",
|
|
title: "未能读取消息",
|
|
message: errorMessage,
|
|
actionTitle: "重试"
|
|
) {
|
|
Task { await model.refresh() }
|
|
}
|
|
} else {
|
|
AIEmptyState(
|
|
systemImage: "message",
|
|
title: "暂无消息",
|
|
message: "所有节点都没有返回短信。"
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
if let errorMessage = model.errorMessage {
|
|
AIStatusBanner(
|
|
errorMessage, tint: AITheme.warning, systemImage: "exclamationmark.triangle")
|
|
}
|
|
ForEach(model.filteredRows) { message in
|
|
messageCard(message)
|
|
}
|
|
}
|
|
}
|
|
.padding(16)
|
|
.padding(.bottom, 8)
|
|
}
|
|
.aiBottomActionBar {
|
|
Button {
|
|
model.showingSendForm = true
|
|
} label: {
|
|
Label("发送消息", systemImage: "paperplane")
|
|
}
|
|
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
|
|
}
|
|
.background(AITheme.canvas.ignoresSafeArea())
|
|
.navigationTitle("消息中心")
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
AIIconButton("刷新", systemImage: "arrow.clockwise") {
|
|
Task { await model.refresh() }
|
|
}
|
|
.disabled(model.isLoading)
|
|
}
|
|
}
|
|
.confirmationDialog(
|
|
"删除这条消息?操作不可恢复。",
|
|
isPresented: Binding(
|
|
get: { pendingDeleteMessage != nil },
|
|
set: { if !$0 { pendingDeleteMessage = nil } }
|
|
),
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("删除消息", role: .destructive) {
|
|
guard let message = pendingDeleteMessage else { return }
|
|
Task { await model.delete(message) }
|
|
}
|
|
Button("取消", role: .cancel) {}
|
|
}
|
|
.refreshable {
|
|
await model.refresh()
|
|
}
|
|
.task {
|
|
await model.refresh()
|
|
}
|
|
.sheet(isPresented: $model.showingSendForm) {
|
|
SendMessageFormView(model: model)
|
|
}
|
|
}
|
|
|
|
private var filterBar: some View {
|
|
VStack(spacing: 8) {
|
|
AISegmentedControl(
|
|
"消息方向",
|
|
selection: $model.direction,
|
|
options: MessagesViewModel.DirectionFilter.allCases.map {
|
|
(value: $0, title: $0.rawValue)
|
|
}
|
|
)
|
|
|
|
Picker("实例", selection: $model.selectedInstanceID) {
|
|
Text("全部实例").tag(Optional<UUID>.none)
|
|
ForEach(model.instances) { instance in
|
|
Text(instance.name).tag(Optional(instance.id))
|
|
}
|
|
}
|
|
.pickerStyle(.menu)
|
|
.aiSelectSurface(compact: true)
|
|
// 选择器右侧原本是一整片空白,这里补上当前条数,让这一行读完就知道结果规模。
|
|
.overlay(alignment: .trailing) {
|
|
Text("\(model.filteredRows.count) 条")
|
|
.font(.caption.weight(.medium))
|
|
.monospacedDigit()
|
|
.foregroundStyle(AITheme.muted)
|
|
.padding(.trailing, 12)
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
AISearchField("搜索号码或内容", text: $model.searchText, minHeight: AITheme.compactControlHeight)
|
|
}
|
|
}
|
|
|
|
|
|
private func messageCard(_ message: SMSMessage) -> some View {
|
|
AICard(padding: 14) {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
Text(message.phoneNumber.isEmpty ? "未知号码" : message.phoneNumber)
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundStyle(AITheme.ink)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
.layoutPriority(1)
|
|
AITag(
|
|
message.direction.label,
|
|
systemImage: message.direction == .outgoing ? "arrow.up.right" : "arrow.down.left")
|
|
Spacer(minLength: 8)
|
|
Text(TimestampText.short(message.timestamp))
|
|
.font(.caption2.weight(.medium))
|
|
.monospacedDigit()
|
|
.foregroundStyle(AITheme.faint)
|
|
.lineLimit(1)
|
|
.accessibilityLabel("时间 \(message.timestamp)")
|
|
}
|
|
Text(message.content)
|
|
.font(.subheadline)
|
|
.foregroundStyle(AITheme.ink)
|
|
.lineSpacing(2)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
HStack(alignment: .center, spacing: 8) {
|
|
Label(message.instanceName, systemImage: "antenna.radiowaves.left.and.right")
|
|
.font(.caption)
|
|
.labelStyle(.titleAndIcon)
|
|
.imageScale(.small)
|
|
.foregroundStyle(AITheme.muted)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
.layoutPriority(1)
|
|
if let delivery = message.deliveryLabel {
|
|
Text(delivery)
|
|
.font(.caption2.weight(.medium))
|
|
.foregroundStyle(delivery == "发送失败" ? AITheme.danger : AITheme.faint)
|
|
.lineLimit(1)
|
|
}
|
|
Spacer(minLength: 8)
|
|
AISymbolAction("删除这条消息", systemImage: "trash", tint: AITheme.danger) {
|
|
pendingDeleteMessage = message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|