using System; using System.Collections.ObjectModel; using System.Windows; using System.Windows.Media; using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; using MonitorSnapshot = ivf_tl_Control.MonitorSnapshot; using ControlAppData = ivf_tl_Control.AppData; namespace ivf_tl_Operate.ViewModel { /// /// M5-03-3/4:只读「服务监控」页 ViewModel(需求 7 只读服务监控、需求 10 链路健康 + 不假装实时)。 /// /// 纯只读:仅定时调用同进程 的 /// 拉取只读快照并映射到展示属性, /// **不持有任何可控制 control 的引用、页面无任何写控件/下发按钮**(约束 4 只读边界)。 /// /// 刷新:DispatcherTimer 默认 2s(阈值/延迟具体值 [D10] 占位,留 M7 校准)。 /// 链路健康按「最后成功通讯时间」是否过期上色:绿=正常、红=失联/过期。 /// [M7] 各字段运行时取值、断线红/恢复绿需运行环境验证(本地不可构建/运行)。 /// public partial class ServiceMonitorViewModel : ObservableObject, IDisposable { private readonly DispatcherTimer _timer; // [D10] 失联判定阈值占位:超过该秒数未成功通讯即视为过期/失联(留 M7 按实时指标校准)。 private const int StaleSeconds = 30; public ServiceMonitorViewModel() { Houses = new ObservableCollection(); Refresh(); _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; _timer.Tick += (s, e) => Refresh(); _timer.Start(); } // —— 顶部链路健康 —— [ObservableProperty] private string snapshotTime; [ObservableProperty] private bool controlHosted; [ObservableProperty] private string controlHostedText; [ObservableProperty] private Brush controlHostedBrush; [ObservableProperty] private string mqttText; [ObservableProperty] private Brush mqttBrush; [ObservableProperty] private string mqttLastOk; [ObservableProperty] private string serverUrl; [ObservableProperty] private string httpText; [ObservableProperty] private Brush httpBrush; [ObservableProperty] private string httpLastOk; [ObservableProperty] private string kafkaText; [ObservableProperty] private Brush kafkaBrush; [ObservableProperty] private string kafkaLastOk; [ObservableProperty] private string uploadQueueText; [ObservableProperty] private Brush uploadQueueBrush; [ObservableProperty] private string diskText; [ObservableProperty] private Brush diskBrush; public ObservableCollection Houses { get; } private static readonly Brush Green = new SolidColorBrush(Color.FromRgb(0x2E, 0xA0, 0x43)); private static readonly Brush Red = new SolidColorBrush(Color.FromRgb(0xD0, 0x32, 0x2D)); private static readonly Brush Gray = new SolidColorBrush(Color.FromRgb(0x95, 0x95, 0x96)); private static readonly Brush Amber = new SolidColorBrush(Color.FromRgb(0xE0, 0x9A, 0x12)); /// 拉取只读快照并刷新展示属性。任何异常被吞掉,避免监控页崩。 public void Refresh() { try { MonitorSnapshot snap = null; try { snap = ControlAppData.Instance != null ? ControlAppData.Instance.GetMonitorSnapshot() : null; } catch { snap = null; } if (snap == null) { ControlHosted = false; ControlHostedText = "后台未托管 / 取数失败"; ControlHostedBrush = Gray; SnapshotTime = "—"; return; } SnapshotTime = snap.SnapshotAt.ToString("yyyy-MM-dd HH:mm:ss"); ControlHosted = snap.ControlHosted; ControlHostedText = snap.ControlHosted ? "后台服务已托管" : "后台未托管"; ControlHostedBrush = snap.ControlHosted ? Green : Red; // MQTT 链路 MqttText = snap.MqttConnected ? "已连接" : "未连接"; MqttBrush = snap.MqttConnected ? Green : Red; MqttLastOk = FmtOk(snap.LastMqttOkAt); // 服务器 / HTTP 链路(按最后成功时间过期判定) ServerUrl = string.IsNullOrEmpty(snap.ServerUrl) ? "—" : snap.ServerUrl; HttpBrush = LinkBrush(snap.LastHttpOkAt); HttpText = snap.LastHttpOkAt == null ? "未知" : (IsStale(snap.LastHttpOkAt) ? "可能失联" : "正常"); HttpLastOk = FmtOk(snap.LastHttpOkAt); // Kafka / 图片上传链路 KafkaBrush = LinkBrush(snap.LastKafkaOkAt); KafkaText = snap.LastKafkaOkAt == null ? "未知" : (IsStale(snap.LastKafkaOkAt) ? "可能失联" : "正常"); KafkaLastOk = FmtOk(snap.LastKafkaOkAt); // 上传队列(内存缓存 + 落盘待传) int pending = snap.ImageCacheCount + snap.PendingDiskImageCount; UploadQueueText = snap.ImageBacklogAlarm ? $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}(堆积告警>{snap.PendingImageAlarmThreshold})" : $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}"; // M5-04-3:断网累积兜底——以 control 侧权威 ImageBacklogAlarm 判定红色显著提示(不静默堆积); // 未达阈值但有待传时琥珀提示。[D10] 阈值由 control PendingImageAlarmThreshold 提供,留 M7 校准。 UploadQueueBrush = snap.ImageBacklogAlarm ? Red : (pending == 0 ? Green : Amber); // 磁盘 if (string.IsNullOrEmpty(snap.DiskPath)) { DiskText = "—"; DiskBrush = Gray; } else if (!snap.DiskExist) { DiskText = $"{snap.DiskPath}: 盘不存在"; DiskBrush = Red; } else { DiskText = $"{snap.DiskPath}: 剩余 {snap.DiskFreeGb:0.0} GB"; DiskBrush = snap.DiskFreeGb < 10 ? Red : (snap.DiskFreeGb < 30 ? Amber : Green); } // 舱室 Houses.Clear(); foreach (var h in snap.Houses) { Houses.Add(new HouseMonitorRowVm { HouseSn = h.HouseSn, PortName = string.IsNullOrEmpty(h.PortName) ? "—" : h.PortName, RunState = string.IsNullOrEmpty(h.RunState) ? "—" : h.RunState, Temperature = $"{h.Temperature:0.0}", Pressure = $"{h.Pressure:0.0}", StateText = string.IsNullOrEmpty(h.HouseState) ? "—" : h.HouseState, StateBrush = (h.HouseState != null && h.HouseState.Contains("关")) ? Green : Red, ComText = string.IsNullOrEmpty(h.ComState) ? "—" : h.ComState, ComBrush = (h.ComState == "已连接") ? Green : Red, CcdText = (h.CcdState == "正常" && !h.CcdError) ? "正常" : "异常", CcdBrush = (h.CcdState == "正常" && !h.CcdError) ? Green : Red, }); } } catch { // 监控页只读,取数异常不抛 } } private static bool IsStale(DateTime? t) => t == null || (DateTime.Now - t.Value).TotalSeconds > StaleSeconds; private static Brush LinkBrush(DateTime? t) { if (t == null) return Gray; // 未知(尚未成功通讯) return IsStale(t) ? Red : Green; // 过期=红 / 新鲜=绿 } private static string FmtOk(DateTime? t) => t == null ? "—" : t.Value.ToString("HH:mm:ss"); public void Dispose() { try { _timer?.Stop(); } catch { } } } /// 单舱室只读展示行。 public class HouseMonitorRowVm { public int HouseSn { get; set; } public string PortName { get; set; } public string RunState { get; set; } public string Temperature { get; set; } public string Pressure { get; set; } public string StateText { get; set; } public Brush StateBrush { get; set; } public string ComText { get; set; } public Brush ComBrush { get; set; } public string CcdText { get; set; } public Brush CcdBrush { get; set; } } }