459 lines
16 KiB
C#
459 lines
16 KiB
C#
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>();
|
||
LineTempCodes = new ObservableCollection<LineTempCode>();
|
||
|
||
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();
|
||
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;
|
||
/// <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; }
|
||
|
||
/// <summary>
|
||
/// 产线临时条码队列(内包扫码入队,外箱扫码确认后出队)
|
||
/// </summary>
|
||
public ObservableCollection<LineTempCode> 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<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}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从数据库刷新产线临时队列(LineTempCode 表)并同步到 UI。
|
||
/// </summary>
|
||
private async Task RefreshLineTempCodesAsync()
|
||
{
|
||
try
|
||
{
|
||
var list = await Task.Run(() =>
|
||
{
|
||
return _fsql.Select<LineTempCode>()
|
||
.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<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;
|
||
}
|
||
}
|