|
|
@@ -1,3 +1,4 @@
|
|
|
+using System.Collections.ObjectModel;
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
using LiveChartsCore;
|
|
|
@@ -12,26 +13,71 @@ using YZWater.Core.Services;
|
|
|
namespace YZWater.Core.ViewModels;
|
|
|
|
|
|
/// <summary>
|
|
|
-/// 流量记录视图 ViewModel
|
|
|
+/// 图表分组模型 — 对应 ViewC 中的一个 CartesianChart
|
|
|
+/// </summary>
|
|
|
+public class ChartGroupModel
|
|
|
+{
|
|
|
+ /// <summary>图表标题(如"泵电流"、"液位")</summary>
|
|
|
+ public string Title { get; set; } = "";
|
|
|
+
|
|
|
+ /// <summary>Y 轴单位</summary>
|
|
|
+ public string YAxisUnit { get; set; } = "";
|
|
|
+
|
|
|
+ /// <summary>曲线系列</summary>
|
|
|
+ public ISeries[] Series { get; set; } = Array.Empty<ISeries>();
|
|
|
+
|
|
|
+ /// <summary>每条曲线对应的 TagName(与 Series 数组索引一一对应)</summary>
|
|
|
+ public List<string> TagNames { get; set; } = new();
|
|
|
+
|
|
|
+ /// <summary>Y 轴下限</summary>
|
|
|
+ public double? YMinLimit { get; set; }
|
|
|
+
|
|
|
+ /// <summary>Y 轴上限</summary>
|
|
|
+ public double? YMaxLimit { get; set; }
|
|
|
+
|
|
|
+ /// <summary>X 轴(时间轴)</summary>
|
|
|
+ public Axis[] XAxes { get; set; } = Array.Empty<Axis>();
|
|
|
+
|
|
|
+ /// <summary>Y 轴</summary>
|
|
|
+ public Axis[] YAxes { get; set; } = Array.Empty<Axis>();
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 流量记录视图 ViewModel — 按项目统计配置动态生成图表
|
|
|
/// </summary>
|
|
|
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 ObservableCollection<ChartGroupModel> _chartGroups = new();
|
|
|
|
|
|
// ═══ 查询条件 ═══
|
|
|
[ObservableProperty] private DateTimeOffset? _startDate = DateTimeOffset.Now.AddDays(-1);
|
|
|
[ObservableProperty] private DateTimeOffset? _endDate = DateTimeOffset.Now;
|
|
|
[ObservableProperty] private List<FlowRecord> _flowRecords = new();
|
|
|
|
|
|
+ // ═══ 统计报表 ═══
|
|
|
+ [ObservableProperty] private string _reportMode = "day"; // "day" / "month" / "year"
|
|
|
+ [ObservableProperty] private DateTimeOffset? _reportDate = DateTimeOffset.Now;
|
|
|
+ [ObservableProperty] private ObservableCollection<StatisticsSummary> _summaries = new();
|
|
|
+
|
|
|
+ // ═══ 进出水统计卡片 ═══
|
|
|
+ [ObservableProperty] private float _totalInflow;
|
|
|
+ [ObservableProperty] private float _totalOutflow;
|
|
|
+ [ObservableProperty] private float _flowDelta;
|
|
|
+ [ObservableProperty] private float _currentInflowRate;
|
|
|
+ [ObservableProperty] private float _currentOutflowRate;
|
|
|
+
|
|
|
+ // ─── 自动颜色分配 ───
|
|
|
+ private static readonly SKColor[] Palette =
|
|
|
+ {
|
|
|
+ SKColors.CornflowerBlue, SKColors.MediumSeaGreen, SKColors.Orange,
|
|
|
+ SKColors.MediumPurple, SKColors.Crimson, SKColors.Goldenrod,
|
|
|
+ SKColors.DodgerBlue, SKColors.HotPink, SKColors.Teal, SKColors.Sienna
|
|
|
+ };
|
|
|
+ private int _colorIndex;
|
|
|
+
|
|
|
// ─── 翻译属性 ───
|
|
|
public string TitleText => T("FlowRecords");
|
|
|
public string SubtitleText => T("FlowHistory");
|
|
|
@@ -40,18 +86,33 @@ public partial class ViewCViewModel : TranslatableViewModelBase
|
|
|
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 string StatisticsText => T("Statistics");
|
|
|
+ public string DayReportText => T("DayReport");
|
|
|
+ public string MonthReportText => T("MonthReport");
|
|
|
+ public string YearReportText => T("YearReport");
|
|
|
+ public string AvgHeaderText => T("AvgHeader");
|
|
|
+ public string MaxHeaderText => T("MaxHeader");
|
|
|
+ public string MinHeaderText => T("MinHeader");
|
|
|
+ public string TotalHeaderText => T("TotalHeaderText");
|
|
|
+ public string UnitHeaderText => T("UnitHeader");
|
|
|
+ public string ItemHeaderText => T("ItemHeader");
|
|
|
+
|
|
|
+ // ─── 进出水统计卡片翻译 ───
|
|
|
+ public string FlowSummaryText => T("FlowSummary");
|
|
|
+ public string CurrentInflowText => T("CurrentInflow");
|
|
|
+ public string CurrentOutflowText => T("CurrentOutflow");
|
|
|
+ public string TotalInflowText => T("TotalInflowCard");
|
|
|
+ public string TotalOutflowText => T("TotalOutflowCard");
|
|
|
+ public string FlowDeltaText => T("FlowDeltaCard");
|
|
|
+
|
|
|
public ViewCViewModel()
|
|
|
{
|
|
|
RegisterTranslation(nameof(TitleText), "FlowRecords");
|
|
|
@@ -61,114 +122,251 @@ public partial class ViewCViewModel : TranslatableViewModelBase
|
|
|
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");
|
|
|
|
|
|
+ // 统计报表翻译
|
|
|
+ RegisterTranslation(nameof(StatisticsText), "Statistics");
|
|
|
+ RegisterTranslation(nameof(DayReportText), "DayReport");
|
|
|
+ RegisterTranslation(nameof(MonthReportText), "MonthReport");
|
|
|
+ RegisterTranslation(nameof(YearReportText), "YearReport");
|
|
|
+ RegisterTranslation(nameof(AvgHeaderText), "AvgHeader");
|
|
|
+ RegisterTranslation(nameof(MaxHeaderText), "MaxHeader");
|
|
|
+ RegisterTranslation(nameof(MinHeaderText), "MinHeader");
|
|
|
+ RegisterTranslation(nameof(TotalHeaderText), "TotalHeaderText");
|
|
|
+ RegisterTranslation(nameof(UnitHeaderText), "UnitHeader");
|
|
|
+ RegisterTranslation(nameof(ItemHeaderText), "ItemHeader");
|
|
|
+
|
|
|
+ // 进出水统计卡片翻译
|
|
|
+ RegisterTranslation(nameof(FlowSummaryText), "FlowSummary");
|
|
|
+ RegisterTranslation(nameof(CurrentInflowText), "CurrentInflow");
|
|
|
+ RegisterTranslation(nameof(CurrentOutflowText), "CurrentOutflow");
|
|
|
+ RegisterTranslation(nameof(TotalInflowText), "TotalInflowCard");
|
|
|
+ RegisterTranslation(nameof(TotalOutflowText), "TotalOutflowCard");
|
|
|
+ RegisterTranslation(nameof(FlowDeltaText), "FlowDeltaCard");
|
|
|
+
|
|
|
SubscribeLanguageChanged();
|
|
|
- InitializeCharts();
|
|
|
+ BuildChartGroups();
|
|
|
|
|
|
PlcPollingService.Instance.DataUpdated += OnDataUpdated;
|
|
|
_ = LoadFlowRecordsAsync();
|
|
|
}
|
|
|
|
|
|
- private void OnDataUpdated(PlcDataModel data)
|
|
|
- {
|
|
|
- AddRealtimePoint(data);
|
|
|
- }
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
+ // 构建图表组
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 添加实时数据点
|
|
|
+ /// 根据项目统计配置构建图表组。
|
|
|
+ /// 有配置时按 ChartGroup 分组;无配置时走旧版硬编码。
|
|
|
/// </summary>
|
|
|
- private void AddRealtimePoint(PlcDataModel data)
|
|
|
+ private void BuildChartGroups()
|
|
|
{
|
|
|
- try
|
|
|
+ _colorIndex = 0;
|
|
|
+ var profile = ProjectService.Current;
|
|
|
+
|
|
|
+ if (profile?.Statistics?.Items is { Count: > 0 })
|
|
|
{
|
|
|
- var now = DateTime.Now;
|
|
|
- AddPointToSeries(InflowSeries, now, data.InflowRate);
|
|
|
- AddPointToSeries(OutflowSeries, now, data.OutflowRate);
|
|
|
+ BuildFromStatisticsConfig(profile.Statistics);
|
|
|
}
|
|
|
- catch (Exception ex)
|
|
|
+ else
|
|
|
{
|
|
|
- Log.Error(ex, "更新实时图表失败");
|
|
|
+ BuildLegacyChartGroups();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static void AddPointToSeries(ISeries[] series, DateTime time, float value)
|
|
|
+ /// <summary>
|
|
|
+ /// 按 StatisticsConfig 动态构建图表组
|
|
|
+ /// </summary>
|
|
|
+ private void BuildFromStatisticsConfig(StatisticsConfig config)
|
|
|
{
|
|
|
- if (series?.Length > 0 && series[0] is LineSeries<DateTimePoint> lineSeries)
|
|
|
+ var enabledItems = config.Items.Where(i => i.Enabled).ToList();
|
|
|
+
|
|
|
+ // 按 ChartGroup 分组(ChartGroup 为 null 时用 DisplayName 作为分组名)
|
|
|
+ var groups = enabledItems
|
|
|
+ .GroupBy(i => i.ChartGroup ?? i.DisplayName)
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ var newGroups = new ObservableCollection<ChartGroupModel>();
|
|
|
+
|
|
|
+ foreach (var group in groups)
|
|
|
{
|
|
|
- var values = lineSeries.Values?.ToList() ?? new List<DateTimePoint>();
|
|
|
- values.Add(new DateTimePoint(time, value));
|
|
|
- if (values.Count > 120) values.RemoveAt(0);
|
|
|
- lineSeries.Values = values.ToArray();
|
|
|
+ var items = group.ToList();
|
|
|
+ var unit = items.First().Unit;
|
|
|
+ var tagNames = items.Select(i => i.Key).ToList();
|
|
|
+
|
|
|
+ var series = new ISeries[items.Count];
|
|
|
+ for (int i = 0; i < items.Count; i++)
|
|
|
+ {
|
|
|
+ var color = ParseColor(items[i].Color) ?? NextColor();
|
|
|
+ series[i] = CreateLineSeries(items[i].GetLocalizedDisplayName(), color);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 每个图表组创建独立的 X/Y 轴(LiveCharts 不允许跨图表共享 Axis 实例)
|
|
|
+ var timeAxis = CreateTimeAxis();
|
|
|
+ var yAxis = new Axis { Name = unit, MinLimit = 0 };
|
|
|
+
|
|
|
+ newGroups.Add(new ChartGroupModel
|
|
|
+ {
|
|
|
+ Title = group.Key,
|
|
|
+ YAxisUnit = unit,
|
|
|
+ Series = series,
|
|
|
+ TagNames = tagNames,
|
|
|
+ YMinLimit = 0,
|
|
|
+ YMaxLimit = null,
|
|
|
+ XAxes = new[] { timeAxis },
|
|
|
+ YAxes = new[] { yAxis }
|
|
|
+ });
|
|
|
}
|
|
|
+
|
|
|
+ ChartGroups = newGroups;
|
|
|
+ Log.Debug("按统计配置构建了 {Count} 个图表组", newGroups.Count);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 初始化所有图表
|
|
|
+ /// 旧版硬编码图表组(无统计配置时兼容)
|
|
|
/// </summary>
|
|
|
- private void InitializeCharts()
|
|
|
+ private void BuildLegacyChartGroups()
|
|
|
{
|
|
|
- var timeAxis = new Axis
|
|
|
+ // 每个图表创建独立的 Axis(LiveCharts 不允许跨图表共享 Axis 实例)
|
|
|
+ ChartGroups = new ObservableCollection<ChartGroupModel>
|
|
|
{
|
|
|
- Labeler = value => new DateTime((long)value).ToString("MM-dd HH:mm"),
|
|
|
- UnitWidth = TimeSpan.FromHours(1).Ticks,
|
|
|
- LabelsRotation = 45
|
|
|
+ // 进水流量
|
|
|
+ new()
|
|
|
+ {
|
|
|
+ Title = T("InflowTrend"),
|
|
|
+ YAxisUnit = "m³/h",
|
|
|
+ Series = new[] { CreateLineSeries(T("InflowSeries"), SKColors.CornflowerBlue) },
|
|
|
+ TagNames = new List<string> { "InflowRate" },
|
|
|
+ XAxes = new[] { CreateTimeAxis() },
|
|
|
+ YAxes = new[] { new Axis { Name = "m³/h", MinLimit = 0 } }
|
|
|
+ },
|
|
|
+ // 出水流量
|
|
|
+ new()
|
|
|
+ {
|
|
|
+ Title = T("OutflowTrend"),
|
|
|
+ YAxisUnit = "m³/h",
|
|
|
+ Series = new[] { CreateLineSeries(T("OutflowSeries"), SKColors.MediumSeaGreen) },
|
|
|
+ TagNames = new List<string> { "OutflowRate" },
|
|
|
+ XAxes = new[] { CreateTimeAxis() },
|
|
|
+ YAxes = new[] { new Axis { Name = "m³/h", MinLimit = 0 } }
|
|
|
+ },
|
|
|
+ // 液位(4 个水箱)
|
|
|
+ new()
|
|
|
+ {
|
|
|
+ Title = T("TankLevelTrend"),
|
|
|
+ YAxisUnit = "%",
|
|
|
+ Series = new[]
|
|
|
+ {
|
|
|
+ CreateLineSeries(T("Tank1Name"), SKColors.CornflowerBlue),
|
|
|
+ CreateLineSeries(T("Tank2Name"), SKColors.MediumSeaGreen),
|
|
|
+ CreateLineSeries(T("Tank3Name"), SKColors.Orange),
|
|
|
+ CreateLineSeries(T("Tank4Name"), SKColors.MediumPurple),
|
|
|
+ },
|
|
|
+ TagNames = new List<string> { "Tank1Level", "Tank2Level", "Tank3Level", "Tank4Level" },
|
|
|
+ XAxes = new[] { CreateTimeAxis() },
|
|
|
+ YAxes = new[] { new Axis { Name = "%", MinLimit = 0, MaxLimit = 100 } }
|
|
|
+ },
|
|
|
+ // 泵频率(5 个泵)
|
|
|
+ new()
|
|
|
+ {
|
|
|
+ Title = T("PumpFreqTrend"),
|
|
|
+ YAxisUnit = "Hz",
|
|
|
+ Series = new[]
|
|
|
+ {
|
|
|
+ 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),
|
|
|
+ },
|
|
|
+ TagNames = new List<string> { "Pump1Freq", "Pump2Freq", "Pump3Freq", "Pump4Freq", "Pump5Freq" },
|
|
|
+ XAxes = new[] { CreateTimeAxis() },
|
|
|
+ YAxes = new[] { new Axis { Name = "Hz", MinLimit = 0 } }
|
|
|
+ }
|
|
|
};
|
|
|
+ }
|
|
|
+
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
+ // 实时数据
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
|
|
|
- // 流量图表
|
|
|
- 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 } };
|
|
|
+ private void OnDataUpdated(PlcDataModel data)
|
|
|
+ {
|
|
|
+ // 轮询线程回调 — 图表序列为 UI 绑定内容,必须切回 UI 线程更新
|
|
|
+ RunOnUi(() => AddRealtimePoint(data));
|
|
|
+ }
|
|
|
|
|
|
- // 液位图表
|
|
|
- TankLevelSeries = new ISeries[]
|
|
|
+ private void AddRealtimePoint(PlcDataModel data)
|
|
|
+ {
|
|
|
+ try
|
|
|
{
|
|
|
- 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 } };
|
|
|
+ var now = DateTime.Now;
|
|
|
|
|
|
- // 泵频率图表
|
|
|
- PumpFreqSeries = new ISeries[]
|
|
|
+ foreach (var group in ChartGroups)
|
|
|
+ {
|
|
|
+ for (int i = 0; i < group.TagNames.Count && i < group.Series.Length; i++)
|
|
|
+ {
|
|
|
+ var tagName = group.TagNames[i];
|
|
|
+
|
|
|
+ // 坏点里存的是最后一次有效值,画成水平线会被读成"参数一直稳定",本周期不画
|
|
|
+ if (data.IsTagBad(tagName)) continue;
|
|
|
+
|
|
|
+ // 优先从动态寄存器读取
|
|
|
+ float value;
|
|
|
+ if (data.AnalogRegisters.TryGetValue(tagName, out var regValue))
|
|
|
+ {
|
|
|
+ value = regValue;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 兼容旧 TagName(InflowRate / OutflowRate 等)
|
|
|
+ value = tagName switch
|
|
|
+ {
|
|
|
+ "InflowRate" => data.InflowRate,
|
|
|
+ "OutflowRate" => data.OutflowRate,
|
|
|
+ "Tank1Level" => data.Tank1Level,
|
|
|
+ "Tank2Level" => data.Tank2Level,
|
|
|
+ "Tank3Level" => data.Tank3Level,
|
|
|
+ "Tank4Level" => data.Tank4Level,
|
|
|
+ "Pump1Freq" => data.Pump1Freq,
|
|
|
+ "Pump2Freq" => data.Pump2Freq,
|
|
|
+ "Pump3Freq" => data.Pump3Freq,
|
|
|
+ "Pump4Freq" => data.Pump4Freq,
|
|
|
+ "Pump5Freq" => data.Pump5Freq,
|
|
|
+ _ => 0
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ AddPointToSeries(group.Series[i], now, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
{
|
|
|
- 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 } };
|
|
|
+ Log.Error(ex, "更新实时图表失败");
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- private static LineSeries<DateTimePoint> CreateLineSeries(string name, SKColor color)
|
|
|
+ private static void AddPointToSeries(ISeries series, DateTime time, float value)
|
|
|
{
|
|
|
- return new LineSeries<DateTimePoint>
|
|
|
+ if (series is LineSeries<DateTimePoint> lineSeries)
|
|
|
{
|
|
|
- Name = name,
|
|
|
- Values = new List<DateTimePoint>(),
|
|
|
- Stroke = new SolidColorPaint(color, 2),
|
|
|
- Fill = null,
|
|
|
- GeometryStroke = null,
|
|
|
- GeometrySize = 0
|
|
|
- };
|
|
|
+ var values = lineSeries.Values?.ToList() ?? new List<DateTimePoint>();
|
|
|
+ values.Add(new DateTimePoint(time, value));
|
|
|
+ if (values.Count > 120) values.RemoveAt(0);
|
|
|
+ lineSeries.Values = values.ToArray();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// 预设时间范围
|
|
|
- /// </summary>
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
+ // 历史数据查询
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
+
|
|
|
[RelayCommand]
|
|
|
private void SetTimeRange(string range)
|
|
|
{
|
|
|
@@ -186,16 +384,13 @@ public partial class ViewCViewModel : TranslatableViewModelBase
|
|
|
_ = LoadFlowRecordsAsync();
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// 加载历史数据
|
|
|
- /// </summary>
|
|
|
[RelayCommand]
|
|
|
private async Task LoadFlowRecordsAsync()
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
var start = StartDate?.DateTime ?? DateTime.Now.AddDays(-1);
|
|
|
- var end = EndDate?.DateTime ?? DateTime.Now;
|
|
|
+ var end = QueryTimeHelper.NormalizeEnd(EndDate?.DateTime, DateTime.Now);
|
|
|
|
|
|
// 加载流量记录
|
|
|
var records = await DatabaseService.Db.Queryable<FlowRecord>()
|
|
|
@@ -204,17 +399,46 @@ public partial class ViewCViewModel : TranslatableViewModelBase
|
|
|
.ToListAsync();
|
|
|
FlowRecords = records;
|
|
|
|
|
|
- // 加载模拟量记录
|
|
|
- var analogRecords = await DatabaseService.Db.Queryable<AnalogRecord>()
|
|
|
- .Where(r => r.RecordTime >= start && r.RecordTime <= end)
|
|
|
- .OrderBy(r => r.RecordTime)
|
|
|
- .ToListAsync();
|
|
|
+ // ── 计算进出水统计卡片 ──
|
|
|
+ if (records.Count > 0)
|
|
|
+ {
|
|
|
+ // 累计量取最新记录的值
|
|
|
+ var latest = records.OrderByDescending(r => r.RecordTime).First();
|
|
|
+ TotalInflow = latest.TotalInflow;
|
|
|
+ TotalOutflow = latest.TotalOutflow;
|
|
|
+ FlowDelta = latest.TotalInflow - latest.TotalOutflow;
|
|
|
+
|
|
|
+ // 当前瞬时流量取最新记录
|
|
|
+ CurrentInflowRate = latest.InflowRate;
|
|
|
+ CurrentOutflowRate = latest.OutflowRate;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ TotalInflow = 0;
|
|
|
+ TotalOutflow = 0;
|
|
|
+ FlowDelta = 0;
|
|
|
+ CurrentInflowRate = 0;
|
|
|
+ CurrentOutflowRate = 0;
|
|
|
+ }
|
|
|
|
|
|
- UpdateFlowChart(records);
|
|
|
- UpdateTankLevelChart(analogRecords);
|
|
|
- UpdatePumpFreqChart(analogRecords);
|
|
|
+ // 收集所有需要查询的 TagName
|
|
|
+ var allTagNames = ChartGroups.SelectMany(g => g.TagNames).Distinct().ToList();
|
|
|
+
|
|
|
+ if (allTagNames.Count > 0)
|
|
|
+ {
|
|
|
+ // 加载模拟量记录
|
|
|
+ var analogRecords = await DatabaseService.Db.Queryable<AnalogRecord>()
|
|
|
+ .Where(r => r.RecordTime >= start && r.RecordTime <= end
|
|
|
+ && allTagNames.Contains(r.TagName))
|
|
|
+ .OrderBy(r => r.RecordTime)
|
|
|
+ .ToListAsync();
|
|
|
+
|
|
|
+ // 按 TagName 分组后填充到各图表
|
|
|
+ UpdateChartsFromAnalogRecords(analogRecords);
|
|
|
+ }
|
|
|
|
|
|
- Log.Debug("加载了 {Flow} 条流量记录, {Analog} 条模拟量记录", records.Count, analogRecords.Count);
|
|
|
+ Log.Debug("加载了 {Flow} 条流量记录, {ChartGroups} 个图表组已更新",
|
|
|
+ records.Count, ChartGroups.Count);
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
@@ -222,61 +446,38 @@ public partial class ViewCViewModel : TranslatableViewModelBase
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private void UpdateFlowChart(List<FlowRecord> 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<AnalogRecord> records)
|
|
|
+ /// <summary>
|
|
|
+ /// 根据模拟量记录更新所有图表
|
|
|
+ /// </summary>
|
|
|
+ private void UpdateChartsFromAnalogRecords(List<AnalogRecord> 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;
|
|
|
- }
|
|
|
- }
|
|
|
+ // 按 TagName 分组
|
|
|
+ var byTag = records.GroupBy(r => r.TagName).ToDictionary(g => g.Key, g => g.ToList());
|
|
|
|
|
|
- private void UpdatePumpFreqChart(List<AnalogRecord> records)
|
|
|
- {
|
|
|
- var tags = new[] { "Pump1Freq", "Pump2Freq", "Pump3Freq", "Pump4Freq", "Pump5Freq" };
|
|
|
- for (int i = 0; i < tags.Length && i < PumpFreqSeries.Length; i++)
|
|
|
+ foreach (var group in ChartGroups)
|
|
|
{
|
|
|
- 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;
|
|
|
+ for (int i = 0; i < group.TagNames.Count && i < group.Series.Length; i++)
|
|
|
+ {
|
|
|
+ var tagName = group.TagNames[i];
|
|
|
+
|
|
|
+ if (byTag.TryGetValue(tagName, out var tagRecords))
|
|
|
+ {
|
|
|
+ var data = tagRecords
|
|
|
+ .GroupBy(r => GetTimeGroupKey(r.RecordTime))
|
|
|
+ .Select(g => new DateTimePoint(g.Key, g.Average(r => r.Value)))
|
|
|
+ .OrderBy(p => p.DateTime)
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ if (group.Series[i] is LineSeries<DateTimePoint> lineSeries)
|
|
|
+ lineSeries.Values = data;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// 根据时间范围自动选择分组精度
|
|
|
- /// </summary>
|
|
|
- private static DateTime GetTimeGroupKey(DateTime time)
|
|
|
- {
|
|
|
- return time.Date.AddHours(time.Hour).AddMinutes(time.Minute / 5 * 5);
|
|
|
- }
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
+ // 导出 / 清理
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
|
|
|
[RelayCommand]
|
|
|
private async Task ExportDataAsync()
|
|
|
@@ -284,22 +485,64 @@ public partial class ViewCViewModel : TranslatableViewModelBase
|
|
|
try
|
|
|
{
|
|
|
var filePath = $"FlowRecords_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
|
|
|
- var lines = new List<string>
|
|
|
- {
|
|
|
- "时间,进水流量(m³/h),出水流量(m³/h),累计进水量(m³),累计出水量(m³),备注"
|
|
|
- };
|
|
|
+ var profile = ProjectService.Current;
|
|
|
|
|
|
- foreach (var record in FlowRecords)
|
|
|
+ var lines = new List<string>();
|
|
|
+
|
|
|
+ if (profile?.Statistics?.Items is { Count: > 0 })
|
|
|
+ {
|
|
|
+ // 动态表头(仅导出的已启用的统计项)
|
|
|
+ var enabledItems = profile.Statistics.Items.Where(i => i.Enabled).ToList();
|
|
|
+ var lang = LanguageService.Instance;
|
|
|
+ var header = lang.Get("CsvHeaderTime") + "," + string.Join(",", enabledItems.Select(i => $"{i.GetLocalizedDisplayName()}({i.Unit})"));
|
|
|
+ lines.Add(header);
|
|
|
+
|
|
|
+ // 从 AnalogRecord 查询对应时间段的数据
|
|
|
+ var start = StartDate?.DateTime ?? DateTime.Now.AddDays(-1);
|
|
|
+ var end = QueryTimeHelper.NormalizeEnd(EndDate?.DateTime, DateTime.Now);
|
|
|
+ var tagNames = enabledItems.Select(i => i.Key).ToList();
|
|
|
+
|
|
|
+ var analogRecords = await DatabaseService.Db.Queryable<AnalogRecord>()
|
|
|
+ .Where(r => r.RecordTime >= start && r.RecordTime <= end && tagNames.Contains(r.TagName))
|
|
|
+ .OrderBy(r => r.RecordTime)
|
|
|
+ .ToListAsync();
|
|
|
+
|
|
|
+ // 按时间分组
|
|
|
+ var timeGroups = analogRecords
|
|
|
+ .GroupBy(r => GetTimeGroupKey(r.RecordTime))
|
|
|
+ .OrderBy(g => g.Key);
|
|
|
+
|
|
|
+ foreach (var timeGroup in timeGroups)
|
|
|
+ {
|
|
|
+ var valuesByTag = timeGroup.GroupBy(r => r.TagName)
|
|
|
+ .ToDictionary(g => g.Key, g => g.Average(r => r.Value));
|
|
|
+
|
|
|
+ var row = timeGroup.Key.ToString("yyyy-MM-dd HH:mm:ss");
|
|
|
+ foreach (var item in enabledItems)
|
|
|
+ {
|
|
|
+ var val = valuesByTag.TryGetValue(item.Key, out var v) ? v : 0;
|
|
|
+ row += $",{val:F1}";
|
|
|
+ }
|
|
|
+ lines.Add(row);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
{
|
|
|
- lines.Add($"{record.RecordTime:yyyy-MM-dd HH:mm:ss},{record.InflowRate},{record.OutflowRate},{record.TotalInflow},{record.TotalOutflow},{record.Remark}");
|
|
|
+ // 旧版 CSV 格式
|
|
|
+ var lang = LanguageService.Instance;
|
|
|
+ lines.Add($"{lang.Get("CsvHeaderTime")},{lang.Get("CsvHeaderInflow")},{lang.Get("CsvHeaderOutflow")},{lang.Get("CsvHeaderTotalIn")},{lang.Get("CsvHeaderTotalOut")},{lang.Get("CsvHeaderRemark")}");
|
|
|
+ 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);
|
|
|
+ Log.Information("数据已导出到: {FilePath}", filePath);
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
- Log.Error(ex, "导出流量记录失败");
|
|
|
+ Log.Error(ex, "导出数据失败");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -308,16 +551,22 @@ public partial class ViewCViewModel : TranslatableViewModelBase
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
- var cutoffDate = DateTime.Now.AddDays(-30);
|
|
|
+ var config = ConfigService.GetConfig();
|
|
|
+ var flowRetention = config?.DataRetention?.FlowRecordRetentionDays ?? 360;
|
|
|
+ var analogRetention = config?.DataRetention?.AnalogRecordRetentionDays ?? 360;
|
|
|
+
|
|
|
+ var flowCutoff = DateTime.Now.AddDays(-flowRetention);
|
|
|
+ var analogCutoff = DateTime.Now.AddDays(-analogRetention);
|
|
|
+
|
|
|
await DatabaseService.Db.Deleteable<FlowRecord>()
|
|
|
- .Where(r => r.RecordTime < cutoffDate)
|
|
|
+ .Where(r => r.RecordTime < flowCutoff)
|
|
|
.ExecuteCommandAsync();
|
|
|
await DatabaseService.Db.Deleteable<AnalogRecord>()
|
|
|
- .Where(r => r.RecordTime < cutoffDate)
|
|
|
+ .Where(r => r.RecordTime < analogCutoff)
|
|
|
.ExecuteCommandAsync();
|
|
|
|
|
|
await LoadFlowRecordsAsync();
|
|
|
- Log.Information("已清除 30 天前的历史数据");
|
|
|
+ Log.Information("已清除过期历史数据(流量 >{Flow}天, 模拟量 >{Analog}天)", flowRetention, analogRetention);
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
@@ -325,6 +574,178 @@ public partial class ViewCViewModel : TranslatableViewModelBase
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
+ // 统计报表(日/月/年)
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
+
|
|
|
+ /// <summary>切换统计报表模式(day/month/year)</summary>
|
|
|
+ [RelayCommand]
|
|
|
+ private void SetReportMode(string mode)
|
|
|
+ {
|
|
|
+ ReportMode = mode;
|
|
|
+ _ = LoadStatisticsAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>加载统计报表</summary>
|
|
|
+ [RelayCommand]
|
|
|
+ private async Task LoadStatisticsAsync()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var profile = ProjectService.Current;
|
|
|
+ if (profile?.Statistics?.Items == null || profile.Statistics.Items.Count == 0)
|
|
|
+ {
|
|
|
+ Summaries = new ObservableCollection<StatisticsSummary>();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var date = ReportDate?.DateTime ?? DateTime.Now;
|
|
|
+ var (start, end) = ReportMode switch
|
|
|
+ {
|
|
|
+ "day" => (date.Date, date.Date.AddDays(1).AddTicks(-1)),
|
|
|
+ "month" => (new DateTime(date.Year, date.Month, 1), new DateTime(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month)).AddDays(1).AddTicks(-1)),
|
|
|
+ "year" => (new DateTime(date.Year, 1, 1), new DateTime(date.Year, 12, 31).AddDays(1).AddTicks(-1)),
|
|
|
+ _ => (date.Date, date.Date.AddDays(1).AddTicks(-1))
|
|
|
+ };
|
|
|
+
|
|
|
+ var enabledItems = profile.Statistics.Items.Where(i => i.Enabled).ToList();
|
|
|
+ var summaries = new ObservableCollection<StatisticsSummary>();
|
|
|
+
|
|
|
+ foreach (var item in enabledItems)
|
|
|
+ {
|
|
|
+ var summary = new StatisticsSummary
|
|
|
+ {
|
|
|
+ TagName = item.Key,
|
|
|
+ DisplayName = item.GetLocalizedDisplayName(),
|
|
|
+ Unit = item.Unit,
|
|
|
+ AggType = item.AggType
|
|
|
+ };
|
|
|
+
|
|
|
+ switch (item.AggType)
|
|
|
+ {
|
|
|
+ case "delta":
|
|
|
+ // 累计量:取周期末值 - 初值 = 增量
|
|
|
+ var firstRecord = await DatabaseService.Db.Queryable<AnalogRecord>()
|
|
|
+ .Where(r => r.TagName == item.Key && r.RecordTime >= start && r.RecordTime <= end)
|
|
|
+ .OrderBy(r => r.RecordTime)
|
|
|
+ .FirstAsync();
|
|
|
+ var lastRecord = await DatabaseService.Db.Queryable<AnalogRecord>()
|
|
|
+ .Where(r => r.TagName == item.Key && r.RecordTime >= start && r.RecordTime <= end)
|
|
|
+ .OrderByDescending(r => r.RecordTime)
|
|
|
+ .FirstAsync();
|
|
|
+ if (firstRecord != null && lastRecord != null)
|
|
|
+ summary.TotalValue = lastRecord.Value - firstRecord.Value;
|
|
|
+ break;
|
|
|
+
|
|
|
+ case "max_only":
|
|
|
+ // 累积值(如日台时):取最大值
|
|
|
+ var maxResult = await DatabaseService.Db.Queryable<AnalogRecord>()
|
|
|
+ .Where(r => r.TagName == item.Key && r.RecordTime >= start && r.RecordTime <= end)
|
|
|
+ .MaxAsync(r => (double?)r.Value);
|
|
|
+ summary.MaxValue = maxResult;
|
|
|
+ break;
|
|
|
+
|
|
|
+ default: // avg_max_min
|
|
|
+ var records = await DatabaseService.Db.Queryable<AnalogRecord>()
|
|
|
+ .Where(r => r.TagName == item.Key && r.RecordTime >= start && r.RecordTime <= end)
|
|
|
+ .ToListAsync();
|
|
|
+ if (records.Count > 0)
|
|
|
+ {
|
|
|
+ summary.AvgValue = records.Average(r => r.Value);
|
|
|
+ summary.MaxValue = records.Max(r => r.Value);
|
|
|
+ summary.MinValue = records.Min(r => r.Value);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ summaries.Add(summary);
|
|
|
+ }
|
|
|
+
|
|
|
+ Summaries = summaries;
|
|
|
+ Log.Debug("统计报表已加载: {Mode} 模式, {Count} 个统计项", ReportMode, summaries.Count);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ Log.Error(ex, "加载统计报表失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
+ // 工具方法
|
|
|
+ // ═══════════════════════════════════════════════
|
|
|
+
|
|
|
+ private static Axis CreateTimeAxis()
|
|
|
+ {
|
|
|
+ // 初始默认显示最近 24 小时(有数据后 LiveCharts 会自动调整范围)
|
|
|
+ var now = DateTime.Now;
|
|
|
+ return new Axis
|
|
|
+ {
|
|
|
+ Labeler = value =>
|
|
|
+ {
|
|
|
+ var dt = new DateTime((long)value);
|
|
|
+ return dt.ToString("MM-dd HH:mm");
|
|
|
+ },
|
|
|
+ UnitWidth = TimeSpan.FromHours(1).Ticks,
|
|
|
+ MinLimit = now.AddHours(-24).Ticks,
|
|
|
+ MaxLimit = now.Ticks
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static LineSeries<DateTimePoint> CreateLineSeries(string name, SKColor color)
|
|
|
+ {
|
|
|
+ return new LineSeries<DateTimePoint>
|
|
|
+ {
|
|
|
+ Name = name,
|
|
|
+ Values = new List<DateTimePoint>(),
|
|
|
+ Stroke = new SolidColorPaint(color, 2),
|
|
|
+ Fill = null,
|
|
|
+ GeometryStroke = null,
|
|
|
+ GeometrySize = 0
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private SKColor NextColor()
|
|
|
+ {
|
|
|
+ var color = Palette[_colorIndex % Palette.Length];
|
|
|
+ _colorIndex++;
|
|
|
+ return color;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static SKColor? ParseColor(string? hex)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(hex)) return null;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 支持 "#RRGGBB" 和 "#AARRGGBB" 格式
|
|
|
+ var h = hex.TrimStart('#');
|
|
|
+ if (h.Length == 6)
|
|
|
+ {
|
|
|
+ var r = Convert.ToByte(h[..2], 16);
|
|
|
+ var g = Convert.ToByte(h[2..4], 16);
|
|
|
+ var b = Convert.ToByte(h[4..6], 16);
|
|
|
+ return new SKColor(r, g, b);
|
|
|
+ }
|
|
|
+ if (h.Length == 8)
|
|
|
+ {
|
|
|
+ var a = Convert.ToByte(h[..2], 16);
|
|
|
+ var r = Convert.ToByte(h[2..4], 16);
|
|
|
+ var g = Convert.ToByte(h[4..6], 16);
|
|
|
+ var b = Convert.ToByte(h[6..8], 16);
|
|
|
+ return new SKColor(r, g, b, a);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch { /* ignore parse errors */ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 根据时间范围自动选择分组精度(5 分钟桶)
|
|
|
+ /// </summary>
|
|
|
+ private static DateTime GetTimeGroupKey(DateTime time)
|
|
|
+ {
|
|
|
+ return time.Date.AddHours(time.Hour).AddMinutes(time.Minute / 5 * 5);
|
|
|
+ }
|
|
|
+
|
|
|
public new void Dispose()
|
|
|
{
|
|
|
if (_disposed) return;
|