using FATrace.WPLApp.Core; using Prism.Commands; using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using System.Windows.Threading; using FreeSql; using FATrace.Model; using FATrace.WPLApp.Services; using System.IO; using System.Windows; using System.Text; using Prism.Events; using FATrace.WPLApp.Events; using System.Threading; namespace FATrace.WPLApp.ViewModels { public class DashboardViewModel : NavigationViewModel { private readonly IFreeSql _fsql; private readonly ILogService _log; private readonly DataServices _data; private int _dashboardRefreshRequested; private int _dashboardRefreshLoopRunning; private DispatcherTimer _logTimer; private bool _initialized; private TextWriter _originalConsoleOut; private ConsoleInterceptWriter _consoleInterceptor; private readonly IEventAggregator _ea; public DashboardViewModel(IFreeSql fsql, ILogService log, DataServices data, IEventAggregator ea) { _fsql = fsql; _log = log; _data = data; _ea = ea; LiveMessages = new ObservableCollection(); LineTempCodes = new ObservableCollection(); RefreshCommand = new DelegateCommand(async () => await RefreshStatsAsync()); ClearLogsCommand = new DelegateCommand(() => LiveMessages.Clear()); PlcConnected = _data.PlcConnected; _data.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(DataServices.PlcConnected)) { var val = _data.PlcConnected; if (Application.Current?.Dispatcher?.CheckAccess() == true) PlcConnected = val; else Application.Current?.Dispatcher?.BeginInvoke(new Action(() => PlcConnected = val)); } }; // 订阅产线信号事件,生成人类可读的运行消息 _data.LineSglModel.WeightScanCodeHandle += (s, e) => { var code = _data.WeightScanCode; if (Application.Current?.Dispatcher?.CheckAccess() == true) LatestWeightScanCode = code; else Application.Current?.Dispatcher?.BeginInvoke(new Action(() => LatestWeightScanCode = code)); AppendLiveMessage($"称重扫码触发: {code}"); }; _data.LineSglModel.BoxSprayCodeReqHandle += (s, e) => { var code = _data.BoxSprayCode; if (Application.Current?.Dispatcher?.CheckAccess() == true) LatestBoxSprayCode = code; else Application.Current?.Dispatcher?.BeginInvoke(new Action(() => LatestBoxSprayCode = code)); //AppendLiveMessage("外箱喷码请求: 已向PLC下发喷码数据"); AppendLiveMessage($"外箱喷码请求: 已向PLC下发喷码数据;{code}"); }; _data.LineSglModel.BoxScanCodeReqHandle += (s, e) => { var code = _data.BoxScanCode; if (Application.Current?.Dispatcher?.CheckAccess() == true) LatestBoxScanCode = code; else Application.Current?.Dispatcher?.BeginInvoke(new Action(() => LatestBoxScanCode = code)); AppendLiveMessage($"外箱扫码触发: {code}"); }; // 订阅外部刷新事件(来自 DataServices 的 BoxScanCode 完成后) try { EnsureDashboardRefreshSubscription(); } catch (Exception ex) { _log?.Warn($"DashboardRefreshEvent 订阅失败: {ex.Message}"); } } private void EnsureDashboardRefreshSubscription() { if (_dashEventToken != null) return; // 注意:Prism 默认 keepSubscriberReferenceAlive=false(弱引用)。 // 若使用 lambda,目标可能是 closure 对象,存在被 GC 导致订阅失效的风险。 // 这里使用具名方法 + keepSubscriberReferenceAlive=true,确保订阅稳定。 _dashEventToken = _ea.GetEvent() .Subscribe(OnDashboardRefreshEvent, ThreadOption.UIThread, true); } private void OnDashboardRefreshEvent(bool _) { RequestDashboardRefresh(); } /// /// 触发一次 Dashboard 刷新(合并 + 互斥):短时间多次触发只会串行执行,并在执行结束后最多再补跑一次。 /// private void RequestDashboardRefresh() { // 标记有刷新请求 Interlocked.Exchange(ref _dashboardRefreshRequested, 1); // 若刷新循环已在跑,则仅标记请求即可 if (Interlocked.Exchange(ref _dashboardRefreshLoopRunning, 1) == 1) { return; } var dispatcher = Application.Current?.Dispatcher; if (dispatcher == null) { // 极少数场景(无 UI Dispatcher),直接后台跑 _ = DashboardRefreshLoopAsync(); return; } dispatcher.BeginInvoke(new Action(async () => { try { await DashboardRefreshLoopAsync(); } finally { Interlocked.Exchange(ref _dashboardRefreshLoopRunning, 0); // 如果在执行过程中又来了刷新请求,则再次启动一次循环(确保不漏刷新) if (Interlocked.Exchange(ref _dashboardRefreshRequested, 0) == 1) { RequestDashboardRefresh(); } } })); } /// /// 在 UI 线程串行执行刷新,且合并多次触发。 /// private async Task DashboardRefreshLoopAsync() { // 只要存在刷新请求,就执行一次刷新;过程中再来请求,会在下一轮继续跑 while (Interlocked.Exchange(ref _dashboardRefreshRequested, 0) == 1) { try { await RefreshStatsAsync(); await RefreshLineTempCodesAsync(); } catch (Exception ex) { _log.Error($"DashboardRefreshEvent 刷新失败: {ex}"); } } } #region Properties private double _todayWeight; public double TodayWeight { get => _todayWeight; set { _todayWeight = value; RaisePropertyChanged(); } } private double _monthWeight; public double MonthWeight { get => _monthWeight; set { _monthWeight = value; RaisePropertyChanged(); } } private double _yearWeight; public double YearWeight { get => _yearWeight; set { _yearWeight = value; RaisePropertyChanged(); } } private double _totalWeight; public double TotalWeight { get => _totalWeight; set { _totalWeight = value; RaisePropertyChanged(); } } private string _latestWeightScanCode; public string LatestWeightScanCode { get => _latestWeightScanCode; set { _latestWeightScanCode = value; RaisePropertyChanged(); } } private string _latestBoxScanCode; public string LatestBoxScanCode { get => _latestBoxScanCode; set { _latestBoxScanCode = value; RaisePropertyChanged(); } } private string _latestBoxSprayCode; /// /// 最近一次外箱喷码数据(即下发给 PLC 的源字符串) /// public string LatestBoxSprayCode { get => _latestBoxSprayCode; set { _latestBoxSprayCode = value; RaisePropertyChanged(); } } private bool _plcConnected; public bool PlcConnected { get => _plcConnected; set { _plcConnected = value; RaisePropertyChanged(); } } public ObservableCollection LiveMessages { get; } /// /// 产线临时条码队列(内包扫码入队,外箱扫码确认后出队) /// public ObservableCollection LineTempCodes { get; } #endregion #region Commands public DelegateCommand RefreshCommand { get; } public DelegateCommand ClearLogsCommand { get; } #endregion private void AppendLiveMessage(string message) { if (string.IsNullOrWhiteSpace(message)) return; var line = $"{DateTime.Now:HH:mm:ss} {message}"; void add() => LiveMessages.Add(line); if (Application.Current?.Dispatcher?.CheckAccess() == true) add(); else Application.Current?.Dispatcher?.BeginInvoke(new Action(add)); // 限制最大条数,避免内存增长 const int max = 500; void trim() { if (LiveMessages.Count > max) { while (LiveMessages.Count > max) LiveMessages.RemoveAt(0); } } if (Application.Current?.Dispatcher?.CheckAccess() == true) trim(); else Application.Current?.Dispatcher?.BeginInvoke(new Action(trim)); } private async Task RefreshStatsAsync() { try { var todayStart = DateTime.Today; var todayEnd = todayStart.AddDays(1).AddTicks(-1); var monthStart = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); var monthEnd = monthStart.AddMonths(1).AddTicks(-1); var yearStart = new DateTime(DateTime.Today.Year, 1, 1); var yearEnd = yearStart.AddYears(1).AddTicks(-1); var ret = await Task.Run(() => { double t = Convert.ToDouble( _fsql.Select() .Where(a => a.WeightTime >= todayStart && a.WeightTime <= todayEnd) .Sum(a => a.Weight)); double m = Convert.ToDouble( _fsql.Select() .Where(a => a.WeightTime >= monthStart && a.WeightTime <= monthEnd) .Sum(a => a.Weight)); double y = Convert.ToDouble( _fsql.Select() .Where(a => a.WeightTime >= yearStart && a.WeightTime <= yearEnd) .Sum(a => a.Weight)); double all = Convert.ToDouble(_fsql.Select().Sum(a => a.Weight)); return (t, m, y, all); }); TodayWeight = ret.t; MonthWeight = ret.m; YearWeight = ret.y; TotalWeight = ret.all; } catch (Exception ex) { _log.Error($"Dashboard 统计刷新失败: {ex}"); } } /// /// 从数据库刷新产线临时队列(LineTempCode 表)并同步到 UI。 /// private async Task RefreshLineTempCodesAsync() { try { var list = await Task.Run(() => { return _fsql.Select() .OrderBy(a => a.Id) .Limit(200) .ToList(); }); void apply() { LineTempCodes.Clear(); foreach (var item in list) { LineTempCodes.Add(item); } } var dispatcher = Application.Current?.Dispatcher; if (dispatcher == null) { apply(); return; } if (dispatcher.CheckAccess()) { apply(); } else { await dispatcher.InvokeAsync(apply).Task.ConfigureAwait(false); } } catch (Exception ex) { _log.Error($"刷新 LineTempCode 队列失败: {ex}"); } } public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext) { try { EnsureDashboardRefreshSubscription(); } catch (Exception ex) { _log?.Warn($"DashboardRefreshEvent OnNavigatedTo 订阅失败: {ex.Message}"); } if (!_initialized) { _initialized = true; //StartLogTimer(); await RefreshStatsAsync(); // 初始化展示最近一次扫描值 LatestWeightScanCode = _data.WeightScanCode; LatestBoxScanCode = _data.BoxScanCode; LatestBoxSprayCode = _data.BoxSprayCode; await RefreshLineTempCodesAsync(); TryHookConsole(); } } public override void OnNavigatedFrom(Prism.Regions.NavigationContext navigationContext) { if (_logTimer != null) { try { _logTimer.Stop(); } catch { } _logTimer = null; } UnhookConsole(); try { if (_dashEventToken != null) { _ea.GetEvent().Unsubscribe(_dashEventToken); _dashEventToken = null; } } catch (Exception ex) { _log?.Warn($"DashboardRefreshEvent 退订失败: {ex.Message}"); } } private void TryHookConsole() { try { if (_consoleInterceptor != null) return; _originalConsoleOut = Console.Out; _consoleInterceptor = new ConsoleInterceptWriter(_originalConsoleOut, AppendLiveMessage); Console.SetOut(_consoleInterceptor); } catch { } } private void UnhookConsole() { try { if (_consoleInterceptor != null && _originalConsoleOut != null) { Console.SetOut(_originalConsoleOut); } } catch { } finally { _consoleInterceptor = null; _originalConsoleOut = null; } } private class ConsoleInterceptWriter : TextWriter { private readonly TextWriter _inner; private readonly Action _onLine; private readonly StringWriter _buffer = new StringWriter(); public ConsoleInterceptWriter(TextWriter inner, Action onLine) { _inner = inner; _onLine = onLine; } public override Encoding Encoding => _inner.Encoding; public override void Write(char value) { _inner.Write(value); if (value == '\n') { var line = _buffer.ToString(); _buffer.GetStringBuilder().Clear(); if (!string.IsNullOrWhiteSpace(line)) _onLine(line.TrimEnd('\r')); } else { _buffer.Write(value); } } public override void Write(string value) { _inner.Write(value); if (value == null) return; foreach (var ch in value) { Write(ch); } } public override void WriteLine(string value) { _inner.WriteLine(value); if (!string.IsNullOrWhiteSpace(value)) _onLine(value); } } private SubscriptionToken _dashEventToken; } }