- 修改 appConfig.json 中 PLC 和扫描仪的 IP 地址及字节序配置 - 将多个地方的 ResultCode 类型从 short 更新为 ushort,以确保与 PLC 通信一致 - 更新相关服务和模型以适应新的 ResultCode 类型 - 调整单元测试以验证新配置和类型的正确性
675 lines
22 KiB
C#
675 lines
22 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 提供主窗口与首页共享的运行状态、日志和命令绑定能力。
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 初始化主窗口视图模型。
|
|
/// </summary>
|
|
/// <param name="stateStore">运行态存储。</param>
|
|
/// <param name="dispatcherService">Dispatcher 调度服务。</param>
|
|
/// <param name="workflowControlService">流程控制服务。</param>
|
|
/// <param name="config">应用配置。</param>
|
|
/// <param name="adminUnlockDialogService">管理员解锁弹窗服务。</param>
|
|
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<UiLogEntry>();
|
|
RecentBoards = new ObservableCollection<BoardProcessRecord>();
|
|
PlcMonitorItems = new ObservableCollection<PlcMonitorItem>();
|
|
AdminUnlockStatus = "管理员功能已锁定";
|
|
|
|
Logs.CollectionChanged += OnLogsCollectionChanged;
|
|
RecentBoards.CollectionChanged += OnRecentBoardsCollectionChanged;
|
|
|
|
_stateStore.SnapshotChanged += OnSnapshotChanged;
|
|
_stateStore.LogAdded += OnLogAdded;
|
|
ApplySnapshot(_stateStore.GetSnapshot());
|
|
RecalculateLogStatistics();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取运行日志集合。
|
|
/// </summary>
|
|
public ObservableCollection<UiLogEntry> Logs { get; }
|
|
|
|
/// <summary>
|
|
/// 获取最近处理记录集合。
|
|
/// </summary>
|
|
public ObservableCollection<BoardProcessRecord> RecentBoards { get; }
|
|
|
|
/// <summary>
|
|
/// 获取 PLC 实时监控集合。
|
|
/// </summary>
|
|
public ObservableCollection<PlcMonitorItem> PlcMonitorItems { 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>
|
|
/// 获取或设置文件校验通道状态文本。
|
|
/// </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 ushort _resultCode;
|
|
|
|
/// <summary>
|
|
/// 获取或设置最近触发时间。
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private string _lastTriggeredAt = "-";
|
|
|
|
/// <summary>
|
|
/// 获取或设置最近完成时间。
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private string _lastCompletedAt = "-";
|
|
|
|
/// <summary>
|
|
/// 获取或设置是否忙碌。
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private bool _isBusy;
|
|
|
|
/// <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 bool _isLogAreaExpanded;
|
|
|
|
/// <summary>
|
|
/// 获取或设置今日异常日志条数。
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private int _todayErrorCount;
|
|
|
|
/// <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>
|
|
[ObservableProperty]
|
|
private string _recordStatisticDateText = "-";
|
|
|
|
/// <summary>
|
|
/// 获取或设置管理员功能是否已解锁。
|
|
/// </summary>
|
|
[NotifyCanExecuteChangedFor(nameof(UnlockAdminCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(ResetCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(ReconnectPlcCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(ReconnectScannerCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(TestAndonCommand))]
|
|
[ObservableProperty]
|
|
private bool _isAdminUnlocked;
|
|
|
|
/// <summary>
|
|
/// 获取或设置管理员解锁状态文本。
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private string _adminUnlockStatus = string.Empty;
|
|
|
|
/// <summary>
|
|
/// 获取或设置当前是否正在执行手动操作。
|
|
/// </summary>
|
|
[NotifyCanExecuteChangedFor(nameof(UnlockAdminCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(ResetCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(ReconnectPlcCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(ReconnectScannerCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(TestAndonCommand))]
|
|
[ObservableProperty]
|
|
private bool _isManualActionRunning;
|
|
|
|
/// <summary>
|
|
/// 获取或设置当前手动操作状态文本。
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private string _manualActionStatus = "待命";
|
|
|
|
/// <summary>
|
|
/// 获取当前是否允许执行管理员解锁。
|
|
/// </summary>
|
|
private bool CanUnlockAdmin => !IsManualActionRunning;
|
|
|
|
/// <summary>
|
|
/// 获取当前是否允许执行手动操作。
|
|
/// </summary>
|
|
private bool CanExecuteManualAction => IsAdminUnlocked && !IsManualActionRunning;
|
|
|
|
/// <summary>
|
|
/// 执行手动复位命令。
|
|
/// </summary>
|
|
/// <returns>表示命令执行完成的任务。</returns>
|
|
[RelayCommand(CanExecute = nameof(CanExecuteManualAction))]
|
|
private Task ResetAsync()
|
|
{
|
|
return ExecuteManualActionAsync("正在执行手动复位...", () => _workflowControlService.ResetAsync(CancellationToken.None));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 执行 PLC 重连命令。
|
|
/// </summary>
|
|
/// <returns>表示命令执行完成的任务。</returns>
|
|
[RelayCommand(CanExecute = nameof(CanExecuteManualAction))]
|
|
private Task ReconnectPlcAsync()
|
|
{
|
|
return ExecuteManualActionAsync("正在重连 PLC...", () => _workflowControlService.ReconnectPlcAsync(CancellationToken.None));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 执行扫码枪重连命令。
|
|
/// </summary>
|
|
/// <returns>表示命令执行完成的任务。</returns>
|
|
[RelayCommand(CanExecute = nameof(CanExecuteManualAction))]
|
|
private Task ReconnectScannerAsync()
|
|
{
|
|
return ExecuteManualActionAsync("正在重连扫码枪...", () => _workflowControlService.ReconnectScannerAsync(CancellationToken.None));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 执行安灯测试命令。
|
|
/// </summary>
|
|
/// <returns>表示命令执行完成的任务。</returns>
|
|
[RelayCommand(CanExecute = nameof(CanExecuteManualAction))]
|
|
private Task TestAndonAsync()
|
|
{
|
|
return ExecuteManualActionAsync("正在测试安灯接口...", () => _workflowControlService.TestAndonAsync(CancellationToken.None));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 执行管理员解锁命令。
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
|
|
/// <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 ToggleLogArea()
|
|
{
|
|
IsLogAreaExpanded = !IsLogAreaExpanded;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 打开追踪区命令占位。后续应由导航服务跳转至追踪页面。
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private void OpenTrackingArea()
|
|
{
|
|
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 刷新处理记录命令占位。重新计算统计并更新最后刷新时间。
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private void RefreshProcessRecords()
|
|
{
|
|
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
}
|
|
|
|
/// <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;
|
|
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
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 当 <see cref="IsAdminUnlocked"/> 发生变化时同步解锁状态文案。
|
|
/// </summary>
|
|
/// <param name="value">最新解锁状态。</param>
|
|
partial void OnIsAdminUnlockedChanged(bool value)
|
|
{
|
|
if (!value)
|
|
{
|
|
_adminUnlockedUntil = null;
|
|
_adminUnlockTimer.Stop();
|
|
AdminUnlockStatus = "管理员功能已锁定";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 当 <see cref="IsManualActionRunning"/> 变化时刷新状态文案。
|
|
/// </summary>
|
|
/// <param name="value">最新执行状态。</param>
|
|
partial void OnIsManualActionRunningChanged(bool value)
|
|
{
|
|
if (!value)
|
|
{
|
|
ManualActionStatus = "待命";
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
HasProcessRecords = RecentBoards.Count > 0;
|
|
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
}
|
|
|
|
/// <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>
|
|
/// <param name="sender">事件源。</param>
|
|
/// <param name="snapshot">最新快照。</param>
|
|
private async void OnSnapshotChanged(object? sender, RuntimeSnapshot snapshot)
|
|
{
|
|
await ApplySnapshotOnUiAsync(snapshot).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理新增日志事件。
|
|
/// </summary>
|
|
/// <param name="sender">事件源。</param>
|
|
/// <param name="entry">新增日志。</param>
|
|
private async void OnLogAdded(object? sender, UiLogEntry entry)
|
|
{
|
|
await AppendLogOnUiAsync(entry).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理管理员解锁倒计时,超时后自动恢复锁定。
|
|
/// </summary>
|
|
/// <param name="sender">事件源。</param>
|
|
/// <param name="e">事件参数。</param>
|
|
private void OnAdminUnlockTimerTick(object? sender, EventArgs e)
|
|
{
|
|
if (!IsAdminUnlocked || !_adminUnlockedUntil.HasValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_adminUnlockedUntil.Value <= DateTimeOffset.Now)
|
|
{
|
|
IsAdminUnlocked = false;
|
|
return;
|
|
}
|
|
|
|
UpdateAdminUnlockStatus();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 更新管理员解锁状态文案。
|
|
/// </summary>
|
|
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}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 统一执行手动操作,并在执行期间维护按钮可用状态。
|
|
/// </summary>
|
|
/// <param name="runningStatus">执行中的状态文本。</param>
|
|
/// <param name="action">待执行操作。</param>
|
|
/// <returns>表示执行完成的任务。</returns>
|
|
private async Task ExecuteManualActionAsync(string runningStatus, Func<Task> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 在 UI 线程中安全应用最新运行态快照;若应用关闭导致 Dispatcher 调度被取消,则忽略本次刷新。
|
|
/// </summary>
|
|
/// <param name="snapshot">待应用的运行态快照。</param>
|
|
/// <returns>表示刷新流程结束的任务。</returns>
|
|
private async Task ApplySnapshotOnUiAsync(RuntimeSnapshot snapshot)
|
|
{
|
|
try
|
|
{
|
|
await _dispatcherService.InvokeAsync(() => ApplySnapshot(snapshot)).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// 应用关闭或 Dispatcher 停止时,取消属于预期行为,避免 async void 事件将其上抛为未处理异常。
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 在 UI 线程中安全追加日志;若应用关闭导致 Dispatcher 调度被取消,则忽略本次日志刷新。
|
|
/// </summary>
|
|
/// <param name="entry">待追加的日志项。</param>
|
|
/// <returns>表示日志刷新流程结束的任务。</returns>
|
|
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 事件将其上抛为未处理异常。
|
|
}
|
|
}
|
|
}
|