Files
FATrace/FATrace.WPLApp/ViewModels/DashboardViewModel.cs
2026-01-29 22:15:34 +08:00

405 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string>();
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<DashboardRefreshEvent>()
.Subscribe(OnDashboardRefreshEvent, ThreadOption.UIThread, true);
}
private void OnDashboardRefreshEvent(bool _)
{
RequestDashboardRefresh();
}
/// <summary>
/// 触发一次 Dashboard 刷新(合并 + 互斥):短时间多次触发只会串行执行,并在执行结束后最多再补跑一次。
/// </summary>
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();
}
}
}));
}
/// <summary>
/// 在 UI 线程串行执行刷新,且合并多次触发。
/// </summary>
private async Task DashboardRefreshLoopAsync()
{
// 只要存在刷新请求,就执行一次刷新;过程中再来请求,会在下一轮继续跑
while (Interlocked.Exchange(ref _dashboardRefreshRequested, 0) == 1)
{
try
{
await RefreshStatsAsync();
}
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;
/// <summary>
/// 最近一次外箱喷码数据(即下发给 PLC 的源字符串)
/// </summary>
public string LatestBoxSprayCode { get => _latestBoxSprayCode; set { _latestBoxSprayCode = value; RaisePropertyChanged(); } }
private bool _plcConnected;
public bool PlcConnected { get => _plcConnected; set { _plcConnected = value; RaisePropertyChanged(); } }
public ObservableCollection<string> LiveMessages { 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<RawProUse>()
.Where(a => a.WeightTime >= todayStart && a.WeightTime <= todayEnd)
.Sum(a => a.Weight));
double m = Convert.ToDouble(
_fsql.Select<RawProUse>()
.Where(a => a.WeightTime >= monthStart && a.WeightTime <= monthEnd)
.Sum(a => a.Weight));
double y = Convert.ToDouble(
_fsql.Select<RawProUse>()
.Where(a => a.WeightTime >= yearStart && a.WeightTime <= yearEnd)
.Sum(a => a.Weight));
double all = Convert.ToDouble(_fsql.Select<RawProUse>().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}");
}
}
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;
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<DashboardRefreshEvent>().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<string> _onLine;
private readonly StringWriter _buffer = new StringWriter();
public ConsoleInterceptWriter(TextWriter inner, Action<string> 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;
}
}