using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Windows.Threading; 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; /// /// 提供主窗口与首页共享的运行状态、日志和命令绑定能力。 /// public partial class MainWindowViewModel : ObservableObject { private readonly IAppStateStore _stateStore; private readonly IDispatcherService _dispatcherService; private readonly IWorkflowControlService _workflowControlService; private readonly WorkflowOptions _workflowOptions; private readonly SecurityOptions _securityOptions; private readonly IAdminUnlockDialogService _adminUnlockDialogService; private readonly DispatcherTimer _adminUnlockTimer; private DateTimeOffset _lastAppliedRecordDataUpdatedAt = DateTimeOffset.MinValue; private DateTimeOffset? _adminUnlockedUntil; /// /// 初始化主窗口视图模型。 /// /// 运行态存储。 /// Dispatcher 调度服务。 /// 流程控制服务。 /// 应用配置。 /// 管理员解锁弹窗服务。 public MainWindowViewModel( IAppStateStore stateStore, IDispatcherService dispatcherService, IWorkflowControlService workflowControlService, AppConfig config, IAdminUnlockDialogService adminUnlockDialogService) { _stateStore = stateStore ?? throw new ArgumentNullException(nameof(stateStore)); _dispatcherService = dispatcherService ?? throw new ArgumentNullException(nameof(dispatcherService)); _workflowControlService = workflowControlService ?? throw new ArgumentNullException(nameof(workflowControlService)); ArgumentNullException.ThrowIfNull(config); _adminUnlockDialogService = adminUnlockDialogService ?? throw new ArgumentNullException(nameof(adminUnlockDialogService)); _workflowOptions = config.Workflow; _securityOptions = config.Security; _adminUnlockTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _adminUnlockTimer.Tick += OnAdminUnlockTimerTick; Title = "Axi Omron PCB Check"; Logs = new ObservableCollection(); RecentBoards = new ObservableCollection(); PlcMonitorItems = new ObservableCollection(); AdminUnlockStatus = "管理员功能已锁定"; Logs.CollectionChanged += OnLogsCollectionChanged; RecentBoards.CollectionChanged += OnRecentBoardsCollectionChanged; _stateStore.SnapshotChanged += OnSnapshotChanged; _stateStore.LogAdded += OnLogAdded; ApplySnapshot(_stateStore.GetSnapshot()); RecalculateLogStatistics(); } /// /// 获取运行日志集合。 /// public ObservableCollection Logs { get; } /// /// 获取最近处理记录集合。 /// public ObservableCollection RecentBoards { get; } /// /// 获取 PLC 实时监控集合。 /// public ObservableCollection PlcMonitorItems { get; } /// /// 获取最近运行日志集合(与 为同一集合,供 UI 绑定语义更清晰)。 /// public ObservableCollection RecentLogs => Logs; /// /// 获取最近处理记录集合(与 为同一集合,供 UI 绑定语义更清晰)。 /// public ObservableCollection RecentProcessRecords => RecentBoards; /// /// 获取或设置主窗口标题。 /// [ObservableProperty] private string _title = string.Empty; /// /// 获取或设置 PLC 状态文本。 /// [ObservableProperty] private string _plcStatus = "未连接"; /// /// 获取或设置扫码枪状态文本。 /// [ObservableProperty] private string _scannerStatus = "未验证"; /// /// 获取或设置文件校验通道状态文本。 /// [ObservableProperty] private string _sftpStatus = "未验证"; /// /// 获取或设置安灯状态文本。 /// [ObservableProperty] private string _andonStatus = "未验证"; /// /// 获取或设置当前流程状态文本。 /// [ObservableProperty] private string _workflowStateText = "空闲等待"; /// /// 获取或设置当前条码。 /// [ObservableProperty] private string _currentBarcode = string.Empty; /// /// 获取或设置当前结果描述。 /// [ObservableProperty] private string _resultDescription = "等待触发"; /// /// 获取或设置当前故障信息。 /// [ObservableProperty] private string _faultMessage = string.Empty; /// /// 获取或设置结果代码。 /// [ObservableProperty] private ushort _resultCode; /// /// 获取或设置最近触发时间。 /// [ObservableProperty] private string _lastTriggeredAt = "-"; /// /// 获取或设置最近完成时间。 /// [ObservableProperty] private string _lastCompletedAt = "-"; /// /// 获取或设置是否忙碌。 /// [ObservableProperty] private bool _isBusy; /// /// 获取或设置最近更新时间。 /// [ObservableProperty] private string _lastUpdatedAt = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); /// /// 获取或设置运行日志区的最后刷新时间文本。 /// [ObservableProperty] private string _lastLogUpdateTime = "-"; /// /// 获取或设置运行日志区域是否展开;该状态仅影响首页布局,不参与业务流程。 /// [ObservableProperty] private bool _isLogAreaExpanded; /// /// 获取或设置今日异常日志条数。 /// [ObservableProperty] private int _todayErrorCount; /// /// 获取或设置最近一次异常日志的时间文本。 /// [ObservableProperty] private string _lastErrorTime = "-"; /// /// 获取或设置当前是否存在处理记录,供空状态显示切换使用。 /// [ObservableProperty] private bool _hasProcessRecords; /// /// 获取或设置今日处理总数。 /// [ObservableProperty] private int _todayProcessCount; /// /// 获取或设置今日 OK 数。 /// [ObservableProperty] private int _todayOkCount; /// /// 获取或设置今日 NG 数。 /// [ObservableProperty] private int _todayNgCount; /// /// 获取或设置处理记录区的最后刷新时间文本。 /// [ObservableProperty] private string _lastProcessUpdateTime = "-"; /// /// 获取或设置当前处理记录统计日期文本。 /// [ObservableProperty] private string _recordStatisticDateText = "-"; /// /// 获取或设置管理员功能是否已解锁。 /// [NotifyCanExecuteChangedFor(nameof(UnlockAdminCommand))] [NotifyCanExecuteChangedFor(nameof(ResetCommand))] [NotifyCanExecuteChangedFor(nameof(ReconnectPlcCommand))] [NotifyCanExecuteChangedFor(nameof(ReconnectScannerCommand))] [NotifyCanExecuteChangedFor(nameof(TestAndonCommand))] [ObservableProperty] private bool _isAdminUnlocked; /// /// 获取或设置管理员解锁状态文本。 /// [ObservableProperty] private string _adminUnlockStatus = string.Empty; /// /// 获取或设置当前是否正在执行手动操作。 /// [NotifyCanExecuteChangedFor(nameof(UnlockAdminCommand))] [NotifyCanExecuteChangedFor(nameof(ResetCommand))] [NotifyCanExecuteChangedFor(nameof(ReconnectPlcCommand))] [NotifyCanExecuteChangedFor(nameof(ReconnectScannerCommand))] [NotifyCanExecuteChangedFor(nameof(TestAndonCommand))] [ObservableProperty] private bool _isManualActionRunning; /// /// 获取或设置当前手动操作状态文本。 /// [ObservableProperty] private string _manualActionStatus = "待命"; /// /// 获取当前是否允许执行管理员解锁。 /// private bool CanUnlockAdmin => !IsManualActionRunning; /// /// 获取当前是否允许执行手动操作。 /// private bool CanExecuteManualAction => IsAdminUnlocked && !IsManualActionRunning; /// /// 执行手动复位命令。 /// /// 表示命令执行完成的任务。 [RelayCommand(CanExecute = nameof(CanExecuteManualAction))] private Task ResetAsync() { return ExecuteManualActionAsync("正在执行手动复位...", () => _workflowControlService.ResetAsync(CancellationToken.None)); } /// /// 执行 PLC 重连命令。 /// /// 表示命令执行完成的任务。 [RelayCommand(CanExecute = nameof(CanExecuteManualAction))] private Task ReconnectPlcAsync() { return ExecuteManualActionAsync("正在重连 PLC...", () => _workflowControlService.ReconnectPlcAsync(CancellationToken.None)); } /// /// 执行扫码枪重连命令。 /// /// 表示命令执行完成的任务。 [RelayCommand(CanExecute = nameof(CanExecuteManualAction))] private Task ReconnectScannerAsync() { return ExecuteManualActionAsync("正在重连扫码枪...", () => _workflowControlService.ReconnectScannerAsync(CancellationToken.None)); } /// /// 执行安灯测试命令。 /// /// 表示命令执行完成的任务。 [RelayCommand(CanExecute = nameof(CanExecuteManualAction))] private Task TestAndonAsync() { return ExecuteManualActionAsync("正在测试安灯接口...", () => _workflowControlService.TestAndonAsync(CancellationToken.None)); } /// /// 执行管理员解锁命令。 /// [RelayCommand(CanExecute = nameof(CanUnlockAdmin))] private void UnlockAdmin() { string password = _securityOptions.AdminPassword?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(password)) { AdminUnlockStatus = "未配置管理员密码"; return; } TimeSpan sessionTimeout = TimeSpan.FromMinutes(Math.Max(1, _securityOptions.AdminSessionTimeoutMinutes)); bool unlocked = _adminUnlockDialogService.ShowUnlockDialog(password, sessionTimeout); if (!unlocked) { return; } _adminUnlockedUntil = DateTimeOffset.Now.Add(sessionTimeout); IsAdminUnlocked = true; UpdateAdminUnlockStatus(); _adminUnlockTimer.Start(); } /// /// 查看全部运行日志命令占位。由工具栏触发,暂时只刷新统计与时间戳。 /// [RelayCommand] private void ShowAllLogs() { RecalculateLogStatistics(); } /// /// 仅显示错误日志命令占位。后续可接入筛选视图,当前刷新统计与时间戳。 /// [RelayCommand] private void ShowErrorLogs() { RecalculateLogStatistics(); } /// /// 导出运行日志命令占位。后续可接入日志文件导出逻辑。 /// [RelayCommand] private void ExportLogs() { LastLogUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); } /// /// 切换运行日志区域展开状态,收起或恢复上方状态概览区域。 /// [RelayCommand] private void ToggleLogArea() { IsLogAreaExpanded = !IsLogAreaExpanded; } /// /// 打开追踪区命令占位。后续应由导航服务跳转至追踪页面。 /// [RelayCommand] private void OpenTrackingArea() { LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); } /// /// 刷新处理记录命令占位。重新计算统计并更新最后刷新时间。 /// [RelayCommand] private void RefreshProcessRecords() { LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); } /// /// 将快照应用到当前视图模型状态。 /// /// 运行态快照。 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; ResultCode = snapshot.ResultCode; LastTriggeredAt = snapshot.LastTriggeredAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-"; LastCompletedAt = snapshot.LastCompletedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-"; IsBusy = snapshot.IsBusy; LastUpdatedAt = snapshot.LastUpdatedAt.ToString("yyyy-MM-dd HH:mm:ss"); RecordStatisticDateText = snapshot.RecordStatisticDate?.ToString("yyyy-MM-dd") ?? "-"; TodayProcessCount = snapshot.TodayProcessCount; TodayOkCount = snapshot.TodayOkCount; TodayNgCount = snapshot.TodayNgCount; PlcMonitorItems.Clear(); foreach (PlcMonitorItem item in snapshot.PlcMonitorItems) { PlcMonitorItems.Add(item.Clone()); } if (snapshot.RecordsLastUpdatedAt != _lastAppliedRecordDataUpdatedAt) { _lastAppliedRecordDataUpdatedAt = snapshot.RecordsLastUpdatedAt; RecentBoards.Clear(); foreach (BoardProcessRecord record in snapshot.BoardRecords .OrderByDescending(item => item.CompletedAt) .Take(_workflowOptions.MaxBoardRecords)) { RecentBoards.Add(new BoardProcessRecord { StartedAt = record.StartedAt, CompletedAt = record.CompletedAt, Barcode = record.Barcode, ScanTryCount = record.ScanTryCount, SftpTryCount = record.SftpTryCount, ResultCode = record.ResultCode, ResultDescription = record.ResultDescription, ReleaseSent = record.ReleaseSent, AlarmRaised = record.AlarmRaised, ExceptionSummary = record.ExceptionSummary }); } } } /// /// 当 发生变化时同步解锁状态文案。 /// /// 最新解锁状态。 partial void OnIsAdminUnlockedChanged(bool value) { if (!value) { _adminUnlockedUntil = null; _adminUnlockTimer.Stop(); AdminUnlockStatus = "管理员功能已锁定"; } } /// /// 当 变化时刷新状态文案。 /// /// 最新执行状态。 partial void OnIsManualActionRunningChanged(bool value) { if (!value) { ManualActionStatus = "待命"; } } /// /// 处理日志集合变化事件,刷新日志区统计字段。 /// /// 事件源。 /// 集合变化参数。 private void OnLogsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { RecalculateLogStatistics(); } /// /// 处理处理记录集合变化事件,刷新处理区统计字段。 /// /// 事件源。 /// 集合变化参数。 private void OnRecentBoardsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { HasProcessRecords = RecentBoards.Count > 0; LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); } /// /// 根据当前日志集合重新计算日志区的统计信息与最后刷新时间。 /// 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"); } /// /// 处理运行态快照变化事件。 /// /// 事件源。 /// 最新快照。 private async void OnSnapshotChanged(object? sender, RuntimeSnapshot snapshot) { await ApplySnapshotOnUiAsync(snapshot).ConfigureAwait(false); } /// /// 处理新增日志事件。 /// /// 事件源。 /// 新增日志。 private async void OnLogAdded(object? sender, UiLogEntry entry) { await AppendLogOnUiAsync(entry).ConfigureAwait(false); } /// /// 处理管理员解锁倒计时,超时后自动恢复锁定。 /// /// 事件源。 /// 事件参数。 private void OnAdminUnlockTimerTick(object? sender, EventArgs e) { if (!IsAdminUnlocked || !_adminUnlockedUntil.HasValue) { return; } if (_adminUnlockedUntil.Value <= DateTimeOffset.Now) { IsAdminUnlocked = false; return; } UpdateAdminUnlockStatus(); } /// /// 更新管理员解锁状态文案。 /// private void UpdateAdminUnlockStatus() { if (!_adminUnlockedUntil.HasValue) { AdminUnlockStatus = "管理员功能已锁定"; return; } TimeSpan remaining = _adminUnlockedUntil.Value - DateTimeOffset.Now; if (remaining <= TimeSpan.Zero) { IsAdminUnlocked = false; return; } AdminUnlockStatus = $"管理员功能已解锁,剩余 {remaining.Minutes:D2}:{remaining.Seconds:D2}"; } /// /// 统一执行手动操作,并在执行期间维护按钮可用状态。 /// /// 执行中的状态文本。 /// 待执行操作。 /// 表示执行完成的任务。 private async Task ExecuteManualActionAsync(string runningStatus, Func action) { if (IsManualActionRunning) { return; } await _dispatcherService.InvokeAsync(() => { IsManualActionRunning = true; ManualActionStatus = runningStatus; }).ConfigureAwait(false); try { await action().ConfigureAwait(false); } finally { await _dispatcherService.InvokeAsync(() => { IsManualActionRunning = false; }).ConfigureAwait(false); } } /// /// 在 UI 线程中安全应用最新运行态快照;若应用关闭导致 Dispatcher 调度被取消,则忽略本次刷新。 /// /// 待应用的运行态快照。 /// 表示刷新流程结束的任务。 private async Task ApplySnapshotOnUiAsync(RuntimeSnapshot snapshot) { try { await _dispatcherService.InvokeAsync(() => ApplySnapshot(snapshot)).ConfigureAwait(false); } catch (OperationCanceledException) { // 应用关闭或 Dispatcher 停止时,取消属于预期行为,避免 async void 事件将其上抛为未处理异常。 } } /// /// 在 UI 线程中安全追加日志;若应用关闭导致 Dispatcher 调度被取消,则忽略本次日志刷新。 /// /// 待追加的日志项。 /// 表示日志刷新流程结束的任务。 private async Task AppendLogOnUiAsync(UiLogEntry entry) { try { await _dispatcherService.InvokeAsync(() => { Logs.Insert(0, entry); while (Logs.Count > _workflowOptions.MaxUiLogEntries) { Logs.RemoveAt(Logs.Count - 1); } }).ConfigureAwait(false); } catch (OperationCanceledException) { // 应用关闭或 Dispatcher 停止时,取消属于预期行为,避免 async void 事件将其上抛为未处理异常。 } } }