diff --git a/MonitorModule/ViewModels/CurveRecallViewModel.cs b/MonitorModule/ViewModels/CurveRecallViewModel.cs
index 1d800ad..5f22749 100644
--- a/MonitorModule/ViewModels/CurveRecallViewModel.cs
+++ b/MonitorModule/ViewModels/CurveRecallViewModel.cs
@@ -1,68 +1,381 @@
using Logger;
-using MahApps.Metro.Controls;
-using Model.Entity;
-using Model.Models;
-using NLog.Targets;
using OxyPlot;
-using Service.Interface;
+using OxyPlot.Axes;
+using OxyPlot.Legends;
+using OxyPlot.Series;
+using Prism.Commands;
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Data;
-using System.Diagnostics;
+using System.IO;
using System.Linq;
using System.Text;
-using System.Threading.Tasks;
using System.Windows.Input;
-using System.Windows.Threading;
-using UIShare.GlobalVariable;
-using UIShare.UIViewModel;
using UIShare.ViewModelBase;
namespace MonitorModule.ViewModels
{
+ ///
+ /// 曲线回读:加载 RecordView 导出的 CSV 文件,按 (MonitorName, Scope) 分组绘制曲线。
+ ///
public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region 属性
public bool KeepAlive => true;
+
+ public PlotModel Plot { get; }
+
+ /// 已加载的曲线通道列表
+ public ObservableCollection Channels { get; } = new();
+
+ private CurveRecallChannelVM? _selectedChannel;
+ public CurveRecallChannelVM? SelectedChannel
+ {
+ get => _selectedChannel;
+ set => SetProperty(ref _selectedChannel, value);
+ }
+
+ private string _statusMessage = "请加载 CSV 文件以回读曲线";
+ public string StatusMessage
+ {
+ get => _statusMessage;
+ set => SetProperty(ref _statusMessage, value);
+ }
#endregion
#region 命令
-
+ public ICommand LoadCsvCommand { get; }
+ public ICommand ResetViewCommand { get; }
+ public ICommand ClearCommand { get; }
#endregion
#region 私有字段
-
+ // 颜色调色盘(与 MonitorView 一致)
+ private static readonly OxyColor[] _palette =
+ {
+ OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
+ OxyColors.DarkOrange, OxyColors.MediumPurple, OxyColors.Goldenrod,
+ OxyColors.Teal, OxyColors.Crimson, OxyColors.OliveDrab
+ };
+ private int _colorIndex;
#endregion
+
public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
-
+ Plot = BuildEmptyPlot();
+ LoadCsvCommand = new DelegateCommand(OnLoadCsv);
+ ResetViewCommand = new DelegateCommand(OnResetView);
+ ClearCommand = new DelegateCommand(OnClear);
}
#region 命令处理
+ ///
+ /// 打开 CSV 文件对话框,解析数据并按 (MonitorName, Scope) 分组绘制曲线。
+ ///
+ private void OnLoadCsv()
+ {
+ try
+ {
+ var dlg = new Microsoft.Win32.OpenFileDialog
+ {
+ Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
+ Title = "选择要加载的监测数据 CSV 文件"
+ };
+ if (dlg.ShowDialog() != true) return;
-
+ var records = ParseCsv(dlg.FileName);
+ if (records.Count == 0)
+ {
+ StatusMessage = "CSV 文件为空或格式不正确";
+ return;
+ }
+ // 按 (MonitorName, Scope) 分组
+ var groups = records.GroupBy(r => (r.MonitorName, r.Scope))
+ .OrderBy(g => g.Key.MonitorName)
+ .ThenBy(g => g.Key.Scope);
+
+ // 清空旧数据
+ ClearPlotAndChannels();
+
+ foreach (var group in groups)
+ {
+ var color = _palette[_colorIndex % _palette.Length];
+ _colorIndex++;
+
+ string displayName = string.IsNullOrEmpty(group.Key.Scope)
+ ? group.Key.MonitorName
+ : $"{group.Key.MonitorName} [{group.Key.Scope}]";
+
+ var channel = new CurveRecallChannelVM
+ {
+ MonitorName = group.Key.MonitorName,
+ Scope = group.Key.Scope,
+ DisplayName = displayName,
+ Color = color,
+ IsDisplayed = true
+ };
+
+ // 创建 LineSeries 并填充数据点
+ channel.Series = new LineSeries
+ {
+ Title = displayName,
+ Color = color,
+ StrokeThickness = 1.5
+ };
+
+ // 按时间排序
+ var sortedPoints = group.OrderBy(r => r.CreateTime);
+ foreach (var point in sortedPoints)
+ {
+ channel.Series.Points.Add(new DataPoint(point.CreateTime.ToOADate(), point.MonitorValue));
+ }
+
+ channel.PropertyChanged += OnChannelPropertyChanged;
+
+ Plot.Series.Add(channel.Series);
+ Channels.Add(channel);
+ }
+
+ Plot.InvalidatePlot(true);
+ StatusMessage = $"已加载 {Path.GetFileName(dlg.FileName)},共 {Channels.Count} 条曲线,{records.Count} 个数据点";
+ LoggerHelper.Info($"曲线回读:加载 {dlg.FileName},{Channels.Count} 条曲线,{records.Count} 个数据点");
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.Error($"加载 CSV 失败:{ex.Message}");
+ StatusMessage = $"加载失败:{ex.Message}";
+ }
+ }
+
+ private void OnResetView()
+ {
+ Plot.ResetAllAxes();
+ Plot.InvalidatePlot(false);
+ StatusMessage = "视图已复原";
+ }
+
+ private void OnClear()
+ {
+ ClearPlotAndChannels();
+ Plot.InvalidatePlot(true);
+ StatusMessage = "已清空,请重新加载 CSV 文件";
+ }
+ #endregion
+
+ #region CSV 解析
+ ///
+ /// 解析 RecordView 导出的 CSV 文件,返回监测记录列表。
+ ///
+ private static List ParseCsv(string path)
+ {
+ var results = new List();
+
+ using var sr = new StreamReader(path, Encoding.UTF8);
+
+ // 读取表头
+ string? headerLine = sr.ReadLine();
+ if (headerLine == null) return results;
+
+ var headers = ParseCsvLine(headerLine);
+ int monitorNameIdx = headers.FindIndex(h => h.Trim() == "MonitorName");
+ int monitorValueIdx = headers.FindIndex(h => h.Trim() == "MonitorValue");
+ int scopeIdx = headers.FindIndex(h => h.Trim() == "Scope");
+ int createTimeIdx = headers.FindIndex(h => h.Trim() == "CreateTime");
+
+ if (monitorNameIdx < 0 || monitorValueIdx < 0 || scopeIdx < 0 || createTimeIdx < 0)
+ return results;
+
+ int maxIdx = Math.Max(monitorNameIdx, Math.Max(monitorValueIdx, Math.Max(scopeIdx, createTimeIdx)));
+
+ string? line;
+ while ((line = sr.ReadLine()) != null)
+ {
+ if (string.IsNullOrWhiteSpace(line)) continue;
+ var fields = ParseCsvLine(line);
+ if (fields.Count <= maxIdx) continue;
+
+ if (!double.TryParse(fields[monitorValueIdx].Trim(), out double value)) continue;
+ if (!DateTime.TryParse(fields[createTimeIdx].Trim(), out DateTime createTime)) continue;
+
+ results.Add(new MonitorRecord
+ {
+ MonitorName = fields[monitorNameIdx].Trim(),
+ MonitorValue = value,
+ Scope = fields[scopeIdx].Trim(),
+ CreateTime = createTime
+ });
+ }
+
+ return results;
+ }
+
+ ///
+ /// 解析单行 CSV,正确处理引号转义。
+ ///
+ private static List ParseCsvLine(string line)
+ {
+ var fields = new List();
+ bool inQuotes = false;
+ var sb = new StringBuilder();
+
+ for (int i = 0; i < line.Length; i++)
+ {
+ char c = line[i];
+ if (inQuotes)
+ {
+ if (c == '"')
+ {
+ if (i + 1 < line.Length && line[i + 1] == '"')
+ {
+ sb.Append('"');
+ i++;
+ }
+ else
+ {
+ inQuotes = false;
+ }
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ else
+ {
+ if (c == '"')
+ {
+ inQuotes = true;
+ }
+ else if (c == ',')
+ {
+ fields.Add(sb.ToString());
+ sb.Clear();
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ }
+
+ fields.Add(sb.ToString());
+ return fields;
+ }
+ #endregion
+
+ #region OxyPlot 构建
+ ///
+ /// 构建空的 OxyPlot 模型(与 MonitorView 完全一致的配置)。
+ ///
+ private static PlotModel BuildEmptyPlot()
+ {
+ var pm = new PlotModel
+ {
+ Title = "曲线回读",
+ PlotAreaBorderColor = OxyColors.LightGray,
+ Background = OxyColors.White
+ };
+ pm.Axes.Add(new DateTimeAxis
+ {
+ Position = AxisPosition.Bottom,
+ Title = "时间",
+ StringFormat = "HH:mm:ss",
+ MajorGridlineStyle = LineStyle.Dot,
+ MinorGridlineStyle = LineStyle.None
+ });
+ pm.Axes.Add(new LinearAxis
+ {
+ Position = AxisPosition.Left,
+ Title = "值",
+ MajorGridlineStyle = LineStyle.Dot,
+ MinorGridlineStyle = LineStyle.None
+ });
+ pm.Legends.Add(new Legend
+ {
+ LegendPosition = LegendPosition.RightTop,
+ LegendBackground = OxyColor.FromAColor(200, OxyColors.White),
+ LegendBorder = OxyColors.LightGray
+ });
+ return pm;
+ }
#endregion
#region 辅助方法
+ private void OnChannelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(CurveRecallChannelVM.IsDisplayed)) return;
+ if (sender is not CurveRecallChannelVM channel) return;
+ if (channel.IsDisplayed)
+ {
+ if (channel.Series != null && !Plot.Series.Contains(channel.Series))
+ {
+ Plot.Series.Add(channel.Series);
+ }
+ }
+ else
+ {
+ if (channel.Series != null)
+ {
+ Plot.Series.Remove(channel.Series);
+ }
+ }
+ Plot.InvalidatePlot(true);
+ }
+ private void ClearPlotAndChannels()
+ {
+ foreach (var ch in Channels)
+ {
+ ch.PropertyChanged -= OnChannelPropertyChanged;
+ }
+ Plot.Series.Clear();
+ Channels.Clear();
+ _colorIndex = 0;
+ }
#endregion
- #region 生命周期
+ #region 生命周期
public override void OnNavigatedTo(NavigationContext navigationContext)
{
-
}
public void Dispose()
{
-
+ ClearPlotAndChannels();
+ }
+ #endregion
+ }
+
+ ///
+ /// CSV 监测记录(单行数据)。
+ ///
+ internal class MonitorRecord
+ {
+ public string MonitorName { get; set; } = string.Empty;
+ public double MonitorValue { get; set; }
+ public string Scope { get; set; } = string.Empty;
+ public DateTime CreateTime { get; set; }
+ }
+
+ ///
+ /// 曲线回读通道:MonitorName + Scope 唯一标识一条曲线。
+ ///
+ public class CurveRecallChannelVM : Prism.Mvvm.BindableBase
+ {
+ public string MonitorName { get; set; } = string.Empty;
+ public string Scope { get; set; } = string.Empty;
+ public string DisplayName { get; set; } = string.Empty;
+ public OxyColor Color { get; set; } = OxyColors.SteelBlue;
+
+ private bool _isDisplayed = true;
+ public bool IsDisplayed
+ {
+ get => _isDisplayed;
+ set => SetProperty(ref _isDisplayed, value);
}
- #endregion
-
+ public LineSeries? Series { get; set; }
}
}
diff --git a/MonitorModule/Views/CurveRecallView.xaml b/MonitorModule/Views/CurveRecallView.xaml
index 81f0933..b76de0f 100644
--- a/MonitorModule/Views/CurveRecallView.xaml
+++ b/MonitorModule/Views/CurveRecallView.xaml
@@ -1,14 +1,158 @@
-
-
-
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:prism="http://prismlibrary.com/"
+ xmlns:oxy="http://oxyplot.org/wpf"
+ xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
+ prism:ViewModelLocator.AutoWireViewModel="True"
+ mc:Ignorable="d"
+ d:DesignHeight="700"
+ d:DesignWidth="1200">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+