diff --git a/AuroraDesk.Application/AuroraDesk.Application.csproj b/AuroraDesk.Application/AuroraDesk.Application.csproj
index d53635e..dfe0c7a 100644
--- a/AuroraDesk.Application/AuroraDesk.Application.csproj
+++ b/AuroraDesk.Application/AuroraDesk.Application.csproj
@@ -11,7 +11,6 @@
-
diff --git a/AuroraDesk.Core/AuroraDesk.Core.csproj b/AuroraDesk.Core/AuroraDesk.Core.csproj
index bed6a0b..a55fc06 100644
--- a/AuroraDesk.Core/AuroraDesk.Core.csproj
+++ b/AuroraDesk.Core/AuroraDesk.Core.csproj
@@ -7,8 +7,8 @@
-
+
diff --git a/AuroraDesk.Core/Entities/Connection.cs b/AuroraDesk.Core/Entities/Connection.cs
new file mode 100644
index 0000000..bbe8ae3
--- /dev/null
+++ b/AuroraDesk.Core/Entities/Connection.cs
@@ -0,0 +1,50 @@
+using ReactiveUI;
+using System;
+
+namespace AuroraDesk.Core.Entities;
+
+///
+/// 连接实体 - 表示两个连接点之间的连线
+///
+public class Connection : ReactiveObject
+{
+ private ConnectionPoint? _sourcePoint;
+ private ConnectionPoint? _targetPoint;
+
+ ///
+ /// 连接唯一标识符
+ ///
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+
+ ///
+ /// 源连接点(输出点)
+ ///
+ public ConnectionPoint? SourcePoint
+ {
+ get => _sourcePoint;
+ set => this.RaiseAndSetIfChanged(ref _sourcePoint, value);
+ }
+
+ ///
+ /// 目标连接点(输入点)
+ ///
+ public ConnectionPoint? TargetPoint
+ {
+ get => _targetPoint;
+ set => this.RaiseAndSetIfChanged(ref _targetPoint, value);
+ }
+
+ ///
+ /// 验证连接是否有效(输出点必须连接到输入点)
+ ///
+ public bool IsValid()
+ {
+ if (SourcePoint == null || TargetPoint == null)
+ return false;
+
+ return SourcePoint.Type == ConnectionPointType.Output &&
+ TargetPoint.Type == ConnectionPointType.Input &&
+ SourcePoint.Node != TargetPoint.Node; // 不能连接到同一个节点
+ }
+}
+
diff --git a/AuroraDesk.Core/Entities/ConnectionPoint.cs b/AuroraDesk.Core/Entities/ConnectionPoint.cs
new file mode 100644
index 0000000..07723b9
--- /dev/null
+++ b/AuroraDesk.Core/Entities/ConnectionPoint.cs
@@ -0,0 +1,78 @@
+using ReactiveUI;
+using System;
+
+namespace AuroraDesk.Core.Entities;
+
+///
+/// 连接点实体 - 节点的输入/输出连接点
+///
+public class ConnectionPoint : ReactiveObject
+{
+ private string _label = string.Empty;
+ private ConnectionPointType _type = ConnectionPointType.Output;
+ private int _index;
+ private bool _isConnected;
+
+ ///
+ /// 连接点唯一标识符
+ ///
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+
+ ///
+ /// 连接点标签(显示在连接点旁边的文本)
+ ///
+ public string Label
+ {
+ get => _label;
+ set => this.RaiseAndSetIfChanged(ref _label, value);
+ }
+
+ ///
+ /// 连接点类型(输入或输出)
+ ///
+ public ConnectionPointType Type
+ {
+ get => _type;
+ set => this.RaiseAndSetIfChanged(ref _type, value);
+ }
+
+ ///
+ /// 连接点索引(在节点上的位置顺序)
+ ///
+ public int Index
+ {
+ get => _index;
+ set => this.RaiseAndSetIfChanged(ref _index, value);
+ }
+
+ ///
+ /// 是否已连接
+ ///
+ public bool IsConnected
+ {
+ get => _isConnected;
+ set => this.RaiseAndSetIfChanged(ref _isConnected, value);
+ }
+
+ ///
+ /// 所属节点
+ ///
+ public Node? Node { get; set; }
+}
+
+///
+/// 连接点类型枚举
+///
+public enum ConnectionPointType
+{
+ ///
+ /// 输入连接点(在左侧)
+ ///
+ Input,
+
+ ///
+ /// 输出连接点(在右侧)
+ ///
+ Output
+}
+
diff --git a/AuroraDesk.Core/Entities/Node.cs b/AuroraDesk.Core/Entities/Node.cs
new file mode 100644
index 0000000..1d31003
--- /dev/null
+++ b/AuroraDesk.Core/Entities/Node.cs
@@ -0,0 +1,98 @@
+using ReactiveUI;
+using System;
+using System.Collections.ObjectModel;
+
+namespace AuroraDesk.Core.Entities;
+
+///
+/// 节点实体 - 表示画布上的一个可拖拽组件
+///
+public class Node : ReactiveObject
+{
+ private double _x;
+ private double _y;
+ private double _width = 120;
+ private double _height = 80;
+ private string _title = string.Empty;
+ private string _content = string.Empty;
+ private bool _isSelected;
+ private ObservableCollection _connectionPoints = new();
+
+ ///
+ /// 节点唯一标识符
+ ///
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+
+ ///
+ /// X坐标位置
+ ///
+ public double X
+ {
+ get => _x;
+ set => this.RaiseAndSetIfChanged(ref _x, value);
+ }
+
+ ///
+ /// Y坐标位置
+ ///
+ public double Y
+ {
+ get => _y;
+ set => this.RaiseAndSetIfChanged(ref _y, value);
+ }
+
+ ///
+ /// 节点宽度
+ ///
+ public double Width
+ {
+ get => _width;
+ set => this.RaiseAndSetIfChanged(ref _width, value);
+ }
+
+ ///
+ /// 节点高度
+ ///
+ public double Height
+ {
+ get => _height;
+ set => this.RaiseAndSetIfChanged(ref _height, value);
+ }
+
+ ///
+ /// 节点标题
+ ///
+ public string Title
+ {
+ get => _title;
+ set => this.RaiseAndSetIfChanged(ref _title, value);
+ }
+
+ ///
+ /// 节点内容(显示在节点内部的文本)
+ ///
+ public string Content
+ {
+ get => _content;
+ set => this.RaiseAndSetIfChanged(ref _content, value);
+ }
+
+ ///
+ /// 是否被选中
+ ///
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set => this.RaiseAndSetIfChanged(ref _isSelected, value);
+ }
+
+ ///
+ /// 连接点集合
+ ///
+ public ObservableCollection ConnectionPoints
+ {
+ get => _connectionPoints;
+ set => this.RaiseAndSetIfChanged(ref _connectionPoints, value);
+ }
+}
+
diff --git a/AuroraDesk.Core/Entities/NodeTemplate.cs b/AuroraDesk.Core/Entities/NodeTemplate.cs
new file mode 100644
index 0000000..fc582bd
--- /dev/null
+++ b/AuroraDesk.Core/Entities/NodeTemplate.cs
@@ -0,0 +1,139 @@
+using ReactiveUI;
+using System;
+
+namespace AuroraDesk.Core.Entities;
+
+///
+/// 节点模板 - 定义可用的节点类型
+///
+public class NodeTemplate : ReactiveObject
+{
+ private string _name = string.Empty;
+ private string _displayName = string.Empty;
+ private string _description = string.Empty;
+ private string _content = string.Empty;
+ private int _inputCount = 1;
+ private int _outputCount = 3;
+ private double _width = 120;
+ private double _height = 80;
+
+ ///
+ /// 模板唯一标识符
+ ///
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+
+ ///
+ /// 模板名称(用于内部标识)
+ ///
+ public string Name
+ {
+ get => _name;
+ set => this.RaiseAndSetIfChanged(ref _name, value);
+ }
+
+ ///
+ /// 显示名称
+ ///
+ public string DisplayName
+ {
+ get => _displayName;
+ set => this.RaiseAndSetIfChanged(ref _displayName, value);
+ }
+
+ ///
+ /// 描述
+ ///
+ public string Description
+ {
+ get => _description;
+ set => this.RaiseAndSetIfChanged(ref _description, value);
+ }
+
+ ///
+ /// 节点内容(显示在节点内部的文本)
+ ///
+ public string Content
+ {
+ get => _content;
+ set => this.RaiseAndSetIfChanged(ref _content, value);
+ }
+
+ ///
+ /// 输入连接点数量
+ ///
+ public int InputCount
+ {
+ get => _inputCount;
+ set => this.RaiseAndSetIfChanged(ref _inputCount, value);
+ }
+
+ ///
+ /// 输出连接点数量
+ ///
+ public int OutputCount
+ {
+ get => _outputCount;
+ set => this.RaiseAndSetIfChanged(ref _outputCount, value);
+ }
+
+ ///
+ /// 节点宽度
+ ///
+ public double Width
+ {
+ get => _width;
+ set => this.RaiseAndSetIfChanged(ref _width, value);
+ }
+
+ ///
+ /// 节点高度
+ ///
+ public double Height
+ {
+ get => _height;
+ set => this.RaiseAndSetIfChanged(ref _height, value);
+ }
+
+ ///
+ /// 根据模板创建节点实例
+ ///
+ public Node CreateNode(double x, double y)
+ {
+ var node = new Node
+ {
+ X = x,
+ Y = y,
+ Title = DisplayName,
+ Content = Content,
+ Width = Width,
+ Height = Height
+ };
+
+ // 创建输入连接点(左侧)
+ for (int i = 0; i < InputCount; i++)
+ {
+ node.ConnectionPoints.Add(new ConnectionPoint
+ {
+ Label = "",
+ Type = ConnectionPointType.Input,
+ Index = i,
+ Node = node
+ });
+ }
+
+ // 创建输出连接点(右侧)
+ for (int i = 0; i < OutputCount; i++)
+ {
+ node.ConnectionPoints.Add(new ConnectionPoint
+ {
+ Label = (i + 1).ToString(),
+ Type = ConnectionPointType.Output,
+ Index = i,
+ Node = node
+ });
+ }
+
+ return node;
+ }
+}
+
diff --git a/AuroraDesk.Core/Interfaces/INodeCanvasService.cs b/AuroraDesk.Core/Interfaces/INodeCanvasService.cs
new file mode 100644
index 0000000..10039ae
--- /dev/null
+++ b/AuroraDesk.Core/Interfaces/INodeCanvasService.cs
@@ -0,0 +1,57 @@
+using AuroraDesk.Core.Entities;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace AuroraDesk.Core.Interfaces;
+
+///
+/// 节点画布服务接口
+///
+public interface INodeCanvasService
+{
+ ///
+ /// 节点集合
+ ///
+ ObservableCollection Nodes { get; }
+
+ ///
+ /// 连接集合
+ ///
+ ObservableCollection Connections { get; }
+
+ ///
+ /// 添加节点到画布
+ ///
+ void AddNode(Node node);
+
+ ///
+ /// 移除节点(同时移除相关连接)
+ ///
+ void RemoveNode(Node node);
+
+ ///
+ /// 更新节点位置
+ ///
+ void UpdateNodePosition(Node node, double x, double y);
+
+ ///
+ /// 创建连接
+ ///
+ bool CreateConnection(ConnectionPoint sourcePoint, ConnectionPoint targetPoint);
+
+ ///
+ /// 移除连接
+ ///
+ void RemoveConnection(Connection connection);
+
+ ///
+ /// 获取节点的所有连接
+ ///
+ IEnumerable GetNodeConnections(Node node);
+
+ ///
+ /// 清除所有节点和连接
+ ///
+ void Clear();
+}
+
diff --git a/AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj b/AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj
index fda068f..822e1d6 100644
--- a/AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj
+++ b/AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj
@@ -15,8 +15,8 @@
-
+
diff --git a/AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs
index 80d588b..8502098 100644
--- a/AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs
+++ b/AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs
@@ -46,6 +46,9 @@ public static class ServiceCollectionExtensions
// 注册标签页管理服务(重构新增,分离标签页管理职责)
services.AddTransient();
+ // 注册节点画布服务
+ services.AddSingleton();
+
return services;
}
}
diff --git a/AuroraDesk.Infrastructure/Services/NavigationService.cs b/AuroraDesk.Infrastructure/Services/NavigationService.cs
index 3f5c397..6ec6c08 100644
--- a/AuroraDesk.Infrastructure/Services/NavigationService.cs
+++ b/AuroraDesk.Infrastructure/Services/NavigationService.cs
@@ -140,6 +140,36 @@ public class NavigationService : INavigationService
Title = "图片浏览",
IconType = IconType.Photo,
ViewModel = _pageViewModelFactory.CreatePageViewModel("image-gallery", screen)
+ },
+ new NavigationItem
+ {
+ Id = "udp-tools",
+ Title = "UDP 工具",
+ IconType = IconType.Signal,
+ Children = new ObservableCollection
+ {
+ new NavigationItem
+ {
+ Id = "udp-client",
+ Title = "UDP 客户端",
+ IconType = IconType.ArrowRight,
+ ViewModel = _pageViewModelFactory.CreatePageViewModel("udp-client", screen)
+ },
+ new NavigationItem
+ {
+ Id = "udp-server",
+ Title = "UDP 服务端",
+ IconType = IconType.Server,
+ ViewModel = _pageViewModelFactory.CreatePageViewModel("udp-server", screen)
+ }
+ }
+ },
+ new NavigationItem
+ {
+ Id = "node-canvas",
+ Title = "节点编辑器",
+ IconType = IconType.SquaresPlus,
+ ViewModel = _pageViewModelFactory.CreatePageViewModel("node-canvas", screen)
}
};
diff --git a/AuroraDesk.Infrastructure/Services/NavigationStateService.cs b/AuroraDesk.Infrastructure/Services/NavigationStateService.cs
index 9101a6c..bad2eae 100644
--- a/AuroraDesk.Infrastructure/Services/NavigationStateService.cs
+++ b/AuroraDesk.Infrastructure/Services/NavigationStateService.cs
@@ -1,9 +1,9 @@
using AuroraDesk.Core.Interfaces;
using AuroraDesk.Core.Entities;
using Microsoft.Extensions.Logging;
-using ReactiveUI;
using System.Collections.ObjectModel;
using System.Collections.Generic;
+using ReactiveUI;
namespace AuroraDesk.Infrastructure.Services;
diff --git a/AuroraDesk.Infrastructure/Services/NodeCanvasService.cs b/AuroraDesk.Infrastructure/Services/NodeCanvasService.cs
new file mode 100644
index 0000000..7a310a0
--- /dev/null
+++ b/AuroraDesk.Infrastructure/Services/NodeCanvasService.cs
@@ -0,0 +1,157 @@
+using AuroraDesk.Core.Entities;
+using AuroraDesk.Core.Interfaces;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Linq;
+
+namespace AuroraDesk.Infrastructure.Services;
+
+///
+/// 节点画布服务实现
+///
+public class NodeCanvasService : INodeCanvasService
+{
+ private readonly ILogger? _logger;
+ private readonly ObservableCollection _nodes = new();
+ private readonly ObservableCollection _connections = new();
+
+ public ObservableCollection Nodes => _nodes;
+ public ObservableCollection Connections => _connections;
+
+ public NodeCanvasService(ILogger? logger = null)
+ {
+ _logger = logger;
+ }
+
+ public void AddNode(Node node)
+ {
+ if (node == null)
+ throw new ArgumentNullException(nameof(node));
+
+ if (!_nodes.Contains(node))
+ {
+ _nodes.Add(node);
+ _logger?.LogDebug("添加节点: {NodeId}, Title: {Title}", node.Id, node.Title);
+ }
+ }
+
+ public void RemoveNode(Node node)
+ {
+ if (node == null) return;
+
+ // 移除与节点相关的所有连接
+ var connectionsToRemove = _connections
+ .Where(c => c.SourcePoint?.Node == node || c.TargetPoint?.Node == node)
+ .ToList();
+
+ foreach (var connection in connectionsToRemove)
+ {
+ RemoveConnection(connection);
+ }
+
+ _nodes.Remove(node);
+ _logger?.LogDebug("移除节点: {NodeId}, Title: {Title}", node.Id, node.Title);
+ }
+
+ public void UpdateNodePosition(Node node, double x, double y)
+ {
+ if (node == null) return;
+
+ node.X = x;
+ node.Y = y;
+ _logger?.LogTrace("更新节点位置: {NodeId}, X: {X}, Y: {Y}", node.Id, x, y);
+ }
+
+ public bool CreateConnection(ConnectionPoint sourcePoint, ConnectionPoint targetPoint)
+ {
+ if (sourcePoint == null || targetPoint == null)
+ return false;
+
+ // 验证连接点类型
+ if (sourcePoint.Type != ConnectionPointType.Output ||
+ targetPoint.Type != ConnectionPointType.Input)
+ {
+ _logger?.LogWarning("连接点类型不匹配: Source={SourceType}, Target={TargetType}",
+ sourcePoint.Type, targetPoint.Type);
+ return false;
+ }
+
+ // 不能连接到同一个节点
+ if (sourcePoint.Node == targetPoint.Node)
+ {
+ _logger?.LogWarning("不能连接到同一个节点");
+ return false;
+ }
+
+ // 检查是否已经存在相同的连接
+ var existingConnection = _connections.FirstOrDefault(c =>
+ c.SourcePoint == sourcePoint && c.TargetPoint == targetPoint);
+
+ if (existingConnection != null)
+ {
+ _logger?.LogWarning("连接已存在");
+ return false;
+ }
+
+ // 检查目标连接点是否已经被连接(可以支持一对多,这里先实现一对一)
+ var targetAlreadyConnected = _connections.Any(c => c.TargetPoint == targetPoint);
+ if (targetAlreadyConnected)
+ {
+ _logger?.LogWarning("目标连接点已被连接");
+ return false;
+ }
+
+ var connection = new Connection
+ {
+ SourcePoint = sourcePoint,
+ TargetPoint = targetPoint
+ };
+
+ if (connection.IsValid())
+ {
+ _connections.Add(connection);
+ sourcePoint.IsConnected = true;
+ targetPoint.IsConnected = true;
+ _logger?.LogDebug("创建连接: {ConnectionId}, Source={SourceId}, Target={TargetId}",
+ connection.Id, sourcePoint.Id, targetPoint.Id);
+ return true;
+ }
+
+ _logger?.LogWarning("连接验证失败");
+ return false;
+ }
+
+ public void RemoveConnection(Connection connection)
+ {
+ if (connection == null) return;
+
+ if (_connections.Remove(connection))
+ {
+ if (connection.SourcePoint != null)
+ connection.SourcePoint.IsConnected = false;
+ if (connection.TargetPoint != null)
+ connection.TargetPoint.IsConnected = false;
+
+ _logger?.LogDebug("移除连接: {ConnectionId}", connection.Id);
+ }
+ }
+
+ public IEnumerable GetNodeConnections(Node node)
+ {
+ if (node == null)
+ return Enumerable.Empty();
+
+ return _connections.Where(c =>
+ c.SourcePoint?.Node == node || c.TargetPoint?.Node == node);
+ }
+
+ public void Clear()
+ {
+ _connections.Clear();
+ _nodes.Clear();
+ _logger?.LogDebug("清空画布");
+ }
+}
+
diff --git a/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs b/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs
new file mode 100644
index 0000000..c0ea34a
--- /dev/null
+++ b/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs
@@ -0,0 +1,149 @@
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using Avalonia;
+using AuroraDesk.Core.Entities;
+using System;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+
+namespace AuroraDesk.Presentation.Converters;
+
+///
+/// 节点画布相关转换器
+///
+public static class NodeCanvasConverters
+{
+ public static readonly IValueConverter BooleanToBorderBrushConverter = new BooleanToBorderBrushConverter();
+ public static readonly IValueConverter BooleanToBorderThicknessConverter = new BooleanToBorderThicknessConverter();
+ public static readonly IValueConverter FilterInputPointsConverter = new FilterInputPointsConverter();
+ public static readonly IValueConverter FilterOutputPointsConverter = new FilterOutputPointsConverter();
+ public static readonly IValueConverter NodeToSelectionTextConverter = new NodeToSelectionTextConverter();
+ public static readonly IValueConverter IsNotNullConverter = new IsNotNullConverter();
+}
+
+///
+/// 布尔值到边框颜色转换器(选中时显示蓝色边框)
+///
+public class BooleanToBorderBrushConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool isSelected && isSelected)
+ {
+ // 使用 StaticResource 的颜色,如果无法获取则使用默认蓝色
+ try
+ {
+ var color = Color.Parse("#3498DB"); // PrimaryBlue
+ return new SolidColorBrush(color);
+ }
+ catch
+ {
+ return new SolidColorBrush(Color.FromRgb(52, 152, 219)); // #3498DB
+ }
+ }
+ return new SolidColorBrush(Colors.Transparent);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+///
+/// 布尔值到边框厚度转换器(选中时显示边框)
+///
+public class BooleanToBorderThicknessConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool isSelected && isSelected)
+ {
+ return new Thickness(2);
+ }
+ return new Thickness(0);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+///
+/// 过滤输入连接点转换器
+///
+public class FilterInputPointsConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is ObservableCollection points)
+ {
+ return points.Where(p => p.Type == ConnectionPointType.Input).ToList();
+ }
+ return Enumerable.Empty();
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+///
+/// 过滤输出连接点转换器
+///
+public class FilterOutputPointsConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is ObservableCollection points)
+ {
+ return points.Where(p => p.Type == ConnectionPointType.Output).ToList();
+ }
+ return Enumerable.Empty();
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+///
+/// 节点到选择文本转换器
+///
+public class NodeToSelectionTextConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is Node node)
+ {
+ return $"已选中1个对象 (节点)";
+ }
+ return "未选中对象";
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+///
+/// 非空转换器(用于控制可见性)
+///
+public class IsNotNullConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return value != null;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
diff --git a/AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs b/AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs
index be27e43..29934b0 100644
--- a/AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs
+++ b/AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs
@@ -45,6 +45,9 @@ public static class ServiceCollectionExtensions
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
// 注意:MainWindowViewModel 的注册移到主项目的 App.axaml.cs 中
// 因为它依赖 AppViewModel,而 AppViewModel 在 AddReactiveUI() 中注册
diff --git a/AuroraDesk.Presentation/Services/PageViewModelFactory.cs b/AuroraDesk.Presentation/Services/PageViewModelFactory.cs
index aa49971..4f001a2 100644
--- a/AuroraDesk.Presentation/Services/PageViewModelFactory.cs
+++ b/AuroraDesk.Presentation/Services/PageViewModelFactory.cs
@@ -55,6 +55,9 @@ public class PageViewModelFactory : IPageViewModelFactory
"icons" => CreateIconsPageViewModel(screen),
"editor" => CreateEditorPageViewModel(screen),
"image-gallery" => CreateImageGalleryPageViewModel(screen),
+ "udp-client" => CreateUdpClientPageViewModel(screen),
+ "udp-server" => CreateUdpServerPageViewModel(screen),
+ "node-canvas" => CreateNodeCanvasPageViewModel(screen),
_ => throw new ArgumentException($"Unknown page: {pageId}", nameof(pageId))
};
}
@@ -76,5 +79,24 @@ public class PageViewModelFactory : IPageViewModelFactory
var logger = _serviceProvider.GetService>();
return ActivatorUtilities.CreateInstance(_serviceProvider, screen, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
}
+
+ private UdpClientPageViewModel CreateUdpClientPageViewModel(IScreen screen)
+ {
+ var logger = _serviceProvider.GetService>();
+ return ActivatorUtilities.CreateInstance(_serviceProvider, screen, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
+ }
+
+ private UdpServerPageViewModel CreateUdpServerPageViewModel(IScreen screen)
+ {
+ var logger = _serviceProvider.GetService>();
+ return ActivatorUtilities.CreateInstance(_serviceProvider, screen, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
+ }
+
+ private NodeCanvasPageViewModel CreateNodeCanvasPageViewModel(IScreen screen)
+ {
+ var nodeCanvasService = _serviceProvider.GetRequiredService();
+ var logger = _serviceProvider.GetService>();
+ return ActivatorUtilities.CreateInstance(_serviceProvider, screen, nodeCanvasService, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
+ }
}
diff --git a/AuroraDesk.Presentation/ViewModels/Pages/NodeCanvasPageViewModel.cs b/AuroraDesk.Presentation/ViewModels/Pages/NodeCanvasPageViewModel.cs
new file mode 100644
index 0000000..7de00e8
--- /dev/null
+++ b/AuroraDesk.Presentation/ViewModels/Pages/NodeCanvasPageViewModel.cs
@@ -0,0 +1,319 @@
+using AuroraDesk.Core.Entities;
+using AuroraDesk.Core.Interfaces;
+using AuroraDesk.Presentation.ViewModels.Base;
+using ReactiveUI;
+using System;
+using System.Collections.ObjectModel;
+using System.Reactive;
+using System.Reactive.Linq;
+using Microsoft.Extensions.Logging;
+
+namespace AuroraDesk.Presentation.ViewModels.Pages;
+
+///
+/// 节点画布页面 ViewModel
+///
+public class NodeCanvasPageViewModel : RoutableViewModel
+{
+ private readonly INodeCanvasService _nodeCanvasService;
+ private readonly ILogger? _logger;
+ private Node? _selectedNode;
+ private ConnectionPoint? _connectingSourcePoint;
+ private bool _isConnecting;
+ private double _canvasOffsetX;
+ private double _canvasOffsetY;
+ private double _canvasZoom = 1.0;
+
+ public ObservableCollection Nodes => _nodeCanvasService.Nodes;
+ public ObservableCollection Connections => _nodeCanvasService.Connections;
+
+ private ObservableCollection _nodeTemplates = new();
+ public ObservableCollection NodeTemplates
+ {
+ get => _nodeTemplates;
+ set => this.RaiseAndSetIfChanged(ref _nodeTemplates, value);
+ }
+
+ ///
+ /// 选中的节点
+ ///
+ public Node? SelectedNode
+ {
+ get => _selectedNode;
+ set => this.RaiseAndSetIfChanged(ref _selectedNode, value);
+ }
+
+ ///
+ /// 正在连接的源连接点
+ ///
+ public ConnectionPoint? ConnectingSourcePoint
+ {
+ get => _connectingSourcePoint;
+ set => this.RaiseAndSetIfChanged(ref _connectingSourcePoint, value);
+ }
+
+ ///
+ /// 是否正在连接模式
+ ///
+ public bool IsConnecting
+ {
+ get => _isConnecting;
+ set => this.RaiseAndSetIfChanged(ref _isConnecting, value);
+ }
+
+ ///
+ /// 画布偏移X
+ ///
+ public double CanvasOffsetX
+ {
+ get => _canvasOffsetX;
+ set => this.RaiseAndSetIfChanged(ref _canvasOffsetX, value);
+ }
+
+ ///
+ /// 画布偏移Y
+ ///
+ public double CanvasOffsetY
+ {
+ get => _canvasOffsetY;
+ set => this.RaiseAndSetIfChanged(ref _canvasOffsetY, value);
+ }
+
+ ///
+ /// 画布缩放
+ ///
+ public double CanvasZoom
+ {
+ get => _canvasZoom;
+ set => this.RaiseAndSetIfChanged(ref _canvasZoom, value);
+ }
+
+ // 命令
+ public ReactiveCommand ClearCanvasCommand { get; }
+ public ReactiveCommand DeleteNodeCommand { get; }
+ public ReactiveCommand StartConnectionCommand { get; }
+ public ReactiveCommand CompleteConnectionCommand { get; }
+ public ReactiveCommand DeleteConnectionCommand { get; }
+ public ReactiveCommand<(double x, double y), Unit> AddNodeCommand { get; }
+ public ReactiveCommand<(NodeTemplate template, double x, double y), Unit> AddNodeFromTemplateCommand { get; }
+
+ public NodeCanvasPageViewModel(
+ IScreen screen,
+ INodeCanvasService nodeCanvasService,
+ ILogger? logger = null) : base(screen, "node-canvas")
+ {
+ _nodeCanvasService = nodeCanvasService;
+ _logger = logger;
+
+ // 初始化节点模板
+ InitializeNodeTemplates();
+
+ // 初始化命令
+ ClearCanvasCommand = ReactiveCommand.Create(ClearCanvas);
+ DeleteNodeCommand = ReactiveCommand.Create(DeleteNode);
+ StartConnectionCommand = ReactiveCommand.Create(StartConnection);
+ CompleteConnectionCommand = ReactiveCommand.Create(CompleteConnection);
+ DeleteConnectionCommand = ReactiveCommand.Create(DeleteConnection);
+ AddNodeCommand = ReactiveCommand.Create<(double x, double y)>(AddNode);
+ AddNodeFromTemplateCommand = ReactiveCommand.Create<(NodeTemplate template, double x, double y)>(AddNodeFromTemplate);
+
+ // 监听选中节点变化
+ this.WhenAnyValue(x => x.SelectedNode)
+ .Subscribe(node =>
+ {
+ // 取消其他节点的选中状态
+ foreach (var n in Nodes)
+ {
+ n.IsSelected = n == node;
+ }
+ });
+
+ _logger?.LogInformation("NodeCanvasPageViewModel 已创建");
+ }
+
+ ///
+ /// 初始化节点模板
+ ///
+ private void InitializeNodeTemplates()
+ {
+ NodeTemplates.Add(new NodeTemplate
+ {
+ Name = "power-splitter",
+ DisplayName = "功分器",
+ Description = "3功分器",
+ Content = "3功分42",
+ InputCount = 1,
+ OutputCount = 3,
+ Width = 120,
+ Height = 80
+ });
+
+ NodeTemplates.Add(new NodeTemplate
+ {
+ Name = "basic-node",
+ DisplayName = "基础节点",
+ Description = "基础节点模板",
+ Content = "节点",
+ InputCount = 1,
+ OutputCount = 1,
+ Width = 100,
+ Height = 60
+ });
+ }
+
+ ///
+ /// 添加节点到画布
+ ///
+ private void AddNode((double x, double y) position)
+ {
+ // 确保位置在合理范围内(至少为正数)
+ var x = Math.Max(0, position.x);
+ var y = Math.Max(0, position.y);
+
+ var node = new Node
+ {
+ X = x,
+ Y = y,
+ Title = $"节点 {Nodes.Count + 1}",
+ Content = "3功分42",
+ Width = 120,
+ Height = 80
+ };
+
+ // 添加连接点(右侧输出点)
+ for (int i = 0; i < 3; i++)
+ {
+ node.ConnectionPoints.Add(new ConnectionPoint
+ {
+ Label = (i + 1).ToString(),
+ Type = ConnectionPointType.Output,
+ Index = i,
+ Node = node
+ });
+ }
+
+ // 添加连接点(左侧输入点)
+ node.ConnectionPoints.Add(new ConnectionPoint
+ {
+ Label = "",
+ Type = ConnectionPointType.Input,
+ Index = 0,
+ Node = node
+ });
+
+ _nodeCanvasService.AddNode(node);
+ SelectedNode = node;
+ _logger?.LogDebug("添加节点: {NodeId} 在位置 ({X}, {Y})", node.Id, position.x, position.y);
+ }
+
+ ///
+ /// 删除节点
+ ///
+ private void DeleteNode(Node node)
+ {
+ if (node == null) return;
+
+ _nodeCanvasService.RemoveNode(node);
+ if (SelectedNode == node)
+ {
+ SelectedNode = null;
+ }
+ _logger?.LogDebug("删除节点: {NodeId}", node.Id);
+ }
+
+ ///
+ /// 开始连接
+ ///
+ private void StartConnection(ConnectionPoint connectionPoint)
+ {
+ if (connectionPoint == null || connectionPoint.Type != ConnectionPointType.Output)
+ return;
+
+ ConnectingSourcePoint = connectionPoint;
+ IsConnecting = true;
+ _logger?.LogDebug("开始连接,源连接点: {PointId}", connectionPoint.Id);
+ }
+
+ ///
+ /// 完成连接
+ ///
+ private void CompleteConnection(ConnectionPoint targetPoint)
+ {
+ if (targetPoint == null || ConnectingSourcePoint == null)
+ {
+ CancelConnection();
+ return;
+ }
+
+ if (targetPoint.Type != ConnectionPointType.Input)
+ {
+ CancelConnection();
+ return;
+ }
+
+ var success = _nodeCanvasService.CreateConnection(ConnectingSourcePoint, targetPoint);
+ if (success)
+ {
+ _logger?.LogDebug("连接成功: {SourceId} -> {TargetId}",
+ ConnectingSourcePoint.Id, targetPoint.Id);
+ }
+ else
+ {
+ _logger?.LogWarning("连接失败: {SourceId} -> {TargetId}",
+ ConnectingSourcePoint.Id, targetPoint.Id);
+ }
+
+ CancelConnection();
+ }
+
+ ///
+ /// 取消连接
+ ///
+ public void CancelConnection()
+ {
+ ConnectingSourcePoint = null;
+ IsConnecting = false;
+ }
+
+ ///
+ /// 删除连接
+ ///
+ private void DeleteConnection(Connection connection)
+ {
+ if (connection == null) return;
+
+ _nodeCanvasService.RemoveConnection(connection);
+ _logger?.LogDebug("删除连接: {ConnectionId}", connection.Id);
+ }
+
+ ///
+ /// 清空画布
+ ///
+ private void ClearCanvas()
+ {
+ _nodeCanvasService.Clear();
+ SelectedNode = null;
+ _logger?.LogDebug("清空画布");
+ }
+
+ ///
+ /// 更新节点位置
+ ///
+ public void UpdateNodePosition(Node node, double x, double y)
+ {
+ _nodeCanvasService.UpdateNodePosition(node, x, y);
+ }
+
+ ///
+ /// 从模板添加节点到画布
+ ///
+ private void AddNodeFromTemplate((NodeTemplate template, double x, double y) args)
+ {
+ var node = args.template.CreateNode(args.x, args.y);
+ _nodeCanvasService.AddNode(node);
+ SelectedNode = node;
+ _logger?.LogDebug("从模板添加节点: {TemplateName} 在位置 ({X}, {Y})",
+ args.template.Name, args.x, args.y);
+ }
+}
+
diff --git a/AuroraDesk.Presentation/ViewModels/Pages/UdpClientPageViewModel.cs b/AuroraDesk.Presentation/ViewModels/Pages/UdpClientPageViewModel.cs
new file mode 100644
index 0000000..2ee9f6a
--- /dev/null
+++ b/AuroraDesk.Presentation/ViewModels/Pages/UdpClientPageViewModel.cs
@@ -0,0 +1,304 @@
+using AuroraDesk.Presentation.ViewModels.Base;
+using Microsoft.Extensions.Logging;
+using ReactiveUI;
+using System;
+using System.Collections.ObjectModel;
+using System.Net;
+using System.Net.Sockets;
+using System.Reactive;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+
+namespace AuroraDesk.Presentation.ViewModels.Pages;
+
+///
+/// UDP 客户端页面 ViewModel
+///
+public class UdpClientPageViewModel : RoutableViewModel
+{
+ private readonly ILogger? _logger;
+ private UdpClient? _udpClient;
+ private CancellationTokenSource? _receiveCancellationTokenSource;
+ private bool _isConnected;
+ private string _serverIp = "127.0.0.1";
+ private int _serverPort = 8080;
+ private int _localPort = 0; // 0 表示系统自动分配
+ private string _message = string.Empty;
+ private string _statusMessage = "未连接";
+ private readonly ObservableCollection _receivedMessages = new();
+ private readonly ObservableCollection _sentMessages = new();
+
+ public UdpClientPageViewModel(
+ IScreen hostScreen,
+ ILogger? logger = null)
+ : base(hostScreen, "UdpClient")
+ {
+ _logger = logger;
+
+ // 创建命令
+ ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync,
+ this.WhenAnyValue(x => x.IsConnected, isConnected => !isConnected));
+ DisconnectCommand = ReactiveCommand.Create(Disconnect,
+ this.WhenAnyValue(x => x.IsConnected));
+ SendMessageCommand = ReactiveCommand.CreateFromTask(SendMessageAsync,
+ this.WhenAnyValue(x => x.IsConnected, x => x.Message,
+ (isConnected, message) => isConnected && !string.IsNullOrWhiteSpace(message)));
+ ClearMessagesCommand = ReactiveCommand.Create(ClearMessages);
+ }
+
+ ///
+ /// 是否已连接
+ ///
+ public bool IsConnected
+ {
+ get => _isConnected;
+ set => this.RaiseAndSetIfChanged(ref _isConnected, value);
+ }
+
+ ///
+ /// 服务器 IP 地址
+ ///
+ public string ServerIp
+ {
+ get => _serverIp;
+ set => this.RaiseAndSetIfChanged(ref _serverIp, value);
+ }
+
+ ///
+ /// 服务器端口
+ ///
+ public int ServerPort
+ {
+ get => _serverPort;
+ set => this.RaiseAndSetIfChanged(ref _serverPort, value);
+ }
+
+ ///
+ /// 本地端口
+ ///
+ public int LocalPort
+ {
+ get => _localPort;
+ set => this.RaiseAndSetIfChanged(ref _localPort, value);
+ }
+
+ ///
+ /// 要发送的消息
+ ///
+ public string Message
+ {
+ get => _message;
+ set => this.RaiseAndSetIfChanged(ref _message, value);
+ }
+
+ ///
+ /// 状态消息
+ ///
+ public string StatusMessage
+ {
+ get => _statusMessage;
+ set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
+ }
+
+ ///
+ /// 接收到的消息列表
+ ///
+ public ObservableCollection ReceivedMessages => _receivedMessages;
+
+ ///
+ /// 已发送的消息列表
+ ///
+ public ObservableCollection SentMessages => _sentMessages;
+
+ ///
+ /// 连接命令
+ ///
+ public ReactiveCommand ConnectCommand { get; }
+
+ ///
+ /// 断开连接命令
+ ///
+ public ReactiveCommand DisconnectCommand { get; }
+
+ ///
+ /// 发送消息命令
+ ///
+ public ReactiveCommand SendMessageCommand { get; }
+
+ ///
+ /// 清空消息命令
+ ///
+ public ReactiveCommand ClearMessagesCommand { get; }
+
+ ///
+ /// 连接到服务器
+ ///
+ private async Task ConnectAsync()
+ {
+ try
+ {
+ if (!IPAddress.TryParse(ServerIp, out var ipAddress))
+ {
+ StatusMessage = $"无效的 IP 地址: {ServerIp}";
+ _logger?.LogWarning("无效的 IP 地址: {Ip}", ServerIp);
+ return;
+ }
+
+ if (ServerPort < 1 || ServerPort > 65535)
+ {
+ StatusMessage = $"无效的端口: {ServerPort}";
+ _logger?.LogWarning("无效的端口: {Port}", ServerPort);
+ return;
+ }
+
+ // 创建 UDP 客户端
+ _udpClient = new UdpClient(LocalPort);
+ _udpClient.EnableBroadcast = true;
+
+ var localEndPoint = (IPEndPoint)_udpClient.Client.LocalEndPoint!;
+ LocalPort = localEndPoint.Port;
+
+ IsConnected = true;
+ StatusMessage = $"已连接到 {ServerIp}:{ServerPort} (本地端口: {LocalPort})";
+ _logger?.LogInformation("UDP 客户端已连接到 {ServerIp}:{ServerPort}, 本地端口: {LocalPort}",
+ ServerIp, ServerPort, LocalPort);
+
+ // 启动接收任务
+ _receiveCancellationTokenSource = new CancellationTokenSource();
+ _ = Task.Run(() => ReceiveMessagesAsync(_receiveCancellationTokenSource.Token));
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "连接失败");
+ StatusMessage = $"连接失败: {ex.Message}";
+ Disconnect();
+ }
+ }
+
+ ///
+ /// 断开连接
+ ///
+ private void Disconnect()
+ {
+ try
+ {
+ _receiveCancellationTokenSource?.Cancel();
+ _receiveCancellationTokenSource?.Dispose();
+ _receiveCancellationTokenSource = null;
+
+ _udpClient?.Close();
+ _udpClient?.Dispose();
+ _udpClient = null;
+
+ IsConnected = false;
+ StatusMessage = "已断开连接";
+ _logger?.LogInformation("UDP 客户端已断开连接");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "断开连接时出错");
+ }
+ }
+
+ ///
+ /// 发送消息
+ ///
+ private async Task SendMessageAsync()
+ {
+ if (_udpClient == null || string.IsNullOrWhiteSpace(Message))
+ return;
+
+ try
+ {
+ if (!IPAddress.TryParse(ServerIp, out var ipAddress))
+ {
+ StatusMessage = $"无效的 IP 地址: {ServerIp}";
+ return;
+ }
+
+ var remoteEndPoint = new IPEndPoint(ipAddress, ServerPort);
+ var data = Encoding.UTF8.GetBytes(Message);
+
+ await _udpClient.SendAsync(data, data.Length, remoteEndPoint);
+
+ var timestamp = DateTime.Now.ToString("HH:mm:ss");
+ var displayMessage = $"[{timestamp}] 发送: {Message}";
+
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ SentMessages.Add(displayMessage);
+ });
+
+ _logger?.LogInformation("发送消息到 {ServerIp}:{ServerPort}: {Message}",
+ ServerIp, ServerPort, Message);
+
+ Message = string.Empty; // 清空输入框
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "发送消息失败");
+ StatusMessage = $"发送失败: {ex.Message}";
+ }
+ }
+
+ ///
+ /// 接收消息(后台任务)
+ ///
+ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested && _udpClient != null)
+ {
+ try
+ {
+ var result = await _udpClient.ReceiveAsync();
+ var receivedData = Encoding.UTF8.GetString(result.Buffer);
+ var remoteEndPoint = result.RemoteEndPoint;
+ var timestamp = DateTime.Now.ToString("HH:mm:ss");
+ var displayMessage = $"[{timestamp}] 来自 {remoteEndPoint.Address}:{remoteEndPoint.Port}: {receivedData}";
+
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ ReceivedMessages.Add(displayMessage);
+ });
+
+ _logger?.LogDebug("收到消息: {Message} 来自 {EndPoint}", receivedData, remoteEndPoint);
+ }
+ catch (ObjectDisposedException)
+ {
+ // UDP 客户端已关闭,正常退出
+ break;
+ }
+ catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted)
+ {
+ // 操作被取消,正常退出
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "接收消息时出错");
+ await Task.Delay(1000, cancellationToken); // 等待后重试
+ }
+ }
+ }
+
+ ///
+ /// 清空消息
+ ///
+ private void ClearMessages()
+ {
+ ReceivedMessages.Clear();
+ SentMessages.Clear();
+ _logger?.LogInformation("已清空消息列表");
+ }
+
+ ///
+ /// 清理资源
+ ///
+ public void Dispose()
+ {
+ Disconnect();
+ }
+}
+
diff --git a/AuroraDesk.Presentation/ViewModels/Pages/UdpServerPageViewModel.cs b/AuroraDesk.Presentation/ViewModels/Pages/UdpServerPageViewModel.cs
new file mode 100644
index 0000000..c8947b3
--- /dev/null
+++ b/AuroraDesk.Presentation/ViewModels/Pages/UdpServerPageViewModel.cs
@@ -0,0 +1,308 @@
+using AuroraDesk.Presentation.ViewModels.Base;
+using Microsoft.Extensions.Logging;
+using ReactiveUI;
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Reactive;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+
+namespace AuroraDesk.Presentation.ViewModels.Pages;
+
+///
+/// UDP 服务端页面 ViewModel
+///
+public class UdpServerPageViewModel : RoutableViewModel
+{
+ private readonly ILogger? _logger;
+ private UdpClient? _udpServer;
+ private CancellationTokenSource? _listenCancellationTokenSource;
+ private bool _isListening;
+ private int _listenPort = 8080;
+ private string _statusMessage = "未启动";
+ private readonly ObservableCollection _receivedMessages = new();
+ private readonly ObservableCollection _clients = new();
+
+ public UdpServerPageViewModel(
+ IScreen hostScreen,
+ ILogger? logger = null)
+ : base(hostScreen, "UdpServer")
+ {
+ _logger = logger;
+
+ // 创建命令
+ StartListeningCommand = ReactiveCommand.CreateFromTask(StartListeningAsync,
+ this.WhenAnyValue(x => x.IsListening, isListening => !isListening));
+ StopListeningCommand = ReactiveCommand.Create(StopListening,
+ this.WhenAnyValue(x => x.IsListening));
+ ClearMessagesCommand = ReactiveCommand.Create(ClearMessages);
+ ClearClientsCommand = ReactiveCommand.Create(ClearClients);
+ }
+
+ ///
+ /// 是否正在监听
+ ///
+ public bool IsListening
+ {
+ get => _isListening;
+ set => this.RaiseAndSetIfChanged(ref _isListening, value);
+ }
+
+ ///
+ /// 监听端口
+ ///
+ public int ListenPort
+ {
+ get => _listenPort;
+ set => this.RaiseAndSetIfChanged(ref _listenPort, value);
+ }
+
+ ///
+ /// 状态消息
+ ///
+ public string StatusMessage
+ {
+ get => _statusMessage;
+ set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
+ }
+
+ ///
+ /// 接收到的消息列表
+ ///
+ public ObservableCollection ReceivedMessages => _receivedMessages;
+
+ ///
+ /// 客户端列表
+ ///
+ public ObservableCollection Clients => _clients;
+
+ ///
+ /// 开始监听命令
+ ///
+ public ReactiveCommand StartListeningCommand { get; }
+
+ ///
+ /// 停止监听命令
+ ///
+ public ReactiveCommand StopListeningCommand { get; }
+
+ ///
+ /// 清空消息命令
+ ///
+ public ReactiveCommand ClearMessagesCommand { get; }
+
+ ///
+ /// 清空客户端命令
+ ///
+ public ReactiveCommand ClearClientsCommand { get; }
+
+ ///
+ /// 开始监听
+ ///
+ private async Task StartListeningAsync()
+ {
+ try
+ {
+ if (ListenPort < 1 || ListenPort > 65535)
+ {
+ StatusMessage = $"无效的端口: {ListenPort}";
+ _logger?.LogWarning("无效的端口: {Port}", ListenPort);
+ return;
+ }
+
+ // 创建 UDP 服务器
+ var localEndPoint = new IPEndPoint(IPAddress.Any, ListenPort);
+ _udpServer = new UdpClient(localEndPoint);
+ _udpServer.EnableBroadcast = true;
+
+ IsListening = true;
+ StatusMessage = $"正在监听端口 {ListenPort}...";
+ _logger?.LogInformation("UDP 服务器开始监听端口 {Port}", ListenPort);
+
+ // 启动接收任务
+ _listenCancellationTokenSource = new CancellationTokenSource();
+ _ = Task.Run(() => ListenForMessagesAsync(_listenCancellationTokenSource.Token));
+ }
+ catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
+ {
+ StatusMessage = $"端口 {ListenPort} 已被占用";
+ _logger?.LogWarning("端口 {Port} 已被占用", ListenPort);
+ IsListening = false;
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "启动监听失败");
+ StatusMessage = $"启动失败: {ex.Message}";
+ IsListening = false;
+ }
+ }
+
+ ///
+ /// 停止监听
+ ///
+ private void StopListening()
+ {
+ try
+ {
+ _listenCancellationTokenSource?.Cancel();
+ _listenCancellationTokenSource?.Dispose();
+ _listenCancellationTokenSource = null;
+
+ _udpServer?.Close();
+ _udpServer?.Dispose();
+ _udpServer = null;
+
+ IsListening = false;
+ StatusMessage = "已停止监听";
+ _logger?.LogInformation("UDP 服务器已停止监听");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "停止监听时出错");
+ }
+ }
+
+ ///
+ /// 监听消息(后台任务)
+ ///
+ private async Task ListenForMessagesAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested && _udpServer != null)
+ {
+ try
+ {
+ var result = await _udpServer.ReceiveAsync();
+ var receivedData = Encoding.UTF8.GetString(result.Buffer);
+ var remoteEndPoint = result.RemoteEndPoint;
+ var timestamp = DateTime.Now.ToString("HH:mm:ss");
+ var displayMessage = $"[{timestamp}] 来自 {remoteEndPoint.Address}:{remoteEndPoint.Port}: {receivedData}";
+
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ ReceivedMessages.Add(displayMessage);
+
+ // 更新客户端列表
+ var clientAddress = remoteEndPoint.Address.ToString();
+ var clientPort = remoteEndPoint.Port;
+ var clientKey = $"{clientAddress}:{clientPort}";
+
+ var existingClient = Clients.FirstOrDefault(c => c.Address == clientAddress && c.Port == clientPort);
+ if (existingClient == null)
+ {
+ Clients.Add(new ClientInfo
+ {
+ Address = clientAddress,
+ Port = clientPort,
+ FirstSeen = DateTime.Now,
+ LastSeen = DateTime.Now,
+ MessageCount = 1
+ });
+ }
+ else
+ {
+ existingClient.LastSeen = DateTime.Now;
+ existingClient.MessageCount++;
+ }
+ });
+
+ _logger?.LogDebug("收到消息: {Message} 来自 {EndPoint}", receivedData, remoteEndPoint);
+
+ // 自动回复(可选)
+ var responseMessage = $"收到: {receivedData}";
+ var responseData = Encoding.UTF8.GetBytes(responseMessage);
+ await _udpServer.SendAsync(responseData, responseData.Length, remoteEndPoint);
+ }
+ catch (ObjectDisposedException)
+ {
+ // UDP 服务器已关闭,正常退出
+ break;
+ }
+ catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted)
+ {
+ // 操作被取消,正常退出
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "接收消息时出错");
+ await Task.Delay(1000, cancellationToken); // 等待后重试
+ }
+ }
+ }
+
+ ///
+ /// 清空消息
+ ///
+ private void ClearMessages()
+ {
+ ReceivedMessages.Clear();
+ _logger?.LogInformation("已清空消息列表");
+ }
+
+ ///
+ /// 清空客户端列表
+ ///
+ private void ClearClients()
+ {
+ Clients.Clear();
+ _logger?.LogInformation("已清空客户端列表");
+ }
+
+ ///
+ /// 清理资源
+ ///
+ public void Dispose()
+ {
+ StopListening();
+ }
+}
+
+///
+/// 客户端信息
+///
+public class ClientInfo : ReactiveObject
+{
+ private string _address = string.Empty;
+ private int _port;
+ private DateTime _firstSeen;
+ private DateTime _lastSeen;
+ private int _messageCount;
+
+ public string Address
+ {
+ get => _address;
+ set => this.RaiseAndSetIfChanged(ref _address, value);
+ }
+
+ public int Port
+ {
+ get => _port;
+ set => this.RaiseAndSetIfChanged(ref _port, value);
+ }
+
+ public DateTime FirstSeen
+ {
+ get => _firstSeen;
+ set => this.RaiseAndSetIfChanged(ref _firstSeen, value);
+ }
+
+ public DateTime LastSeen
+ {
+ get => _lastSeen;
+ set => this.RaiseAndSetIfChanged(ref _lastSeen, value);
+ }
+
+ public int MessageCount
+ {
+ get => _messageCount;
+ set => this.RaiseAndSetIfChanged(ref _messageCount, value);
+ }
+
+ public string DisplayName => $"{Address}:{Port}";
+}
+
diff --git a/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml b/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml
new file mode 100644
index 0000000..9cd5af6
--- /dev/null
+++ b/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml
@@ -0,0 +1,494 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs b/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs
new file mode 100644
index 0000000..1a52a15
--- /dev/null
+++ b/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs
@@ -0,0 +1,547 @@
+using ReactiveUI.Avalonia;
+using ReactiveUI;
+using Avalonia.Markup.Xaml;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
+using Avalonia;
+using Avalonia.Media;
+using Avalonia.Collections;
+using Avalonia.VisualTree;
+using AuroraDesk.Presentation.ViewModels.Pages;
+using AuroraDesk.Core.Entities;
+using System.Linq;
+using System.Reactive.Disposables;
+using System;
+using Avalonia.Layout;
+
+namespace AuroraDesk.Presentation.Views.Pages;
+
+public partial class NodeCanvasPageView : ReactiveUserControl
+{
+ private Canvas? _canvasContainer;
+ private Canvas? _gridBackgroundLayer;
+ private ScrollViewer? _canvasScrollViewer;
+ private Node? _draggedNode;
+ private Point? _dragStartPoint;
+ private ConnectionPoint? _hoveredConnectionPoint;
+ private Path? _tempConnectionLine;
+ private const double GridSize = 20;
+
+ public NodeCanvasPageView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // 延迟初始化,等待加载完成
+ this.Loaded += (s, e) =>
+ {
+ _canvasContainer = this.FindControl("CanvasContainer");
+ _gridBackgroundLayer = this.FindControl("GridBackgroundLayer");
+ _canvasScrollViewer = this.FindControl("CanvasScrollViewer");
+
+ if (_canvasContainer != null)
+ {
+ _canvasContainer.PointerPressed += OnCanvasPointerPressed;
+ _canvasContainer.PointerMoved += OnCanvasPointerMoved;
+ _canvasContainer.PointerReleased += OnCanvasPointerReleased;
+
+ // 在 Canvas 上添加拖拽事件
+ _canvasContainer.AddHandler(DragDrop.DragOverEvent, OnCanvasDragOver);
+ _canvasContainer.AddHandler(DragDrop.DropEvent, OnCanvasDrop);
+
+ // 监听 Canvas 尺寸变化,更新网格
+ _canvasContainer.SizeChanged += (sender, args) => DrawGridBackground();
+ }
+
+ // 在 ScrollViewer 上也添加拖拽事件,确保可以接收从左侧拖拽的组件
+ if (_canvasScrollViewer != null)
+ {
+ _canvasScrollViewer.AddHandler(DragDrop.DragOverEvent, OnCanvasDragOver);
+ _canvasScrollViewer.AddHandler(DragDrop.DropEvent, OnCanvasDrop);
+ }
+
+ // 设置组件库的拖拽支持
+ SetupTemplateDragAndDrop();
+
+ // 绘制网格背景
+ DrawGridBackground();
+
+ // 监听连接变化,更新连接线
+ if (ViewModel != null)
+ {
+ ViewModel.WhenAnyValue(x => x.Connections)
+ .Subscribe(_ => UpdateConnectionLines());
+
+ ViewModel.WhenAnyValue(x => x.Nodes)
+ .Subscribe(nodes =>
+ {
+ UpdateConnectionLines();
+ UpdateNodesOnCanvas(nodes);
+ // 调试:检查节点数量
+ System.Diagnostics.Debug.WriteLine($"节点数量: {nodes?.Count ?? 0}");
+ if (nodes != null)
+ {
+ foreach (var node in nodes)
+ {
+ System.Diagnostics.Debug.WriteLine($"节点: {node.Title}, 位置: ({node.X}, {node.Y})");
+ }
+ }
+ });
+
+ ViewModel.WhenAnyValue(x => x.IsConnecting, x => x.ConnectingSourcePoint)
+ .Subscribe(_ => UpdateConnectionLines());
+ }
+ };
+ }
+
+ private void SetupTemplateDragAndDrop()
+ {
+ // 延迟设置,等待模板加载完成
+ if (ViewModel != null)
+ {
+ ViewModel.WhenAnyValue(x => x.NodeTemplates)
+ .Subscribe(_ =>
+ {
+ // 使用延迟来确保UI已渲染
+ System.Threading.Tasks.Task.Delay(100).ContinueWith(_ =>
+ {
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ SetupTemplateItems();
+ });
+ });
+ });
+ }
+ }
+
+ private void SetupTemplateItems()
+ {
+ // 查找所有模板项
+ var templateItems = this.GetVisualDescendants()
+ .OfType()
+ .Where(b => b.Name == "TemplateItem")
+ .ToList();
+
+ foreach (var item in templateItems)
+ {
+ SetupTemplateItemDrag(item);
+ }
+ }
+
+ private void SetupTemplateItemDrag(Border item)
+ {
+ if (item.DataContext is NodeTemplate template)
+ {
+ item.PointerPressed += async (sender, args) =>
+ {
+ if (args.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
+ {
+ // 设置允许拖拽的光标
+ item.Cursor = new Cursor(StandardCursorType.DragMove);
+
+ var dataTransfer = new DataObject();
+ dataTransfer.Set("NodeTemplate", template);
+
+ // 执行拖拽操作
+ var result = await DragDrop.DoDragDrop(args, dataTransfer, DragDropEffects.Copy);
+
+ // 恢复光标
+ item.Cursor = Cursor.Default;
+ }
+ };
+ }
+ }
+
+ private void OnCanvasDragOver(object? sender, DragEventArgs e)
+ {
+ if (e.Data.GetDataFormats().Contains("NodeTemplate"))
+ {
+ e.DragEffects = DragDropEffects.Copy;
+ }
+ }
+
+ private void OnCanvasDrop(object? sender, DragEventArgs e)
+ {
+ if (_canvasContainer == null || ViewModel == null) return;
+
+ if (e.Data.GetDataFormats().Contains("NodeTemplate") &&
+ e.Data.Get("NodeTemplate") is NodeTemplate template)
+ {
+ // 获取相对于 Canvas 的位置
+ var point = e.GetPosition(_canvasContainer);
+ ViewModel.AddNodeFromTemplateCommand.Execute((template, point.X, point.Y)).Subscribe();
+ e.Handled = true;
+ }
+ }
+
+ private void UpdateConnectionLines()
+ {
+ if (_canvasContainer == null || ViewModel == null) return;
+
+ // 移除临时连接线
+ if (_tempConnectionLine != null && _canvasContainer.Children.Contains(_tempConnectionLine))
+ {
+ _canvasContainer.Children.Remove(_tempConnectionLine);
+ _tempConnectionLine = null;
+ }
+
+ // 更新所有连接线
+ var connections = ViewModel.Connections.ToList();
+ foreach (var connection in connections)
+ {
+ UpdateConnectionLine(connection);
+ }
+
+ // 如果正在连接,显示临时连接线
+ if (ViewModel.IsConnecting && ViewModel.ConnectingSourcePoint != null)
+ {
+ ShowTempConnectionLine(ViewModel.ConnectingSourcePoint);
+ }
+ }
+
+ private void UpdateConnectionLine(Connection connection)
+ {
+ if (_canvasContainer == null || connection.SourcePoint == null || connection.TargetPoint == null)
+ return;
+
+ var sourcePoint = GetConnectionPointPosition(connection.SourcePoint);
+ var targetPoint = GetConnectionPointPosition(connection.TargetPoint);
+
+ // 查找或创建连接线Path
+ var path = _canvasContainer.Children.OfType()
+ .FirstOrDefault(p => p.Tag == connection);
+
+ if (path == null)
+ {
+ path = new Path
+ {
+ Stroke = new SolidColorBrush(Color.FromRgb(52, 152, 219)),
+ StrokeThickness = 2,
+ Tag = connection
+ };
+ _canvasContainer.Children.Add(path);
+ }
+
+ // 创建贝塞尔曲线
+ var geometry = new PathGeometry();
+ var figure = new PathFigure
+ {
+ StartPoint = sourcePoint
+ };
+
+ var controlPoint1 = new Point(sourcePoint.X + 50, sourcePoint.Y);
+ var controlPoint2 = new Point(targetPoint.X - 50, targetPoint.Y);
+ var bezierSegment = new BezierSegment
+ {
+ Point1 = controlPoint1,
+ Point2 = controlPoint2,
+ Point3 = targetPoint
+ };
+
+ figure.Segments.Add(bezierSegment);
+ geometry.Figures.Add(figure);
+ path.Data = geometry;
+ }
+
+ private void ShowTempConnectionLine(ConnectionPoint sourcePoint)
+ {
+ if (_canvasContainer == null) return;
+
+ var startPoint = GetConnectionPointPosition(sourcePoint);
+ var endPoint = new Point(startPoint.X + 100, startPoint.Y);
+
+ if (_tempConnectionLine == null)
+ {
+ _tempConnectionLine = new Path
+ {
+ Stroke = new SolidColorBrush(Color.FromRgb(52, 152, 219)),
+ StrokeThickness = 2,
+ StrokeDashArray = new AvaloniaList { 5, 5 }
+ };
+ _canvasContainer.Children.Add(_tempConnectionLine);
+ }
+
+ var geometry = new PathGeometry();
+ var figure = new PathFigure { StartPoint = startPoint };
+ var lineSegment = new LineSegment { Point = endPoint };
+ figure.Segments.Add(lineSegment);
+ geometry.Figures.Add(figure);
+ _tempConnectionLine.Data = geometry;
+ }
+
+ private Point GetConnectionPointPosition(ConnectionPoint point)
+ {
+ if (point.Node == null || _canvasContainer == null)
+ return new Point(0, 0);
+
+ var nodeX = point.Node.X;
+ var nodeY = point.Node.Y;
+ var nodeHeight = point.Node.Height;
+
+ // 计算连接点在Canvas上的绝对位置
+ if (point.Type == ConnectionPointType.Input)
+ {
+ // 输入点在左侧,垂直居中
+ var inputIndex = point.Node.ConnectionPoints
+ .Where(p => p.Type == ConnectionPointType.Input)
+ .OrderBy(p => p.Index)
+ .ToList()
+ .IndexOf(point);
+
+ var spacing = nodeHeight / (point.Node.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Input) + 1);
+ return new Point(nodeX, nodeY + spacing * (inputIndex + 1));
+ }
+ else
+ {
+ // 输出点在右侧,垂直排列
+ var outputIndex = point.Node.ConnectionPoints
+ .Where(p => p.Type == ConnectionPointType.Output)
+ .OrderBy(p => p.Index)
+ .ToList()
+ .IndexOf(point);
+
+ var spacing = nodeHeight / (point.Node.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Output) + 1);
+ return new Point(nodeX + point.Node.Width, nodeY + spacing * (outputIndex + 1));
+ }
+ }
+
+ private void OnCanvasPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (_canvasContainer == null || ViewModel == null) return;
+
+ var point = e.GetPosition(_canvasContainer);
+ var source = e.Source as Control;
+
+ // 检查是否点击了连接点
+ var connectionPoint = FindConnectionPoint(source);
+ if (connectionPoint != null)
+ {
+ HandleConnectionPointClick(connectionPoint);
+ e.Handled = true;
+ return;
+ }
+
+ // 检查是否点击了节点(但不是连接点)
+ var node = FindNode(source);
+ if (node != null && connectionPoint == null)
+ {
+ ViewModel.SelectedNode = node;
+ _draggedNode = node;
+ _dragStartPoint = point;
+ e.Handled = true;
+ return;
+ }
+
+ // 点击空白区域,取消选中和连接
+ ViewModel.SelectedNode = null;
+ ViewModel.CancelConnection();
+
+ // 在点击位置添加新节点(仅在左键点击时)
+ if (e.GetCurrentPoint(_canvasContainer).Properties.IsLeftButtonPressed)
+ {
+ // 调试:输出点击位置
+ System.Diagnostics.Debug.WriteLine($"点击位置: ({point.X}, {point.Y})");
+ System.Diagnostics.Debug.WriteLine($"Canvas大小: {_canvasContainer.Bounds.Width} x {_canvasContainer.Bounds.Height}");
+
+ ViewModel.AddNodeCommand.Execute((point.X, point.Y)).Subscribe();
+ }
+ }
+
+ private void OnCanvasPointerMoved(object? sender, PointerEventArgs e)
+ {
+ if (_canvasContainer == null || ViewModel == null) return;
+
+ var point = e.GetPosition(_canvasContainer);
+
+ // 拖拽节点
+ if (_draggedNode != null && _dragStartPoint.HasValue)
+ {
+ var deltaX = point.X - _dragStartPoint.Value.X;
+ var deltaY = point.Y - _dragStartPoint.Value.Y;
+ ViewModel.UpdateNodePosition(_draggedNode,
+ _draggedNode.X + deltaX,
+ _draggedNode.Y + deltaY);
+ _dragStartPoint = point;
+ }
+
+ // 如果正在连接,更新临时连接线
+ if (ViewModel.IsConnecting && ViewModel.ConnectingSourcePoint != null)
+ {
+ var sourcePos = GetConnectionPointPosition(ViewModel.ConnectingSourcePoint);
+ if (_tempConnectionLine != null)
+ {
+ var geometry = new PathGeometry();
+ var figure = new PathFigure { StartPoint = sourcePos };
+ var lineSegment = new LineSegment { Point = point };
+ figure.Segments.Add(lineSegment);
+ geometry.Figures.Add(figure);
+ _tempConnectionLine.Data = geometry;
+ }
+ }
+ }
+
+ private void OnCanvasPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ _draggedNode = null;
+ _dragStartPoint = null;
+ }
+
+ private void HandleConnectionPointClick(ConnectionPoint connectionPoint)
+ {
+ if (ViewModel == null) return;
+
+ if (ViewModel.IsConnecting)
+ {
+ // 完成连接
+ if (connectionPoint.Type == ConnectionPointType.Input)
+ {
+ ViewModel.CompleteConnectionCommand.Execute(connectionPoint).Subscribe();
+ }
+ else
+ {
+ // 取消当前连接,开始新连接
+ ViewModel.CancelConnection();
+ ViewModel.StartConnectionCommand.Execute(connectionPoint).Subscribe();
+ }
+ }
+ else
+ {
+ // 开始连接
+ if (connectionPoint.Type == ConnectionPointType.Output)
+ {
+ ViewModel.StartConnectionCommand.Execute(connectionPoint).Subscribe();
+ }
+ }
+ }
+
+ private Node? FindNode(Control? control)
+ {
+ if (control == null || ViewModel == null) return null;
+
+ // 向上查找直到找到包含Node的容器
+ var current = control;
+ while (current != null)
+ {
+ if (current.DataContext is Node node && ViewModel.Nodes.Contains(node))
+ {
+ return node;
+ }
+ current = current.Parent as Control;
+ }
+
+ return null;
+ }
+
+ private ConnectionPoint? FindConnectionPoint(Control? control)
+ {
+ if (control == null) return null;
+
+ var current = control;
+ while (current != null)
+ {
+ if (current.DataContext is ConnectionPoint point)
+ {
+ return point;
+ }
+ current = current.Parent as Control;
+ }
+
+ return null;
+ }
+
+ private void DrawGridBackground()
+ {
+ if (_gridBackgroundLayer == null || _canvasContainer == null) return;
+
+ // 清空之前的网格线
+ _gridBackgroundLayer.Children.Clear();
+
+ var width = _canvasContainer.Bounds.Width > 0 ? _canvasContainer.Bounds.Width : 2000;
+ var height = _canvasContainer.Bounds.Height > 0 ? _canvasContainer.Bounds.Height : 2000;
+
+ // 创建网格线画笔
+ var gridBrush = new SolidColorBrush(Color.FromRgb(224, 224, 224)); // #E0E0E0
+
+ // 绘制垂直线
+ for (double x = 0; x <= width; x += GridSize)
+ {
+ var line = new Line
+ {
+ StartPoint = new Point(x, 0),
+ EndPoint = new Point(x, height),
+ Stroke = gridBrush,
+ StrokeThickness = 0.5
+ };
+ _gridBackgroundLayer.Children.Add(line);
+ }
+
+ // 绘制水平线
+ for (double y = 0; y <= height; y += GridSize)
+ {
+ var line = new Line
+ {
+ StartPoint = new Point(0, y),
+ EndPoint = new Point(width, y),
+ Stroke = gridBrush,
+ StrokeThickness = 0.5
+ };
+ _gridBackgroundLayer.Children.Add(line);
+ }
+ }
+
+ private void UpdateNodesOnCanvas(System.Collections.ObjectModel.ObservableCollection? nodes)
+ {
+ if (_canvasContainer == null || nodes == null) return;
+
+ // 查找 ItemsControl
+ var itemsControl = this.FindControl("NodesItemsControl");
+ if (itemsControl == null)
+ {
+ itemsControl = _canvasContainer.GetVisualDescendants()
+ .OfType()
+ .FirstOrDefault();
+ }
+
+ if (itemsControl != null)
+ {
+ // 强制更新 ItemsControl 的布局
+ itemsControl.InvalidateMeasure();
+ itemsControl.InvalidateArrange();
+
+ // 延迟更新,确保容器已生成
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ try
+ {
+ foreach (var item in itemsControl.Items)
+ {
+ if (item is Node node)
+ {
+ // 在 Avalonia 中,通过遍历视觉树来查找对应的容器
+ var container = itemsControl.GetVisualDescendants()
+ .OfType()
+ .FirstOrDefault(cc => cc.Content == node);
+
+ if (container is ContentControl contentControl)
+ {
+ Canvas.SetLeft(contentControl, node.X);
+ Canvas.SetTop(contentControl, node.Y);
+ System.Diagnostics.Debug.WriteLine($"设置节点 {node.Title} 位置: ({node.X}, {node.Y})");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"更新节点位置时出错: {ex.Message}");
+ }
+ }, Avalonia.Threading.DispatcherPriority.Normal);
+ }
+ }
+}
+
diff --git a/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml b/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml
new file mode 100644
index 0000000..837a6b7
--- /dev/null
+++ b/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml
@@ -0,0 +1,546 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml.cs b/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml.cs
new file mode 100644
index 0000000..0a15027
--- /dev/null
+++ b/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml.cs
@@ -0,0 +1,19 @@
+using ReactiveUI.Avalonia;
+using Avalonia.Markup.Xaml;
+using AuroraDesk.Presentation.ViewModels.Pages;
+
+namespace AuroraDesk.Presentation.Views.Pages;
+
+public partial class UdpClientPageView : ReactiveUserControl
+{
+ public UdpClientPageView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
+
diff --git a/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml b/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml
new file mode 100644
index 0000000..ba7994a
--- /dev/null
+++ b/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml
@@ -0,0 +1,558 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml.cs b/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml.cs
new file mode 100644
index 0000000..6d69691
--- /dev/null
+++ b/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml.cs
@@ -0,0 +1,69 @@
+using ReactiveUI.Avalonia;
+using Avalonia.Markup.Xaml;
+using Avalonia.Controls;
+using AuroraDesk.Presentation.ViewModels.Pages;
+using HeroIconsAvalonia.Controls;
+using ReactiveUI;
+using HeroIconsAvalonia.Enums;
+
+namespace AuroraDesk.Presentation.Views.Pages;
+
+public partial class UdpServerPageView : ReactiveUserControl
+{
+ private Button? _toggleButton;
+ private HeroIcon? _toggleIcon;
+ private Expander? _clientListExpander;
+
+ public UdpServerPageView()
+ {
+ InitializeComponent();
+ this.WhenActivated(disposables =>
+ {
+ // 查找控件
+ _toggleButton = this.FindControl