初版
This commit is contained in:
315
FATrace.WPLApp/ViewModels/DashboardViewModel.cs
Normal file
315
FATrace.WPLApp/ViewModels/DashboardViewModel.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
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;
|
||||
|
||||
namespace FATrace.WPLApp.ViewModels
|
||||
{
|
||||
public class DashboardViewModel : NavigationViewModel
|
||||
{
|
||||
private readonly IFreeSql _fsql;
|
||||
private readonly ILogService _log;
|
||||
private readonly DataServices _data;
|
||||
|
||||
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) =>
|
||||
{
|
||||
AppendLiveMessage("外箱喷码请求: 已向PLC下发喷码数据");
|
||||
};
|
||||
_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
|
||||
{
|
||||
_dashEventToken = _ea.GetEvent<DashboardRefreshEvent>().Subscribe(_ =>
|
||||
{
|
||||
Application.Current?.Dispatcher?.BeginInvoke(new Action(async () => await RefreshStatsAsync()));
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
#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 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 StartLogTimer()
|
||||
{
|
||||
if (_logTimer != null) return;
|
||||
_logTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(500)
|
||||
};
|
||||
_logTimer.Tick += (s, e) =>
|
||||
{
|
||||
// 从 DataServices 的队列中取出消息并展示
|
||||
int drain = 0;
|
||||
while (_data.TryDequeueMessage(out var msg))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(msg))
|
||||
{
|
||||
AppendLiveMessage(msg);
|
||||
drain++;
|
||||
if (drain >= 50) break; // 防止一次处理过多
|
||||
}
|
||||
}
|
||||
};
|
||||
_logTimer.Start();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
_initialized = true;
|
||||
//StartLogTimer();
|
||||
await RefreshStatsAsync();
|
||||
// 初始化展示最近一次扫描值
|
||||
LatestWeightScanCode = _data.WeightScanCode;
|
||||
LatestBoxScanCode = _data.BoxScanCode;
|
||||
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); } catch { }
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,13 @@ namespace FATrace.WPLApp.ViewModels
|
||||
{
|
||||
private readonly IFreeSql _fsql;
|
||||
private readonly ILogService _log;
|
||||
private readonly CsvServices csvServices;
|
||||
|
||||
public RawProUseViewModel(IFreeSql fsql, ILogService log)
|
||||
public RawProUseViewModel(IFreeSql fsql, ILogService log, CsvServices csvServices)
|
||||
{
|
||||
_fsql = fsql;
|
||||
_log = log;
|
||||
|
||||
this.csvServices = csvServices;
|
||||
Items = new ObservableCollection<RawProUseDto>();
|
||||
// 集合变化时动态刷新导出命令可用性
|
||||
Items.CollectionChanged += (s, e) =>
|
||||
@@ -51,6 +52,8 @@ namespace FATrace.WPLApp.ViewModels
|
||||
.ObservesProperty(() => IsBusy)
|
||||
// Items 是集合引用,通常不会替换引用,这里通过 CollectionChanged 手动触发
|
||||
;
|
||||
ExportCSVCommand = new DelegateCommand(ExportToCsv, () => !IsBusy && Items.Count > 0)
|
||||
.ObservesProperty(() => IsBusy);
|
||||
ClearCommand = new DelegateCommand(ClearFilters, () => !IsBusy)
|
||||
.ObservesProperty(() => IsBusy);
|
||||
|
||||
@@ -212,6 +215,7 @@ namespace FATrace.WPLApp.ViewModels
|
||||
#region 命令
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ExportCommand { get; }
|
||||
public DelegateCommand ExportCSVCommand { get; }
|
||||
public DelegateCommand ClearCommand { get; }
|
||||
public DelegateCommand FirstPageCommand { get; }
|
||||
public DelegateCommand PrevPageCommand { get; }
|
||||
@@ -356,6 +360,63 @@ namespace FATrace.WPLApp.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 CsvServices 将当前界面数据导出为 CSV 文件(每条记录一个文件,命名为 InBagCode.csv)
|
||||
/// </summary>
|
||||
private void ExportToCsv()
|
||||
{
|
||||
if (IsBusy) return;
|
||||
if (Items.Count == 0)
|
||||
{
|
||||
System.Windows.MessageBox.Show("无可导出的数据", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
var dr = "D:\\迅雷下载";
|
||||
// 转换为 RawProUserCsvDto
|
||||
var all = Items.Select(it => new RawProUserCsvDto
|
||||
{
|
||||
RawCode = it.RawCode,
|
||||
RawName = it.RawName,
|
||||
InBagCode = it.InBagCode,
|
||||
BoxCode = it.BoxCode,
|
||||
Batch = it.Batch,
|
||||
ShelfLife = it.ShelfLife,
|
||||
Weight = it.Weight,
|
||||
DeliveryDate = it.WeightTime.ToString("yyyyMMdd"),
|
||||
RemainWeight = it.RemainWeight,
|
||||
StockWeight = it.StockWeight,
|
||||
WeightTime = it.WeightTime,
|
||||
OpUser = it.OpUser,
|
||||
CheckUser = it.CheckUser,
|
||||
OutTime = it.OutTime
|
||||
}).ToList();
|
||||
|
||||
// 过滤 InBagCode 为空的记录,避免导出失败
|
||||
var valid = all.Where(x => !string.IsNullOrWhiteSpace(x.InBagCode)).ToList();
|
||||
int skipped = all.Count - valid.Count;
|
||||
|
||||
var svc = csvServices;
|
||||
var paths = svc.ExportMany(valid, dr, overwrite: true);
|
||||
|
||||
_log.Info($"RawProUse CSV 导出完成: 目录={dr}, 成功={valid.Count}, 跳过={skipped}");
|
||||
System.Windows.MessageBox.Show($"导出完成:成功 {valid.Count} 条,跳过 {skipped} 条。\n目录:{dr}", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error($"RawProUse CSV 导出失败: {ex}");
|
||||
System.Windows.MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出当前查询结果为 Excel(.xlsx)
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user