Bladeren bron

refactor(ui): 重构设备详情状态提示为顶部浮动横幅

移除固定校验文本框,新增支持自动隐藏及颜色区分(成功/警告/错误)的浮动状态消息组件,优化交互反馈体验。
yangkaixiang 1 maand geleden
bovenliggende
commit
d91d4d45b3

+ 18 - 6
windows/QuickIP.Client/DeviceDetailsWindow.xaml

@@ -17,6 +17,23 @@
             <RowDefinition Height="*" />
         </Grid.RowDefinitions>
 
+        <Border x:Name="StatusMessageBorder"
+                Visibility="Collapsed"
+                Panel.ZIndex="100"
+                HorizontalAlignment="Center"
+                VerticalAlignment="Top"
+                Margin="24,12,24,0"
+                MaxWidth="860"
+                Padding="14,10"
+                Background="#111827"
+                CornerRadius="10">
+            <TextBlock x:Name="StatusMessageTextBlock"
+                       FontSize="13"
+                       Foreground="White"
+                       TextWrapping="Wrap"
+                       Text="" />
+        </Border>
+
         <TextBlock FontSize="22"
                    FontWeight="SemiBold"
                    Foreground="#111827"
@@ -102,7 +119,6 @@
                     <RowDefinition Height="Auto" />
                     <RowDefinition Height="Auto" />
                     <RowDefinition Height="Auto" />
-                    <RowDefinition Height="Auto" />
                 </Grid.RowDefinitions>
 
                 <TextBlock FontSize="13" FontWeight="SemiBold" Foreground="#111827" Text="新配置" />
@@ -132,11 +148,7 @@
                     </StackPanel>
                 </UniformGrid>
 
-                <Border Grid.Row="3" Margin="0,12,0,0" Padding="10" Background="#EEF2FF" CornerRadius="10">
-                    <TextBlock x:Name="ConfigValidationTextBlock" FontSize="12" Foreground="#3730A3" TextWrapping="Wrap" Text="点击 1/2/3 按顺序操作:先读取当前配置,再校验,最后应用。" />
-                </Border>
-
-                <Border Grid.Row="4" Margin="0,12,0,0" Padding="12" Background="#FEF2F2" CornerRadius="10">
+                <Border Grid.Row="3" Margin="0,12,0,0" Padding="12" Background="#FEF2F2" CornerRadius="10">
                     <Grid>
                         <Grid.ColumnDefinitions>
                             <ColumnDefinition Width="*" />

+ 86 - 17
windows/QuickIP.Client/DeviceDetailsWindow.xaml.cs

@@ -1,6 +1,8 @@
 using System.Globalization;
 using System.Windows;
 using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
 using QuickIP.Client.Models;
 using QuickIP.Client.Services;
 
@@ -14,6 +16,7 @@ public partial class DeviceDetailsWindow : Window
     private readonly string _password;
     private bool _configValidated;
     private bool _suppressConfigChangeHandling;
+    private CancellationTokenSource? _statusMessageCts;
 
     public DeviceDetailsWindow(string baseAddress, string localIPv4, string password)
     {
@@ -76,7 +79,6 @@ public partial class DeviceDetailsWindow : Window
         NewMaskTextBox.Text = string.Empty;
         NewGatewayTextBox.Text = string.Empty;
         NewDnsTextBox.Text = string.Empty;
-        ConfigValidationTextBlock.Text = "点击 1/2/3 按顺序操作:先读取当前配置,再校验,最后应用。";
         _configValidated = false;
     }
 
@@ -99,7 +101,7 @@ public partial class DeviceDetailsWindow : Window
             RemoteConfigIpTextBlock.Text = "读取失败";
             RemoteConfigGatewayTextBlock.Text = "读取失败";
             RemoteConfigDnsTextBlock.Text = "读取失败";
-            ConfigValidationTextBlock.Text = $"读取目标接口 {interfaceName} 配置失败:{result.Message}";
+            ShowStatusMessage($"读取目标接口 {interfaceName} 配置失败:{result.Message}");
             return;
         }
 
@@ -115,7 +117,7 @@ public partial class DeviceDetailsWindow : Window
         NewDnsTextBox.Text = config.Dns.FirstOrDefault() ?? string.Empty;
         _suppressConfigChangeHandling = false;
         _configValidated = false;
-        ConfigValidationTextBlock.Text = "已回填目标接口当前配置,可直接修改后校验。";
+        ShowStatusMessage("已回填目标接口当前配置,可直接修改后校验。");
         UpdateButtonStates();
     }
 
@@ -146,14 +148,14 @@ 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;
-            ConfigValidationTextBlock.Text = result.Success ? $"校验通过。{warnings}" : $"校验失败。{errors}{warnings}";
+            ShowStatusMessage(result.Success ? $"校验通过。{warnings}" : $"校验失败。{errors}{warnings}");
         }
         else
         {
-            ConfigValidationTextBlock.Text = $"校验失败:{result.Message}";
+            ShowStatusMessage($"校验失败:{result.Message}");
         }
 
-        ConfigValidationTextBlock.Text = _configValidated ? "配置已通过校验,可提交应用。" : "当前配置尚未通过校验。";
+        ShowStatusMessage(_configValidated ? "配置已通过校验,可提交应用。" : "当前配置尚未通过校验。");
         UpdateButtonStates();
     }
 
@@ -183,11 +185,11 @@ public partial class DeviceDetailsWindow : Window
         var applyResult = await _agentApiService.ApplyInterfaceConfigAsync(_baseAddress, _password, _localIPv4, request);
         if (!applyResult.Success || applyResult.Data is null)
         {
-            ConfigValidationTextBlock.Text = $"提交配置任务失败:{applyResult.Message}";
+            ShowStatusMessage($"提交配置任务失败:{applyResult.Message}");
             return;
         }
 
-        ConfigValidationTextBlock.Text = $"配置任务已提交:{applyResult.Data.TaskId},正在轮询状态。";
+        ShowStatusMessage($"配置任务已提交:{applyResult.Data.TaskId},正在轮询状态。");
         await PollTaskAsync(applyResult.Data.TaskId);
     }
 
@@ -203,17 +205,17 @@ public partial class DeviceDetailsWindow : Window
                 if (result.StatusCode is null)
                 {
                     transientFailureCount++;
-                    ConfigValidationTextBlock.Text = $"任务 {taskId} 轮询中,检测到短暂断连,正在重试({transientFailureCount})。";
+                    ShowStatusMessage($"任务 {taskId} 轮询中,检测到短暂断连,正在重试({transientFailureCount})。");
                     continue;
                 }
 
-                ConfigValidationTextBlock.Text = $"读取任务状态失败:{result.Message}";
+                ShowStatusMessage($"读取任务状态失败:{result.Message}");
                 return;
             }
 
             transientFailureCount = 0;
             var task = result.Data;
-            ConfigValidationTextBlock.Text = $"任务 {task.TaskId} / {task.Status} / {task.Step} / {task.Detail}";
+            ShowStatusMessage($"任务 {task.TaskId} / {task.Status} / {task.Step} / {task.Detail}");
             if (task.Status is "success" or "failed" or "rolled_back")
             {
                 if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
@@ -225,7 +227,7 @@ public partial class DeviceDetailsWindow : Window
             }
         }
 
-        ConfigValidationTextBlock.Text = $"任务 {taskId} 轮询超时,请稍后手动刷新。";
+        ShowStatusMessage($"任务 {taskId} 轮询超时,请稍后手动刷新。");
     }
 
     private async void RebootButton_OnClick(object sender, RoutedEventArgs e)
@@ -254,24 +256,24 @@ public partial class DeviceDetailsWindow : Window
         var result = await action();
         if (!result.Success || result.Data is null)
         {
-            ConfigValidationTextBlock.Text = $"{title}失败:{result.Message}";
+            ShowStatusMessage($"{title}失败:{result.Message}");
             return;
         }
 
-        ConfigValidationTextBlock.Text = $"{title}任务已提交:{result.Data.TaskId}。命令已发出,设备可能立即断开。";
+        ShowStatusMessage($"{title}任务已提交:{result.Data.TaskId}。命令已发出,设备可能立即断开。");
     }
 
     private RemoteInterfaceConfig? BuildConfigRequest(string interfaceName)
     {
         if (string.IsNullOrWhiteSpace(NewIpTextBox.Text))
         {
-            ConfigValidationTextBlock.Text = "IP 地址不能为空。";
+            ShowStatusMessage("IP 地址不能为空。");
             return null;
         }
 
         if (!TryMaskToPrefix(NewMaskTextBox.Text, out var prefix))
         {
-            ConfigValidationTextBlock.Text = "子网掩码格式不正确。";
+            ShowStatusMessage("子网掩码格式不正确。");
             return null;
         }
 
@@ -352,10 +354,77 @@ public partial class DeviceDetailsWindow : Window
         }
 
         _configValidated = false;
-        ConfigValidationTextBlock.Text = "配置内容已变更,请重新点击“2. 校验配置”。";
+        ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
         UpdateButtonStates();
     }
 
+    private void ShowStatusMessage(string message)
+    {
+        ApplyStatusMessageStyle(message);
+        StatusMessageTextBlock.Text = message;
+        StatusMessageBorder.Opacity = 0;
+        StatusMessageBorder.Visibility = Visibility.Visible;
+        StatusMessageBorder.BeginAnimation(OpacityProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(160)));
+        _statusMessageCts?.Cancel();
+        _statusMessageCts = new CancellationTokenSource();
+        _ = HideStatusMessageAsync(_statusMessageCts.Token);
+    }
+
+    private async Task HideStatusMessageAsync(CancellationToken cancellationToken)
+    {
+        try
+        {
+            await Task.Delay(3000, cancellationToken);
+            await Dispatcher.InvokeAsync(() =>
+            {
+                var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(200));
+                animation.Completed += (_, _) =>
+                {
+                    if (!cancellationToken.IsCancellationRequested)
+                    {
+                        StatusMessageBorder.Visibility = Visibility.Collapsed;
+                    }
+                };
+                StatusMessageBorder.BeginAnimation(OpacityProperty, animation);
+            });
+        }
+        catch (TaskCanceledException)
+        {
+        }
+    }
+
+    private void ApplyStatusMessageStyle(string message)
+    {
+        var (background, foreground) = GetStatusMessageBrushes(message);
+        StatusMessageBorder.Background = background;
+        StatusMessageTextBlock.Foreground = foreground;
+    }
+
+    private static (Brush Background, Brush Foreground) GetStatusMessageBrushes(string message)
+    {
+        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 (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));
+    }
+
     private void UpdateButtonStates()
     {
         ReloadInterfaceConfigButton.IsEnabled = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;

+ 35 - 25
windows/QuickIP.Client/MainWindow.xaml

@@ -1,8 +1,8 @@
 <Window x:Class="QuickIP.Client.MainWindow"
-         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+          xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
          mc:Ignorable="d"
          Title="QuickIP"
          Height="680"
@@ -16,6 +16,23 @@
             <RowDefinition Height="Auto" />
         </Grid.RowDefinitions>
 
+        <Border x:Name="StatusMessageBorder"
+                Visibility="Collapsed"
+                Panel.ZIndex="100"
+                HorizontalAlignment="Center"
+                VerticalAlignment="Top"
+                Margin="24,16,24,0"
+                MaxWidth="760"
+                Padding="14,10"
+                Background="#111827"
+                CornerRadius="10">
+            <TextBlock x:Name="StatusTextBlock"
+                       FontSize="13"
+                       Foreground="White"
+                       TextWrapping="Wrap"
+                       Text="" />
+        </Border>
+
         <Grid Grid.Row="0" Margin="24,24,24,16">
             <Grid.ColumnDefinitions>
                 <ColumnDefinition Width="2.2*" />
@@ -32,7 +49,6 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
-                        <RowDefinition Height="Auto" />
                         <RowDefinition Height="*" />
                     </Grid.RowDefinitions>
 
@@ -76,13 +92,20 @@
                         </StackPanel>
                     </Border>
 
-                    <Border Grid.Row="3" Margin="0,24,0,0" Padding="16" Background="#EEF2FF" CornerRadius="10">
-                        <TextBlock x:Name="StatusTextBlock"
-                                   FontSize="13"
-                                   Foreground="#3730A3"
-                                   TextWrapping="Wrap"
-                                   Text="请选择一块有线网卡。" />
+                    <Border Grid.Row="3" Margin="0,16,0,0" Padding="16" Background="#F3F4F6" CornerRadius="10" VerticalAlignment="Bottom">
+                        <Grid>
+                            <Grid.RowDefinitions>
+                                <RowDefinition Height="Auto" />
+                                <RowDefinition Height="*" />
+                            </Grid.RowDefinitions>
+                            <TextBlock FontSize="13" FontWeight="SemiBold" Foreground="#111827" Text="运行日志" />
+                            <ListBox x:Name="EventLogListBox"
+                                      Grid.Row="1"
+                                      Margin="0,12,0,0"
+                                      Height="108" />
+                        </Grid>
                     </Border>
+
                 </Grid>
             </Border>
 
@@ -97,6 +120,7 @@
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
                             <RowDefinition Height="*" />
                         </Grid.RowDefinitions>
 
@@ -177,20 +201,6 @@
                             <TextBlock Margin="0,8,0,0" Foreground="#4B5563" Text="4. 自动带出或输入密码并验证连接" />
                         </StackPanel>
                     </Border>
-
-                    <Border Grid.Row="6" Margin="0,16,0,0" Padding="16" Background="#F3F4F6" CornerRadius="10">
-                        <Grid>
-                            <Grid.RowDefinitions>
-                                <RowDefinition Height="Auto" />
-                                <RowDefinition Height="*" />
-                            </Grid.RowDefinitions>
-                            <TextBlock FontSize="13" FontWeight="SemiBold" Foreground="#111827" Text="运行日志" />
-                            <ListBox x:Name="EventLogListBox"
-                                     Grid.Row="1"
-                                     Margin="0,12,0,0"
-                                     MinHeight="180" />
-                        </Grid>
-                    </Border>
                 </Grid>
             </Border>
         </Grid>

+ 66 - 0
windows/QuickIP.Client/MainWindow.xaml.cs

@@ -2,6 +2,9 @@
 using System.Globalization;
 using System.Windows;
 using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using System.Windows.Threading;
 using QuickIP.Client.Models;
 using QuickIP.Client.Services;
 
@@ -23,6 +26,7 @@ public partial class MainWindow : Window
     private bool _isShowingPassword;
     private bool _isBusy;
     private bool _suppressPasswordSync;
+    private CancellationTokenSource? _statusMessageCts;
 
     public MainWindow()
     {
@@ -629,13 +633,75 @@ public partial class MainWindow : Window
 
     private void SetStatus(string message, bool addLog)
     {
+        ApplyStatusMessageStyle(message);
         StatusTextBlock.Text = message;
+        StatusMessageBorder.Opacity = 0;
+        StatusMessageBorder.Visibility = Visibility.Visible;
+        StatusMessageBorder.BeginAnimation(OpacityProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(160)));
+        _statusMessageCts?.Cancel();
+        _statusMessageCts = new CancellationTokenSource();
+        _ = HideStatusMessageAsync(_statusMessageCts.Token);
         if (addLog)
         {
             AppendLog(message, false);
         }
     }
 
+    private async Task HideStatusMessageAsync(CancellationToken cancellationToken)
+    {
+        try
+        {
+            await Task.Delay(3000, cancellationToken);
+            await Dispatcher.InvokeAsync(() =>
+            {
+                var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(200));
+                animation.Completed += (_, _) =>
+                {
+                    if (!cancellationToken.IsCancellationRequested)
+                    {
+                        StatusMessageBorder.Visibility = Visibility.Collapsed;
+                    }
+                };
+                StatusMessageBorder.BeginAnimation(OpacityProperty, animation);
+            });
+        }
+        catch (TaskCanceledException)
+        {
+        }
+    }
+
+    private void ApplyStatusMessageStyle(string message)
+    {
+        var (background, foreground) = GetStatusMessageBrushes(message);
+        StatusMessageBorder.Background = background;
+        StatusTextBlock.Foreground = foreground;
+    }
+
+    private static (Brush Background, Brush Foreground) GetStatusMessageBrushes(string message)
+    {
+        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 (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));
+    }
+
     private void AppendLog(string message, bool isInitial)
     {
         var prefix = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture);