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; }
}
}