From e90dc805fc68128e294a0bc75775e1314935517e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chsc=E2=80=9D?= <“huangsucan@kaiyili-lab.com”> Date: Thu, 30 Jul 2026 16:33:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=9E=E8=AF=BB=E5=9B=BE=E5=83=8F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ACP/App.xaml.cs | 1 + ACP/ViewModels/ShellViewModel.cs | 16 + CurveModule/CurveModule.cs | 2 + .../ViewModels/CurveRecallViewModel.cs | 375 +++++++++++++++++- .../Dialogs/CurveStatisticsViewModel.cs | 95 +++++ CurveModule/Views/CurveRecallView.xaml | 20 + CurveModule/Views/CurveRecallView.xaml.cs | 27 ++ .../Views/Dialogs/CurveStatisticsView.xaml | 89 +++++ .../Views/Dialogs/CurveStatisticsView.xaml.cs | 12 + UIShare/GlobalVariable/ConfigService.cs | 31 ++ UIShare/GlobalVariable/GlobalConfig.cs | 17 + 11 files changed, 674 insertions(+), 11 deletions(-) create mode 100644 CurveModule/ViewModels/Dialogs/CurveStatisticsViewModel.cs create mode 100644 CurveModule/Views/Dialogs/CurveStatisticsView.xaml create mode 100644 CurveModule/Views/Dialogs/CurveStatisticsView.xaml.cs create mode 100644 UIShare/GlobalVariable/GlobalConfig.cs diff --git a/ACP/App.xaml.cs b/ACP/App.xaml.cs index 7541911..97e8863 100644 --- a/ACP/App.xaml.cs +++ b/ACP/App.xaml.cs @@ -106,6 +106,7 @@ namespace ACP containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); //注册AutoMapper var config = new MapperConfiguration( cfg => cfg.AddProfile(), diff --git a/ACP/ViewModels/ShellViewModel.cs b/ACP/ViewModels/ShellViewModel.cs index 8474c62..c9c5ef3 100644 --- a/ACP/ViewModels/ShellViewModel.cs +++ b/ACP/ViewModels/ShellViewModel.cs @@ -37,6 +37,7 @@ namespace ACP.ViewModels private readonly INotificationManager _notificationManager; private readonly IModuleManager _moduleManager; private readonly GlobalInfo _globalInfo; + private readonly GlobalConfig _globalConfig; // 💡 新增:UI 刷新定时器,用于高频让前端重新拉取当前工位的 SW 累计时间 private readonly DispatcherTimer _uiRefreshTimer; @@ -165,6 +166,21 @@ namespace ACP.ViewModels _regionManager = containerProvider.Resolve(); _notificationManager = containerProvider.Resolve(); _moduleManager = containerProvider.Resolve(); + _globalConfig = containerProvider.Resolve(); + if (ConfigService.IsExit("GlobalConfig")) + { + string filePath = System.IO.Path.Combine(_globalConfig.SystemPath, "GlobalConfig.json"); + if (System.IO.File.Exists(filePath)) + { + string json = System.IO.File.ReadAllText(filePath); + Newtonsoft.Json.JsonConvert.PopulateObject(json, _globalConfig); + } + } + else + { + _globalConfig = new GlobalConfig(); + ConfigService.SaveGlobalConfig(_globalConfig); + } LeftDrawerOpenCommand = new DelegateCommand(LeftDrawerOpen); ShowDialogManagerViewCommand = new DelegateCommand(ShowDialogManagerView); MinimizeCommand = new DelegateCommand(MinimizeWindow); diff --git a/CurveModule/CurveModule.cs b/CurveModule/CurveModule.cs index 13243e7..5a905c4 100644 --- a/CurveModule/CurveModule.cs +++ b/CurveModule/CurveModule.cs @@ -1,5 +1,6 @@ using CurveModule.Views; +using CurveModule.Views.Dialogs; using System.Reflection; namespace CurveModule @@ -14,6 +15,7 @@ namespace CurveModule public void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigation("CurveRecallView"); + containerRegistry.RegisterForNavigation("CurveStatistics"); } } diff --git a/CurveModule/ViewModels/CurveRecallViewModel.cs b/CurveModule/ViewModels/CurveRecallViewModel.cs index 888f4d6..b7b9a81 100644 --- a/CurveModule/ViewModels/CurveRecallViewModel.cs +++ b/CurveModule/ViewModels/CurveRecallViewModel.cs @@ -1,5 +1,6 @@ using Logger; using OxyPlot; +using OxyPlot.Annotations; using OxyPlot.Axes; using OxyPlot.Legends; using OxyPlot.Series; @@ -11,7 +12,9 @@ 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 { @@ -20,6 +23,9 @@ namespace CurveModule.ViewModels /// public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable { + #region 私有字段 + private GlobalConfig _globalConfig; + #endregion #region 属性 public bool KeepAlive => true; @@ -47,6 +53,11 @@ namespace CurveModule.ViewModels 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 私有字段 @@ -58,39 +69,84 @@ namespace CurveModule.ViewModels OxyColors.Teal, OxyColors.Crimson, OxyColors.OliveDrab }; private int _colorIndex; + + // 双光标截取 + private LineAnnotation? _cursor1; + private LineAnnotation? _cursor2; + private LineAnnotation? _draggingCursor; + private List _allRecords = new(); #endregion + /// 截取按钮文本(随状态切换) + private string _captureButtonText = "开始截取图形"; + public string CaptureButtonText + { + get => _captureButtonText; + set => SetProperty(ref _captureButtonText, value); + } + public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider) { + _globalConfig = containerProvider.Resolve(); Plot = BuildEmptyPlot(); - LoadCsvCommand = new DelegateCommand(OnLoadCsv); + LoadCsvCommand = new DelegateCommand(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); + } + } /// /// 打开 CSV 文件对话框,解析数据并按 (MonitorName, Scope) 分组绘制曲线。 /// - private void OnLoadCsv() + + private void OnLoadCsv(string filename) { try { - var dlg = new Microsoft.Win32.OpenFileDialog + // 1. 统一处理 filename + if (string.IsNullOrEmpty(filename)) { - Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*", - Title = "选择要加载的监测数据 CSV 文件" - }; - if (dlg.ShowDialog() != true) return; + 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 records = ParseCsv(filename); + _allRecords = records; - var records = ParseCsv(dlg.FileName); if (records.Count == 0) { StatusMessage = "CSV 文件为空或格式不正确"; return; } - // 按 (MonitorName, Scope) 分组 + // 3. 按 (MonitorName, Scope) 分组 var groups = records.GroupBy(r => (r.MonitorName, r.Scope)) .OrderBy(g => g.Key.MonitorName) .ThenBy(g => g.Key.Scope); @@ -138,8 +194,10 @@ namespace CurveModule.ViewModels } Plot.InvalidatePlot(true); - StatusMessage = $"已加载 {Path.GetFileName(dlg.FileName)},共 {Channels.Count} 条曲线,{records.Count} 个数据点"; - LoggerHelper.Info($"曲线回读:加载 {dlg.FileName},{Channels.Count} 条曲线,{records.Count} 个数据点"); + + // 4. 统一使用 filename 变量进行日志和状态更新 + StatusMessage = $"已加载 {Path.GetFileName(filename)},共 {Channels.Count} 条曲线,{records.Count} 个数据点"; + LoggerHelper.Info($"曲线回读:加载 {filename},{Channels.Count} 条曲线,{records.Count} 个数据点"); } catch (Exception ex) { @@ -157,10 +215,43 @@ namespace CurveModule.ViewModels private void OnClear() { + RemoveCursors(); + CaptureButtonText = "开始截取图形"; ClearPlotAndChannels(); + _allRecords.Clear(); Plot.InvalidatePlot(true); StatusMessage = "已清空,请重新加载 CSV 文件"; } + + /// + /// 对每条曲线计算数学统计(最大、最小、平均、斜率、积分),弹窗显示。 + /// + private void OnStatistics() + { + if (Channels.Count == 0) + { + ShowInfoMessageBox("请先加载 CSV 文件", () => { }); + return; + } + + var results = new ObservableCollection(); + 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 解析 @@ -302,6 +393,79 @@ namespace CurveModule.ViewModels #endregion #region 辅助方法 + /// + /// 对单条曲线计算数学统计:最大值、最小值、平均值、线性回归斜率、梯形积分。 + /// 斜率单位:值/秒;积分单位:值×秒。 + /// + 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; @@ -334,6 +498,194 @@ namespace CurveModule.ViewModels Channels.Clear(); _colorIndex = 0; } + + #endregion + + #region 双光标截取 + /// + /// 切换截取模式:添加/移除双光标,结束时导出选中范围 CSV。 + /// + 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); + } + } + + /// + /// 在图表上添加两条可拖拽的竖直光标。 + /// + private void CreateCursors() + { + if (Channels.Count == 0) return; + + var dateAxis = Plot.Axes.OfType().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 = "已添加双光标,拖拽竖线选择范围,完成后点击「结束截取图形」"; + } + + /// + /// 移除双光标并清理事件。 + /// + 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().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); + } + + /// + /// 导出双光标选定时间范围内的原始记录到 CSV。 + /// + 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 生命周期 @@ -343,6 +695,7 @@ namespace CurveModule.ViewModels public void Dispose() { + RemoveCursors(); ClearPlotAndChannels(); } #endregion diff --git a/CurveModule/ViewModels/Dialogs/CurveStatisticsViewModel.cs b/CurveModule/ViewModels/Dialogs/CurveStatisticsViewModel.cs new file mode 100644 index 0000000..74d61d5 --- /dev/null +++ b/CurveModule/ViewModels/Dialogs/CurveStatisticsViewModel.cs @@ -0,0 +1,95 @@ +using Prism.Commands; +using System.Collections.ObjectModel; +using System.Windows.Input; +using UIShare.PubEvent; +using UIShare.ViewModelBase; + +namespace CurveModule.ViewModels.Dialogs +{ + /// + /// 曲线统计结果弹窗 ViewModel。 + /// + public class CurveStatisticsViewModel : DialogViewModelBase + { + #region 属性 + + private string _title = "曲线数学统计"; + public string Title + { + get => _title; + set => SetProperty(ref _title, value); + } + + public ObservableCollection Results { get; } = new(); + + #endregion + + #region 命令 + public ICommand CloseCommand { get; } + #endregion + + public CurveStatisticsViewModel(IContainerProvider containerProvider) : base(containerProvider) + { + CloseCommand = new DelegateCommand(Close); + } + + private void Close() + { + RequestClose.Invoke(ButtonResult.OK); + } + + #region Prism Dialog 规范 + + public override void OnDialogClosed() + { + _eventAggregator.GetEvent().Publish(false); + } + + public override void OnDialogOpened(IDialogParameters parameters) + { + _eventAggregator.GetEvent().Publish(true); + if (parameters.ContainsKey("Results")) + { + var list = parameters.GetValue>("Results"); + Results.Clear(); + foreach (var item in list) + Results.Add(item); + } + } + + #endregion + } + + /// + /// 单条曲线的数学统计结果。 + /// + public class CurveStatisticsResult + { + /// 曲线名称 + public string DisplayName { get; set; } = string.Empty; + + /// 数据点数 + public int PointCount { get; set; } + + /// 最大值 + public double Max { get; set; } + + /// 最小值 + public double Min { get; set; } + + /// 平均值 + public double Average { get; set; } + + /// 线性回归斜率(单位/秒) + public double Slope { get; set; } + + /// 梯形积分值(单位·秒) + public double Integral { get; set; } + + public string MaxText => Max.ToString("G6"); + public string MinText => Min.ToString("G6"); + public string AverageText => Average.ToString("G6"); + public string SlopeText => Slope.ToString("G6"); + public string IntegralText => Integral.ToString("G6"); + } +} diff --git a/CurveModule/Views/CurveRecallView.xaml b/CurveModule/Views/CurveRecallView.xaml index 18ecbb9..b1217dd 100644 --- a/CurveModule/Views/CurveRecallView.xaml +++ b/CurveModule/Views/CurveRecallView.xaml @@ -4,12 +4,18 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:prism="http://prismlibrary.com/" + xmlns:i="http://schemas.microsoft.com/xaml/behaviors" 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"> + + + + + @@ -46,6 +52,19 @@ Command="{Binding ClearCommand}" Padding="12,4" Margin="6,0,0,0" ToolTip="清空已加载的曲线数据"/> +