using Logger; using OxyPlot; using OxyPlot.Axes; using OxyPlot.Legends; using OxyPlot.Series; using Prism.Commands; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Text; using System.Windows.Input; using UIShare.ViewModelBase; namespace CurveModule.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 生命周期 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); } public LineSeries? Series { get; set; } } }