735 lines
25 KiB
C#
735 lines
25 KiB
C#
using Logger;
|
||
using OxyPlot;
|
||
using OxyPlot.Annotations;
|
||
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 CurveModule.ViewModels.Dialogs;
|
||
using UIShare.ViewModelBase;
|
||
using UIShare.GlobalVariable;
|
||
|
||
namespace CurveModule.ViewModels
|
||
{
|
||
/// <summary>
|
||
/// 曲线回读:加载 RecordView 导出的 CSV 文件,按 (MonitorName, Scope) 分组绘制曲线。
|
||
/// </summary>
|
||
public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||
{
|
||
#region 私有字段
|
||
private GlobalConfig _globalConfig;
|
||
#endregion
|
||
#region 属性
|
||
public bool KeepAlive => true;
|
||
|
||
public PlotModel Plot { get; }
|
||
|
||
/// <summary>已加载的曲线通道列表</summary>
|
||
public ObservableCollection<CurveRecallChannelVM> 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; }
|
||
public ICommand StatisticsCommand { get; }
|
||
public ICommand SaveCommand { get; }
|
||
public ICommand LoadCommand { get; }
|
||
public ICommand CaptureCommand { 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;
|
||
|
||
// 双光标截取
|
||
private LineAnnotation? _cursor1;
|
||
private LineAnnotation? _cursor2;
|
||
private LineAnnotation? _draggingCursor;
|
||
private List<MonitorRecord> _allRecords = new();
|
||
#endregion
|
||
|
||
/// <summary>截取按钮文本(随状态切换)</summary>
|
||
private string _captureButtonText = "开始截取图形";
|
||
public string CaptureButtonText
|
||
{
|
||
get => _captureButtonText;
|
||
set => SetProperty(ref _captureButtonText, value);
|
||
}
|
||
|
||
public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||
{
|
||
_globalConfig = containerProvider.Resolve<GlobalConfig>();
|
||
Plot = BuildEmptyPlot();
|
||
LoadCsvCommand = new DelegateCommand<string>(OnLoadCsv);
|
||
ResetViewCommand = new DelegateCommand(OnResetView);
|
||
ClearCommand = new DelegateCommand(OnClear);
|
||
StatisticsCommand = new DelegateCommand(OnStatistics);
|
||
SaveCommand = new DelegateCommand(OnSave);
|
||
LoadCommand = new DelegateCommand(Onload);
|
||
CaptureCommand = new DelegateCommand(OnToggleCapture);
|
||
}
|
||
|
||
|
||
#region 命令处理
|
||
private void OnSave()
|
||
{
|
||
ConfigService.SaveGlobalConfig(_globalConfig);
|
||
}
|
||
private void Onload()
|
||
{
|
||
if (File.Exists(_globalConfig.DefaultCSVFilePath))
|
||
{
|
||
OnLoadCsv(_globalConfig.DefaultCSVFilePath);
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 打开 CSV 文件对话框,解析数据并按 (MonitorName, Scope) 分组绘制曲线。
|
||
/// </summary>
|
||
|
||
private void OnLoadCsv(string filename)
|
||
{
|
||
try
|
||
{
|
||
// 1. 统一处理 filename
|
||
if (string.IsNullOrEmpty(filename))
|
||
{
|
||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||
{
|
||
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
|
||
Title = "选择要加载的监测数据 CSV 文件"
|
||
};
|
||
|
||
if (dlg.ShowDialog() != true)
|
||
return;
|
||
|
||
// 将弹窗选中的路径统一赋给 filename
|
||
filename = dlg.FileName;
|
||
_globalConfig.DefaultCSVFilePath = filename;
|
||
}
|
||
|
||
// 2. 统一解析数据
|
||
List<MonitorRecord> records = ParseCsv(filename);
|
||
_allRecords = records;
|
||
|
||
if (records.Count == 0)
|
||
{
|
||
StatusMessage = "CSV 文件为空或格式不正确";
|
||
return;
|
||
}
|
||
|
||
// 3. 按 (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);
|
||
|
||
// 4. 统一使用 filename 变量进行日志和状态更新
|
||
StatusMessage = $"已加载 {Path.GetFileName(filename)},共 {Channels.Count} 条曲线,{records.Count} 个数据点";
|
||
LoggerHelper.Info($"曲线回读:加载 {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()
|
||
{
|
||
RemoveCursors();
|
||
CaptureButtonText = "开始截取图形";
|
||
ClearPlotAndChannels();
|
||
_allRecords.Clear();
|
||
Plot.InvalidatePlot(true);
|
||
StatusMessage = "已清空,请重新加载 CSV 文件";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对每条曲线计算数学统计(最大、最小、平均、斜率、积分),弹窗显示。
|
||
/// </summary>
|
||
private void OnStatistics()
|
||
{
|
||
if (Channels.Count == 0)
|
||
{
|
||
ShowInfoMessageBox("请先加载 CSV 文件", () => { });
|
||
return;
|
||
}
|
||
|
||
var results = new ObservableCollection<CurveStatisticsResult>();
|
||
foreach (var channel in Channels)
|
||
{
|
||
if (channel.Series == null || channel.Series.Points.Count == 0) continue;
|
||
results.Add(ComputeStatistics(channel));
|
||
}
|
||
|
||
if (results.Count == 0)
|
||
{
|
||
ShowInfoMessageBox("没有可统计的曲线数据", () => { });
|
||
return;
|
||
}
|
||
|
||
var parameters = new DialogParameters();
|
||
parameters.Add("Results", results);
|
||
_dialogService.ShowDialog("CurveStatistics", parameters, _ => { });
|
||
StatusMessage = $"已计算 {results.Count} 条曲线的数学统计";
|
||
}
|
||
#endregion
|
||
|
||
#region CSV 解析
|
||
/// <summary>
|
||
/// 解析 RecordView 导出的 CSV 文件,返回监测记录列表。
|
||
/// </summary>
|
||
private static List<MonitorRecord> ParseCsv(string path)
|
||
{
|
||
var results = new List<MonitorRecord>();
|
||
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析单行 CSV,正确处理引号转义。
|
||
/// </summary>
|
||
private static List<string> ParseCsvLine(string line)
|
||
{
|
||
var fields = new List<string>();
|
||
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 构建
|
||
/// <summary>
|
||
/// 构建空的 OxyPlot 模型(与 MonitorView 完全一致的配置)。
|
||
/// </summary>
|
||
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 辅助方法
|
||
/// <summary>
|
||
/// 对单条曲线计算数学统计:最大值、最小值、平均值、线性回归斜率、梯形积分。
|
||
/// 斜率单位:值/秒;积分单位:值×秒。
|
||
/// </summary>
|
||
private static CurveStatisticsResult ComputeStatistics(CurveRecallChannelVM channel)
|
||
{
|
||
var points = channel.Series!.Points;
|
||
int n = points.Count;
|
||
|
||
if (n == 0)
|
||
return new CurveStatisticsResult
|
||
{
|
||
DisplayName = channel.DisplayName,
|
||
PointCount = 0
|
||
};
|
||
|
||
double max = double.MinValue;
|
||
double min = double.MaxValue;
|
||
double sumY = 0;
|
||
|
||
// 线性回归累计量
|
||
double sumX = 0, sumXY = 0, sumX2 = 0;
|
||
|
||
// 梯形积分累计量
|
||
double integral = 0;
|
||
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
double x = points[i].X; // OADate 天数
|
||
double y = points[i].Y;
|
||
|
||
if (y > max) max = y;
|
||
if (y < min) min = y;
|
||
sumY += y;
|
||
|
||
sumX += x;
|
||
sumXY += x * y;
|
||
sumX2 += x * x;
|
||
|
||
if (i > 0)
|
||
{
|
||
double dx = x - points[i - 1].X;
|
||
integral += dx * (y + points[i - 1].Y) / 2.0;
|
||
}
|
||
}
|
||
|
||
double average = sumY / n;
|
||
|
||
// 线性回归斜率(单位:值 / OADate 天)
|
||
double slopePerDay = 0;
|
||
double denominator = n * sumX2 - sumX * sumX;
|
||
if (Math.Abs(denominator) > 1e-30)
|
||
{
|
||
slopePerDay = (n * sumXY - sumX * sumY) / denominator;
|
||
}
|
||
// 转换为 值/秒
|
||
double slopePerSecond = slopePerDay / 86400.0;
|
||
|
||
// 积分从 OADate·值 转换为 秒·值
|
||
double integralSeconds = integral * 86400.0;
|
||
|
||
return new CurveStatisticsResult
|
||
{
|
||
DisplayName = channel.DisplayName,
|
||
PointCount = n,
|
||
Max = max,
|
||
Min = min,
|
||
Average = average,
|
||
Slope = slopePerSecond,
|
||
Integral = integralSeconds
|
||
};
|
||
}
|
||
|
||
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 双光标截取
|
||
/// <summary>
|
||
/// 切换截取模式:添加/移除双光标,结束时导出选中范围 CSV。
|
||
/// </summary>
|
||
private void OnToggleCapture()
|
||
{
|
||
if (_cursor1 == null)
|
||
{
|
||
CreateCursors();
|
||
CaptureButtonText = "结束截取图形";
|
||
}
|
||
else
|
||
{
|
||
double x1 = _cursor1.X;
|
||
double x2 = _cursor2!.X;
|
||
double minX = Math.Min(x1, x2);
|
||
double maxX = Math.Max(x1, x2);
|
||
|
||
RemoveCursors();
|
||
CaptureButtonText = "开始截取图形";
|
||
|
||
ExportCapturedCsv(minX, maxX);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在图表上添加两条可拖拽的竖直光标。
|
||
/// </summary>
|
||
private void CreateCursors()
|
||
{
|
||
if (Channels.Count == 0) return;
|
||
|
||
var dateAxis = Plot.Axes.OfType<DateTimeAxis>().FirstOrDefault();
|
||
if (dateAxis == null) return;
|
||
|
||
double xMin = dateAxis.ActualMinimum;
|
||
double xMax = dateAxis.ActualMaximum;
|
||
double range = xMax - xMin;
|
||
if (range <= 0) return;
|
||
|
||
_cursor1 = new LineAnnotation
|
||
{
|
||
Type = LineAnnotationType.Vertical,
|
||
X = xMin + range * 0.3,
|
||
Color = OxyColors.SteelBlue,
|
||
StrokeThickness = 2,
|
||
LineStyle = LineStyle.Dash,
|
||
Text = "▼",
|
||
TextColor = OxyColors.SteelBlue,
|
||
TextHorizontalAlignment = HorizontalAlignment.Center,
|
||
TextVerticalAlignment = VerticalAlignment.Top
|
||
};
|
||
|
||
_cursor2 = new LineAnnotation
|
||
{
|
||
Type = LineAnnotationType.Vertical,
|
||
X = xMin + range * 0.7,
|
||
Color = OxyColors.SteelBlue,
|
||
StrokeThickness = 2,
|
||
LineStyle = LineStyle.Dash,
|
||
Text = "▼",
|
||
TextColor = OxyColors.SteelBlue,
|
||
TextHorizontalAlignment = HorizontalAlignment.Center,
|
||
TextVerticalAlignment = VerticalAlignment.Top
|
||
};
|
||
|
||
_cursor1.MouseDown += OnCursorMouseDown;
|
||
_cursor1.MouseUp += OnCursorMouseUp;
|
||
_cursor2.MouseDown += OnCursorMouseDown;
|
||
_cursor2.MouseUp += OnCursorMouseUp;
|
||
Plot.MouseDown += OnPlotMouseMove;
|
||
|
||
Plot.Annotations.Add(_cursor1);
|
||
Plot.Annotations.Add(_cursor2);
|
||
Plot.InvalidatePlot(true);
|
||
StatusMessage = "已添加双光标,拖拽竖线选择范围,完成后点击「结束截取图形」";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移除双光标并清理事件。
|
||
/// </summary>
|
||
private void RemoveCursors()
|
||
{
|
||
if (_cursor1 != null)
|
||
{
|
||
_cursor1.MouseDown -= OnCursorMouseDown;
|
||
_cursor1.MouseUp -= OnCursorMouseUp;
|
||
Plot.Annotations.Remove(_cursor1);
|
||
_cursor1 = null;
|
||
}
|
||
if (_cursor2 != null)
|
||
{
|
||
_cursor2.MouseDown -= OnCursorMouseDown;
|
||
_cursor2.MouseUp -= OnCursorMouseUp;
|
||
Plot.Annotations.Remove(_cursor2);
|
||
_cursor2 = null;
|
||
}
|
||
Plot.MouseDown -= OnPlotMouseMove;
|
||
_draggingCursor = null;
|
||
Plot.InvalidatePlot(true);
|
||
}
|
||
|
||
private void OnCursorMouseDown(object sender, OxyMouseDownEventArgs e)
|
||
{
|
||
if (sender is LineAnnotation line)
|
||
_draggingCursor = line;
|
||
}
|
||
|
||
private void OnCursorMouseUp(object sender, OxyMouseEventArgs e)
|
||
{
|
||
_draggingCursor = null;
|
||
}
|
||
|
||
private void OnPlotMouseMove(object sender, OxyMouseDownEventArgs e)
|
||
{
|
||
if (_draggingCursor == null) return;
|
||
|
||
var position = ((OxyMouseEventArgs)e).Position;
|
||
var axis = Plot.Axes.OfType<DateTimeAxis>().FirstOrDefault();
|
||
if (axis == null) return;
|
||
|
||
double dataX = axis.InverseTransform(position.X, position.Y, null).X;
|
||
|
||
// 限制光标不超出数据范围
|
||
double xMin = axis.ActualMinimum;
|
||
double xMax = axis.ActualMaximum;
|
||
dataX = Math.Max(xMin, Math.Min(xMax, dataX));
|
||
|
||
_draggingCursor.X = dataX;
|
||
_draggingCursor.Text = $"▼ {DateTime.FromOADate(dataX):HH:mm:ss}";
|
||
|
||
Plot.InvalidatePlot(true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导出双光标选定时间范围内的原始记录到 CSV。
|
||
/// </summary>
|
||
private void ExportCapturedCsv(double minXOaDate, double maxXOaDate)
|
||
{
|
||
if (_allRecords.Count == 0)
|
||
{
|
||
ShowInfoMessageBox("没有加载数据,无法导出", () => { });
|
||
return;
|
||
}
|
||
|
||
var startTime = DateTime.FromOADate(minXOaDate);
|
||
var endTime = DateTime.FromOADate(maxXOaDate);
|
||
|
||
var filtered = _allRecords
|
||
.Where(r => r.CreateTime >= startTime && r.CreateTime <= endTime)
|
||
.OrderBy(r => r.CreateTime)
|
||
.ToList();
|
||
|
||
if (filtered.Count == 0)
|
||
{
|
||
ShowInfoMessageBox("选定范围内没有数据", () => { });
|
||
return;
|
||
}
|
||
|
||
var dlg = new Microsoft.Win32.SaveFileDialog
|
||
{
|
||
Filter = "CSV 文件 (*.csv)|*.csv",
|
||
Title = "导出截取范围数据",
|
||
FileName = $"captured_{DateTime.Now:yyyyMMdd_HHmmss}.csv"
|
||
};
|
||
if (dlg.ShowDialog() != true) return;
|
||
|
||
using var sw = new StreamWriter(dlg.FileName, false, Encoding.UTF8);
|
||
sw.WriteLine("MonitorName,MonitorValue,Scope,CreateTime");
|
||
foreach (var r in filtered)
|
||
{
|
||
sw.WriteLine($"{EscapeCsvField(r.MonitorName)},{r.MonitorValue},{EscapeCsvField(r.Scope)},{r.CreateTime:yyyy-MM-dd HH:mm:ss.fff}");
|
||
}
|
||
|
||
StatusMessage = $"已导出 {filtered.Count} 条记录到 {Path.GetFileName(dlg.FileName)}";
|
||
LoggerHelper.Info($"导出截取范围 CSV:{dlg.FileName},{filtered.Count} 条记录");
|
||
}
|
||
|
||
private static string EscapeCsvField(string field)
|
||
{
|
||
if (string.IsNullOrEmpty(field)) return "";
|
||
if (field.Contains(',') || field.Contains('"') || field.Contains('\n'))
|
||
return $"\"{field.Replace("\"", "\"\"")}\"";
|
||
return field;
|
||
}
|
||
#endregion
|
||
|
||
#region 生命周期
|
||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
||
{
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
RemoveCursors();
|
||
ClearPlotAndChannels();
|
||
}
|
||
#endregion
|
||
}
|
||
|
||
/// <summary>
|
||
/// CSV 监测记录(单行数据)。
|
||
/// </summary>
|
||
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; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 曲线回读通道:MonitorName + Scope 唯一标识一条曲线。
|
||
/// </summary>
|
||
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; }
|
||
}
|
||
}
|