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 { ZStack { AITheme.canvas.ignoresSafeArea() 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) } } .navigationTitle(model.instance.name) .refreshable { await model.refresh() } .task { await model.refresh() } } private var header: some View { AICard { VStack(alignment: .leading, spacing: 12) { AISectionTitle("设备详情", systemImage: "iphone.gen3") Button { revealsSensitive.toggle() } label: { Label( revealsSensitive ? "隐藏敏感值" : "显示敏感值", systemImage: revealsSensitive ? "eye.slash" : "eye" ) } .buttonStyle( AIButtonStyle( kind: .secondary, size: .compact, fixedWidth: AITheme.footerSecondaryButtonWidth ) ) Text(model.instance.origin.absoluteString) .font(.subheadline) .foregroundStyle(AITheme.muted) .lineLimit(2) .truncationMode(.middle) if model.isLoading { Text("正在读取设备能力…") .font(.subheadline) .foregroundStyle(AITheme.muted) } } } } private func sectionCard(_ section: DeviceSection) -> some View { AICard { VStack(alignment: .leading, spacing: 12) { AISectionTitle(section.title, systemImage: "list.bullet.rectangle") if section.values.isEmpty { Text("此模块当前不可用或未返回数据。") .font(.subheadline) .foregroundStyle(AITheme.muted) } else { ForEach(sortedValues(section.values), id: \.key) { item in AIKeyValueRow(fieldLabel(item.key), value: display(item.key, item.value)) } } } } } 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) } } }