fix(ios): improve fleet overview and app identity

- Show stable group health in a fixed 2x2 metric grid
- Add link-chain state to fleet cards and parse hardware/control/SIM/cellular
- Replace the wrapped icon with a full-bleed app icon
- Bump the IPA build number
This commit is contained in:
chick
2026-09-13 00:46:26 +08:00
parent 409345cba3
commit 2ff23d1fb0
23 changed files with 3600 additions and 269 deletions
@@ -3,16 +3,27 @@ import SwiftUI
public struct AICard<Content: View>: View {
private let content: Content
private let padding: CGFloat
private let emphasis: Bool
public init(padding: CGFloat = 16, @ViewBuilder content: () -> Content) {
public init(
padding: CGFloat = 16,
emphasis: Bool = false,
@ViewBuilder content: () -> Content
) {
self.padding = padding
self.emphasis = emphasis
self.content = content()
}
public var body: some View {
content
.padding(padding)
.background(AICard.backgroundGradient)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(padding)
.background(
AICard.backgroundGradient(
raisedTop: emphasis
)
)
.clipShape(RoundedRectangle(cornerRadius: AITheme.cardRadius, style: .continuous))
.shadow(color: .black.opacity(0.07), radius: 18, x: 0, y: 8)
.overlay {
@@ -22,10 +33,12 @@ public struct AICard<Content: View>: View {
}
}
private extension AICard {
static var backgroundGradient: LinearGradient {
extension AICard {
fileprivate static func backgroundGradient(raisedTop: Bool) -> LinearGradient {
LinearGradient(
colors: [AITheme.surfaceRaised, AITheme.surface],
colors: raisedTop
? [AITheme.surfaceRaised, AITheme.surfaceRaised.opacity(0.94)]
: [AITheme.surfaceRaised, AITheme.surface],
startPoint: .top,
endPoint: .bottom
)
@@ -150,35 +163,49 @@ public struct AIStatCard: View {
private let value: String
private let detail: String?
private let tint: Color
private let systemImage: String?
public init(title: String, value: String, detail: String? = nil, tint: Color) {
public init(
title: String,
value: String,
detail: String? = nil,
systemImage: String? = nil,
tint: Color
) {
self.title = title
self.value = value
self.detail = detail
self.systemImage = systemImage
self.tint = tint
}
public var body: some View {
AICard(padding: 14) {
VStack(alignment: .leading, spacing: 5) {
Text(title)
.font(.caption.weight(.semibold))
.lineLimit(1)
.minimumScaleFactor(0.85)
.foregroundStyle(AITheme.muted)
Text(value)
.font(.title3.weight(.bold))
.foregroundStyle(value == "0" || value.hasPrefix("0/") ? AITheme.muted : tint)
.lineLimit(1)
.minimumScaleFactor(0.75)
if let detail {
Text(detail)
.font(.caption)
.foregroundStyle(AITheme.muted)
.lineLimit(2)
AICard(padding: 12) {
HStack(alignment: .center, spacing: 10) {
if let systemImage {
Image(systemName: systemImage)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(tint)
.frame(width: 32, height: 32)
.background(tint.opacity(0.10))
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.accessibilityHidden(true)
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.caption2.weight(.semibold))
.lineLimit(1)
.minimumScaleFactor(0.85)
.foregroundStyle(AITheme.muted)
Text(value)
.font(.headline.weight(.semibold))
.foregroundStyle(value == "0" || value.hasPrefix("0/") ? AITheme.muted : tint)
.lineLimit(1)
.minimumScaleFactor(0.75)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .combine)
}
}
}
@@ -446,6 +473,7 @@ public struct AIKeyValueRow: View {
.font(.subheadline)
.foregroundStyle(AITheme.muted)
.lineLimit(2)
.layoutPriority(1)
Spacer(minLength: 12)
Text(value)
.font(.subheadline.weight(.medium))
@@ -455,6 +483,7 @@ public struct AIKeyValueRow: View {
.truncationMode(.middle)
.minimumScaleFactor(0.85)
.textSelection(.enabled)
.layoutPriority(2)
}
.accessibilityElement(children: .combine)
}
@@ -600,8 +629,7 @@ extension View {
}
func aiBottomActionBar<Content: View>(@ViewBuilder content: () -> Content) -> some View {
VStack(spacing: 0) {
self
safeAreaInset(edge: .bottom, spacing: 0) {
content()
.padding(.horizontal, 16)
.padding(.top, 12)
@@ -0,0 +1,51 @@
import SwiftUI
struct AISkeletonLine: View {
let width: CGFloat?
let height: CGFloat
init(width: CGFloat? = nil, height: CGFloat) {
self.width = width
self.height = height
}
init(width fraction: CGFloat, height: CGFloat) {
self.init(width: nil, height: height)
self.fraction = fraction
}
private var fraction: CGFloat?
var body: some View {
Group {
if let fraction {
GeometryReader { proxy in
Capsule(style: .continuous)
.frame(width: max(0, proxy.size.width * fraction))
}
} else if let width {
Capsule(style: .continuous)
.frame(width: width)
}
}
.frame(height: height)
.background(AISkeletonFill())
}
}
struct AISkeletonCard: View {
let height: CGFloat
var body: some View {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.frame(height: height)
.background(AISkeletonFill())
}
}
private struct AISkeletonFill: View {
var body: some View {
Rectangle()
.fill(AITheme.control)
}
}