|
@@ -1,4 +1,7 @@
|
|
|
using System.Globalization;
|
|
using System.Globalization;
|
|
|
|
|
+using System.Collections.ObjectModel;
|
|
|
|
|
+using System.ComponentModel;
|
|
|
|
|
+using System.Runtime.CompilerServices;
|
|
|
using System.Windows;
|
|
using System.Windows;
|
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls;
|
|
|
using System.Windows.Media;
|
|
using System.Windows.Media;
|
|
@@ -12,17 +15,26 @@ public partial class DeviceDetailsWindow : Window
|
|
|
{
|
|
{
|
|
|
private const int ApplyConfirmationTimeoutSeconds = 20;
|
|
private const int ApplyConfirmationTimeoutSeconds = 20;
|
|
|
private readonly ServerApiService _serverApiService = new();
|
|
private readonly ServerApiService _serverApiService = new();
|
|
|
|
|
+ private readonly ObservableCollection<EditableAddress> _addresses = [];
|
|
|
|
|
+ private readonly ObservableCollection<EditableRoute> _routes = [];
|
|
|
|
|
+ private readonly ObservableCollection<EditableDns> _dns = [];
|
|
|
private readonly string _baseAddress;
|
|
private readonly string _baseAddress;
|
|
|
private readonly string _localIPv4;
|
|
private readonly string _localIPv4;
|
|
|
private readonly string _password;
|
|
private readonly string _password;
|
|
|
private bool _configValidated;
|
|
private bool _configValidated;
|
|
|
|
|
+ private bool _configDirty;
|
|
|
private bool _isBusy;
|
|
private bool _isBusy;
|
|
|
|
|
+ private bool _isRestoringInterfaceSelection;
|
|
|
private bool _suppressConfigChangeHandling;
|
|
private bool _suppressConfigChangeHandling;
|
|
|
|
|
+ private RemoteInterfaceInfo? _currentSelectedInterface;
|
|
|
private CancellationTokenSource? _statusMessageCts;
|
|
private CancellationTokenSource? _statusMessageCts;
|
|
|
|
|
|
|
|
public DeviceDetailsWindow(string baseAddress, string localIPv4, string password)
|
|
public DeviceDetailsWindow(string baseAddress, string localIPv4, string password)
|
|
|
{
|
|
{
|
|
|
InitializeComponent();
|
|
InitializeComponent();
|
|
|
|
|
+ AddressesDataGrid.ItemsSource = _addresses;
|
|
|
|
|
+ RoutesDataGrid.ItemsSource = _routes;
|
|
|
|
|
+ DnsDataGrid.ItemsSource = _dns;
|
|
|
_baseAddress = baseAddress;
|
|
_baseAddress = baseAddress;
|
|
|
_localIPv4 = localIPv4;
|
|
_localIPv4 = localIPv4;
|
|
|
_password = password;
|
|
_password = password;
|
|
@@ -62,15 +74,16 @@ public partial class DeviceDetailsWindow : Window
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- ShowStatusMessage($"当前管理接口:{interfaces.ManagementInterface};建议目标接口:{interfaces.SuggestedTargetInterface};{(interfaces.RequiresTargetSelection ? "需要手动选择目标接口。" : "已自动识别建议目标接口。")}");
|
|
|
|
|
|
|
+ ShowStatusMessage($"当前管理接口:{interfaces.ManagementInterface}。请选择需要配置的目标接口。");
|
|
|
var suggested = interfaces.Interfaces.FirstOrDefault(item => item.SystemName == interfaces.SuggestedTargetInterface)
|
|
var suggested = interfaces.Interfaces.FirstOrDefault(item => item.SystemName == interfaces.SuggestedTargetInterface)
|
|
|
?? interfaces.Interfaces.FirstOrDefault(item => item.IsSuggestedTarget)
|
|
?? interfaces.Interfaces.FirstOrDefault(item => item.IsSuggestedTarget)
|
|
|
?? interfaces.Interfaces.FirstOrDefault(item => !item.IsManagementInterface);
|
|
?? interfaces.Interfaces.FirstOrDefault(item => !item.IsManagementInterface);
|
|
|
- RemoteTargetInterfaceComboBox.ItemsSource = interfaces.Interfaces;
|
|
|
|
|
|
|
+ RemoteTargetInterfaceTabControl.ItemsSource = interfaces.Interfaces;
|
|
|
if (suggested is not null)
|
|
if (suggested is not null)
|
|
|
{
|
|
{
|
|
|
- RemoteTargetInterfaceComboBox.SelectedItem = suggested;
|
|
|
|
|
|
|
+ RemoteTargetInterfaceTabControl.SelectedItem = suggested;
|
|
|
await LoadRemoteInterfaceConfigAsync(suggested.SystemName);
|
|
await LoadRemoteInterfaceConfigAsync(suggested.SystemName);
|
|
|
|
|
+ _currentSelectedInterface = suggested;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -80,28 +93,52 @@ public partial class DeviceDetailsWindow : Window
|
|
|
RemoteHostnameTextBlock.Text = "-";
|
|
RemoteHostnameTextBlock.Text = "-";
|
|
|
RemoteOsVersionTextBlock.Text = "-";
|
|
RemoteOsVersionTextBlock.Text = "-";
|
|
|
RemoteServerVersionTextBlock.Text = "-";
|
|
RemoteServerVersionTextBlock.Text = "-";
|
|
|
- RemoteTargetInterfaceComboBox.ItemsSource = null;
|
|
|
|
|
- RemoteConfigInterfaceTextBlock.Text = "-";
|
|
|
|
|
- RemoteConfigIpTextBlock.Text = "-";
|
|
|
|
|
- RemoteConfigGatewayTextBlock.Text = "-";
|
|
|
|
|
- RemoteConfigDnsTextBlock.Text = "-";
|
|
|
|
|
- NewAddressesTextBox.Text = string.Empty;
|
|
|
|
|
- NewRoutesTextBox.Text = string.Empty;
|
|
|
|
|
- NewDnsTextBox.Text = string.Empty;
|
|
|
|
|
|
|
+ RemoteTargetInterfaceTabControl.ItemsSource = null;
|
|
|
|
|
+ _addresses.Clear();
|
|
|
|
|
+ _routes.Clear();
|
|
|
|
|
+ _dns.Clear();
|
|
|
|
|
+ DefaultGatewayCheckBox.IsChecked = false;
|
|
|
|
|
+ DefaultGatewayTextBox.Text = string.Empty;
|
|
|
|
|
+ CustomRoutesCheckBox.IsChecked = false;
|
|
|
_configValidated = false;
|
|
_configValidated = false;
|
|
|
|
|
+ _configDirty = false;
|
|
|
|
|
+ _currentSelectedInterface = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private async void RemoteTargetInterfaceComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
|
|
|
|
+ private async void RemoteTargetInterfaceTabControl_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
{
|
|
{
|
|
|
try
|
|
try
|
|
|
{
|
|
{
|
|
|
- if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
|
|
|
|
|
|
|
+ if (RemoteTargetInterfaceTabControl.SelectedItem is not RemoteInterfaceInfo selected)
|
|
|
{
|
|
{
|
|
|
UpdateButtonStates();
|
|
UpdateButtonStates();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (_isRestoringInterfaceSelection)
|
|
|
|
|
+ {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (_configDirty && _currentSelectedInterface is not null && selected.SystemName != _currentSelectedInterface.SystemName)
|
|
|
|
|
+ {
|
|
|
|
|
+ var result = MessageBox.Show(
|
|
|
|
|
+ this,
|
|
|
|
|
+ "当前配置已修改,切换接口会丢失未应用内容。是否继续?",
|
|
|
|
|
+ "确认切换接口",
|
|
|
|
|
+ MessageBoxButton.OKCancel,
|
|
|
|
|
+ MessageBoxImage.Warning);
|
|
|
|
|
+ if (result != MessageBoxResult.OK)
|
|
|
|
|
+ {
|
|
|
|
|
+ _isRestoringInterfaceSelection = true;
|
|
|
|
|
+ RemoteTargetInterfaceTabControl.SelectedItem = _currentSelectedInterface;
|
|
|
|
|
+ _isRestoringInterfaceSelection = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
await LoadRemoteInterfaceConfigAsync(selected.SystemName, useBusyState: true);
|
|
await LoadRemoteInterfaceConfigAsync(selected.SystemName, useBusyState: true);
|
|
|
|
|
+ _currentSelectedInterface = selected;
|
|
|
}
|
|
}
|
|
|
catch (Exception ex)
|
|
catch (Exception ex)
|
|
|
{
|
|
{
|
|
@@ -122,26 +159,45 @@ public partial class DeviceDetailsWindow : Window
|
|
|
var result = await _serverApiService.GetInterfaceConfigAsync(_baseAddress, _password, _localIPv4, interfaceName);
|
|
var result = await _serverApiService.GetInterfaceConfigAsync(_baseAddress, _password, _localIPv4, interfaceName);
|
|
|
if (!result.Success || result.Data is null)
|
|
if (!result.Success || result.Data is null)
|
|
|
{
|
|
{
|
|
|
- RemoteConfigInterfaceTextBlock.Text = interfaceName;
|
|
|
|
|
- RemoteConfigIpTextBlock.Text = "读取失败";
|
|
|
|
|
- RemoteConfigGatewayTextBlock.Text = "读取失败";
|
|
|
|
|
- RemoteConfigDnsTextBlock.Text = "读取失败";
|
|
|
|
|
ShowStatusMessage($"读取目标接口 {interfaceName} 配置失败:{result.Message}");
|
|
ShowStatusMessage($"读取目标接口 {interfaceName} 配置失败:{result.Message}");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var config = result.Data;
|
|
var config = result.Data;
|
|
|
- RemoteConfigInterfaceTextBlock.Text = config.Interface;
|
|
|
|
|
- RemoteConfigIpTextBlock.Text = FormatCurrentIp(config);
|
|
|
|
|
- RemoteConfigGatewayTextBlock.Text = FormatRoutes(config.EffectiveRoutes);
|
|
|
|
|
- RemoteConfigDnsTextBlock.Text = config.DnsSummary;
|
|
|
|
|
_suppressConfigChangeHandling = true;
|
|
_suppressConfigChangeHandling = true;
|
|
|
Dhcp4CheckBox.IsChecked = false;
|
|
Dhcp4CheckBox.IsChecked = false;
|
|
|
- NewAddressesTextBox.Text = string.Join(Environment.NewLine, config.EffectiveAddresses.Select(item => $"{item.IP}/{item.Prefix}"));
|
|
|
|
|
- NewRoutesTextBox.Text = string.Join(Environment.NewLine, config.EffectiveRoutes.Select(item => $"{item.To} via {item.Via}"));
|
|
|
|
|
- NewDnsTextBox.Text = config.Dns is null ? string.Empty : string.Join(Environment.NewLine, config.Dns);
|
|
|
|
|
|
|
+ _addresses.Clear();
|
|
|
|
|
+ foreach (var address in config.EffectiveAddresses)
|
|
|
|
|
+ {
|
|
|
|
|
+ _addresses.Add(new EditableAddress { IP = address.IP, Mask = PrefixToMask(address.Prefix) });
|
|
|
|
|
+ }
|
|
|
|
|
+ _routes.Clear();
|
|
|
|
|
+ DefaultGatewayCheckBox.IsChecked = false;
|
|
|
|
|
+ DefaultGatewayTextBox.Text = string.Empty;
|
|
|
|
|
+ foreach (var route in config.EffectiveRoutes)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (route.To.Equals("default", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
+ {
|
|
|
|
|
+ DefaultGatewayCheckBox.IsChecked = true;
|
|
|
|
|
+ DefaultGatewayTextBox.Text = route.Via;
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ _routes.Add(new EditableRoute { To = route.To, Via = route.Via });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ CustomRoutesCheckBox.IsChecked = _routes.Count > 0;
|
|
|
|
|
+ _dns.Clear();
|
|
|
|
|
+ if (config.Dns is not null)
|
|
|
|
|
+ {
|
|
|
|
|
+ foreach (var dns in config.Dns)
|
|
|
|
|
+ {
|
|
|
|
|
+ _dns.Add(new EditableDns { Address = dns });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
_suppressConfigChangeHandling = false;
|
|
_suppressConfigChangeHandling = false;
|
|
|
_configValidated = false;
|
|
_configValidated = false;
|
|
|
|
|
+ _configDirty = false;
|
|
|
ShowStatusMessage("已读取Linux端IP配置。");
|
|
ShowStatusMessage("已读取Linux端IP配置。");
|
|
|
UpdateButtonStates();
|
|
UpdateButtonStates();
|
|
|
}
|
|
}
|
|
@@ -156,7 +212,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
|
|
private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
{
|
|
|
- if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
|
|
|
|
|
|
|
+ if (RemoteTargetInterfaceTabControl.SelectedItem is RemoteInterfaceInfo selected)
|
|
|
{
|
|
{
|
|
|
await LoadRemoteInterfaceConfigAsync(selected.SystemName);
|
|
await LoadRemoteInterfaceConfigAsync(selected.SystemName);
|
|
|
}
|
|
}
|
|
@@ -164,7 +220,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
|
|
private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
{
|
|
|
- if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
|
|
|
|
|
|
|
+ if (RemoteTargetInterfaceTabControl.SelectedItem is not RemoteInterfaceInfo selected)
|
|
|
{
|
|
{
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -201,7 +257,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
|
|
private async void ApplyConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
private async void ApplyConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
{
|
|
|
- if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
|
|
|
|
|
|
|
+ if (RemoteTargetInterfaceTabControl.SelectedItem is not RemoteInterfaceInfo selected)
|
|
|
{
|
|
{
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -285,7 +341,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
if (task.Status is "success" or "failed" or "rolled_back")
|
|
if (task.Status is "success" or "failed" or "rolled_back")
|
|
|
{
|
|
{
|
|
|
ShowTaskCompletionDialog(task);
|
|
ShowTaskCompletionDialog(task);
|
|
|
- if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
|
|
|
|
|
|
|
+ if (RemoteTargetInterfaceTabControl.SelectedItem is RemoteInterfaceInfo selected)
|
|
|
{
|
|
{
|
|
|
await LoadRemoteInterfaceConfigAsync(selected.SystemName);
|
|
await LoadRemoteInterfaceConfigAsync(selected.SystemName);
|
|
|
}
|
|
}
|
|
@@ -419,28 +475,32 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
|
|
private RemoteInterfaceConfig? BuildConfigRequest(string interfaceName)
|
|
private RemoteInterfaceConfig? BuildConfigRequest(string interfaceName)
|
|
|
{
|
|
{
|
|
|
|
|
+ CommitConfigEdits();
|
|
|
var dhcp4 = Dhcp4CheckBox.IsChecked == true;
|
|
var dhcp4 = Dhcp4CheckBox.IsChecked == true;
|
|
|
var addresses = Array.Empty<RemoteInterfaceAddressConfig>();
|
|
var addresses = Array.Empty<RemoteInterfaceAddressConfig>();
|
|
|
var routes = Array.Empty<RemoteInterfaceRouteConfig>();
|
|
var routes = Array.Empty<RemoteInterfaceRouteConfig>();
|
|
|
- if (!dhcp4 && string.IsNullOrWhiteSpace(NewAddressesTextBox.Text))
|
|
|
|
|
|
|
+ if (!dhcp4)
|
|
|
{
|
|
{
|
|
|
- ShowStatusMessage("IP 地址不能为空,至少需要填写一行地址。");
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (_addresses.All(item => string.IsNullOrWhiteSpace(item.IP) && string.IsNullOrWhiteSpace(item.Mask)))
|
|
|
|
|
+ {
|
|
|
|
|
+ ShowStatusMessage("IP 地址不能为空,至少需要填写一行地址。");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (!dhcp4 && !TryParseAddresses(NewAddressesTextBox.Text, out addresses, out var addressError))
|
|
|
|
|
- {
|
|
|
|
|
- ShowStatusMessage(addressError);
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!TryBuildAddresses(out addresses, out var addressError))
|
|
|
|
|
+ {
|
|
|
|
|
+ ShowStatusMessage(addressError);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (!dhcp4 && !TryParseRoutes(NewRoutesTextBox.Text, out routes, out var routeError))
|
|
|
|
|
- {
|
|
|
|
|
- ShowStatusMessage(routeError);
|
|
|
|
|
- return null;
|
|
|
|
|
|
|
+ if (!TryBuildRoutes(out routes, out var routeError))
|
|
|
|
|
+ {
|
|
|
|
|
+ ShowStatusMessage(routeError);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- var dns = ParseListText(NewDnsTextBox.Text);
|
|
|
|
|
|
|
+ var dns = _dns.Select(item => item.Address.Trim()).Where(item => item != string.Empty).ToArray();
|
|
|
return new RemoteInterfaceConfig
|
|
return new RemoteInterfaceConfig
|
|
|
{
|
|
{
|
|
|
Interface = interfaceName,
|
|
Interface = interfaceName,
|
|
@@ -451,6 +511,16 @@ public partial class DeviceDetailsWindow : Window
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private void CommitConfigEdits()
|
|
|
|
|
+ {
|
|
|
|
|
+ AddressesDataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
|
|
|
|
|
+ AddressesDataGrid.CommitEdit(DataGridEditingUnit.Row, true);
|
|
|
|
|
+ RoutesDataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
|
|
|
|
|
+ RoutesDataGrid.CommitEdit(DataGridEditingUnit.Row, true);
|
|
|
|
|
+ DnsDataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
|
|
|
|
|
+ DnsDataGrid.CommitEdit(DataGridEditingUnit.Row, true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static string FormatCurrentIp(RemoteInterfaceConfig config)
|
|
private static string FormatCurrentIp(RemoteInterfaceConfig config)
|
|
|
{
|
|
{
|
|
|
if (config.EffectiveAddresses.Count == 0)
|
|
if (config.EffectiveAddresses.Count == 0)
|
|
@@ -472,6 +542,91 @@ public partial class DeviceDetailsWindow : Window
|
|
|
return routes.Count == 0 ? "无" : string.Join(Environment.NewLine, routes.Select(item => $"{item.To} via {item.Via}"));
|
|
return routes.Count == 0 ? "无" : string.Join(Environment.NewLine, routes.Select(item => $"{item.To} via {item.Via}"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private bool TryBuildAddresses(out RemoteInterfaceAddressConfig[] addresses, out string error)
|
|
|
|
|
+ {
|
|
|
|
|
+ var result = new List<RemoteInterfaceAddressConfig>();
|
|
|
|
|
+ foreach (var row in _addresses)
|
|
|
|
|
+ {
|
|
|
|
|
+ var ip = row.IP.Trim();
|
|
|
|
|
+ var maskText = row.Mask.Trim();
|
|
|
|
|
+ if (ip == string.Empty && maskText == string.Empty)
|
|
|
|
|
+ {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ip.Contains('/'))
|
|
|
|
|
+ {
|
|
|
|
|
+ var parts = ip.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
|
+ if (parts.Length != 2 || !int.TryParse(parts[1], out var cidrPrefix) || cidrPrefix < 0 || cidrPrefix > 32)
|
|
|
|
|
+ {
|
|
|
|
|
+ addresses = [];
|
|
|
|
|
+ error = $"地址格式不正确:{ip}";
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ row.IP = parts[0];
|
|
|
|
|
+ row.Mask = PrefixToMask(cidrPrefix);
|
|
|
|
|
+ result.Add(new RemoteInterfaceAddressConfig { IP = parts[0], Prefix = cidrPrefix });
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ip == string.Empty || maskText == string.Empty)
|
|
|
|
|
+ {
|
|
|
|
|
+ addresses = [];
|
|
|
|
|
+ error = "IP 地址和子网掩码都需要填写。";
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!TryMaskOrPrefixToPrefix(maskText, out var prefix))
|
|
|
|
|
+ {
|
|
|
|
|
+ addresses = [];
|
|
|
|
|
+ error = $"子网掩码格式不正确:{ip} {maskText}";
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ row.Mask = PrefixToMask(prefix);
|
|
|
|
|
+ result.Add(new RemoteInterfaceAddressConfig { IP = ip, Prefix = prefix });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ addresses = result.ToArray();
|
|
|
|
|
+ error = string.Empty;
|
|
|
|
|
+ return addresses.Length > 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private bool TryBuildRoutes(out RemoteInterfaceRouteConfig[] routes, out string error)
|
|
|
|
|
+ {
|
|
|
|
|
+ var result = new List<RemoteInterfaceRouteConfig>();
|
|
|
|
|
+ if (DefaultGatewayCheckBox.IsChecked == true)
|
|
|
|
|
+ {
|
|
|
|
|
+ var gateway = DefaultGatewayTextBox.Text.Trim();
|
|
|
|
|
+ if (gateway == string.Empty)
|
|
|
|
|
+ {
|
|
|
|
|
+ routes = [];
|
|
|
|
|
+ error = "启用默认网关时,网关地址不能为空。";
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ result.Add(new RemoteInterfaceRouteConfig { To = "default", Via = gateway });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (CustomRoutesCheckBox.IsChecked == true)
|
|
|
|
|
+ {
|
|
|
|
|
+ foreach (var row in _routes)
|
|
|
|
|
+ {
|
|
|
|
|
+ var to = row.To.Trim();
|
|
|
|
|
+ var via = row.Via.Trim();
|
|
|
|
|
+ if (to == string.Empty && via == string.Empty)
|
|
|
|
|
+ {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (to == string.Empty || via == string.Empty)
|
|
|
|
|
+ {
|
|
|
|
|
+ routes = [];
|
|
|
|
|
+ error = "自定义路由的目标网段和网关地址都需要填写。";
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ result.Add(new RemoteInterfaceRouteConfig { To = to, Via = via });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ routes = result.ToArray();
|
|
|
|
|
+ error = string.Empty;
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static bool TryParseAddresses(string text, out RemoteInterfaceAddressConfig[] addresses, out string error)
|
|
private static bool TryParseAddresses(string text, out RemoteInterfaceAddressConfig[] addresses, out string error)
|
|
|
{
|
|
{
|
|
|
var result = new List<RemoteInterfaceAddressConfig>();
|
|
var result = new List<RemoteInterfaceAddressConfig>();
|
|
@@ -631,10 +786,108 @@ public partial class DeviceDetailsWindow : Window
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
_configValidated = false;
|
|
_configValidated = false;
|
|
|
|
|
+ _configDirty = true;
|
|
|
ShowStatusMessage("配置模式已变更,请重新点击“2. 校验配置”。");
|
|
ShowStatusMessage("配置模式已变更,请重新点击“2. 校验配置”。");
|
|
|
UpdateButtonStates();
|
|
UpdateButtonStates();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private void GatewayOrRouteModeChanged_OnChanged(object sender, RoutedEventArgs e)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_suppressConfigChangeHandling)
|
|
|
|
|
+ {
|
|
|
|
|
+ UpdateButtonStates();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _configValidated = false;
|
|
|
|
|
+ _configDirty = true;
|
|
|
|
|
+ ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
|
|
|
|
|
+ UpdateButtonStates();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void ConfigGrid_OnCellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (e.Row.Item is EditableAddress address)
|
|
|
|
|
+ {
|
|
|
|
|
+ NormalizeAddressRow(address);
|
|
|
|
|
+ }
|
|
|
|
|
+ MarkConfigChanged("配置内容已变更,请重新点击“2. 校验配置”。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void AddAddressButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
|
|
+ {
|
|
|
|
|
+ _addresses.Add(new EditableAddress { Mask = "255.255.255.0" });
|
|
|
|
|
+ MarkConfigChanged("已添加 IP 地址,请填写后重新校验配置。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void AddRouteButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
|
|
+ {
|
|
|
|
|
+ _routes.Add(new EditableRoute());
|
|
|
|
|
+ MarkConfigChanged("已添加路由,请填写后重新校验配置。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void AddDnsButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
|
|
+ {
|
|
|
|
|
+ _dns.Add(new EditableDns());
|
|
|
|
|
+ MarkConfigChanged("已添加 DNS,请填写后重新校验配置。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void DeleteAddressButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
|
|
+ {
|
|
|
|
|
+ if ((sender as FrameworkElement)?.DataContext is not EditableAddress address)
|
|
|
|
|
+ {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ _addresses.Remove(address);
|
|
|
|
|
+ MarkConfigChanged("已删除 IP 地址,请重新校验配置。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void DeleteRouteButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
|
|
+ {
|
|
|
|
|
+ if ((sender as FrameworkElement)?.DataContext is EditableRoute route)
|
|
|
|
|
+ {
|
|
|
|
|
+ _routes.Remove(route);
|
|
|
|
|
+ MarkConfigChanged("已删除路由,请重新校验配置。");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void DeleteDnsButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
|
|
+ {
|
|
|
|
|
+ if ((sender as FrameworkElement)?.DataContext is EditableDns dns)
|
|
|
|
|
+ {
|
|
|
|
|
+ _dns.Remove(dns);
|
|
|
|
|
+ MarkConfigChanged("已删除 DNS,请重新校验配置。");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void MarkConfigChanged(string message)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_suppressConfigChangeHandling)
|
|
|
|
|
+ {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _configValidated = false;
|
|
|
|
|
+ _configDirty = true;
|
|
|
|
|
+ ShowStatusMessage(message);
|
|
|
|
|
+ UpdateButtonStates();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void NormalizeAddressRow(EditableAddress row)
|
|
|
|
|
+ {
|
|
|
|
|
+ var ip = row.IP.Trim();
|
|
|
|
|
+ if (!ip.Contains('/'))
|
|
|
|
|
+ {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var parts = ip.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
|
+ if (parts.Length == 2 && int.TryParse(parts[1], out var prefix) && prefix >= 0 && prefix <= 32)
|
|
|
|
|
+ {
|
|
|
|
|
+ row.IP = parts[0];
|
|
|
|
|
+ row.Mask = PrefixToMask(prefix);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private void ShowStatusMessage(string message)
|
|
private void ShowStatusMessage(string message)
|
|
|
{
|
|
{
|
|
|
ApplyStatusMessageStyle(message);
|
|
ApplyStatusMessageStyle(message);
|
|
@@ -731,17 +984,26 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
|
|
private void UpdateButtonStates()
|
|
private void UpdateButtonStates()
|
|
|
{
|
|
{
|
|
|
- var hasSelectedInterface = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
|
|
|
|
|
|
|
+ var hasSelectedInterface = RemoteTargetInterfaceTabControl.SelectedItem is RemoteInterfaceInfo;
|
|
|
var canEdit = !_isBusy && hasSelectedInterface;
|
|
var canEdit = !_isBusy && hasSelectedInterface;
|
|
|
|
|
+ var canEditStatic = canEdit && Dhcp4CheckBox.IsChecked != true;
|
|
|
|
|
+ var canEditGateway = canEditStatic && DefaultGatewayCheckBox.IsChecked == true;
|
|
|
|
|
+ var canEditCustomRoutes = canEditStatic && CustomRoutesCheckBox.IsChecked == true;
|
|
|
|
|
|
|
|
- RemoteTargetInterfaceComboBox.IsEnabled = !_isBusy && RemoteTargetInterfaceComboBox.Items.Count > 0;
|
|
|
|
|
|
|
+ RemoteTargetInterfaceTabControl.IsEnabled = !_isBusy && RemoteTargetInterfaceTabControl.Items.Count > 0;
|
|
|
ReloadInterfaceConfigButton.IsEnabled = canEdit;
|
|
ReloadInterfaceConfigButton.IsEnabled = canEdit;
|
|
|
ValidateConfigButton.IsEnabled = canEdit;
|
|
ValidateConfigButton.IsEnabled = canEdit;
|
|
|
ApplyConfigButton.IsEnabled = !_isBusy && _configValidated && hasSelectedInterface;
|
|
ApplyConfigButton.IsEnabled = !_isBusy && _configValidated && hasSelectedInterface;
|
|
|
Dhcp4CheckBox.IsEnabled = canEdit;
|
|
Dhcp4CheckBox.IsEnabled = canEdit;
|
|
|
- NewAddressesTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
|
|
|
|
|
- NewRoutesTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
|
|
|
|
|
- NewDnsTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
|
|
|
|
|
|
|
+ AddressesDataGrid.IsEnabled = canEditStatic;
|
|
|
|
|
+ DefaultGatewayCheckBox.IsEnabled = canEditStatic;
|
|
|
|
|
+ DefaultGatewayTextBox.IsEnabled = canEditGateway;
|
|
|
|
|
+ CustomRoutesCheckBox.IsEnabled = canEditStatic;
|
|
|
|
|
+ RoutesDataGrid.IsEnabled = canEditCustomRoutes;
|
|
|
|
|
+ DnsDataGrid.IsEnabled = canEditStatic;
|
|
|
|
|
+ AddAddressButton.IsEnabled = canEditStatic;
|
|
|
|
|
+ AddRouteButton.IsEnabled = canEditCustomRoutes;
|
|
|
|
|
+ AddDnsButton.IsEnabled = canEditStatic;
|
|
|
RebootButton.IsEnabled = !_isBusy;
|
|
RebootButton.IsEnabled = !_isBusy;
|
|
|
ShutdownButton.IsEnabled = !_isBusy;
|
|
ShutdownButton.IsEnabled = !_isBusy;
|
|
|
}
|
|
}
|
|
@@ -753,4 +1015,45 @@ public partial class DeviceDetailsWindow : Window
|
|
|
BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message;
|
|
BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message;
|
|
|
UpdateButtonStates();
|
|
UpdateButtonStates();
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private sealed class EditableAddress : INotifyPropertyChanged
|
|
|
|
|
+ {
|
|
|
|
|
+ private string _ip = string.Empty;
|
|
|
|
|
+ private string _mask = string.Empty;
|
|
|
|
|
+
|
|
|
|
|
+ public string IP
|
|
|
|
|
+ {
|
|
|
|
|
+ get => _ip;
|
|
|
|
|
+ set => SetField(ref _ip, value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public string Mask
|
|
|
|
|
+ {
|
|
|
|
|
+ get => _mask;
|
|
|
|
|
+ set => SetField(ref _mask, value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
|
+
|
|
|
|
|
+ private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
|
|
|
|
+ {
|
|
|
|
|
+ if (EqualityComparer<T>.Default.Equals(field, value))
|
|
|
|
|
+ {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ field = value;
|
|
|
|
|
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private sealed class EditableRoute
|
|
|
|
|
+ {
|
|
|
|
|
+ public string To { get; set; } = string.Empty;
|
|
|
|
|
+ public string Via { get; set; } = string.Empty;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private sealed class EditableDns
|
|
|
|
|
+ {
|
|
|
|
|
+ public string Address { get; set; } = string.Empty;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|