添加项目文件。

This commit is contained in:
“hsc”
2026-07-29 13:12:27 +08:00
parent d6da766e2d
commit 8cf45d36b9
297 changed files with 35814 additions and 0 deletions

View File

@@ -0,0 +1,398 @@
using Common.Attributes;
using DeviceCommand.Base;
using Prism.Events;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Windows.Input;
using UIShare.GlobalVariable;
using UIShare.PubEvent;
using UIShare.UIViewModel;
using UIShare.ViewModelBase;
using ZLGUSBCANFD;
namespace MonitorModule.ViewModels.Dialogs
{
public class ValueLimitViewModel : DialogViewModelBase
{
#region
private string _Title = "报警设置";
public string Title
{
get { return _Title; }
set { SetProperty(ref _Title, value); }
}
private ValueLimitVM _SelectValueLimitVM;
public ValueLimitVM SelectValueLimitVM
{
get => _SelectValueLimitVM;
set => SetProperty(ref _SelectValueLimitVM, value);
}
private ObservableCollection<ValueLimitVM> _ValueLimitList;
public ObservableCollection<ValueLimitVM> ValueLimitList
{
get => _ValueLimitList;
set => SetProperty(ref _ValueLimitList, value);
}
//private ObservableCollection<string> _DeviceSingleList = new();
//public ObservableCollection<string> DeviceSingleList
//{
// get => _DeviceSingleList;
// set => SetProperty(ref _DeviceSingleList, value);
//}
//private string _SelectCurve;
//public string SelectCurve
//{
// get => _SelectCurve;
// set => SetProperty(ref _SelectCurve, value);
//}
#endregion
#region
public ICommand SaveCommand { get; set; }
public ICommand CloseCommand { get; set; }
//public ICommand SelectSignalCommand { get; set; }
public ICommand DeleteCommand { get; set; }
#endregion
private GlobalInfo _globalInfo { get; set; }
private SystemConfig _systemConfig { get; set; }
private IScopedProvider _scope { get; set; }
private DeviceManager? _deviceManager;
/// <summary>SignalName → CAN信号标识用于DBC加载/卸载时清理</summary>
private readonly Dictionary<string, (string Fingerprint, string MethodName)> _canSignalMap = new();
private SubscriptionToken? _dbcLoadedToken;
private SubscriptionToken? _dbcUnloadedToken;
public ValueLimitViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_globalInfo = containerProvider.Resolve<GlobalInfo>();
_eventAggregator = containerProvider.Resolve<IEventAggregator>();
SaveCommand = new DelegateCommand(OnSave);
CloseCommand = new DelegateCommand(OnClose);
//SelectSignalCommand = new DelegateCommand(SelectSignal);
DeleteCommand = new DelegateCommand(Deletel);
}
#region
private void Deletel()
{
if (SelectValueLimitVM == null)
{
return;
}
else
{
ValueLimitList.Remove(SelectValueLimitVM);
SelectValueLimitVM = null;
}
}
private void InitData()
{
ValueLimitList = new ObservableCollection<ValueLimitVM>();
//DeviceSingleList = new ObservableCollection<string>();
_canSignalMap.Clear();
// 1. 加载已保存的 ValueLimitList保留用户设置
if (_systemConfig?.ValueLimitList != null)
{
foreach (var item in _systemConfig.ValueLimitList)
ValueLimitList.Add(item);
}
// 2. 发现设备方法信号并加入 ValueLimitList / DeviceSingleList
foreach (var (displayName, fingerprint, methodName) in DiscoverDeviceSignals())
{
//DeviceSingleList.Add(displayName);
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
}
// 3. 发现 CAN 信号并加入 ValueLimitList同时收集有效信号名
var validCanSignalNames = new HashSet<string>();
foreach (var (displayName, fingerprint, methodName) in DiscoverCanSignals())
{
_canSignalMap[displayName] = (fingerprint, methodName);
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
validCanSignalNames.Add(displayName);
}
// 4. 清理已不存在于 DBC 中的 CAN 信号Fingerprint 以 "CAN" 开头且不在有效集合中)
var staleCanSignals = ValueLimitList
.Where(x => !string.IsNullOrEmpty(x.Fingerprint) &&
x.Fingerprint.StartsWith("CAN") &&
!validCanSignalNames.Contains(x.SignalName))
.ToList();
foreach (var item in staleCanSignals)
ValueLimitList.Remove(item);
}
/// <summary>
/// 发现当前作用域所有可监测的设备方法信号(与 MonitorViewModel 逻辑一致)。
/// 返回 (DisplayName, Fingerprint, MethodName) 列表。
/// </summary>
private IEnumerable<(string DisplayName, string Fingerprint, string MethodName)> DiscoverDeviceSignals()
{
if (_deviceManager?.DeviceMap == null) yield break;
var instanceToFp = BuildInstanceToFingerprint();
foreach (var kvp in _deviceManager.DeviceMap)
{
string deviceName = kvp.Key;
var device = kvp.Value;
if (!instanceToFp.TryGetValue(device, out string? fingerprint))
continue;
var methods = device.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false;
var parms = m.GetParameters();
return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
});
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<MonitorableAttribute>();
string displayName = !string.IsNullOrEmpty(attr?.Description)
? $"{deviceName}.{attr.Description}"
: $"{deviceName}.{method.Name}";
yield return (displayName, fingerprint, method.Name);
}
}
}
/// <summary>
/// 构建设备实例 → 硬件指纹的反向查找表。
/// </summary>
private Dictionary<object, string> BuildInstanceToFingerprint()
{
var map = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
if (_globalInfo?.HardwarePool == null) return map;
foreach (var poolEntry in _globalInfo.HardwarePool)
{
var lazy = poolEntry.Value;
if (lazy.IsValueCreated && lazy.Value != null)
map[lazy.Value] = poolEntry.Key;
}
return map;
}
/// <summary>
/// 发现当前已加载 DBC 中的 CAN 信号(以 ConfigurationList 为白名单)。
/// 返回 (DisplayName, Fingerprint, MethodName) 列表。
/// </summary>
private IEnumerable<(string DisplayName, string Fingerprint, string MethodName)> DiscoverCanSignals()
{
if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) yield break;
if (_systemConfig?.ConfigurationList == null) yield break;
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
foreach (var cfg in _systemConfig.ConfigurationList)
{
if (string.IsNullOrEmpty(cfg.SignalName)) continue;
if (cfg.Channel < 0 || cfg.Channel >= msgDb.Count) continue;
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, (uint)cfg.Channel);
string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName);
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
if (ExistsInDbc(methodName, msgDb[cfg.Channel]))
yield return (displayName, fingerprint, methodName);
}
}
/// <summary>
/// 判断指定的 MethodName 是否存在于该通道的 DBC 消息数据库中。
/// </summary>
private static bool ExistsInDbc(string methodName, List<_Msg_> messages)
{
if (string.IsNullOrEmpty(methodName)) return false;
int dotIndex = methodName.IndexOf('.');
if (dotIndex <= 0 || dotIndex >= methodName.Length - 1) return false;
if (!uint.TryParse(methodName.Substring(0, dotIndex), System.Globalization.NumberStyles.HexNumber, null, out uint msgId))
return false;
string signalName = methodName.Substring(dotIndex + 1);
return messages.Any(m => m.msg_id == msgId && m.signal_Name != null && m.signal_Name.Contains(signalName));
}
/// <summary>
/// 确保 ValueLimitList 中存在指定信号;不存在则添加默认值。
/// </summary>
private void EnsureValueLimit(string fingerprint, string displayName, string signalName, string methodName = "")
{
if (ValueLimitList.Any(x => x.SignalName == signalName)) return;
ValueLimitList.Add(new ValueLimitVM
{
DisplayName = displayName,
Fingerprint = fingerprint,
SignalName = signalName,
MethodName = methodName,
Upper = 9999,
Lower = -9999,
UpperExtreme = 9999,
LowerExtreme = -9999,
});
}
/// <summary>
/// 从 ValueLimitList 和 DeviceSingleList 中移除指定信号。
/// </summary>
private void RemoveValueLimit(string signalName)
{
var item = ValueLimitList.FirstOrDefault(x => x.SignalName == signalName);
if (item != null) ValueLimitList.Remove(item);
//if (DeviceSingleList.Contains(signalName))
// DeviceSingleList.Remove(signalName);
}
//private void SelectSignal()
//{
// if (ValueLimitList.Where(x => x.SignalName == SelectCurve).Count() > 0)
// {
// return;
// }
// ValueLimitList.Add(new ValueLimitVM
// {
// SignalName = SelectCurve,
// Upper = 9999,
// Lower = -9999,
// UpperExtreme = 9999,
// LowerExtreme = -9999,
// });
//}
private void OnClose()
{
RequestClose.Invoke();
}
private void OnSave()
{
_systemConfig.ValueLimitList = ValueLimitList;
ConfigService.Save(_systemConfig);
}
#endregion
public override void OnDialogOpened(IDialogParameters parameters)
{
// 1. 优先执行基类的打开逻辑(如果基类有需要初始化的通用事务)
base.OnDialogOpened(parameters);
// 2. 解析参数
if (parameters != null && parameters.ContainsKey("Scope")) // 修复Prism 中应使用 ContainsKey
{
_scope = parameters.GetValue<IScopedProvider>("Scope");
_systemConfig = _scope.Resolve<SystemConfig>();
_deviceManager = _scope.Resolve<DeviceManager>();
// 3. 订阅 DBC 加载/卸载事件
_dbcLoadedToken = _eventAggregator.GetEvent<DBCLoadedEvent>()
.Subscribe(OnDbcLoaded, ThreadOption.UIThread);
_dbcUnloadedToken = _eventAggregator.GetEvent<DBCUnloadedEvent>()
.Subscribe(OnDbcUnloaded, ThreadOption.UIThread);
// 4. 初始化数据
InitData();
}
}
public override void OnDialogClosed()
{
base.OnDialogClosed();
// 取消 DBC 事件订阅,避免内存泄漏
if (_dbcLoadedToken != null)
_eventAggregator.GetEvent<DBCLoadedEvent>().Unsubscribe(_dbcLoadedToken);
if (_dbcUnloadedToken != null)
_eventAggregator.GetEvent<DBCUnloadedEvent>().Unsubscribe(_dbcUnloadedToken);
}
/// <summary>
/// DBCLoadedEvent 回调:清理该通道中不存在于 DBC 的 CAN 信号限制项。
/// </summary>
private void OnDbcLoaded(DBCLoadedArgs args)
{
if (args.Scope != _systemConfig?.Title) return;
if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return;
int channel = (int)args.Channel;
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
if (channel < 0 || channel >= msgDb.Count) return;
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
// 1. 移除该通道中已失效的 CAN 信号限制项
var staleSignals = _canSignalMap
.Where(kvp => kvp.Value.Fingerprint == fingerprint &&
!ExistsInDbc(kvp.Value.MethodName, msgDb[channel]))
.Select(kvp => kvp.Key)
.ToList();
foreach (var signalName in staleSignals)
{
RemoveValueLimit(signalName);
_canSignalMap.Remove(signalName);
}
// 2. 根据 ConfigurationList 补充新加载的有效信号
if (_systemConfig?.ConfigurationList != null)
{
foreach (var cfg in _systemConfig.ConfigurationList.Where(c => c.Channel == channel))
{
if (string.IsNullOrEmpty(cfg.SignalName)) continue;
string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName);
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
if (ExistsInDbc(methodName, msgDb[channel]))
{
_canSignalMap[displayName] = (fingerprint, methodName);
//if (!DeviceSingleList.Contains(displayName))
// DeviceSingleList.Add(displayName);
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
}
}
}
}
/// <summary>
/// DBCUnloadedEvent 回调:删除该通道所有 CAN 信号的限制项。
/// </summary>
private void OnDbcUnloaded(DBCUnloadedArgs args)
{
if (args.Scope != _systemConfig?.Title) return;
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
var toRemove = _canSignalMap
.Where(kvp => kvp.Value.Fingerprint == fingerprint)
.Select(kvp => kvp.Key)
.ToList();
foreach (var signalName in toRemove)
{
RemoveValueLimit(signalName);
_canSignalMap.Remove(signalName);
}
}
}
}

View File

@@ -0,0 +1,852 @@
using Common.Attributes;
using Model.Entity;
using Model.Models;
using NLog;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using Service.Interface;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;
using UIShare.GlobalVariable;
using UIShare.PubEvent;
using UIShare.UIViewModel;
using UIShare.ViewModelBase;
using ZLGUSBCANFD;
namespace MonitorModule.ViewModels
{
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region
private IMonitorValueService _monitorValueService;
public bool KeepAlive => true;
public ScopedContext _scopedContext { get; set; } = null!;
public GlobalInfo _globalInfo { get; set; }
private string _testStatus = string.Empty;
public string TestStatus
{
get => _testStatus;
set => SetProperty(ref _testStatus, value);
}
public PlotModel Plot { get; }
/// <summary>已添加的监测通道列表</summary>
public ObservableCollection<MonitorChannelVM> Channels
{
get => _systemConfig?.Channels ?? new ObservableCollection<MonitorChannelVM>();
set
{
if (_systemConfig != null && _systemConfig.Channels != value)
{
_systemConfig.Channels = value;
RaisePropertyChanged();
}
}
}
/// <summary>可添加的设备方法列表(供用户选择)</summary>
public ObservableCollection<AvailableMethodItem> AvailableMethods { get; } = new();
private MonitorChannelVM? _selectedChannel;
public MonitorChannelVM? SelectedChannel
{
get => _selectedChannel;
set => SetProperty(ref _selectedChannel, value);
}
private AvailableMethodItem? _selectedAvailableMethod;
public AvailableMethodItem? SelectedAvailableMethod
{
get => _selectedAvailableMethod;
set => SetProperty(ref _selectedAvailableMethod, value);
}
private string _statusMessage = "图表已就绪,暂无监测项";
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
#endregion
#region
public ICommand AddChannelCommand { get; }
public ICommand DeleteChannelCommand { get; }
public ICommand ResetViewCommand { get; }
public ICommand RefreshDataCommand { get; }
public ICommand RefreshCommand { get; }
public ICommand SaveCommand { get; }
#endregion
#region
private bool IsInitiated = false;
private IScopedProvider? _scope;
private DeviceManager? _deviceManager;
private SystemConfig? _systemConfig;
private CancellationTokenSource? _monitorCTS = new();
// 颜色调色盘
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;
/// <summary>设备数据广播器(全局单例)</summary>
private HardwareDataBroadcaster? _broadcaster;
/// <summary>CAN 信号广播器(全局单例)</summary>
private CANSignalBroadcaster? _canSignalBroadcaster;
/// <summary>用于 OxyPlot X 轴相对秒数</summary>
private readonly Stopwatch _stopwatch = new();
/// <summary>OxyPlot 刷新定时器(仅负责滚动 X 轴 + InvalidatePlot</summary>
private readonly DispatcherTimer _plotRefreshTimer;
/// <summary>事件订阅令牌,用于 Dispose 时取消订阅</summary>
private SubscriptionToken? _subscriptionToken;
private SubscriptionToken? _dbcLoadedToken;
private SubscriptionToken? _dbcUnloadedToken;
// ==========================================
// 批量入库核心结构
// ==========================================
private readonly ConcurrentQueue<MonitorValueEntity> _insertQueue = new();
private Task? _dbFlushTask;
private const int BulkInsertThreshold = 50;
private int _isFlushing = 0;
#endregion
public MonitorViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve<GlobalInfo>();
_monitorValueService = container.Resolve<IMonitorValueService>();
Plot = BuildEmptyPlot();
AddChannelCommand = new DelegateCommand(OnAddChannel);
DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
ResetViewCommand = new DelegateCommand(OnResetView);
RefreshDataCommand = new DelegateCommand(OnRefreshData);
RefreshCommand = new DelegateCommand(OnExpand);
SaveCommand = new DelegateCommand(OnSave);
// OxyPlot 刷新定时器1000ms只负责视觉更新
_plotRefreshTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromMilliseconds(1000)
};
_plotRefreshTimer.Tick += OnPlotRefreshTick;
}
public void Dispose()
{
_monitorCTS?.Cancel();
_monitorCTS?.Dispose();
_monitorCTS = null;
_plotRefreshTimer.Stop();
_plotRefreshTimer.Tick -= OnPlotRefreshTick;
_stopwatch.Stop();
// 取消 EventAggregator 订阅
if (_subscriptionToken != null)
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Unsubscribe(_subscriptionToken);
if (_dbcLoadedToken != null)
_eventAggregator.GetEvent<DBCLoadedEvent>().Unsubscribe(_dbcLoadedToken);
if (_dbcUnloadedToken != null)
_eventAggregator.GetEvent<DBCUnloadedEvent>().Unsubscribe(_dbcUnloadedToken);
if (_dbFlushTask != null)
{
try { _dbFlushTask.Wait(TimeSpan.FromSeconds(1.5)); } catch { }
}
// 3. 释放容器作用域
_scope?.Dispose();
Channels.Clear();
AvailableMethods.Clear();
}
private void OnSave()
{
// 将当前 Channels 序列化为 MonitorChannelConfig 列表保存到 MonitorChannels
if (_systemConfig != null)
{
_systemConfig.MonitorChannels = new ObservableCollection<MonitorChannelConfig>(Channels
.Select(c => new MonitorChannelConfig
{
Fingerprint = c.Fingerprint,
MethodName = c.MethodName,
IsDisplayed = c.IsDisplayed
}));
}
ConfigService.Save(_systemConfig);
}
#region 广
/// <summary>
/// 由 HardwareDataBroadcaster 通过 EventAggregator 广播触发。
/// 已通过 ThreadOption.UIThread 确保在 UI 线程执行。
/// 通过 HardwareFingerprint + MethodName 匹配监测通道。
/// </summary>
private void OnHardwareDataReceived(HardwareReportArgs args)
{
// 仅处理属于当前 Scope 的事件
if (string.IsNullOrEmpty(TestStatus) || args.Scope != TestStatus) return;
// 以硬件指纹 + 方法名唯一匹配通道(与逻辑名解耦)
var channel = Channels.FirstOrDefault(c =>
c.Fingerprint == args.HardwareFingerprint &&
c.MethodName == args.MethodName &&
c.IsMonitored);
if (channel == null) return;
// 记录数据点线程安全Record 内部 ConcurrentQueue + Series 操作)
channel.Record(args.Time, args.Value);
// 入队数据库批量写入
// CreateTime 使用 args.Time采样触发时刻而不是 DateTime.Now设备响应到达时刻
// 这样即使设备响应有延迟,数据库里的时间戳仍然按设定采样间隔分布。
var entity = new MonitorValueEntity
{
MonitorName = channel.DisplayName,
MonitorValue = args.Value,
Scope = args.Scope,
CreateTime = args.Time,
IsDel = 0
};
_insertQueue.Enqueue(entity);
if (_insertQueue.Count >= BulkInsertThreshold)
{
_ = Task.Run(async () => await DoFlushWorkAsync().ConfigureAwait(false));
}
}
#endregion
#region OxyPlot
private void OnPlotRefreshTick(object? sender, EventArgs e)
{
if (!_stopwatch.IsRunning) return;
var now = DateTime.Now;
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null)
{
xAxis.Minimum = DateTimeAxis.ToDouble(now.AddSeconds(-20));
xAxis.Maximum = DateTimeAxis.ToDouble(now.AddSeconds(0.5));
}
Plot.InvalidatePlot(true);
}
#endregion
#region
private void StartDbFlushWorker(CancellationToken token)
{
_dbFlushTask = Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
await DoFlushWorkAsync().ConfigureAwait(false);
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DB Worker Error] {ex.Message}");
}
}
await DoFlushWorkAsync().ConfigureAwait(false);
}, token);
}
private async Task DoFlushWorkAsync()
{
if (_insertQueue.IsEmpty) return;
if (Interlocked.CompareExchange(ref _isFlushing, 1, 0) != 0) return;
try
{
var listToInsert = new List<MonitorValueEntity>();
while (_insertQueue.TryDequeue(out var entity)) listToInsert.Add(entity);
if (listToInsert.Count > 0)
{
var result = await _monitorValueService.InsertRangeAsync(listToInsert).ConfigureAwait(false);
}
}
finally { Interlocked.Exchange(ref _isFlushing, 0); }
}
#endregion
#region Navigation
public override void OnNavigatedTo(NavigationContext navigationContext)
{
base.OnNavigatedTo(navigationContext);
if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name"))
{
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
Plot.Title = $"监控 - {TestStatus}";
Plot.InvalidatePlot(false);
_scope = _globalInfo.ScopeDic[TestStatus];
_scopedContext = _scope.Resolve<ScopedContext>();
_deviceManager = _scope.Resolve<DeviceManager>();
_systemConfig = _scope.Resolve<SystemConfig>();
RaisePropertyChanged(nameof(Channels));
_broadcaster = _scope.Resolve<HardwareDataBroadcaster>();
_canSignalBroadcaster = _scope.Resolve<CANSignalBroadcaster>();
IsInitiated = true;
if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
// 1. 启动后台数据库消费
StartDbFlushWorker(_monitorCTS.Token);
// 2. 扫描可监测方法(供 UI 选择)
DiscoverAvailableMethods();
// 3. 从配置自动恢复已保存的监测通道
RestoreChannelsFromConfig();
// 4. 启动广播器Discover + Start
_broadcaster.Discover();
_broadcaster.Start();
_canSignalBroadcaster.Discover();
_canSignalBroadcaster.Start();
// 5. 订阅 HardwareDataReportedEventUI 线程回调)
_subscriptionToken = _eventAggregator.GetEvent<HardwareDataReportedEvent>()
.Subscribe(OnHardwareDataReceived, ThreadOption.UIThread);
// 5b. 订阅 DBC 加载/卸载事件,动态发现或清除 CAN 信号
_dbcLoadedToken = _eventAggregator.GetEvent<DBCLoadedEvent>()
.Subscribe(OnDbcLoaded, ThreadOption.UIThread);
_dbcUnloadedToken = _eventAggregator.GetEvent<DBCUnloadedEvent>()
.Subscribe(OnDbcUnloaded, ThreadOption.UIThread);
// 5c. 根据 SystemConfig.ConfigurationList 刷新已加载 DBC 中的 CAN 信号
RefreshConfiguredCanSignals();
// 6. 启动 OxyPlot 视觉刷新定时器
_stopwatch.Start();
_plotRefreshTimer.Start();
}
}
#endregion
#region PlotModel
private static PlotModel BuildEmptyPlot()
{
var pm = new PlotModel
{
Title = string.Empty,
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 UI
/// <summary>
/// 通过 GlobalInfo.HardwarePool 构建 设备实例引用→硬件指纹 的反向查找表。
/// </summary>
private Dictionary<object, string> BuildInstanceToFingerprint()
{
var map = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
foreach (var poolEntry in _globalInfo.HardwarePool)
{
var lazy = poolEntry.Value;
if (lazy.IsValueCreated && lazy.Value != null)
map[lazy.Value] = poolEntry.Key;
}
return map;
}
private void DiscoverAvailableMethods()
{
AvailableMethods.Clear();
if (_deviceManager?.DeviceMap == null || _deviceManager.DeviceMap.Count == 0)
{
StatusMessage = "当前工位无可用设备";
return;
}
var instanceToFp = BuildInstanceToFingerprint();
foreach (var kvp in _deviceManager.DeviceMap)
{
string deviceName = kvp.Key;
var device = kvp.Value;
var deviceType = device.GetType();
// 反查该实例的硬件指纹
if (!instanceToFp.TryGetValue(device, out string? fingerprint))
continue;
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false;
var parms = m.GetParameters();
return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
});
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<MonitorableAttribute>();
string displayName = !string.IsNullOrEmpty(attr?.Description)
? $"{deviceName}.{attr.Description}"
: $"{deviceName}.{method.Name}";
AvailableMethods.Add(new AvailableMethodItem
{
DeviceName = deviceName,
Fingerprint = fingerprint,
MethodName = method.Name,
DisplayName = displayName,
MethodInfo = method,
Device = device
});
}
}
StatusMessage = AvailableMethods.Count > 0
? $"发现 {AvailableMethods.Count} 个可监测项"
: "未发现可监测的设备方法";
}
/// <summary>
/// 从 SystemConfig.MonitorChannels 持久化列表自动还原监测通道。
/// </summary>
private void RestoreChannelsFromConfig()
{
if (_systemConfig?.MonitorChannels == null || _systemConfig.MonitorChannels.Count == 0) return;
int restored = 0;
foreach (var config in _systemConfig.MonitorChannels)
{
if (string.IsNullOrWhiteSpace(config.Fingerprint) ||
string.IsNullOrWhiteSpace(config.MethodName)) continue;
// 已在 Channels 中存在则跳过
if (Channels.Any(c => c.Fingerprint == config.Fingerprint && c.MethodName == config.MethodName))
continue;
// 在 AvailableMethods 中查找匹配项
var method = AvailableMethods.FirstOrDefault(m =>
m.Fingerprint == config.Fingerprint && m.MethodName == config.MethodName);
if (method == null) continue;
var color = _palette[_colorIndex % _palette.Length];
_colorIndex++;
var channel = new MonitorChannelVM
{
DeviceName = method.DeviceName,
Fingerprint = method.Fingerprint,
MethodName = method.MethodName,
DisplayName = method.DisplayName,
Color = color,
IsMonitored = true,
IsDisplayed = config.IsDisplayed
};
// 仅当 IsDisplayed=true 时才创建 LineSeries
if (channel.IsDisplayed)
{
channel.Series = new LineSeries
{
Title = channel.DisplayName,
Color = color,
StrokeThickness = 1.5
};
Plot.Series.Add(channel.Series);
}
channel.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed))
OnChannelDisplayChanged(channel);
};
Channels.Add(channel);
restored++;
}
if (restored > 0)
{
Plot.InvalidatePlot(true);
StatusMessage = $"已从配置自动恢复 {restored} 个监测通道";
}
}
/// <summary>
/// 根据 SystemConfig.ConfigurationList 刷新所有已加载 DBC 中的 CAN 信号。
/// 只添加 ConfigurationList 中配置且实际存在于 DBC 的信号;不存在则清理。
/// </summary>
private void RefreshConfiguredCanSignals()
{
if (_systemConfig?.ConfigurationList == null || _systemConfig.ConfigurationList.Count == 0) return;
if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return;
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
foreach (var channel in _systemConfig.ConfigurationList.Select(c => c.Channel).Distinct())
{
if (channel < 0 || channel >= msgDb.Count) continue;
RefreshCanSignalsForChannel((uint)channel, msgDb[channel]);
}
}
/// <summary>
/// 刷新单个通道的 CAN 信号:以 ConfigurationList 为白名单,对照 DBC 实际存在性。
/// - DBC 中存在 → 加入 AvailableMethods若 MonitorChannels 中也有,则恢复 Channel。
/// - DBC 中不存在 → 从 AvailableMethods 移除;若已在 Channels 中,则删除。
/// </summary>
private void RefreshCanSignalsForChannel(uint channel, List<_Msg_> messages)
{
if (_systemConfig?.ConfigurationList == null) return;
string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, channel);
var configs = _systemConfig.ConfigurationList.Where(c => c.Channel == (int)channel).ToList();
bool changed = false;
foreach (var cfg in configs)
{
if (string.IsNullOrEmpty(cfg.SignalName)) continue;
string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName);
bool existsInDbc = messages.Any(m =>
m.msg_id == cfg.MessageID &&
m.signal_Name != null &&
m.signal_Name.Contains(cfg.SignalName));
if (existsInDbc)
{
// 加入 AvailableMethods去重
if (!AvailableMethods.Any(m => m.Fingerprint == fingerprint && m.MethodName == methodName))
{
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
AvailableMethods.Add(new AvailableMethodItem
{
DeviceName = $"CAN{channel}",
Fingerprint = fingerprint,
MethodName = methodName,
DisplayName = displayName,
MethodInfo = null!,
Device = null!
});
changed = true;
}
// 如果 MonitorChannels 中有持久化记录,自动恢复 Channel
var monitorConfig = _systemConfig.MonitorChannels?.FirstOrDefault(m =>
m.Fingerprint == fingerprint && m.MethodName == methodName);
if (monitorConfig != null && !Channels.Any(c =>
c.Fingerprint == fingerprint && c.MethodName == methodName))
{
AddCanChannel(cfg, fingerprint, methodName, monitorConfig.IsDisplayed);
changed = true;
}
}
else
{
// DBC 中不存在:从 AvailableMethods 移除
var methodToRemove = AvailableMethods.FirstOrDefault(m =>
m.Fingerprint == fingerprint && m.MethodName == methodName);
if (methodToRemove != null)
{
AvailableMethods.Remove(methodToRemove);
changed = true;
}
// 如果该信号已经在 Channels或 MonitorChannels删除
var channelToRemove = Channels.FirstOrDefault(c =>
c.Fingerprint == fingerprint && c.MethodName == methodName);
if (channelToRemove != null)
{
if (channelToRemove.Series != null) Plot.Series.Remove(channelToRemove.Series);
Channels.Remove(channelToRemove);
changed = true;
}
}
}
if (changed)
{
Plot.InvalidatePlot(true);
StatusMessage = $"CAN{channel} 已刷新配置信号";
}
}
/// <summary>
/// DBCLoadedEvent 回调DBC 加载后刷新该通道的配置信号。
/// </summary>
private void OnDbcLoaded(DBCLoadedArgs args)
{
if (args.Scope != TestStatus) return;
if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return;
int channel = (int)args.Channel;
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
if (channel < 0 || channel >= msgDb.Count) return;
RefreshCanSignalsForChannel(args.Channel, msgDb[channel]);
}
/// <summary>
/// DBCUnloadedEvent 回调:移除对应通道的 CAN 信号AvailableMethods + Channels
/// </summary>
private void OnDbcUnloaded(DBCUnloadedArgs args)
{
if (args.Scope != TestStatus) return;
string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
// 从 AvailableMethods 中移除
var toRemoveMethods = AvailableMethods.Where(m => m.Fingerprint == fingerprint).ToList();
foreach (var m in toRemoveMethods)
AvailableMethods.Remove(m);
// 从 Channels 中移除(并清理 Plot
var toRemoveChannels = Channels.Where(c => c.Fingerprint == fingerprint).ToList();
foreach (var c in toRemoveChannels)
{
if (c.Series != null) Plot.Series.Remove(c.Series);
Channels.Remove(c);
}
if (toRemoveMethods.Count > 0 || toRemoveChannels.Count > 0)
{
Plot.InvalidatePlot(true);
StatusMessage = $"CAN{args.Channel} DBC 已卸载,移除 {toRemoveMethods.Count} 个信号";
}
}
/// <summary>
/// 将单个 CAN 信号添加为监测通道。
/// </summary>
private void AddCanChannel(CANSignalConfig cfg, string fingerprint, string methodName, bool isDisplayed)
{
var color = _palette[_colorIndex % _palette.Length];
_colorIndex++;
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
var channel = new MonitorChannelVM
{
DeviceName = $"CAN{cfg.Channel}",
Fingerprint = fingerprint,
MethodName = methodName,
DisplayName = displayName,
Color = color,
IsMonitored = true,
IsDisplayed = isDisplayed
};
if (channel.IsDisplayed)
{
channel.Series = new LineSeries
{
Title = channel.DisplayName,
Color = color,
StrokeThickness = 1.5
};
Plot.Series.Add(channel.Series);
}
channel.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed))
OnChannelDisplayChanged(channel);
};
Channels.Add(channel);
}
#endregion
#region
private void OnAddChannel()
{
var method = SelectedAvailableMethod;
if (method == null)
{
StatusMessage = "请先在列表中选择一个监测项";
return;
}
// 以指纹+方法名去重,防止同一物理设备重复添加
if (Channels.Any(c => c.Fingerprint == method.Fingerprint && c.MethodName == method.MethodName))
{
StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
return;
}
var color = _palette[_colorIndex % _palette.Length];
_colorIndex++;
var channel = new MonitorChannelVM
{
DeviceName = method.DeviceName,
Fingerprint = method.Fingerprint,
MethodName = method.MethodName,
DisplayName = method.DisplayName,
Color = color,
IsMonitored = true,
IsDisplayed = true
};
channel.Series = new LineSeries
{
Title = channel.DisplayName,
Color = color,
StrokeThickness = 1.5
};
Plot.Series.Add(channel.Series);
channel.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed))
OnChannelDisplayChanged(channel);
};
Channels.Add(channel);
SelectedChannel = channel;
Plot.InvalidatePlot(true);
StatusMessage = $"已添加监测项 [{channel.DisplayName}],当前共 {Channels.Count} 项";
}
private void OnDeleteChannel()
{
var target = SelectedChannel ?? Channels.LastOrDefault();
if (target == null)
{
StatusMessage = "无可删除的监测项";
return;
}
if (target.Series != null) Plot.Series.Remove(target.Series);
Channels.Remove(target);
SelectedChannel = Channels.LastOrDefault();
Plot.InvalidatePlot(true);
StatusMessage = $"已删除监测项 [{target.DisplayName}],剩余 {Channels.Count} 项";
}
private void OnResetView()
{
Plot.ResetAllAxes();
Plot.InvalidatePlot(false);
StatusMessage = "视图已按数据范围复原";
}
private void OnRefreshData()
{
Plot.InvalidatePlot(true);
StatusMessage = $"已刷新({DateTime.Now:HH:mm:ss}";
}
private void OnExpand()
{
if (string.IsNullOrEmpty(TestStatus)) return;
_globalInfo.CurrentScope = TestStatus;
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
}
#endregion
#region /
public void OnChannelDisplayChanged(MonitorChannelVM channel)
{
if (channel.IsDisplayed)
{
if (channel.Series == null)
{
channel.Series = new LineSeries
{
Title = channel.DisplayName,
Color = channel.Color,
StrokeThickness = 1.5
};
var recent = channel.DataPoints.ToArray();
var startIdx = Math.Max(0, recent.Length - 10000);
for (int i = startIdx; i < recent.Length; i++)
channel.Series.Points.Add(new DataPoint(recent[i].Time.ToOADate(), recent[i].DisplayValue));
Plot.Series.Add(channel.Series);
Plot.InvalidatePlot(true);
}
}
else
{
if (channel.Series != null)
{
Plot.Series.Remove(channel.Series);
channel.Series = null;
Plot.InvalidatePlot(true);
}
}
}
public void OnChannelMathChanged(MonitorChannelVM channel)
{
if (channel.Series == null) return;
var points = channel.DataPoints.ToArray();
channel.Series.Points.Clear();
var startIdx = Math.Max(0, points.Length - 10000);
for (int i = startIdx; i < points.Length; i++)
channel.Series.Points.Add(new DataPoint(points[i].Time.ToOADate(), points[i].DisplayValue));
Plot.InvalidatePlot(true);
}
#endregion
}
}

View File

@@ -0,0 +1,410 @@
using Common.Attributes;
using DeviceCommand.Base;
using Logger;
using Model.Entity;
using ORM;
using SqlSugar;
using System.Collections.ObjectModel;
using System.Data;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows.Input;
using UIShare.GlobalVariable;
using UIShare.PubEvent;
using UIShare.ViewModelBase;
namespace MonitorModule.ViewModels
{
/// <summary>
/// 记录界面:查询 MonitorValueEntity 表中的历史监测数据。
/// 功能:监测项筛选 / 台架筛选 / 日期筛选 / 分页 / 导出 CSV。
/// 使用 SqlSugarContext.DbContext 全局单例,与写入端共用同一数据库连接。
/// </summary>
public class RecordViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region
public bool KeepAlive => true;
public ScopedContext _scopedContext { get; set; }
public GlobalInfo _globalInfo { get; }
public string TestStatus
{
get => _testStatus;
set => SetProperty(ref _testStatus, value);
}
/// <summary>监测项名称下拉列表</summary>
public ObservableCollection<string> MonitorNames
{
get => _monitorNames;
set => SetProperty(ref _monitorNames, value);
}
/// <summary>选中的监测项null = 全部)</summary>
public string? SelectedMonitorName
{
get => _selectedMonitorName;
set => SetProperty(ref _selectedMonitorName, value);
}
/// <summary>台架(作用域)下拉列表</summary>
public ObservableCollection<string> ScopeNames
{
get => _scopeNames;
set => SetProperty(ref _scopeNames, value);
}
/// <summary>选中的台架null = 全部)</summary>
public string? SelectedScope
{
get => _selectedScope;
set => SetProperty(ref _selectedScope, value);
}
/// <summary>查询日期null = 全部日期)</summary>
public DateTime? SelectedStartDate
{
get => _selectedStartDate;
set => SetProperty(ref _selectedStartDate, value);
}
public DateTime? SelectedEndDate
{
get => _selectedEndDate;
set => SetProperty(ref _selectedEndDate, value);
}
public DataTable ResultTable
{
get => _resultTable;
set => SetProperty(ref _resultTable, value);
}
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
public int PageIndex
{
get => _pageIndex;
set => SetProperty(ref _pageIndex, value);
}
public int PageSize
{
get => _pageSize;
set
{
if (SetProperty(ref _pageSize, value <= 0 ? 50 : value))
{
RaisePropertyChanged(nameof(TotalPages));
PageIndex = 1;
Query();
}
}
}
public long TotalCount
{
get => _totalCount;
set
{
SetProperty(ref _totalCount, value);
RaisePropertyChanged(nameof(TotalPages));
}
}
public int TotalPages
{
get
{
if (PageSize <= 0) return 1;
var pages = (int)((TotalCount + PageSize - 1) / PageSize);
return pages <= 0 ? 1 : pages;
}
}
#endregion
#region
public ICommand LoadedCommand { get; }
public ICommand QueryCommand { get; }
public ICommand ClearFilterCommand { get; }
public ICommand FirstPageCommand { get; }
public ICommand PrevPageCommand { get; }
public ICommand NextPageCommand { get; }
public ICommand LastPageCommand { get; }
public ICommand ExportCsvCommand { get; }
public ICommand RefreshCommand { get; }
#endregion
#region
private IScopedProvider _scope;
private string _testStatus = string.Empty;
private ObservableCollection<string> _monitorNames = new();
private string? _selectedMonitorName;
private ObservableCollection<string> _scopeNames = new(new[] { "全部", "TestCell1", "TestCell2", "TestCell3", "TestCell4", "TestCell5", "TestCell6", "TestCell7", "TestCell8" });
private string? _selectedScope = "全部";
private DateTime? _selectedStartDate;
private DateTime? _selectedEndDate;
private DataTable _resultTable = new();
private string _statusMessage = "未连接";
private int _pageIndex = 1;
private int _pageSize = 50;
private long _totalCount;
private bool IsInitiated = false;
#endregion
public RecordViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve<GlobalInfo>();
LoadedCommand = new DelegateCommand(LoadFilterOptions);
QueryCommand = new DelegateCommand(() => { PageIndex = 1; Query(); });
ClearFilterCommand = new DelegateCommand(ClearFilter);
FirstPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex = 1; Query(); } });
PrevPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex--; Query(); } });
NextPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex++; Query(); } });
LastPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex = TotalPages; Query(); } });
ExportCsvCommand = new DelegateCommand(ExportCsv);
RefreshCommand = new DelegateCommand(OnExpand);
}
public void Dispose()
{
_scope?.Dispose();
}
#region
/// <summary>
/// 加载筛选条件:监测项通过反射发现(与 MonitorViewModel 一致),台架从数据库查询。
/// </summary>
private void LoadFilterOptions()
{
// 监测项:反射发现当前作用域所有带 [Monitorable] 的设备方法
DiscoverMonitorNames();
Query();
}
/// <summary>
/// 反射扫描当前作用域 DeviceManager 中所有带 [Monitorable] 特性的设备方法,
/// 生成与 MonitorViewModel 完全一致的 DisplayName 列表,填充到下拉框。
/// 这样即使数据库还没数据,用户也能看到所有可筛选的监测项。
/// </summary>
private void DiscoverMonitorNames()
{
MonitorNames = new ObservableCollection<string>();
if (_scope == null) return;
try
{
var deviceManager = _scope.Resolve<DeviceManager>();
if (deviceManager?.DeviceMap == null || deviceManager.DeviceMap.Count == 0)
{
StatusMessage = "当前工位无可用设备";
return;
}
// 构建 设备实例 → 硬件指纹 的反向查找表
var instanceToFp = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
foreach (var poolEntry in _globalInfo.HardwarePool)
{
var lazy = poolEntry.Value;
if (lazy.IsValueCreated && lazy.Value != null)
instanceToFp[lazy.Value] = poolEntry.Key;
}
var names = new List<string>();
foreach (var kvp in deviceManager.DeviceMap)
{
string deviceName = kvp.Key;
var device = kvp.Value;
var deviceType = device.GetType();
// 只列出在硬件指纹池中注册的设备
if (!instanceToFp.ContainsKey(device))
continue;
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false;
var parms = m.GetParameters();
return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
});
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<MonitorableAttribute>();
string displayName = !string.IsNullOrEmpty(attr?.Description)
? $"{deviceName}.{attr.Description}"
: $"{deviceName}.{method.Name}";
names.Add(displayName);
}
}
names.Sort();
MonitorNames = new ObservableCollection<string>(names);
}
catch (Exception ex)
{
LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope, $"反射发现监测项失败:{ex.Message}");
}
}
/// <summary>清除所有筛选条件</summary>
private void ClearFilter()
{
SelectedMonitorName = null;
SelectedScope = "全部";
SelectedStartDate = null;
SelectedEndDate = null;
PageIndex = 1;
Query();
}
#endregion
#region
/// <summary>
/// 按当前筛选条件分页查询 MonitorValueEntity。
/// </summary>
private void Query()
{
try
{
var db = SqlSugarContext.DbContext;
// 预计算日期范围,避免在表达式树中访问可空属性
DateTime? dateStart = SelectedStartDate?.Date;
DateTime? dateEnd = SelectedEndDate?.Date.AddDays(1);
var query = db.Queryable<MonitorValueEntity>()
.WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName)
.WhereIF(!string.IsNullOrEmpty(SelectedScope) && SelectedScope != "全部", x => x.Scope == SelectedScope)
.WhereIF(dateStart.HasValue, x => x.CreateTime >= dateStart && x.CreateTime < dateEnd!.Value)
.OrderBy(x => x.CreateTime, OrderByType.Desc);
TotalCount = query.Count();
// 修正越界
if (PageIndex > TotalPages) PageIndex = TotalPages;
if (PageIndex < 1) PageIndex = 1;
int skip = (PageIndex - 1) * PageSize;
ResultTable = query
.Skip(skip)
.Take(PageSize)
.ToDataTable();
StatusMessage = $"共 {TotalCount} 行 第 {PageIndex}/{TotalPages} 页";
}
catch (Exception ex)
{
LoggerHelper.ErrorWithNotify(TestStatus, $"查询失败:{ex.Message}");
StatusMessage = $"查询失败:{ex.Message}";
ResultTable = new DataTable();
TotalCount = 0;
}
}
#endregion
#region CSV
/// <summary>
/// 按当前筛选条件导出全部结果到 CSV不分页
/// </summary>
private void ExportCsv()
{
try
{
var db = SqlSugarContext.DbContext;
DateTime? dateStart = SelectedStartDate?.Date;
DateTime? dateEnd = SelectedEndDate?.Date.AddDays(1);
var query = db.Queryable<MonitorValueEntity>()
.WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName)
.WhereIF(!string.IsNullOrEmpty(SelectedScope) && SelectedScope != "全部", x => x.Scope == SelectedScope)
.WhereIF(dateStart.HasValue, x => x.CreateTime >= dateStart && x.CreateTime < dateEnd!.Value)
.OrderBy(x => x.CreateTime, OrderByType.Desc);
var dt = query.ToDataTable();
if (dt.Rows.Count == 0)
{
StatusMessage = "无可导出数据";
return;
}
var dlg = new Microsoft.Win32.SaveFileDialog
{
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
FileName = $"MonitorData_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
Title = "导出监测数据为 CSV"
};
if (dlg.ShowDialog() != true) return;
WriteCsv(dlg.FileName, dt);
StatusMessage = $"导出完成:{dlg.FileName}{dt.Rows.Count} 行)";
LoggerHelper.InfoWithNotify(TestStatus, $"工位 [{TestStatus}] 导出监测数据至 {dlg.FileName},共 {dt.Rows.Count} 行");
}
catch (Exception ex)
{
LoggerHelper.ErrorWithNotify(TestStatus, $"导出失败:{ex.Message}");
StatusMessage = $"导出失败:{ex.Message}";
}
}
private static void WriteCsv(string path, DataTable dt)
{
// 写 UTF-8 BOM 让 Excel 直接识别中文
using var sw = new StreamWriter(path, false, new UTF8Encoding(true));
// 表头
sw.WriteLine(string.Join(",", dt.Columns.Cast<DataColumn>().Select(c => Escape(c.ColumnName))));
// 行
foreach (DataRow row in dt.Rows)
{
sw.WriteLine(string.Join(",", row.ItemArray.Select(v => Escape(v?.ToString() ?? string.Empty))));
}
}
private static string Escape(string field)
{
if (field.Contains('"') || field.Contains(',') || field.Contains('\r') || field.Contains('\n'))
{
return "\"" + field.Replace("\"", "\"\"") + "\"";
}
return field;
}
#endregion
#region
private void OnExpand()
{
if (string.IsNullOrEmpty(TestStatus)) return;
_globalInfo.CurrentScope = TestStatus;
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
}
#endregion
#region
public override void OnNavigatedTo(NavigationContext navigationContext)
{
base.OnNavigatedTo(navigationContext);
if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name"))
{
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
_scope = _globalInfo.ScopeDic[TestStatus];
_scopedContext = _scope.Resolve<ScopedContext>();
IsInitiated = true;
}
LoadFilterOptions();
}
#endregion
}
}