import SwiftUI struct InstanceDetailView: View { @StateObject private var model: DeviceViewModel @State private var revealsSensitive = false init(instance: Instance, model: DeviceViewModel? = nil) { _model = StateObject(wrappedValue: model ?? DefaultContainer.makeDeviceModel(instance: instance)) } var body: some View { ScrollView { LazyVStack(spacing: 12) { header 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) } } } .padding(16) .padding(.bottom, 8) } .background(AITheme.canvas.ignoresSafeArea()) .navigationTitle(model.instance.name) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .refreshable { await model.refresh() } .task { await model.refresh() } } private var header: some View { AICard(padding: 18) { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .top, spacing: 14) { Image(systemName: "cpu") .font(.system(size: 21, weight: .semibold)) .foregroundStyle(AITheme.primary) .frame(width: 46, height: 46) .background(AITheme.primary.opacity(0.11)) .clipShape(RoundedRectangle(cornerRadius: 15, style: .continuous)) .accessibilityHidden(true) VStack(alignment: .leading, spacing: 5) { Text(model.instance.name) .font(.title3.weight(.bold)) .kerning(-0.2) .foregroundStyle(AITheme.ink) .lineLimit(2) Text(model.instance.origin.absoluteString) .font(.caption) .foregroundStyle(AITheme.muted) .lineLimit(2) .truncationMode(.middle) } } HStack(spacing: 10) { AITag(revealsSensitive ? "敏感值已显示" : "敏感值已隐藏", tint: AITheme.primary) if let availableCount { AITag("\(availableCount)/6 模块", tint: AITheme.success) } } Button { withAnimation(.easeOut(duration: 0.18)) { revealsSensitive.toggle() } } label: { Label( revealsSensitive ? "隐藏敏感值" : "显示敏感值", systemImage: revealsSensitive ? "eye.slash" : "eye" ) } .buttonStyle(AIButtonStyle(kind: .secondary, size: .compact, fixedWidth: 112)) if model.isLoading { AILoadingCard("正在读取设备详情") } } } } private var availableCount: Int? { guard let snapshot = model.snapshot else { return nil } return snapshot.sections.filter { !$0.values.isEmpty }.count } @ViewBuilder private func sectionCard(_ section: DeviceSection) -> some View { if section.values.isEmpty { AICard(padding: 14) { HStack(spacing: 10) { Image(systemName: "wifi.slash") .font(.subheadline.weight(.semibold)) .foregroundStyle(AITheme.muted) .frame(width: 32, height: 32) .background(AITheme.control) .clipShape(RoundedRectangle(cornerRadius: 11, style: .continuous)) .accessibilityHidden(true) VStack(alignment: .leading, spacing: 2) { Text(section.title) .font(.subheadline.weight(.semibold)) .foregroundStyle(AITheme.ink) Text("模块未返回数据") .font(.caption) .foregroundStyle(AITheme.muted) } } } } else { AICard(padding: 18) { VStack(alignment: .leading, spacing: 16) { 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)) } AIKeyValueRow(fieldLabel(item.key), value: display(item.key, item.value)) } } } } } private func sectionIcon(_ id: String) -> String { switch id { case "device": return "iphone.gen3" case "sim": return "simcard" case "network": return "antenna.radiowaves.left.and.right" case "stats": return "gauge.with.needle" case "cpu": return "cpu" case "connectivity": return "dot.radiowaves.left.and.right" default: return "list.bullet.rectangle" } } private let fieldLabels: [String: String] = [ "model": "型号", "manufacturer": "制造商", "imei": "IMEI", "revision": "固件版本", "online": "在线", "powered": "基带电源", "present": "SIM 在位", "iccid": "ICCID", "imsi": "IMSI", "phone_numbers": "号码", "sms_center": "短信中心", "operator_name": "运营商", "registration_status": "注册状态", "technology_preference": "网络制式", "signal_strength": "信号强度", "brand": "品牌", "meid": "MEID", "phone_number": "号码", "firmware_version": "固件版本", "os_version": "系统版本", "baseband_version": "基带版本", "uptime": "运行时间", "uptime_seconds": "运行时间", "slots": "卡槽数", "active_slots": "活跃数", "active": "已激活", "enabled": "已启用", "operator": "运营商", "carrier": "运营商", "network_type": "接入制式", "technology": "技术", "radio_mode": "网络模式", "signal": "信号强度", "signal_dbm": "信号强度(dBm)", "signal_percent": "信号质量", "rsrp": "RSRP", "rsrq": "RSRQ", "sinr": "SINR", "mcc": "MCC", "mnc": "MNC", "registration": "注册状态", "registration_state": "注册状态", "roaming": "漫游", "airplane_mode": "飞行模式", "ipv4": "IPv4 地址", "ipv6": "IPv6 地址", "ip_address": "IP 地址", "gateway": "网关", "dns": "DNS", "download": "下行速率", "upload": "上行速率", "download_bytes": "下行流量", "upload_bytes": "上行流量", "messages_today": "今日消息数", "calls": "通话数", "usage": "使用率", "temperature": "温度", "max_temperature_c": "最高温度", "cpu_percent": "CPU 使用率", "cpu_load": "CPU 负载", "memory_percent": "内存使用率", "memory_total_mb": "内存总量", "memory_available_mb": "可用内存", "battery_level": "电量", "battery_percent": "电量", "latency": "延迟", "connected": "连接状态", "interface": "网络接口", "ssid": "无线网络", "apn": "APN", "core_count": "核心数", "model_name": "CPU 型号", "state": "连接状态", "latency_ms": "延迟(ms)" ] private let valueOrder: [String] = [ "model", "manufacturer", "brand", "imei", "meid", "revision", "firmware_version", "online", "powered", "present", "slots", "active_slots", "active", "iccid", "imsi", "phone_numbers", "phone_number", "sms_center", "operator_name", "operator", "carrier", "registration_status", "registration_state", "technology_preference", "network_type", "radio_mode", "signal_strength", "signal_dbm", "signal_percent", "rsrp", "rsrq", "sinr", "state", "connected", "latency", "latency_ms", "uptime", "uptime_seconds", "cpu_percent", "cpu_load", "memory_percent", "memory_total_mb", "memory_available_mb", "usage", "temperature", "max_temperature_c", "battery_level", "battery_percent", "download", "upload", "download_bytes", "upload_bytes", "ipv4", "ipv6", "ip_address", "gateway", "dns", "interface", "ssid", "apn", "core_count", "model_name", "messages_today", "calls" ] private let durationKeys: Set = ["uptime", "uptime_seconds"] private let percentKeys: Set = ["usage", "cpu_percent", "memory_percent"] private let byteRateKeys: Set = ["download", "upload"] private let byteCountKeys: Set = ["download_bytes", "upload_bytes"] private let sensitiveKeys: Set = ["imei", "iccid", "imsi", "phone_numbers", "sms_center"] private func sortedValues(_ values: [String: String]) -> [(key: String, value: String)] { values .map { (key: $0.key, value: $0.value) } .sorted { lhs, rhs in let lhsRank = valueOrder.firstIndex(of: lhs.key) ?? Int.max let rhsRank = valueOrder.firstIndex(of: rhs.key) ?? Int.max if lhsRank != rhsRank { return lhsRank < rhsRank } return lhs.key < rhs.key } } private func fieldLabel(_ key: String) -> String { if let exact = fieldLabels[key] { return exact } let parts = key.split(separator: "_").map(String.init) for count in [2, 1] where parts.count >= count { let suffix = parts.suffix(count).joined(separator: "_") if let label = fieldLabels[suffix] { return label } } return parts.joined(separator: " ") } private func display(_ key: String, _ value: String) -> String { if matches(key, durationKeys), let seconds = Double(value), seconds >= 0 { return Duration.seconds(seconds).formatted(.units(allowed: [.days, .hours, .minutes], width: .abbreviated)) } if matches(key, percentKeys), let number = Double(value), number >= 0 { return "\(number.formatted(.number.precision(.fractionLength(0...1))))%" } if matches(key, byteRateKeys), let bytesPerSecond = Double(value), bytesPerSecond >= 0 { return ByteCountFormatter.string(fromByteCount: Int64(bytesPerSecond), countStyle: .decimal) + "/s" } if matches(key, byteCountKeys), let bytes = Double(value), bytes >= 0 { return ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .decimal) } if matches(key, ["memory_total_mb"]), let megabytes = Double(value) { return ByteCountFormatter.string(fromByteCount: Int64(megabytes * 1_048_576), countStyle: .memory) } if matches(key, ["memory_available_mb"]), let megabytes = Double(value) { return ByteCountFormatter.string(fromByteCount: Int64(megabytes * 1_048_576), countStyle: .memory) } if key.hasSuffix("temperature"), let temperature = Double(value) { return "\(temperature.formatted(.number.precision(.fractionLength(0...1))))°C" } if sensitiveKeys.contains(key), !revealsSensitive { return masked(value) } return value } private func masked(_ value: String) -> String { guard let first = value.first, value.count > 6 else { return String(repeating: "•", count: min(value.count, 8)) } let tail = value.suffix(4) return "\(first) •••• \(tail)" } private func matches(_ key: String, _ keys: Set) -> Bool { let parts = key.split(separator: "_").map(String.init) let suffixes = [key] + (1...2).compactMap { count in parts.count >= count ? parts.suffix(count).joined(separator: "_") : nil } return suffixes.contains { keys.contains($0) } } }