Bladeren bron

style(ui): 优化设备详情页状态提示与按钮布局

统一添加按钮居中对齐,重构状态消息显示逻辑以支持类型区分,并更新编译说明。
yangkaixiang 1 maand geleden
bovenliggende
commit
0dbb4f51d9

+ 1 - 1
AGENTS.md

@@ -2,5 +2,5 @@
 
 ## Build Rules
 
-- Windows 编译时,如果因进程占用导致编译失败,可以直接结束占用进程后重新编译。
+- Windows 编译时,如果因进程占用导致编译失败,可以直接结束占用进程后重新编译。编译结束后重新打开程序。
 - 每次编译 server 端前,需要先修改版本号。

+ 33 - 11
windows/NetworkTool.Client/DeviceDetailsWindow.xaml

@@ -166,7 +166,7 @@
                                                              </DataGridTemplateColumn>
                                                         </DataGrid.Columns>
                                                     </DataGrid>
-                                                    <Button Grid.Row="2" Margin="18,10,0,0" HorizontalAlignment="Left" MinHeight="30" Padding="12,0" Click="AddAddressButton_OnClick" Content="+ 添加 IP">
+                                                    <Button Grid.Row="2" Margin="18,10,0,0" HorizontalAlignment="Center" MinHeight="30" Padding="12,0" Click="AddAddressButton_OnClick" Content="+ 添加 IP">
                                                         <Button.Style>
                                                             <Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
                                                                 <Setter Property="IsEnabled" Value="True" />
@@ -250,7 +250,7 @@
                                                              </DataGridTemplateColumn>
                                                         </DataGrid.Columns>
                                                     </DataGrid>
-                                                    <Button Grid.Row="3" Margin="18,10,0,0" HorizontalAlignment="Left" MinHeight="30" Padding="12,0" Click="AddRouteButton_OnClick" Content="+ 添加路由">
+                                                    <Button Grid.Row="3" Margin="18,10,0,0" HorizontalAlignment="Center" MinHeight="30" Padding="12,0" Click="AddRouteButton_OnClick" Content="+ 添加路由">
                                                         <Button.Style>
                                                             <Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
                                                                 <Setter Property="Visibility" Value="Collapsed" />
@@ -285,7 +285,7 @@
                                                              </DataGridTemplateColumn>
                                                         </DataGrid.Columns>
                                                     </DataGrid>
-                                                    <Button Grid.Row="2" Margin="18,10,0,0" HorizontalAlignment="Left" MinHeight="30" Padding="12,0" Click="AddDnsButton_OnClick" Content="+ 添加 DNS" />
+                                                    <Button Grid.Row="2" Margin="18,10,0,0" HorizontalAlignment="Center" MinHeight="30" Padding="12,0" Click="AddDnsButton_OnClick" Content="+ 添加 DNS" />
                                                 </Grid>
                                             </Border>
                                         </Grid>
@@ -301,7 +301,8 @@
                             <ColumnDefinition Width="Auto" />
                         </Grid.ColumnDefinitions>
 
-                        <TextBlock VerticalAlignment="Center"
+                        <TextBlock x:Name="ConfigStateTextBlock"
+                                   VerticalAlignment="Center"
                                    FontSize="12"
                                    Foreground="#6B7280"
                                    Text="先确认所有接口配置,再校验新配置,最后一次性应用。" />
@@ -401,14 +402,35 @@
                 VerticalAlignment="Top"
                 Margin="24,12,24,0"
                 MaxWidth="860"
-                Padding="14,10"
-                Background="#111827"
+                Padding="12,10"
+                Background="White"
                 CornerRadius="10">
-            <TextBlock x:Name="StatusMessageTextBlock"
-                       FontSize="13"
-                       Foreground="White"
-                       TextWrapping="Wrap"
-                       Text="" />
+            <Border.Effect>
+                <DropShadowEffect BlurRadius="18" Direction="270" Opacity="0.16" ShadowDepth="6" Color="#111827" />
+            </Border.Effect>
+            <StackPanel Orientation="Horizontal">
+                <Border x:Name="StatusIconBorder"
+                        Width="16"
+                        Height="16"
+                        Margin="0,1,10,0"
+                        VerticalAlignment="Top"
+                        Background="#508DF8"
+                        CornerRadius="8">
+                    <TextBlock x:Name="StatusIconTextBlock"
+                               HorizontalAlignment="Center"
+                               VerticalAlignment="Center"
+                               FontSize="11"
+                               FontWeight="Bold"
+                               Foreground="White"
+                               LineHeight="16"
+                               Text="i" />
+                </Border>
+                <TextBlock x:Name="StatusMessageTextBlock"
+                           FontSize="13"
+                           Foreground="#1F2937"
+                           TextWrapping="Wrap"
+                           Text="" />
+            </StackPanel>
         </Border>
     </Grid>
 </Window>

+ 91 - 72
windows/NetworkTool.Client/DeviceDetailsWindow.xaml.cs

@@ -49,7 +49,7 @@ public partial class DeviceDetailsWindow : Window
         }
         catch (Exception ex)
         {
-            ShowStatusMessage($"读取设备信息失败:{ex.Message}");
+            ShowStatusMessage($"读取设备信息失败:{ex.Message}", StatusMessageType.Error);
             SetBusyState(false);
         }
     }
@@ -66,11 +66,11 @@ public partial class DeviceDetailsWindow : Window
         var interfaces = await _serverApiService.GetInterfacesAsync(_baseAddress, _password, _localIPv4);
         if (interfaces is null)
         {
-            ShowStatusMessage("设备已连接,但暂时无法读取 Linux 接口列表。");
+            ShowStatusMessage("设备已连接,但暂时无法读取 Linux 接口列表。", StatusMessageType.Warning);
             return;
         }
 
-        ShowStatusMessage($"当前管理接口:{interfaces.ManagementInterface}。正在读取全部接口配置。");
+        SetConfigStateMessage($"当前管理接口:{interfaces.ManagementInterface}。正在读取全部接口配置。", false);
         foreach (var info in interfaces.Interfaces)
         {
             var editor = new InterfaceEditor(info);
@@ -80,7 +80,8 @@ public partial class DeviceDetailsWindow : Window
 
         _configValidated = false;
         _configDirty = false;
-        ShowStatusMessage("已读取全部接口配置。");
+        SetConfigStateMessage("已读取全部接口配置。", false);
+        ShowStatusMessage("已读取全部接口配置。", StatusMessageType.Success);
     }
 
     private void ClearDetails()
@@ -115,7 +116,7 @@ public partial class DeviceDetailsWindow : Window
             var result = await _serverApiService.GetInterfaceConfigAsync(_baseAddress, _password, _localIPv4, editor.SystemName);
             if (!result.Success || result.Data is null)
             {
-                ShowStatusMessage($"读取目标接口 {editor.SystemName} 配置失败:{result.Message}");
+                ShowStatusMessage($"读取目标接口 {editor.SystemName} 配置失败:{result.Message}", StatusMessageType.Error);
                 return;
             }
 
@@ -181,7 +182,8 @@ public partial class DeviceDetailsWindow : Window
 
             _configValidated = false;
             _configDirty = false;
-            ShowStatusMessage("已刷新全部接口配置。");
+            SetConfigStateMessage("已刷新全部接口配置。", false);
+            ShowStatusMessage("已刷新全部接口配置。", StatusMessageType.Success);
         }
         finally
         {
@@ -224,11 +226,15 @@ public partial class DeviceDetailsWindow : Window
             {
                 var warnings = result.Data.Warnings.Count > 0 ? $" 警告:{string.Join(";", result.Data.Warnings)}" : string.Empty;
                 var errors = result.Data.Errors.Count > 0 ? $" 错误:{string.Join(";", result.Data.Errors)}" : string.Empty;
-                ShowStatusMessage(_configValidated ? $"全部接口校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}");
+                SetConfigStateMessage(_configValidated ? "配置已校验通过,可以应用。" : "配置校验未通过,请修正后重新校验。", !_configValidated);
+                ShowStatusMessage(
+                    _configValidated ? $"全部接口校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}",
+                    _configValidated ? StatusMessageType.Success : StatusMessageType.Error);
             }
             else
             {
-                ShowStatusMessage($"校验失败:{result.Message}");
+                SetConfigStateMessage("配置校验未通过,请修正后重新校验。", true);
+                ShowStatusMessage($"校验失败:{result.Message}", StatusMessageType.Error);
             }
 
             UpdateButtonStates();
@@ -259,11 +265,11 @@ public partial class DeviceDetailsWindow : Window
             var applyResult = await _serverApiService.ApplyInterfaceConfigsAsync(_baseAddress, _password, _localIPv4, request);
             if (!applyResult.Success || applyResult.Data is null)
             {
-                ShowStatusMessage($"提交配置任务失败:{applyResult.Message}");
+                ShowStatusMessage($"提交配置任务失败:{applyResult.Message}", StatusMessageType.Error);
                 return;
             }
 
-            ShowStatusMessage("配置任务已提交,正在应用并等待连通确认...");
+            ShowStatusMessage("配置任务已提交,正在应用并等待连通确认...", StatusMessageType.Info);
             await PollTaskAsync(applyResult.Data.TaskId);
         }
         finally
@@ -276,6 +282,7 @@ public partial class DeviceDetailsWindow : Window
     {
         var transientFailureCount = 0;
         var confirmationRequested = false;
+        string? lastTaskMessage = null;
         for (var i = 0; i < 20; i++)
         {
             await Task.Delay(1000);
@@ -285,17 +292,22 @@ public partial class DeviceDetailsWindow : Window
                 if (result.StatusCode is null)
                 {
                     transientFailureCount++;
-                    ShowStatusMessage($"设备连接短暂中断,正在重试({transientFailureCount})。");
+                    ShowStatusMessage($"设备连接短暂中断,正在重试({transientFailureCount})。", StatusMessageType.Warning);
                     continue;
                 }
 
-                ShowStatusMessage($"读取任务状态失败:{result.Message}");
+                ShowStatusMessage($"读取任务状态失败:{result.Message}", StatusMessageType.Error);
                 return;
             }
 
             transientFailureCount = 0;
             var task = result.Data;
-            ShowStatusMessage(FormatTaskStatusMessage(task));
+            var (taskMessage, taskMessageType) = FormatTaskStatusMessage(task);
+            if (!string.Equals(taskMessage, lastTaskMessage, StringComparison.Ordinal))
+            {
+                lastTaskMessage = taskMessage;
+                ShowStatusMessage(taskMessage, taskMessageType);
+            }
             if (task.Status == "running" && task.Step == "confirming" && !confirmationRequested)
             {
                 confirmationRequested = true;
@@ -303,17 +315,28 @@ public partial class DeviceDetailsWindow : Window
                 if (confirm)
                 {
                     var confirmResult = await _serverApiService.ConfirmApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
-                    ShowStatusMessage(confirmResult.Success ? "已发送保留配置确认。" : $"发送确认失败:{confirmResult.Message}");
+                    ShowStatusMessage(
+                        confirmResult.Success ? "已发送保留配置确认。" : $"发送确认失败:{confirmResult.Message}",
+                        confirmResult.Success ? StatusMessageType.Success : StatusMessageType.Error);
                 }
                 else
                 {
                     var cancelResult = await _serverApiService.CancelApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
-                    ShowStatusMessage(cancelResult.Success ? "已取消保留配置,正在回滚。" : $"发送取消失败:{cancelResult.Message}");
+                    ShowStatusMessage(
+                        cancelResult.Success ? "已取消保留配置,正在回滚。" : $"发送取消失败:{cancelResult.Message}",
+                        cancelResult.Success ? StatusMessageType.Warning : StatusMessageType.Error);
                 }
             }
 
             if (task.Status is "success" or "failed" or "rolled_back")
             {
+                if (task.Status == "success")
+                {
+                    _configValidated = false;
+                    _configDirty = false;
+                    SetConfigStateMessage("配置已应用,当前显示为设备最新配置。", false);
+                }
+
                 ShowTaskCompletionDialog(task);
                 foreach (var editor in _interfaces)
                 {
@@ -324,7 +347,7 @@ public partial class DeviceDetailsWindow : Window
             }
         }
 
-        ShowStatusMessage($"任务 {taskId} 轮询超时,请稍后手动刷新。");
+        ShowStatusMessage($"任务 {taskId} 轮询超时,请稍后手动刷新。", StatusMessageType.Warning);
     }
 
     private bool ShowApplyConfirmationDialog(int timeoutSeconds)
@@ -441,11 +464,11 @@ public partial class DeviceDetailsWindow : Window
         var result = await action();
         if (!result.Success || result.Data is null)
         {
-            ShowStatusMessage($"{title}失败:{result.Message}");
+            ShowStatusMessage($"{title}失败:{result.Message}", StatusMessageType.Error);
             return;
         }
 
-        ShowStatusMessage($"{title}任务已提交:{result.Data.TaskId}。命令已发出,设备可能立即断开。");
+        ShowStatusMessage($"{title}任务已提交:{result.Data.TaskId}。命令已发出,设备可能立即断开。", StatusMessageType.Success);
     }
 
     private RemoteInterfaceConfig[]? BuildConfigRequests()
@@ -465,7 +488,7 @@ public partial class DeviceDetailsWindow : Window
 
         if (result.Count == 0)
         {
-            ShowStatusMessage("接口配置不能为空。");
+            ShowStatusMessage("接口配置不能为空。", StatusMessageType.Error);
             return null;
         }
 
@@ -481,19 +504,19 @@ public partial class DeviceDetailsWindow : Window
         {
             if (editor.Addresses.All(item => string.IsNullOrWhiteSpace(item.IP) && string.IsNullOrWhiteSpace(item.Mask)))
             {
-                ShowStatusMessage($"{editor.SystemName}:IP 地址不能为空,至少需要填写一行地址。");
+                ShowStatusMessage($"{editor.SystemName}:IP 地址不能为空,至少需要填写一行地址。", StatusMessageType.Error);
                 return null;
             }
 
             if (!TryBuildAddresses(editor, out addresses, out var addressError))
             {
-                ShowStatusMessage($"{editor.SystemName}:{addressError}");
+                ShowStatusMessage($"{editor.SystemName}:{addressError}", StatusMessageType.Error);
                 return null;
             }
 
             if (!TryBuildRoutes(editor, out routes, out var routeError))
             {
-                ShowStatusMessage($"{editor.SystemName}:{routeError}");
+                ShowStatusMessage($"{editor.SystemName}:{routeError}", StatusMessageType.Error);
                 return null;
             }
         }
@@ -829,7 +852,8 @@ public partial class DeviceDetailsWindow : Window
         }
 
         _configValidated = false;
-        ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
+        _configDirty = true;
+        SetConfigStateMessage("配置已修改,需重新校验后才能应用。", true);
         UpdateButtonStates();
     }
 
@@ -843,7 +867,7 @@ public partial class DeviceDetailsWindow : Window
 
         _configValidated = false;
         _configDirty = true;
-        ShowStatusMessage("配置模式已变更,请重新点击“2. 校验配置”。");
+        SetConfigStateMessage("配置已修改,需重新校验后才能应用。", true);
         UpdateButtonStates();
     }
 
@@ -857,7 +881,7 @@ public partial class DeviceDetailsWindow : Window
 
         _configValidated = false;
         _configDirty = true;
-        ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
+        SetConfigStateMessage("配置已修改,需重新校验后才能应用。", true);
         UpdateButtonStates();
     }
 
@@ -871,7 +895,7 @@ public partial class DeviceDetailsWindow : Window
         {
             NormalizeRouteRow(route);
         }
-        MarkConfigChanged("配置内容已变更,请重新点击“2. 校验配置”。");
+        MarkConfigChanged();
     }
 
     private void DataGrid_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
@@ -897,7 +921,7 @@ public partial class DeviceDetailsWindow : Window
             return;
         }
         editor.Addresses.Add(new EditableAddress(editor) { Mask = "255.255.255.0" });
-        MarkConfigChanged("已添加 IP 地址,请填写后重新校验配置。");
+        MarkConfigChanged();
     }
 
     private void AddRouteButton_OnClick(object sender, RoutedEventArgs e)
@@ -907,7 +931,7 @@ public partial class DeviceDetailsWindow : Window
             return;
         }
         editor.Routes.Add(new EditableRoute(editor));
-        MarkConfigChanged("已添加路由,请填写后重新校验配置。");
+        MarkConfigChanged();
     }
 
     private void AddDnsButton_OnClick(object sender, RoutedEventArgs e)
@@ -917,7 +941,7 @@ public partial class DeviceDetailsWindow : Window
             return;
         }
         editor.Dns.Add(new EditableDns(editor));
-        MarkConfigChanged("已添加 DNS,请填写后重新校验配置。");
+        MarkConfigChanged();
     }
 
     private void DeleteAddressButton_OnClick(object sender, RoutedEventArgs e)
@@ -927,7 +951,7 @@ public partial class DeviceDetailsWindow : Window
             return;
         }
         address.Owner.Addresses.Remove(address);
-        MarkConfigChanged("已删除 IP 地址,请重新校验配置。");
+        MarkConfigChanged();
     }
 
     private void DeleteRouteButton_OnClick(object sender, RoutedEventArgs e)
@@ -935,7 +959,7 @@ public partial class DeviceDetailsWindow : Window
         if ((sender as FrameworkElement)?.DataContext is EditableRoute route)
         {
             route.Owner.Routes.Remove(route);
-            MarkConfigChanged("已删除路由,请重新校验配置。");
+            MarkConfigChanged();
         }
     }
 
@@ -944,11 +968,11 @@ public partial class DeviceDetailsWindow : Window
         if ((sender as FrameworkElement)?.DataContext is EditableDns dns)
         {
             dns.Owner.Dns.Remove(dns);
-            MarkConfigChanged("已删除 DNS,请重新校验配置。");
+            MarkConfigChanged();
         }
     }
 
-    private void MarkConfigChanged(string message)
+    private void MarkConfigChanged()
     {
         if (_suppressConfigChangeHandling)
         {
@@ -957,10 +981,18 @@ public partial class DeviceDetailsWindow : Window
 
         _configValidated = false;
         _configDirty = true;
-        ShowStatusMessage(message);
+        SetConfigStateMessage("配置已修改,需重新校验后才能应用。", true);
         UpdateButtonStates();
     }
 
+    private void SetConfigStateMessage(string message, bool requiresAttention)
+    {
+        ConfigStateTextBlock.Text = message;
+        ConfigStateTextBlock.Foreground = requiresAttention
+            ? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#C2410C"))
+            : new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280"));
+    }
+
     private static void NormalizeAddressRow(EditableAddress row)
     {
         var ip = row.IP.Trim();
@@ -991,9 +1023,9 @@ public partial class DeviceDetailsWindow : Window
         }
     }
 
-    private void ShowStatusMessage(string message)
+    private void ShowStatusMessage(string message, StatusMessageType type)
     {
-        ApplyStatusMessageStyle(message);
+        ApplyStatusMessageStyle(type);
         StatusMessageTextBlock.Text = message;
         StatusMessageBorder.Opacity = 0;
         StatusMessageBorder.Visibility = Visibility.Visible;
@@ -1026,60 +1058,47 @@ public partial class DeviceDetailsWindow : Window
         }
     }
 
-    private void ApplyStatusMessageStyle(string message)
+    private void ApplyStatusMessageStyle(StatusMessageType type)
     {
-        var (background, foreground) = GetStatusMessageBrushes(message);
-        StatusMessageBorder.Background = background;
-        StatusMessageTextBlock.Foreground = foreground;
+        var (background, icon) = GetStatusMessageVisuals(type);
+        StatusIconBorder.Background = background;
+        StatusIconTextBlock.Text = icon;
+        StatusMessageTextBlock.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1F2937"));
     }
 
-    private static (Brush Background, Brush Foreground) GetStatusMessageBrushes(string message)
+    private static (Brush Background, string Icon) GetStatusMessageVisuals(StatusMessageType type)
     {
-        if (ContainsAny(message, "失败", "错误", "拒绝", "超时", "不能为空", "不正确", "无法"))
+        return type switch
         {
-            return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B91C1C")), Brushes.White);
-        }
-
-        if (ContainsAny(message, "未发现", "请", "重试", "警告", "需要"))
-        {
-            return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#C2410C")), Brushes.White);
-        }
-
-        if (ContainsAny(message, "成功", "已切换", "已刷新", "已读取", "已加载", "已发现", "已提交", "已回填"))
-        {
-            return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#047857")), Brushes.White);
-        }
-
-        return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#111827")), Brushes.White);
-    }
-
-    private static bool ContainsAny(string message, params string[] markers)
-    {
-        return markers.Any(marker => message.Contains(marker, StringComparison.Ordinal));
+            StatusMessageType.Success => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#27C346")), "✓"),
+            StatusMessageType.Error => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F76965")), "×"),
+            StatusMessageType.Warning => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF9626")), "!"),
+            _ => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#508DF8")), "i"),
+        };
     }
 
-    private static string FormatTaskStatusMessage(RemoteTaskResult task)
+    private static (string Message, StatusMessageType Type) FormatTaskStatusMessage(RemoteTaskResult task)
     {
         return task.Status switch
         {
-            "success" => string.IsNullOrWhiteSpace(task.Detail) ? "配置已成功应用。" : task.Detail,
-            "failed" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败。" : task.Detail,
-            "rolled_back" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败,已自动回滚。" : task.Detail,
+            "success" => (string.IsNullOrWhiteSpace(task.Detail) ? "配置已成功应用。" : task.Detail, StatusMessageType.Success),
+            "failed" => (string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败。" : task.Detail, StatusMessageType.Error),
+            "rolled_back" => (string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败,已自动回滚。" : task.Detail, StatusMessageType.Error),
             _ => task.Step switch
             {
-                "validating" => "正在校验配置...",
-                "writing_netplan" => "正在写入 Linux 网络配置...",
-                "applying" => "正在应用 Linux 网络配置...",
-                "confirming" => string.IsNullOrWhiteSpace(task.Detail) ? "等待确认保留配置..." : task.Detail,
-                "rolling_back" => "配置应用失败,正在自动回滚...",
-                _ => string.IsNullOrWhiteSpace(task.Detail) ? "正在处理,请稍候..." : task.Detail,
+                "validating" => ("正在校验配置...", StatusMessageType.Info),
+                "writing_netplan" => ("正在写入 Linux 网络配置...", StatusMessageType.Info),
+                "applying" => ("正在应用 Linux 网络配置...", StatusMessageType.Info),
+                "confirming" => (string.IsNullOrWhiteSpace(task.Detail) ? "等待确认保留配置..." : task.Detail, StatusMessageType.Warning),
+                "rolling_back" => ("配置应用失败,正在自动回滚...", StatusMessageType.Warning),
+                _ => (string.IsNullOrWhiteSpace(task.Detail) ? "正在处理,请稍候..." : task.Detail, StatusMessageType.Info),
             }
         };
     }
 
     private void ShowTaskCompletionDialog(RemoteTaskResult task)
     {
-        var message = FormatTaskStatusMessage(task);
+        var (message, _) = FormatTaskStatusMessage(task);
         var title = task.Status == "success" ? "应用配置成功" : "应用配置失败";
         var image = task.Status == "success" ? MessageBoxImage.Information : MessageBoxImage.Warning;
         MessageBox.Show(this, message, title, MessageBoxButton.OK, image);

+ 85 - 29
windows/NetworkTool.Client/MainWindow.xaml

@@ -102,16 +102,38 @@
 
                     <Border Padding="16" Background="#F9FAFB" CornerRadius="10">
                         <StackPanel>
-                            <TextBlock FontSize="13" FontWeight="SemiBold" Foreground="#111827" Text="本机网卡" />
-                            <TextBlock Margin="0,12,0,0"
-                                       FontSize="13"
-                                       Foreground="#374151"
-                                       Text="本机有线网卡" />
-                            <Grid Margin="0,8,0,0">
+                            <Grid>
                                 <Grid.ColumnDefinitions>
+                                    <ColumnDefinition Width="Auto" />
+                                    <ColumnDefinition Width="Auto" />
                                     <ColumnDefinition Width="*" />
                                     <ColumnDefinition Width="Auto" />
                                 </Grid.ColumnDefinitions>
+                                <TextBlock VerticalAlignment="Center"
+                                           FontSize="13"
+                                           FontWeight="SemiBold"
+                                           Foreground="#111827"
+                                           Text="本机网卡" />
+                                <CheckBox x:Name="ShowUsableAdaptersOnlyCheckBox"
+                                          Grid.Column="1"
+                                          Margin="16,0,0,0"
+                                          VerticalAlignment="Center"
+                                          VerticalContentAlignment="Center"
+                                          Checked="ShowUsableAdaptersOnlyCheckBox_OnChanged"
+                                          IsChecked="True"
+                                          Unchecked="ShowUsableAdaptersOnlyCheckBox_OnChanged"
+                                          Content="仅显示可用网卡" />
+                                <Button x:Name="RefreshAdaptersButton"
+                                        Grid.Column="3"
+                                        MinHeight="32"
+                                        Padding="14,0"
+                                        Click="RefreshAdaptersButton_OnClick"
+                                        Content="刷新" />
+                            </Grid>
+                            <Grid Margin="0,12,0,0">
+                                <Grid.ColumnDefinitions>
+                                    <ColumnDefinition Width="*" />
+                                </Grid.ColumnDefinitions>
                                 <ComboBox x:Name="AdapterComboBox"
                                           MinHeight="36"
                                           VerticalContentAlignment="Center"
@@ -133,13 +155,6 @@
                                         </DataTemplate>
                                     </ComboBox.ItemTemplate>
                                 </ComboBox>
-                                <Button x:Name="RefreshAdaptersButton"
-                                        Grid.Column="1"
-                                        Margin="8,0,0,0"
-                                        MinHeight="36"
-                                        Padding="14,0"
-                                        Click="RefreshAdaptersButton_OnClick"
-                                        Content="刷新" />
                             </Grid>
                         </StackPanel>
                     </Border>
@@ -170,16 +185,9 @@
                                         Content="重新搜索设备" />
                             </Grid>
 
-                            <TextBlock x:Name="DiscoveryStateTextBlock"
-                                       Grid.Row="1"
-                                       Margin="0,10,0,0"
-                                       Foreground="#4B5563"
-                                       Text="选择网卡后会自动搜索设备。"
-                                       TextWrapping="Wrap" />
-
                             <ListView x:Name="DiscoveredDevicesListView"
-                                      Grid.Row="2"
-                                      Margin="0,12,0,0"
+                                       Grid.Row="2"
+                                       Margin="0,12,0,0"
                                       MinHeight="220"
                                       Style="{StaticResource DiscoveryListViewStyle}"
                                       ItemContainerStyle="{StaticResource DiscoveryListViewItemStyle}"
@@ -217,6 +225,33 @@
             </Grid>
         </Border>
 
+        <Border x:Name="BusyOverlay"
+                Visibility="Collapsed"
+                Panel.ZIndex="90"
+                Background="#80F5F7FB">
+            <Border HorizontalAlignment="Center"
+                    VerticalAlignment="Center"
+                    Padding="18,16"
+                    Background="White"
+                    CornerRadius="12">
+                <Border.Effect>
+                    <DropShadowEffect BlurRadius="18" Direction="270" Opacity="0.12" ShadowDepth="6" Color="#111827" />
+                </Border.Effect>
+                <StackPanel>
+                    <ProgressBar Width="220"
+                                 Height="6"
+                                 IsIndeterminate="True" />
+                    <TextBlock x:Name="BusyMessageTextBlock"
+                               Margin="0,12,0,0"
+                               HorizontalAlignment="Center"
+                               FontSize="13"
+                               FontWeight="SemiBold"
+                               Foreground="#111827"
+                               Text="正在处理,请稍候..." />
+                </StackPanel>
+            </Border>
+        </Border>
+
         <Border x:Name="StatusMessageBorder"
                 Visibility="Collapsed"
                 Panel.ZIndex="100"
@@ -224,14 +259,35 @@
                 VerticalAlignment="Top"
                 Margin="24,16,24,0"
                 MaxWidth="760"
-                Padding="14,10"
-                Background="#111827"
+                Padding="12,10"
+                Background="White"
                 CornerRadius="10">
-            <TextBlock x:Name="StatusTextBlock"
-                       FontSize="13"
-                       Foreground="White"
-                       TextWrapping="Wrap"
-                       Text="" />
+            <Border.Effect>
+                <DropShadowEffect BlurRadius="18" Direction="270" Opacity="0.16" ShadowDepth="6" Color="#111827" />
+            </Border.Effect>
+            <StackPanel Orientation="Horizontal">
+                <Border x:Name="StatusIconBorder"
+                        Width="16"
+                        Height="16"
+                        Margin="0,1,10,0"
+                        VerticalAlignment="Top"
+                        Background="#508DF8"
+                        CornerRadius="8">
+                    <TextBlock x:Name="StatusIconTextBlock"
+                               HorizontalAlignment="Center"
+                               VerticalAlignment="Center"
+                               FontSize="11"
+                               FontWeight="Bold"
+                               Foreground="White"
+                               LineHeight="16"
+                               Text="i" />
+                </Border>
+                <TextBlock x:Name="StatusTextBlock"
+                           FontSize="13"
+                           Foreground="#1F2937"
+                           TextWrapping="Wrap"
+                           Text="" />
+            </StackPanel>
         </Border>
     </Grid>
 </Window>

+ 68 - 71
windows/NetworkTool.Client/MainWindow.xaml.cs

@@ -16,6 +16,7 @@ public partial class MainWindow : Window
     private readonly PasswordStoreService _passwordStoreService = new();
     private readonly DiscoveryService _discoveryService = new();
     private readonly ServerApiService _serverApiService = new();
+    private IReadOnlyList<AdapterInfo> _allAdapters = [];
     private IReadOnlyList<AdapterInfo> _adapters = [];
     private IReadOnlyList<DiscoveredDevice> _discoveredDevices = [];
     private bool _isBusy;
@@ -34,25 +35,37 @@ public partial class MainWindow : Window
 
     private void LoadInitialState()
     {
-        _adapters = _networkAdapterService.GetEthernetAdapters();
-        _adapters = _adapters
+        _allAdapters = _networkAdapterService.GetAdapters();
+        ApplyAdapterFilter();
+        UpdateButtonStates();
+    }
+
+    private void ApplyAdapterFilter(string? selectedAdapterId = null)
+    {
+        _adapters = _allAdapters
+            .Where(adapter => ShowUsableAdaptersOnlyCheckBox.IsChecked != true || IsUsableAdapter(adapter))
             .OrderByDescending(adapter => adapter.RecommendationScore)
             .ThenBy(adapter => adapter.Name)
             .ToList();
         AdapterComboBox.ItemsSource = _adapters;
 
-        var recommendedAdapter = _networkAdapterService.GetRecommendedAdapter(_adapters);
+        var selected = selectedAdapterId is null
+            ? _networkAdapterService.GetRecommendedAdapter(_adapters)
+            : _adapters.FirstOrDefault(adapter => adapter.Id == selectedAdapterId) ?? _networkAdapterService.GetRecommendedAdapter(_adapters);
 
-        if (recommendedAdapter is not null)
+        if (selected is not null)
         {
-            AdapterComboBox.SelectedItem = recommendedAdapter;
+            AdapterComboBox.SelectedItem = selected;
         }
         else if (_adapters.Count > 0)
         {
             AdapterComboBox.SelectedIndex = 0;
         }
+    }
 
-        UpdateButtonStates();
+    private static bool IsUsableAdapter(AdapterInfo adapter)
+    {
+        return adapter.HasLink && !string.IsNullOrWhiteSpace(adapter.IPv4Address);
     }
 
     private void AdapterComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -60,7 +73,7 @@ public partial class MainWindow : Window
         if (AdapterComboBox.SelectedItem is not AdapterInfo adapter)
         {
             ClearDiscoveredDevices();
-            SetStatus("请选择一块有线网卡。", false);
+            SetStatus("请选择一块网卡。", StatusMessageType.Warning, false);
             UpdateButtonStates();
             return;
         }
@@ -68,7 +81,7 @@ public partial class MainWindow : Window
         if (!adapter.HasLink)
         {
             ClearDiscoveredDevices();
-            SetStatus("当前网卡未检测到链路,请检查网线连接。", true);
+            SetStatus("当前网卡未检测到链路,请检查网线连接。", StatusMessageType.Warning, true);
             UpdateButtonStates();
             return;
         }
@@ -79,15 +92,15 @@ public partial class MainWindow : Window
 
     private async void RefreshAdaptersButton_OnClick(object sender, RoutedEventArgs e)
     {
-        SetBusyState(true);
+        SetBusyState(true, "正在刷新本机网卡...");
         try
         {
             RefreshAdapters();
-            SetStatus("已刷新本机网卡。", true);
+            SetStatus("已刷新本机网卡。", StatusMessageType.Success, true);
         }
         catch (Exception ex)
         {
-            SetStatus($"刷新本机网卡失败:{ex.Message}", true);
+            SetStatus($"刷新本机网卡失败:{ex.Message}", StatusMessageType.Error, true);
             MessageBox.Show(this, ex.Message, "刷新失败", MessageBoxButton.OK, MessageBoxImage.Error);
         }
         finally
@@ -109,10 +122,22 @@ public partial class MainWindow : Window
         }
         else
         {
-            SetStatus("请先选择一块网卡。", true);
+            SetStatus("请先选择一块网卡。", StatusMessageType.Warning, true);
         }
     }
 
+    private void ShowUsableAdaptersOnlyCheckBox_OnChanged(object sender, RoutedEventArgs e)
+    {
+        if (AdapterComboBox is null || ShowUsableAdaptersOnlyCheckBox is null)
+        {
+            return;
+        }
+
+        var selectedAdapterId = (AdapterComboBox.SelectedItem as AdapterInfo)?.Id;
+        ApplyAdapterFilter(selectedAdapterId);
+        UpdateButtonStates();
+    }
+
     private async Task SearchDevicesAsync(AdapterInfo adapter)
     {
         if (_isBusy)
@@ -123,20 +148,19 @@ public partial class MainWindow : Window
         if (string.IsNullOrWhiteSpace(adapter.IPv4Address))
         {
             ClearDiscoveredDevices();
-            SetStatus("当前网卡没有可用 IPv4,无法搜索设备。", true);
+            SetStatus("当前网卡没有可用 IPv4,无法搜索设备。", StatusMessageType.Error, true);
             return;
         }
 
         if (!adapter.HasLink)
         {
             ClearDiscoveredDevices();
-            SetStatus("当前网卡未检测到链路,请检查网线连接。", true);
+            SetStatus("当前网卡未检测到链路,请检查网线连接。", StatusMessageType.Warning, true);
             return;
         }
 
-        SetBusyState(true);
+        SetBusyState(true, "正在搜索设备...");
         ClearDiscoveredDevices();
-        SetStatus("正在通过当前网卡广播搜索设备。", true);
         await Dispatcher.InvokeAsync(() => { }, System.Windows.Threading.DispatcherPriority.Render);
 
         try
@@ -145,15 +169,15 @@ public partial class MainWindow : Window
             DiscoveredDevicesListView.ItemsSource = _discoveredDevices;
             if (_discoveredDevices.Count == 0)
             {
-                SetStatus("未发现 169.254 开头的设备 IP,请确认网卡、网线、远端服务和维护网段配置。", true);
+                SetStatus("未发现 169.254 开头的设备 IP,请确认网卡、网线、远端服务和维护网段配置。", StatusMessageType.Warning, true);
                 return;
             }
 
-            SetStatus($"已发现 {_discoveredDevices.Count} 台设备,请双击 IP 连接。", true);
+            SetStatus($"已发现 {_discoveredDevices.Count} 台设备,请双击 IP 连接。", StatusMessageType.Success, true);
         }
         catch (Exception ex)
         {
-            SetStatus($"搜索设备失败:{ex.Message}", true);
+            SetStatus($"搜索设备失败:{ex.Message}", StatusMessageType.Error, true);
             MessageBox.Show(this, ex.Message, "搜索设备失败", MessageBoxButton.OK, MessageBoxImage.Error);
         }
         finally
@@ -181,28 +205,14 @@ public partial class MainWindow : Window
 
     private void RefreshAdapters(string? selectedAdapterId = null)
     {
-        _adapters = _networkAdapterService.GetEthernetAdapters();
-        _adapters = _adapters
-            .OrderByDescending(adapter => adapter.RecommendationScore)
-            .ThenBy(adapter => adapter.Name)
-            .ToList();
-        AdapterComboBox.ItemsSource = _adapters;
-
-        var selected = selectedAdapterId is null
-            ? _networkAdapterService.GetRecommendedAdapter(_adapters)
-            : _adapters.FirstOrDefault(adapter => adapter.Id == selectedAdapterId) ?? _networkAdapterService.GetRecommendedAdapter(_adapters);
-
-        if (selected is not null)
-        {
-            AdapterComboBox.SelectedItem = selected;
-        }
+        _allAdapters = _networkAdapterService.GetAdapters();
+        ApplyAdapterFilter(selectedAdapterId);
     }
 
     private void ClearDiscoveredDevices()
     {
         _discoveredDevices = [];
         DiscoveredDevicesListView.ItemsSource = _discoveredDevices;
-        DiscoveryStateTextBlock.Text = "选择网卡后会自动搜索设备。";
     }
 
     private async void DiscoveredDevicesListView_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
@@ -246,13 +256,12 @@ public partial class MainWindow : Window
 
         if (string.IsNullOrWhiteSpace(password))
         {
-            SetStatus("请输入管理密码。", true);
+            SetStatus("请输入管理密码。", StatusMessageType.Warning, true);
             return;
         }
 
         var selectedAdapter = AdapterComboBox.SelectedItem as AdapterInfo;
-        SetBusyState(true);
-        SetStatus($"正在连接 {device.Lan2Ip}。", true);
+        SetBusyState(true, $"正在连接 {device.Lan2Ip}...");
 
         try
         {
@@ -261,23 +270,23 @@ public partial class MainWindow : Window
             if (result.Success)
             {
                 SavePasswordForDevice(device, password);
-                SetStatus("连接成功。", true);
+                SetStatus("连接成功。", StatusMessageType.Success, true);
                 OpenDeviceDetailsWindow(baseAddress, selectedAdapter?.IPv4Address ?? string.Empty, password);
                 return;
             }
 
             if (result.StatusCode == 401)
             {
-                SetStatus("管理密码错误,请确认密码是否正确。", true);
+                SetStatus("管理密码错误,请确认密码是否正确。", StatusMessageType.Error, true);
                 MessageBox.Show(this, "管理密码校验失败,请确认密码是否正确。", "密码错误", MessageBoxButton.OK, MessageBoxImage.Warning);
                 return;
             }
 
-            SetStatus($"设备已发现,但 HTTP 验证失败:{result.Message}", true);
+            SetStatus($"设备已发现,但 HTTP 验证失败:{result.Message}", StatusMessageType.Error, true);
         }
         catch (Exception ex)
         {
-            SetStatus($"连接失败:{ex.Message}", true);
+            SetStatus($"连接失败:{ex.Message}", StatusMessageType.Error, true);
             MessageBox.Show(this, ex.Message, "连接失败", MessageBoxButton.OK, MessageBoxImage.Error);
         }
         finally
@@ -324,10 +333,9 @@ public partial class MainWindow : Window
         return device.DeviceId;
     }
 
-    private void SetStatus(string message, bool addLog)
+    private void SetStatus(string message, StatusMessageType type, bool addLog)
     {
-        ApplyStatusMessageStyle(message);
-        DiscoveryStateTextBlock.Text = message;
+        ApplyStatusMessageStyle(type);
         StatusTextBlock.Text = message;
         StatusMessageBorder.Opacity = 0;
         StatusMessageBorder.Visibility = Visibility.Visible;
@@ -361,41 +369,30 @@ public partial class MainWindow : Window
         }
     }
 
-    private void ApplyStatusMessageStyle(string message)
+    private void ApplyStatusMessageStyle(StatusMessageType type)
     {
-        var (background, foreground) = GetStatusMessageBrushes(message);
-        StatusMessageBorder.Background = background;
-        StatusTextBlock.Foreground = foreground;
+        var (background, icon) = GetStatusMessageVisuals(type);
+        StatusIconBorder.Background = background;
+        StatusIconTextBlock.Text = icon;
+        StatusTextBlock.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1F2937"));
     }
 
-    private static (Brush Background, Brush Foreground) GetStatusMessageBrushes(string message)
+    private static (Brush Background, string Icon) GetStatusMessageVisuals(StatusMessageType type)
     {
-        if (ContainsAny(message, "失败", "错误", "拒绝", "超时", "不能为空", "不正确", "无法"))
-        {
-            return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B91C1C")), Brushes.White);
-        }
-
-        if (ContainsAny(message, "未发现", "请", "重试", "警告", "需要"))
-        {
-            return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#C2410C")), Brushes.White);
-        }
-
-        if (ContainsAny(message, "成功", "已切换", "已刷新", "已读取", "已加载", "已发现", "已提交", "已回填"))
+        return type switch
         {
-            return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#047857")), Brushes.White);
-        }
-
-        return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#111827")), Brushes.White);
-    }
-
-    private static bool ContainsAny(string message, params string[] markers)
-    {
-        return markers.Any(marker => message.Contains(marker, StringComparison.Ordinal));
+            StatusMessageType.Success => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#27C346")), "✓"),
+            StatusMessageType.Error => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F76965")), "×"),
+            StatusMessageType.Warning => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF9626")), "!"),
+            _ => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#508DF8")), "i"),
+        };
     }
 
-    private void SetBusyState(bool isBusy)
+    private void SetBusyState(bool isBusy, string? message = null)
     {
         _isBusy = isBusy;
+        BusyOverlay.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
+        BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message;
         AdapterComboBox.IsEnabled = !isBusy;
         RefreshAdaptersButton.IsEnabled = !isBusy;
         SearchDevicesButton.IsEnabled = !isBusy && AdapterComboBox.SelectedItem is AdapterInfo adapter && adapter.HasLink;

+ 2 - 14
windows/NetworkTool.Client/Services/NetworkAdapterService.cs

@@ -8,11 +8,10 @@ namespace NetworkTool.Client.Services;
 
 public sealed class NetworkAdapterService
 {
-    public IReadOnlyList<AdapterInfo> GetEthernetAdapters()
+    public IReadOnlyList<AdapterInfo> GetAdapters()
     {
         return NetworkInterface
             .GetAllNetworkInterfaces()
-            .Where(IsSupportedEthernetAdapter)
             .Select(BuildAdapterInfo)
             .OrderByDescending(adapter => adapter.RecommendationScore)
             .ThenBy(adapter => adapter.Name)
@@ -27,17 +26,6 @@ public sealed class NetworkAdapterService
             .FirstOrDefault();
     }
 
-    private static bool IsSupportedEthernetAdapter(NetworkInterface adapter)
-    {
-        if (adapter.NetworkInterfaceType is not NetworkInterfaceType.Ethernet and not NetworkInterfaceType.GigabitEthernet)
-        {
-            return false;
-        }
-
-        return adapter.Description.Contains("VMware", System.StringComparison.OrdinalIgnoreCase)
-            || !adapter.Description.Contains("Virtual", System.StringComparison.OrdinalIgnoreCase);
-    }
-
     private static AdapterInfo BuildAdapterInfo(NetworkInterface adapter)
     {
         var ipv4Address = adapter
@@ -103,7 +91,7 @@ public sealed class NetworkAdapterService
                 return ("推荐", "已连接且当前 IPv4 为 169.254 网段,最像初始化直连网卡。");
             }
 
-            return ("推荐", "已检测到已连接的有线网卡,适合作为初始化连接口。");
+            return ("推荐", "已检测到已连接的网卡,适合作为初始化连接口。");
         }
 
         if (score >= 40)

+ 9 - 0
windows/NetworkTool.Client/StatusMessageType.cs

@@ -0,0 +1,9 @@
+namespace NetworkTool.Client;
+
+internal enum StatusMessageType
+{
+    Info,
+    Success,
+    Warning,
+    Error,
+}