✨ feat(*): 添加管理员解锁与 SFTP 探活能力
* 新增管理员解锁弹窗、手动操作权限控制与倒计时状态 * 支持系统设置页测试 SFTP 连接,并在启动时执行探活 * 补充设计时服务、全局异常兜底与相关单元测试
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
@@ -17,6 +18,10 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
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? _adminUnlockedUntil;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化主窗口视图模型。
|
||||
@@ -25,21 +30,31 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
/// <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)
|
||||
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>();
|
||||
AdminUnlockStatus = "管理员功能已锁定";
|
||||
|
||||
Logs.CollectionChanged += OnLogsCollectionChanged;
|
||||
RecentBoards.CollectionChanged += OnRecentBoardsCollectionChanged;
|
||||
@@ -246,44 +261,114 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private string _lastProcessUpdateTime = "-";
|
||||
|
||||
/// <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]
|
||||
[RelayCommand(CanExecute = nameof(CanExecuteManualAction))]
|
||||
private Task ResetAsync()
|
||||
{
|
||||
return _workflowControlService.ResetAsync(CancellationToken.None);
|
||||
return ExecuteManualActionAsync("正在执行手动复位...", () => _workflowControlService.ResetAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行 PLC 重连命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
[RelayCommand(CanExecute = nameof(CanExecuteManualAction))]
|
||||
private Task ReconnectPlcAsync()
|
||||
{
|
||||
return _workflowControlService.ReconnectPlcAsync(CancellationToken.None);
|
||||
return ExecuteManualActionAsync("正在重连 PLC...", () => _workflowControlService.ReconnectPlcAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行扫码枪重连命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
[RelayCommand(CanExecute = nameof(CanExecuteManualAction))]
|
||||
private Task ReconnectScannerAsync()
|
||||
{
|
||||
return _workflowControlService.ReconnectScannerAsync(CancellationToken.None);
|
||||
return ExecuteManualActionAsync("正在重连扫码枪...", () => _workflowControlService.ReconnectScannerAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行安灯测试命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
[RelayCommand(CanExecute = nameof(CanExecuteManualAction))]
|
||||
private Task TestAndonAsync()
|
||||
{
|
||||
return _workflowControlService.TestAndonAsync(CancellationToken.None);
|
||||
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>
|
||||
@@ -368,6 +453,32 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
ActiveAlarmCount = value ? 1 : 0;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -502,4 +613,78 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
}
|
||||
}).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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user