1
0

4 Коммиты dd332033ce ... cc191f6184

Автор SHA1 Сообщение Дата
  yangkaixiang cc191f6184 fix(ui): 优化网络配置校验逻辑并提示空行错误 1 месяц назад
  yangkaixiang 40b2cdfced feat(ui): 支持连接时密码错误自动重试 1 месяц назад
  yangkaixiang 0bd2e5ee61 feat(ui): DHCP模式下隐藏静态IP区并优化变更摘要 1 месяц назад
  yangkaixiang 650a89607c style(ui): 统一将“接口”术语调整为“网口” 1 месяц назад

+ 16 - 7
windows/NetworkTool.Client/DeviceDetailsWindow.xaml

@@ -1,7 +1,7 @@
 <Window x:Class="NetworkTool.Client.DeviceDetailsWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        Title="设备信息与口配置"
+        Title="设备信息与口配置"
         Height="760"
         Width="960"
         MinHeight="680"
@@ -77,6 +77,15 @@
             </Style.Triggers>
         </Style>
 
+        <Style x:Key="StaticIpv4SectionBorderStyle" TargetType="Border" BasedOn="{StaticResource ModifiedSectionBorderStyle}">
+            <Setter Property="Visibility" Value="Visible" />
+            <Style.Triggers>
+                <DataTrigger Binding="{Binding Dhcp4}" Value="True">
+                    <Setter Property="Visibility" Value="Collapsed" />
+                </DataTrigger>
+            </Style.Triggers>
+        </Style>
+
         <Style x:Key="ModifiedBadgeStyle" TargetType="TextBlock">
             <Setter Property="Margin" Value="8,0,0,0" />
             <Setter Property="Padding" Value="6,1" />
@@ -131,7 +140,7 @@
                         <TextBlock Margin="8,2,0,0"
                                    FontSize="12"
                                    Foreground="#6B7280"
-                                   Text="{Binding ElementName=InterfacesItemsControl, Path=Items.Count, StringFormat=({0} 个口)}" />
+                                   Text="{Binding ElementName=InterfacesItemsControl, Path=Items.Count, StringFormat=({0} 个口)}" />
                     </StackPanel>
 
                     <Button x:Name="ReloadInterfaceConfigButton"
@@ -140,7 +149,7 @@
                             Padding="12,0"
                             VerticalAlignment="Center"
                             Click="ReloadInterfaceConfigButton_OnClick"
-                            Content="刷新全部口配置" />
+                            Content="刷新全部口配置" />
 
                     <ItemsControl x:Name="InterfacesItemsControl" Grid.Row="1" Grid.ColumnSpan="2" Margin="0,12,0,0">
                         <ItemsControl.ItemTemplate>
@@ -148,7 +157,7 @@
                                  <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 SystemName}" />
+                                             <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">
@@ -170,7 +179,7 @@
                                                 <RowDefinition Height="Auto" />
                                             </Grid.RowDefinitions>
 
-                                             <Border Padding="12" CornerRadius="10" Style="{StaticResource ModifiedSectionBorderStyle}" Tag="{Binding IsAddressModified}">
+                                              <Border Padding="12" CornerRadius="10" Style="{StaticResource StaticIpv4SectionBorderStyle}" Tag="{Binding IsAddressModified}">
                                                   <Grid>
                                                     <Grid.RowDefinitions>
                                                         <RowDefinition Height="Auto" />
@@ -219,7 +228,7 @@
                                                 </Grid>
                                             </Border>
 
-                                             <Border Grid.Row="1" Margin="0,12,0,0" Padding="12" CornerRadius="10" Style="{StaticResource ModifiedSectionBorderStyle}" Tag="{Binding IsGatewayModified}">
+                                              <Border Grid.Row="1" Margin="0,12,0,0" Padding="12" CornerRadius="10" Style="{StaticResource StaticIpv4SectionBorderStyle}" Tag="{Binding IsGatewayModified}">
                                                  <Grid>
                                                     <Grid.RowDefinitions>
                                                         <RowDefinition Height="Auto" />
@@ -368,7 +377,7 @@
                                    VerticalAlignment="Center"
                                    FontSize="12"
                                    Foreground="#6B7280"
-                                   Text="先确认所有口配置,再校验新配置,最后一次性应用。" />
+                                   Text="先确认所有口配置,再校验新配置,最后一次性应用。" />
 
                         <StackPanel Grid.Column="1" Margin="12,0,0,0" Orientation="Horizontal">
                             <Button x:Name="ValidateConfigButton"

+ 77 - 27
windows/NetworkTool.Client/DeviceDetailsWindow.xaml.cs

@@ -66,22 +66,22 @@ public partial class DeviceDetailsWindow : Window
         var interfaces = await _serverApiService.GetInterfacesAsync(_baseAddress, _password, _localIPv4);
         if (interfaces is null)
         {
-            ShowStatusMessage("设备已连接,但暂时无法读取 Linux 口列表。", StatusMessageType.Warning);
+            ShowStatusMessage("设备已连接,但暂时无法读取 Linux 口列表。", StatusMessageType.Warning);
             return;
         }
 
-        SetConfigStateMessage($"当前管理接口:{interfaces.ManagementInterface}。正在读取全部接口配置。", false);
-        foreach (var info in interfaces.Interfaces)
+        SetConfigStateMessage($"当前管理网口:{interfaces.ManagementInterface}。正在读取全部网口配置。", false);
+        for (var i = 0; i < interfaces.Interfaces.Count; i++)
         {
-            var editor = new InterfaceEditor(info);
+            var editor = new InterfaceEditor(interfaces.Interfaces[i], i + 1);
             _interfaces.Add(editor);
             await LoadRemoteInterfaceConfigAsync(editor);
         }
 
         _configValidated = false;
         _configDirty = false;
-        SetConfigStateMessage("已读取全部口配置。", false);
-        ShowStatusMessage("已读取全部口配置。", StatusMessageType.Success);
+        SetConfigStateMessage("已读取全部口配置。", false);
+        ShowStatusMessage("已读取全部口配置。", StatusMessageType.Success);
     }
 
     private void ClearDetails()
@@ -96,7 +96,7 @@ public partial class DeviceDetailsWindow : Window
     {
         var hostPart = string.IsNullOrWhiteSpace(hostname) ? _remoteHost : $"{hostname} ({_remoteHost})";
         var versionPart = string.IsNullOrWhiteSpace(serverVersion) ? string.Empty : $" - Server {serverVersion}";
-        Title = string.IsNullOrWhiteSpace(hostPart) ? $"设备信息与接口配置{versionPart}" : $"设备信息与接口配置 - {hostPart}{versionPart}";
+        Title = string.IsNullOrWhiteSpace(hostPart) ? $"设备信息与网口配置{versionPart}" : $"设备信息与网口配置 - {hostPart}{versionPart}";
     }
 
     private static string GetRemoteHost(string baseAddress)
@@ -116,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}", StatusMessageType.Error);
+                ShowStatusMessage($"读取目标网口 {editor.DisplayLabel} 配置失败:{result.Message}", StatusMessageType.Error);
                 return;
             }
 
@@ -173,7 +173,7 @@ public partial class DeviceDetailsWindow : Window
             return;
         }
 
-        SetBusyState(true, "正在刷新全部口配置...");
+        SetBusyState(true, "正在刷新全部口配置...");
         try
         {
             foreach (var editor in _interfaces)
@@ -183,8 +183,8 @@ public partial class DeviceDetailsWindow : Window
 
             _configValidated = false;
             _configDirty = false;
-            SetConfigStateMessage("已刷新全部口配置。", false);
-            ShowStatusMessage("已刷新全部口配置。", StatusMessageType.Success);
+            SetConfigStateMessage("已刷新全部口配置。", false);
+            ShowStatusMessage("已刷新全部口配置。", StatusMessageType.Success);
         }
         finally
         {
@@ -229,7 +229,7 @@ public partial class DeviceDetailsWindow : Window
                 var errors = result.Data.Errors.Count > 0 ? $" 错误:{string.Join(";", result.Data.Errors)}" : string.Empty;
                 SetConfigStateMessage(_configValidated ? "配置已校验通过,可以应用。" : "配置校验未通过,请修正后重新校验。", !_configValidated);
                 ShowStatusMessage(
-                    _configValidated ? $"全部口校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}",
+                    _configValidated ? $"全部口校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}",
                     _configValidated ? StatusMessageType.Success : StatusMessageType.Error);
             }
             else
@@ -256,7 +256,7 @@ public partial class DeviceDetailsWindow : Window
 
         var changeSummary = FormatChangeSummary();
         var confirmMessage = string.IsNullOrWhiteSpace(changeSummary)
-            ? "将要一次性应用以下口配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。"
+            ? "将要一次性应用以下口配置:\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)
         {
@@ -492,7 +492,7 @@ public partial class DeviceDetailsWindow : Window
 
         if (result.Count == 0)
         {
-            ShowStatusMessage("口配置不能为空。", StatusMessageType.Error);
+            ShowStatusMessage("口配置不能为空。", StatusMessageType.Error);
             return null;
         }
 
@@ -506,7 +506,7 @@ public partial class DeviceDetailsWindow : Window
         var routes = Array.Empty<RemoteInterfaceRouteConfig>();
         if (!dhcp4)
         {
-            if (editor.Addresses.All(item => string.IsNullOrWhiteSpace(item.IP) && string.IsNullOrWhiteSpace(item.Mask)))
+            if (editor.Addresses.Count == 0)
             {
                 ShowStatusMessage($"{editor.SystemName}:IP 地址不能为空,至少需要填写一行地址。", StatusMessageType.Error);
                 return null;
@@ -525,7 +525,12 @@ public partial class DeviceDetailsWindow : Window
             }
         }
 
-        var dns = editor.Dns.Select(item => item.Address.Trim()).Where(item => item != string.Empty).ToArray();
+        if (!TryBuildDns(editor, out var dns, out var dnsError))
+        {
+            ShowStatusMessage($"{editor.SystemName}:{dnsError}", StatusMessageType.Error);
+            return null;
+        }
+
         return new RemoteInterfaceConfig
         {
             Interface = editor.SystemName,
@@ -579,7 +584,7 @@ public partial class DeviceDetailsWindow : Window
     private static string FormatConfigSummary(IReadOnlyList<RemoteInterfaceConfig> configs)
     {
         return string.Join(Environment.NewLine + Environment.NewLine, configs.Select(item =>
-            $"口:{item.Interface}\n" +
+            $"口:{item.Interface}\n" +
             $"模式:{(item.Dhcp4 ? "DHCP 自动获取" : "静态 IPv4")}\n" +
             $"IP:{(item.Dhcp4 ? "自动获取" : FormatAddresses(item.Addresses))}\n" +
             $"路由:{(item.Dhcp4 ? "自动获取" : FormatRoutes(item.Routes))}\n" +
@@ -612,13 +617,16 @@ public partial class DeviceDetailsWindow : Window
     private bool TryBuildAddresses(InterfaceEditor editor, out RemoteInterfaceAddressConfig[] addresses, out string error)
     {
         var result = new List<RemoteInterfaceAddressConfig>();
-        foreach (var row in editor.Addresses)
+        for (var i = 0; i < editor.Addresses.Count; i++)
         {
+            var row = editor.Addresses[i];
             var ip = row.IP.Trim();
             var maskText = row.Mask.Trim();
             if (ip == string.Empty && maskText == string.Empty)
             {
-                continue;
+                addresses = [];
+                error = $"第 {i + 1} 行 IP 配置为空,请填写或删除该行。";
+                return false;
             }
             if (ip.Contains('/'))
             {
@@ -651,8 +659,14 @@ public partial class DeviceDetailsWindow : Window
         }
 
         addresses = result.ToArray();
+        if (addresses.Length == 0)
+        {
+            error = "IP 地址不能为空,至少需要填写一行地址。";
+            return false;
+        }
+
         error = string.Empty;
-        return addresses.Length > 0;
+        return true;
     }
 
     private bool TryBuildRoutes(InterfaceEditor editor, out RemoteInterfaceRouteConfig[] routes, out string error)
@@ -671,14 +685,17 @@ public partial class DeviceDetailsWindow : Window
         }
         if (editor.CustomRoutesEnabled)
         {
-            foreach (var row in editor.Routes)
+            for (var i = 0; i < editor.Routes.Count; i++)
             {
+                var row = editor.Routes[i];
                 var to = row.To.Trim();
                 var maskText = row.Mask.Trim();
                 var via = row.Via.Trim();
                 if (to == string.Empty && maskText == string.Empty && via == string.Empty)
                 {
-                    continue;
+                    routes = [];
+                    error = $"自定义路由第 {i + 1} 行为空,请填写或删除该行。";
+                    return false;
                 }
                 if (to.Contains('/'))
                 {
@@ -716,6 +733,27 @@ public partial class DeviceDetailsWindow : Window
         return true;
     }
 
+    private static bool TryBuildDns(InterfaceEditor editor, out string[] dns, out string error)
+    {
+        var result = new List<string>();
+        for (var i = 0; i < editor.Dns.Count; i++)
+        {
+            var address = editor.Dns[i].Address.Trim();
+            if (address == string.Empty)
+            {
+                dns = [];
+                error = $"DNS 第 {i + 1} 行为空,请填写或删除该行。";
+                return false;
+            }
+
+            result.Add(address);
+        }
+
+        dns = result.ToArray();
+        error = string.Empty;
+        return true;
+    }
+
     private static bool TryParseAddresses(string text, out RemoteInterfaceAddressConfig[] addresses, out string error)
     {
         var result = new List<RemoteInterfaceAddressConfig>();
@@ -995,7 +1033,7 @@ public partial class DeviceDetailsWindow : Window
 
     private string FormatChangedFields()
     {
-        var fields = _interfaces.Where(item => item.HasChanges).Select(item => $"{item.SystemName} 的 {item.ChangedFieldsText}");
+        var fields = _interfaces.Where(item => item.HasChanges).Select(item => $"{item.DisplayLabel} 的 {item.ChangedFieldsText}");
         return string.Join(";", fields);
     }
 
@@ -1148,14 +1186,16 @@ public partial class DeviceDetailsWindow : Window
         private string[] _originalGatewayKeys = [];
         private string[] _originalDnsKeys = [];
 
-        public InterfaceEditor(RemoteInterfaceInfo info)
+        public InterfaceEditor(RemoteInterfaceInfo info, int displayIndex)
         {
             SystemName = info.SystemName;
+            DisplayLabel = $"网口{displayIndex}:{SystemName}";
             StatusSummary = info.StatusSummary;
             LinkUp = info.LinkUp;
         }
 
         public string SystemName { get; }
+        public string DisplayLabel { get; }
         public string StatusSummary { get; }
         public bool LinkUp { get; }
         public ObservableCollection<EditableAddress> Addresses { get; } = [];
@@ -1218,14 +1258,14 @@ public partial class DeviceDetailsWindow : Window
 
         public string FormatChangeSummary()
         {
-            var lines = new List<string> { $"接口:{SystemName}" };
+            var lines = new List<string> { DisplayLabel };
             if (IsAddressModified)
             {
-                lines.Add($"IP:{FormatKeys(_originalAddressKeys)} -> {FormatKeys(GetAddressKeys())}");
+                lines.Add($"IP:{FormatAddressSummary(_originalDhcp4, _originalAddressKeys)} -> {FormatAddressSummary(Dhcp4, GetAddressKeys())}");
             }
             if (IsGatewayModified)
             {
-                lines.Add($"网关:{FormatKeys(_originalGatewayKeys)} -> {FormatKeys(GetGatewayKeys())}");
+                lines.Add($"网关:{FormatGatewaySummary(_originalDhcp4, _originalGatewayKeys)} -> {FormatGatewaySummary(Dhcp4, GetGatewayKeys())}");
             }
             if (IsDnsModified)
             {
@@ -1270,6 +1310,16 @@ public partial class DeviceDetailsWindow : Window
             return keys.Count == 0 ? "无" : string.Join(", ", keys);
         }
 
+        private static string FormatAddressSummary(bool dhcp4, IReadOnlyList<string> keys)
+        {
+            return dhcp4 ? "自动获取" : FormatKeys(keys);
+        }
+
+        private static string FormatGatewaySummary(bool dhcp4, IReadOnlyList<string> keys)
+        {
+            return dhcp4 ? "自动获取" : FormatKeys(keys);
+        }
+
         private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
         {
             if (EqualityComparer<T>.Default.Equals(field, value))

+ 37 - 25
windows/NetworkTool.Client/MainWindow.xaml.cs

@@ -261,40 +261,52 @@ public partial class MainWindow : Window
         }
 
         var selectedAdapter = AdapterComboBox.SelectedItem as AdapterInfo;
-        SetBusyState(true, $"正在连接 {device.Lan2Ip}...");
+        var httpPort = device.HttpPort > 0 ? device.HttpPort : 48888;
+        var baseAddress = $"http://{device.Lan2Ip}:{httpPort}";
 
-        try
+        while (true)
         {
-            var httpPort = device.HttpPort > 0 ? device.HttpPort : 48888;
-            var baseAddress = $"http://{device.Lan2Ip}:{httpPort}";
-            var result = await _serverApiService.CheckHealthAsync(baseAddress, password, selectedAdapter?.IPv4Address ?? string.Empty);
-            if (result.Success)
+            SetBusyState(true, $"正在连接 {device.Lan2Ip}...");
+
+            try
             {
-                SavePasswordForDevice(device, password);
-                SetStatus("连接成功。", StatusMessageType.Success, true);
-                OpenDeviceDetailsWindow(baseAddress, selectedAdapter?.IPv4Address ?? string.Empty, password);
+                var result = await _serverApiService.CheckHealthAsync(baseAddress, password, selectedAdapter?.IPv4Address ?? string.Empty);
+                if (result.Success)
+                {
+                    SavePasswordForDevice(device, password);
+                    SetStatus("连接成功。", StatusMessageType.Success, true);
+                    OpenDeviceDetailsWindow(baseAddress, selectedAdapter?.IPv4Address ?? string.Empty, password);
+                    return;
+                }
+
+                if (result.StatusCode == 401)
+                {
+                    _passwordStoreService.ClearPassword(deviceKey);
+                    SetStatus("管理密码错误,请重新输入。", StatusMessageType.Error, true);
+                    SetBusyState(false);
+                    MessageBox.Show(this, "管理密码校验失败,请重新输入管理密码。", "密码错误", MessageBoxButton.OK, MessageBoxImage.Warning);
+                    if (!TryPromptForPassword(device, out password))
+                    {
+                        return;
+                    }
+
+                    continue;
+                }
+
+                SetStatus($"设备已发现,但 HTTP 验证失败:{result.Message}", StatusMessageType.Error, true);
                 return;
             }
-
-            if (result.StatusCode == 401)
+            catch (Exception ex)
             {
-                SetStatus("管理密码错误,请确认密码是否正确。", StatusMessageType.Error, true);
-                MessageBox.Show(this, "管理密码校验失败,请确认密码是否正确。", "密码错误", MessageBoxButton.OK, MessageBoxImage.Warning);
+                SetStatus($"连接失败:{ex.Message}", StatusMessageType.Error, true);
+                MessageBox.Show(this, ex.Message, "连接失败", MessageBoxButton.OK, MessageBoxImage.Error);
                 return;
             }
-
-            SetStatus($"设备已发现,但 HTTP 验证失败:{result.Message}", StatusMessageType.Error, true);
-        }
-        catch (Exception ex)
-        {
-            SetStatus($"连接失败:{ex.Message}", StatusMessageType.Error, true);
-            MessageBox.Show(this, ex.Message, "连接失败", MessageBoxButton.OK, MessageBoxImage.Error);
-        }
-        finally
-        {
-            SetBusyState(false);
+            finally
+            {
+                SetBusyState(false);
+            }
         }
-
     }
 
     private void OpenDeviceDetailsWindow(string baseAddress, string localIPv4, string password)

+ 1 - 1
windows/NetworkTool.Client/Models/RemoteInterfaceInfo.cs

@@ -53,7 +53,7 @@ public sealed class RemoteInterfaceInfo
         : string.Join("; ", IPv4.Select(item => $"{item.Address}/{item.Prefix}"));
 
     public string StatusSummary => IsManagementInterface
-        ? $"管理口 / {(LinkUp ? "已连接" : "未连接")}"
+        ? $"管理口 / {(LinkUp ? "已连接" : "未连接")}"
         : LinkUp ? "已连接" : "未连接";
 
     public string DisplayName => $"{SystemName} / {StatusSummary}";