26 changed files with 4270 additions and 4146 deletions
@ -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; // 不能连接到同一个节点
|
|||
} |
|||
} |
|||
|
|||
@ -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 |
|||
} |
|||
|
|||
@ -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); |
|||
} |
|||
} |
|||
|
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
@ -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(); |
|||
} |
|||
|
|||
@ -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("清空画布"); |
|||
} |
|||
} |
|||
|
|||
@ -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(); |
|||
} |
|||
} |
|||
|
|||
@ -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); |
|||
} |
|||
} |
|||
|
|||
@ -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(); |
|||
} |
|||
} |
|||
|
|||
@ -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}"; |
|||
} |
|||
|
|||
@ -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="提示:
从组件库拖拽节点到画布
或点击画布空白处添加节点" |
|||
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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
|
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
|
|||
File diff suppressed because it is too large
Loading…
Reference in new issue