ViewCViewModel.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. using CommunityToolkit.Mvvm.ComponentModel;
  2. using CommunityToolkit.Mvvm.Input;
  3. using LiveChartsCore;
  4. using LiveChartsCore.Defaults;
  5. using LiveChartsCore.SkiaSharpView;
  6. using LiveChartsCore.SkiaSharpView.Painting;
  7. using SkiaSharp;
  8. using Serilog;
  9. using YZWater.Core.Models;
  10. using YZWater.Core.Services;
  11. namespace YZWater.Core.ViewModels;
  12. /// <summary>
  13. /// 流量记录视图 ViewModel
  14. /// </summary>
  15. public partial class ViewCViewModel : TranslatableViewModelBase
  16. {
  17. private bool _disposed;
  18. // ═══ 图表数据 ═══
  19. [ObservableProperty] private ISeries[] _inflowSeries;
  20. [ObservableProperty] private ISeries[] _outflowSeries;
  21. [ObservableProperty] private ISeries[] _tankLevelSeries;
  22. [ObservableProperty] private ISeries[] _pumpFreqSeries;
  23. [ObservableProperty] private Axis[] _xAxes;
  24. [ObservableProperty] private Axis[] _yAxes;
  25. [ObservableProperty] private Axis[] _tankYAxes;
  26. [ObservableProperty] private Axis[] _pumpYAxes;
  27. // ═══ 查询条件 ═══
  28. [ObservableProperty] private DateTimeOffset? _startDate = DateTimeOffset.Now.AddDays(-1);
  29. [ObservableProperty] private DateTimeOffset? _endDate = DateTimeOffset.Now;
  30. [ObservableProperty] private List<FlowRecord> _flowRecords = new();
  31. // ─── 翻译属性 ───
  32. public string TitleText => T("FlowRecords");
  33. public string SubtitleText => T("FlowHistory");
  34. public string FromText => T("From");
  35. public string ToText => T("To");
  36. public string QueryText => T("Query");
  37. public string ExportCsvText => T("ExportCsv");
  38. public string PurgeOldText => T("PurgeOld");
  39. public string InflowTrendText => T("InflowTrend");
  40. public string OutflowTrendText => T("OutflowTrend");
  41. public string TankLevelTrendText => T("TankLevelTrend");
  42. public string PumpFreqTrendText => T("PumpFreqTrend");
  43. public string QuickRangeText => T("QuickRange");
  44. public string FlowTrendText => T("FlowTrend");
  45. public string TimeHeaderText => T("TimeHeader");
  46. public string InflowHeaderText => T("InflowHeader");
  47. public string OutflowHeaderText => T("OutflowHeader");
  48. public string TotalInHeaderText => T("TotalInHeader");
  49. public string TotalOutHeaderText => T("TotalOutHeader");
  50. public ViewCViewModel()
  51. {
  52. RegisterTranslation(nameof(TitleText), "FlowRecords");
  53. RegisterTranslation(nameof(SubtitleText), "FlowHistory");
  54. RegisterTranslation(nameof(FromText), "From");
  55. RegisterTranslation(nameof(ToText), "To");
  56. RegisterTranslation(nameof(QueryText), "Query");
  57. RegisterTranslation(nameof(ExportCsvText), "ExportCsv");
  58. RegisterTranslation(nameof(PurgeOldText), "PurgeOld");
  59. RegisterTranslation(nameof(InflowTrendText), "InflowTrend");
  60. RegisterTranslation(nameof(OutflowTrendText), "OutflowTrend");
  61. RegisterTranslation(nameof(TankLevelTrendText), "TankLevelTrend");
  62. RegisterTranslation(nameof(PumpFreqTrendText), "PumpFreqTrend");
  63. RegisterTranslation(nameof(QuickRangeText), "QuickRange");
  64. RegisterTranslation(nameof(FlowTrendText), "FlowTrend");
  65. RegisterTranslation(nameof(TimeHeaderText), "TimeHeader");
  66. RegisterTranslation(nameof(InflowHeaderText), "InflowHeader");
  67. RegisterTranslation(nameof(OutflowHeaderText), "OutflowHeader");
  68. RegisterTranslation(nameof(TotalInHeaderText), "TotalInHeader");
  69. RegisterTranslation(nameof(TotalOutHeaderText), "TotalOutHeader");
  70. SubscribeLanguageChanged();
  71. InitializeCharts();
  72. PlcPollingService.Instance.DataUpdated += OnDataUpdated;
  73. _ = LoadFlowRecordsAsync();
  74. }
  75. private void OnDataUpdated(PlcDataModel data)
  76. {
  77. AddRealtimePoint(data);
  78. }
  79. /// <summary>
  80. /// 添加实时数据点
  81. /// </summary>
  82. private void AddRealtimePoint(PlcDataModel data)
  83. {
  84. try
  85. {
  86. var now = DateTime.Now;
  87. AddPointToSeries(InflowSeries, now, data.InflowRate);
  88. AddPointToSeries(OutflowSeries, now, data.OutflowRate);
  89. }
  90. catch (Exception ex)
  91. {
  92. Log.Error(ex, "更新实时图表失败");
  93. }
  94. }
  95. private static void AddPointToSeries(ISeries[] series, DateTime time, float value)
  96. {
  97. if (series?.Length > 0 && series[0] is LineSeries<DateTimePoint> lineSeries)
  98. {
  99. var values = lineSeries.Values?.ToList() ?? new List<DateTimePoint>();
  100. values.Add(new DateTimePoint(time, value));
  101. if (values.Count > 120) values.RemoveAt(0);
  102. lineSeries.Values = values.ToArray();
  103. }
  104. }
  105. /// <summary>
  106. /// 初始化所有图表
  107. /// </summary>
  108. private void InitializeCharts()
  109. {
  110. var timeAxis = new Axis
  111. {
  112. Labeler = value => new DateTime((long)value).ToString("MM-dd HH:mm"),
  113. UnitWidth = TimeSpan.FromHours(1).Ticks,
  114. LabelsRotation = 45
  115. };
  116. // 流量图表
  117. InflowSeries = new ISeries[] { CreateLineSeries(T("InflowSeries"), SKColors.CornflowerBlue) };
  118. OutflowSeries = new ISeries[] { CreateLineSeries(T("OutflowSeries"), SKColors.MediumSeaGreen) };
  119. XAxes = new[] { timeAxis };
  120. YAxes = new[] { new Axis { Name = "m³/h", MinLimit = 0 } };
  121. // 液位图表
  122. TankLevelSeries = new ISeries[]
  123. {
  124. CreateLineSeries(T("Tank1Name"), SKColors.CornflowerBlue),
  125. CreateLineSeries(T("Tank2Name"), SKColors.MediumSeaGreen),
  126. CreateLineSeries(T("Tank3Name"), SKColors.Orange),
  127. CreateLineSeries(T("Tank4Name"), SKColors.MediumPurple),
  128. };
  129. TankYAxes = new[] { new Axis { Name = "%", MinLimit = 0, MaxLimit = 100 } };
  130. // 泵频率图表
  131. PumpFreqSeries = new ISeries[]
  132. {
  133. CreateLineSeries(T("Pump1Name"), SKColors.CornflowerBlue),
  134. CreateLineSeries(T("Pump2Name"), SKColors.MediumSeaGreen),
  135. CreateLineSeries(T("Pump3Name"), SKColors.Orange),
  136. CreateLineSeries(T("Pump4Name"), SKColors.MediumPurple),
  137. CreateLineSeries(T("Pump5Name"), SKColors.Crimson),
  138. };
  139. PumpYAxes = new[] { new Axis { Name = "Hz", MinLimit = 0 } };
  140. }
  141. private static LineSeries<DateTimePoint> CreateLineSeries(string name, SKColor color)
  142. {
  143. return new LineSeries<DateTimePoint>
  144. {
  145. Name = name,
  146. Values = new List<DateTimePoint>(),
  147. Stroke = new SolidColorPaint(color, 2),
  148. Fill = null,
  149. GeometryStroke = null,
  150. GeometrySize = 0
  151. };
  152. }
  153. /// <summary>
  154. /// 预设时间范围
  155. /// </summary>
  156. [RelayCommand]
  157. private void SetTimeRange(string range)
  158. {
  159. EndDate = DateTimeOffset.Now;
  160. StartDate = range switch
  161. {
  162. "1h" => DateTimeOffset.Now.AddHours(-1),
  163. "4h" => DateTimeOffset.Now.AddHours(-4),
  164. "8h" => DateTimeOffset.Now.AddHours(-8),
  165. "24h" => DateTimeOffset.Now.AddHours(-24),
  166. "7d" => DateTimeOffset.Now.AddDays(-7),
  167. "30d" => DateTimeOffset.Now.AddDays(-30),
  168. _ => DateTimeOffset.Now.AddDays(-1)
  169. };
  170. _ = LoadFlowRecordsAsync();
  171. }
  172. /// <summary>
  173. /// 加载历史数据
  174. /// </summary>
  175. [RelayCommand]
  176. private async Task LoadFlowRecordsAsync()
  177. {
  178. try
  179. {
  180. var start = StartDate?.DateTime ?? DateTime.Now.AddDays(-1);
  181. var end = EndDate?.DateTime ?? DateTime.Now;
  182. // 加载流量记录
  183. var records = await DatabaseService.Db.Queryable<FlowRecord>()
  184. .Where(r => r.RecordTime >= start && r.RecordTime <= end)
  185. .OrderByDescending(r => r.RecordTime)
  186. .ToListAsync();
  187. FlowRecords = records;
  188. // 加载模拟量记录
  189. var analogRecords = await DatabaseService.Db.Queryable<AnalogRecord>()
  190. .Where(r => r.RecordTime >= start && r.RecordTime <= end)
  191. .OrderBy(r => r.RecordTime)
  192. .ToListAsync();
  193. UpdateFlowChart(records);
  194. UpdateTankLevelChart(analogRecords);
  195. UpdatePumpFreqChart(analogRecords);
  196. Log.Debug("加载了 {Flow} 条流量记录, {Analog} 条模拟量记录", records.Count, analogRecords.Count);
  197. }
  198. catch (Exception ex)
  199. {
  200. Log.Error(ex, "加载历史数据失败");
  201. }
  202. }
  203. private void UpdateFlowChart(List<FlowRecord> records)
  204. {
  205. var inflowData = records
  206. .GroupBy(r => GetTimeGroupKey(r.RecordTime))
  207. .Select(g => new DateTimePoint(g.Key, g.Average(r => r.InflowRate)))
  208. .OrderBy(p => p.DateTime)
  209. .ToList();
  210. var outflowData = records
  211. .GroupBy(r => GetTimeGroupKey(r.RecordTime))
  212. .Select(g => new DateTimePoint(g.Key, g.Average(r => r.OutflowRate)))
  213. .OrderBy(p => p.DateTime)
  214. .ToList();
  215. InflowSeries[0].Values = inflowData;
  216. OutflowSeries[0].Values = outflowData;
  217. }
  218. private void UpdateTankLevelChart(List<AnalogRecord> records)
  219. {
  220. var tags = new[] { "Tank1Level", "Tank2Level", "Tank3Level", "Tank4Level" };
  221. for (int i = 0; i < tags.Length && i < TankLevelSeries.Length; i++)
  222. {
  223. var data = records
  224. .Where(r => r.TagName == tags[i])
  225. .GroupBy(r => GetTimeGroupKey(r.RecordTime))
  226. .Select(g => new DateTimePoint(g.Key, g.Average(r => r.Value)))
  227. .OrderBy(p => p.DateTime)
  228. .ToList();
  229. TankLevelSeries[i].Values = data;
  230. }
  231. }
  232. private void UpdatePumpFreqChart(List<AnalogRecord> records)
  233. {
  234. var tags = new[] { "Pump1Freq", "Pump2Freq", "Pump3Freq", "Pump4Freq", "Pump5Freq" };
  235. for (int i = 0; i < tags.Length && i < PumpFreqSeries.Length; i++)
  236. {
  237. var data = records
  238. .Where(r => r.TagName == tags[i])
  239. .GroupBy(r => GetTimeGroupKey(r.RecordTime))
  240. .Select(g => new DateTimePoint(g.Key, g.Average(r => r.Value)))
  241. .OrderBy(p => p.DateTime)
  242. .ToList();
  243. PumpFreqSeries[i].Values = data;
  244. }
  245. }
  246. /// <summary>
  247. /// 根据时间范围自动选择分组精度
  248. /// </summary>
  249. private static DateTime GetTimeGroupKey(DateTime time)
  250. {
  251. return time.Date.AddHours(time.Hour).AddMinutes(time.Minute / 5 * 5);
  252. }
  253. [RelayCommand]
  254. private async Task ExportDataAsync()
  255. {
  256. try
  257. {
  258. var filePath = $"FlowRecords_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
  259. var lines = new List<string>
  260. {
  261. "时间,进水流量(m³/h),出水流量(m³/h),累计进水量(m³),累计出水量(m³),备注"
  262. };
  263. foreach (var record in FlowRecords)
  264. {
  265. lines.Add($"{record.RecordTime:yyyy-MM-dd HH:mm:ss},{record.InflowRate},{record.OutflowRate},{record.TotalInflow},{record.TotalOutflow},{record.Remark}");
  266. }
  267. await File.WriteAllLinesAsync(filePath, lines);
  268. Log.Information("流量记录已导出到: {FilePath}", filePath);
  269. }
  270. catch (Exception ex)
  271. {
  272. Log.Error(ex, "导出流量记录失败");
  273. }
  274. }
  275. [RelayCommand]
  276. private async Task ClearOldDataAsync()
  277. {
  278. try
  279. {
  280. var cutoffDate = DateTime.Now.AddDays(-30);
  281. await DatabaseService.Db.Deleteable<FlowRecord>()
  282. .Where(r => r.RecordTime < cutoffDate)
  283. .ExecuteCommandAsync();
  284. await DatabaseService.Db.Deleteable<AnalogRecord>()
  285. .Where(r => r.RecordTime < cutoffDate)
  286. .ExecuteCommandAsync();
  287. await LoadFlowRecordsAsync();
  288. Log.Information("已清除 30 天前的历史数据");
  289. }
  290. catch (Exception ex)
  291. {
  292. Log.Error(ex, "清除旧数据失败");
  293. }
  294. }
  295. public new void Dispose()
  296. {
  297. if (_disposed) return;
  298. _disposed = true;
  299. PlcPollingService.Instance.DataUpdated -= OnDataUpdated;
  300. base.Dispose();
  301. }
  302. }