瀏覽代碼

feat(ui): 重构主界面,新增远程设备信息与网络配置面板

yangkaixiang 1 月之前
父節點
當前提交
73bd25a236

+ 144 - 0
windows/QuickIP.Client/DeviceDetailsWindow.xaml

@@ -0,0 +1,144 @@
+<Window x:Class="QuickIP.Client.DeviceDetailsWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        Title="设备信息与接口配置"
+        Height="760"
+        Width="1100"
+        MinHeight="680"
+        MinWidth="1000"
+        WindowStartupLocation="CenterOwner">
+    <Grid Background="#F5F7FB" Margin="20">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+        </Grid.RowDefinitions>
+
+        <TextBlock FontSize="22"
+                   FontWeight="SemiBold"
+                   Foreground="#111827"
+                   Text="设备信息与 Linux 接口" />
+
+        <UniformGrid Grid.Row="1" Margin="0,12,0,0" Columns="4">
+            <Border Margin="0,0,12,0" Padding="14" Background="White" CornerRadius="10">
+                <StackPanel>
+                    <TextBlock FontSize="12" Foreground="#6B7280" Text="设备 ID" />
+                    <TextBlock x:Name="RemoteDeviceIdTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                </StackPanel>
+            </Border>
+            <Border Margin="0,0,12,0" Padding="14" Background="White" CornerRadius="10">
+                <StackPanel>
+                    <TextBlock FontSize="12" Foreground="#6B7280" Text="主机名" />
+                    <TextBlock x:Name="RemoteHostnameTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                </StackPanel>
+            </Border>
+            <Border Margin="0,0,12,0" Padding="14" Background="White" CornerRadius="10">
+                <StackPanel>
+                    <TextBlock FontSize="12" Foreground="#6B7280" Text="Ubuntu 版本" />
+                    <TextBlock x:Name="RemoteOsVersionTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                </StackPanel>
+            </Border>
+            <Border Padding="14" Background="White" CornerRadius="10">
+                <StackPanel>
+                    <TextBlock FontSize="12" Foreground="#6B7280" Text="Agent 版本" />
+                    <TextBlock x:Name="RemoteAgentVersionTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                </StackPanel>
+            </Border>
+        </UniformGrid>
+
+        <Border Grid.Row="2" Margin="0,12,0,0" Padding="14" Background="White" CornerRadius="10">
+            <StackPanel>
+                <TextBlock FontSize="13" FontWeight="SemiBold" Foreground="#111827" Text="目标接口" />
+                <ComboBox x:Name="RemoteTargetInterfaceComboBox"
+                          Margin="0,10,0,0"
+                          MinHeight="36"
+                          DisplayMemberPath="DisplayName"
+                          SelectionChanged="RemoteTargetInterfaceComboBox_OnSelectionChanged" />
+                <TextBlock x:Name="RemoteSummaryTextBlock"
+                           Margin="0,12,0,0"
+                           FontSize="12"
+                           Foreground="#3730A3"
+                           TextWrapping="Wrap"
+                           Text="正在读取 Linux 管理接口和建议目标接口。" />
+            </StackPanel>
+        </Border>
+
+        <Border Grid.Row="3" Margin="0,12,0,0" Padding="14" Background="White" CornerRadius="10">
+            <UniformGrid Columns="4">
+                <Border Margin="0,0,12,0" Padding="12" Background="#EEF2FF" CornerRadius="10">
+                    <StackPanel>
+                        <TextBlock FontSize="12" Foreground="#6B7280" Text="接口名" />
+                        <TextBlock x:Name="RemoteConfigInterfaceTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                    </StackPanel>
+                </Border>
+                <Border Margin="0,0,12,0" Padding="12" Background="#EEF2FF" CornerRadius="10">
+                    <StackPanel>
+                        <TextBlock FontSize="12" Foreground="#6B7280" Text="当前 IP" />
+                        <TextBlock x:Name="RemoteConfigIpTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                    </StackPanel>
+                </Border>
+                <Border Margin="0,0,12,0" Padding="12" Background="#EEF2FF" CornerRadius="10">
+                    <StackPanel>
+                        <TextBlock FontSize="12" Foreground="#6B7280" Text="当前网关" />
+                        <TextBlock x:Name="RemoteConfigGatewayTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                    </StackPanel>
+                </Border>
+                <Border Padding="12" Background="#EEF2FF" CornerRadius="10">
+                    <StackPanel>
+                        <TextBlock FontSize="12" Foreground="#6B7280" Text="当前 DNS" />
+                        <TextBlock x:Name="RemoteConfigDnsTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                    </StackPanel>
+                </Border>
+            </UniformGrid>
+        </Border>
+
+        <Border Grid.Row="4" Margin="0,12,0,0" Padding="14" Background="White" CornerRadius="10">
+            <Grid>
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                </Grid.RowDefinitions>
+
+                <TextBlock FontSize="13" FontWeight="SemiBold" Foreground="#111827" Text="新配置" />
+
+                <StackPanel Grid.Row="1" Margin="0,12,0,0" Orientation="Horizontal">
+                    <Button x:Name="ReloadInterfaceConfigButton" MinHeight="36" Padding="14,0" Click="ReloadInterfaceConfigButton_OnClick" Content="1. 读取当前配置" />
+                    <Button x:Name="ValidateConfigButton" Margin="10,0,0,0" MinHeight="36" Padding="14,0" Click="ValidateConfigButton_OnClick" Content="2. 校验配置" />
+                    <Button x:Name="ApplyConfigButton" Margin="10,0,0,0" MinHeight="36" Padding="14,0" Click="ApplyConfigButton_OnClick" Content="3. 应用配置" />
+                </StackPanel>
+
+                <UniformGrid Grid.Row="2" Margin="0,12,0,0" Columns="2">
+                    <StackPanel Margin="0,0,12,12">
+                        <TextBlock FontSize="12" Foreground="#6B7280" Text="IP 地址" />
+                        <TextBox x:Name="NewIpTextBox" Margin="0,8,0,0" MinHeight="32" TextChanged="ConfigInputChanged_OnChanged" />
+                    </StackPanel>
+                    <StackPanel Margin="0,0,0,12">
+                        <TextBlock FontSize="12" Foreground="#6B7280" Text="子网掩码" />
+                        <TextBox x:Name="NewMaskTextBox" Margin="0,8,0,0" MinHeight="32" TextChanged="ConfigInputChanged_OnChanged" />
+                    </StackPanel>
+                    <StackPanel Margin="0,0,12,0">
+                        <TextBlock FontSize="12" Foreground="#6B7280" Text="网关" />
+                        <TextBox x:Name="NewGatewayTextBox" Margin="0,8,0,0" MinHeight="32" TextChanged="ConfigInputChanged_OnChanged" />
+                    </StackPanel>
+                    <StackPanel Margin="0,0,0,0">
+                        <TextBlock FontSize="12" Foreground="#6B7280" Text="首选 DNS" />
+                        <TextBox x:Name="NewDnsTextBox" Margin="0,8,0,0" MinHeight="32" TextChanged="ConfigInputChanged_OnChanged" />
+                    </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="10" Background="#ECFDF5" CornerRadius="10">
+                    <TextBlock x:Name="ApplyTaskStatusTextBlock" FontSize="12" Foreground="#065F46" TextWrapping="Wrap" Text="尚未提交配置任务。" />
+                </Border>
+            </Grid>
+        </Border>
+    </Grid>
+</Window>

+ 326 - 0
windows/QuickIP.Client/DeviceDetailsWindow.xaml.cs

@@ -0,0 +1,326 @@
+using System.Globalization;
+using System.Windows;
+using System.Windows.Controls;
+using QuickIP.Client.Models;
+using QuickIP.Client.Services;
+
+namespace QuickIP.Client;
+
+public partial class DeviceDetailsWindow : Window
+{
+    private readonly AgentApiService _agentApiService = new();
+    private readonly string _baseAddress;
+    private readonly string _localIPv4;
+    private readonly string _password;
+    private bool _configValidated;
+    private bool _suppressConfigChangeHandling;
+
+    public DeviceDetailsWindow(string baseAddress, string localIPv4, string password)
+    {
+        InitializeComponent();
+        _baseAddress = baseAddress;
+        _localIPv4 = localIPv4;
+        _password = password;
+        Loaded += DeviceDetailsWindow_OnLoaded;
+    }
+
+    private async void DeviceDetailsWindow_OnLoaded(object sender, RoutedEventArgs e)
+    {
+        await LoadRemoteDetailsAsync();
+        UpdateButtonStates();
+    }
+
+    private async Task LoadRemoteDetailsAsync()
+    {
+        ClearDetails();
+        var device = await _agentApiService.GetDeviceInfoAsync(_baseAddress, _password, _localIPv4);
+        if (device is not null)
+        {
+            RemoteDeviceIdTextBlock.Text = device.DeviceId;
+            RemoteHostnameTextBlock.Text = device.Hostname;
+            RemoteOsVersionTextBlock.Text = device.OSVersion;
+            RemoteAgentVersionTextBlock.Text = device.AgentVersion;
+        }
+
+        var interfaces = await _agentApiService.GetInterfacesAsync(_baseAddress, _password, _localIPv4);
+        if (interfaces is null)
+        {
+            RemoteSummaryTextBlock.Text = "设备已连接,但暂时无法读取 Linux 接口列表。";
+            return;
+        }
+
+        RemoteSummaryTextBlock.Text = $"当前管理接口:{interfaces.ManagementInterface};建议目标接口:{interfaces.SuggestedTargetInterface};{(interfaces.RequiresTargetSelection ? "需要手动选择目标接口。" : "已自动识别建议目标接口。")}";
+        var suggested = interfaces.Interfaces.FirstOrDefault(item => item.SystemName == interfaces.SuggestedTargetInterface)
+            ?? interfaces.Interfaces.FirstOrDefault(item => item.IsSuggestedTarget)
+            ?? interfaces.Interfaces.FirstOrDefault(item => !item.IsManagementInterface);
+        RemoteTargetInterfaceComboBox.ItemsSource = interfaces.Interfaces;
+        if (suggested is not null)
+        {
+            RemoteTargetInterfaceComboBox.SelectedItem = suggested;
+            await LoadRemoteInterfaceConfigAsync(suggested.SystemName);
+        }
+    }
+
+    private void ClearDetails()
+    {
+        RemoteDeviceIdTextBlock.Text = "-";
+        RemoteHostnameTextBlock.Text = "-";
+        RemoteOsVersionTextBlock.Text = "-";
+        RemoteAgentVersionTextBlock.Text = "-";
+        RemoteTargetInterfaceComboBox.ItemsSource = null;
+        RemoteConfigInterfaceTextBlock.Text = "-";
+        RemoteConfigIpTextBlock.Text = "-";
+        RemoteConfigGatewayTextBlock.Text = "-";
+        RemoteConfigDnsTextBlock.Text = "-";
+        NewIpTextBox.Text = string.Empty;
+        NewMaskTextBox.Text = string.Empty;
+        NewGatewayTextBox.Text = string.Empty;
+        NewDnsTextBox.Text = string.Empty;
+        ConfigValidationTextBlock.Text = "点击 1/2/3 按顺序操作:先读取当前配置,再校验,最后应用。";
+        ApplyTaskStatusTextBlock.Text = "尚未提交配置任务。";
+        _configValidated = false;
+    }
+
+    private async void RemoteTargetInterfaceComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
+        {
+            return;
+        }
+
+        await LoadRemoteInterfaceConfigAsync(selected.SystemName);
+    }
+
+    private async Task LoadRemoteInterfaceConfigAsync(string interfaceName)
+    {
+        var result = await _agentApiService.GetInterfaceConfigAsync(_baseAddress, _password, _localIPv4, interfaceName);
+        if (!result.Success || result.Data is null)
+        {
+            RemoteConfigInterfaceTextBlock.Text = interfaceName;
+            RemoteConfigIpTextBlock.Text = "读取失败";
+            RemoteConfigGatewayTextBlock.Text = "读取失败";
+            RemoteConfigDnsTextBlock.Text = "读取失败";
+            ConfigValidationTextBlock.Text = $"读取目标接口 {interfaceName} 配置失败:{result.Message}";
+            return;
+        }
+
+        var config = result.Data;
+        RemoteConfigInterfaceTextBlock.Text = config.Interface;
+        RemoteConfigIpTextBlock.Text = string.IsNullOrWhiteSpace(config.IP) ? "无" : $"{config.IP}/{config.Prefix}";
+        RemoteConfigGatewayTextBlock.Text = string.IsNullOrWhiteSpace(config.Gateway) ? "无" : config.Gateway;
+        RemoteConfigDnsTextBlock.Text = config.DnsSummary;
+        _suppressConfigChangeHandling = true;
+        NewIpTextBox.Text = config.IP;
+        NewMaskTextBox.Text = PrefixToMask(config.Prefix);
+        NewGatewayTextBox.Text = config.Gateway;
+        NewDnsTextBox.Text = config.Dns.FirstOrDefault() ?? string.Empty;
+        _suppressConfigChangeHandling = false;
+        _configValidated = false;
+        ConfigValidationTextBlock.Text = "已回填目标接口当前配置,可直接修改后校验。";
+        ApplyTaskStatusTextBlock.Text = "尚未提交配置任务。";
+        UpdateButtonStates();
+    }
+
+    private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
+    {
+        if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
+        {
+            await LoadRemoteInterfaceConfigAsync(selected.SystemName);
+        }
+    }
+
+    private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
+    {
+        if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
+        {
+            return;
+        }
+
+        var request = BuildConfigRequest(selected.SystemName);
+        if (request is null)
+        {
+            return;
+        }
+
+        var result = await _agentApiService.ValidateInterfaceConfigAsync(_baseAddress, _password, _localIPv4, request);
+        _configValidated = result.Success && result.Data?.Valid == true;
+        if (result.Data is not null)
+        {
+            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}";
+        }
+        else
+        {
+            ConfigValidationTextBlock.Text = $"校验失败:{result.Message}";
+        }
+
+        ApplyTaskStatusTextBlock.Text = _configValidated ? "配置已通过校验,可提交应用。" : "当前配置尚未通过校验。";
+        UpdateButtonStates();
+    }
+
+    private async void ApplyConfigButton_OnClick(object sender, RoutedEventArgs e)
+    {
+        if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
+        {
+            return;
+        }
+
+        var request = BuildConfigRequest(selected.SystemName);
+        if (request is null)
+        {
+            return;
+        }
+
+        var confirmMessage = $"将要把以下配置应用到接口 {selected.SystemName}:\n\n" +
+                             $"IP:{request.IP}/{request.Prefix}\n" +
+                             $"网关:{(string.IsNullOrWhiteSpace(request.Gateway) ? "无" : request.Gateway)}\n" +
+                             $"DNS:{(request.Dns.Count == 0 ? "无" : string.Join(", ", request.Dns))}\n\n" +
+                             "请确认是否继续。";
+        if (MessageBox.Show(this, confirmMessage, "确认应用配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
+        {
+            return;
+        }
+
+        var applyResult = await _agentApiService.ApplyInterfaceConfigAsync(_baseAddress, _password, _localIPv4, request);
+        if (!applyResult.Success || applyResult.Data is null)
+        {
+            ApplyTaskStatusTextBlock.Text = $"提交配置任务失败:{applyResult.Message}";
+            return;
+        }
+
+        ApplyTaskStatusTextBlock.Text = $"配置任务已提交:{applyResult.Data.TaskId},正在轮询状态。";
+        await PollTaskAsync(applyResult.Data.TaskId);
+    }
+
+    private async Task PollTaskAsync(string taskId)
+    {
+        for (var i = 0; i < 10; i++)
+        {
+            await Task.Delay(1000);
+            var result = await _agentApiService.GetTaskAsync(_baseAddress, _password, _localIPv4, taskId);
+            if (!result.Success || result.Data is null)
+            {
+                ApplyTaskStatusTextBlock.Text = $"读取任务状态失败:{result.Message}";
+                return;
+            }
+
+            var task = result.Data;
+            ApplyTaskStatusTextBlock.Text = $"任务 {task.TaskId} / {task.Status} / {task.Step} / {task.Detail}";
+            if (task.Status is "success" or "failed" or "rolled_back")
+            {
+                if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
+                {
+                    await LoadRemoteInterfaceConfigAsync(selected.SystemName);
+                }
+
+                return;
+            }
+        }
+
+        ApplyTaskStatusTextBlock.Text = $"任务 {taskId} 轮询超时,请稍后手动刷新。";
+    }
+
+    private RemoteInterfaceConfig? BuildConfigRequest(string interfaceName)
+    {
+        if (string.IsNullOrWhiteSpace(NewIpTextBox.Text))
+        {
+            ConfigValidationTextBlock.Text = "IP 地址不能为空。";
+            return null;
+        }
+
+        if (!TryMaskToPrefix(NewMaskTextBox.Text, out var prefix))
+        {
+            ConfigValidationTextBlock.Text = "子网掩码格式不正确。";
+            return null;
+        }
+
+        var dns = string.IsNullOrWhiteSpace(NewDnsTextBox.Text) ? Array.Empty<string>() : new[] { NewDnsTextBox.Text.Trim() };
+        return new RemoteInterfaceConfig
+        {
+            Interface = interfaceName,
+            IP = NewIpTextBox.Text.Trim(),
+            Prefix = prefix,
+            Gateway = NewGatewayTextBox.Text.Trim(),
+            Dns = dns,
+        };
+    }
+
+    private static string PrefixToMask(int prefix)
+    {
+        if (prefix < 0 || prefix > 32)
+        {
+            return string.Empty;
+        }
+
+        var mask = prefix == 0 ? 0u : uint.MaxValue << (32 - prefix);
+        return string.Join('.', new[] { (mask >> 24) & 255, (mask >> 16) & 255, (mask >> 8) & 255, mask & 255 });
+    }
+
+    private static bool TryMaskToPrefix(string maskText, out int prefix)
+    {
+        prefix = 0;
+        if (string.IsNullOrWhiteSpace(maskText))
+        {
+            return false;
+        }
+
+        var parts = maskText.Trim().Split('.');
+        if (parts.Length != 4)
+        {
+            return false;
+        }
+
+        uint mask = 0;
+        foreach (var part in parts)
+        {
+            if (!byte.TryParse(part, out var octet))
+            {
+                return false;
+            }
+
+            mask = (mask << 8) | octet;
+        }
+
+        var seenZero = false;
+        for (var i = 31; i >= 0; i--)
+        {
+            var bit = (mask & (1u << i)) != 0;
+            if (bit && seenZero)
+            {
+                return false;
+            }
+
+            if (bit)
+            {
+                prefix++;
+            }
+            else
+            {
+                seenZero = true;
+            }
+        }
+
+        return true;
+    }
+
+    private void ConfigInputChanged_OnChanged(object sender, TextChangedEventArgs e)
+    {
+        if (_suppressConfigChangeHandling)
+        {
+            return;
+        }
+
+        _configValidated = false;
+        ConfigValidationTextBlock.Text = "配置内容已变更,请重新点击“2. 校验配置”。";
+        ApplyTaskStatusTextBlock.Text = "当前配置尚未通过校验。";
+        UpdateButtonStates();
+    }
+
+    private void UpdateButtonStates()
+    {
+        ReloadInterfaceConfigButton.IsEnabled = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
+        ValidateConfigButton.IsEnabled = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
+        ApplyConfigButton.IsEnabled = _configValidated && RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
+    }
+}

+ 170 - 50
windows/QuickIP.Client/MainWindow.xaml

@@ -12,29 +12,11 @@
          WindowStartupLocation="CenterScreen">
     <Grid Background="#F5F7FB">
         <Grid.RowDefinitions>
-            <RowDefinition Height="Auto" />
             <RowDefinition Height="*" />
             <RowDefinition Height="Auto" />
         </Grid.RowDefinitions>
 
-        <Border Grid.Row="0"
-                Margin="24,24,24,16"
-                Padding="24"
-                Background="White"
-                CornerRadius="12">
-            <StackPanel>
-                <TextBlock FontSize="28"
-                           FontWeight="SemiBold"
-                           Foreground="#111827"
-                           Text="QuickIP 连接页" />
-                <TextBlock Margin="0,8,0,0"
-                           FontSize="14"
-                           Foreground="#4B5563"
-                           Text="先选择本机有线网卡,客户端会优先尝试直连管理口;只有直连失败时,才需要切换到维护网络。" />
-            </StackPanel>
-        </Border>
-
-        <Grid Grid.Row="1" Margin="24,0,24,16">
+        <Grid Grid.Row="0" Margin="24,24,24,16">
             <Grid.ColumnDefinitions>
                 <ColumnDefinition Width="2.2*" />
                 <ColumnDefinition Width="1.4*" />
@@ -91,34 +73,7 @@
                         </Border>
                     </StackPanel>
 
-                    <UniformGrid Grid.Row="2" Margin="0,24,0,0" Columns="2">
-                        <Border Margin="0,0,12,12" Padding="16" Background="#F9FAFB" CornerRadius="10">
-                            <StackPanel>
-                                <TextBlock FontSize="12" Foreground="#6B7280" Text="网卡名称 / 级别" />
-                                <TextBlock x:Name="AdapterNameTextBlock" Margin="0,8,0,0" FontSize="16" FontWeight="SemiBold" Foreground="#111827" Text="-" />
-                            </StackPanel>
-                        </Border>
-                        <Border Margin="0,0,0,12" Padding="16" Background="#F9FAFB" CornerRadius="10">
-                            <StackPanel>
-                                <TextBlock FontSize="12" Foreground="#6B7280" Text="链路状态" />
-                                <TextBlock x:Name="AdapterLinkTextBlock" Margin="0,8,0,0" FontSize="16" FontWeight="SemiBold" Foreground="#111827" Text="-" />
-                            </StackPanel>
-                        </Border>
-                        <Border Margin="0,0,12,0" Padding="16" Background="#F9FAFB" CornerRadius="10">
-                            <StackPanel>
-                                <TextBlock FontSize="12" Foreground="#6B7280" Text="当前 IPv4" />
-                                <TextBlock x:Name="AdapterIPv4TextBlock" Margin="0,8,0,0" FontSize="16" FontWeight="SemiBold" Foreground="#111827" Text="-" />
-                            </StackPanel>
-                        </Border>
-                        <Border Margin="0,0,0,0" Padding="16" Background="#F9FAFB" CornerRadius="10">
-                            <StackPanel>
-                                <TextBlock FontSize="12" Foreground="#6B7280" Text="网卡类型" />
-                                <TextBlock x:Name="AdapterTypeTextBlock" Margin="0,8,0,0" FontSize="16" FontWeight="SemiBold" Foreground="#111827" Text="-" />
-                            </StackPanel>
-                        </Border>
-                    </UniformGrid>
-
-                    <Border Grid.Row="2" Margin="0,156,0,0" Padding="16" Background="#ECFDF5" CornerRadius="10" VerticalAlignment="Top">
+                    <Border Grid.Row="2" Margin="0,24,0,0" Padding="16" Background="#ECFDF5" CornerRadius="10" VerticalAlignment="Top">
                         <StackPanel>
                             <TextBlock FontSize="12" Foreground="#065F46" Text="管理口探测结果" />
                             <TextBlock x:Name="AdapterProbeTextBlock"
@@ -257,13 +212,178 @@
             </Border>
         </Grid>
 
-        <Border Grid.Row="2"
+        <Border Grid.Row="1"
+                x:Name="RemoteDetailsBorder"
+                Visibility="Collapsed"
                 Margin="24,0,24,24"
                 Padding="16"
                 Background="White"
                 CornerRadius="12">
-            <TextBlock Foreground="#6B7280"
-                       Text="当前版本已支持推荐网卡、本机密码保存、切换到维护网络、UDP 发现和最小 HTTP 连接验证。点击按钮后会在右侧日志里显示详细步骤。" />
+            <Grid>
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                </Grid.RowDefinitions>
+
+                <TextBlock FontSize="18"
+                           FontWeight="SemiBold"
+                           Foreground="#111827"
+                           Text="设备信息与 Linux 接口" />
+
+                <UniformGrid Grid.Row="1" Margin="0,16,0,0" Columns="4">
+                    <Border Margin="0,0,12,0" Padding="14" Background="#F9FAFB" CornerRadius="10">
+                        <StackPanel>
+                            <TextBlock FontSize="12" Foreground="#6B7280" Text="设备 ID" />
+                            <TextBlock x:Name="RemoteDeviceIdTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                        </StackPanel>
+                    </Border>
+                    <Border Margin="0,0,12,0" Padding="14" Background="#F9FAFB" CornerRadius="10">
+                        <StackPanel>
+                            <TextBlock FontSize="12" Foreground="#6B7280" Text="主机名" />
+                            <TextBlock x:Name="RemoteHostnameTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                        </StackPanel>
+                    </Border>
+                    <Border Margin="0,0,12,0" Padding="14" Background="#F9FAFB" CornerRadius="10">
+                        <StackPanel>
+                            <TextBlock FontSize="12" Foreground="#6B7280" Text="Ubuntu 版本" />
+                            <TextBlock x:Name="RemoteOsVersionTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                        </StackPanel>
+                    </Border>
+                    <Border Padding="14" Background="#F9FAFB" CornerRadius="10">
+                        <StackPanel>
+                            <TextBlock FontSize="12" Foreground="#6B7280" Text="Agent 版本" />
+                            <TextBlock x:Name="RemoteAgentVersionTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                        </StackPanel>
+                    </Border>
+                </UniformGrid>
+
+                <Border Grid.Row="2" Margin="0,16,0,0" Padding="16" Background="#F9FAFB" CornerRadius="10">
+                    <Grid>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                        </Grid.RowDefinitions>
+
+                        <TextBlock FontSize="13" FontWeight="SemiBold" Foreground="#111827" Text="目标接口" />
+                        <ComboBox x:Name="RemoteTargetInterfaceComboBox"
+                                  Grid.Row="1"
+                                  Margin="0,12,0,0"
+                                  MinHeight="36"
+                                  DisplayMemberPath="DisplayName"
+                                  SelectionChanged="RemoteTargetInterfaceComboBox_OnSelectionChanged" />
+
+                        <UniformGrid Grid.Row="2" Margin="0,16,0,0" Columns="4">
+                            <Border Margin="0,0,12,0" Padding="12" Background="#EEF2FF" CornerRadius="10">
+                                <StackPanel>
+                                    <TextBlock FontSize="12" Foreground="#6B7280" Text="接口名" />
+                                    <TextBlock x:Name="RemoteConfigInterfaceTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                                </StackPanel>
+                            </Border>
+                            <Border Margin="0,0,12,0" Padding="12" Background="#EEF2FF" CornerRadius="10">
+                                <StackPanel>
+                                    <TextBlock FontSize="12" Foreground="#6B7280" Text="当前 IP" />
+                                    <TextBlock x:Name="RemoteConfigIpTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                                </StackPanel>
+                            </Border>
+                            <Border Margin="0,0,12,0" Padding="12" Background="#EEF2FF" CornerRadius="10">
+                                <StackPanel>
+                                    <TextBlock FontSize="12" Foreground="#6B7280" Text="当前网关" />
+                                    <TextBlock x:Name="RemoteConfigGatewayTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                                </StackPanel>
+                            </Border>
+                            <Border Padding="12" Background="#EEF2FF" CornerRadius="10">
+                                <StackPanel>
+                                    <TextBlock FontSize="12" Foreground="#6B7280" Text="当前 DNS" />
+                                    <TextBlock x:Name="RemoteConfigDnsTextBlock" Margin="0,8,0,0" FontSize="14" FontWeight="SemiBold" Foreground="#111827" Text="-" />
+                                </StackPanel>
+                            </Border>
+                        </UniformGrid>
+                    </Grid>
+                </Border>
+
+                <Border Grid.Row="3" Margin="0,16,0,0" Padding="12" Background="#EEF2FF" CornerRadius="10">
+                    <TextBlock x:Name="RemoteSummaryTextBlock"
+                                FontSize="12"
+                                Foreground="#3730A3"
+                                TextWrapping="Wrap"
+                                Text="连接成功后,这里会显示 Linux 管理接口和建议目标接口。" />
+                </Border>
+
+                <Border Grid.Row="4" Margin="0,12,0,0" Padding="12" Background="#F9FAFB" CornerRadius="10">
+                    <Grid>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                        </Grid.RowDefinitions>
+
+                        <TextBlock FontSize="13" FontWeight="SemiBold" Foreground="#111827" Text="新配置" />
+
+                        <StackPanel Grid.Row="1" Margin="0,12,0,0" Orientation="Horizontal">
+                            <Button x:Name="ReloadInterfaceConfigButton"
+                                    MinHeight="36"
+                                    Padding="14,0"
+                                    Click="ReloadInterfaceConfigButton_OnClick"
+                                    Content="1. 读取当前配置" />
+                            <Button x:Name="ValidateConfigButton"
+                                    Margin="10,0,0,0"
+                                    MinHeight="36"
+                                    Padding="14,0"
+                                    Click="ValidateConfigButton_OnClick"
+                                    Content="2. 校验配置" />
+                            <Button x:Name="ApplyConfigButton"
+                                    Margin="10,0,0,0"
+                                    MinHeight="36"
+                                    Padding="14,0"
+                                    Click="ApplyConfigButton_OnClick"
+                                    Content="3. 应用配置" />
+                        </StackPanel>
+
+                        <UniformGrid Grid.Row="2" Margin="0,12,0,0" Columns="2">
+                            <StackPanel Margin="0,0,12,12">
+                                <TextBlock FontSize="12" Foreground="#6B7280" Text="IP 地址" />
+                                <TextBox x:Name="NewIpTextBox" Margin="0,8,0,0" MinHeight="32" TextChanged="ConfigInputChanged_OnChanged" />
+                            </StackPanel>
+                            <StackPanel Margin="0,0,0,12">
+                                <TextBlock FontSize="12" Foreground="#6B7280" Text="子网掩码" />
+                                <TextBox x:Name="NewMaskTextBox" Margin="0,8,0,0" MinHeight="32" TextChanged="ConfigInputChanged_OnChanged" />
+                            </StackPanel>
+                            <StackPanel Margin="0,0,12,0">
+                                <TextBlock FontSize="12" Foreground="#6B7280" Text="网关" />
+                                <TextBox x:Name="NewGatewayTextBox" Margin="0,8,0,0" MinHeight="32" TextChanged="ConfigInputChanged_OnChanged" />
+                            </StackPanel>
+                            <StackPanel Margin="0,0,0,0">
+                                <TextBlock FontSize="12" Foreground="#6B7280" Text="首选 DNS" />
+                                <TextBox x:Name="NewDnsTextBox" Margin="0,8,0,0" MinHeight="32" TextChanged="ConfigInputChanged_OnChanged" />
+                            </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="10" Background="#ECFDF5" CornerRadius="10">
+                            <TextBlock x:Name="ApplyTaskStatusTextBlock"
+                                       FontSize="12"
+                                       Foreground="#065F46"
+                                       TextWrapping="Wrap"
+                                       Text="尚未提交配置任务。" />
+                        </Border>
+                    </Grid>
+                </Border>
+            </Grid>
         </Border>
     </Grid>
 </Window>

+ 346 - 9
windows/QuickIP.Client/MainWindow.xaml.cs

@@ -16,6 +16,10 @@ public partial class MainWindow : Window
     private readonly AgentApiService _agentApiService = new();
     private readonly AdminPrivilegeService _adminPrivilegeService = new();
     private IReadOnlyList<AdapterInfo> _adapters = [];
+    private string _connectedBaseAddress = string.Empty;
+    private string _connectedLocalIPv4 = string.Empty;
+    private bool _configValidated;
+    private bool _suppressConfigChangeHandling;
     private bool _isShowingPassword;
     private bool _suppressPasswordSync;
 
@@ -77,6 +81,7 @@ public partial class MainWindow : Window
         }
 
         AppendLog("客户端已加载连接页。", true);
+        ClearRemoteDetails();
         UpdateButtonStates();
     }
 
@@ -85,12 +90,14 @@ public partial class MainWindow : Window
         if (AdapterComboBox.SelectedItem is not AdapterInfo adapter)
         {
             UpdateAdapterDetails(null);
+            ClearRemoteDetails();
             SetStatus("请选择一块有线网卡。", false);
             UpdateButtonStates();
             return;
         }
 
         UpdateAdapterDetails(adapter);
+        ClearRemoteDetails();
         SetStatus(adapter.HasLink
             ? $"已选择 {adapter.RecommendationLabel} 网卡,可切换到维护网络。{adapter.RecommendationReason}"
             : "当前网卡未检测到链路,请检查网线连接。", true);
@@ -164,8 +171,11 @@ public partial class MainWindow : Window
                 var directResult = await _agentApiService.CheckHealthAsync("http://169.254.100.2:48888", GetCurrentPassword(), adapter.IPv4Address);
                 if (directResult.Success)
                 {
+                    _connectedBaseAddress = "http://169.254.100.2:48888";
+                    _connectedLocalIPv4 = adapter.IPv4Address;
                     DiscoveredDeviceTextBlock.Text = $"已直接访问管理口:169.254.100.2 / {adapter.Name}";
                     SetStatus("连接成功,无需切换本机网卡。", true);
+                    OpenDeviceDetailsWindow(_connectedBaseAddress, _connectedLocalIPv4);
                     return;
                 }
 
@@ -199,10 +209,21 @@ public partial class MainWindow : Window
             SetStatus("已发现设备,正在验证连接。", true);
 
             var discoveredResult = await _agentApiService.CheckHealthAsync($"http://{device.Lan2Ip}:48888", GetCurrentPassword(), selectedAdapter?.IPv4Address ?? string.Empty);
-            SetStatus(discoveredResult.Success ? "连接成功。" : $"设备已发现,但 HTTP 验证失败:{discoveredResult.Message}", true);
+            if (discoveredResult.Success)
+            {
+                _connectedBaseAddress = $"http://{device.Lan2Ip}:48888";
+                _connectedLocalIPv4 = selectedAdapter?.IPv4Address ?? string.Empty;
+                SetStatus("连接成功。", true);
+                OpenDeviceDetailsWindow(_connectedBaseAddress, _connectedLocalIPv4);
+            }
+            else
+            {
+                SetStatus($"设备已发现,但 HTTP 验证失败:{discoveredResult.Message}", true);
+            }
         }
         catch (Exception ex)
         {
+            ClearRemoteDetails();
             SetStatus($"连接失败:{ex.Message}", true);
             MessageBox.Show(this, ex.Message, "连接失败", MessageBoxButton.OK, MessageBoxImage.Error);
         }
@@ -233,6 +254,9 @@ public partial class MainWindow : Window
         var hasAdapter = adapter is not null;
         SwitchMaintenanceButton.IsEnabled = hasAdapter;
         DiscoverConnectButton.IsEnabled = hasAdapter && adapter!.HasLink;
+        ReloadInterfaceConfigButton.IsEnabled = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
+        ValidateConfigButton.IsEnabled = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
+        ApplyConfigButton.IsEnabled = _configValidated && RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
     }
 
     private async Task RefreshAdaptersAsync(string? selectedAdapterId = null)
@@ -263,21 +287,334 @@ public partial class MainWindow : Window
     {
         if (adapter is null)
         {
-            AdapterNameTextBlock.Text = "-";
-            AdapterLinkTextBlock.Text = "-";
-            AdapterIPv4TextBlock.Text = "-";
-            AdapterTypeTextBlock.Text = "-";
             AdapterProbeTextBlock.Text = "-";
             return;
         }
 
-        AdapterNameTextBlock.Text = $"{adapter.Description} / {adapter.RecommendationLabel}";
-        AdapterLinkTextBlock.Text = adapter.HasLink ? "已连接" : "未连接";
-        AdapterIPv4TextBlock.Text = string.IsNullOrWhiteSpace(adapter.IPv4Address) ? "无" : adapter.IPv4Address;
-        AdapterTypeTextBlock.Text = adapter.Type;
         AdapterProbeTextBlock.Text = $"{adapter.ProbeStatus} / {adapter.ProbeReason}";
     }
 
+    private async Task LoadRemoteDetailsAsync(string baseAddress, string localIPv4)
+    {
+        ClearRemoteDetails();
+        RemoteDetailsBorder.Visibility = Visibility.Visible;
+        SetStatus("正在读取设备信息。", true);
+        var device = await _agentApiService.GetDeviceInfoAsync(baseAddress, GetCurrentPassword(), localIPv4);
+        if (device is not null)
+        {
+            RemoteDeviceIdTextBlock.Text = device.DeviceId;
+            RemoteHostnameTextBlock.Text = device.Hostname;
+            RemoteOsVersionTextBlock.Text = device.OSVersion;
+            RemoteAgentVersionTextBlock.Text = device.AgentVersion;
+        }
+
+        SetStatus("正在读取 Linux 接口列表。", true);
+        var interfaces = await _agentApiService.GetInterfacesAsync(baseAddress, GetCurrentPassword(), localIPv4);
+        if (interfaces is not null)
+        {
+            RemoteSummaryTextBlock.Text = $"当前管理接口:{interfaces.ManagementInterface};建议目标接口:{interfaces.SuggestedTargetInterface};{(interfaces.RequiresTargetSelection ? "需要手动选择目标接口。" : "已自动识别建议目标接口。")}";
+
+            var suggested = interfaces.Interfaces.FirstOrDefault(item => item.SystemName == interfaces.SuggestedTargetInterface)
+                ?? interfaces.Interfaces.FirstOrDefault(item => item.IsSuggestedTarget)
+                ?? interfaces.Interfaces.FirstOrDefault(item => !item.IsManagementInterface);
+
+            RemoteTargetInterfaceComboBox.ItemsSource = interfaces.Interfaces;
+            if (suggested is not null)
+            {
+                RemoteTargetInterfaceComboBox.SelectedItem = suggested;
+                await LoadRemoteInterfaceConfigAsync(suggested.SystemName);
+            }
+
+            SetStatus("已加载设备信息和 Linux 接口列表。", true);
+            return;
+        }
+
+        RemoteSummaryTextBlock.Text = "设备已连接,但暂时无法读取 Linux 接口列表。";
+    }
+
+    private void OpenDeviceDetailsWindow(string baseAddress, string localIPv4)
+    {
+        var window = new DeviceDetailsWindow(baseAddress, localIPv4, GetCurrentPassword())
+        {
+            Owner = this,
+        };
+        window.ShowDialog();
+    }
+
+    private void ClearRemoteDetails()
+    {
+        RemoteDetailsBorder.Visibility = Visibility.Collapsed;
+        RemoteDeviceIdTextBlock.Text = "-";
+        RemoteHostnameTextBlock.Text = "-";
+        RemoteOsVersionTextBlock.Text = "-";
+        RemoteAgentVersionTextBlock.Text = "-";
+        RemoteTargetInterfaceComboBox.ItemsSource = null;
+        RemoteConfigInterfaceTextBlock.Text = "-";
+        RemoteConfigIpTextBlock.Text = "-";
+        RemoteConfigGatewayTextBlock.Text = "-";
+        RemoteConfigDnsTextBlock.Text = "-";
+        NewIpTextBox.Text = string.Empty;
+        NewMaskTextBox.Text = string.Empty;
+        NewGatewayTextBox.Text = string.Empty;
+        NewDnsTextBox.Text = string.Empty;
+        ConfigValidationTextBlock.Text = "读取目标接口当前配置后,可在此修改并校验。";
+        ApplyTaskStatusTextBlock.Text = "尚未提交配置任务。";
+        _configValidated = false;
+        RemoteSummaryTextBlock.Text = "连接成功后,这里会显示 Linux 管理接口和建议目标接口。";
+        UpdateButtonStates();
+    }
+
+    private async void RemoteTargetInterfaceComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected || string.IsNullOrWhiteSpace(_connectedBaseAddress))
+        {
+            return;
+        }
+
+        await LoadRemoteInterfaceConfigAsync(selected.SystemName);
+    }
+
+    private async Task LoadRemoteInterfaceConfigAsync(string interfaceName)
+    {
+        SetStatus($"正在读取目标接口 {interfaceName} 当前配置。", true);
+        var result = await _agentApiService.GetInterfaceConfigAsync(_connectedBaseAddress, GetCurrentPassword(), _connectedLocalIPv4, interfaceName);
+        if (!result.Success || result.Data is null)
+        {
+            RemoteConfigInterfaceTextBlock.Text = interfaceName;
+            RemoteConfigIpTextBlock.Text = "读取失败";
+            RemoteConfigGatewayTextBlock.Text = "读取失败";
+            RemoteConfigDnsTextBlock.Text = "读取失败";
+            SetStatus($"读取目标接口 {interfaceName} 配置失败:{result.Message}", true);
+            return;
+        }
+
+        var config = result.Data;
+        RemoteConfigInterfaceTextBlock.Text = config.Interface;
+        RemoteConfigIpTextBlock.Text = string.IsNullOrWhiteSpace(config.IP) ? "无" : $"{config.IP}/{config.Prefix}";
+        RemoteConfigGatewayTextBlock.Text = string.IsNullOrWhiteSpace(config.Gateway) ? "无" : config.Gateway;
+        RemoteConfigDnsTextBlock.Text = config.DnsSummary;
+        _suppressConfigChangeHandling = true;
+        NewIpTextBox.Text = config.IP;
+        NewMaskTextBox.Text = PrefixToMask(config.Prefix);
+        NewGatewayTextBox.Text = config.Gateway;
+        NewDnsTextBox.Text = config.Dns.FirstOrDefault() ?? string.Empty;
+        _suppressConfigChangeHandling = false;
+        _configValidated = false;
+        ConfigValidationTextBlock.Text = "已回填目标接口当前配置,可直接修改后校验。";
+        ApplyTaskStatusTextBlock.Text = "尚未提交配置任务。";
+        UpdateButtonStates();
+        SetStatus($"已读取目标接口 {interfaceName} 当前配置。", true);
+    }
+
+    private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
+    {
+        if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
+        {
+            return;
+        }
+
+        await LoadRemoteInterfaceConfigAsync(selected.SystemName);
+    }
+
+    private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
+    {
+        if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
+        {
+            return;
+        }
+
+        var request = BuildConfigRequest(selected.SystemName);
+        if (request is null)
+        {
+            return;
+        }
+
+        SetStatus($"正在校验目标接口 {selected.SystemName} 的新配置。", true);
+        var result = await _agentApiService.ValidateInterfaceConfigAsync(_connectedBaseAddress, GetCurrentPassword(), _connectedLocalIPv4, request);
+        _configValidated = result.Success && result.Data?.Valid == true;
+        if (result.Data is not null)
+        {
+            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}";
+        }
+        else
+        {
+            ConfigValidationTextBlock.Text = $"校验失败:{result.Message}";
+        }
+
+        ApplyTaskStatusTextBlock.Text = _configValidated ? "配置已通过校验,可提交应用。" : "当前配置尚未通过校验。";
+        UpdateButtonStates();
+    }
+
+    private async void ApplyConfigButton_OnClick(object sender, RoutedEventArgs e)
+    {
+        if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
+        {
+            return;
+        }
+
+        var request = BuildConfigRequest(selected.SystemName);
+        if (request is null)
+        {
+            return;
+        }
+
+        var confirmMessage = $"将要把以下配置应用到接口 {selected.SystemName}:\n\n" +
+                             $"IP:{request.IP}/{request.Prefix}\n" +
+                             $"网关:{(string.IsNullOrWhiteSpace(request.Gateway) ? "无" : request.Gateway)}\n" +
+                             $"DNS:{(request.Dns.Count == 0 ? "无" : string.Join(", ", request.Dns))}\n\n" +
+                             "请确认是否继续。";
+        if (MessageBox.Show(this, confirmMessage, "确认应用配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
+        {
+            return;
+        }
+
+        SetStatus($"正在提交目标接口 {selected.SystemName} 的配置任务。", true);
+        var applyResult = await _agentApiService.ApplyInterfaceConfigAsync(_connectedBaseAddress, GetCurrentPassword(), _connectedLocalIPv4, request);
+        if (!applyResult.Success || applyResult.Data is null)
+        {
+            ApplyTaskStatusTextBlock.Text = $"提交配置任务失败:{applyResult.Message}";
+            return;
+        }
+
+        ApplyTaskStatusTextBlock.Text = $"配置任务已提交:{applyResult.Data.TaskId},正在轮询状态。";
+        await PollTaskAsync(applyResult.Data.TaskId);
+    }
+
+    private async Task PollTaskAsync(string taskId)
+    {
+        for (var i = 0; i < 10; i++)
+        {
+            await Task.Delay(1000);
+            var result = await _agentApiService.GetTaskAsync(_connectedBaseAddress, GetCurrentPassword(), _connectedLocalIPv4, taskId);
+            if (!result.Success || result.Data is null)
+            {
+                ApplyTaskStatusTextBlock.Text = $"读取任务状态失败:{result.Message}";
+                return;
+            }
+
+            var task = result.Data;
+            ApplyTaskStatusTextBlock.Text = $"任务 {task.TaskId} / {task.Status} / {task.Step} / {task.Detail}";
+            if (task.Status is "success" or "failed" or "rolled_back")
+            {
+                if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
+                {
+                    await LoadRemoteInterfaceConfigAsync(selected.SystemName);
+                }
+
+                return;
+            }
+        }
+
+        ApplyTaskStatusTextBlock.Text = $"任务 {taskId} 轮询超时,请稍后手动刷新。";
+    }
+
+    private RemoteInterfaceConfig? BuildConfigRequest(string interfaceName)
+    {
+        if (string.IsNullOrWhiteSpace(NewIpTextBox.Text))
+        {
+            ConfigValidationTextBlock.Text = "IP 地址不能为空。";
+            return null;
+        }
+
+        if (!TryMaskToPrefix(NewMaskTextBox.Text, out var prefix))
+        {
+            ConfigValidationTextBlock.Text = "子网掩码格式不正确。";
+            return null;
+        }
+
+        var dns = string.IsNullOrWhiteSpace(NewDnsTextBox.Text)
+            ? Array.Empty<string>()
+            : new[] { NewDnsTextBox.Text.Trim() };
+
+        return new RemoteInterfaceConfig
+        {
+            Interface = interfaceName,
+            IP = NewIpTextBox.Text.Trim(),
+            Prefix = prefix,
+            Gateway = NewGatewayTextBox.Text.Trim(),
+            Dns = dns,
+        };
+    }
+
+    private static string PrefixToMask(int prefix)
+    {
+        if (prefix < 0 || prefix > 32)
+        {
+            return string.Empty;
+        }
+
+        var mask = prefix == 0 ? 0u : uint.MaxValue << (32 - prefix);
+        return string.Join('.', new[]
+        {
+            (mask >> 24) & 255,
+            (mask >> 16) & 255,
+            (mask >> 8) & 255,
+            mask & 255,
+        });
+    }
+
+    private static bool TryMaskToPrefix(string maskText, out int prefix)
+    {
+        prefix = 0;
+        if (string.IsNullOrWhiteSpace(maskText))
+        {
+            return false;
+        }
+
+        var parts = maskText.Trim().Split('.');
+        if (parts.Length != 4)
+        {
+            return false;
+        }
+
+        uint mask = 0;
+        foreach (var part in parts)
+        {
+            if (!byte.TryParse(part, out var octet))
+            {
+                return false;
+            }
+
+            mask = (mask << 8) | octet;
+        }
+
+        var seenZero = false;
+        for (var i = 31; i >= 0; i--)
+        {
+            var bit = (mask & (1u << i)) != 0;
+            if (bit && seenZero)
+            {
+                return false;
+            }
+
+            if (bit)
+            {
+                prefix++;
+            }
+            else
+            {
+                seenZero = true;
+            }
+        }
+
+        return true;
+    }
+
+    private void ConfigInputChanged_OnChanged(object sender, TextChangedEventArgs e)
+    {
+        if (_suppressConfigChangeHandling)
+        {
+            return;
+        }
+
+        _configValidated = false;
+        ConfigValidationTextBlock.Text = "配置内容已变更,请重新点击“校验配置”。";
+        ApplyTaskStatusTextBlock.Text = "当前配置尚未通过校验。";
+        UpdateButtonStates();
+    }
+
     private void SetStatus(string message, bool addLog)
     {
         StatusTextBlock.Text = message;

+ 1 - 1
windows/QuickIP.Client/Models/AdapterInfo.cs

@@ -18,7 +18,7 @@ public sealed class AdapterInfo
     public string ProbeReason { get; set; } = "尚未对 169.254.100.2 进行可达性探测。";
     public bool IsReachableToMaintenance { get; set; }
 
-    public string DisplayName => $"{RecommendationLabel} | {Name} ({Type}) | {ProbeStatus}";
+    public string DisplayName => $"{RecommendationLabel} | {Name} | {Type} | IPv4: {(string.IsNullOrWhiteSpace(IPv4Address) ? "无" : IPv4Address)} | {ProbeStatus}";
 
     public static AdapterInfo FromNetworkInterface(
         NetworkInterface adapter,

+ 9 - 0
windows/QuickIP.Client/Models/ApiCallResult.cs

@@ -0,0 +1,9 @@
+namespace QuickIP.Client.Models;
+
+public sealed class ApiCallResult<T>
+{
+    public bool Success { get; init; }
+    public int? StatusCode { get; init; }
+    public string Message { get; init; } = string.Empty;
+    public T? Data { get; init; }
+}

+ 11 - 0
windows/QuickIP.Client/Models/DiscoveredDevice.cs

@@ -1,10 +1,21 @@
+using System.Text.Json.Serialization;
+
 namespace QuickIP.Client.Models;
 
 public sealed class DiscoveredDevice
 {
+    [JsonPropertyName("device_id")]
     public required string DeviceId { get; init; }
+
+    [JsonPropertyName("hostname")]
     public required string Hostname { get; init; }
+
+    [JsonPropertyName("agent_version")]
     public required string AgentVersion { get; init; }
+
+    [JsonPropertyName("lan2_ip")]
     public required string Lan2Ip { get; init; }
+
+    [JsonPropertyName("auth_required")]
     public required bool AuthRequired { get; init; }
 }

+ 12 - 0
windows/QuickIP.Client/Models/RemoteApplyTaskResponse.cs

@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace QuickIP.Client.Models;
+
+public sealed class RemoteApplyTaskResponse
+{
+    [JsonPropertyName("interface")]
+    public string Interface { get; init; } = string.Empty;
+
+    [JsonPropertyName("task_id")]
+    public string TaskId { get; init; } = string.Empty;
+}

+ 21 - 0
windows/QuickIP.Client/Models/RemoteDeviceInfo.cs

@@ -0,0 +1,21 @@
+using System.Text.Json.Serialization;
+
+namespace QuickIP.Client.Models;
+
+public sealed class RemoteDeviceInfo
+{
+    [JsonPropertyName("device_id")]
+    public string DeviceId { get; init; } = string.Empty;
+
+    [JsonPropertyName("hostname")]
+    public string Hostname { get; init; } = string.Empty;
+
+    [JsonPropertyName("os_version")]
+    public string OSVersion { get; init; } = string.Empty;
+
+    [JsonPropertyName("agent_version")]
+    public string AgentVersion { get; init; } = string.Empty;
+
+    [JsonPropertyName("uptime_seconds")]
+    public long UptimeSeconds { get; init; }
+}

+ 12 - 0
windows/QuickIP.Client/Models/RemoteInterfaceConfig.cs

@@ -0,0 +1,12 @@
+namespace QuickIP.Client.Models;
+
+public sealed class RemoteInterfaceConfig
+{
+    public string Interface { get; init; } = string.Empty;
+    public string IP { get; init; } = string.Empty;
+    public int Prefix { get; init; }
+    public string Gateway { get; init; } = string.Empty;
+    public IReadOnlyList<string> Dns { get; init; } = [];
+
+    public string DnsSummary => Dns.Count == 0 ? "无" : string.Join(", ", Dns);
+}

+ 71 - 0
windows/QuickIP.Client/Models/RemoteInterfaceInfo.cs

@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json.Serialization;
+
+namespace QuickIP.Client.Models;
+
+public sealed class RemoteInterfaceAddress
+{
+    [JsonPropertyName("address")]
+    public string Address { get; init; } = string.Empty;
+
+    [JsonPropertyName("prefix")]
+    public int Prefix { get; init; }
+
+    [JsonPropertyName("source")]
+    public string Source { get; init; } = string.Empty;
+}
+
+public sealed class RemoteInterfaceInfo
+{
+    [JsonPropertyName("name")]
+    public string Name { get; init; } = string.Empty;
+
+    [JsonPropertyName("system_name")]
+    public string SystemName { get; init; } = string.Empty;
+
+    [JsonPropertyName("role")]
+    public string Role { get; init; } = string.Empty;
+
+    [JsonPropertyName("link_up")]
+    public bool LinkUp { get; init; }
+
+    [JsonPropertyName("is_management_interface")]
+    public bool IsManagementInterface { get; init; }
+
+    [JsonPropertyName("is_suggested_target")]
+    public bool IsSuggestedTarget { get; init; }
+
+    [JsonPropertyName("mac")]
+    public string Mac { get; init; } = string.Empty;
+
+    [JsonPropertyName("gateway")]
+    public string Gateway { get; init; } = string.Empty;
+
+    [JsonPropertyName("dns")]
+    public IReadOnlyList<string> Dns { get; init; } = [];
+
+    [JsonPropertyName("ipv4")]
+    public IReadOnlyList<RemoteInterfaceAddress> IPv4 { get; init; } = [];
+
+    public string IPv4Summary => IPv4.Count == 0
+        ? "无"
+        : string.Join("; ", IPv4.Select(item => $"{item.Address}/{item.Prefix}"));
+
+    public string DisplayName => $"{Name} / {SystemName} / {(LinkUp ? "已连接" : "未连接")}";
+}
+
+public sealed class RemoteInterfacesInfo
+{
+    [JsonPropertyName("management_interface")]
+    public string ManagementInterface { get; init; } = string.Empty;
+
+    [JsonPropertyName("suggested_target_interface")]
+    public string SuggestedTargetInterface { get; init; } = string.Empty;
+
+    [JsonPropertyName("requires_target_selection")]
+    public bool RequiresTargetSelection { get; init; }
+
+    [JsonPropertyName("interfaces")]
+    public IReadOnlyList<RemoteInterfaceInfo> Interfaces { get; init; } = [];
+}

+ 21 - 0
windows/QuickIP.Client/Models/RemoteTaskResult.cs

@@ -0,0 +1,21 @@
+using System.Text.Json.Serialization;
+
+namespace QuickIP.Client.Models;
+
+public sealed class RemoteTaskResult
+{
+    [JsonPropertyName("task_id")]
+    public string TaskId { get; init; } = string.Empty;
+
+    [JsonPropertyName("status")]
+    public string Status { get; init; } = string.Empty;
+
+    [JsonPropertyName("step")]
+    public string Step { get; init; } = string.Empty;
+
+    [JsonPropertyName("detail")]
+    public string Detail { get; init; } = string.Empty;
+
+    [JsonPropertyName("rollback")]
+    public bool Rollback { get; init; }
+}

+ 8 - 0
windows/QuickIP.Client/Models/RemoteValidateResult.cs

@@ -0,0 +1,8 @@
+namespace QuickIP.Client.Models;
+
+public sealed class RemoteValidateResult
+{
+    public bool Valid { get; init; }
+    public IReadOnlyList<string> Warnings { get; init; } = [];
+    public IReadOnlyList<string> Errors { get; init; } = [];
+}

+ 201 - 0
windows/QuickIP.Client/Services/AgentApiService.cs

@@ -1,12 +1,19 @@
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Net.Sockets;
+using System.Text.Json;
 using QuickIP.Client.Models;
 
 namespace QuickIP.Client.Services;
 
 public sealed class AgentApiService
 {
+    private readonly JsonSerializerOptions _jsonOptions = new()
+    {
+        PropertyNameCaseInsensitive = true,
+    };
+
     public async Task<HealthCheckResult> CheckHealthAsync(string baseAddress, string password, string localIPv4, CancellationToken cancellationToken = default)
     {
         try
@@ -46,4 +53,198 @@ public sealed class AgentApiService
             };
         }
     }
+
+    public async Task<RemoteDeviceInfo?> GetDeviceInfoAsync(string baseAddress, string password, string localIPv4, CancellationToken cancellationToken = default)
+    {
+        try
+        {
+            using var client = CreateClient(baseAddress, password, localIPv4);
+            using var response = await client.GetAsync("/api/device/info", cancellationToken);
+            if (!response.IsSuccessStatusCode)
+            {
+                return null;
+            }
+
+            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+            var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteDeviceInfo>>(stream, _jsonOptions, cancellationToken);
+            return wrapper?.Data;
+        }
+        catch
+        {
+            return null;
+        }
+    }
+
+    public async Task<RemoteInterfacesInfo?> GetInterfacesAsync(string baseAddress, string password, string localIPv4, CancellationToken cancellationToken = default)
+    {
+        try
+        {
+            using var client = CreateClient(baseAddress, password, localIPv4);
+            using var response = await client.GetAsync("/api/network/interfaces", cancellationToken);
+            if (!response.IsSuccessStatusCode)
+            {
+                return null;
+            }
+
+            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+            var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteInterfacesInfo>>(stream, _jsonOptions, cancellationToken);
+            return wrapper?.Data;
+        }
+        catch
+        {
+            return null;
+        }
+    }
+
+    public async Task<ApiCallResult<RemoteInterfaceConfig>> GetInterfaceConfigAsync(string baseAddress, string password, string localIPv4, string interfaceName, CancellationToken cancellationToken = default)
+    {
+        try
+        {
+            using var client = CreateClient(baseAddress, password, localIPv4);
+            using var response = await client.GetAsync($"/api/network/config?interface={Uri.EscapeDataString(interfaceName)}", cancellationToken);
+            if (!response.IsSuccessStatusCode)
+            {
+                return new ApiCallResult<RemoteInterfaceConfig>
+                {
+                    Success = false,
+                    StatusCode = (int)response.StatusCode,
+                    Message = $"读取接口配置失败,HTTP 状态码 {(int)response.StatusCode}。",
+                };
+            }
+
+            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+            var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteInterfaceConfig>>(stream, _jsonOptions, cancellationToken);
+            if (wrapper?.Data is null)
+            {
+                return new ApiCallResult<RemoteInterfaceConfig>
+                {
+                    Success = false,
+                    Message = "接口配置返回内容为空。",
+                };
+            }
+
+            return new ApiCallResult<RemoteInterfaceConfig>
+            {
+                Success = true,
+                StatusCode = (int)response.StatusCode,
+                Message = "成功",
+                Data = wrapper.Data,
+            };
+        }
+        catch (Exception ex)
+        {
+            return new ApiCallResult<RemoteInterfaceConfig>
+            {
+                Success = false,
+                Message = ex.Message,
+            };
+        }
+    }
+
+    public async Task<ApiCallResult<RemoteValidateResult>> ValidateInterfaceConfigAsync(string baseAddress, string password, string localIPv4, RemoteInterfaceConfig input, CancellationToken cancellationToken = default)
+    {
+        try
+        {
+            using var client = CreateClient(baseAddress, password, localIPv4);
+            using var response = await client.PostAsJsonAsync("/api/network/validate", input, _jsonOptions, cancellationToken);
+            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+            var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteValidateResult>>(stream, _jsonOptions, cancellationToken);
+
+            return new ApiCallResult<RemoteValidateResult>
+            {
+                Success = response.IsSuccessStatusCode && wrapper?.Data is not null,
+                StatusCode = (int)response.StatusCode,
+                Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "校验通过" : $"校验失败,HTTP 状态码 {(int)response.StatusCode}。"),
+                Data = wrapper?.Data,
+            };
+        }
+        catch (Exception ex)
+        {
+            return new ApiCallResult<RemoteValidateResult>
+            {
+                Success = false,
+                Message = ex.Message,
+            };
+        }
+    }
+
+    public async Task<ApiCallResult<RemoteApplyTaskResponse>> ApplyInterfaceConfigAsync(string baseAddress, string password, string localIPv4, RemoteInterfaceConfig input, CancellationToken cancellationToken = default)
+    {
+        try
+        {
+            using var client = CreateClient(baseAddress, password, localIPv4);
+            using var response = await client.PostAsJsonAsync("/api/network/apply", input, _jsonOptions, cancellationToken);
+            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+            var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteApplyTaskResponse>>(stream, _jsonOptions, cancellationToken);
+
+            return new ApiCallResult<RemoteApplyTaskResponse>
+            {
+                Success = response.IsSuccessStatusCode && wrapper?.Data is not null,
+                StatusCode = (int)response.StatusCode,
+                Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "配置任务已提交" : $"提交失败,HTTP 状态码 {(int)response.StatusCode}。"),
+                Data = wrapper?.Data,
+            };
+        }
+        catch (Exception ex)
+        {
+            return new ApiCallResult<RemoteApplyTaskResponse>
+            {
+                Success = false,
+                Message = ex.Message,
+            };
+        }
+    }
+
+    public async Task<ApiCallResult<RemoteTaskResult>> GetTaskAsync(string baseAddress, string password, string localIPv4, string taskId, CancellationToken cancellationToken = default)
+    {
+        try
+        {
+            using var client = CreateClient(baseAddress, password, localIPv4);
+            using var response = await client.GetAsync($"/api/tasks/{Uri.EscapeDataString(taskId)}", cancellationToken);
+            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+            var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteTaskResult>>(stream, _jsonOptions, cancellationToken);
+
+            return new ApiCallResult<RemoteTaskResult>
+            {
+                Success = response.IsSuccessStatusCode && wrapper?.Data is not null,
+                StatusCode = (int)response.StatusCode,
+                Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "成功" : $"任务查询失败,HTTP 状态码 {(int)response.StatusCode}。"),
+                Data = wrapper?.Data,
+            };
+        }
+        catch (Exception ex)
+        {
+            return new ApiCallResult<RemoteTaskResult>
+            {
+                Success = false,
+                Message = ex.Message,
+            };
+        }
+    }
+
+    private HttpClient CreateClient(string baseAddress, string password, string localIPv4)
+    {
+        var handler = new SocketsHttpHandler();
+        if (!string.IsNullOrWhiteSpace(localIPv4))
+        {
+            handler.ConnectCallback = async (context, token) =>
+            {
+                var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+                socket.Bind(new IPEndPoint(IPAddress.Parse(localIPv4), 0));
+                await socket.ConnectAsync(context.DnsEndPoint, token);
+                return new NetworkStream(socket, ownsSocket: true);
+            };
+        }
+
+        var client = new HttpClient(handler) { BaseAddress = new Uri(baseAddress), Timeout = TimeSpan.FromSeconds(5) };
+        client.DefaultRequestHeaders.Add("X-Admin-Password", password);
+        return client;
+    }
+
+    private sealed class ApiEnvelope<T>
+    {
+        public int Code { get; set; }
+        public string Message { get; set; } = string.Empty;
+        public T? Data { get; set; }
+    }
 }