using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LiveChartsCore; using LiveChartsCore.Defaults; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using SkiaSharp; using Serilog; using YZWater.Core.Models; using YZWater.Core.Services; namespace YZWater.Core.ViewModels; /// /// 流量记录视图 ViewModel /// public partial class ViewCViewModel : TranslatableViewModelBase { private bool _disposed; // ═══ 图表数据 ═══ [ObservableProperty] private ISeries[] _inflowSeries; [ObservableProperty] private ISeries[] _outflowSeries; [ObservableProperty] private ISeries[] _tankLevelSeries; [ObservableProperty] private ISeries[] _pumpFreqSeries; [ObservableProperty] private Axis[] _xAxes; [ObservableProperty] private Axis[] _yAxes; [ObservableProperty] private Axis[] _tankYAxes; [ObservableProperty] private Axis[] _pumpYAxes; // ═══ 查询条件 ═══ [ObservableProperty] private DateTimeOffset? _startDate = DateTimeOffset.Now.AddDays(-1); [ObservableProperty] private DateTimeOffset? _endDate = DateTimeOffset.Now; [ObservableProperty] private List _flowRecords = new(); // ─── 翻译属性 ─── public string TitleText => T("FlowRecords"); public string SubtitleText => T("FlowHistory"); public string FromText => T("From"); public string ToText => T("To"); public string QueryText => T("Query"); public string ExportCsvText => T("ExportCsv"); public string PurgeOldText => T("PurgeOld"); public string InflowTrendText => T("InflowTrend"); public string OutflowTrendText => T("OutflowTrend"); public string TankLevelTrendText => T("TankLevelTrend"); public string PumpFreqTrendText => T("PumpFreqTrend"); public string QuickRangeText => T("QuickRange"); public string FlowTrendText => T("FlowTrend"); public string TimeHeaderText => T("TimeHeader"); public string InflowHeaderText => T("InflowHeader"); public string OutflowHeaderText => T("OutflowHeader"); public string TotalInHeaderText => T("TotalInHeader"); public string TotalOutHeaderText => T("TotalOutHeader"); public ViewCViewModel() { RegisterTranslation(nameof(TitleText), "FlowRecords"); RegisterTranslation(nameof(SubtitleText), "FlowHistory"); RegisterTranslation(nameof(FromText), "From"); RegisterTranslation(nameof(ToText), "To"); RegisterTranslation(nameof(QueryText), "Query"); RegisterTranslation(nameof(ExportCsvText), "ExportCsv"); RegisterTranslation(nameof(PurgeOldText), "PurgeOld"); RegisterTranslation(nameof(InflowTrendText), "InflowTrend"); RegisterTranslation(nameof(OutflowTrendText), "OutflowTrend"); RegisterTranslation(nameof(TankLevelTrendText), "TankLevelTrend"); RegisterTranslation(nameof(PumpFreqTrendText), "PumpFreqTrend"); RegisterTranslation(nameof(QuickRangeText), "QuickRange"); RegisterTranslation(nameof(FlowTrendText), "FlowTrend"); RegisterTranslation(nameof(TimeHeaderText), "TimeHeader"); RegisterTranslation(nameof(InflowHeaderText), "InflowHeader"); RegisterTranslation(nameof(OutflowHeaderText), "OutflowHeader"); RegisterTranslation(nameof(TotalInHeaderText), "TotalInHeader"); RegisterTranslation(nameof(TotalOutHeaderText), "TotalOutHeader"); SubscribeLanguageChanged(); InitializeCharts(); PlcPollingService.Instance.DataUpdated += OnDataUpdated; _ = LoadFlowRecordsAsync(); } private void OnDataUpdated(PlcDataModel data) { AddRealtimePoint(data); } /// /// 添加实时数据点 /// private void AddRealtimePoint(PlcDataModel data) { try { var now = DateTime.Now; AddPointToSeries(InflowSeries, now, data.InflowRate); AddPointToSeries(OutflowSeries, now, data.OutflowRate); } catch (Exception ex) { Log.Error(ex, "更新实时图表失败"); } } private static void AddPointToSeries(ISeries[] series, DateTime time, float value) { if (series?.Length > 0 && series[0] is LineSeries lineSeries) { var values = lineSeries.Values?.ToList() ?? new List(); values.Add(new DateTimePoint(time, value)); if (values.Count > 120) values.RemoveAt(0); lineSeries.Values = values.ToArray(); } } /// /// 初始化所有图表 /// private void InitializeCharts() { var timeAxis = new Axis { Labeler = value => new DateTime((long)value).ToString("MM-dd HH:mm"), UnitWidth = TimeSpan.FromHours(1).Ticks, LabelsRotation = 45 }; // 流量图表 InflowSeries = new ISeries[] { CreateLineSeries(T("InflowSeries"), SKColors.CornflowerBlue) }; OutflowSeries = new ISeries[] { CreateLineSeries(T("OutflowSeries"), SKColors.MediumSeaGreen) }; XAxes = new[] { timeAxis }; YAxes = new[] { new Axis { Name = "m³/h", MinLimit = 0 } }; // 液位图表 TankLevelSeries = new ISeries[] { CreateLineSeries(T("Tank1Name"), SKColors.CornflowerBlue), CreateLineSeries(T("Tank2Name"), SKColors.MediumSeaGreen), CreateLineSeries(T("Tank3Name"), SKColors.Orange), CreateLineSeries(T("Tank4Name"), SKColors.MediumPurple), }; TankYAxes = new[] { new Axis { Name = "%", MinLimit = 0, MaxLimit = 100 } }; // 泵频率图表 PumpFreqSeries = new ISeries[] { CreateLineSeries(T("Pump1Name"), SKColors.CornflowerBlue), CreateLineSeries(T("Pump2Name"), SKColors.MediumSeaGreen), CreateLineSeries(T("Pump3Name"), SKColors.Orange), CreateLineSeries(T("Pump4Name"), SKColors.MediumPurple), CreateLineSeries(T("Pump5Name"), SKColors.Crimson), }; PumpYAxes = new[] { new Axis { Name = "Hz", MinLimit = 0 } }; } private static LineSeries CreateLineSeries(string name, SKColor color) { return new LineSeries { Name = name, Values = new List(), Stroke = new SolidColorPaint(color, 2), Fill = null, GeometryStroke = null, GeometrySize = 0 }; } /// /// 预设时间范围 /// [RelayCommand] private void SetTimeRange(string range) { EndDate = DateTimeOffset.Now; StartDate = range switch { "1h" => DateTimeOffset.Now.AddHours(-1), "4h" => DateTimeOffset.Now.AddHours(-4), "8h" => DateTimeOffset.Now.AddHours(-8), "24h" => DateTimeOffset.Now.AddHours(-24), "7d" => DateTimeOffset.Now.AddDays(-7), "30d" => DateTimeOffset.Now.AddDays(-30), _ => DateTimeOffset.Now.AddDays(-1) }; _ = LoadFlowRecordsAsync(); } /// /// 加载历史数据 /// [RelayCommand] private async Task LoadFlowRecordsAsync() { try { var start = StartDate?.DateTime ?? DateTime.Now.AddDays(-1); var end = EndDate?.DateTime ?? DateTime.Now; // 加载流量记录 var records = await DatabaseService.Db.Queryable() .Where(r => r.RecordTime >= start && r.RecordTime <= end) .OrderByDescending(r => r.RecordTime) .ToListAsync(); FlowRecords = records; // 加载模拟量记录 var analogRecords = await DatabaseService.Db.Queryable() .Where(r => r.RecordTime >= start && r.RecordTime <= end) .OrderBy(r => r.RecordTime) .ToListAsync(); UpdateFlowChart(records); UpdateTankLevelChart(analogRecords); UpdatePumpFreqChart(analogRecords); Log.Debug("加载了 {Flow} 条流量记录, {Analog} 条模拟量记录", records.Count, analogRecords.Count); } catch (Exception ex) { Log.Error(ex, "加载历史数据失败"); } } private void UpdateFlowChart(List records) { var inflowData = records .GroupBy(r => GetTimeGroupKey(r.RecordTime)) .Select(g => new DateTimePoint(g.Key, g.Average(r => r.InflowRate))) .OrderBy(p => p.DateTime) .ToList(); var outflowData = records .GroupBy(r => GetTimeGroupKey(r.RecordTime)) .Select(g => new DateTimePoint(g.Key, g.Average(r => r.OutflowRate))) .OrderBy(p => p.DateTime) .ToList(); InflowSeries[0].Values = inflowData; OutflowSeries[0].Values = outflowData; } private void UpdateTankLevelChart(List records) { var tags = new[] { "Tank1Level", "Tank2Level", "Tank3Level", "Tank4Level" }; for (int i = 0; i < tags.Length && i < TankLevelSeries.Length; i++) { var data = records .Where(r => r.TagName == tags[i]) .GroupBy(r => GetTimeGroupKey(r.RecordTime)) .Select(g => new DateTimePoint(g.Key, g.Average(r => r.Value))) .OrderBy(p => p.DateTime) .ToList(); TankLevelSeries[i].Values = data; } } private void UpdatePumpFreqChart(List records) { var tags = new[] { "Pump1Freq", "Pump2Freq", "Pump3Freq", "Pump4Freq", "Pump5Freq" }; for (int i = 0; i < tags.Length && i < PumpFreqSeries.Length; i++) { var data = records .Where(r => r.TagName == tags[i]) .GroupBy(r => GetTimeGroupKey(r.RecordTime)) .Select(g => new DateTimePoint(g.Key, g.Average(r => r.Value))) .OrderBy(p => p.DateTime) .ToList(); PumpFreqSeries[i].Values = data; } } /// /// 根据时间范围自动选择分组精度 /// private static DateTime GetTimeGroupKey(DateTime time) { return time.Date.AddHours(time.Hour).AddMinutes(time.Minute / 5 * 5); } [RelayCommand] private async Task ExportDataAsync() { try { var filePath = $"FlowRecords_{DateTime.Now:yyyyMMdd_HHmmss}.csv"; var lines = new List { "时间,进水流量(m³/h),出水流量(m³/h),累计进水量(m³),累计出水量(m³),备注" }; foreach (var record in FlowRecords) { lines.Add($"{record.RecordTime:yyyy-MM-dd HH:mm:ss},{record.InflowRate},{record.OutflowRate},{record.TotalInflow},{record.TotalOutflow},{record.Remark}"); } await File.WriteAllLinesAsync(filePath, lines); Log.Information("流量记录已导出到: {FilePath}", filePath); } catch (Exception ex) { Log.Error(ex, "导出流量记录失败"); } } [RelayCommand] private async Task ClearOldDataAsync() { try { var cutoffDate = DateTime.Now.AddDays(-30); await DatabaseService.Db.Deleteable() .Where(r => r.RecordTime < cutoffDate) .ExecuteCommandAsync(); await DatabaseService.Db.Deleteable() .Where(r => r.RecordTime < cutoffDate) .ExecuteCommandAsync(); await LoadFlowRecordsAsync(); Log.Information("已清除 30 天前的历史数据"); } catch (Exception ex) { Log.Error(ex, "清除旧数据失败"); } } public new void Dispose() { if (_disposed) return; _disposed = true; PlcPollingService.Instance.DataUpdated -= OnDataUpdated; base.Dispose(); } }