Selaa lähdekoodia

feat(ui): 支持设备搜索实时增量更新并添加进度提示

yangkaixiang 1 kuukausi sitten
vanhempi
commit
30e05bca03

+ 18 - 1
windows/NetworkTool.Client/MainWindow.xaml

@@ -177,6 +177,8 @@
                                 <Grid.ColumnDefinitions>
                                     <ColumnDefinition Width="*" />
                                     <ColumnDefinition Width="Auto" />
+                                    <ColumnDefinition Width="Auto" />
+                                    <ColumnDefinition Width="Auto" />
                                 </Grid.ColumnDefinitions>
                                 <TextBlock VerticalAlignment="Center"
                                            FontSize="13"
@@ -184,11 +186,26 @@
                                            Foreground="#111827"
                                            Text="发现设备(双击连接)" />
                                 <Button x:Name="SearchDevicesButton"
-                                        Grid.Column="1"
+                                        Grid.Column="3"
                                         MinHeight="32"
                                         Padding="14,0"
                                         Click="SearchDevicesButton_OnClick"
                                         Content="重新搜索设备" />
+                                <ProgressBar x:Name="SearchProgressBar"
+                                             Grid.Column="1"
+                                             Width="72"
+                                             Height="6"
+                                             Margin="12,0,8,0"
+                                             VerticalAlignment="Center"
+                                             IsIndeterminate="True"
+                                             Visibility="Collapsed" />
+                                <TextBlock x:Name="SearchProgressTextBlock"
+                                           Grid.Column="2"
+                                           Margin="0,0,12,0"
+                                           VerticalAlignment="Center"
+                                           Foreground="#6B7280"
+                                           Text="搜索中"
+                                           Visibility="Collapsed" />
                             </Grid>
 
                             <ListView x:Name="DiscoveredDevicesListView"

+ 61 - 8
windows/NetworkTool.Client/MainWindow.xaml.cs

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
@@ -18,8 +19,10 @@ public partial class MainWindow : Window
     private readonly ServerApiService _serverApiService = new();
     private IReadOnlyList<AdapterInfo> _allAdapters = [];
     private IReadOnlyList<AdapterInfo> _adapters = [];
-    private IReadOnlyList<DiscoveredDevice> _discoveredDevices = [];
+    private readonly ObservableCollection<DiscoveredDevice> _discoveredDevices = [];
     private bool _isBusy;
+    private bool _isSearchingDevices;
+    private CancellationTokenSource? _deviceSearchCts;
     private CancellationTokenSource? _statusMessageCts;
 
     public MainWindow()
@@ -74,6 +77,7 @@ public partial class MainWindow : Window
     {
         if (AdapterComboBox.SelectedItem is not AdapterInfo adapter)
         {
+            CancelDeviceSearch();
             ClearDiscoveredDevices();
             UpdateAdapterPlaceholder();
             SetStatus("请选择一块网卡。", StatusMessageType.Warning, false);
@@ -84,6 +88,7 @@ public partial class MainWindow : Window
         UpdateAdapterPlaceholder();
         if (!adapter.HasLink)
         {
+            CancelDeviceSearch();
             ClearDiscoveredDevices();
             SetStatus("当前网卡未检测到链路,请检查网线连接。", StatusMessageType.Warning, true);
             UpdateButtonStates();
@@ -157,6 +162,7 @@ public partial class MainWindow : Window
 
         if (string.IsNullOrWhiteSpace(adapter.IPv4Address))
         {
+            CancelDeviceSearch();
             ClearDiscoveredDevices();
             SetStatus("当前网卡没有可用 IPv4,无法搜索设备。", StatusMessageType.Error, true);
             return;
@@ -164,19 +170,25 @@ public partial class MainWindow : Window
 
         if (!adapter.HasLink)
         {
+            CancelDeviceSearch();
             ClearDiscoveredDevices();
             SetStatus("当前网卡未检测到链路,请检查网线连接。", StatusMessageType.Warning, true);
             return;
         }
 
-        SetBusyState(true, "正在搜索设备...");
+        _deviceSearchCts?.Cancel();
+        var searchCts = new CancellationTokenSource();
+        _deviceSearchCts = searchCts;
+        SetDeviceSearchState(true);
         ClearDiscoveredDevices();
         await Dispatcher.InvokeAsync(() => { }, System.Windows.Threading.DispatcherPriority.Render);
 
         try
         {
-            _discoveredDevices = await _discoveryService.DiscoverManyAsync(adapter.IPv4Address);
-            DiscoveredDevicesListView.ItemsSource = _discoveredDevices;
+            await _discoveryService.DiscoverManyAsync(
+                adapter.IPv4Address,
+                device => Dispatcher.Invoke(() => UpsertDiscoveredDevice(device)),
+                searchCts.Token);
             if (_discoveredDevices.Count == 0)
             {
                 SetStatus("未发现 169.254 开头的设备 IP,请确认网卡、网线、远端服务和维护网段配置。", StatusMessageType.Warning, true);
@@ -185,6 +197,9 @@ public partial class MainWindow : Window
 
             SetStatus($"已发现 {_discoveredDevices.Count} 台设备,请双击 IP 连接。", StatusMessageType.Success, true);
         }
+        catch (OperationCanceledException)
+        {
+        }
         catch (Exception ex)
         {
             SetStatus($"搜索设备失败:{ex.Message}", StatusMessageType.Error, true);
@@ -192,7 +207,10 @@ public partial class MainWindow : Window
         }
         finally
         {
-            SetBusyState(false);
+            if (ReferenceEquals(_deviceSearchCts, searchCts))
+            {
+                SetDeviceSearchState(false);
+            }
         }
     }
 
@@ -210,7 +228,7 @@ public partial class MainWindow : Window
         var adapter = AdapterComboBox.SelectedItem as AdapterInfo;
         var hasAdapter = adapter is not null;
         RefreshAdaptersButton.IsEnabled = !_isBusy;
-        SearchDevicesButton.IsEnabled = !_isBusy && hasAdapter && adapter!.HasLink;
+        SearchDevicesButton.IsEnabled = !_isBusy && !_isSearchingDevices && hasAdapter && adapter!.HasLink;
     }
 
     private void RefreshAdapters(string? selectedAdapterId = null)
@@ -221,10 +239,30 @@ public partial class MainWindow : Window
 
     private void ClearDiscoveredDevices()
     {
-        _discoveredDevices = [];
+        _discoveredDevices.Clear();
         DiscoveredDevicesListView.ItemsSource = _discoveredDevices;
     }
 
+    private void UpsertDiscoveredDevice(DiscoveredDevice device)
+    {
+        var existing = _discoveredDevices.FirstOrDefault(value => string.Equals(value.Lan2Ip, device.Lan2Ip, StringComparison.OrdinalIgnoreCase));
+        if (existing is not null)
+        {
+            var index = _discoveredDevices.IndexOf(existing);
+            _discoveredDevices[index] = device;
+            return;
+        }
+
+        var insertIndex = 0;
+        while (insertIndex < _discoveredDevices.Count && string.Compare(_discoveredDevices[insertIndex].Lan2Ip, device.Lan2Ip, StringComparison.OrdinalIgnoreCase) < 0)
+        {
+            insertIndex++;
+        }
+
+        _discoveredDevices.Insert(insertIndex, device);
+        SetStatus($"已发现 {_discoveredDevices.Count} 台设备,搜索仍在继续。", StatusMessageType.Success, true);
+    }
+
     private async void DiscoveredDevicesListView_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
     {
         if (!_isBusy && DiscoveredDevicesListView.SelectedItem is DiscoveredDevice device)
@@ -418,6 +456,21 @@ public partial class MainWindow : Window
         BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message;
         AdapterComboBox.IsEnabled = !isBusy;
         RefreshAdaptersButton.IsEnabled = !isBusy;
-        SearchDevicesButton.IsEnabled = !isBusy && AdapterComboBox.SelectedItem is AdapterInfo adapter && adapter.HasLink;
+        SearchDevicesButton.IsEnabled = !isBusy && !_isSearchingDevices && AdapterComboBox.SelectedItem is AdapterInfo adapter && adapter.HasLink;
+    }
+
+    private void SetDeviceSearchState(bool isSearching)
+    {
+        _isSearchingDevices = isSearching;
+        SearchProgressBar.Visibility = isSearching ? Visibility.Visible : Visibility.Collapsed;
+        SearchProgressTextBlock.Visibility = isSearching ? Visibility.Visible : Visibility.Collapsed;
+        SearchDevicesButton.Content = isSearching ? "搜索中..." : "重新搜索设备";
+        UpdateButtonStates();
+    }
+
+    private void CancelDeviceSearch()
+    {
+        _deviceSearchCts?.Cancel();
+        SetDeviceSearchState(false);
     }
 }

+ 49 - 5
windows/NetworkTool.Client/Services/DiscoveryService.cs

@@ -11,11 +11,16 @@ namespace NetworkTool.Client.Services;
 public sealed class DiscoveryService
 {
     private const int DiscoveryPort = 50000;
+    private static readonly TimeSpan DiscoveryTimeout = TimeSpan.FromSeconds(5);
+    private static readonly TimeSpan DiscoveryRetryInterval = TimeSpan.FromSeconds(1);
 
     [DllImport("iphlpapi.dll", ExactSpelling = true)]
     private static extern int SendARP(int destinationIp, int sourceIp, byte[] macAddress, ref int physicalAddressLength);
 
-    public async Task<IReadOnlyList<DiscoveredDevice>> DiscoverManyAsync(string localIPv4, CancellationToken cancellationToken = default)
+    public async Task<IReadOnlyList<DiscoveredDevice>> DiscoverManyAsync(
+        string localIPv4,
+        Action<DiscoveredDevice>? onDeviceDiscovered = null,
+        CancellationToken cancellationToken = default)
     {
         if (string.IsNullOrWhiteSpace(localIPv4))
         {
@@ -37,17 +42,53 @@ public sealed class DiscoveryService
         };
 
         var payload = JsonSerializer.SerializeToUtf8Bytes(request);
-        await client.SendAsync(payload, payload.Length, new IPEndPoint(IPAddress.Broadcast, DiscoveryPort));
+        var broadcastEndPoint = new IPEndPoint(IPAddress.Broadcast, DiscoveryPort);
+        await client.SendAsync(payload, payload.Length, broadcastEndPoint);
 
         using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
-        timeoutCts.CancelAfter(TimeSpan.FromSeconds(3));
+        timeoutCts.CancelAfter(DiscoveryTimeout);
 
         var devicesByIp = new Dictionary<string, DiscoveredDevice>(StringComparer.OrdinalIgnoreCase);
+        var nextRetryAt = DateTimeOffset.UtcNow + DiscoveryRetryInterval;
         while (!timeoutCts.IsCancellationRequested)
         {
             try
             {
-                var result = await client.ReceiveAsync(timeoutCts.Token);
+                var retryDelay = nextRetryAt - DateTimeOffset.UtcNow;
+                if (retryDelay <= TimeSpan.Zero)
+                {
+                    await client.SendAsync(payload, payload.Length, broadcastEndPoint);
+                    nextRetryAt = DateTimeOffset.UtcNow + DiscoveryRetryInterval;
+                    continue;
+                }
+
+                using var receiveCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token);
+                var receiveTask = client.ReceiveAsync(receiveCts.Token).AsTask();
+                var retryDelayTask = Task.Delay(retryDelay, timeoutCts.Token);
+                var completedTask = await Task.WhenAny(receiveTask, retryDelayTask);
+
+                if (completedTask == retryDelayTask)
+                {
+                    receiveCts.Cancel();
+                    try
+                    {
+                        await receiveTask;
+                    }
+                    catch (OperationCanceledException)
+                    {
+                    }
+
+                    if (timeoutCts.IsCancellationRequested)
+                    {
+                        break;
+                    }
+
+                    await client.SendAsync(payload, payload.Length, broadcastEndPoint);
+                    nextRetryAt = DateTimeOffset.UtcNow + DiscoveryRetryInterval;
+                    continue;
+                }
+
+                var result = await receiveTask;
                 var response = JsonSerializer.Deserialize<DiscoveryResponse>(result.Buffer);
                 if (response is null
                     || response.MessageType != "discover_response"
@@ -63,7 +104,7 @@ public sealed class DiscoveryService
                     mac = ResolveMacAddress(result.RemoteEndPoint.Address);
                 }
 
-                devicesByIp[response.Lan2Ip] = new DiscoveredDevice
+                var device = new DiscoveredDevice
                 {
                     DeviceId = response.DeviceId ?? string.Empty,
                     Hostname = response.Hostname ?? string.Empty,
@@ -73,6 +114,9 @@ public sealed class DiscoveryService
                     HttpPort = response.HttpPort,
                     AuthRequired = response.AuthRequired,
                 };
+
+                devicesByIp[response.Lan2Ip] = device;
+                onDeviceDiscovered?.Invoke(device);
             }
             catch (OperationCanceledException)
             {