Compare commits
9 Commits
14d818083d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e71f68428 | |||
| 388edd1ada | |||
| 6057a71315 | |||
| 12de1b61e0 | |||
| 95700bfb8c | |||
| 013729003a | |||
| add1cd95be | |||
| 2a13c0ad34 | |||
| 92d477972f |
@@ -1,71 +1,381 @@
|
|||||||
using Logger;
|
using Logger;
|
||||||
using MahApps.Metro.Controls;
|
|
||||||
using Model.Entity;
|
|
||||||
using Model.Models;
|
|
||||||
using NLog.Targets;
|
|
||||||
using OxyPlot;
|
using OxyPlot;
|
||||||
using Service.Interface;
|
using OxyPlot.Axes;
|
||||||
|
using OxyPlot.Legends;
|
||||||
|
using OxyPlot.Series;
|
||||||
|
using Prism.Commands;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Data;
|
using System.IO;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Threading;
|
|
||||||
|
|
||||||
using UIShare.GlobalVariable;
|
|
||||||
using UIShare.UIViewModel;
|
|
||||||
using UIShare.ViewModelBase;
|
using UIShare.ViewModelBase;
|
||||||
|
|
||||||
namespace CurveModule.ViewModels
|
namespace CurveModule.ViewModels
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 曲线回读:加载 RecordView 导出的 CSV 文件,按 (MonitorName, Scope) 分组绘制曲线。
|
||||||
|
/// </summary>
|
||||||
public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||||
{
|
{
|
||||||
#region 属性
|
#region 属性
|
||||||
public bool KeepAlive => true;
|
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
|
#endregion
|
||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
|
public ICommand LoadCsvCommand { get; }
|
||||||
|
public ICommand ResetViewCommand { get; }
|
||||||
|
public ICommand ClearCommand { get; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 私有字段
|
#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
|
#endregion
|
||||||
|
|
||||||
public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||||
{
|
{
|
||||||
|
Plot = BuildEmptyPlot();
|
||||||
|
LoadCsvCommand = new DelegateCommand(OnLoadCsv);
|
||||||
|
ResetViewCommand = new DelegateCommand(OnResetView);
|
||||||
|
ClearCommand = new DelegateCommand(OnClear);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 命令处理
|
#region 命令处理
|
||||||
|
/// <summary>
|
||||||
|
/// 打开 CSV 文件对话框,解析数据并按 (MonitorName, Scope) 分组绘制曲线。
|
||||||
|
/// </summary>
|
||||||
|
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 解析
|
||||||
|
/// <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
|
#endregion
|
||||||
|
|
||||||
#region 辅助方法
|
#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
|
#endregion
|
||||||
#region 生命周期
|
|
||||||
|
|
||||||
|
#region 生命周期
|
||||||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
public override void OnNavigatedTo(NavigationContext navigationContext)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
public LineSeries? Series { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,158 @@
|
|||||||
<UserControl x:Class="CurveModule.Views.CurveRecallView"
|
<UserControl x:Class="CurveModule.Views.CurveRecallView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
mc:Ignorable="d"
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
xmlns:oxy="http://oxyplot.org/wpf"
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||||
d:DesignHeight="1080" d:DesignWidth="1920">
|
mc:Ignorable="d"
|
||||||
<Grid>
|
d:DesignHeight="700"
|
||||||
|
d:DesignWidth="1200">
|
||||||
</Grid>
|
<UserControl.Resources>
|
||||||
|
<converters:LessThanConverter x:Key="LessThanConverter"/>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Border Background="#F5F7FA">
|
||||||
|
<Grid x:Name="RootGrid" Margin="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Row0:标题 -->
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="曲线回读"
|
||||||
|
FontSize="20" FontWeight="Bold"
|
||||||
|
Margin="4,0,0,8"/>
|
||||||
|
|
||||||
|
<!-- Row1:工具栏 -->
|
||||||
|
<Border Grid.Row="1"
|
||||||
|
Background="White"
|
||||||
|
BorderBrush="#DDD" BorderThickness="1"
|
||||||
|
CornerRadius="4" Padding="8" Margin="0,0,0,6">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="📂 加载 CSV"
|
||||||
|
Command="{Binding LoadCsvCommand}"
|
||||||
|
Padding="12,4"/>
|
||||||
|
<Button Content="↺ 复原视图"
|
||||||
|
Command="{Binding ResetViewCommand}"
|
||||||
|
Padding="12,4" Margin="6,0,0,0"
|
||||||
|
ToolTip="按数据范围重置坐标轴缩放/平移"/>
|
||||||
|
<Button Content="✕ 清空"
|
||||||
|
Command="{Binding ClearCommand}"
|
||||||
|
Padding="12,4" Margin="6,0,0,0"
|
||||||
|
ToolTip="清空已加载的曲线数据"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row2:主体 -->
|
||||||
|
<Grid Grid.Row="2">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition>
|
||||||
|
<ColumnDefinition.Style>
|
||||||
|
<Style TargetType="ColumnDefinition">
|
||||||
|
<Setter Property="Width" Value="260"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ActualWidth, ElementName=RootGrid, Converter={StaticResource LessThanConverter}, ConverterParameter=600}" Value="True">
|
||||||
|
<Setter Property="Width" Value="0"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ColumnDefinition.Style>
|
||||||
|
</ColumnDefinition>
|
||||||
|
<ColumnDefinition>
|
||||||
|
<ColumnDefinition.Style>
|
||||||
|
<Style TargetType="ColumnDefinition">
|
||||||
|
<Setter Property="Width" Value="6"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ActualWidth, ElementName=RootGrid, Converter={StaticResource LessThanConverter}, ConverterParameter=600}" Value="True">
|
||||||
|
<Setter Property="Width" Value="0"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ColumnDefinition.Style>
|
||||||
|
</ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 左侧:通道列表 -->
|
||||||
|
<Border Grid.Column="0"
|
||||||
|
Background="White"
|
||||||
|
BorderBrush="#DDD" BorderThickness="1"
|
||||||
|
CornerRadius="4">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Row="0" Background="#ECEFF4" Padding="8,4">
|
||||||
|
<TextBlock Text="曲线通道(勾选显示/隐藏)" FontWeight="Bold"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ListBox Grid.Row="1"
|
||||||
|
ItemsSource="{Binding Channels}"
|
||||||
|
SelectedItem="{Binding SelectedChannel}"
|
||||||
|
BorderThickness="0">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Margin="2">
|
||||||
|
<DockPanel>
|
||||||
|
<CheckBox IsChecked="{Binding IsDisplayed}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip="勾选=在图表上显示,取消=隐藏"/>
|
||||||
|
<Border Width="10" Height="10"
|
||||||
|
CornerRadius="2"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="4,0,6,0">
|
||||||
|
<Border.Background>
|
||||||
|
<SolidColorBrush Color="SteelBlue"/>
|
||||||
|
</Border.Background>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="{Binding DisplayName}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
</DockPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
Text="{Binding StatusMessage}"
|
||||||
|
FontSize="11" Foreground="#666"
|
||||||
|
Margin="4,2" TextWrapping="Wrap"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<GridSplitter Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="Transparent"/>
|
||||||
|
|
||||||
|
<!-- 右侧:OxyPlot 图表 -->
|
||||||
|
<Border Grid.Column="2"
|
||||||
|
Background="White"
|
||||||
|
BorderBrush="#DDD" BorderThickness="1"
|
||||||
|
CornerRadius="4">
|
||||||
|
<oxy:PlotView Model="{Binding Plot}"
|
||||||
|
Background="Transparent"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Row3:状态栏 -->
|
||||||
|
<Border Grid.Row="3"
|
||||||
|
Background="#ECEFF4"
|
||||||
|
Padding="8,4" Margin="0,6,0,0"
|
||||||
|
CornerRadius="2">
|
||||||
|
<TextBlock Text="{Binding StatusMessage}"
|
||||||
|
Foreground="#444"
|
||||||
|
FontSize="12"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ namespace ExportModule.ViewModels
|
|||||||
get => _totalCount;
|
get => _totalCount;
|
||||||
set => SetProperty(ref _totalCount, value);
|
set => SetProperty(ref _totalCount, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TestReportModel? _selectedTestReport;
|
||||||
|
public TestReportModel? SelectedTestReport
|
||||||
|
{
|
||||||
|
get => _selectedTestReport;
|
||||||
|
set => SetProperty(ref _selectedTestReport, value);
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
@@ -144,24 +151,21 @@ namespace ExportModule.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 导出 Excel(根据当前筛选条件导出完整实体数据)
|
/// 导出 Excel:根据用户在表格中选中的 TestReportModel.TestRoundId 查询并导出完整步骤数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task OnExport()
|
private async Task OnExport()
|
||||||
{
|
{
|
||||||
DateTime? queryEnd = EndTime;
|
if (SelectedTestReport == null)
|
||||||
if (queryEnd.HasValue)
|
|
||||||
{
|
{
|
||||||
queryEnd = queryEnd.Value.Date.AddDays(1).AddSeconds(-1);
|
ShowErrorMessageBox("请先在列表中选择一条测试记录。", () => { });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string? scope = SelectedScope == "全部" ? null : SelectedScope;
|
|
||||||
|
|
||||||
// 查询全量实体数据
|
|
||||||
StatusMessage = "正在查询导出数据...";
|
StatusMessage = "正在查询导出数据...";
|
||||||
var entityResult = await _testReportService.GetEntitiesByFilter(StartTime, queryEnd, scope);
|
var entityResult = await _testReportService.GetByTestRoundIdAsync(SelectedTestReport.TestRoundId);
|
||||||
if (!entityResult.IsSuccess || entityResult.Data == null || entityResult.Data.Count == 0)
|
if (!entityResult.IsSuccess || entityResult.Data == null || entityResult.Data.Count == 0)
|
||||||
{
|
{
|
||||||
ShowErrorMessageBox("没有数据可导出,请先查询或调整筛选条件。", () => { });
|
ShowErrorMessageBox("未找到该测试记录的详细步骤数据。", () => { });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,15 +173,15 @@ namespace ExportModule.ViewModels
|
|||||||
{
|
{
|
||||||
Filter = "Excel 工作簿 (*.xlsx)|*.xlsx|所有文件 (*.*)|*.*",
|
Filter = "Excel 工作簿 (*.xlsx)|*.xlsx|所有文件 (*.*)|*.*",
|
||||||
DefaultExt = ".xlsx",
|
DefaultExt = ".xlsx",
|
||||||
FileName = $"测试报告_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"
|
FileName = $"测试报告_{SelectedTestReport.Scope}_{SelectedTestReport.StartTime:yyyyMMdd_HHmmss}.xlsx"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dialog.ShowDialog() != true) return;
|
if (dialog.ShowDialog() != true) return;
|
||||||
|
|
||||||
var entities = entityResult.Data;
|
var entities = entityResult.Data;
|
||||||
StatusMessage = $"正在导出 {entities.Count} 条记录...";
|
StatusMessage = $"正在导出 {entities.Count} 条步骤记录...";
|
||||||
await Task.Run(() => ExportToExcel(dialog.FileName, entities));
|
await Task.Run(() => ExportToExcel(dialog.FileName, entities));
|
||||||
ShowInfoMessageBox($"导出完成,共 {entities.Count} 条记录,已保存至:{dialog.FileName}", () => { });
|
ShowInfoMessageBox($"导出完成,共 {entities.Count} 条步骤记录,已保存至:{dialog.FileName}", () => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -135,6 +135,7 @@
|
|||||||
Background="White"
|
Background="White"
|
||||||
AlternatingRowBackground="#F5F7FA"
|
AlternatingRowBackground="#F5F7FA"
|
||||||
SelectionMode="Single"
|
SelectionMode="Single"
|
||||||
|
SelectedItem="{Binding SelectedTestReport}"
|
||||||
Margin="8">
|
Margin="8">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="TestRoundId"
|
<DataGridTextColumn Header="TestRoundId"
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ namespace MonitorModule
|
|||||||
{
|
{
|
||||||
containerRegistry.RegisterForNavigation<MonitorView>("MonitorView");
|
containerRegistry.RegisterForNavigation<MonitorView>("MonitorView");
|
||||||
containerRegistry.RegisterForNavigation<RecordView>("RecordView");
|
containerRegistry.RegisterForNavigation<RecordView>("RecordView");
|
||||||
containerRegistry.RegisterForNavigation<CurveRecallView>("CurveRecallView");
|
|
||||||
containerRegistry.RegisterDialog<ValueLimitView>("ValueLimitView");
|
containerRegistry.RegisterDialog<ValueLimitView>("ValueLimitView");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
using Logger;
|
|
||||||
using MahApps.Metro.Controls;
|
|
||||||
using Model.Entity;
|
|
||||||
using Model.Models;
|
|
||||||
using NLog.Targets;
|
|
||||||
using OxyPlot;
|
|
||||||
using Service.Interface;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Data;
|
|
||||||
using System.Diagnostics;
|
|
||||||
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
|
|
||||||
{
|
|
||||||
public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
|
||||||
{
|
|
||||||
#region 属性
|
|
||||||
public bool KeepAlive => true;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 命令
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 私有字段
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#region 命令处理
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 辅助方法
|
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
#region 生命周期
|
|
||||||
|
|
||||||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<UserControl x:Class="MonitorModule.Views.CurveRecallView"
|
|
||||||
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"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
|
||||||
d:DesignHeight="1080" d:DesignWidth="1920">
|
|
||||||
<Grid>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace MonitorModule.Views
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// CurveRecallView.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
public partial class CurveRecallView : UserControl
|
|
||||||
{
|
|
||||||
public CurveRecallView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -93,7 +93,20 @@ namespace TestingModule.ViewModels.Dialogs
|
|||||||
var index = Program.Parameters
|
var index = Program.Parameters
|
||||||
.Select((x, i) => new { x, i })
|
.Select((x, i) => new { x, i })
|
||||||
.FirstOrDefault(p => p.x.ID == _ScopedContext.SelectedParameter.ID)?.i;
|
.FirstOrDefault(p => p.x.ID == _ScopedContext.SelectedParameter.ID)?.i;
|
||||||
|
if (!_ScopedContext.SelectedParameter.IsEditable)
|
||||||
|
{
|
||||||
|
//公共变量只允许更改ParameterCategory为Input,Temp
|
||||||
|
if (Program.Parameters[index.Value].Value != Parameter.Value
|
||||||
|
|| Program.Parameters[index.Value].UpperLimit != Parameter.UpperLimit
|
||||||
|
|| Program.Parameters[index.Value].LowerLimit != Parameter.LowerLimit
|
||||||
|
|| Program.Parameters[index.Value].Name != Parameter.Name
|
||||||
|
|| Parameter.Category == ParameterCategory.Output
|
||||||
|
|| Program.Parameters[index.Value].Type != Parameter.Type)
|
||||||
|
{
|
||||||
|
RequestClose.Invoke(ButtonResult.Yes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (index.HasValue)
|
if (index.HasValue)
|
||||||
{
|
{
|
||||||
Program.Parameters[index.Value] = Parameter;
|
Program.Parameters[index.Value] = Parameter;
|
||||||
|
|||||||
@@ -190,13 +190,13 @@ namespace TestingModule.ViewModels
|
|||||||
|
|
||||||
private void ParameterDelete()
|
private void ParameterDelete()
|
||||||
{
|
{
|
||||||
if (!_globalInfo.IsAdmin) return;
|
if (!_globalInfo.IsAdmin || !SelectedParameter.IsEditable) return;
|
||||||
Program.Parameters.Remove(SelectedParameter);
|
Program.Parameters.Remove(SelectedParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ParameterEdit()
|
private void ParameterEdit()
|
||||||
{
|
{
|
||||||
if (!_globalInfo.IsAdmin||!SelectedParameter.IsEditable) return;
|
if (!_globalInfo.IsAdmin) return;
|
||||||
var param = new DialogParameters
|
var param = new DialogParameters
|
||||||
{
|
{
|
||||||
{ "Mode",SelectedParameter==null?"ADD":"Edit" },
|
{ "Mode",SelectedParameter==null?"ADD":"Edit" },
|
||||||
@@ -207,10 +207,11 @@ namespace TestingModule.ViewModels
|
|||||||
if (r.Result == ButtonResult.OK)
|
if (r.Result == ButtonResult.OK)
|
||||||
{
|
{
|
||||||
_eventAggregator.GetEvent<ParamsChangedEvent>().Publish();
|
_eventAggregator.GetEvent<ParamsChangedEvent>().Publish();
|
||||||
|
ShowInfoMessageBox("保存成功", () => { });
|
||||||
}
|
}
|
||||||
else
|
else if (r.Result == ButtonResult.Yes)
|
||||||
{
|
{
|
||||||
|
ShowErrorMessageBox("公共变量只能设置变量类型(Input,Temp)", ()=>{ });
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace UIShare.ViewModelBase
|
|||||||
var dialogParams = new DialogParameters();
|
var dialogParams = new DialogParameters();
|
||||||
dialogParams.Add("Title", "错误");
|
dialogParams.Add("Title", "错误");
|
||||||
dialogParams.Add("Message", Message);
|
dialogParams.Add("Message", Message);
|
||||||
dialogParams.Add("Icon", "info");
|
dialogParams.Add("Icon", "error");
|
||||||
dialogParams.Add("ShowOk", true);
|
dialogParams.Add("ShowOk", true);
|
||||||
_dialogService.ShowDialog("MessageBox", dialogParams, result =>
|
_dialogService.ShowDialog("MessageBox", dialogParams, result =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace ZLGUSBCANFD
|
|||||||
// 每个通道分配独立的 DBC 引擎句柄与锁,彻底解决台架间DBC冲突与锁竞争
|
// 每个通道分配独立的 DBC 引擎句柄与锁,彻底解决台架间DBC冲突与锁竞争
|
||||||
private readonly uint[] _dbcHandles;
|
private readonly uint[] _dbcHandles;
|
||||||
private readonly bool[] _isDbcLoadedArray;
|
private readonly bool[] _isDbcLoadedArray;
|
||||||
private readonly object[] _channelLocks; // 通道级细粒度锁
|
private readonly object[] _channelLocks; // 通道级细粒度锁
|
||||||
|
|
||||||
// 异步高性能接收线程控制
|
// 异步高性能接收线程控制
|
||||||
private volatile bool _isRunning = false;
|
private volatile bool _isRunning = false;
|
||||||
@@ -619,7 +619,7 @@ namespace ZLGUSBCANFD
|
|||||||
/// <param name="物理值">要写入的实际物理数值</param>
|
/// <param name="物理值">要写入的实际物理数值</param>
|
||||||
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(单位毫秒)</param>
|
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(单位毫秒)</param>
|
||||||
/// <returns>操作是否成功</returns>
|
/// <returns>操作是否成功</returns>
|
||||||
public virtual bool 设置报文(uint 通道号, string 帧IDStr, string 信号名称, double 物理值, int 循环发送间隔毫秒 = 0, bool 直接发送=false)
|
public virtual bool 设置报文(uint 通道号, string 帧IDStr, string 信号名称, double 物理值, int 循环发送间隔毫秒 = 0, bool 直接发送 = false)
|
||||||
{
|
{
|
||||||
if (!TryParseFrameId(帧IDStr, out uint 帧ID))
|
if (!TryParseFrameId(帧IDStr, out uint 帧ID))
|
||||||
{
|
{
|
||||||
@@ -877,7 +877,7 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
||||||
Marshal.FreeHGlobal(ptrDbcMsg);
|
Marshal.FreeHGlobal(ptrDbcMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user