Explorar el Código

refactor(ui): 统一配置项修改标记样式并右对齐

移除各配置块内联的修改标记,统一使用右侧独立徽章显示,简化样式定义。
yangkaixiang hace 1 mes
padre
commit
eb35355e43

+ 1 - 1
AGENTS.md

@@ -4,5 +4,5 @@
 
 - Windows 编译时,如果因进程占用导致编译失败,可以直接结束占用进程后重新编译。编译结束后重新打开程序。
 - 每次编译 server 端前,需要先修改版本号。
-- 编译win端,需要修改主界面标题栏的版本号。
+- 编译win端,需要修改主界面标题栏的版本号。
 - 版本号格式统一使用 `yyyy.MM.dd.HHmm`,例如 `2026.05.13.1446`。

+ 30 - 38
windows/NetworkTool.Client/DeviceDetailsWindow.xaml

@@ -65,19 +65,13 @@
             <Setter Property="BorderThickness" Value="0" />
         </Style>
 
-        <Style x:Key="ModifiedSectionBorderStyle" TargetType="Border">
+        <Style x:Key="ConfigSectionBorderStyle" TargetType="Border">
             <Setter Property="Background" Value="White" />
             <Setter Property="BorderBrush" Value="#E2E8F0" />
             <Setter Property="BorderThickness" Value="1" />
-            <Style.Triggers>
-                <DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
-                    <Setter Property="Background" Value="#FFFBEB" />
-                    <Setter Property="BorderBrush" Value="#F59E0B" />
-                </DataTrigger>
-            </Style.Triggers>
         </Style>
 
-        <Style x:Key="StaticIpv4SectionBorderStyle" TargetType="Border" BasedOn="{StaticResource ModifiedSectionBorderStyle}">
+        <Style x:Key="StaticIpv4SectionBorderStyle" TargetType="Border" BasedOn="{StaticResource ConfigSectionBorderStyle}">
             <Setter Property="Visibility" Value="Visible" />
             <Style.Triggers>
                 <DataTrigger Binding="{Binding Dhcp4}" Value="True">
@@ -94,7 +88,7 @@
             <Setter Property="FontWeight" Value="SemiBold" />
             <Setter Property="Foreground" Value="#92400E" />
             <Setter Property="Background" Value="#FEF3C7" />
-            <Setter Property="Text" Value="已修改" />
+            <Setter Property="Text" Value="*已修改" />
             <Setter Property="Visibility" Value="Collapsed" />
             <Style.Triggers>
                 <DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
@@ -156,21 +150,28 @@
                             <DataTemplate>
                                  <Border Margin="0,0,0,14" Padding="14" Background="#F8FAFC" BorderBrush="#D8DEE9" BorderThickness="1" CornerRadius="12">
                                      <StackPanel>
-                                        <StackPanel Orientation="Horizontal">
-                                             <TextBlock FontSize="15" FontWeight="SemiBold" Foreground="#0F172A" Text="{Binding DisplayLabel}" />
-                                            <TextBlock Margin="10,2,0,0" FontSize="12" Text="{Binding StatusSummary}">
-                                                <TextBlock.Style>
-                                                    <Style TargetType="TextBlock">
-                                                        <Setter Property="Foreground" Value="#6B7280" />
+                                         <Grid>
+                                             <Grid.ColumnDefinitions>
+                                                 <ColumnDefinition Width="*" />
+                                                 <ColumnDefinition Width="Auto" />
+                                             </Grid.ColumnDefinitions>
+                                             <StackPanel Orientation="Horizontal">
+                                                 <TextBlock FontSize="15" FontWeight="SemiBold" Foreground="#0F172A" Text="{Binding DisplayLabel}" />
+                                                 <TextBlock Margin="10,2,0,0" FontSize="12" Text="{Binding StatusSummary}">
+                                                 <TextBlock.Style>
+                                                     <Style TargetType="TextBlock">
+                                                         <Setter Property="Foreground" Value="#6B7280" />
                                                         <Style.Triggers>
                                                             <DataTrigger Binding="{Binding LinkUp}" Value="True">
                                                                 <Setter Property="Foreground" Value="#16A34A" />
                                                             </DataTrigger>
-                                                        </Style.Triggers>
-                                                    </Style>
-                                                </TextBlock.Style>
-                                            </TextBlock>
-                                        </StackPanel>
+                                                         </Style.Triggers>
+                                                     </Style>
+                                                 </TextBlock.Style>
+                                                 </TextBlock>
+                                             </StackPanel>
+                                             <TextBlock Grid.Column="1" Style="{StaticResource ModifiedBadgeStyle}" Tag="{Binding HasChanges}" />
+                                         </Grid>
                                         <CheckBox Margin="0,12,0,0" VerticalContentAlignment="Center" IsChecked="{Binding Dhcp4, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Checked="ConfigModeChanged_OnChanged" Unchecked="ConfigModeChanged_OnChanged" Content="使用 DHCP 自动获取 IPv4 配置" />
                                         <Grid Margin="0,12,0,0">
                                             <Grid.RowDefinitions>
@@ -179,17 +180,14 @@
                                                 <RowDefinition Height="Auto" />
                                             </Grid.RowDefinitions>
 
-                                              <Border Padding="12" CornerRadius="10" Style="{StaticResource StaticIpv4SectionBorderStyle}" Tag="{Binding IsAddressModified}">
+                                              <Border Padding="12" CornerRadius="10" Style="{StaticResource StaticIpv4SectionBorderStyle}">
                                                   <Grid>
                                                     <Grid.RowDefinitions>
                                                         <RowDefinition Height="Auto" />
                                                         <RowDefinition Height="*" />
                                                         <RowDefinition Height="Auto" />
                                                     </Grid.RowDefinitions>
-                                                     <StackPanel Orientation="Horizontal">
-                                                         <TextBlock Style="{StaticResource SectionTitleStyle}" Text="IP 地址" />
-                                                         <TextBlock Style="{StaticResource ModifiedBadgeStyle}" Tag="{Binding IsAddressModified}" />
-                                                     </StackPanel>
+                                                      <TextBlock Style="{StaticResource SectionTitleStyle}" Text="IP 地址" />
                                                      <DataGrid Grid.Row="1" Margin="18,10,0,0" ItemsSource="{Binding Addresses}" AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="Column" CellEditEnding="ConfigGrid_OnCellEditEnding" PreviewMouseWheel="DataGrid_OnPreviewMouseWheel">
                                                          <DataGrid.Style>
                                                              <Style TargetType="DataGrid" BasedOn="{StaticResource ConfigDataGridStyle}">
@@ -228,7 +226,7 @@
                                                 </Grid>
                                             </Border>
 
-                                              <Border Grid.Row="1" Margin="0,12,0,0" Padding="12" CornerRadius="10" Style="{StaticResource StaticIpv4SectionBorderStyle}" Tag="{Binding IsGatewayModified}">
+                                               <Border Grid.Row="1" Margin="0,12,0,0" Padding="12" CornerRadius="10" Style="{StaticResource StaticIpv4SectionBorderStyle}">
                                                  <Grid>
                                                     <Grid.RowDefinitions>
                                                         <RowDefinition Height="Auto" />
@@ -237,10 +235,7 @@
                                                         <RowDefinition Height="Auto" />
                                                     </Grid.RowDefinitions>
                                                     <StackPanel>
-                                                         <StackPanel Orientation="Horizontal">
-                                                             <TextBlock Style="{StaticResource SectionTitleStyle}" Text="网关" />
-                                                             <TextBlock Style="{StaticResource ModifiedBadgeStyle}" Tag="{Binding IsGatewayModified}" />
-                                                         </StackPanel>
+                                                          <TextBlock Style="{StaticResource SectionTitleStyle}" Text="网关" />
                                                         <StackPanel Margin="18,8,0,12" Orientation="Horizontal">
                                                             <TextBlock VerticalAlignment="Center" FontSize="12" Foreground="#6B7280" Text="默认网关:" />
                                                             <CheckBox Margin="8,0,0,0" VerticalContentAlignment="Center" IsChecked="{Binding DefaultGatewayEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Checked="GatewayOrRouteModeChanged_OnChanged" Unchecked="GatewayOrRouteModeChanged_OnChanged" Content="启用">
@@ -334,17 +329,14 @@
                                                 </Grid>
                                             </Border>
 
-                                             <Border Grid.Row="2" Margin="0,12,0,0" Padding="12" CornerRadius="10" Style="{StaticResource ModifiedSectionBorderStyle}" Tag="{Binding IsDnsModified}">
+                                             <Border Grid.Row="2" Margin="0,12,0,0" Padding="12" CornerRadius="10" Style="{StaticResource ConfigSectionBorderStyle}">
                                                  <Grid>
                                                     <Grid.RowDefinitions>
                                                         <RowDefinition Height="Auto" />
                                                         <RowDefinition Height="*" />
                                                         <RowDefinition Height="Auto" />
                                                     </Grid.RowDefinitions>
-                                                     <StackPanel Orientation="Horizontal">
-                                                         <TextBlock Style="{StaticResource SectionTitleStyle}" Text="DNS" />
-                                                         <TextBlock Style="{StaticResource ModifiedBadgeStyle}" Tag="{Binding IsDnsModified}" />
-                                                     </StackPanel>
+                                                      <TextBlock Style="{StaticResource SectionTitleStyle}" Text="DNS" />
                                                      <DataGrid Grid.Row="1" Margin="18,10,0,0" Style="{StaticResource ConfigDataGridStyle}" ItemsSource="{Binding Dns}" AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="Column" CellEditEnding="ConfigGrid_OnCellEditEnding" PreviewMouseWheel="DataGrid_OnPreviewMouseWheel">
                                                          <DataGrid.Columns>
                                                              <DataGridTextColumn Header="DNS 地址" Binding="{Binding Address, UpdateSourceTrigger=PropertyChanged}" ElementStyle="{StaticResource ConfigDataGridTextStyle}" EditingElementStyle="{StaticResource ConfigDataGridEditingTextStyle}" Width="*" />
@@ -377,14 +369,14 @@
                                    VerticalAlignment="Center"
                                    FontSize="12"
                                    Foreground="#6B7280"
-                                   Text="先确认所有网口配置,再校验新配置,最后一次性应用。" />
+                                   Text="先确认所有网口配置,再校验新配置,最后保存配置。" />
 
                         <StackPanel Grid.Column="1" Margin="12,0,0,0" Orientation="Horizontal">
                             <Button x:Name="ValidateConfigButton"
                                      MinHeight="36"
                                      Padding="14,0"
                                      Click="ValidateConfigButton_OnClick"
-                                     Content="校验全部配置" />
+                                      Content="校验" />
                             <Button x:Name="ApplyConfigButton"
                                     Margin="10,0,0,0"
                                     MinHeight="36"
@@ -392,7 +384,7 @@
                                     FontWeight="SemiBold"
                                     Style="{StaticResource PrimaryButtonStyle}"
                                     Click="ApplyConfigButton_OnClick"
-                                    Content="应用全部配置" />
+                                    Content="保存配置" />
                         </StackPanel>
                     </Grid>
                 </Grid>

+ 172 - 50
windows/NetworkTool.Client/DeviceDetailsWindow.xaml.cs

@@ -4,6 +4,7 @@ using System.ComponentModel;
 using System.Runtime.CompilerServices;
 using System.Windows;
 using System.Windows.Controls;
+using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Animation;
@@ -168,7 +169,7 @@ public partial class DeviceDetailsWindow : Window
 
     private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
     {
-        if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未应用,重新获取会丢失未应用内容。是否继续重新获取?", "确认重新获取配置"))
+        if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未保存,重新获取会丢失未保存内容。是否继续重新获取?", "确认重新获取配置"))
         {
             return;
         }
@@ -194,7 +195,7 @@ public partial class DeviceDetailsWindow : Window
 
     private void DeviceDetailsWindow_OnClosing(object? sender, CancelEventArgs e)
     {
-        if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未应用。是否关闭窗口?", "确认关闭窗口"))
+        if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未保存。是否关闭窗口?", "确认关闭窗口"))
         {
             e.Cancel = true;
         }
@@ -227,9 +228,9 @@ 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;
-                SetConfigStateMessage(_configValidated ? "配置已校验通过,可以应用。" : "配置校验未通过,请修正后重新校验。", !_configValidated);
+                SetConfigStateMessage(_configValidated ? "配置已校验通过,可以保存。" : "配置校验未通过,请修正后重新校验。", !_configValidated);
                 ShowStatusMessage(
-                    _configValidated ? $"全部网口校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}",
+                    _configValidated ? $"全部网口校验通过,可保存配置。{warnings}" : $"校验失败。{errors}{warnings}",
                     _configValidated ? StatusMessageType.Success : StatusMessageType.Error);
             }
             else
@@ -256,14 +257,14 @@ public partial class DeviceDetailsWindow : Window
 
         var changeSummary = FormatChangeSummary();
         var confirmMessage = string.IsNullOrWhiteSpace(changeSummary)
-            ? "将要一次性应用以下网口配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。"
-            : "将要一次性应用以下已修改配置:\n\n" + changeSummary + "\n\n完整目标配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。";
-        if (MessageBox.Show(this, confirmMessage, "确认应用配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
+            ? "将要一次性保存以下网口配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。"
+            : "将要一次性保存以下已修改配置:\n\n" + changeSummary + "\n\n完整目标配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。";
+        if (MessageBox.Show(this, confirmMessage, "确认保存配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
         {
             return;
         }
 
-        SetBusyState(true, "正在提交并应用配置,请稍候...");
+        SetBusyState(true, "正在提交并保存配置,请稍候...");
         try
         {
             var applyResult = await _serverApiService.ApplyInterfaceConfigsAsync(_baseAddress, _password, _localIPv4, request);
@@ -273,7 +274,7 @@ public partial class DeviceDetailsWindow : Window
                 return;
             }
 
-            ShowStatusMessage("配置任务已提交,正在应用并等待连通确认...", StatusMessageType.Info);
+            ShowStatusMessage("配置任务已提交,正在保存并等待连通确认...", StatusMessageType.Info);
             await PollTaskAsync(applyResult.Data.TaskId);
         }
         finally
@@ -338,14 +339,14 @@ public partial class DeviceDetailsWindow : Window
                 {
                     _configValidated = false;
                     _configDirty = false;
-                    SetConfigStateMessage("配置已应用,当前显示为设备最新配置。", false);
+                    SetConfigStateMessage("配置已保存,当前显示为设备最新配置。", false);
+                    foreach (var editor in _interfaces)
+                    {
+                        await LoadRemoteInterfaceConfigAsync(editor);
+                    }
                 }
 
                 ShowTaskCompletionDialog(task);
-                foreach (var editor in _interfaces)
-                {
-                    await LoadRemoteInterfaceConfigAsync(editor);
-                }
 
                 return;
             }
@@ -358,27 +359,41 @@ public partial class DeviceDetailsWindow : Window
     {
         var remaining = timeoutSeconds;
         var result = false;
-        var messageTextBlock = new TextBlock
+        var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
+        var remainingTextBlock = new TextBlock
         {
-            Width = 420,
-            TextWrapping = TextWrapping.Wrap,
-            FontSize = 13,
-            Foreground = Brushes.Black,
+            VerticalAlignment = VerticalAlignment.Center,
+            FontSize = 14,
+            FontWeight = FontWeights.SemiBold,
+        };
+        var progressBar = new ProgressBar
+        {
+            Height = 8,
+            Minimum = 0,
+            Maximum = timeoutSeconds,
+            Value = timeoutSeconds,
+            Foreground = new SolidColorBrush(Color.FromRgb(24, 119, 242)),
+            Background = new SolidColorBrush(Color.FromRgb(229, 231, 235)),
+            BorderBrush = Brushes.Transparent,
+            BorderThickness = new Thickness(0),
         };
         var confirmButton = new Button
         {
-            MinWidth = 88,
-            MinHeight = 32,
-            Margin = new Thickness(0, 0, 10, 0),
-            Content = "保留",
+            Width = 100,
+            Height = 34,
+            Margin = new Thickness(0, 0, 12, 0),
+            Content = "保留配置",
+            FontSize = 13,
             IsDefault = true,
         };
         confirmButton.Style = (Style)FindResource("PrimaryButtonStyle");
         var cancelButton = new Button
         {
-            MinWidth = 88,
-            MinHeight = 32,
-            Content = "回滚",
+            Width = 100,
+            Height = 34,
+            Content = "撤销修改",
+            FontSize = 13,
+            Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55)),
             IsCancel = true,
         };
         var dialog = new Window
@@ -388,41 +403,62 @@ public partial class DeviceDetailsWindow : Window
             WindowStartupLocation = WindowStartupLocation.CenterOwner,
             ResizeMode = ResizeMode.NoResize,
             SizeToContent = SizeToContent.WidthAndHeight,
-            Content = new StackPanel
+            Content = new Border
             {
-                Margin = new Thickness(18),
-                Children =
+                Width = 460,
+                Padding = new Thickness(16),
+                Background = Brushes.White,
+                Child = new Grid
                 {
-                    messageTextBlock,
-                    new StackPanel
+                    RowDefinitions =
                     {
-                        Margin = new Thickness(0, 18, 0, 0),
-                        HorizontalAlignment = HorizontalAlignment.Right,
-                        Orientation = Orientation.Horizontal,
-                        Children = { confirmButton, cancelButton },
+                        new RowDefinition { Height = GridLength.Auto },
+                        new RowDefinition { Height = GridLength.Auto },
+                        new RowDefinition { Height = GridLength.Auto },
+                    },
+                    Children =
+                    {
+                        CreateApplyConfirmNoticeCard(),
+                        CreateApplyConfirmCountdownCard(remainingTextBlock, progressBar),
+                        new StackPanel
+                        {
+                            Margin = new Thickness(0, 24, 0, 0),
+                            HorizontalAlignment = HorizontalAlignment.Right,
+                            Orientation = Orientation.Horizontal,
+                            Children = { confirmButton, cancelButton },
+                        }.SetGridRow(2),
                     },
                 },
             },
         };
 
-        void UpdateMessage()
+        void UpdateMessage(double secondsLeft)
         {
-            messageTextBlock.Text = "当前客户端仍可连接到设备。是否确认保留这次网络配置?\n\n超时或取消时,Linux 端会自动回滚。";
-            confirmButton.Content = $"保留({remaining}秒)";
+            remaining = Math.Max(0, (int)Math.Ceiling(secondsLeft));
+            remainingTextBlock.Inlines.Clear();
+            remainingTextBlock.Inlines.Add(new Run($"{remaining} 秒后")
+            {
+                Foreground = new SolidColorBrush(Color.FromRgb(24, 119, 242)),
+            });
+            remainingTextBlock.Inlines.Add(new Run("撤销修改")
+            {
+                Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55)),
+            });
+            progressBar.Value = Math.Max(0, secondsLeft);
         }
 
-        var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
+        var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
         timer.Tick += (_, _) =>
         {
-            remaining--;
-            if (remaining <= 0)
+            var secondsLeft = (deadline - DateTime.UtcNow).TotalSeconds;
+            if (secondsLeft <= 0)
             {
                 timer.Stop();
                 dialog.DialogResult = false;
                 dialog.Close();
                 return;
             }
-            UpdateMessage();
+            UpdateMessage(secondsLeft);
         };
         confirmButton.Click += (_, _) =>
         {
@@ -437,12 +473,83 @@ public partial class DeviceDetailsWindow : Window
         };
         dialog.Closed += (_, _) => timer.Stop();
 
-        UpdateMessage();
+        UpdateMessage(timeoutSeconds);
         timer.Start();
         dialog.ShowDialog();
         return result;
     }
 
+    private static Border CreateApplyConfirmNoticeCard()
+    {
+        return new Border
+        {
+            Height = 36,
+            Padding = new Thickness(14, 0, 14, 0),
+            Background = new SolidColorBrush(Color.FromRgb(255, 251, 235)),
+            BorderBrush = new SolidColorBrush(Color.FromRgb(253, 230, 138)),
+            BorderThickness = new Thickness(1),
+            CornerRadius = new CornerRadius(8),
+            Child = new StackPanel
+            {
+                VerticalAlignment = VerticalAlignment.Center,
+                Orientation = Orientation.Horizontal,
+                Children =
+                {
+                    new TextBlock
+                    {
+                        Text = "⏳",
+                        Margin = new Thickness(0, 0, 12, 0),
+                        VerticalAlignment = VerticalAlignment.Center,
+                        FontSize = 16,
+                        Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)),
+                    },
+                    new TextBlock
+                    {
+                        Text = "请在倒计时结束前点击“保留配置”。",
+                        VerticalAlignment = VerticalAlignment.Center,
+                        FontSize = 13,
+                        Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55)),
+                    },
+                },
+            },
+        }.SetGridRow(0);
+    }
+
+    private static Border CreateApplyConfirmCountdownCard(TextBlock remainingTextBlock, ProgressBar progressBar)
+    {
+        return new Border
+        {
+            Margin = new Thickness(0, 16, 0, 0),
+            Padding = new Thickness(18, 12, 18, 12),
+            BorderBrush = new SolidColorBrush(Color.FromRgb(229, 231, 235)),
+            BorderThickness = new Thickness(1),
+            CornerRadius = new CornerRadius(8),
+            Child = new StackPanel
+            {
+                Children =
+                {
+                    new StackPanel
+                    {
+                        Orientation = Orientation.Horizontal,
+                        Children =
+                        {
+                            new TextBlock
+                            {
+                                Text = "◷",
+                                Margin = new Thickness(0, 0, 12, 0),
+                                VerticalAlignment = VerticalAlignment.Center,
+                                FontSize = 18,
+                                Foreground = new SolidColorBrush(Color.FromRgb(24, 119, 242)),
+                            },
+                            remainingTextBlock,
+                        },
+                    },
+                    progressBar.SetMargin(new Thickness(0, 12, 0, 0)),
+                },
+            },
+        }.SetGridRow(1);
+    }
+
     private async void RebootButton_OnClick(object sender, RoutedEventArgs e)
     {
         await ExecuteSystemActionAsync(
@@ -1017,7 +1124,7 @@ public partial class DeviceDetailsWindow : Window
         _configValidated = false;
         RefreshChangeState();
         SetConfigStateMessage(
-            _configDirty ? $"配置已修改:{FormatChangedFields()}。需重新校验后才能应用。" : "配置未修改。",
+            _configDirty ? "配置已修改,需重新校验后才能保存。" : "配置未修改。",
             _configDirty);
         UpdateButtonStates();
     }
@@ -1134,16 +1241,16 @@ public partial class DeviceDetailsWindow : Window
     {
         return task.Status switch
         {
-            "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),
+            "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" => ("正在校验配置...", StatusMessageType.Info),
                 "writing_netplan" => ("正在写入 Linux 网络配置...", StatusMessageType.Info),
-                "applying" => ("正在应用 Linux 网络配置...", StatusMessageType.Info),
+                "applying" => ("正在保存 Linux 网络配置...", StatusMessageType.Info),
                 "confirming" => (string.IsNullOrWhiteSpace(task.Detail) ? "等待确认保留配置..." : task.Detail, StatusMessageType.Warning),
-                "rolling_back" => ("配置应用失败,正在自动回滚...", StatusMessageType.Warning),
+                "rolling_back" => ("配置保存失败,正在自动回滚...", StatusMessageType.Warning),
                 _ => (string.IsNullOrWhiteSpace(task.Detail) ? "正在处理,请稍候..." : task.Detail, StatusMessageType.Info),
             }
         };
@@ -1152,7 +1259,7 @@ public partial class DeviceDetailsWindow : Window
     private void ShowTaskCompletionDialog(RemoteTaskResult task)
     {
         var (message, _) = FormatTaskStatusMessage(task);
-        var title = task.Status == "success" ? "应用配置成功" : "应用配置失败";
+        var title = task.Status == "success" ? "保存配置成功" : "保存配置失败";
         var image = task.Status == "success" ? MessageBoxImage.Information : MessageBoxImage.Warning;
         MessageBox.Show(this, message, title, MessageBoxButton.OK, image);
     }
@@ -1447,3 +1554,18 @@ public partial class DeviceDetailsWindow : Window
         }
     }
 }
+
+internal static class ApplyConfirmationDialogElementExtensions
+{
+    public static T SetGridRow<T>(this T element, int row) where T : UIElement
+    {
+        Grid.SetRow(element, row);
+        return element;
+    }
+
+    public static T SetMargin<T>(this T element, Thickness margin) where T : FrameworkElement
+    {
+        element.Margin = margin;
+        return element;
+    }
+}

+ 1 - 1
windows/NetworkTool.Client/NetworkTool.Client.csproj

@@ -6,7 +6,7 @@
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
     <UseWPF>true</UseWPF>
-    <InformationalVersion>2026.05.13.1446</InformationalVersion>
+    <InformationalVersion>2026.05.13.1557</InformationalVersion>
   </PropertyGroup>
 
 </Project>