✨ feat: 初始化 PCB 检测 WPF 应用程序
* 创建 AxiOmron.PcbCheck 项目主框架及解决方案 * 添加 Dashboard 和系统设置页面 * 实现 Modbus TCP PLC、扫码枪、SFTP 查询等核心服务 * 集成 Andon 报警、工作流托管服务与日志配置 * 补充项目文档和 UI 设计规范
This commit is contained in:
505
src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs
Normal file
505
src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,505 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 提供主窗口与首页共享的运行状态、日志和命令绑定能力。
|
||||
/// </summary>
|
||||
public partial class MainWindowViewModel : ObservableObject
|
||||
{
|
||||
private readonly IAppStateStore _stateStore;
|
||||
private readonly IDispatcherService _dispatcherService;
|
||||
private readonly IWorkflowControlService _workflowControlService;
|
||||
private readonly WorkflowOptions _workflowOptions;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化主窗口视图模型。
|
||||
/// </summary>
|
||||
/// <param name="stateStore">运行态存储。</param>
|
||||
/// <param name="dispatcherService">Dispatcher 调度服务。</param>
|
||||
/// <param name="workflowControlService">流程控制服务。</param>
|
||||
/// <param name="config">应用配置。</param>
|
||||
public MainWindowViewModel(
|
||||
IAppStateStore stateStore,
|
||||
IDispatcherService dispatcherService,
|
||||
IWorkflowControlService workflowControlService,
|
||||
AppConfig config)
|
||||
{
|
||||
_stateStore = stateStore ?? throw new ArgumentNullException(nameof(stateStore));
|
||||
_dispatcherService = dispatcherService ?? throw new ArgumentNullException(nameof(dispatcherService));
|
||||
_workflowControlService = workflowControlService ?? throw new ArgumentNullException(nameof(workflowControlService));
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_workflowOptions = config.Workflow;
|
||||
|
||||
Title = "Axi Omron PCB Check";
|
||||
Logs = new ObservableCollection<UiLogEntry>();
|
||||
RecentBoards = new ObservableCollection<BoardProcessRecord>();
|
||||
|
||||
Logs.CollectionChanged += OnLogsCollectionChanged;
|
||||
RecentBoards.CollectionChanged += OnRecentBoardsCollectionChanged;
|
||||
|
||||
_stateStore.SnapshotChanged += OnSnapshotChanged;
|
||||
_stateStore.LogAdded += OnLogAdded;
|
||||
_stateStore.RecordAdded += OnRecordAdded;
|
||||
ApplySnapshot(_stateStore.GetSnapshot());
|
||||
RecalculateLogStatistics();
|
||||
RecalculateProcessStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取运行日志集合。
|
||||
/// </summary>
|
||||
public ObservableCollection<UiLogEntry> Logs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近处理记录集合。
|
||||
/// </summary>
|
||||
public ObservableCollection<BoardProcessRecord> RecentBoards { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近运行日志集合(与 <see cref="Logs"/> 为同一集合,供 UI 绑定语义更清晰)。
|
||||
/// </summary>
|
||||
public ObservableCollection<UiLogEntry> RecentLogs => Logs;
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近处理记录集合(与 <see cref="RecentBoards"/> 为同一集合,供 UI 绑定语义更清晰)。
|
||||
/// </summary>
|
||||
public ObservableCollection<BoardProcessRecord> RecentProcessRecords => RecentBoards;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置主窗口标题。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _title = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _plcStatus = "未连接";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码枪状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _scannerStatus = "未验证";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _sftpStatus = "未验证";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置安灯状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _andonStatus = "未验证";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前流程状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _workflowStateText = "空闲等待";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前条码。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _currentBarcode = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前结果描述。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _resultDescription = "等待触发";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前故障信息。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _faultMessage = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码次数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _scanTryCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 查询次数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _sftpTryCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置结果代码。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private ushort _resultCode;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置报警代码。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private ushort _alarmCode;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最近触发时间。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastTriggeredAt = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最近完成时间。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastCompletedAt = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否忙碌。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _isBusy;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否存在系统故障。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _isFaulted;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否已完成。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _isDone;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否已触发报警。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _isAlarmRaised;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最近更新时间。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastUpdatedAt = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置运行日志区的最后刷新时间文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastLogUpdateTime = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置今日异常日志条数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _todayErrorCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前活跃告警数量。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _activeAlarmCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最近一次异常日志的时间文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastErrorTime = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前是否存在处理记录,供空状态显示切换使用。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _hasProcessRecords;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置今日处理总数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _todayProcessCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置今日 OK 数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _todayOkCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置今日 NG 数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _todayNgCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置处理记录区的最后刷新时间文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastProcessUpdateTime = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 执行手动复位命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
private Task ResetAsync()
|
||||
{
|
||||
return _workflowControlService.ResetAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行 PLC 重连命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
private Task ReconnectPlcAsync()
|
||||
{
|
||||
return _workflowControlService.ReconnectPlcAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行扫码枪重连命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
private Task ReconnectScannerAsync()
|
||||
{
|
||||
return _workflowControlService.ReconnectScannerAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行安灯测试命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
private Task TestAndonAsync()
|
||||
{
|
||||
return _workflowControlService.TestAndonAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查看全部运行日志命令占位。由工具栏触发,暂时只刷新统计与时间戳。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ShowAllLogs()
|
||||
{
|
||||
RecalculateLogStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅显示错误日志命令占位。后续可接入筛选视图,当前刷新统计与时间戳。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ShowErrorLogs()
|
||||
{
|
||||
RecalculateLogStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出运行日志命令占位。后续可接入日志文件导出逻辑。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ExportLogs()
|
||||
{
|
||||
LastLogUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开追踪区命令占位。后续应由导航服务跳转至追踪页面。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void OpenTrackingArea()
|
||||
{
|
||||
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新处理记录命令占位。重新计算统计并更新最后刷新时间。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void RefreshProcessRecords()
|
||||
{
|
||||
RecalculateProcessStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将快照应用到当前视图模型状态。
|
||||
/// </summary>
|
||||
/// <param name="snapshot">运行态快照。</param>
|
||||
private void ApplySnapshot(RuntimeSnapshot snapshot)
|
||||
{
|
||||
PlcStatus = snapshot.PlcStatus;
|
||||
ScannerStatus = snapshot.ScannerStatus;
|
||||
SftpStatus = snapshot.SftpStatus;
|
||||
AndonStatus = snapshot.AndonStatus;
|
||||
WorkflowStateText = snapshot.WorkflowStateText;
|
||||
CurrentBarcode = snapshot.CurrentBarcode;
|
||||
ResultDescription = snapshot.ResultDescription;
|
||||
FaultMessage = snapshot.FaultMessage;
|
||||
ScanTryCount = snapshot.ScanTryCount;
|
||||
SftpTryCount = snapshot.SftpTryCount;
|
||||
ResultCode = snapshot.ResultCode;
|
||||
AlarmCode = snapshot.AlarmCode;
|
||||
LastTriggeredAt = snapshot.LastTriggeredAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||
LastCompletedAt = snapshot.LastCompletedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||
IsBusy = snapshot.IsBusy;
|
||||
IsFaulted = snapshot.SystemFault;
|
||||
IsDone = snapshot.ProcessDone;
|
||||
IsAlarmRaised = snapshot.AlarmRaised;
|
||||
ActiveAlarmCount = snapshot.AlarmRaised ? 1 : 0;
|
||||
LastUpdatedAt = snapshot.LastUpdatedAt.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 <see cref="IsAlarmRaised"/> 发生变化时同步活跃告警计数。
|
||||
/// </summary>
|
||||
/// <param name="value">最新告警状态。</param>
|
||||
partial void OnIsAlarmRaisedChanged(bool value)
|
||||
{
|
||||
ActiveAlarmCount = value ? 1 : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理日志集合变化事件,刷新日志区统计字段。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="e">集合变化参数。</param>
|
||||
private void OnLogsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RecalculateLogStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理处理记录集合变化事件,刷新处理区统计字段。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="e">集合变化参数。</param>
|
||||
private void OnRecentBoardsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RecalculateProcessStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前日志集合重新计算日志区的统计信息与最后刷新时间。
|
||||
/// </summary>
|
||||
private void RecalculateLogStatistics()
|
||||
{
|
||||
DateTime today = DateTime.Today;
|
||||
int errorToday = 0;
|
||||
DateTimeOffset? lastErrorAt = null;
|
||||
|
||||
foreach (UiLogEntry entry in Logs)
|
||||
{
|
||||
bool isError = string.Equals(entry.Level, "Error", StringComparison.OrdinalIgnoreCase);
|
||||
if (!isError)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.Timestamp.LocalDateTime.Date == today)
|
||||
{
|
||||
errorToday++;
|
||||
}
|
||||
|
||||
if (lastErrorAt is null || entry.Timestamp > lastErrorAt.Value)
|
||||
{
|
||||
lastErrorAt = entry.Timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
TodayErrorCount = errorToday;
|
||||
LastErrorTime = lastErrorAt.HasValue
|
||||
? lastErrorAt.Value.ToString("MM-dd HH:mm:ss")
|
||||
: "-";
|
||||
LastLogUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前处理记录集合重新计算处理区的统计信息与最后刷新时间。
|
||||
/// </summary>
|
||||
private void RecalculateProcessStatistics()
|
||||
{
|
||||
DateTime today = DateTime.Today;
|
||||
int totalToday = 0;
|
||||
int okToday = 0;
|
||||
int ngToday = 0;
|
||||
|
||||
foreach (BoardProcessRecord record in RecentBoards)
|
||||
{
|
||||
if (record.CompletedAt.LocalDateTime.Date != today)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
totalToday++;
|
||||
if (record.ResultCode == (ushort)WorkflowResultCode.Passed)
|
||||
{
|
||||
okToday++;
|
||||
}
|
||||
else if (record.ResultCode != (ushort)WorkflowResultCode.Processing
|
||||
&& record.ResultCode != (ushort)WorkflowResultCode.None)
|
||||
{
|
||||
ngToday++;
|
||||
}
|
||||
}
|
||||
|
||||
TodayProcessCount = totalToday;
|
||||
TodayOkCount = okToday;
|
||||
TodayNgCount = ngToday;
|
||||
HasProcessRecords = RecentBoards.Count > 0;
|
||||
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理运行态快照变化事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="snapshot">最新快照。</param>
|
||||
private async void OnSnapshotChanged(object? sender, RuntimeSnapshot snapshot)
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() => ApplySnapshot(snapshot)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理新增日志事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="entry">新增日志。</param>
|
||||
private async void OnLogAdded(object? sender, UiLogEntry entry)
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() =>
|
||||
{
|
||||
Logs.Insert(0, entry);
|
||||
while (Logs.Count > _workflowOptions.MaxUiLogEntries)
|
||||
{
|
||||
Logs.RemoveAt(Logs.Count - 1);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理新增单板记录事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="record">新增单板记录。</param>
|
||||
private async void OnRecordAdded(object? sender, BoardProcessRecord record)
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() =>
|
||||
{
|
||||
RecentBoards.Insert(0, record);
|
||||
while (RecentBoards.Count > _workflowOptions.MaxBoardRecords)
|
||||
{
|
||||
RecentBoards.RemoveAt(RecentBoards.Count - 1);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user