DeviceDetailsWindow.xaml.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. using System.Globalization;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Windows.Media;
  5. using System.Windows.Media.Animation;
  6. using QuickIP.Client.Models;
  7. using QuickIP.Client.Services;
  8. namespace QuickIP.Client;
  9. public partial class DeviceDetailsWindow : Window
  10. {
  11. private const int ApplyConfirmationTimeoutSeconds = 20;
  12. private readonly AgentApiService _agentApiService = new();
  13. private readonly string _baseAddress;
  14. private readonly string _localIPv4;
  15. private readonly string _password;
  16. private bool _configValidated;
  17. private bool _isBusy;
  18. private bool _suppressConfigChangeHandling;
  19. private CancellationTokenSource? _statusMessageCts;
  20. public DeviceDetailsWindow(string baseAddress, string localIPv4, string password)
  21. {
  22. InitializeComponent();
  23. _baseAddress = baseAddress;
  24. _localIPv4 = localIPv4;
  25. _password = password;
  26. Loaded += DeviceDetailsWindow_OnLoaded;
  27. }
  28. private async void DeviceDetailsWindow_OnLoaded(object sender, RoutedEventArgs e)
  29. {
  30. try
  31. {
  32. await LoadRemoteDetailsAsync();
  33. UpdateButtonStates();
  34. }
  35. catch (Exception ex)
  36. {
  37. ShowStatusMessage($"读取设备信息失败:{ex.Message}");
  38. SetBusyState(false);
  39. }
  40. }
  41. private async Task LoadRemoteDetailsAsync()
  42. {
  43. ClearDetails();
  44. var device = await _agentApiService.GetDeviceInfoAsync(_baseAddress, _password, _localIPv4);
  45. if (device is not null)
  46. {
  47. RemoteDeviceIdTextBlock.Text = device.DeviceId;
  48. RemoteHostnameTextBlock.Text = device.Hostname;
  49. RemoteOsVersionTextBlock.Text = device.OSVersion;
  50. RemoteAgentVersionTextBlock.Text = device.AgentVersion;
  51. }
  52. var interfaces = await _agentApiService.GetInterfacesAsync(_baseAddress, _password, _localIPv4);
  53. if (interfaces is null)
  54. {
  55. ShowStatusMessage("设备已连接,但暂时无法读取 Linux 接口列表。");
  56. return;
  57. }
  58. ShowStatusMessage($"当前管理接口:{interfaces.ManagementInterface};建议目标接口:{interfaces.SuggestedTargetInterface};{(interfaces.RequiresTargetSelection ? "需要手动选择目标接口。" : "已自动识别建议目标接口。")}");
  59. var suggested = interfaces.Interfaces.FirstOrDefault(item => item.SystemName == interfaces.SuggestedTargetInterface)
  60. ?? interfaces.Interfaces.FirstOrDefault(item => item.IsSuggestedTarget)
  61. ?? interfaces.Interfaces.FirstOrDefault(item => !item.IsManagementInterface);
  62. RemoteTargetInterfaceComboBox.ItemsSource = interfaces.Interfaces;
  63. if (suggested is not null)
  64. {
  65. RemoteTargetInterfaceComboBox.SelectedItem = suggested;
  66. await LoadRemoteInterfaceConfigAsync(suggested.SystemName);
  67. }
  68. }
  69. private void ClearDetails()
  70. {
  71. RemoteDeviceIdTextBlock.Text = "-";
  72. RemoteHostnameTextBlock.Text = "-";
  73. RemoteOsVersionTextBlock.Text = "-";
  74. RemoteAgentVersionTextBlock.Text = "-";
  75. RemoteTargetInterfaceComboBox.ItemsSource = null;
  76. RemoteConfigInterfaceTextBlock.Text = "-";
  77. RemoteConfigIpTextBlock.Text = "-";
  78. RemoteConfigGatewayTextBlock.Text = "-";
  79. RemoteConfigDnsTextBlock.Text = "-";
  80. NewIpTextBox.Text = string.Empty;
  81. NewMaskTextBox.Text = string.Empty;
  82. NewGatewayTextBox.Text = string.Empty;
  83. NewDnsTextBox.Text = string.Empty;
  84. _configValidated = false;
  85. }
  86. private async void RemoteTargetInterfaceComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  87. {
  88. try
  89. {
  90. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
  91. {
  92. UpdateButtonStates();
  93. return;
  94. }
  95. await LoadRemoteInterfaceConfigAsync(selected.SystemName, useBusyState: true);
  96. }
  97. catch (Exception ex)
  98. {
  99. ShowStatusMessage($"读取目标接口配置失败:{ex.Message}");
  100. SetBusyState(false);
  101. }
  102. }
  103. private async Task LoadRemoteInterfaceConfigAsync(string interfaceName, bool useBusyState = false)
  104. {
  105. if (useBusyState)
  106. {
  107. SetBusyState(true, "正在读取 Linux 端 IP 配置...");
  108. }
  109. try
  110. {
  111. var result = await _agentApiService.GetInterfaceConfigAsync(_baseAddress, _password, _localIPv4, interfaceName);
  112. if (!result.Success || result.Data is null)
  113. {
  114. RemoteConfigInterfaceTextBlock.Text = interfaceName;
  115. RemoteConfigIpTextBlock.Text = "读取失败";
  116. RemoteConfigGatewayTextBlock.Text = "读取失败";
  117. RemoteConfigDnsTextBlock.Text = "读取失败";
  118. ShowStatusMessage($"读取目标接口 {interfaceName} 配置失败:{result.Message}");
  119. return;
  120. }
  121. var config = result.Data;
  122. RemoteConfigInterfaceTextBlock.Text = config.Interface;
  123. RemoteConfigIpTextBlock.Text = FormatCurrentIp(config);
  124. RemoteConfigGatewayTextBlock.Text = string.IsNullOrWhiteSpace(config.Gateway) ? "无" : config.Gateway;
  125. RemoteConfigDnsTextBlock.Text = config.DnsSummary;
  126. _suppressConfigChangeHandling = true;
  127. Dhcp4CheckBox.IsChecked = false;
  128. NewIpTextBox.Text = config.IP;
  129. NewMaskTextBox.Text = PrefixToMask(config.Prefix);
  130. NewGatewayTextBox.Text = config.Gateway;
  131. NewDnsTextBox.Text = config.Dns?.FirstOrDefault() ?? string.Empty;
  132. _suppressConfigChangeHandling = false;
  133. _configValidated = false;
  134. ShowStatusMessage("已读取Linux端IP配置。");
  135. UpdateButtonStates();
  136. }
  137. finally
  138. {
  139. if (useBusyState)
  140. {
  141. SetBusyState(false);
  142. }
  143. }
  144. }
  145. private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
  146. {
  147. if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
  148. {
  149. await LoadRemoteInterfaceConfigAsync(selected.SystemName);
  150. }
  151. }
  152. private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
  153. {
  154. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
  155. {
  156. return;
  157. }
  158. var request = BuildConfigRequest(selected.SystemName);
  159. if (request is null)
  160. {
  161. return;
  162. }
  163. SetBusyState(true, "正在校验配置,请稍候...");
  164. try
  165. {
  166. var result = await _agentApiService.ValidateInterfaceConfigAsync(_baseAddress, _password, _localIPv4, request);
  167. _configValidated = result.Success && result.Data?.Valid == true;
  168. if (result.Data is not null)
  169. {
  170. var warnings = result.Data.Warnings.Count > 0 ? $" 警告:{string.Join(";", result.Data.Warnings)}" : string.Empty;
  171. var errors = result.Data.Errors.Count > 0 ? $" 错误:{string.Join(";", result.Data.Errors)}" : string.Empty;
  172. ShowStatusMessage(_configValidated ? $"校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}");
  173. }
  174. else
  175. {
  176. ShowStatusMessage($"校验失败:{result.Message}");
  177. }
  178. UpdateButtonStates();
  179. }
  180. finally
  181. {
  182. SetBusyState(false);
  183. }
  184. }
  185. private async void ApplyConfigButton_OnClick(object sender, RoutedEventArgs e)
  186. {
  187. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
  188. {
  189. return;
  190. }
  191. var request = BuildConfigRequest(selected.SystemName);
  192. if (request is null)
  193. {
  194. return;
  195. }
  196. var confirmMessage = $"将要把以下配置应用到接口 {selected.SystemName}:\n\n" +
  197. $"模式:{(request.Dhcp4 ? "DHCP 自动获取" : "静态 IPv4")}\n" +
  198. $"IP:{(request.Dhcp4 ? "自动获取" : $"{request.IP}/{request.Prefix}")}\n" +
  199. $"网关:{(string.IsNullOrWhiteSpace(request.Gateway) ? "无" : request.Gateway)}\n" +
  200. $"DNS:{(request.Dns.Count == 0 ? "无" : string.Join(", ", request.Dns))}\n\n" +
  201. "请确认是否继续。";
  202. if (MessageBox.Show(this, confirmMessage, "确认应用配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
  203. {
  204. return;
  205. }
  206. SetBusyState(true, "正在提交并应用配置,请稍候...");
  207. try
  208. {
  209. var applyResult = await _agentApiService.ApplyInterfaceConfigAsync(_baseAddress, _password, _localIPv4, request);
  210. if (!applyResult.Success || applyResult.Data is null)
  211. {
  212. ShowStatusMessage($"提交配置任务失败:{applyResult.Message}");
  213. return;
  214. }
  215. ShowStatusMessage("配置任务已提交,正在应用并等待连通确认...");
  216. await PollTaskAsync(applyResult.Data.TaskId);
  217. }
  218. finally
  219. {
  220. SetBusyState(false);
  221. }
  222. }
  223. private async Task PollTaskAsync(string taskId)
  224. {
  225. var transientFailureCount = 0;
  226. var confirmationRequested = false;
  227. for (var i = 0; i < 20; i++)
  228. {
  229. await Task.Delay(1000);
  230. var result = await _agentApiService.GetTaskAsync(_baseAddress, _password, _localIPv4, taskId);
  231. if (!result.Success || result.Data is null)
  232. {
  233. if (result.StatusCode is null)
  234. {
  235. transientFailureCount++;
  236. ShowStatusMessage($"设备连接短暂中断,正在重试({transientFailureCount})。");
  237. continue;
  238. }
  239. ShowStatusMessage($"读取任务状态失败:{result.Message}");
  240. return;
  241. }
  242. transientFailureCount = 0;
  243. var task = result.Data;
  244. ShowStatusMessage(FormatTaskStatusMessage(task));
  245. if (task.Status == "running" && task.Step == "confirming" && !confirmationRequested)
  246. {
  247. confirmationRequested = true;
  248. var confirm = ShowApplyConfirmationDialog(ApplyConfirmationTimeoutSeconds);
  249. if (confirm)
  250. {
  251. var confirmResult = await _agentApiService.ConfirmApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
  252. ShowStatusMessage(confirmResult.Success ? "已发送保留配置确认。" : $"发送确认失败:{confirmResult.Message}");
  253. }
  254. else
  255. {
  256. var cancelResult = await _agentApiService.CancelApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
  257. ShowStatusMessage(cancelResult.Success ? "已取消保留配置,正在回滚。" : $"发送取消失败:{cancelResult.Message}");
  258. }
  259. }
  260. if (task.Status is "success" or "failed" or "rolled_back")
  261. {
  262. ShowTaskCompletionDialog(task);
  263. if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
  264. {
  265. await LoadRemoteInterfaceConfigAsync(selected.SystemName);
  266. }
  267. return;
  268. }
  269. }
  270. ShowStatusMessage($"任务 {taskId} 轮询超时,请稍后手动刷新。");
  271. }
  272. private bool ShowApplyConfirmationDialog(int timeoutSeconds)
  273. {
  274. var remaining = timeoutSeconds;
  275. var result = false;
  276. var messageTextBlock = new TextBlock
  277. {
  278. Width = 420,
  279. TextWrapping = TextWrapping.Wrap,
  280. FontSize = 13,
  281. Foreground = Brushes.Black,
  282. };
  283. var confirmButton = new Button
  284. {
  285. MinWidth = 88,
  286. MinHeight = 32,
  287. Margin = new Thickness(0, 0, 10, 0),
  288. Content = "确认保留",
  289. IsDefault = true,
  290. };
  291. var cancelButton = new Button
  292. {
  293. MinWidth = 88,
  294. MinHeight = 32,
  295. Content = "取消回滚",
  296. IsCancel = true,
  297. };
  298. var dialog = new Window
  299. {
  300. Title = "确认保留网络配置",
  301. Owner = this,
  302. WindowStartupLocation = WindowStartupLocation.CenterOwner,
  303. ResizeMode = ResizeMode.NoResize,
  304. SizeToContent = SizeToContent.WidthAndHeight,
  305. Content = new StackPanel
  306. {
  307. Margin = new Thickness(18),
  308. Children =
  309. {
  310. messageTextBlock,
  311. new StackPanel
  312. {
  313. Margin = new Thickness(0, 18, 0, 0),
  314. HorizontalAlignment = HorizontalAlignment.Right,
  315. Orientation = Orientation.Horizontal,
  316. Children = { confirmButton, cancelButton },
  317. },
  318. },
  319. },
  320. };
  321. void UpdateMessage()
  322. {
  323. messageTextBlock.Text = $"当前客户端仍可连接到设备。是否确认保留这次网络配置?\n\n剩余 {remaining} 秒;超时或取消时,Linux 端会自动回滚。";
  324. }
  325. var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
  326. timer.Tick += (_, _) =>
  327. {
  328. remaining--;
  329. if (remaining <= 0)
  330. {
  331. timer.Stop();
  332. dialog.DialogResult = false;
  333. dialog.Close();
  334. return;
  335. }
  336. UpdateMessage();
  337. };
  338. confirmButton.Click += (_, _) =>
  339. {
  340. result = true;
  341. dialog.DialogResult = true;
  342. dialog.Close();
  343. };
  344. cancelButton.Click += (_, _) =>
  345. {
  346. dialog.DialogResult = false;
  347. dialog.Close();
  348. };
  349. dialog.Closed += (_, _) => timer.Stop();
  350. UpdateMessage();
  351. timer.Start();
  352. dialog.ShowDialog();
  353. return result;
  354. }
  355. private async void RebootButton_OnClick(object sender, RoutedEventArgs e)
  356. {
  357. await ExecuteSystemActionAsync(
  358. "重启设备",
  359. "设备将立即重启,当前窗口和连接可能马上中断。是否继续?",
  360. () => _agentApiService.RebootAsync(_baseAddress, _password, _localIPv4));
  361. }
  362. private async void ShutdownButton_OnClick(object sender, RoutedEventArgs e)
  363. {
  364. await ExecuteSystemActionAsync(
  365. "关闭设备",
  366. "设备将立即关机,当前窗口和连接可能马上中断。是否继续?",
  367. () => _agentApiService.ShutdownAsync(_baseAddress, _password, _localIPv4));
  368. }
  369. private async Task ExecuteSystemActionAsync(string title, string confirmMessage, Func<Task<ApiCallResult<RemoteSystemActionResponse>>> action)
  370. {
  371. if (MessageBox.Show(this, confirmMessage, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning) != MessageBoxResult.OK)
  372. {
  373. return;
  374. }
  375. var result = await action();
  376. if (!result.Success || result.Data is null)
  377. {
  378. ShowStatusMessage($"{title}失败:{result.Message}");
  379. return;
  380. }
  381. ShowStatusMessage($"{title}任务已提交:{result.Data.TaskId}。命令已发出,设备可能立即断开。");
  382. }
  383. private RemoteInterfaceConfig? BuildConfigRequest(string interfaceName)
  384. {
  385. var dhcp4 = Dhcp4CheckBox.IsChecked == true;
  386. var prefix = 0;
  387. if (!dhcp4 && string.IsNullOrWhiteSpace(NewIpTextBox.Text))
  388. {
  389. ShowStatusMessage("IP 地址不能为空。");
  390. return null;
  391. }
  392. if (!dhcp4 && !TryMaskToPrefix(NewMaskTextBox.Text, out prefix))
  393. {
  394. ShowStatusMessage("子网掩码格式不正确。");
  395. return null;
  396. }
  397. var dns = string.IsNullOrWhiteSpace(NewDnsTextBox.Text) ? Array.Empty<string>() : new[] { NewDnsTextBox.Text.Trim() };
  398. return new RemoteInterfaceConfig
  399. {
  400. Interface = interfaceName,
  401. Dhcp4 = dhcp4,
  402. IP = dhcp4 ? string.Empty : NewIpTextBox.Text.Trim(),
  403. Prefix = prefix,
  404. Gateway = dhcp4 ? string.Empty : NewGatewayTextBox.Text.Trim(),
  405. Dns = dhcp4 ? Array.Empty<string>() : dns,
  406. };
  407. }
  408. private static string FormatCurrentIp(RemoteInterfaceConfig config)
  409. {
  410. if (string.IsNullOrWhiteSpace(config.IP))
  411. {
  412. return config.Dhcp4 ? "DHCP 自动获取,暂无 IPv4" : "无";
  413. }
  414. var text = $"{config.IP}/{config.Prefix}";
  415. return config.Dhcp4 ? $"{text} (DHCP)" : text;
  416. }
  417. private static string PrefixToMask(int prefix)
  418. {
  419. if (prefix < 0 || prefix > 32)
  420. {
  421. return string.Empty;
  422. }
  423. var mask = prefix == 0 ? 0u : uint.MaxValue << (32 - prefix);
  424. return string.Join('.', new[] { (mask >> 24) & 255, (mask >> 16) & 255, (mask >> 8) & 255, mask & 255 });
  425. }
  426. private static bool TryMaskToPrefix(string maskText, out int prefix)
  427. {
  428. prefix = 0;
  429. if (string.IsNullOrWhiteSpace(maskText))
  430. {
  431. return false;
  432. }
  433. var parts = maskText.Trim().Split('.');
  434. if (parts.Length != 4)
  435. {
  436. return false;
  437. }
  438. uint mask = 0;
  439. foreach (var part in parts)
  440. {
  441. if (!byte.TryParse(part, out var octet))
  442. {
  443. return false;
  444. }
  445. mask = (mask << 8) | octet;
  446. }
  447. var seenZero = false;
  448. for (var i = 31; i >= 0; i--)
  449. {
  450. var bit = (mask & (1u << i)) != 0;
  451. if (bit && seenZero)
  452. {
  453. return false;
  454. }
  455. if (bit)
  456. {
  457. prefix++;
  458. }
  459. else
  460. {
  461. seenZero = true;
  462. }
  463. }
  464. return true;
  465. }
  466. private void ConfigInputChanged_OnChanged(object sender, TextChangedEventArgs e)
  467. {
  468. if (_suppressConfigChangeHandling)
  469. {
  470. return;
  471. }
  472. _configValidated = false;
  473. ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
  474. UpdateButtonStates();
  475. }
  476. private void ConfigModeChanged_OnChanged(object sender, RoutedEventArgs e)
  477. {
  478. if (_suppressConfigChangeHandling)
  479. {
  480. UpdateButtonStates();
  481. return;
  482. }
  483. _configValidated = false;
  484. ShowStatusMessage("配置模式已变更,请重新点击“2. 校验配置”。");
  485. UpdateButtonStates();
  486. }
  487. private void ShowStatusMessage(string message)
  488. {
  489. ApplyStatusMessageStyle(message);
  490. StatusMessageTextBlock.Text = message;
  491. StatusMessageBorder.Opacity = 0;
  492. StatusMessageBorder.Visibility = Visibility.Visible;
  493. StatusMessageBorder.BeginAnimation(OpacityProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(160)));
  494. _statusMessageCts?.Cancel();
  495. _statusMessageCts = new CancellationTokenSource();
  496. _ = HideStatusMessageAsync(_statusMessageCts.Token);
  497. }
  498. private async Task HideStatusMessageAsync(CancellationToken cancellationToken)
  499. {
  500. try
  501. {
  502. await Task.Delay(3000, cancellationToken);
  503. await Dispatcher.InvokeAsync(() =>
  504. {
  505. var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(200));
  506. animation.Completed += (_, _) =>
  507. {
  508. if (!cancellationToken.IsCancellationRequested)
  509. {
  510. StatusMessageBorder.Visibility = Visibility.Collapsed;
  511. }
  512. };
  513. StatusMessageBorder.BeginAnimation(OpacityProperty, animation);
  514. });
  515. }
  516. catch (TaskCanceledException)
  517. {
  518. }
  519. }
  520. private void ApplyStatusMessageStyle(string message)
  521. {
  522. var (background, foreground) = GetStatusMessageBrushes(message);
  523. StatusMessageBorder.Background = background;
  524. StatusMessageTextBlock.Foreground = foreground;
  525. }
  526. private static (Brush Background, Brush Foreground) GetStatusMessageBrushes(string message)
  527. {
  528. if (ContainsAny(message, "失败", "错误", "拒绝", "超时", "不能为空", "不正确", "无法"))
  529. {
  530. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B91C1C")), Brushes.White);
  531. }
  532. if (ContainsAny(message, "未发现", "请", "重试", "警告", "需要"))
  533. {
  534. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#C2410C")), Brushes.White);
  535. }
  536. if (ContainsAny(message, "成功", "已切换", "已刷新", "已读取", "已加载", "已发现", "已提交", "已回填"))
  537. {
  538. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#047857")), Brushes.White);
  539. }
  540. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#111827")), Brushes.White);
  541. }
  542. private static bool ContainsAny(string message, params string[] markers)
  543. {
  544. return markers.Any(marker => message.Contains(marker, StringComparison.Ordinal));
  545. }
  546. private static string FormatTaskStatusMessage(RemoteTaskResult task)
  547. {
  548. return task.Status switch
  549. {
  550. "success" => string.IsNullOrWhiteSpace(task.Detail) ? "配置已成功应用。" : task.Detail,
  551. "failed" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败。" : task.Detail,
  552. "rolled_back" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败,已自动回滚。" : task.Detail,
  553. _ => task.Step switch
  554. {
  555. "validating" => "正在校验配置...",
  556. "writing_netplan" => "正在写入 Linux 网络配置...",
  557. "applying" => "正在应用 Linux 网络配置...",
  558. "confirming" => string.IsNullOrWhiteSpace(task.Detail) ? "等待确认保留配置..." : task.Detail,
  559. "rolling_back" => "配置应用失败,正在自动回滚...",
  560. _ => string.IsNullOrWhiteSpace(task.Detail) ? "正在处理,请稍候..." : task.Detail,
  561. }
  562. };
  563. }
  564. private void ShowTaskCompletionDialog(RemoteTaskResult task)
  565. {
  566. var message = FormatTaskStatusMessage(task);
  567. var title = task.Status == "success" ? "应用配置成功" : "应用配置失败";
  568. var image = task.Status == "success" ? MessageBoxImage.Information : MessageBoxImage.Warning;
  569. MessageBox.Show(this, message, title, MessageBoxButton.OK, image);
  570. }
  571. private void UpdateButtonStates()
  572. {
  573. var hasSelectedInterface = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
  574. var canEdit = !_isBusy && hasSelectedInterface;
  575. RemoteTargetInterfaceComboBox.IsEnabled = !_isBusy && RemoteTargetInterfaceComboBox.Items.Count > 0;
  576. ReloadInterfaceConfigButton.IsEnabled = canEdit;
  577. ValidateConfigButton.IsEnabled = canEdit;
  578. ApplyConfigButton.IsEnabled = !_isBusy && _configValidated && hasSelectedInterface;
  579. Dhcp4CheckBox.IsEnabled = canEdit;
  580. NewIpTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
  581. NewMaskTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
  582. NewGatewayTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
  583. NewDnsTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
  584. RebootButton.IsEnabled = !_isBusy;
  585. ShutdownButton.IsEnabled = !_isBusy;
  586. }
  587. private void SetBusyState(bool isBusy, string? message = null)
  588. {
  589. _isBusy = isBusy;
  590. BusyOverlay.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
  591. BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message;
  592. UpdateButtonStates();
  593. }
  594. }