release(ios): 0.2.2 构建 9 修复消息发送遮挡并中性化石墨灰阶

- 消息中心发送入口从被标签栏盖住的底部操作条移到导航栏右侧,发信恢复可用
- 停用态主按钮保留主色淡底与同色描边,与次级按钮拉开层级
- 消息行删除改安静线性图标,去掉一屏红块
- 缺数据读数统一为短横,卡片延迟补空格单位
- 中性灰阶改为严格 R=G=B,浅色主题不再读出冷调;图标石墨底同步去蓝
- 详情页延迟量表改用与卡片同一算法,身份信息值改为右对齐
- 标签加号按钮与输入框同高同圆角
This commit is contained in:
chick
2026-09-15 08:19:18 +08:00
parent 9ad0c46088
commit c3d17b3daf
21 changed files with 203 additions and 125 deletions
@@ -237,7 +237,9 @@ struct InstanceDetailView: View {
section: "connectivity",
keys: ["latency_ms", "latency"],
format: loading ? nil : { "\(Int($0.rounded())) ms" },
progress: loading ? nil : { _ in 1 },
// 进度条读的是"链路质量",和节点卡片同一把尺子(1000ms 归零),
// 之前恒为 1,满格黑条压在 38ms 上会被读成"延迟占满了什么"。
progress: loading ? nil : { max(0.04, min(1, 1 - $0 / 1000)) },
tint: loading ? nil : { latencyTint($0) }
)
let cpu = summaryValue(
@@ -562,18 +564,22 @@ struct InstanceDetailView: View {
} else if items.isEmpty == false {
VStack(alignment: .leading, spacing: 6) {
ForEach(items, id: \.label) { item in
HStack(alignment: .firstTextBaseline, spacing: 8) {
// 值一律右对齐,和下方分区卡片的键值行同一节奏;
// 之前左边缩进 52pt、值顶在中间,同屏两种排法读起来像两张不同的表。
HStack(alignment: .firstTextBaseline, spacing: 12) {
Text(item.label)
.font(.caption2.weight(.semibold))
.foregroundStyle(AITheme.muted)
.frame(width: 52, alignment: .leading)
.fixedSize(horizontal: true, vertical: false)
Spacer(minLength: 12)
Text(item.value)
.font(.caption.weight(.medium))
.monospacedDigit()
.foregroundStyle(AITheme.ink)
.lineLimit(1)
.truncationMode(.middle)
Spacer(minLength: 0)
.multilineTextAlignment(.trailing)
.layoutPriority(1)
}
.accessibilityElement(children: .combine)
}
@@ -180,6 +180,9 @@ struct FleetView: View {
}
private extension FleetView {
/// 概览读数缺数据时只画一根短横:「不可用」在四列窄格里既占位又容易被读成故障结论。
static let noDataText = "—"
private static let relativeFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .abbreviated
@@ -361,7 +364,7 @@ private extension FleetView {
)
FleetHeadlineStat(
title: "异常节点",
value: settledCount == 0 ? "--" : "\(abnormalCount)",
value: settledCount == 0 ? Self.noDataText : "\(abnormalCount)",
detail: abnormalDetail,
tint: abnormalTint
)
@@ -666,7 +669,7 @@ private extension FleetView {
private var hasSettledResults: Bool { summaryRows.isEmpty == false && settledCount > 0 }
private var onlineRateValue: String {
guard hasSettledResults else { return "--" }
guard hasSettledResults else { return Self.noDataText }
return "\(Int((Double(onlineCount) / Double(summaryRows.count) * 100).rounded()))%"
}
@@ -749,10 +752,10 @@ private extension FleetView {
private var storageSummary: String {
let pairs = storagePairs
guard pairs.isEmpty == false else { return "不可用" }
guard pairs.isEmpty == false else { return Self.noDataText }
let used = pairs.map { max(0, $0.total - $0.available) }.reduce(0, +)
let total = pairs.map(\.total).reduce(0, +)
guard total > 0 else { return "不可用" }
guard total > 0 else { return Self.noDataText }
return "\(Int((Double(used) / Double(total) * 100).rounded()))% 已用"
}
@@ -777,7 +780,7 @@ private extension FleetView {
private var trafficSummary: String {
let values = summaryRows.compactMap(\.downloadRateBytesPerSecond) + summaryRows.compactMap(\.uploadRateBytesPerSecond)
guard values.isEmpty == false else { return "不可用" }
guard values.isEmpty == false else { return Self.noDataText }
return ByteCountFormatter.string(fromByteCount: Int64(values.reduce(0, +)), countStyle: .decimal) + "/s"
}
@@ -793,7 +796,7 @@ private extension FleetView {
}
private func formatAverage(_ values: [Double], transform: (Double) -> String) -> String {
guard values.isEmpty == false else { return "不可用" }
guard values.isEmpty == false else { return Self.noDataText }
let average = values.reduce(0, +) / Double(values.count)
return transform(average)
}
@@ -927,25 +930,25 @@ private extension FleetView {
[
FleetGauge(
label: "信号",
value: row.signalPercent.map { "\($0)%" } ?? "--",
value: row.signalPercent.map { "\($0)%" } ?? Self.noDataText,
ratio: row.signalPercent.map { Double($0) / 100 },
tint: row.signalPercent.flatMap(signalColor) ?? AITheme.muted
),
FleetGauge(
label: "延迟",
value: row.latencyMilliseconds.map { "\($0)ms" } ?? "--",
value: row.latencyMilliseconds.map { "\($0) ms" } ?? Self.noDataText,
ratio: row.latencyMilliseconds.map { max(0, 1 - Double($0) / 1000) },
tint: row.latencyMilliseconds.flatMap(latencyColor) ?? AITheme.muted
),
FleetGauge(
label: "CPU",
value: row.cpuPercent.map(Self.percentText) ?? "--",
value: row.cpuPercent.map(Self.percentText) ?? Self.noDataText,
ratio: row.cpuPercent.map { $0 / 100 },
tint: row.cpuPercent.flatMap(resourceColor) ?? AITheme.muted
),
FleetGauge(
label: "内存",
value: row.memoryPercent.map(Self.percentText) ?? "--",
value: row.memoryPercent.map(Self.percentText) ?? Self.noDataText,
ratio: row.memoryPercent.map { $0 / 100 },
tint: row.memoryPercent.flatMap(resourceColor) ?? AITheme.muted
),
@@ -993,7 +996,7 @@ private extension FleetView {
private static func cpuValue(percent: Double?, cores: Int?) -> String? {
guard percent != nil || cores != nil else { return nil }
let usage = percent.map(Self.percentText) ?? "--"
let usage = percent.map(Self.percentText) ?? FleetView.noDataText
guard let cores, cores > 0 else { return usage }
return "\(usage) · \(cores) 核"
}
@@ -1296,7 +1299,7 @@ extension FleetView {
// 没有数据时用弱色占位,读起来不会像"0%"那样的假结论。
var isPlaceholder: Bool {
value == "--" || value == "不可用" || value == "暂无"
value == "--" || value == "—" || value == "不可用" || value == "暂无"
}
}
@@ -1546,7 +1549,7 @@ extension FleetView {
.font(.caption2)
.foregroundStyle(AITheme.faint)
.lineLimit(1)
Text(value ?? "--")
Text(value ?? FleetView.noDataText)
.font(.footnote.weight(.semibold))
.monospacedDigit()
.foregroundStyle(isAlert ? tint : AITheme.ink)
@@ -22,16 +22,17 @@ struct TagEditorView: View {
commit(draft)
} label: {
Image(systemName: "plus")
.font(.system(size: 15, weight: .semibold))
.frame(width: 44, height: 44)
// 加号按钮和左边的输入框同一高度、同一圆角,两个控件才读得出是一组。
.font(.system(size: 16, weight: .semibold))
.frame(width: AITheme.inputHeight, height: AITheme.inputHeight)
.foregroundStyle(canAdd ? AITheme.primary : AITheme.muted)
.background(canAdd ? AITheme.primary.opacity(0.10) : AITheme.control)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.clipShape(RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous)
.strokeBorder(canAdd ? AITheme.primary.opacity(0.40) : AITheme.line)
}
.contentShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.contentShape(RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous))
}
.buttonStyle(.plain)
.disabled(canAdd == false)
@@ -237,7 +237,7 @@ struct LogsView: View {
status: entry.outcome.label,
statusTint: outcomeTint(entry.outcome),
details: [
"HTTP \(entry.httpStatus.map(String.init) ?? "--") · \(entry.durationMs) ms",
"HTTP \(entry.httpStatus.map(String.init) ?? "—") · \(entry.durationMs) ms",
entry.errorCode ?? "无错误码",
]
)
@@ -46,22 +46,21 @@ struct MessagesView: View {
.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 {
// 发送入口和节点页的「添加实例」保持一致:放在导航栏右侧,
// 底部固定条在 TabView 里会被自定义标签栏盖住,之前整条 CTA 是不可达的。
ToolbarItem(placement: .primaryAction) {
AIIconButton("刷新", systemImage: "arrow.clockwise") {
Task { await model.refresh() }
HStack(spacing: 8) {
AIIconButton("刷新", systemImage: "arrow.clockwise") {
Task { await model.refresh() }
}
.disabled(model.isLoading)
AIIconButton("发送消息", systemImage: "paperplane") {
model.showingSendForm = true
}
}
.disabled(model.isLoading)
}
}
.confirmationDialog(
@@ -164,7 +163,7 @@ struct MessagesView: View {
.lineLimit(1)
}
Spacer(minLength: 8)
AISymbolAction("删除这条消息", systemImage: "trash", tint: AITheme.danger) {
AISymbolAction("删除这条消息", systemImage: "trash", tint: AITheme.danger, quiet: true) {
pendingDeleteMessage = message
}
}
@@ -15,7 +15,7 @@ struct SettingsRuntime: Equatable {
final class SettingsViewModel: ObservableObject {
@Published private(set) var runtime: SettingsRuntime?
@Published private(set) var isLoading = false
@Published var storageLabel = "--"
@Published var storageLabel = "—"
@Published var notice: String?
@Published var errorMessage: String?
@@ -61,7 +61,7 @@ final class SettingsViewModel: ObservableObject {
let connectionCount = try await store.connectionLogs().count
storageLabel = "\(runtimeCount) 条运行日志 · \(connectionCount) 条连接日志"
} catch {
storageLabel = "--"
storageLabel = "—"
errorMessage = "无法读取本地数据状态。"
}
}