using System.Security; using System.Windows.Input; namespace Zcbot.WindowsNode; internal sealed class MainWindowViewModel : ObservableObject, IDisposable { private readonly IStartupRegistration startupRegistration; private readonly IDiagnosticService diagnosticService; private readonly AsyncCommand registerCommand; private readonly AsyncCommand reconnectCommand; private readonly AsyncCommand resetIdentityCommand; private string statusText = "尚未注册"; private string statusDetail = "等待节点状态"; private string identityText = "尚未建立节点身份"; private string serverUrl = "http://127.0.0.1:8765"; private string nodeName = Environment.MachineName.ToLowerInvariant(); private string enrollmentCode = string.Empty; private string operationMessage = string.Empty; private string statusTone = "neutral"; private bool isRegistered; private bool isBusy; private bool isExiting; private string currentPage = "overview"; private bool isStartupEnabled; private NodeConfig? currentConfig; internal MainWindowViewModel( IStartupRegistration startupRegistration, IDiagnosticService diagnosticService, SoftwarePageViewModel softwarePage, JobPageViewModel jobPage, AnsysAcceptanceViewModel acceptancePage, DataRootPageViewModel dataRootPage) { this.startupRegistration = startupRegistration; this.diagnosticService = diagnosticService; SoftwarePage = softwarePage; JobPage = jobPage; AcceptancePage = acceptancePage; DataRootPage = dataRootPage; try { isStartupEnabled = startupRegistration.IsEnabled; } catch (Exception exception) when ( exception is UnauthorizedAccessException or IOException or SecurityException) { operationMessage = $"无法读取自动启动设置:{exception.Message}"; } CopyDiagnosticsCommand = new RelayCommand(CopyDiagnostics, () => IsRegistered); HideCommand = new RelayCommand(() => HideRequested?.Invoke(), () => !IsExiting); ExitCommand = new AsyncCommand( () => ExitRequested?.Invoke() ?? Task.CompletedTask, () => !IsExiting); ShowOverviewCommand = new RelayCommand(() => Navigate("overview")); ShowSoftwareCommand = new AsyncCommand(async () => { Navigate("software"); await SoftwarePage.RefreshAsync(); AcceptancePage.RefreshStatus(); }); ShowJobsCommand = new RelayCommand(() => { Navigate("jobs"); JobPage.Refresh(); }); ShowSettingsCommand = new RelayCommand(() => { Navigate("settings"); DataRootPage.Refresh(); }); registerCommand = new AsyncCommand(RegisterAsync, CanRegister); reconnectCommand = new AsyncCommand(ReconnectAsync, () => IsRegistered && !IsBusy); resetIdentityCommand = new AsyncCommand(ResetIdentityAsync, () => IsRegistered && !IsBusy); RegisterCommand = registerCommand; ReconnectCommand = reconnectCommand; ResetIdentityCommand = resetIdentityCommand; } internal event Func? CopyDiagnosticsRequested; internal event Action? HideRequested; internal event Func? ExitRequested; internal event Func? RegisterRequested; internal event Func>? ReconnectRequested; internal event Func>? ResetIdentityRequested; public string StatusText { get => statusText; private set => SetProperty(ref statusText, value); } public string StatusDetail { get => statusDetail; private set => SetProperty(ref statusDetail, value); } public string IdentityText { get => identityText; private set => SetProperty(ref identityText, value); } public string ServerUrl { get => serverUrl; set { if (SetProperty(ref serverUrl, value)) registerCommand.RaiseCanExecuteChanged(); } } public string NodeName { get => nodeName; set { if (SetProperty(ref nodeName, value)) registerCommand.RaiseCanExecuteChanged(); } } public string EnrollmentCode { get => enrollmentCode; set { if (SetProperty(ref enrollmentCode, value)) registerCommand.RaiseCanExecuteChanged(); } } public string OperationMessage { get => operationMessage; private set { if (SetProperty(ref operationMessage, value)) { OnPropertyChanged(nameof(HasOperationMessage)); } } } public bool HasOperationMessage => !string.IsNullOrWhiteSpace(OperationMessage); public string StatusTone { get => statusTone; private set => SetProperty(ref statusTone, value); } public bool IsRegistered { get => isRegistered; private set { if (SetProperty(ref isRegistered, value)) { OnPropertyChanged(nameof(IsRegistrationVisible)); RaiseOperationCanExecuteChanged(); ((RelayCommand)CopyDiagnosticsCommand).RaiseCanExecuteChanged(); } } } public bool IsRegistrationVisible => !IsRegistered; public bool IsBusy { get => isBusy; private set { if (SetProperty(ref isBusy, value)) RaiseOperationCanExecuteChanged(); } } public bool IsExiting { get => isExiting; private set { if (SetProperty(ref isExiting, value)) { ((RelayCommand)HideCommand).RaiseCanExecuteChanged(); ((AsyncCommand)ExitCommand).RaiseCanExecuteChanged(); } } } public bool IsOverviewPage => currentPage == "overview"; public bool IsSoftwarePage => currentPage == "software"; public bool IsJobsPage => currentPage == "jobs"; public bool IsSettingsPage => currentPage == "settings"; public string PageTitle => currentPage switch { "software" => "专业软件", "jobs" => "本机任务", "settings" => "运行设置", _ => "节点概览", }; public bool IsStartupEnabled { get => isStartupEnabled; set { if (value == isStartupEnabled) return; try { startupRegistration.SetEnabled(value); SetProperty(ref isStartupEnabled, value); OperationMessage = value ? "已启用登录 Windows 后自动启动" : "已关闭登录 Windows 后自动启动"; } catch (Exception exception) when ( exception is UnauthorizedAccessException or IOException or SecurityException) { OperationMessage = $"自动启动设置失败:{exception.Message}"; OnPropertyChanged(); } } } public ICommand CopyDiagnosticsCommand { get; } public ICommand HideCommand { get; } public ICommand ExitCommand { get; } public ICommand ShowOverviewCommand { get; } public ICommand ShowSoftwareCommand { get; } public ICommand ShowJobsCommand { get; } public ICommand ShowSettingsCommand { get; } public ICommand RegisterCommand { get; } public ICommand ReconnectCommand { get; } public ICommand ResetIdentityCommand { get; } public SoftwarePageViewModel SoftwarePage { get; } public JobPageViewModel JobPage { get; } public AnsysAcceptanceViewModel AcceptancePage { get; } public DataRootPageViewModel DataRootPage { get; } internal void ApplyStatus(NodeStatus status, NodeConfig? config = null) { StatusText = status.State switch { NodeState.Online => "节点在线", NodeState.Connecting => "正在连接", NodeState.AuthenticationRequired => "需要重新注册", NodeState.Offline => "节点离线", NodeState.Stopped => "节点已停止", _ => "尚未注册", }; StatusTone = status.State switch { NodeState.Online => "success", NodeState.Connecting => "warning", NodeState.NotRegistered or NodeState.AuthenticationRequired => "error", _ => "neutral", }; StatusDetail = $"{status.Message} {status.ChangedAt:HH:mm:ss}"; IsRegistered = config is not null; currentConfig = config; IdentityText = config is null ? "尚未建立节点身份" : $"节点:{config.NodeName}\nNode ID:{config.NodeId}\n服务:{config.ServerUrl}"; if (config is not null) { ServerUrl = config.ServerUrl.AbsoluteUri.TrimEnd('/'); NodeName = config.NodeName; EnrollmentCode = string.Empty; } } internal void BeginExit() { SoftwarePage.CancelInstall(); IsExiting = true; StatusText = "正在退出"; StatusTone = "neutral"; StatusDetail = "正在等待本机任务与后台进程安全收尾"; } public void Dispose() { SoftwarePage.Dispose(); AcceptancePage.Dispose(); } private bool CanRegister() => !IsRegistered && !IsBusy && !string.IsNullOrWhiteSpace(ServerUrl) && !string.IsNullOrWhiteSpace(NodeName) && !string.IsNullOrWhiteSpace(EnrollmentCode); private void CopyDiagnostics() { if (currentConfig is null) return; OperationMessage = CopyDiagnosticsRequested?.Invoke( diagnosticService.Build(currentConfig)) == true ? "诊断信息已复制;内容不包含 Node Token。" : "无法写入剪贴板,请稍后重试。"; } private async Task RegisterAsync() { if (RegisterRequested is null) return; await RunOperationAsync(async () => { var options = EnrollOptions.Parse([ "--server", ServerUrl, "--name", NodeName, "--code", EnrollmentCode]); await RegisterRequested(options); OperationMessage = "注册成功,正在连接节点"; }, "正在注册节点…"); } private async Task ReconnectAsync() { if (ReconnectRequested is null) return; await RunOperationAsync(async () => { OperationMessage = await ReconnectRequested() ? "已开始重新连接" : "本机尚未注册"; }, "正在等待任务收尾并重新连接…"); } private async Task ResetIdentityAsync() { if (ResetIdentityRequested is null) return; await RunOperationAsync(async () => { if (await ResetIdentityRequested()) { OperationMessage = "本机身份已清除,请使用新注册码注册"; } else { OperationMessage = "已取消清除本机身份"; } }, "正在清除本机身份…"); } private async Task RunOperationAsync(Func operation, string progress) { IsBusy = true; OperationMessage = progress; try { await operation(); } catch (Exception exception) when ( exception is NodeConfigurationException or HttpRequestException or IOException or UnauthorizedAccessException) { OperationMessage = exception.Message; } finally { IsBusy = false; } } private void RaiseOperationCanExecuteChanged() { registerCommand.RaiseCanExecuteChanged(); reconnectCommand.RaiseCanExecuteChanged(); resetIdentityCommand.RaiseCanExecuteChanged(); } private void Navigate(string page) { if (currentPage == page) return; currentPage = page; OnPropertyChanged(nameof(IsOverviewPage)); OnPropertyChanged(nameof(IsSoftwarePage)); OnPropertyChanged(nameof(IsJobsPage)); OnPropertyChanged(nameof(IsSettingsPage)); OnPropertyChanged(nameof(PageTitle)); } }