Browse Source

节点画布有一点问题

refactor/namespace-and-layering
root 1 month ago
parent
commit
15c34d59a2
  1. 1
      AuroraDesk.Application/AuroraDesk.Application.csproj
  2. 2
      AuroraDesk.Core/AuroraDesk.Core.csproj
  3. 50
      AuroraDesk.Core/Entities/Connection.cs
  4. 78
      AuroraDesk.Core/Entities/ConnectionPoint.cs
  5. 98
      AuroraDesk.Core/Entities/Node.cs
  6. 139
      AuroraDesk.Core/Entities/NodeTemplate.cs
  7. 57
      AuroraDesk.Core/Interfaces/INodeCanvasService.cs
  8. 2
      AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj
  9. 3
      AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs
  10. 30
      AuroraDesk.Infrastructure/Services/NavigationService.cs
  11. 2
      AuroraDesk.Infrastructure/Services/NavigationStateService.cs
  12. 157
      AuroraDesk.Infrastructure/Services/NodeCanvasService.cs
  13. 149
      AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs
  14. 3
      AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs
  15. 22
      AuroraDesk.Presentation/Services/PageViewModelFactory.cs
  16. 319
      AuroraDesk.Presentation/ViewModels/Pages/NodeCanvasPageViewModel.cs
  17. 304
      AuroraDesk.Presentation/ViewModels/Pages/UdpClientPageViewModel.cs
  18. 308
      AuroraDesk.Presentation/ViewModels/Pages/UdpServerPageViewModel.cs
  19. 494
      AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml
  20. 547
      AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs
  21. 546
      AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml
  22. 19
      AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml.cs
  23. 558
      AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml
  24. 69
      AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml.cs
  25. 160
      AuroraDesk/modify.md
  26. 4299
      modify.md

1
AuroraDesk.Application/AuroraDesk.Application.csproj

@ -11,7 +11,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI" Version="19.1.1" />
<PackageReference Include="HeroIcons.Avalonia" Version="1.0.4" />
</ItemGroup>
</Project>

2
AuroraDesk.Core/AuroraDesk.Core.csproj

@ -7,8 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI" Version="19.1.1" />
<PackageReference Include="HeroIcons.Avalonia" Version="1.0.4" />
<PackageReference Include="ReactiveUI" Version="22.2.1" />
</ItemGroup>
</Project>

50
AuroraDesk.Core/Entities/Connection.cs

@ -0,0 +1,50 @@
using ReactiveUI;
using System;
namespace AuroraDesk.Core.Entities;
/// <summary>
/// 连接实体 - 表示两个连接点之间的连线
/// </summary>
public class Connection : ReactiveObject
{
private ConnectionPoint? _sourcePoint;
private ConnectionPoint? _targetPoint;
/// <summary>
/// 连接唯一标识符
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 源连接点(输出点)
/// </summary>
public ConnectionPoint? SourcePoint
{
get => _sourcePoint;
set => this.RaiseAndSetIfChanged(ref _sourcePoint, value);
}
/// <summary>
/// 目标连接点(输入点)
/// </summary>
public ConnectionPoint? TargetPoint
{
get => _targetPoint;
set => this.RaiseAndSetIfChanged(ref _targetPoint, value);
}
/// <summary>
/// 验证连接是否有效(输出点必须连接到输入点)
/// </summary>
public bool IsValid()
{
if (SourcePoint == null || TargetPoint == null)
return false;
return SourcePoint.Type == ConnectionPointType.Output &&
TargetPoint.Type == ConnectionPointType.Input &&
SourcePoint.Node != TargetPoint.Node; // 不能连接到同一个节点
}
}

78
AuroraDesk.Core/Entities/ConnectionPoint.cs

@ -0,0 +1,78 @@
using ReactiveUI;
using System;
namespace AuroraDesk.Core.Entities;
/// <summary>
/// 连接点实体 - 节点的输入/输出连接点
/// </summary>
public class ConnectionPoint : ReactiveObject
{
private string _label = string.Empty;
private ConnectionPointType _type = ConnectionPointType.Output;
private int _index;
private bool _isConnected;
/// <summary>
/// 连接点唯一标识符
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 连接点标签(显示在连接点旁边的文本)
/// </summary>
public string Label
{
get => _label;
set => this.RaiseAndSetIfChanged(ref _label, value);
}
/// <summary>
/// 连接点类型(输入或输出)
/// </summary>
public ConnectionPointType Type
{
get => _type;
set => this.RaiseAndSetIfChanged(ref _type, value);
}
/// <summary>
/// 连接点索引(在节点上的位置顺序)
/// </summary>
public int Index
{
get => _index;
set => this.RaiseAndSetIfChanged(ref _index, value);
}
/// <summary>
/// 是否已连接
/// </summary>
public bool IsConnected
{
get => _isConnected;
set => this.RaiseAndSetIfChanged(ref _isConnected, value);
}
/// <summary>
/// 所属节点
/// </summary>
public Node? Node { get; set; }
}
/// <summary>
/// 连接点类型枚举
/// </summary>
public enum ConnectionPointType
{
/// <summary>
/// 输入连接点(在左侧)
/// </summary>
Input,
/// <summary>
/// 输出连接点(在右侧)
/// </summary>
Output
}

98
AuroraDesk.Core/Entities/Node.cs

@ -0,0 +1,98 @@
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
namespace AuroraDesk.Core.Entities;
/// <summary>
/// 节点实体 - 表示画布上的一个可拖拽组件
/// </summary>
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<ConnectionPoint> _connectionPoints = new();
/// <summary>
/// 节点唯一标识符
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// X坐标位置
/// </summary>
public double X
{
get => _x;
set => this.RaiseAndSetIfChanged(ref _x, value);
}
/// <summary>
/// Y坐标位置
/// </summary>
public double Y
{
get => _y;
set => this.RaiseAndSetIfChanged(ref _y, value);
}
/// <summary>
/// 节点宽度
/// </summary>
public double Width
{
get => _width;
set => this.RaiseAndSetIfChanged(ref _width, value);
}
/// <summary>
/// 节点高度
/// </summary>
public double Height
{
get => _height;
set => this.RaiseAndSetIfChanged(ref _height, value);
}
/// <summary>
/// 节点标题
/// </summary>
public string Title
{
get => _title;
set => this.RaiseAndSetIfChanged(ref _title, value);
}
/// <summary>
/// 节点内容(显示在节点内部的文本)
/// </summary>
public string Content
{
get => _content;
set => this.RaiseAndSetIfChanged(ref _content, value);
}
/// <summary>
/// 是否被选中
/// </summary>
public bool IsSelected
{
get => _isSelected;
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
}
/// <summary>
/// 连接点集合
/// </summary>
public ObservableCollection<ConnectionPoint> ConnectionPoints
{
get => _connectionPoints;
set => this.RaiseAndSetIfChanged(ref _connectionPoints, value);
}
}

139
AuroraDesk.Core/Entities/NodeTemplate.cs

@ -0,0 +1,139 @@
using ReactiveUI;
using System;
namespace AuroraDesk.Core.Entities;
/// <summary>
/// 节点模板 - 定义可用的节点类型
/// </summary>
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;
/// <summary>
/// 模板唯一标识符
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 模板名称(用于内部标识)
/// </summary>
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName
{
get => _displayName;
set => this.RaiseAndSetIfChanged(ref _displayName, value);
}
/// <summary>
/// 描述
/// </summary>
public string Description
{
get => _description;
set => this.RaiseAndSetIfChanged(ref _description, value);
}
/// <summary>
/// 节点内容(显示在节点内部的文本)
/// </summary>
public string Content
{
get => _content;
set => this.RaiseAndSetIfChanged(ref _content, value);
}
/// <summary>
/// 输入连接点数量
/// </summary>
public int InputCount
{
get => _inputCount;
set => this.RaiseAndSetIfChanged(ref _inputCount, value);
}
/// <summary>
/// 输出连接点数量
/// </summary>
public int OutputCount
{
get => _outputCount;
set => this.RaiseAndSetIfChanged(ref _outputCount, value);
}
/// <summary>
/// 节点宽度
/// </summary>
public double Width
{
get => _width;
set => this.RaiseAndSetIfChanged(ref _width, value);
}
/// <summary>
/// 节点高度
/// </summary>
public double Height
{
get => _height;
set => this.RaiseAndSetIfChanged(ref _height, value);
}
/// <summary>
/// 根据模板创建节点实例
/// </summary>
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;
}
}

57
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;
/// <summary>
/// 节点画布服务接口
/// </summary>
public interface INodeCanvasService
{
/// <summary>
/// 节点集合
/// </summary>
ObservableCollection<Node> Nodes { get; }
/// <summary>
/// 连接集合
/// </summary>
ObservableCollection<Connection> Connections { get; }
/// <summary>
/// 添加节点到画布
/// </summary>
void AddNode(Node node);
/// <summary>
/// 移除节点(同时移除相关连接)
/// </summary>
void RemoveNode(Node node);
/// <summary>
/// 更新节点位置
/// </summary>
void UpdateNodePosition(Node node, double x, double y);
/// <summary>
/// 创建连接
/// </summary>
bool CreateConnection(ConnectionPoint sourcePoint, ConnectionPoint targetPoint);
/// <summary>
/// 移除连接
/// </summary>
void RemoveConnection(Connection connection);
/// <summary>
/// 获取节点的所有连接
/// </summary>
IEnumerable<Connection> GetNodeConnections(Node node);
/// <summary>
/// 清除所有节点和连接
/// </summary>
void Clear();
}

2
AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj

@ -15,8 +15,8 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
<PackageReference Include="ReactiveUI" Version="19.1.1" />
<PackageReference Include="HeroIcons.Avalonia" Version="1.0.4" />
<PackageReference Include="ReactiveUI" Version="22.2.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />

3
AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs

@ -46,6 +46,9 @@ public static class ServiceCollectionExtensions
// 注册标签页管理服务(重构新增,分离标签页管理职责)
services.AddTransient<ITabManagementService, TabManagementService>();
// 注册节点画布服务
services.AddSingleton<INodeCanvasService, NodeCanvasService>();
return services;
}
}

30
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<NavigationItem>
{
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)
}
};

2
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;

157
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;
/// <summary>
/// 节点画布服务实现
/// </summary>
public class NodeCanvasService : INodeCanvasService
{
private readonly ILogger<NodeCanvasService>? _logger;
private readonly ObservableCollection<Node> _nodes = new();
private readonly ObservableCollection<Connection> _connections = new();
public ObservableCollection<Node> Nodes => _nodes;
public ObservableCollection<Connection> Connections => _connections;
public NodeCanvasService(ILogger<NodeCanvasService>? 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<Connection> GetNodeConnections(Node node)
{
if (node == null)
return Enumerable.Empty<Connection>();
return _connections.Where(c =>
c.SourcePoint?.Node == node || c.TargetPoint?.Node == node);
}
public void Clear()
{
_connections.Clear();
_nodes.Clear();
_logger?.LogDebug("清空画布");
}
}

149
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;
/// <summary>
/// 节点画布相关转换器
/// </summary>
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();
}
/// <summary>
/// 布尔值到边框颜色转换器(选中时显示蓝色边框)
/// </summary>
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();
}
}
/// <summary>
/// 布尔值到边框厚度转换器(选中时显示边框)
/// </summary>
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();
}
}
/// <summary>
/// 过滤输入连接点转换器
/// </summary>
public class FilterInputPointsConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is ObservableCollection<ConnectionPoint> points)
{
return points.Where(p => p.Type == ConnectionPointType.Input).ToList();
}
return Enumerable.Empty<ConnectionPoint>();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 过滤输出连接点转换器
/// </summary>
public class FilterOutputPointsConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is ObservableCollection<ConnectionPoint> points)
{
return points.Where(p => p.Type == ConnectionPointType.Output).ToList();
}
return Enumerable.Empty<ConnectionPoint>();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 节点到选择文本转换器
/// </summary>
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();
}
}
/// <summary>
/// 非空转换器(用于控制可见性)
/// </summary>
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();
}
}

3
AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs

@ -45,6 +45,9 @@ public static class ServiceCollectionExtensions
services.AddTransient<DialogHostPageViewModel>();
services.AddTransient<IconsPageViewModel>();
services.AddTransient<EditorPageViewModel>();
services.AddTransient<UdpClientPageViewModel>();
services.AddTransient<UdpServerPageViewModel>();
services.AddTransient<NodeCanvasPageViewModel>();
// 注意:MainWindowViewModel 的注册移到主项目的 App.axaml.cs 中
// 因为它依赖 AppViewModel,而 AppViewModel 在 AddReactiveUI() 中注册

22
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<Microsoft.Extensions.Logging.ILogger<ImageGalleryPageViewModel>>();
return ActivatorUtilities.CreateInstance<ImageGalleryPageViewModel>(_serviceProvider, screen, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<ImageGalleryPageViewModel>.Instance);
}
private UdpClientPageViewModel CreateUdpClientPageViewModel(IScreen screen)
{
var logger = _serviceProvider.GetService<Microsoft.Extensions.Logging.ILogger<UdpClientPageViewModel>>();
return ActivatorUtilities.CreateInstance<UdpClientPageViewModel>(_serviceProvider, screen, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<UdpClientPageViewModel>.Instance);
}
private UdpServerPageViewModel CreateUdpServerPageViewModel(IScreen screen)
{
var logger = _serviceProvider.GetService<Microsoft.Extensions.Logging.ILogger<UdpServerPageViewModel>>();
return ActivatorUtilities.CreateInstance<UdpServerPageViewModel>(_serviceProvider, screen, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<UdpServerPageViewModel>.Instance);
}
private NodeCanvasPageViewModel CreateNodeCanvasPageViewModel(IScreen screen)
{
var nodeCanvasService = _serviceProvider.GetRequiredService<AuroraDesk.Core.Interfaces.INodeCanvasService>();
var logger = _serviceProvider.GetService<Microsoft.Extensions.Logging.ILogger<NodeCanvasPageViewModel>>();
return ActivatorUtilities.CreateInstance<NodeCanvasPageViewModel>(_serviceProvider, screen, nodeCanvasService, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<NodeCanvasPageViewModel>.Instance);
}
}

319
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;
/// <summary>
/// 节点画布页面 ViewModel
/// </summary>
public class NodeCanvasPageViewModel : RoutableViewModel
{
private readonly INodeCanvasService _nodeCanvasService;
private readonly ILogger<NodeCanvasPageViewModel>? _logger;
private Node? _selectedNode;
private ConnectionPoint? _connectingSourcePoint;
private bool _isConnecting;
private double _canvasOffsetX;
private double _canvasOffsetY;
private double _canvasZoom = 1.0;
public ObservableCollection<Node> Nodes => _nodeCanvasService.Nodes;
public ObservableCollection<Connection> Connections => _nodeCanvasService.Connections;
private ObservableCollection<NodeTemplate> _nodeTemplates = new();
public ObservableCollection<NodeTemplate> NodeTemplates
{
get => _nodeTemplates;
set => this.RaiseAndSetIfChanged(ref _nodeTemplates, value);
}
/// <summary>
/// 选中的节点
/// </summary>
public Node? SelectedNode
{
get => _selectedNode;
set => this.RaiseAndSetIfChanged(ref _selectedNode, value);
}
/// <summary>
/// 正在连接的源连接点
/// </summary>
public ConnectionPoint? ConnectingSourcePoint
{
get => _connectingSourcePoint;
set => this.RaiseAndSetIfChanged(ref _connectingSourcePoint, value);
}
/// <summary>
/// 是否正在连接模式
/// </summary>
public bool IsConnecting
{
get => _isConnecting;
set => this.RaiseAndSetIfChanged(ref _isConnecting, value);
}
/// <summary>
/// 画布偏移X
/// </summary>
public double CanvasOffsetX
{
get => _canvasOffsetX;
set => this.RaiseAndSetIfChanged(ref _canvasOffsetX, value);
}
/// <summary>
/// 画布偏移Y
/// </summary>
public double CanvasOffsetY
{
get => _canvasOffsetY;
set => this.RaiseAndSetIfChanged(ref _canvasOffsetY, value);
}
/// <summary>
/// 画布缩放
/// </summary>
public double CanvasZoom
{
get => _canvasZoom;
set => this.RaiseAndSetIfChanged(ref _canvasZoom, value);
}
// 命令
public ReactiveCommand<Unit, Unit> ClearCanvasCommand { get; }
public ReactiveCommand<Node, Unit> DeleteNodeCommand { get; }
public ReactiveCommand<ConnectionPoint, Unit> StartConnectionCommand { get; }
public ReactiveCommand<ConnectionPoint, Unit> CompleteConnectionCommand { get; }
public ReactiveCommand<Connection, Unit> 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<NodeCanvasPageViewModel>? logger = null) : base(screen, "node-canvas")
{
_nodeCanvasService = nodeCanvasService;
_logger = logger;
// 初始化节点模板
InitializeNodeTemplates();
// 初始化命令
ClearCanvasCommand = ReactiveCommand.Create(ClearCanvas);
DeleteNodeCommand = ReactiveCommand.Create<Node>(DeleteNode);
StartConnectionCommand = ReactiveCommand.Create<ConnectionPoint>(StartConnection);
CompleteConnectionCommand = ReactiveCommand.Create<ConnectionPoint>(CompleteConnection);
DeleteConnectionCommand = ReactiveCommand.Create<Connection>(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 已创建");
}
/// <summary>
/// 初始化节点模板
/// </summary>
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
});
}
/// <summary>
/// 添加节点到画布
/// </summary>
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);
}
/// <summary>
/// 删除节点
/// </summary>
private void DeleteNode(Node node)
{
if (node == null) return;
_nodeCanvasService.RemoveNode(node);
if (SelectedNode == node)
{
SelectedNode = null;
}
_logger?.LogDebug("删除节点: {NodeId}", node.Id);
}
/// <summary>
/// 开始连接
/// </summary>
private void StartConnection(ConnectionPoint connectionPoint)
{
if (connectionPoint == null || connectionPoint.Type != ConnectionPointType.Output)
return;
ConnectingSourcePoint = connectionPoint;
IsConnecting = true;
_logger?.LogDebug("开始连接,源连接点: {PointId}", connectionPoint.Id);
}
/// <summary>
/// 完成连接
/// </summary>
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();
}
/// <summary>
/// 取消连接
/// </summary>
public void CancelConnection()
{
ConnectingSourcePoint = null;
IsConnecting = false;
}
/// <summary>
/// 删除连接
/// </summary>
private void DeleteConnection(Connection connection)
{
if (connection == null) return;
_nodeCanvasService.RemoveConnection(connection);
_logger?.LogDebug("删除连接: {ConnectionId}", connection.Id);
}
/// <summary>
/// 清空画布
/// </summary>
private void ClearCanvas()
{
_nodeCanvasService.Clear();
SelectedNode = null;
_logger?.LogDebug("清空画布");
}
/// <summary>
/// 更新节点位置
/// </summary>
public void UpdateNodePosition(Node node, double x, double y)
{
_nodeCanvasService.UpdateNodePosition(node, x, y);
}
/// <summary>
/// 从模板添加节点到画布
/// </summary>
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);
}
}

304
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;
/// <summary>
/// UDP 客户端页面 ViewModel
/// </summary>
public class UdpClientPageViewModel : RoutableViewModel
{
private readonly ILogger<UdpClientPageViewModel>? _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<string> _receivedMessages = new();
private readonly ObservableCollection<string> _sentMessages = new();
public UdpClientPageViewModel(
IScreen hostScreen,
ILogger<UdpClientPageViewModel>? 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);
}
/// <summary>
/// 是否已连接
/// </summary>
public bool IsConnected
{
get => _isConnected;
set => this.RaiseAndSetIfChanged(ref _isConnected, value);
}
/// <summary>
/// 服务器 IP 地址
/// </summary>
public string ServerIp
{
get => _serverIp;
set => this.RaiseAndSetIfChanged(ref _serverIp, value);
}
/// <summary>
/// 服务器端口
/// </summary>
public int ServerPort
{
get => _serverPort;
set => this.RaiseAndSetIfChanged(ref _serverPort, value);
}
/// <summary>
/// 本地端口
/// </summary>
public int LocalPort
{
get => _localPort;
set => this.RaiseAndSetIfChanged(ref _localPort, value);
}
/// <summary>
/// 要发送的消息
/// </summary>
public string Message
{
get => _message;
set => this.RaiseAndSetIfChanged(ref _message, value);
}
/// <summary>
/// 状态消息
/// </summary>
public string StatusMessage
{
get => _statusMessage;
set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
/// <summary>
/// 接收到的消息列表
/// </summary>
public ObservableCollection<string> ReceivedMessages => _receivedMessages;
/// <summary>
/// 已发送的消息列表
/// </summary>
public ObservableCollection<string> SentMessages => _sentMessages;
/// <summary>
/// 连接命令
/// </summary>
public ReactiveCommand<Unit, Unit> ConnectCommand { get; }
/// <summary>
/// 断开连接命令
/// </summary>
public ReactiveCommand<Unit, Unit> DisconnectCommand { get; }
/// <summary>
/// 发送消息命令
/// </summary>
public ReactiveCommand<Unit, Unit> SendMessageCommand { get; }
/// <summary>
/// 清空消息命令
/// </summary>
public ReactiveCommand<Unit, Unit> ClearMessagesCommand { get; }
/// <summary>
/// 连接到服务器
/// </summary>
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();
}
}
/// <summary>
/// 断开连接
/// </summary>
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, "断开连接时出错");
}
}
/// <summary>
/// 发送消息
/// </summary>
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}";
}
}
/// <summary>
/// 接收消息(后台任务)
/// </summary>
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); // 等待后重试
}
}
}
/// <summary>
/// 清空消息
/// </summary>
private void ClearMessages()
{
ReceivedMessages.Clear();
SentMessages.Clear();
_logger?.LogInformation("已清空消息列表");
}
/// <summary>
/// 清理资源
/// </summary>
public void Dispose()
{
Disconnect();
}
}

308
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;
/// <summary>
/// UDP 服务端页面 ViewModel
/// </summary>
public class UdpServerPageViewModel : RoutableViewModel
{
private readonly ILogger<UdpServerPageViewModel>? _logger;
private UdpClient? _udpServer;
private CancellationTokenSource? _listenCancellationTokenSource;
private bool _isListening;
private int _listenPort = 8080;
private string _statusMessage = "未启动";
private readonly ObservableCollection<string> _receivedMessages = new();
private readonly ObservableCollection<ClientInfo> _clients = new();
public UdpServerPageViewModel(
IScreen hostScreen,
ILogger<UdpServerPageViewModel>? 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);
}
/// <summary>
/// 是否正在监听
/// </summary>
public bool IsListening
{
get => _isListening;
set => this.RaiseAndSetIfChanged(ref _isListening, value);
}
/// <summary>
/// 监听端口
/// </summary>
public int ListenPort
{
get => _listenPort;
set => this.RaiseAndSetIfChanged(ref _listenPort, value);
}
/// <summary>
/// 状态消息
/// </summary>
public string StatusMessage
{
get => _statusMessage;
set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
/// <summary>
/// 接收到的消息列表
/// </summary>
public ObservableCollection<string> ReceivedMessages => _receivedMessages;
/// <summary>
/// 客户端列表
/// </summary>
public ObservableCollection<ClientInfo> Clients => _clients;
/// <summary>
/// 开始监听命令
/// </summary>
public ReactiveCommand<Unit, Unit> StartListeningCommand { get; }
/// <summary>
/// 停止监听命令
/// </summary>
public ReactiveCommand<Unit, Unit> StopListeningCommand { get; }
/// <summary>
/// 清空消息命令
/// </summary>
public ReactiveCommand<Unit, Unit> ClearMessagesCommand { get; }
/// <summary>
/// 清空客户端命令
/// </summary>
public ReactiveCommand<Unit, Unit> ClearClientsCommand { get; }
/// <summary>
/// 开始监听
/// </summary>
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;
}
}
/// <summary>
/// 停止监听
/// </summary>
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, "停止监听时出错");
}
}
/// <summary>
/// 监听消息(后台任务)
/// </summary>
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); // 等待后重试
}
}
}
/// <summary>
/// 清空消息
/// </summary>
private void ClearMessages()
{
ReceivedMessages.Clear();
_logger?.LogInformation("已清空消息列表");
}
/// <summary>
/// 清空客户端列表
/// </summary>
private void ClearClients()
{
Clients.Clear();
_logger?.LogInformation("已清空客户端列表");
}
/// <summary>
/// 清理资源
/// </summary>
public void Dispose()
{
StopListening();
}
}
/// <summary>
/// 客户端信息
/// </summary>
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}";
}

494
AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml

@ -0,0 +1,494 @@
<reactive:ReactiveUserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:AuroraDesk.Presentation.ViewModels.Pages"
xmlns:reactive="using:ReactiveUI.Avalonia"
xmlns:entities="using:AuroraDesk.Core.Entities"
xmlns:converters="using:AuroraDesk.Presentation.Converters"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="AuroraDesk.Presentation.Views.Pages.NodeCanvasPageView"
x:DataType="vm:NodeCanvasPageViewModel">
<reactive:ReactiveUserControl.Resources>
<converters:BooleanToBorderBrushConverter x:Key="BooleanToBorderBrushConverter"/>
<converters:BooleanToBorderThicknessConverter x:Key="BooleanToBorderThicknessConverter"/>
<converters:FilterInputPointsConverter x:Key="FilterInputPointsConverter"/>
<converters:FilterOutputPointsConverter x:Key="FilterOutputPointsConverter"/>
<converters:NodeToSelectionTextConverter x:Key="NodeToSelectionTextConverter"/>
<converters:IsNotNullConverter x:Key="IsNotNullConverter"/>
</reactive:ReactiveUserControl.Resources>
<Design.DataContext>
<vm:NodeCanvasPageViewModel />
</Design.DataContext>
<Grid Background="{StaticResource BackgroundWhite}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="320"/>
</Grid.ColumnDefinitions>
<!-- 左侧工具栏和组件库 -->
<Border Grid.Column="0"
Background="{StaticResource BackgroundLight}"
BorderBrush="{StaticResource BorderLight}"
BorderThickness="0,0,1,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 顶部标题 -->
<Border Grid.Row="0"
Background="{StaticResource BackgroundWhite}"
BorderBrush="{StaticResource BorderLight}"
BorderThickness="0,0,0,1"
Padding="15,12">
<TextBlock Text="节点编辑器"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimary}"/>
</Border>
<!-- 组件库区域 -->
<ScrollViewer x:Name="ComponentLibraryScrollViewer"
Grid.Row="1"
VerticalScrollBarVisibility="Auto"
Padding="12">
<StackPanel Spacing="12">
<TextBlock Text="组件库"
FontSize="13"
FontWeight="SemiBold"
Foreground="{StaticResource TextSecondary}"
Margin="0,0,0,8"/>
<!-- 节点模板列表 -->
<ItemsControl ItemsSource="{Binding NodeTemplates}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="entities:NodeTemplate">
<Border Background="{StaticResource BackgroundWhite}"
CornerRadius="8"
BorderBrush="{StaticResource BorderLight}"
BorderThickness="1"
Padding="12"
Margin="0,0,0,8"
Cursor="Hand"
x:Name="TemplateItem">
<Border.Styles>
<Style Selector="Border:pointerover">
<Setter Property="Background" Value="{StaticResource BackgroundLightHover}"/>
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBlue}"/>
</Style>
<Style Selector="Border:pressed">
<Setter Property="Background" Value="{StaticResource BackgroundLightActive}"/>
</Style>
</Border.Styles>
<!-- 节点预览 -->
<StackPanel Spacing="8">
<Border Background="White"
CornerRadius="6"
BorderBrush="{StaticResource BorderLight}"
BorderThickness="1"
Padding="8"
MinWidth="80"
MinHeight="60"
HorizontalAlignment="Center">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 内容 -->
<TextBlock Grid.Row="0"
Text="{Binding Content}"
FontSize="12"
FontWeight="Medium"
Foreground="{StaticResource TextPrimary}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<!-- 连接点预览 -->
<Grid Grid.Row="1" Margin="0,4,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧输入点 -->
<Border Grid.Column="0"
Width="12"
Height="12"
Background="White"
BorderBrush="{StaticResource PrimaryBlue}"
BorderThickness="1.5"
CornerRadius="6"
HorizontalAlignment="Left"/>
<!-- 右侧输出点 -->
<StackPanel Grid.Column="1"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="4">
<Border Width="12"
Height="12"
Background="White"
BorderBrush="{StaticResource StatusError}"
BorderThickness="1.5"
CornerRadius="6"/>
<Border Width="12"
Height="12"
Background="White"
BorderBrush="{StaticResource StatusError}"
BorderThickness="1.5"
CornerRadius="6"/>
<Border Width="12"
Height="12"
Background="White"
BorderBrush="{StaticResource StatusError}"
BorderThickness="1.5"
CornerRadius="6"/>
</StackPanel>
</Grid>
</Grid>
</Border>
<!-- 模板名称 -->
<TextBlock Text="{Binding DisplayName}"
FontSize="12"
FontWeight="Medium"
Foreground="{StaticResource TextPrimary}"
HorizontalAlignment="Center"/>
<!-- 描述 -->
<TextBlock Text="{Binding Description}"
FontSize="11"
Foreground="{StaticResource TextSecondary}"
HorizontalAlignment="Center"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
<!-- 底部操作按钮 -->
<Border Grid.Row="2"
Background="{StaticResource BackgroundWhite}"
BorderBrush="{StaticResource BorderLight}"
BorderThickness="0,1,0,0"
Padding="12">
<StackPanel Spacing="10">
<Button Content="清空画布"
Command="{Binding ClearCanvasCommand}"
Background="{StaticResource StatusError}"
Foreground="White"
BorderThickness="0"
CornerRadius="6"
Padding="12,8"
FontSize="13"
FontWeight="Medium"
Cursor="Hand">
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="{StaticResource ButtonCloseActive}"/>
</Style>
</Button.Styles>
</Button>
<Border Background="{StaticResource BackgroundLight}"
CornerRadius="6"
Padding="12"
BorderBrush="{StaticResource BorderLight}"
BorderThickness="1">
<TextBlock Text="提示:&#x0a;从组件库拖拽节点到画布&#x0a;或点击画布空白处添加节点"
Foreground="{StaticResource TextSecondary}"
FontSize="11"
LineHeight="18"
TextWrapping="Wrap"/>
</Border>
</StackPanel>
</Border>
</Grid>
</Border>
<!-- 中间画布区域 -->
<Grid Grid.Column="1" Background="{StaticResource BackgroundLight}">
<ScrollViewer x:Name="CanvasScrollViewer"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
Background="{StaticResource BackgroundLight}">
<Canvas x:Name="CanvasContainer"
ClipToBounds="False"
MinWidth="2000"
MinHeight="2000"
Background="{StaticResource BackgroundWhite}">
<!-- 网格背景层 -->
<Canvas x:Name="GridBackgroundLayer"
IsHitTestVisible="False"
ClipToBounds="False"/>
<!-- 节点 -->
<ItemsControl x:Name="NodesItemsControl" ItemsSource="{Binding Nodes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas ClipToBounds="False"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="entities:Node">
<ContentControl Content="{Binding}"
Canvas.Left="{Binding X}"
Canvas.Top="{Binding Y}">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Border Background="White"
CornerRadius="6"
BorderBrush="{Binding IsSelected, Converter={StaticResource BooleanToBorderBrushConverter}}"
BorderThickness="{Binding IsSelected, Converter={StaticResource BooleanToBorderThicknessConverter}}"
Padding="12"
MinWidth="{Binding Width}"
MinHeight="{Binding Height}"
BoxShadow="0 2 8 0 #00000015">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 内容 -->
<TextBlock Grid.Row="0"
Text="{Binding Content}"
FontSize="14"
FontWeight="Medium"
Foreground="{StaticResource TextPrimary}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<!-- 连接点容器 -->
<Grid Grid.Row="1" Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧输入点 -->
<ItemsControl Grid.Column="0"
ItemsSource="{Binding ConnectionPoints, Converter={StaticResource FilterInputPointsConverter}}"
HorizontalAlignment="Left">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Spacing="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="entities:ConnectionPoint">
<Border Width="14"
Height="14"
Background="White"
BorderBrush="{StaticResource PrimaryBlue}"
BorderThickness="2"
CornerRadius="7"
Cursor="Hand"
Margin="0,2">
<Border.Styles>
<Style Selector="Border:pointerover">
<Setter Property="Background" Value="{StaticResource PrimaryBlue}"/>
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBlueHover}"/>
</Style>
</Border.Styles>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 右侧输出点 -->
<ItemsControl Grid.Column="1"
ItemsSource="{Binding ConnectionPoints, Converter={StaticResource FilterOutputPointsConverter}}"
HorizontalAlignment="Right">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Spacing="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="entities:ConnectionPoint">
<Grid Margin="0,2">
<Border Width="14"
Height="14"
Background="White"
BorderBrush="{StaticResource StatusError}"
BorderThickness="2"
CornerRadius="7"
Cursor="Hand"
HorizontalAlignment="Right">
<Border.Styles>
<Style Selector="Border:pointerover">
<Setter Property="Background" Value="{StaticResource StatusError}"/>
<Setter Property="BorderBrush" Value="{StaticResource ButtonCloseActive}"/>
</Style>
</Border.Styles>
</Border>
<TextBlock Text="{Binding Label}"
FontSize="10"
FontWeight="Medium"
Foreground="{StaticResource TextSecondary}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,18,0"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</ScrollViewer>
</Grid>
<!-- 右侧属性面板 -->
<Border Grid.Column="2"
Background="{StaticResource BackgroundWhite}"
BorderBrush="{StaticResource BorderLight}"
BorderThickness="1,0,0,0">
<ScrollViewer>
<StackPanel Margin="16">
<TextBlock Text="特性"
FontSize="18"
FontWeight="Bold"
Foreground="{StaticResource TextPrimary}"
Margin="0,0,0,12"/>
<TextBlock Text="{Binding SelectedNode, Converter={StaticResource NodeToSelectionTextConverter}}"
FontSize="13"
Foreground="{StaticResource TextSecondary}"
Margin="0,0,0,20"/>
<Border Background="{StaticResource BackgroundLight}"
CornerRadius="8"
Padding="16"
Margin="0,0,0,12"
BorderBrush="{StaticResource BorderLight}"
BorderThickness="1"
IsVisible="{Binding SelectedNode, Converter={StaticResource IsNotNullConverter}}">
<StackPanel Spacing="16">
<TextBlock Text="基本信息"
FontSize="14"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimary}"
Margin="0,0,0,4"/>
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12" RowSpacing="10">
<TextBlock Grid.Column="0"
Text="类型:"
Foreground="{StaticResource TextSecondary}"
FontSize="13"
VerticalAlignment="Center"
Width="70"/>
<TextBlock Grid.Column="1"
Text="节点"
Foreground="{StaticResource TextPrimary}"
FontSize="13"
FontWeight="Medium"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="标题:"
Foreground="{StaticResource TextSecondary}"
FontSize="13"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="1"
Grid.Column="1"
Text="{Binding SelectedNode.Title}"
Foreground="{StaticResource TextPrimary}"
FontSize="13"
FontWeight="Medium"
VerticalAlignment="Center"/>
</Grid>
<Separator Margin="0,8,0,8"/>
<TextBlock Text="几何"
FontSize="14"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimary}"
Margin="0,0,0,4"/>
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12" RowSpacing="8">
<TextBlock Grid.Column="0"
Text="X:"
Foreground="{StaticResource TextSecondary}"
FontSize="13"
VerticalAlignment="Center"
Width="70"/>
<TextBlock Grid.Column="1"
Text="{Binding SelectedNode.X, StringFormat='{}{0:F2}'}"
Foreground="{StaticResource TextPrimary}"
FontSize="13"
FontWeight="Medium"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="Y:"
Foreground="{StaticResource TextSecondary}"
FontSize="13"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="1"
Grid.Column="1"
Text="{Binding SelectedNode.Y, StringFormat='{}{0:F2}'}"
Foreground="{StaticResource TextPrimary}"
FontSize="13"
FontWeight="Medium"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="2"
Grid.Column="0"
Text="宽度:"
Foreground="{StaticResource TextSecondary}"
FontSize="13"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="2"
Grid.Column="1"
Text="{Binding SelectedNode.Width, StringFormat='{}{0:F2}'}"
Foreground="{StaticResource TextPrimary}"
FontSize="13"
FontWeight="Medium"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="3"
Grid.Column="0"
Text="高度:"
Foreground="{StaticResource TextSecondary}"
FontSize="13"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="3"
Grid.Column="1"
Text="{Binding SelectedNode.Height, StringFormat='{}{0:F2}'}"
Foreground="{StaticResource TextPrimary}"
FontSize="13"
FontWeight="Medium"
VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</reactive:ReactiveUserControl>

547
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<NodeCanvasPageViewModel>
{
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<Canvas>("CanvasContainer");
_gridBackgroundLayer = this.FindControl<Canvas>("GridBackgroundLayer");
_canvasScrollViewer = this.FindControl<ScrollViewer>("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<Border>()
.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<Path>()
.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<double> { 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<Node>? nodes)
{
if (_canvasContainer == null || nodes == null) return;
// 查找 ItemsControl
var itemsControl = this.FindControl<ItemsControl>("NodesItemsControl");
if (itemsControl == null)
{
itemsControl = _canvasContainer.GetVisualDescendants()
.OfType<ItemsControl>()
.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<ContentControl>()
.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);
}
}
}

546
AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml

@ -0,0 +1,546 @@
<reactive:ReactiveUserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:AuroraDesk.Presentation.ViewModels.Pages"
xmlns:reactive="using:ReactiveUI.Avalonia"
xmlns:heroicons="clr-namespace:HeroIconsAvalonia.Controls;assembly=HeroIconsAvalonia"
xmlns:converters="using:AuroraDesk.Presentation.Converters"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="AuroraDesk.Presentation.Views.Pages.UdpClientPageView"
x:DataType="vm:UdpClientPageViewModel">
<reactive:ReactiveUserControl.Resources>
<converters:InvertedBoolConverter x:Key="InvertedBoolConverter"/>
</reactive:ReactiveUserControl.Resources>
<Design.DataContext>
<vm:UdpClientPageViewModel />
</Design.DataContext>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 状态卡片行 -->
<Grid Grid.Row="0" Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 连接状态 -->
<Border Grid.Column="0"
Background="#3498DB"
CornerRadius="12"
Margin="0,0,8,0"
Padding="20">
<StackPanel>
<heroicons:HeroIcon Type="Link"
Width="28" Height="28"
Foreground="White"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<TextBlock Text="{Binding StatusMessage}"
FontSize="13"
FontWeight="SemiBold"
Foreground="White"
HorizontalAlignment="Center"
TextWrapping="Wrap"
TextAlignment="Center"
Margin="0,0,0,6"/>
<Border Background="White"
CornerRadius="12"
Padding="6,3"
HorizontalAlignment="Center"
IsVisible="{Binding IsConnected}">
<TextBlock Text="已连接"
FontSize="10"
FontWeight="Bold"
Foreground="#3498DB"/>
</Border>
<Border Background="White"
CornerRadius="12"
Padding="6,3"
HorizontalAlignment="Center"
IsVisible="{Binding IsConnected, Converter={StaticResource InvertedBoolConverter}}">
<TextBlock Text="未连接"
FontSize="10"
FontWeight="Bold"
Foreground="#E74C3C"/>
</Border>
</StackPanel>
</Border>
<!-- 已发送 -->
<Border Grid.Column="1"
Background="#27AE60"
CornerRadius="12"
Margin="4,0,4,0"
Padding="20">
<StackPanel>
<heroicons:HeroIcon Type="ArrowUpTray"
Width="28" Height="28"
Foreground="White"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<TextBlock Text="{Binding SentMessages.Count}"
FontSize="28"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"/>
<TextBlock Text="已发送"
FontSize="12"
Foreground="White"
HorizontalAlignment="Center"
Opacity="0.9"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
<!-- 接收消息 -->
<Border Grid.Column="2"
Background="#F39C12"
CornerRadius="12"
Margin="8,0,0,0"
Padding="20">
<StackPanel>
<heroicons:HeroIcon Type="ArrowDownTray"
Width="28" Height="28"
Foreground="White"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<TextBlock Text="{Binding ReceivedMessages.Count}"
FontSize="28"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"/>
<TextBlock Text="接收消息"
FontSize="12"
Foreground="White"
HorizontalAlignment="Center"
Opacity="0.9"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
</Grid>
<!-- 主要内容 -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 第一行:连接配置 -->
<Border Grid.Row="0"
Background="White"
BorderBrush="#E5E7EB"
BorderThickness="1"
CornerRadius="10"
Padding="0"
Margin="0,0,0,12"
HorizontalAlignment="Stretch">
<Expander Header="连接配置"
IsExpanded="True"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<Expander.HeaderTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="16,12">
<heroicons:HeroIcon Type="Cog6Tooth"
Width="18" Height="18"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<TextBlock Text="连接配置"
FontSize="14"
FontWeight="SemiBold"
Foreground="#1F2937"
VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</DataTemplate>
</Expander.HeaderTemplate>
<StackPanel Spacing="10" Margin="16,0,16,16">
<!-- 配置行 -->
<Grid ColumnDefinitions="Auto,200,Auto,200,Auto,200,Auto,*" ColumnSpacing="12">
<TextBlock Grid.Column="0"
Text="IP"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center"/>
<TextBox Grid.Column="1"
Text="{Binding ServerIp}"
Watermark="127.0.0.1"
FontSize="12"
Padding="8,6"
CornerRadius="6"
BorderBrush="#D1D5DB"
Background="#FAFAFA"/>
<TextBlock Grid.Column="2"
Text="端口"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center"
Margin="12,0,0,0"/>
<NumericUpDown Grid.Column="3"
Value="{Binding ServerPort}"
Minimum="1" Maximum="65535"
FontSize="12"
Padding="8,6"
CornerRadius="6"
BorderBrush="#D1D5DB"
Background="#FAFAFA"/>
<TextBlock Grid.Column="4"
Text="本地端口"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center"
Margin="12,0,0,0"/>
<Border Grid.Column="5"
Background="#F3F4F6"
CornerRadius="6"
Padding="8,6">
<TextBlock Text="{Binding LocalPort, StringFormat='{}{0}'}"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center"/>
</Border>
<!-- 按钮 -->
<StackPanel Grid.Column="7"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="8">
<Button Command="{Binding ConnectCommand}"
Background="#3498DB"
Foreground="White"
BorderThickness="0"
CornerRadius="8"
Padding="20,9"
FontSize="13"
FontWeight="SemiBold"
Cursor="Hand"
IsEnabled="{Binding IsConnected, Converter={StaticResource InvertedBoolConverter}}">
<Button.Content>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="6">
<heroicons:HeroIcon Type="CheckCircle"
Width="16" Height="16"
Foreground="White"
VerticalAlignment="Center"/>
<TextBlock Text="连接"
FontSize="13"
FontWeight="SemiBold"
Foreground="White"
VerticalAlignment="Center"/>
</StackPanel>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#2980B9"/>
</Style>
<Style Selector="Button:disabled">
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="Foreground" Value="#9CA3AF"/>
</Style>
</Button.Styles>
</Button>
<Button Command="{Binding DisconnectCommand}"
Background="#E74C3C"
Foreground="White"
BorderThickness="0"
CornerRadius="8"
Padding="20,9"
FontSize="13"
FontWeight="SemiBold"
Cursor="Hand"
IsEnabled="{Binding IsConnected}">
<Button.Content>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="6">
<heroicons:HeroIcon Type="XCircle"
Width="16" Height="16"
Foreground="White"
VerticalAlignment="Center"/>
<TextBlock Text="断开"
FontSize="13"
FontWeight="SemiBold"
Foreground="White"
VerticalAlignment="Center"/>
</StackPanel>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#C0392B"/>
</Style>
<Style Selector="Button:disabled">
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="Foreground" Value="#9CA3AF"/>
</Style>
</Button.Styles>
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Expander>
</Border>
<!-- 第二行:左右分栏 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<!-- 左边:已发送消息 -->
<Border Grid.Column="0"
Background="White"
BorderBrush="#E5E7EB"
BorderThickness="1"
CornerRadius="10"
Padding="0"
Margin="0,0,12,0"
HorizontalAlignment="Stretch">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0"
Background="#F9FAFB"
BorderBrush="#E5E7EB"
BorderThickness="0,0,0,1"
CornerRadius="10,10,0,0"
Padding="16,12">
<Grid>
<StackPanel Orientation="Horizontal" Spacing="8">
<heroicons:HeroIcon Type="ArrowUpTray"
Width="18" Height="18"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<TextBlock Text="已发送消息"
FontSize="14"
FontWeight="SemiBold"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<Border Background="#3498DB"
CornerRadius="8"
Padding="4,2"
VerticalAlignment="Center">
<TextBlock Text="{Binding SentMessages.Count}"
FontSize="10"
FontWeight="Bold"
Foreground="White"/>
</Border>
</StackPanel>
<Button Command="{Binding ClearMessagesCommand}"
HorizontalAlignment="Right"
Background="Transparent"
Foreground="#6B7280"
BorderThickness="0"
CornerRadius="6"
Padding="8,4"
FontSize="12"
Cursor="Hand">
<Button.Content>
<StackPanel Orientation="Horizontal" Spacing="4">
<heroicons:HeroIcon Type="Trash"
Width="14" Height="14"
Foreground="#6B7280"
VerticalAlignment="Center"/>
<TextBlock Text="清空"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center"/>
</StackPanel>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#F3F4F6"/>
<Setter Property="Foreground" Value="#1F2937"/>
</Style>
</Button.Styles>
</Button>
</Grid>
</Border>
<Grid Grid.Row="1"
RowDefinitions="*,Auto"
Margin="16">
<ScrollViewer Grid.Row="0"
VerticalScrollBarVisibility="Auto"
Margin="0,0,0,12">
<ItemsControl ItemsSource="{Binding SentMessages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#FAFAFA"
Padding="14,10"
Margin="0,0,0,1"
BorderBrush="#E5E7EB"
BorderThickness="0,0,0,1">
<TextBlock Text="{Binding}"
TextWrapping="Wrap"
FontSize="12"
Foreground="#1F2937"
LineHeight="18"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Grid Grid.Row="1"
ColumnDefinitions="*,Auto"
ColumnSpacing="8">
<TextBox Grid.Column="0"
Text="{Binding Message}"
Watermark="输入消息..."
AcceptsReturn="True"
TextWrapping="Wrap"
MinHeight="60"
FontSize="12"
Padding="10,8"
CornerRadius="8"
BorderBrush="#D1D5DB"
Background="#FAFAFA"/>
<Button Grid.Column="1"
Command="{Binding SendMessageCommand}"
Background="#27AE60"
Foreground="White"
BorderThickness="0"
CornerRadius="8"
Padding="20,12"
FontSize="13"
FontWeight="SemiBold"
MinWidth="100"
Cursor="Hand"
IsEnabled="{Binding IsConnected}">
<Button.Content>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="6">
<heroicons:HeroIcon Type="PaperAirplane"
Width="16" Height="16"
Foreground="White"
VerticalAlignment="Center"/>
<TextBlock Text="发送"
FontSize="13"
FontWeight="SemiBold"
Foreground="White"
VerticalAlignment="Center"/>
</StackPanel>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#229954"/>
</Style>
<Style Selector="Button:disabled">
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="Foreground" Value="#9CA3AF"/>
</Style>
</Button.Styles>
</Button>
</Grid>
</Grid>
</Grid>
</Border>
<!-- 右边:接收消息(占2/3) -->
<Border Grid.Column="1"
Background="White"
BorderBrush="#E5E7EB"
BorderThickness="1"
CornerRadius="10"
Padding="0"
HorizontalAlignment="Stretch">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0"
Background="#F9FAFB"
BorderBrush="#E5E7EB"
BorderThickness="0,0,0,1"
CornerRadius="10,10,0,0"
Padding="16,12">
<Grid>
<StackPanel Orientation="Horizontal" Spacing="8">
<heroicons:HeroIcon Type="ArrowDownTray"
Width="18" Height="18"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<TextBlock Text="接收消息"
FontSize="14"
FontWeight="SemiBold"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<Border Background="#F39C12"
CornerRadius="8"
Padding="4,2"
VerticalAlignment="Center">
<TextBlock Text="{Binding ReceivedMessages.Count}"
FontSize="10"
FontWeight="Bold"
Foreground="White"/>
</Border>
</StackPanel>
<Button Command="{Binding ClearMessagesCommand}"
HorizontalAlignment="Right"
Background="Transparent"
Foreground="#6B7280"
BorderThickness="0"
CornerRadius="6"
Padding="8,4"
FontSize="12"
Cursor="Hand">
<Button.Content>
<StackPanel Orientation="Horizontal" Spacing="4">
<heroicons:HeroIcon Type="Trash"
Width="14" Height="14"
Foreground="#6B7280"
VerticalAlignment="Center"/>
<TextBlock Text="清空"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center"/>
</StackPanel>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#F3F4F6"/>
<Setter Property="Foreground" Value="#1F2937"/>
</Style>
</Button.Styles>
</Button>
</Grid>
</Border>
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding ReceivedMessages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#F0FDF4"
Padding="14,10"
Margin="0,0,0,1"
BorderBrush="#D1FAE5"
BorderThickness="0,0,0,1">
<TextBlock Text="{Binding}"
TextWrapping="Wrap"
FontSize="12"
Foreground="#1F2937"
LineHeight="18"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Border>
</Grid>
</Grid>
</Grid>
</reactive:ReactiveUserControl>

19
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<UdpClientPageViewModel>
{
public UdpClientPageView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

558
AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml

@ -0,0 +1,558 @@
<reactive:ReactiveUserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:AuroraDesk.Presentation.ViewModels.Pages"
xmlns:reactive="using:ReactiveUI.Avalonia"
xmlns:heroicons="clr-namespace:HeroIconsAvalonia.Controls;assembly=HeroIconsAvalonia"
xmlns:converters="using:AuroraDesk.Presentation.Converters"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="AuroraDesk.Presentation.Views.Pages.UdpServerPageView"
x:DataType="vm:UdpServerPageViewModel">
<reactive:ReactiveUserControl.Resources>
<converters:InvertedBoolConverter x:Key="InvertedBoolConverter"/>
</reactive:ReactiveUserControl.Resources>
<Design.DataContext>
<vm:UdpServerPageViewModel />
</Design.DataContext>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 状态卡片行 -->
<Grid Grid.Row="0" Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 服务器状态 -->
<Border Grid.Column="0"
Background="{Binding IsListening, Converter={StaticResource InvertedBoolConverter}}">
<Border.Background>
<SolidColorBrush Color="#E74C3C"/>
</Border.Background>
<Border Background="{Binding IsListening}">
<Border.Background>
<SolidColorBrush Color="#27AE60"/>
</Border.Background>
<Border CornerRadius="12"
Margin="0,0,8,0"
Padding="20">
<StackPanel>
<heroicons:HeroIcon Type="Server"
Width="28" Height="28"
Foreground="White"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<TextBlock Text="{Binding StatusMessage}"
FontSize="13"
FontWeight="SemiBold"
Foreground="White"
HorizontalAlignment="Center"
TextWrapping="Wrap"
TextAlignment="Center"
Margin="0,0,0,6"/>
<Border Background="White"
CornerRadius="12"
Padding="6,3"
HorizontalAlignment="Center"
IsVisible="{Binding IsListening}">
<TextBlock Text="运行中"
FontSize="10"
FontWeight="Bold"
Foreground="#27AE60"/>
</Border>
<Border Background="White"
CornerRadius="12"
Padding="6,3"
HorizontalAlignment="Center"
IsVisible="{Binding IsListening, Converter={StaticResource InvertedBoolConverter}}">
<TextBlock Text="已停止"
FontSize="10"
FontWeight="Bold"
Foreground="#E74C3C"/>
</Border>
</StackPanel>
</Border>
</Border>
</Border>
<!-- 客户端数 -->
<Border Grid.Column="1"
Background="#3498DB"
CornerRadius="12"
Margin="4,0,4,0"
Padding="20">
<StackPanel>
<heroicons:HeroIcon Type="UserGroup"
Width="28" Height="28"
Foreground="White"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<TextBlock Text="{Binding Clients.Count}"
FontSize="28"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"/>
<TextBlock Text="连接客户端"
FontSize="12"
Foreground="White"
HorizontalAlignment="Center"
Opacity="0.9"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
<!-- 接收消息 -->
<Border Grid.Column="2"
Background="#F39C12"
CornerRadius="12"
Margin="8,0,0,0"
Padding="20">
<StackPanel>
<heroicons:HeroIcon Type="Envelope"
Width="28" Height="28"
Foreground="White"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<TextBlock Text="{Binding ReceivedMessages.Count}"
FontSize="28"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"/>
<TextBlock Text="接收消息"
FontSize="12"
Foreground="White"
HorizontalAlignment="Center"
Opacity="0.9"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
</Grid>
<!-- 主要内容 -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 第一行:服务器配置 -->
<Border Grid.Row="0"
Background="White"
BorderBrush="#E5E7EB"
BorderThickness="1"
CornerRadius="10"
Padding="0"
Margin="0,0,0,12"
HorizontalAlignment="Stretch">
<Expander Header="服务器控制"
IsExpanded="True"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<Expander.HeaderTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="16,12">
<heroicons:HeroIcon Type="Cog6Tooth"
Width="18" Height="18"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<TextBlock Text="服务器控制"
FontSize="14"
FontWeight="SemiBold"
Foreground="#1F2937"
VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</DataTemplate>
</Expander.HeaderTemplate>
<StackPanel Spacing="10" Margin="16,0,16,16">
<!-- 配置行 -->
<Grid ColumnDefinitions="Auto,200,Auto,*" ColumnSpacing="12">
<TextBlock Grid.Column="0"
Text="监听端口"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center"/>
<NumericUpDown Grid.Column="1"
Value="{Binding ListenPort}"
Minimum="1" Maximum="65535"
FontSize="12"
Padding="8,6"
CornerRadius="6"
BorderBrush="#D1D5DB"
Background="#FAFAFA"
IsEnabled="{Binding IsListening, Converter={StaticResource InvertedBoolConverter}}"/>
<!-- 按钮 -->
<StackPanel Grid.Column="3"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="8">
<Button Command="{Binding StartListeningCommand}"
Background="#27AE60"
Foreground="White"
BorderThickness="0"
CornerRadius="8"
Padding="20,9"
FontSize="13"
FontWeight="SemiBold"
Cursor="Hand"
IsEnabled="{Binding IsListening, Converter={StaticResource InvertedBoolConverter}}">
<Button.Content>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="6">
<heroicons:HeroIcon Type="PlayCircle"
Width="16" Height="16"
Foreground="White"
VerticalAlignment="Center"/>
<TextBlock Text="启动"
FontSize="13"
FontWeight="SemiBold"
Foreground="White"
VerticalAlignment="Center"/>
</StackPanel>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#229954"/>
</Style>
<Style Selector="Button:disabled">
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="Foreground" Value="#9CA3AF"/>
</Style>
</Button.Styles>
</Button>
<Button Command="{Binding StopListeningCommand}"
Background="#E74C3C"
Foreground="White"
BorderThickness="0"
CornerRadius="8"
Padding="20,9"
FontSize="13"
FontWeight="SemiBold"
Cursor="Hand"
IsEnabled="{Binding IsListening}">
<Button.Content>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="6">
<heroicons:HeroIcon Type="StopCircle"
Width="16" Height="16"
Foreground="White"
VerticalAlignment="Center"/>
<TextBlock Text="停止"
FontSize="13"
FontWeight="SemiBold"
Foreground="White"
VerticalAlignment="Center"/>
</StackPanel>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#C0392B"/>
</Style>
<Style Selector="Button:disabled">
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="Foreground" Value="#9CA3AF"/>
</Style>
</Button.Styles>
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Expander>
</Border>
<!-- 第二行:左右分栏 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<!-- 左边:客户端列表(支持折叠) -->
<Border Grid.Column="0"
Background="White"
BorderBrush="#E5E7EB"
BorderThickness="1"
CornerRadius="10"
Padding="0"
Margin="0,0,12,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid RowDefinitions="Auto,*"
VerticalAlignment="Stretch">
<Border Grid.Row="0"
Background="#F9FAFB"
BorderBrush="#E5E7EB"
BorderThickness="0,0,0,1"
CornerRadius="10,10,0,0"
Padding="0,12">
<Grid HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto">
<!-- 左侧折叠按钮 -->
<Button Name="ToggleButton"
Grid.Column="0"
Background="Transparent"
Foreground="#6B7280"
BorderThickness="0"
Padding="16,0,12,0"
Cursor="Hand"
Width="40"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<Button.Content>
<heroicons:HeroIcon Name="ToggleIcon"
Type="ChevronDown"
Width="16" Height="16"
Foreground="#6B7280"
VerticalAlignment="Center"/>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#F3F4F6"/>
</Style>
</Button.Styles>
</Button>
<!-- 中间内容 -->
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<heroicons:HeroIcon Type="UserGroup"
Width="18" Height="18"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<TextBlock Text="客户端列表"
FontSize="14"
FontWeight="SemiBold"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<Border Background="#3498DB"
CornerRadius="8"
Padding="4,2"
VerticalAlignment="Center">
<TextBlock Text="{Binding Clients.Count}"
FontSize="10"
FontWeight="Bold"
Foreground="White"/>
</Border>
</StackPanel>
<!-- 右侧清空按钮 -->
<Button Grid.Column="2"
Command="{Binding ClearClientsCommand}"
HorizontalAlignment="Right"
Background="Transparent"
Foreground="#6B7280"
BorderThickness="0"
CornerRadius="6"
Padding="8,4"
Margin="0,0,16,0"
FontSize="12"
Cursor="Hand">
<Button.Content>
<StackPanel Orientation="Horizontal" Spacing="4">
<heroicons:HeroIcon Type="Trash"
Width="14" Height="14"
Foreground="#6B7280"
VerticalAlignment="Center"/>
<TextBlock Text="清空"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center"/>
</StackPanel>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#F3F4F6"/>
<Setter Property="Foreground" Value="#1F2937"/>
</Style>
</Button.Styles>
</Button>
</Grid>
</Border>
<Expander Name="ClientListExpander"
Grid.Row="1"
IsExpanded="True"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalAlignment="Stretch"
Padding="0">
<Expander.HeaderTemplate>
<DataTemplate>
<Border Height="0"/>
</DataTemplate>
</Expander.HeaderTemplate>
<Expander.Styles>
<Style Selector="Expander /template/ ToggleButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
</Expander.Styles>
<ScrollViewer VerticalScrollBarVisibility="Auto"
Margin="16"
VerticalAlignment="Stretch">
<ItemsControl ItemsSource="{Binding Clients}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#FAFAFA"
Padding="14,10"
Margin="0,0,0,1"
BorderBrush="#E5E7EB"
BorderThickness="0,0,0,1">
<StackPanel Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="6">
<heroicons:HeroIcon Type="ComputerDesktop"
Width="14" Height="14"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding DisplayName}"
FontWeight="SemiBold"
FontSize="13"
Foreground="#1F2937"
VerticalAlignment="Center"/>
</StackPanel>
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="8">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="4">
<TextBlock Text="消息:"
FontSize="11"
Foreground="#6B7280"/>
<TextBlock Text="{Binding MessageCount}"
FontSize="11"
FontWeight="SemiBold"
Foreground="#3498DB"/>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4">
<TextBlock Text="首次:"
FontSize="10"
Foreground="#6B7280"/>
<TextBlock Text="{Binding FirstSeen, StringFormat='{}{0:HH:mm:ss}'}"
FontSize="10"
Foreground="#6B7280"/>
</StackPanel>
</Grid>
<TextBlock FontSize="10" Foreground="#6B7280">
<TextBlock.Text>
<MultiBinding StringFormat="最后活动: {0:HH:mm:ss}">
<Binding Path="LastSeen"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Expander>
</Grid>
</Border>
<!-- 右边:接收消息(占2/3) -->
<Border Grid.Column="1"
Background="White"
BorderBrush="#E5E7EB"
BorderThickness="1"
CornerRadius="10"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid RowDefinitions="Auto,*"
VerticalAlignment="Stretch">
<Border Grid.Row="0"
Background="#F9FAFB"
BorderBrush="#E5E7EB"
BorderThickness="0,0,0,1"
CornerRadius="10,10,0,0"
Padding="16,12">
<Grid>
<StackPanel Orientation="Horizontal" Spacing="8">
<heroicons:HeroIcon Type="Envelope"
Width="18" Height="18"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<TextBlock Text="接收消息"
FontSize="14"
FontWeight="SemiBold"
Foreground="#1F2937"
VerticalAlignment="Center"/>
<Border Background="#F39C12"
CornerRadius="8"
Padding="4,2"
VerticalAlignment="Center">
<TextBlock Text="{Binding ReceivedMessages.Count}"
FontSize="10"
FontWeight="Bold"
Foreground="White"/>
</Border>
</StackPanel>
<Button Command="{Binding ClearMessagesCommand}"
HorizontalAlignment="Right"
Background="Transparent"
Foreground="#6B7280"
BorderThickness="0"
CornerRadius="6"
Padding="8,4"
FontSize="12"
Cursor="Hand">
<Button.Content>
<StackPanel Orientation="Horizontal" Spacing="4">
<heroicons:HeroIcon Type="Trash"
Width="14" Height="14"
Foreground="#6B7280"
VerticalAlignment="Center"/>
<TextBlock Text="清空"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center"/>
</StackPanel>
</Button.Content>
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#F3F4F6"/>
<Setter Property="Foreground" Value="#1F2937"/>
</Style>
</Button.Styles>
</Button>
</Grid>
</Border>
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
VerticalAlignment="Stretch">
<ItemsControl ItemsSource="{Binding ReceivedMessages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#F0FDF4"
Padding="14,10"
Margin="0,0,0,1"
BorderBrush="#D1FAE5"
BorderThickness="0,0,0,1">
<TextBlock Text="{Binding}"
TextWrapping="Wrap"
FontSize="12"
Foreground="#1F2937"
LineHeight="18"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Border>
</Grid>
</Grid>
</Grid>
</reactive:ReactiveUserControl>

69
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<UdpServerPageViewModel>
{
private Button? _toggleButton;
private HeroIcon? _toggleIcon;
private Expander? _clientListExpander;
public UdpServerPageView()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
// 查找控件
_toggleButton = this.FindControl<Button>("ToggleButton");
_toggleIcon = this.FindControl<HeroIcon>("ToggleIcon");
_clientListExpander = this.FindControl<Expander>("ClientListExpander");
if (_toggleButton != null && _clientListExpander != null)
{
// 绑定点击事件
_toggleButton.Click += (s, e) =>
{
if (_clientListExpander != null)
{
_clientListExpander.IsExpanded = !_clientListExpander.IsExpanded;
UpdateToggleIcon();
}
};
// 监听 Expander 状态变化
_clientListExpander.PropertyChanged += (s, e) =>
{
if (e.Property.Name == nameof(Expander.IsExpanded))
{
UpdateToggleIcon();
}
};
// 初始化图标状态
UpdateToggleIcon();
}
});
}
private void UpdateToggleIcon()
{
if (_toggleIcon != null && _clientListExpander != null)
{
_toggleIcon.Type = _clientListExpander.IsExpanded
? IconType.ChevronDown
: IconType.ChevronRight;
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

160
AuroraDesk/modify.md

@ -5892,3 +5892,163 @@ var title = _resourceService?.GetString("NavDashboard") ?? "仪表板";
- ✅ **滚动触发**: 一旦有足够内容,用户滚动时会自动触发后续的加载更多
- ✅ **性能优化**: 防抖机制确保不会频繁触发检查和加载,性能稳定
### 实现节点编辑器组件 - 支持拖拽和连线功能
- **日期**: 2025年1月
- **修改内容**: 实现节点编辑器组件,支持拖拽节点到画布、节点间连线功能,遵循 ReactiveUI.Avalonia 和整洁架构
- **修改文件**:
- `AuroraDesk.Core/Entities/Node.cs` - 新建节点实体类
- `AuroraDesk.Core/Entities/ConnectionPoint.cs` - 新建连接点实体类
- `AuroraDesk.Core/Entities/Connection.cs` - 新建连接实体类
- `AuroraDesk.Core/Interfaces/INodeCanvasService.cs` - 新建节点画布服务接口
- `AuroraDesk.Infrastructure/Services/NodeCanvasService.cs` - 实现节点画布服务
- `AuroraDesk.Presentation/ViewModels/Pages/NodeCanvasPageViewModel.cs` - 新建节点画布页面 ViewModel
- `AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml` - 新建节点画布页面 View
- `AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs` - 节点画布页面代码后台
- `AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs` - 新建节点画布相关转换器
- `AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs` - 注册节点画布服务
- `AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs` - 注册 NodeCanvasPageViewModel
- **主要功能**:
- ✅ **节点创建**: 点击画布空白区域创建新节点
- ✅ **节点拖拽**: 支持拖拽节点移动位置
- ✅ **连接点**: 节点左侧为输入点,右侧为输出点(支持多个连接点)
- ✅ **连线功能**: 点击输出点开始连线,点击输入点完成连线
- ✅ **连接线绘制**: 使用贝塞尔曲线绘制连接线,支持动态更新
- ✅ **属性面板**: 右侧显示选中节点的属性信息
- ✅ **工具栏**: 左侧提供操作说明和清空画布功能
- **架构设计**:
- ✅ **Core层**: 实体类(Node, ConnectionPoint, Connection)和接口(INodeCanvasService)
- ✅ **Infrastructure层**: NodeCanvasService 服务实现
- ✅ **Presentation层**: ViewModel 和 View,遵循 ReactiveUI 模式
- ✅ **依赖注入**: 服务通过 DI 注册,ViewModel 通过工厂创建
- **技术实现**:
- 使用 `ObservableCollection` 管理节点和连接集合
- 使用 `ReactiveCommand` 处理用户操作
- 使用 `WhenAnyValue` 监听集合变化,动态更新连接线
- 使用 Canvas 和 Path 实现节点和连接线的绘制
- 使用转换器(Converter)处理 XAML 绑定
- **效果**:
- ✅ **功能完整**: 实现了节点编辑器的核心功能(创建、拖拽、连线)
- ✅ **架构清晰**: 遵循整洁架构原则,层次分明
- ✅ **响应式**: 使用 ReactiveUI 实现响应式编程模式
- ✅ **可扩展**: 易于添加新功能(如节点类型、连线验证等)
### 添加节点编辑器到导航菜单
- **日期**: 2025年1月
- **修改内容**: 将节点编辑器页面添加到导航菜单,使其可以通过导航访问
- **修改文件**:
- `AuroraDesk.Infrastructure/Services/NavigationService.cs` - 添加节点编辑器导航项
- `AuroraDesk.Presentation/Services/PageViewModelFactory.cs` - 添加节点编辑器 ViewModel 创建方法
- **主要修改**:
- ✅ **导航项**: 在 NavigationService 中添加了 "节点编辑器" 导航项,使用 `IconType.SquaresPlus` 图标
- ✅ **路由支持**: 在 PageViewModelFactory 中添加了 "node-canvas" 路由支持
- ✅ **ViewModel 创建**: 添加了 `CreateNodeCanvasPageViewModel` 方法,正确注入依赖(INodeCanvasService 和 ILogger)
- **效果**:
- ✅ **导航可见**: 节点编辑器现在出现在左侧导航菜单中
- ✅ **路由支持**: 可以通过导航菜单点击访问节点编辑器页面
- ✅ **依赖注入**: ViewModel 正确创建,所有依赖服务正确注入
### 美化节点编辑器UI并实现组件库拖拽功能
- **日期**: 2025年1月
- **修改内容**: 美化节点编辑器界面,创建节点组件库,实现从组件库拖拽节点到画布的功能
- **修改文件**:
- `AuroraDesk.Core/Entities/NodeTemplate.cs` - 新建节点模板实体类
- `AuroraDesk.Presentation/ViewModels/Pages/NodeCanvasPageViewModel.cs` - 添加节点模板集合和拖拽命令
- `AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml` - 完全重构UI,美化界面并添加组件库
- `AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs` - 实现拖拽功能
- **UI美化**:
- ✅ **深色主题**: 使用现代化的深色主题(#1E293B, #0F172A等
- ✅ **左侧组件库**: 显示可用的节点模板,包含预览图
- ✅ **节点样式**: 改进节点外观,使用圆角、阴影等现代化设计
- ✅ **连接点样式**: 优化连接点外观,输入点蓝色,输出点红色
- ✅ **属性面板**: 美化右侧属性面板,使用更清晰的布局
- **组件库功能**:
- ✅ **节点模板**: 创建 NodeTemplate 实体,支持定义节点类型(功分器、基础节点等)
- ✅ **模板预览**: 组件库中显示节点模板的预览图
- ✅ **拖拽支持**: 支持从组件库拖拽节点模板到画布
- ✅ **自动创建**: 拖拽到画布后自动根据模板创建节点实例
- **主要改进**:
- ✅ **视觉设计**: 使用现代化的配色方案和布局
- ✅ **组件库**: 左侧显示可用的节点组件,每个组件显示预览
- ✅ **拖拽交互**: 从组件库拖拽节点到画布,更直观的操作方式
- ✅ **节点样式**: 节点使用白色背景、圆角、阴影,更美观
- ✅ **连接点**: 连接点使用不同颜色区分输入/输出,并显示标签
- **技术实现**:
- 使用 `NodeTemplate` 定义节点类型
- 使用 `DragDrop` API 实现拖拽功能
- 监听模板集合变化,动态设置拖拽事件
- 使用 `GetVisualDescendants` 查找模板项
- **效果**:
- ✅ **界面美观**: UI 更加现代化和专业
- ✅ **操作直观**: 从组件库拖拽节点,更符合用户习惯
- ✅ **功能完整**: 支持多种节点类型,易于扩展
- ✅ **用户体验**: 拖拽操作流畅,视觉反馈清晰
### 统一节点编辑器页面样式与其他页面保持一致
- **日期**: 2025年1月
- **修改内容**: 将节点编辑器页面的UI风格从深色主题改为与其他页面一致的浅色主题,使用统一的颜色资源
- **修改文件**:
- `AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml` - 更新所有颜色为 StaticResource 引用
- `AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs` - 优化颜色转换器
- **样式统一**:
- ✅ **背景色**: 从深色(#0F172A, #1E293B)改为浅色(BackgroundWhite, BackgroundLight)
- ✅ **文本颜色**: 使用统一的文本颜色资源(TextPrimary, TextSecondary)
- ✅ **边框颜色**: 使用统一的边框颜色资源(BorderLight)
- ✅ **按钮颜色**: 使用统一的按钮颜色资源(StatusError, PrimaryBlue等)
- ✅ **组件库**: 左侧组件库使用浅色背景,与整体风格一致
- ✅ **画布区域**: 画布背景改为白色,与其他页面内容区域一致
- ✅ **属性面板**: 右侧属性面板使用白色背景,布局与其他页面一致
- **颜色资源**:
- 使用 `{StaticResource BackgroundWhite}` 替代硬编码的深色背景
- 使用 `{StaticResource BackgroundLight}` 替代硬编码的深色边框区域
- 使用 `{StaticResource TextPrimary}` 替代硬编码的白色文本
- 使用 `{StaticResource TextSecondary}` 替代硬编码的灰色文本
- 使用 `{StaticResource BorderLight}` 替代硬编码的边框颜色
- 使用 `{StaticResource StatusError}` 替代硬编码的红色按钮
- 使用 `{StaticResource PrimaryBlue}` 替代硬编码的蓝色连接点
- **主要改进**:
- ✅ **视觉一致性**: 节点编辑器页面现在与其他页面使用相同的颜色主题
- ✅ **资源统一**: 所有颜色都使用 StaticResource,便于统一管理
- ✅ **用户体验**: 统一的视觉风格,减少用户在不同页面间的视觉跳跃
- ✅ **可维护性**: 通过颜色资源统一管理,便于后续主题切换
- **技术细节**:
- 将所有硬编码的颜色值替换为 StaticResource 引用
- 节点、连接点、组件库等所有UI元素都使用统一的颜色资源
- 保持功能不变,只更新视觉样式
- **效果**:
- ✅ **风格统一**: 节点编辑器页面现在与 Dashboard、Users、Editor 等页面使用相同的浅色主题
- ✅ **视觉协调**: 整个应用的视觉风格更加统一和协调
- ✅ **易于维护**: 通过颜色资源统一管理,便于后续修改和维护
### 修复节点编辑器拖拽功能和添加网格背景
- **日期**: 2025年1月
- **修改内容**: 修复左侧组件无法拖拽到右边画布的问题,并为画布添加网格背景
- **修改文件**:
- `AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml` - 添加 ScrollViewer 名称,实现网格背景
- `AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs` - 修复拖拽事件处理,添加 ScrollViewer 拖拽支持
- **问题修复**:
- ✅ **拖拽功能**: 将 DragDrop 事件同时附加到 Canvas 和 ScrollViewer,确保从左侧组件库拖拽的组件可以正确放置到画布上
- ✅ **事件处理**: 在 ScrollViewer 上添加 DragOver 和 Drop 事件处理,解决 Canvas 被 ScrollViewer 包裹导致拖拽事件无法接收的问题
- ✅ **拖拽光标**: 在拖拽时显示拖拽光标,提供更好的视觉反馈
- **网格背景**:
- ✅ **网格图案**: 使用独立的 Canvas 层和 Line 元素创建 20x20 像素的网格背景
- ✅ **网格线**: 使用浅灰色(#E0E0E0)绘制网格线,线宽 0.5
- ✅ **动态绘制**: 根据 Canvas 尺寸动态计算并绘制网格线,支持画布尺寸变化
- ✅ **画布尺寸**: 设置 Canvas 最小尺寸为 2000x2000,确保有足够的画布空间
- **技术实现**:
- 在 ScrollViewer 上添加 `DragDrop.DragOverEvent``DragDrop.DropEvent` 事件处理器
- 使用独立的 `GridBackgroundLayer` Canvas 层绘制网格,使用 `Line` 元素创建网格线
- 在代码后台的 `DrawGridBackground()` 方法中动态绘制网格,根据 Canvas 尺寸计算网格线位置
- 网格层设置 `IsHitTestVisible="False"` 避免影响鼠标事件
- 保持 Canvas 上的原有事件处理,确保节点移动等功能正常
- **效果**:
- ✅ **拖拽可用**: 现在可以从左侧组件库成功拖拽组件到右边画布
- ✅ **网格显示**: 画布显示清晰的网格背景,方便对齐节点
- ✅ **用户体验**: 拖拽操作流畅,网格背景提供更好的视觉参考
- **修复记录**:
- ✅ **修复网格背景**: Avalonia 的 `DrawingBrush` 不支持 `Viewport``ViewportUnits` 属性,改用代码后台使用 `Line` 元素动态绘制网格
- ✅ **网格层**: 创建独立的 `GridBackgroundLayer` Canvas 层,设置 `IsHitTestVisible="False"` 避免影响交互
- ✅ **动态更新**: 监听 Canvas 尺寸变化,自动更新网格线
- ✅ **修复ItemsControl**: 为 ItemsControl 的 Canvas 设置 `ClipToBounds="False"`,确保节点可以显示在可见区域外
- ✅ **位置验证**: 添加位置验证,确保节点坐标至少为0,避免负坐标导致的问题
- ✅ **调试信息**: 添加调试日志,帮助诊断节点添加和显示问题

4299
modify.md

File diff suppressed because it is too large
Loading…
Cancel
Save