release(ios): 发布 0.2.1 并重构图标与界面细节
- 应用图标改用与 Web favicon 同源的扁平标记,金色 SIM 卡体加三格递增信号,29pt 下仍可读 - 标签统一为胶囊形状,提高深色主题下的描边权重,避免标签在暗色下消失 - 消息卡片与详情分区卡片收紧行距,单屏信息密度明显提升 - 详情头部指标精简为 4 行,数据模块改为单条读数 - 批量选择勾选框移出名称区域,工具栏控件统一为等高 34pt - 分组编辑器改为点选即生效,移除说明文字与常驻禁用按钮
This commit is contained in:
@@ -19,13 +19,17 @@ struct InstanceDetailView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 12) {
|
||||
headerCard
|
||||
if let errorMessage = model.errorMessage {
|
||||
AIStatusBanner(errorMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle")
|
||||
}
|
||||
if let snapshot = model.snapshot {
|
||||
ForEach(snapshot.sections) { section in
|
||||
sectionCard(section)
|
||||
if isUnavailable {
|
||||
unavailableCard
|
||||
} else {
|
||||
headerCard
|
||||
if let errorMessage = model.errorMessage {
|
||||
AIStatusBanner(errorMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle")
|
||||
}
|
||||
if let snapshot = model.snapshot {
|
||||
ForEach(snapshot.sections) { section in
|
||||
sectionCard(section)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,56 +55,85 @@ struct InstanceDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 读取失败且从未拿到数据时,整屏骨架没有意义,改成一屏可读的失败卡片。
|
||||
private var isUnavailable: Bool {
|
||||
model.snapshot == nil && model.errorMessage != nil && !model.isLoading
|
||||
}
|
||||
|
||||
private var unavailableCard: some View {
|
||||
VStack(spacing: 12) {
|
||||
AICard(padding: 16, emphasis: true) {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(systemName: "wifi.slash")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundStyle(AITheme.danger)
|
||||
.frame(width: 38, height: 38)
|
||||
.background(AITheme.danger.opacity(0.12))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 11, style: .continuous))
|
||||
.accessibilityHidden(true)
|
||||
Text("节点无法连接")
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundStyle(AITheme.ink)
|
||||
Spacer(minLength: 8)
|
||||
statusChip(title: "不可达", tint: AITheme.danger)
|
||||
}
|
||||
|
||||
Text(model.errorMessage ?? "无法加载设备数据。")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Button {
|
||||
Task { await model.refresh() }
|
||||
} label: {
|
||||
Label("重新检测", systemImage: "arrow.clockwise")
|
||||
}
|
||||
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
|
||||
.disabled(model.isLoading)
|
||||
}
|
||||
}
|
||||
|
||||
AICard {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
AISectionTitle("本机保存的配置", systemImage: "slider.horizontal.3")
|
||||
AIKeyValueRow("地址", value: model.instance.origin.absoluteString, monospaced: true)
|
||||
if let groupName {
|
||||
AIKeyValueRow("分组", value: groupName)
|
||||
}
|
||||
AIKeyValueRow("标签", value: model.instance.tags.isEmpty ? "无" : model.instance.tags.joined(separator: "、"))
|
||||
AIKeyValueRow("访问凭据", value: model.instance.credentialConfigured ? "已保存" : "未设置")
|
||||
}
|
||||
}
|
||||
|
||||
// 配置是否丢失不是用户需要被告知的事,卡片本身已经说明了一切。
|
||||
}
|
||||
}
|
||||
|
||||
private var headerCard: some View {
|
||||
AICard(padding: 16, emphasis: true) {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(model.instance.name)
|
||||
.font(.title3.weight(.bold))
|
||||
.kerning(-0.2)
|
||||
.foregroundStyle(AITheme.ink)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.85)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text(model.instance.origin.absoluteString)
|
||||
.font(.caption)
|
||||
// 导航栏已经显示实例名,卡片内不再重复,只留地址与状态。
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "globe")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.accessibilityHidden(true)
|
||||
Text(model.instance.origin.absoluteString)
|
||||
.font(.footnote.weight(.medium))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(AITheme.ink)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Spacer(minLength: 8)
|
||||
statusChip()
|
||||
}
|
||||
|
||||
DetailMetricGrid(metrics: headerMetrics)
|
||||
DetailMetricGrid(metrics: gaugeMetrics)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Text("数据模块")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(AITheme.muted)
|
||||
Spacer()
|
||||
Text(availableCount.map { "\($0)/\(moduleSections.count)" } ?? "读取中")
|
||||
.font(.caption.weight(.semibold))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(availableCount == moduleSections.count ? AITheme.success : AITheme.muted)
|
||||
}
|
||||
Grid(alignment: .leading, horizontalSpacing: 8, verticalSpacing: 9) {
|
||||
ForEach(stride(from: 0, to: moduleSections.count, by: 2).map {
|
||||
Array(moduleSections[$0..<min($0 + 2, moduleSections.count)])
|
||||
}, id: \.first?.id) { rowSections in
|
||||
GridRow {
|
||||
ForEach(rowSections, id: \.id) { section in
|
||||
moduleIndicator(
|
||||
section.title,
|
||||
isAvailable: section.values.isEmpty == false
|
||||
)
|
||||
}
|
||||
if rowSections.count == 1 {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DetailIdentityStrip(items: identitySummary, isLoading: model.isLoading)
|
||||
|
||||
moduleAvailability
|
||||
|
||||
if groupName != nil || !model.instance.tags.isEmpty {
|
||||
FlowLayout(spacing: 7) {
|
||||
@@ -111,11 +144,74 @@ struct InstanceDetailView: View {
|
||||
AITag(tag)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func statusChip(title: String? = nil, tint overrideTint: Color? = nil) -> some View {
|
||||
let loading = model.isLoading && model.snapshot == nil
|
||||
let tint = overrideTint ?? (loading ? AITheme.muted : statusColor(model.connectionStatus))
|
||||
let label = title ?? (loading ? "检测中" : model.connectionStatus.label)
|
||||
|
||||
return HStack(spacing: 5) {
|
||||
Circle()
|
||||
.fill(tint)
|
||||
.frame(width: 7, height: 7)
|
||||
.accessibilityHidden(true)
|
||||
Text(label)
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(tint)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.frame(minHeight: 30)
|
||||
.background(tint.opacity(0.12))
|
||||
.clipShape(Capsule(style: .continuous))
|
||||
.overlay {
|
||||
Capsule(style: .continuous)
|
||||
.strokeBorder(tint.opacity(0.22))
|
||||
}
|
||||
.accessibilityLabel("连接状态 \(label)")
|
||||
}
|
||||
|
||||
/// 模块可用性压成一行读数:六个胶囊和下方分栏标题一一对应,属于纯重复。
|
||||
/// 只有真的读不到时才展开,直接点名是哪几个模块。
|
||||
private var moduleAvailability: some View {
|
||||
let unavailable = moduleSections.filter { $0.values.isEmpty }.map(\.title)
|
||||
return VStack(alignment: .leading, spacing: 5) {
|
||||
HStack(spacing: 6) {
|
||||
Text("数据模块")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(AITheme.muted)
|
||||
Spacer()
|
||||
if let available = availableCount {
|
||||
Image(systemName: available == moduleSections.count
|
||||
? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundStyle(available == moduleSections.count
|
||||
? AITheme.success : AITheme.warning)
|
||||
.accessibilityHidden(true)
|
||||
Text("\(available)/\(moduleSections.count) 可用")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(available == moduleSections.count
|
||||
? AITheme.success : AITheme.warning)
|
||||
} else {
|
||||
Text("读取中")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(AITheme.muted)
|
||||
}
|
||||
}
|
||||
if unavailable.isEmpty == false {
|
||||
Text("未返回:" + unavailable.joined(separator: " · "))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct HeaderMetric: Identifiable {
|
||||
let title: String
|
||||
let value: String?
|
||||
@@ -126,16 +222,12 @@ struct InstanceDetailView: View {
|
||||
var id: String { title }
|
||||
}
|
||||
|
||||
private var headerMetrics: [HeaderMetric] {
|
||||
let loading = model.snapshot == nil
|
||||
private var allHeaderMetrics: [HeaderMetric] {
|
||||
let loading = model.isLoading
|
||||
let phoneValue = stringValue(
|
||||
section: "sim",
|
||||
keys: ["phone_numbers", "phone_number"]
|
||||
)
|
||||
let firmwareValue = stringValue(
|
||||
section: "device",
|
||||
keys: ["revision", "firmware_version"]
|
||||
)
|
||||
let storageValue = storageSummary
|
||||
let uptimeValue = stringValue(
|
||||
section: "stats",
|
||||
@@ -169,55 +261,66 @@ struct InstanceDetailView: View {
|
||||
progress: loading ? nil : { max(0, min(1, $0 / 100)) },
|
||||
tint: loading ? nil : { resourceTint($0) }
|
||||
)
|
||||
let deviceValue = stringValue(section: "device", keys: ["model", "name", "device"])
|
||||
let carrierValue = stringValue(section: "network", keys: ["operator_name", "operator", "carrier"])
|
||||
let networkValue = stringValue(
|
||||
section: "network",
|
||||
keys: ["network_type", "technology_preference", "technology", "radio_mode"]
|
||||
)
|
||||
let ipValue = stringValue(section: "network", keys: ["ipv4", "ip_address", "ipv6"])
|
||||
|
||||
return [
|
||||
HeaderMetric(
|
||||
title: "连接状态",
|
||||
value: loading ? "--" : model.connectionStatus.label,
|
||||
systemImage: "dot.radiowaves.left.and.right",
|
||||
tint: loading ? AITheme.muted : statusColor(model.connectionStatus),
|
||||
progress: loading ? 0 : (model.connectionStatus == .online ? 1 : 0)
|
||||
),
|
||||
HeaderMetric(
|
||||
makeMetric(
|
||||
title: "号码",
|
||||
value: loading ? "--" : phoneValue ?? "无数据",
|
||||
value: loading ? nil : phoneValue ?? "无数据",
|
||||
systemImage: "simcard",
|
||||
tint: loading ? AITheme.muted : AITheme.ink,
|
||||
progress: loading ? 0 : (phoneValue == nil ? 0 : 1)
|
||||
tint: loading ? nil : AITheme.ink
|
||||
),
|
||||
makeMetric(title: "信号", value: loading ? nil : signal?.value, systemImage: "cellularbars", tint: loading ? nil : signal?.tint, progress: loading ? nil : signal?.progress),
|
||||
makeMetric(title: "延迟", value: loading ? nil : latency?.value, systemImage: "speedometer", tint: loading ? nil : latency?.tint, progress: loading ? nil : latency?.progress),
|
||||
HeaderMetric(
|
||||
makeMetric(
|
||||
title: "CPU",
|
||||
value: loading ? "--" : cpu?.value ?? "无数据",
|
||||
value: loading ? nil : cpu?.value ?? "无数据",
|
||||
systemImage: "cpu",
|
||||
tint: loading ? AITheme.muted : cpu?.tint ?? AITheme.muted,
|
||||
progress: loading ? 0 : cpu?.progress
|
||||
tint: loading ? nil : cpu?.tint ?? AITheme.muted,
|
||||
progress: loading ? nil : cpu?.progress
|
||||
),
|
||||
HeaderMetric(
|
||||
makeMetric(
|
||||
title: "内存",
|
||||
value: loading ? "--" : memory?.value ?? "无数据",
|
||||
value: loading ? nil : memory?.value ?? "无数据",
|
||||
systemImage: "memorychip",
|
||||
tint: loading ? AITheme.muted : memory?.tint ?? AITheme.muted,
|
||||
progress: loading ? 0 : memory?.progress
|
||||
tint: loading ? nil : memory?.tint ?? AITheme.muted,
|
||||
progress: loading ? nil : memory?.progress
|
||||
),
|
||||
makeMetric(title: "设备", value: loading ? nil : deviceValue, systemImage: "iphone.gen3", tint: loading ? nil : AITheme.ink),
|
||||
makeMetric(title: "运营商", value: loading ? nil : carrierValue, systemImage: "antenna.radiowaves.left.and.right", tint: loading ? nil : AITheme.ink),
|
||||
makeMetric(title: "网络", value: loading ? nil : networkValue, systemImage: "dot.radiowaves.left.and.right", tint: loading ? nil : AITheme.ink),
|
||||
makeMetric(title: "IP", value: loading ? nil : ipValue, systemImage: "network", tint: loading ? nil : AITheme.ink),
|
||||
makeMetric(title: "固件", value: loading ? nil : firmwareValue, systemImage: "waveform.path.ecg", tint: loading ? nil : AITheme.ink),
|
||||
makeMetric(title: "存储", value: loading ? nil : storageValue, systemImage: "internaldrive", tint: loading ? nil : storageTint, progress: loading ? nil : storageProgress),
|
||||
makeMetric(title: "运行时间", value: loading ? nil : uptimeValue, systemImage: "clock", tint: loading ? nil : AITheme.ink)
|
||||
]
|
||||
}
|
||||
|
||||
// 头部只保留四块带进度语义的量表,其余信息压缩成文本行。
|
||||
private static let gaugeOrder: [String] = ["信号", "延迟", "CPU", "内存"]
|
||||
|
||||
private var gaugeMetrics: [HeaderMetric] {
|
||||
let metrics = allHeaderMetrics
|
||||
return Self.gaugeOrder.compactMap { title in
|
||||
metrics.first { $0.title == title }
|
||||
}
|
||||
}
|
||||
|
||||
private var identitySummary: [(label: String, value: String)] {
|
||||
let metrics = allHeaderMetrics
|
||||
func derived(_ title: String) -> String? {
|
||||
metrics.first { $0.title == title }?.value
|
||||
}
|
||||
// 顶部只留"这是哪张卡、连在哪个网上"四件事;
|
||||
// 设备型号、固件、存储、运行时间属于硬件清单,下方分栏里已经逐条列出。
|
||||
let pairs: [(String, String?)] = [
|
||||
("号码", derived("号码")),
|
||||
("运营商", stringValue(section: "network", keys: ["operator_name", "operator", "carrier"])),
|
||||
("网络", stringValue(
|
||||
section: "network",
|
||||
keys: ["network_type", "technology_preference", "technology", "radio_mode"]
|
||||
)),
|
||||
("IP", stringValue(section: "network", keys: ["ipv4", "ip_address", "ipv6"]))
|
||||
]
|
||||
return pairs.compactMap { label, value in
|
||||
guard let value, value.isEmpty == false, value != "无数据" else { return nil }
|
||||
return (label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeMetric(
|
||||
title: String,
|
||||
value: String?,
|
||||
@@ -389,43 +492,27 @@ struct InstanceDetailView: View {
|
||||
}
|
||||
.padding(11)
|
||||
.frame(maxWidth: .infinity, minHeight: 76, maxHeight: 76, alignment: .topLeading)
|
||||
.background(AITheme.control)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.aiInsetSurface()
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
// 统一两列栅格。此前首行放 4 个指标、其余两列,导致首行指标卡只有下方一半宽。
|
||||
private struct DetailMetricGrid: View {
|
||||
let metrics: [HeaderMetric]
|
||||
|
||||
private let primaryKeys: Set<String> = ["连接状态", "号码", "信号", "延迟"]
|
||||
private let valueKeys: Set<String> = [
|
||||
"设备", "运营商", "网络", "IP", "固件", "存储", "运行时间"
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
let primary = metrics.filter { primaryKeys.contains($0.title) }
|
||||
let values = metrics.filter { valueKeys.contains($0.title) }
|
||||
|
||||
VStack(alignment: .leading, spacing: 9) {
|
||||
Grid(alignment: .leading, horizontalSpacing: 8, verticalSpacing: 8) {
|
||||
Grid(alignment: .leading, horizontalSpacing: 8, verticalSpacing: 8) {
|
||||
ForEach(
|
||||
stride(from: 0, to: metrics.count, by: 2).map {
|
||||
Array(metrics[$0..<min($0 + 2, metrics.count)])
|
||||
},
|
||||
id: \.first?.id
|
||||
) { rowMetrics in
|
||||
GridRow {
|
||||
ForEach(primary) { DetailHeaderMetric(metric: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
Grid(alignment: .leading, horizontalSpacing: 8, verticalSpacing: 8) {
|
||||
ForEach(
|
||||
stride(from: 0, to: values.count, by: 2).map {
|
||||
Array(values[$0..<min($0 + 2, values.count)])
|
||||
},
|
||||
id: \.first?.id
|
||||
) { rowMetrics in
|
||||
GridRow {
|
||||
ForEach(rowMetrics) { DetailHeaderMetric(metric: $0) }
|
||||
if rowMetrics.count == 1 {
|
||||
Color.clear
|
||||
}
|
||||
ForEach(rowMetrics) { DetailHeaderMetric(metric: $0) }
|
||||
if rowMetrics.count == 1 {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,6 +520,57 @@ struct InstanceDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 设备身份信息改为一行摘要,细节交给下方的分区卡片,避免头部堆成十几块同宽小卡。
|
||||
private struct DetailIdentityStrip: View {
|
||||
let items: [(label: String, value: String)]
|
||||
let isLoading: Bool
|
||||
|
||||
var body: some View {
|
||||
if isLoading {
|
||||
// 首屏还没有数据时用骨架条,避免出现一整列"读取中"。
|
||||
VStack(alignment: .leading, spacing: 9) {
|
||||
ForEach(0..<3, id: \.self) { _ in
|
||||
HStack(spacing: 8) {
|
||||
Capsule(style: .continuous)
|
||||
.fill(AITheme.line.opacity(0.7))
|
||||
.frame(width: 42, height: 10)
|
||||
Capsule(style: .continuous)
|
||||
.fill(AITheme.line.opacity(0.5))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.frame(height: 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(11)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.aiInsetSurface()
|
||||
.accessibilityLabel("设备信息读取中")
|
||||
} else if items.isEmpty == false {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
ForEach(items, id: \.label) { item in
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(item.label)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.frame(width: 52, alignment: .leading)
|
||||
Text(item.value)
|
||||
.font(.caption.weight(.medium))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(AITheme.ink)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
.padding(11)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.aiInsetSurface()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func moduleIndicator(_ title: String, isAvailable: Bool) -> some View {
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
@@ -452,8 +590,12 @@ struct InstanceDetailView: View {
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
// FlowLayout 按固有尺寸排布,这里不能再用 maxWidth: .infinity。
|
||||
private struct FlowLayout: Layout {
|
||||
let spacing: CGFloat
|
||||
var lineSpacing: CGFloat?
|
||||
|
||||
private var rowGap: CGFloat { lineSpacing ?? spacing }
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
@@ -462,19 +604,25 @@ struct InstanceDetailView: View {
|
||||
) -> CGSize {
|
||||
let maxWidth = proposal.width ?? .infinity
|
||||
var x: CGFloat = 0
|
||||
var height: CGFloat = 0
|
||||
var rowHeight: CGFloat = 0
|
||||
var totalHeight: CGFloat = 0
|
||||
var widestRow: CGFloat = 0
|
||||
|
||||
for subview in subviews {
|
||||
let size = subview.sizeThatFits(.unspecified)
|
||||
if x > 0, x + size.width > maxWidth {
|
||||
widestRow = max(widestRow, x - spacing)
|
||||
totalHeight += rowHeight + rowGap
|
||||
x = 0
|
||||
height += size.height + spacing
|
||||
rowHeight = 0
|
||||
}
|
||||
x += size.width + spacing
|
||||
height = max(height, size.height)
|
||||
rowHeight = max(rowHeight, size.height)
|
||||
}
|
||||
widestRow = max(widestRow, x - spacing)
|
||||
totalHeight += rowHeight
|
||||
|
||||
return CGSize(width: max(0, x - spacing), height: height)
|
||||
return CGSize(width: max(0, widestRow), height: max(0, totalHeight))
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
@@ -491,7 +639,7 @@ struct InstanceDetailView: View {
|
||||
let size = subview.sizeThatFits(.unspecified)
|
||||
if x > bounds.minX, x + size.width > bounds.maxX {
|
||||
x = bounds.minX
|
||||
y += rowHeight + spacing
|
||||
y += rowHeight + rowGap
|
||||
rowHeight = 0
|
||||
}
|
||||
subview.place(at: CGPoint(x: x, y: y), proposal: .unspecified)
|
||||
@@ -544,16 +692,19 @@ struct InstanceDetailView: View {
|
||||
}
|
||||
} else {
|
||||
AICard(padding: 16) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
AISectionTitle(section.title, systemImage: sectionIcon(section.id))
|
||||
Divider()
|
||||
.overlay(AITheme.line)
|
||||
ForEach(Array(sortedValues(section.values).enumerated()), id: \.element.key) { index, item in
|
||||
if index > 0 {
|
||||
Divider()
|
||||
.overlay(AITheme.line.opacity(0.6))
|
||||
// 行与行只靠发丝线分隔,纵向节奏交给 row 自己的内边距,
|
||||
// 否则卡片会越拉越长,一屏读不到两组数据。
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(Array(sortedValues(section.values).enumerated()), id: \.element.key) { index, item in
|
||||
if index > 0 {
|
||||
Divider()
|
||||
.overlay(AITheme.line.opacity(0.6))
|
||||
.padding(.vertical, 9)
|
||||
}
|
||||
AIKeyValueRow(fieldLabel(item.key), value: display(item.key, item.value))
|
||||
}
|
||||
AIKeyValueRow(fieldLabel(item.key), value: display(item.key, item.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,7 +716,7 @@ struct InstanceDetailView: View {
|
||||
case "device": return "iphone.gen3"
|
||||
case "sim": return "simcard"
|
||||
case "network": return "antenna.radiowaves.left.and.right"
|
||||
case "stats": return "gauge.with.needle"
|
||||
case "stats": return "chart.bar.fill"
|
||||
case "cpu": return "cpu"
|
||||
case "connectivity": return "dot.radiowaves.left.and.right"
|
||||
default: return "list.bullet.rectangle"
|
||||
|
||||
Reference in New Issue
Block a user