diff --git a/AuroraDesk.Application/AuroraDesk.Application.csproj b/AuroraDesk.Application/AuroraDesk.Application.csproj index d53635e..dfe0c7a 100644 --- a/AuroraDesk.Application/AuroraDesk.Application.csproj +++ b/AuroraDesk.Application/AuroraDesk.Application.csproj @@ -11,7 +11,6 @@ - diff --git a/AuroraDesk.Core/AuroraDesk.Core.csproj b/AuroraDesk.Core/AuroraDesk.Core.csproj index bed6a0b..a55fc06 100644 --- a/AuroraDesk.Core/AuroraDesk.Core.csproj +++ b/AuroraDesk.Core/AuroraDesk.Core.csproj @@ -7,8 +7,8 @@ - + diff --git a/AuroraDesk.Core/Entities/Connection.cs b/AuroraDesk.Core/Entities/Connection.cs new file mode 100644 index 0000000..bbe8ae3 --- /dev/null +++ b/AuroraDesk.Core/Entities/Connection.cs @@ -0,0 +1,50 @@ +using ReactiveUI; +using System; + +namespace AuroraDesk.Core.Entities; + +/// +/// 连接实体 - 表示两个连接点之间的连线 +/// +public class Connection : ReactiveObject +{ + private ConnectionPoint? _sourcePoint; + private ConnectionPoint? _targetPoint; + + /// + /// 连接唯一标识符 + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// 源连接点(输出点) + /// + public ConnectionPoint? SourcePoint + { + get => _sourcePoint; + set => this.RaiseAndSetIfChanged(ref _sourcePoint, value); + } + + /// + /// 目标连接点(输入点) + /// + public ConnectionPoint? TargetPoint + { + get => _targetPoint; + set => this.RaiseAndSetIfChanged(ref _targetPoint, value); + } + + /// + /// 验证连接是否有效(输出点必须连接到输入点) + /// + public bool IsValid() + { + if (SourcePoint == null || TargetPoint == null) + return false; + + return SourcePoint.Type == ConnectionPointType.Output && + TargetPoint.Type == ConnectionPointType.Input && + SourcePoint.Node != TargetPoint.Node; // 不能连接到同一个节点 + } +} + diff --git a/AuroraDesk.Core/Entities/ConnectionPoint.cs b/AuroraDesk.Core/Entities/ConnectionPoint.cs new file mode 100644 index 0000000..07723b9 --- /dev/null +++ b/AuroraDesk.Core/Entities/ConnectionPoint.cs @@ -0,0 +1,78 @@ +using ReactiveUI; +using System; + +namespace AuroraDesk.Core.Entities; + +/// +/// 连接点实体 - 节点的输入/输出连接点 +/// +public class ConnectionPoint : ReactiveObject +{ + private string _label = string.Empty; + private ConnectionPointType _type = ConnectionPointType.Output; + private int _index; + private bool _isConnected; + + /// + /// 连接点唯一标识符 + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// 连接点标签(显示在连接点旁边的文本) + /// + public string Label + { + get => _label; + set => this.RaiseAndSetIfChanged(ref _label, value); + } + + /// + /// 连接点类型(输入或输出) + /// + public ConnectionPointType Type + { + get => _type; + set => this.RaiseAndSetIfChanged(ref _type, value); + } + + /// + /// 连接点索引(在节点上的位置顺序) + /// + public int Index + { + get => _index; + set => this.RaiseAndSetIfChanged(ref _index, value); + } + + /// + /// 是否已连接 + /// + public bool IsConnected + { + get => _isConnected; + set => this.RaiseAndSetIfChanged(ref _isConnected, value); + } + + /// + /// 所属节点 + /// + public Node? Node { get; set; } +} + +/// +/// 连接点类型枚举 +/// +public enum ConnectionPointType +{ + /// + /// 输入连接点(在左侧) + /// + Input, + + /// + /// 输出连接点(在右侧) + /// + Output +} + diff --git a/AuroraDesk.Core/Entities/Node.cs b/AuroraDesk.Core/Entities/Node.cs new file mode 100644 index 0000000..1d31003 --- /dev/null +++ b/AuroraDesk.Core/Entities/Node.cs @@ -0,0 +1,98 @@ +using ReactiveUI; +using System; +using System.Collections.ObjectModel; + +namespace AuroraDesk.Core.Entities; + +/// +/// 节点实体 - 表示画布上的一个可拖拽组件 +/// +public class Node : ReactiveObject +{ + private double _x; + private double _y; + private double _width = 120; + private double _height = 80; + private string _title = string.Empty; + private string _content = string.Empty; + private bool _isSelected; + private ObservableCollection _connectionPoints = new(); + + /// + /// 节点唯一标识符 + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// X坐标位置 + /// + public double X + { + get => _x; + set => this.RaiseAndSetIfChanged(ref _x, value); + } + + /// + /// Y坐标位置 + /// + public double Y + { + get => _y; + set => this.RaiseAndSetIfChanged(ref _y, value); + } + + /// + /// 节点宽度 + /// + public double Width + { + get => _width; + set => this.RaiseAndSetIfChanged(ref _width, value); + } + + /// + /// 节点高度 + /// + public double Height + { + get => _height; + set => this.RaiseAndSetIfChanged(ref _height, value); + } + + /// + /// 节点标题 + /// + public string Title + { + get => _title; + set => this.RaiseAndSetIfChanged(ref _title, value); + } + + /// + /// 节点内容(显示在节点内部的文本) + /// + public string Content + { + get => _content; + set => this.RaiseAndSetIfChanged(ref _content, value); + } + + /// + /// 是否被选中 + /// + public bool IsSelected + { + get => _isSelected; + set => this.RaiseAndSetIfChanged(ref _isSelected, value); + } + + /// + /// 连接点集合 + /// + public ObservableCollection ConnectionPoints + { + get => _connectionPoints; + set => this.RaiseAndSetIfChanged(ref _connectionPoints, value); + } +} + diff --git a/AuroraDesk.Core/Entities/NodeTemplate.cs b/AuroraDesk.Core/Entities/NodeTemplate.cs new file mode 100644 index 0000000..fc582bd --- /dev/null +++ b/AuroraDesk.Core/Entities/NodeTemplate.cs @@ -0,0 +1,139 @@ +using ReactiveUI; +using System; + +namespace AuroraDesk.Core.Entities; + +/// +/// 节点模板 - 定义可用的节点类型 +/// +public class NodeTemplate : ReactiveObject +{ + private string _name = string.Empty; + private string _displayName = string.Empty; + private string _description = string.Empty; + private string _content = string.Empty; + private int _inputCount = 1; + private int _outputCount = 3; + private double _width = 120; + private double _height = 80; + + /// + /// 模板唯一标识符 + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// 模板名称(用于内部标识) + /// + public string Name + { + get => _name; + set => this.RaiseAndSetIfChanged(ref _name, value); + } + + /// + /// 显示名称 + /// + public string DisplayName + { + get => _displayName; + set => this.RaiseAndSetIfChanged(ref _displayName, value); + } + + /// + /// 描述 + /// + public string Description + { + get => _description; + set => this.RaiseAndSetIfChanged(ref _description, value); + } + + /// + /// 节点内容(显示在节点内部的文本) + /// + public string Content + { + get => _content; + set => this.RaiseAndSetIfChanged(ref _content, value); + } + + /// + /// 输入连接点数量 + /// + public int InputCount + { + get => _inputCount; + set => this.RaiseAndSetIfChanged(ref _inputCount, value); + } + + /// + /// 输出连接点数量 + /// + public int OutputCount + { + get => _outputCount; + set => this.RaiseAndSetIfChanged(ref _outputCount, value); + } + + /// + /// 节点宽度 + /// + public double Width + { + get => _width; + set => this.RaiseAndSetIfChanged(ref _width, value); + } + + /// + /// 节点高度 + /// + public double Height + { + get => _height; + set => this.RaiseAndSetIfChanged(ref _height, value); + } + + /// + /// 根据模板创建节点实例 + /// + public Node CreateNode(double x, double y) + { + var node = new Node + { + X = x, + Y = y, + Title = DisplayName, + Content = Content, + Width = Width, + Height = Height + }; + + // 创建输入连接点(左侧) + for (int i = 0; i < InputCount; i++) + { + node.ConnectionPoints.Add(new ConnectionPoint + { + Label = "", + Type = ConnectionPointType.Input, + Index = i, + Node = node + }); + } + + // 创建输出连接点(右侧) + for (int i = 0; i < OutputCount; i++) + { + node.ConnectionPoints.Add(new ConnectionPoint + { + Label = (i + 1).ToString(), + Type = ConnectionPointType.Output, + Index = i, + Node = node + }); + } + + return node; + } +} + diff --git a/AuroraDesk.Core/Interfaces/INodeCanvasService.cs b/AuroraDesk.Core/Interfaces/INodeCanvasService.cs new file mode 100644 index 0000000..10039ae --- /dev/null +++ b/AuroraDesk.Core/Interfaces/INodeCanvasService.cs @@ -0,0 +1,57 @@ +using AuroraDesk.Core.Entities; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace AuroraDesk.Core.Interfaces; + +/// +/// 节点画布服务接口 +/// +public interface INodeCanvasService +{ + /// + /// 节点集合 + /// + ObservableCollection Nodes { get; } + + /// + /// 连接集合 + /// + ObservableCollection Connections { get; } + + /// + /// 添加节点到画布 + /// + void AddNode(Node node); + + /// + /// 移除节点(同时移除相关连接) + /// + void RemoveNode(Node node); + + /// + /// 更新节点位置 + /// + void UpdateNodePosition(Node node, double x, double y); + + /// + /// 创建连接 + /// + bool CreateConnection(ConnectionPoint sourcePoint, ConnectionPoint targetPoint); + + /// + /// 移除连接 + /// + void RemoveConnection(Connection connection); + + /// + /// 获取节点的所有连接 + /// + IEnumerable GetNodeConnections(Node node); + + /// + /// 清除所有节点和连接 + /// + void Clear(); +} + diff --git a/AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj b/AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj index fda068f..822e1d6 100644 --- a/AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj +++ b/AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj @@ -15,8 +15,8 @@ - + diff --git a/AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs index 80d588b..8502098 100644 --- a/AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/AuroraDesk.Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -46,6 +46,9 @@ public static class ServiceCollectionExtensions // 注册标签页管理服务(重构新增,分离标签页管理职责) services.AddTransient(); + // 注册节点画布服务 + services.AddSingleton(); + return services; } } diff --git a/AuroraDesk.Infrastructure/Services/NavigationService.cs b/AuroraDesk.Infrastructure/Services/NavigationService.cs index 3f5c397..6ec6c08 100644 --- a/AuroraDesk.Infrastructure/Services/NavigationService.cs +++ b/AuroraDesk.Infrastructure/Services/NavigationService.cs @@ -140,6 +140,36 @@ public class NavigationService : INavigationService Title = "图片浏览", IconType = IconType.Photo, ViewModel = _pageViewModelFactory.CreatePageViewModel("image-gallery", screen) + }, + new NavigationItem + { + Id = "udp-tools", + Title = "UDP 工具", + IconType = IconType.Signal, + Children = new ObservableCollection + { + new NavigationItem + { + Id = "udp-client", + Title = "UDP 客户端", + IconType = IconType.ArrowRight, + ViewModel = _pageViewModelFactory.CreatePageViewModel("udp-client", screen) + }, + new NavigationItem + { + Id = "udp-server", + Title = "UDP 服务端", + IconType = IconType.Server, + ViewModel = _pageViewModelFactory.CreatePageViewModel("udp-server", screen) + } + } + }, + new NavigationItem + { + Id = "node-canvas", + Title = "节点编辑器", + IconType = IconType.SquaresPlus, + ViewModel = _pageViewModelFactory.CreatePageViewModel("node-canvas", screen) } }; diff --git a/AuroraDesk.Infrastructure/Services/NavigationStateService.cs b/AuroraDesk.Infrastructure/Services/NavigationStateService.cs index 9101a6c..bad2eae 100644 --- a/AuroraDesk.Infrastructure/Services/NavigationStateService.cs +++ b/AuroraDesk.Infrastructure/Services/NavigationStateService.cs @@ -1,9 +1,9 @@ using AuroraDesk.Core.Interfaces; using AuroraDesk.Core.Entities; using Microsoft.Extensions.Logging; -using ReactiveUI; using System.Collections.ObjectModel; using System.Collections.Generic; +using ReactiveUI; namespace AuroraDesk.Infrastructure.Services; diff --git a/AuroraDesk.Infrastructure/Services/NodeCanvasService.cs b/AuroraDesk.Infrastructure/Services/NodeCanvasService.cs new file mode 100644 index 0000000..7a310a0 --- /dev/null +++ b/AuroraDesk.Infrastructure/Services/NodeCanvasService.cs @@ -0,0 +1,157 @@ +using AuroraDesk.Core.Entities; +using AuroraDesk.Core.Interfaces; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; + +namespace AuroraDesk.Infrastructure.Services; + +/// +/// 节点画布服务实现 +/// +public class NodeCanvasService : INodeCanvasService +{ + private readonly ILogger? _logger; + private readonly ObservableCollection _nodes = new(); + private readonly ObservableCollection _connections = new(); + + public ObservableCollection Nodes => _nodes; + public ObservableCollection Connections => _connections; + + public NodeCanvasService(ILogger? logger = null) + { + _logger = logger; + } + + public void AddNode(Node node) + { + if (node == null) + throw new ArgumentNullException(nameof(node)); + + if (!_nodes.Contains(node)) + { + _nodes.Add(node); + _logger?.LogDebug("添加节点: {NodeId}, Title: {Title}", node.Id, node.Title); + } + } + + public void RemoveNode(Node node) + { + if (node == null) return; + + // 移除与节点相关的所有连接 + var connectionsToRemove = _connections + .Where(c => c.SourcePoint?.Node == node || c.TargetPoint?.Node == node) + .ToList(); + + foreach (var connection in connectionsToRemove) + { + RemoveConnection(connection); + } + + _nodes.Remove(node); + _logger?.LogDebug("移除节点: {NodeId}, Title: {Title}", node.Id, node.Title); + } + + public void UpdateNodePosition(Node node, double x, double y) + { + if (node == null) return; + + node.X = x; + node.Y = y; + _logger?.LogTrace("更新节点位置: {NodeId}, X: {X}, Y: {Y}", node.Id, x, y); + } + + public bool CreateConnection(ConnectionPoint sourcePoint, ConnectionPoint targetPoint) + { + if (sourcePoint == null || targetPoint == null) + return false; + + // 验证连接点类型 + if (sourcePoint.Type != ConnectionPointType.Output || + targetPoint.Type != ConnectionPointType.Input) + { + _logger?.LogWarning("连接点类型不匹配: Source={SourceType}, Target={TargetType}", + sourcePoint.Type, targetPoint.Type); + return false; + } + + // 不能连接到同一个节点 + if (sourcePoint.Node == targetPoint.Node) + { + _logger?.LogWarning("不能连接到同一个节点"); + return false; + } + + // 检查是否已经存在相同的连接 + var existingConnection = _connections.FirstOrDefault(c => + c.SourcePoint == sourcePoint && c.TargetPoint == targetPoint); + + if (existingConnection != null) + { + _logger?.LogWarning("连接已存在"); + return false; + } + + // 检查目标连接点是否已经被连接(可以支持一对多,这里先实现一对一) + var targetAlreadyConnected = _connections.Any(c => c.TargetPoint == targetPoint); + if (targetAlreadyConnected) + { + _logger?.LogWarning("目标连接点已被连接"); + return false; + } + + var connection = new Connection + { + SourcePoint = sourcePoint, + TargetPoint = targetPoint + }; + + if (connection.IsValid()) + { + _connections.Add(connection); + sourcePoint.IsConnected = true; + targetPoint.IsConnected = true; + _logger?.LogDebug("创建连接: {ConnectionId}, Source={SourceId}, Target={TargetId}", + connection.Id, sourcePoint.Id, targetPoint.Id); + return true; + } + + _logger?.LogWarning("连接验证失败"); + return false; + } + + public void RemoveConnection(Connection connection) + { + if (connection == null) return; + + if (_connections.Remove(connection)) + { + if (connection.SourcePoint != null) + connection.SourcePoint.IsConnected = false; + if (connection.TargetPoint != null) + connection.TargetPoint.IsConnected = false; + + _logger?.LogDebug("移除连接: {ConnectionId}", connection.Id); + } + } + + public IEnumerable GetNodeConnections(Node node) + { + if (node == null) + return Enumerable.Empty(); + + return _connections.Where(c => + c.SourcePoint?.Node == node || c.TargetPoint?.Node == node); + } + + public void Clear() + { + _connections.Clear(); + _nodes.Clear(); + _logger?.LogDebug("清空画布"); + } +} + diff --git a/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs b/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs new file mode 100644 index 0000000..c0ea34a --- /dev/null +++ b/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs @@ -0,0 +1,149 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia; +using AuroraDesk.Core.Entities; +using System; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; + +namespace AuroraDesk.Presentation.Converters; + +/// +/// 节点画布相关转换器 +/// +public static class NodeCanvasConverters +{ + public static readonly IValueConverter BooleanToBorderBrushConverter = new BooleanToBorderBrushConverter(); + public static readonly IValueConverter BooleanToBorderThicknessConverter = new BooleanToBorderThicknessConverter(); + public static readonly IValueConverter FilterInputPointsConverter = new FilterInputPointsConverter(); + public static readonly IValueConverter FilterOutputPointsConverter = new FilterOutputPointsConverter(); + public static readonly IValueConverter NodeToSelectionTextConverter = new NodeToSelectionTextConverter(); + public static readonly IValueConverter IsNotNullConverter = new IsNotNullConverter(); +} + +/// +/// 布尔值到边框颜色转换器(选中时显示蓝色边框) +/// +public class BooleanToBorderBrushConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is bool isSelected && isSelected) + { + // 使用 StaticResource 的颜色,如果无法获取则使用默认蓝色 + try + { + var color = Color.Parse("#3498DB"); // PrimaryBlue + return new SolidColorBrush(color); + } + catch + { + return new SolidColorBrush(Color.FromRgb(52, 152, 219)); // #3498DB + } + } + return new SolidColorBrush(Colors.Transparent); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +/// +/// 布尔值到边框厚度转换器(选中时显示边框) +/// +public class BooleanToBorderThicknessConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is bool isSelected && isSelected) + { + return new Thickness(2); + } + return new Thickness(0); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +/// +/// 过滤输入连接点转换器 +/// +public class FilterInputPointsConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is ObservableCollection points) + { + return points.Where(p => p.Type == ConnectionPointType.Input).ToList(); + } + return Enumerable.Empty(); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +/// +/// 过滤输出连接点转换器 +/// +public class FilterOutputPointsConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is ObservableCollection points) + { + return points.Where(p => p.Type == ConnectionPointType.Output).ToList(); + } + return Enumerable.Empty(); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +/// +/// 节点到选择文本转换器 +/// +public class NodeToSelectionTextConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Node node) + { + return $"已选中1个对象 (节点)"; + } + return "未选中对象"; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +/// +/// 非空转换器(用于控制可见性) +/// +public class IsNotNullConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value != null; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + diff --git a/AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs b/AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs index be27e43..29934b0 100644 --- a/AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs +++ b/AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs @@ -45,6 +45,9 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); // 注意:MainWindowViewModel 的注册移到主项目的 App.axaml.cs 中 // 因为它依赖 AppViewModel,而 AppViewModel 在 AddReactiveUI() 中注册 diff --git a/AuroraDesk.Presentation/Services/PageViewModelFactory.cs b/AuroraDesk.Presentation/Services/PageViewModelFactory.cs index aa49971..4f001a2 100644 --- a/AuroraDesk.Presentation/Services/PageViewModelFactory.cs +++ b/AuroraDesk.Presentation/Services/PageViewModelFactory.cs @@ -55,6 +55,9 @@ public class PageViewModelFactory : IPageViewModelFactory "icons" => CreateIconsPageViewModel(screen), "editor" => CreateEditorPageViewModel(screen), "image-gallery" => CreateImageGalleryPageViewModel(screen), + "udp-client" => CreateUdpClientPageViewModel(screen), + "udp-server" => CreateUdpServerPageViewModel(screen), + "node-canvas" => CreateNodeCanvasPageViewModel(screen), _ => throw new ArgumentException($"Unknown page: {pageId}", nameof(pageId)) }; } @@ -76,5 +79,24 @@ public class PageViewModelFactory : IPageViewModelFactory var logger = _serviceProvider.GetService>(); return ActivatorUtilities.CreateInstance(_serviceProvider, screen, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); } + + private UdpClientPageViewModel CreateUdpClientPageViewModel(IScreen screen) + { + var logger = _serviceProvider.GetService>(); + return ActivatorUtilities.CreateInstance(_serviceProvider, screen, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + } + + private UdpServerPageViewModel CreateUdpServerPageViewModel(IScreen screen) + { + var logger = _serviceProvider.GetService>(); + return ActivatorUtilities.CreateInstance(_serviceProvider, screen, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + } + + private NodeCanvasPageViewModel CreateNodeCanvasPageViewModel(IScreen screen) + { + var nodeCanvasService = _serviceProvider.GetRequiredService(); + var logger = _serviceProvider.GetService>(); + return ActivatorUtilities.CreateInstance(_serviceProvider, screen, nodeCanvasService, logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + } } diff --git a/AuroraDesk.Presentation/ViewModels/Pages/NodeCanvasPageViewModel.cs b/AuroraDesk.Presentation/ViewModels/Pages/NodeCanvasPageViewModel.cs new file mode 100644 index 0000000..7de00e8 --- /dev/null +++ b/AuroraDesk.Presentation/ViewModels/Pages/NodeCanvasPageViewModel.cs @@ -0,0 +1,319 @@ +using AuroraDesk.Core.Entities; +using AuroraDesk.Core.Interfaces; +using AuroraDesk.Presentation.ViewModels.Base; +using ReactiveUI; +using System; +using System.Collections.ObjectModel; +using System.Reactive; +using System.Reactive.Linq; +using Microsoft.Extensions.Logging; + +namespace AuroraDesk.Presentation.ViewModels.Pages; + +/// +/// 节点画布页面 ViewModel +/// +public class NodeCanvasPageViewModel : RoutableViewModel +{ + private readonly INodeCanvasService _nodeCanvasService; + private readonly ILogger? _logger; + private Node? _selectedNode; + private ConnectionPoint? _connectingSourcePoint; + private bool _isConnecting; + private double _canvasOffsetX; + private double _canvasOffsetY; + private double _canvasZoom = 1.0; + + public ObservableCollection Nodes => _nodeCanvasService.Nodes; + public ObservableCollection Connections => _nodeCanvasService.Connections; + + private ObservableCollection _nodeTemplates = new(); + public ObservableCollection NodeTemplates + { + get => _nodeTemplates; + set => this.RaiseAndSetIfChanged(ref _nodeTemplates, value); + } + + /// + /// 选中的节点 + /// + public Node? SelectedNode + { + get => _selectedNode; + set => this.RaiseAndSetIfChanged(ref _selectedNode, value); + } + + /// + /// 正在连接的源连接点 + /// + public ConnectionPoint? ConnectingSourcePoint + { + get => _connectingSourcePoint; + set => this.RaiseAndSetIfChanged(ref _connectingSourcePoint, value); + } + + /// + /// 是否正在连接模式 + /// + public bool IsConnecting + { + get => _isConnecting; + set => this.RaiseAndSetIfChanged(ref _isConnecting, value); + } + + /// + /// 画布偏移X + /// + public double CanvasOffsetX + { + get => _canvasOffsetX; + set => this.RaiseAndSetIfChanged(ref _canvasOffsetX, value); + } + + /// + /// 画布偏移Y + /// + public double CanvasOffsetY + { + get => _canvasOffsetY; + set => this.RaiseAndSetIfChanged(ref _canvasOffsetY, value); + } + + /// + /// 画布缩放 + /// + public double CanvasZoom + { + get => _canvasZoom; + set => this.RaiseAndSetIfChanged(ref _canvasZoom, value); + } + + // 命令 + public ReactiveCommand ClearCanvasCommand { get; } + public ReactiveCommand DeleteNodeCommand { get; } + public ReactiveCommand StartConnectionCommand { get; } + public ReactiveCommand CompleteConnectionCommand { get; } + public ReactiveCommand DeleteConnectionCommand { get; } + public ReactiveCommand<(double x, double y), Unit> AddNodeCommand { get; } + public ReactiveCommand<(NodeTemplate template, double x, double y), Unit> AddNodeFromTemplateCommand { get; } + + public NodeCanvasPageViewModel( + IScreen screen, + INodeCanvasService nodeCanvasService, + ILogger? logger = null) : base(screen, "node-canvas") + { + _nodeCanvasService = nodeCanvasService; + _logger = logger; + + // 初始化节点模板 + InitializeNodeTemplates(); + + // 初始化命令 + ClearCanvasCommand = ReactiveCommand.Create(ClearCanvas); + DeleteNodeCommand = ReactiveCommand.Create(DeleteNode); + StartConnectionCommand = ReactiveCommand.Create(StartConnection); + CompleteConnectionCommand = ReactiveCommand.Create(CompleteConnection); + DeleteConnectionCommand = ReactiveCommand.Create(DeleteConnection); + AddNodeCommand = ReactiveCommand.Create<(double x, double y)>(AddNode); + AddNodeFromTemplateCommand = ReactiveCommand.Create<(NodeTemplate template, double x, double y)>(AddNodeFromTemplate); + + // 监听选中节点变化 + this.WhenAnyValue(x => x.SelectedNode) + .Subscribe(node => + { + // 取消其他节点的选中状态 + foreach (var n in Nodes) + { + n.IsSelected = n == node; + } + }); + + _logger?.LogInformation("NodeCanvasPageViewModel 已创建"); + } + + /// + /// 初始化节点模板 + /// + private void InitializeNodeTemplates() + { + NodeTemplates.Add(new NodeTemplate + { + Name = "power-splitter", + DisplayName = "功分器", + Description = "3功分器", + Content = "3功分42", + InputCount = 1, + OutputCount = 3, + Width = 120, + Height = 80 + }); + + NodeTemplates.Add(new NodeTemplate + { + Name = "basic-node", + DisplayName = "基础节点", + Description = "基础节点模板", + Content = "节点", + InputCount = 1, + OutputCount = 1, + Width = 100, + Height = 60 + }); + } + + /// + /// 添加节点到画布 + /// + private void AddNode((double x, double y) position) + { + // 确保位置在合理范围内(至少为正数) + var x = Math.Max(0, position.x); + var y = Math.Max(0, position.y); + + var node = new Node + { + X = x, + Y = y, + Title = $"节点 {Nodes.Count + 1}", + Content = "3功分42", + Width = 120, + Height = 80 + }; + + // 添加连接点(右侧输出点) + for (int i = 0; i < 3; i++) + { + node.ConnectionPoints.Add(new ConnectionPoint + { + Label = (i + 1).ToString(), + Type = ConnectionPointType.Output, + Index = i, + Node = node + }); + } + + // 添加连接点(左侧输入点) + node.ConnectionPoints.Add(new ConnectionPoint + { + Label = "", + Type = ConnectionPointType.Input, + Index = 0, + Node = node + }); + + _nodeCanvasService.AddNode(node); + SelectedNode = node; + _logger?.LogDebug("添加节点: {NodeId} 在位置 ({X}, {Y})", node.Id, position.x, position.y); + } + + /// + /// 删除节点 + /// + private void DeleteNode(Node node) + { + if (node == null) return; + + _nodeCanvasService.RemoveNode(node); + if (SelectedNode == node) + { + SelectedNode = null; + } + _logger?.LogDebug("删除节点: {NodeId}", node.Id); + } + + /// + /// 开始连接 + /// + private void StartConnection(ConnectionPoint connectionPoint) + { + if (connectionPoint == null || connectionPoint.Type != ConnectionPointType.Output) + return; + + ConnectingSourcePoint = connectionPoint; + IsConnecting = true; + _logger?.LogDebug("开始连接,源连接点: {PointId}", connectionPoint.Id); + } + + /// + /// 完成连接 + /// + private void CompleteConnection(ConnectionPoint targetPoint) + { + if (targetPoint == null || ConnectingSourcePoint == null) + { + CancelConnection(); + return; + } + + if (targetPoint.Type != ConnectionPointType.Input) + { + CancelConnection(); + return; + } + + var success = _nodeCanvasService.CreateConnection(ConnectingSourcePoint, targetPoint); + if (success) + { + _logger?.LogDebug("连接成功: {SourceId} -> {TargetId}", + ConnectingSourcePoint.Id, targetPoint.Id); + } + else + { + _logger?.LogWarning("连接失败: {SourceId} -> {TargetId}", + ConnectingSourcePoint.Id, targetPoint.Id); + } + + CancelConnection(); + } + + /// + /// 取消连接 + /// + public void CancelConnection() + { + ConnectingSourcePoint = null; + IsConnecting = false; + } + + /// + /// 删除连接 + /// + private void DeleteConnection(Connection connection) + { + if (connection == null) return; + + _nodeCanvasService.RemoveConnection(connection); + _logger?.LogDebug("删除连接: {ConnectionId}", connection.Id); + } + + /// + /// 清空画布 + /// + private void ClearCanvas() + { + _nodeCanvasService.Clear(); + SelectedNode = null; + _logger?.LogDebug("清空画布"); + } + + /// + /// 更新节点位置 + /// + public void UpdateNodePosition(Node node, double x, double y) + { + _nodeCanvasService.UpdateNodePosition(node, x, y); + } + + /// + /// 从模板添加节点到画布 + /// + private void AddNodeFromTemplate((NodeTemplate template, double x, double y) args) + { + var node = args.template.CreateNode(args.x, args.y); + _nodeCanvasService.AddNode(node); + SelectedNode = node; + _logger?.LogDebug("从模板添加节点: {TemplateName} 在位置 ({X}, {Y})", + args.template.Name, args.x, args.y); + } +} + diff --git a/AuroraDesk.Presentation/ViewModels/Pages/UdpClientPageViewModel.cs b/AuroraDesk.Presentation/ViewModels/Pages/UdpClientPageViewModel.cs new file mode 100644 index 0000000..2ee9f6a --- /dev/null +++ b/AuroraDesk.Presentation/ViewModels/Pages/UdpClientPageViewModel.cs @@ -0,0 +1,304 @@ +using AuroraDesk.Presentation.ViewModels.Base; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using System; +using System.Collections.ObjectModel; +using System.Net; +using System.Net.Sockets; +using System.Reactive; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; + +namespace AuroraDesk.Presentation.ViewModels.Pages; + +/// +/// UDP 客户端页面 ViewModel +/// +public class UdpClientPageViewModel : RoutableViewModel +{ + private readonly ILogger? _logger; + private UdpClient? _udpClient; + private CancellationTokenSource? _receiveCancellationTokenSource; + private bool _isConnected; + private string _serverIp = "127.0.0.1"; + private int _serverPort = 8080; + private int _localPort = 0; // 0 表示系统自动分配 + private string _message = string.Empty; + private string _statusMessage = "未连接"; + private readonly ObservableCollection _receivedMessages = new(); + private readonly ObservableCollection _sentMessages = new(); + + public UdpClientPageViewModel( + IScreen hostScreen, + ILogger? logger = null) + : base(hostScreen, "UdpClient") + { + _logger = logger; + + // 创建命令 + ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync, + this.WhenAnyValue(x => x.IsConnected, isConnected => !isConnected)); + DisconnectCommand = ReactiveCommand.Create(Disconnect, + this.WhenAnyValue(x => x.IsConnected)); + SendMessageCommand = ReactiveCommand.CreateFromTask(SendMessageAsync, + this.WhenAnyValue(x => x.IsConnected, x => x.Message, + (isConnected, message) => isConnected && !string.IsNullOrWhiteSpace(message))); + ClearMessagesCommand = ReactiveCommand.Create(ClearMessages); + } + + /// + /// 是否已连接 + /// + public bool IsConnected + { + get => _isConnected; + set => this.RaiseAndSetIfChanged(ref _isConnected, value); + } + + /// + /// 服务器 IP 地址 + /// + public string ServerIp + { + get => _serverIp; + set => this.RaiseAndSetIfChanged(ref _serverIp, value); + } + + /// + /// 服务器端口 + /// + public int ServerPort + { + get => _serverPort; + set => this.RaiseAndSetIfChanged(ref _serverPort, value); + } + + /// + /// 本地端口 + /// + public int LocalPort + { + get => _localPort; + set => this.RaiseAndSetIfChanged(ref _localPort, value); + } + + /// + /// 要发送的消息 + /// + public string Message + { + get => _message; + set => this.RaiseAndSetIfChanged(ref _message, value); + } + + /// + /// 状态消息 + /// + public string StatusMessage + { + get => _statusMessage; + set => this.RaiseAndSetIfChanged(ref _statusMessage, value); + } + + /// + /// 接收到的消息列表 + /// + public ObservableCollection ReceivedMessages => _receivedMessages; + + /// + /// 已发送的消息列表 + /// + public ObservableCollection SentMessages => _sentMessages; + + /// + /// 连接命令 + /// + public ReactiveCommand ConnectCommand { get; } + + /// + /// 断开连接命令 + /// + public ReactiveCommand DisconnectCommand { get; } + + /// + /// 发送消息命令 + /// + public ReactiveCommand SendMessageCommand { get; } + + /// + /// 清空消息命令 + /// + public ReactiveCommand ClearMessagesCommand { get; } + + /// + /// 连接到服务器 + /// + private async Task ConnectAsync() + { + try + { + if (!IPAddress.TryParse(ServerIp, out var ipAddress)) + { + StatusMessage = $"无效的 IP 地址: {ServerIp}"; + _logger?.LogWarning("无效的 IP 地址: {Ip}", ServerIp); + return; + } + + if (ServerPort < 1 || ServerPort > 65535) + { + StatusMessage = $"无效的端口: {ServerPort}"; + _logger?.LogWarning("无效的端口: {Port}", ServerPort); + return; + } + + // 创建 UDP 客户端 + _udpClient = new UdpClient(LocalPort); + _udpClient.EnableBroadcast = true; + + var localEndPoint = (IPEndPoint)_udpClient.Client.LocalEndPoint!; + LocalPort = localEndPoint.Port; + + IsConnected = true; + StatusMessage = $"已连接到 {ServerIp}:{ServerPort} (本地端口: {LocalPort})"; + _logger?.LogInformation("UDP 客户端已连接到 {ServerIp}:{ServerPort}, 本地端口: {LocalPort}", + ServerIp, ServerPort, LocalPort); + + // 启动接收任务 + _receiveCancellationTokenSource = new CancellationTokenSource(); + _ = Task.Run(() => ReceiveMessagesAsync(_receiveCancellationTokenSource.Token)); + } + catch (Exception ex) + { + _logger?.LogError(ex, "连接失败"); + StatusMessage = $"连接失败: {ex.Message}"; + Disconnect(); + } + } + + /// + /// 断开连接 + /// + private void Disconnect() + { + try + { + _receiveCancellationTokenSource?.Cancel(); + _receiveCancellationTokenSource?.Dispose(); + _receiveCancellationTokenSource = null; + + _udpClient?.Close(); + _udpClient?.Dispose(); + _udpClient = null; + + IsConnected = false; + StatusMessage = "已断开连接"; + _logger?.LogInformation("UDP 客户端已断开连接"); + } + catch (Exception ex) + { + _logger?.LogError(ex, "断开连接时出错"); + } + } + + /// + /// 发送消息 + /// + private async Task SendMessageAsync() + { + if (_udpClient == null || string.IsNullOrWhiteSpace(Message)) + return; + + try + { + if (!IPAddress.TryParse(ServerIp, out var ipAddress)) + { + StatusMessage = $"无效的 IP 地址: {ServerIp}"; + return; + } + + var remoteEndPoint = new IPEndPoint(ipAddress, ServerPort); + var data = Encoding.UTF8.GetBytes(Message); + + await _udpClient.SendAsync(data, data.Length, remoteEndPoint); + + var timestamp = DateTime.Now.ToString("HH:mm:ss"); + var displayMessage = $"[{timestamp}] 发送: {Message}"; + + await Dispatcher.UIThread.InvokeAsync(() => + { + SentMessages.Add(displayMessage); + }); + + _logger?.LogInformation("发送消息到 {ServerIp}:{ServerPort}: {Message}", + ServerIp, ServerPort, Message); + + Message = string.Empty; // 清空输入框 + } + catch (Exception ex) + { + _logger?.LogError(ex, "发送消息失败"); + StatusMessage = $"发送失败: {ex.Message}"; + } + } + + /// + /// 接收消息(后台任务) + /// + private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && _udpClient != null) + { + try + { + var result = await _udpClient.ReceiveAsync(); + var receivedData = Encoding.UTF8.GetString(result.Buffer); + var remoteEndPoint = result.RemoteEndPoint; + var timestamp = DateTime.Now.ToString("HH:mm:ss"); + var displayMessage = $"[{timestamp}] 来自 {remoteEndPoint.Address}:{remoteEndPoint.Port}: {receivedData}"; + + await Dispatcher.UIThread.InvokeAsync(() => + { + ReceivedMessages.Add(displayMessage); + }); + + _logger?.LogDebug("收到消息: {Message} 来自 {EndPoint}", receivedData, remoteEndPoint); + } + catch (ObjectDisposedException) + { + // UDP 客户端已关闭,正常退出 + break; + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted) + { + // 操作被取消,正常退出 + break; + } + catch (Exception ex) + { + _logger?.LogError(ex, "接收消息时出错"); + await Task.Delay(1000, cancellationToken); // 等待后重试 + } + } + } + + /// + /// 清空消息 + /// + private void ClearMessages() + { + ReceivedMessages.Clear(); + SentMessages.Clear(); + _logger?.LogInformation("已清空消息列表"); + } + + /// + /// 清理资源 + /// + public void Dispose() + { + Disconnect(); + } +} + diff --git a/AuroraDesk.Presentation/ViewModels/Pages/UdpServerPageViewModel.cs b/AuroraDesk.Presentation/ViewModels/Pages/UdpServerPageViewModel.cs new file mode 100644 index 0000000..c8947b3 --- /dev/null +++ b/AuroraDesk.Presentation/ViewModels/Pages/UdpServerPageViewModel.cs @@ -0,0 +1,308 @@ +using AuroraDesk.Presentation.ViewModels.Base; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reactive; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; + +namespace AuroraDesk.Presentation.ViewModels.Pages; + +/// +/// UDP 服务端页面 ViewModel +/// +public class UdpServerPageViewModel : RoutableViewModel +{ + private readonly ILogger? _logger; + private UdpClient? _udpServer; + private CancellationTokenSource? _listenCancellationTokenSource; + private bool _isListening; + private int _listenPort = 8080; + private string _statusMessage = "未启动"; + private readonly ObservableCollection _receivedMessages = new(); + private readonly ObservableCollection _clients = new(); + + public UdpServerPageViewModel( + IScreen hostScreen, + ILogger? logger = null) + : base(hostScreen, "UdpServer") + { + _logger = logger; + + // 创建命令 + StartListeningCommand = ReactiveCommand.CreateFromTask(StartListeningAsync, + this.WhenAnyValue(x => x.IsListening, isListening => !isListening)); + StopListeningCommand = ReactiveCommand.Create(StopListening, + this.WhenAnyValue(x => x.IsListening)); + ClearMessagesCommand = ReactiveCommand.Create(ClearMessages); + ClearClientsCommand = ReactiveCommand.Create(ClearClients); + } + + /// + /// 是否正在监听 + /// + public bool IsListening + { + get => _isListening; + set => this.RaiseAndSetIfChanged(ref _isListening, value); + } + + /// + /// 监听端口 + /// + public int ListenPort + { + get => _listenPort; + set => this.RaiseAndSetIfChanged(ref _listenPort, value); + } + + /// + /// 状态消息 + /// + public string StatusMessage + { + get => _statusMessage; + set => this.RaiseAndSetIfChanged(ref _statusMessage, value); + } + + /// + /// 接收到的消息列表 + /// + public ObservableCollection ReceivedMessages => _receivedMessages; + + /// + /// 客户端列表 + /// + public ObservableCollection Clients => _clients; + + /// + /// 开始监听命令 + /// + public ReactiveCommand StartListeningCommand { get; } + + /// + /// 停止监听命令 + /// + public ReactiveCommand StopListeningCommand { get; } + + /// + /// 清空消息命令 + /// + public ReactiveCommand ClearMessagesCommand { get; } + + /// + /// 清空客户端命令 + /// + public ReactiveCommand ClearClientsCommand { get; } + + /// + /// 开始监听 + /// + private async Task StartListeningAsync() + { + try + { + if (ListenPort < 1 || ListenPort > 65535) + { + StatusMessage = $"无效的端口: {ListenPort}"; + _logger?.LogWarning("无效的端口: {Port}", ListenPort); + return; + } + + // 创建 UDP 服务器 + var localEndPoint = new IPEndPoint(IPAddress.Any, ListenPort); + _udpServer = new UdpClient(localEndPoint); + _udpServer.EnableBroadcast = true; + + IsListening = true; + StatusMessage = $"正在监听端口 {ListenPort}..."; + _logger?.LogInformation("UDP 服务器开始监听端口 {Port}", ListenPort); + + // 启动接收任务 + _listenCancellationTokenSource = new CancellationTokenSource(); + _ = Task.Run(() => ListenForMessagesAsync(_listenCancellationTokenSource.Token)); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) + { + StatusMessage = $"端口 {ListenPort} 已被占用"; + _logger?.LogWarning("端口 {Port} 已被占用", ListenPort); + IsListening = false; + } + catch (Exception ex) + { + _logger?.LogError(ex, "启动监听失败"); + StatusMessage = $"启动失败: {ex.Message}"; + IsListening = false; + } + } + + /// + /// 停止监听 + /// + private void StopListening() + { + try + { + _listenCancellationTokenSource?.Cancel(); + _listenCancellationTokenSource?.Dispose(); + _listenCancellationTokenSource = null; + + _udpServer?.Close(); + _udpServer?.Dispose(); + _udpServer = null; + + IsListening = false; + StatusMessage = "已停止监听"; + _logger?.LogInformation("UDP 服务器已停止监听"); + } + catch (Exception ex) + { + _logger?.LogError(ex, "停止监听时出错"); + } + } + + /// + /// 监听消息(后台任务) + /// + private async Task ListenForMessagesAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && _udpServer != null) + { + try + { + var result = await _udpServer.ReceiveAsync(); + var receivedData = Encoding.UTF8.GetString(result.Buffer); + var remoteEndPoint = result.RemoteEndPoint; + var timestamp = DateTime.Now.ToString("HH:mm:ss"); + var displayMessage = $"[{timestamp}] 来自 {remoteEndPoint.Address}:{remoteEndPoint.Port}: {receivedData}"; + + await Dispatcher.UIThread.InvokeAsync(() => + { + ReceivedMessages.Add(displayMessage); + + // 更新客户端列表 + var clientAddress = remoteEndPoint.Address.ToString(); + var clientPort = remoteEndPoint.Port; + var clientKey = $"{clientAddress}:{clientPort}"; + + var existingClient = Clients.FirstOrDefault(c => c.Address == clientAddress && c.Port == clientPort); + if (existingClient == null) + { + Clients.Add(new ClientInfo + { + Address = clientAddress, + Port = clientPort, + FirstSeen = DateTime.Now, + LastSeen = DateTime.Now, + MessageCount = 1 + }); + } + else + { + existingClient.LastSeen = DateTime.Now; + existingClient.MessageCount++; + } + }); + + _logger?.LogDebug("收到消息: {Message} 来自 {EndPoint}", receivedData, remoteEndPoint); + + // 自动回复(可选) + var responseMessage = $"收到: {receivedData}"; + var responseData = Encoding.UTF8.GetBytes(responseMessage); + await _udpServer.SendAsync(responseData, responseData.Length, remoteEndPoint); + } + catch (ObjectDisposedException) + { + // UDP 服务器已关闭,正常退出 + break; + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted) + { + // 操作被取消,正常退出 + break; + } + catch (Exception ex) + { + _logger?.LogError(ex, "接收消息时出错"); + await Task.Delay(1000, cancellationToken); // 等待后重试 + } + } + } + + /// + /// 清空消息 + /// + private void ClearMessages() + { + ReceivedMessages.Clear(); + _logger?.LogInformation("已清空消息列表"); + } + + /// + /// 清空客户端列表 + /// + private void ClearClients() + { + Clients.Clear(); + _logger?.LogInformation("已清空客户端列表"); + } + + /// + /// 清理资源 + /// + public void Dispose() + { + StopListening(); + } +} + +/// +/// 客户端信息 +/// +public class ClientInfo : ReactiveObject +{ + private string _address = string.Empty; + private int _port; + private DateTime _firstSeen; + private DateTime _lastSeen; + private int _messageCount; + + public string Address + { + get => _address; + set => this.RaiseAndSetIfChanged(ref _address, value); + } + + public int Port + { + get => _port; + set => this.RaiseAndSetIfChanged(ref _port, value); + } + + public DateTime FirstSeen + { + get => _firstSeen; + set => this.RaiseAndSetIfChanged(ref _firstSeen, value); + } + + public DateTime LastSeen + { + get => _lastSeen; + set => this.RaiseAndSetIfChanged(ref _lastSeen, value); + } + + public int MessageCount + { + get => _messageCount; + set => this.RaiseAndSetIfChanged(ref _messageCount, value); + } + + public string DisplayName => $"{Address}:{Port}"; +} + diff --git a/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml b/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml new file mode 100644 index 0000000..9cd5af6 --- /dev/null +++ b/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs b/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs new file mode 100644 index 0000000..1a52a15 --- /dev/null +++ b/AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs @@ -0,0 +1,547 @@ +using ReactiveUI.Avalonia; +using ReactiveUI; +using Avalonia.Markup.Xaml; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia; +using Avalonia.Media; +using Avalonia.Collections; +using Avalonia.VisualTree; +using AuroraDesk.Presentation.ViewModels.Pages; +using AuroraDesk.Core.Entities; +using System.Linq; +using System.Reactive.Disposables; +using System; +using Avalonia.Layout; + +namespace AuroraDesk.Presentation.Views.Pages; + +public partial class NodeCanvasPageView : ReactiveUserControl +{ + private Canvas? _canvasContainer; + private Canvas? _gridBackgroundLayer; + private ScrollViewer? _canvasScrollViewer; + private Node? _draggedNode; + private Point? _dragStartPoint; + private ConnectionPoint? _hoveredConnectionPoint; + private Path? _tempConnectionLine; + private const double GridSize = 20; + + public NodeCanvasPageView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + // 延迟初始化,等待加载完成 + this.Loaded += (s, e) => + { + _canvasContainer = this.FindControl("CanvasContainer"); + _gridBackgroundLayer = this.FindControl("GridBackgroundLayer"); + _canvasScrollViewer = this.FindControl("CanvasScrollViewer"); + + if (_canvasContainer != null) + { + _canvasContainer.PointerPressed += OnCanvasPointerPressed; + _canvasContainer.PointerMoved += OnCanvasPointerMoved; + _canvasContainer.PointerReleased += OnCanvasPointerReleased; + + // 在 Canvas 上添加拖拽事件 + _canvasContainer.AddHandler(DragDrop.DragOverEvent, OnCanvasDragOver); + _canvasContainer.AddHandler(DragDrop.DropEvent, OnCanvasDrop); + + // 监听 Canvas 尺寸变化,更新网格 + _canvasContainer.SizeChanged += (sender, args) => DrawGridBackground(); + } + + // 在 ScrollViewer 上也添加拖拽事件,确保可以接收从左侧拖拽的组件 + if (_canvasScrollViewer != null) + { + _canvasScrollViewer.AddHandler(DragDrop.DragOverEvent, OnCanvasDragOver); + _canvasScrollViewer.AddHandler(DragDrop.DropEvent, OnCanvasDrop); + } + + // 设置组件库的拖拽支持 + SetupTemplateDragAndDrop(); + + // 绘制网格背景 + DrawGridBackground(); + + // 监听连接变化,更新连接线 + if (ViewModel != null) + { + ViewModel.WhenAnyValue(x => x.Connections) + .Subscribe(_ => UpdateConnectionLines()); + + ViewModel.WhenAnyValue(x => x.Nodes) + .Subscribe(nodes => + { + UpdateConnectionLines(); + UpdateNodesOnCanvas(nodes); + // 调试:检查节点数量 + System.Diagnostics.Debug.WriteLine($"节点数量: {nodes?.Count ?? 0}"); + if (nodes != null) + { + foreach (var node in nodes) + { + System.Diagnostics.Debug.WriteLine($"节点: {node.Title}, 位置: ({node.X}, {node.Y})"); + } + } + }); + + ViewModel.WhenAnyValue(x => x.IsConnecting, x => x.ConnectingSourcePoint) + .Subscribe(_ => UpdateConnectionLines()); + } + }; + } + + private void SetupTemplateDragAndDrop() + { + // 延迟设置,等待模板加载完成 + if (ViewModel != null) + { + ViewModel.WhenAnyValue(x => x.NodeTemplates) + .Subscribe(_ => + { + // 使用延迟来确保UI已渲染 + System.Threading.Tasks.Task.Delay(100).ContinueWith(_ => + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + SetupTemplateItems(); + }); + }); + }); + } + } + + private void SetupTemplateItems() + { + // 查找所有模板项 + var templateItems = this.GetVisualDescendants() + .OfType() + .Where(b => b.Name == "TemplateItem") + .ToList(); + + foreach (var item in templateItems) + { + SetupTemplateItemDrag(item); + } + } + + private void SetupTemplateItemDrag(Border item) + { + if (item.DataContext is NodeTemplate template) + { + item.PointerPressed += async (sender, args) => + { + if (args.GetCurrentPoint(item).Properties.IsLeftButtonPressed) + { + // 设置允许拖拽的光标 + item.Cursor = new Cursor(StandardCursorType.DragMove); + + var dataTransfer = new DataObject(); + dataTransfer.Set("NodeTemplate", template); + + // 执行拖拽操作 + var result = await DragDrop.DoDragDrop(args, dataTransfer, DragDropEffects.Copy); + + // 恢复光标 + item.Cursor = Cursor.Default; + } + }; + } + } + + private void OnCanvasDragOver(object? sender, DragEventArgs e) + { + if (e.Data.GetDataFormats().Contains("NodeTemplate")) + { + e.DragEffects = DragDropEffects.Copy; + } + } + + private void OnCanvasDrop(object? sender, DragEventArgs e) + { + if (_canvasContainer == null || ViewModel == null) return; + + if (e.Data.GetDataFormats().Contains("NodeTemplate") && + e.Data.Get("NodeTemplate") is NodeTemplate template) + { + // 获取相对于 Canvas 的位置 + var point = e.GetPosition(_canvasContainer); + ViewModel.AddNodeFromTemplateCommand.Execute((template, point.X, point.Y)).Subscribe(); + e.Handled = true; + } + } + + private void UpdateConnectionLines() + { + if (_canvasContainer == null || ViewModel == null) return; + + // 移除临时连接线 + if (_tempConnectionLine != null && _canvasContainer.Children.Contains(_tempConnectionLine)) + { + _canvasContainer.Children.Remove(_tempConnectionLine); + _tempConnectionLine = null; + } + + // 更新所有连接线 + var connections = ViewModel.Connections.ToList(); + foreach (var connection in connections) + { + UpdateConnectionLine(connection); + } + + // 如果正在连接,显示临时连接线 + if (ViewModel.IsConnecting && ViewModel.ConnectingSourcePoint != null) + { + ShowTempConnectionLine(ViewModel.ConnectingSourcePoint); + } + } + + private void UpdateConnectionLine(Connection connection) + { + if (_canvasContainer == null || connection.SourcePoint == null || connection.TargetPoint == null) + return; + + var sourcePoint = GetConnectionPointPosition(connection.SourcePoint); + var targetPoint = GetConnectionPointPosition(connection.TargetPoint); + + // 查找或创建连接线Path + var path = _canvasContainer.Children.OfType() + .FirstOrDefault(p => p.Tag == connection); + + if (path == null) + { + path = new Path + { + Stroke = new SolidColorBrush(Color.FromRgb(52, 152, 219)), + StrokeThickness = 2, + Tag = connection + }; + _canvasContainer.Children.Add(path); + } + + // 创建贝塞尔曲线 + var geometry = new PathGeometry(); + var figure = new PathFigure + { + StartPoint = sourcePoint + }; + + var controlPoint1 = new Point(sourcePoint.X + 50, sourcePoint.Y); + var controlPoint2 = new Point(targetPoint.X - 50, targetPoint.Y); + var bezierSegment = new BezierSegment + { + Point1 = controlPoint1, + Point2 = controlPoint2, + Point3 = targetPoint + }; + + figure.Segments.Add(bezierSegment); + geometry.Figures.Add(figure); + path.Data = geometry; + } + + private void ShowTempConnectionLine(ConnectionPoint sourcePoint) + { + if (_canvasContainer == null) return; + + var startPoint = GetConnectionPointPosition(sourcePoint); + var endPoint = new Point(startPoint.X + 100, startPoint.Y); + + if (_tempConnectionLine == null) + { + _tempConnectionLine = new Path + { + Stroke = new SolidColorBrush(Color.FromRgb(52, 152, 219)), + StrokeThickness = 2, + StrokeDashArray = new AvaloniaList { 5, 5 } + }; + _canvasContainer.Children.Add(_tempConnectionLine); + } + + var geometry = new PathGeometry(); + var figure = new PathFigure { StartPoint = startPoint }; + var lineSegment = new LineSegment { Point = endPoint }; + figure.Segments.Add(lineSegment); + geometry.Figures.Add(figure); + _tempConnectionLine.Data = geometry; + } + + private Point GetConnectionPointPosition(ConnectionPoint point) + { + if (point.Node == null || _canvasContainer == null) + return new Point(0, 0); + + var nodeX = point.Node.X; + var nodeY = point.Node.Y; + var nodeHeight = point.Node.Height; + + // 计算连接点在Canvas上的绝对位置 + if (point.Type == ConnectionPointType.Input) + { + // 输入点在左侧,垂直居中 + var inputIndex = point.Node.ConnectionPoints + .Where(p => p.Type == ConnectionPointType.Input) + .OrderBy(p => p.Index) + .ToList() + .IndexOf(point); + + var spacing = nodeHeight / (point.Node.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Input) + 1); + return new Point(nodeX, nodeY + spacing * (inputIndex + 1)); + } + else + { + // 输出点在右侧,垂直排列 + var outputIndex = point.Node.ConnectionPoints + .Where(p => p.Type == ConnectionPointType.Output) + .OrderBy(p => p.Index) + .ToList() + .IndexOf(point); + + var spacing = nodeHeight / (point.Node.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Output) + 1); + return new Point(nodeX + point.Node.Width, nodeY + spacing * (outputIndex + 1)); + } + } + + private void OnCanvasPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (_canvasContainer == null || ViewModel == null) return; + + var point = e.GetPosition(_canvasContainer); + var source = e.Source as Control; + + // 检查是否点击了连接点 + var connectionPoint = FindConnectionPoint(source); + if (connectionPoint != null) + { + HandleConnectionPointClick(connectionPoint); + e.Handled = true; + return; + } + + // 检查是否点击了节点(但不是连接点) + var node = FindNode(source); + if (node != null && connectionPoint == null) + { + ViewModel.SelectedNode = node; + _draggedNode = node; + _dragStartPoint = point; + e.Handled = true; + return; + } + + // 点击空白区域,取消选中和连接 + ViewModel.SelectedNode = null; + ViewModel.CancelConnection(); + + // 在点击位置添加新节点(仅在左键点击时) + if (e.GetCurrentPoint(_canvasContainer).Properties.IsLeftButtonPressed) + { + // 调试:输出点击位置 + System.Diagnostics.Debug.WriteLine($"点击位置: ({point.X}, {point.Y})"); + System.Diagnostics.Debug.WriteLine($"Canvas大小: {_canvasContainer.Bounds.Width} x {_canvasContainer.Bounds.Height}"); + + ViewModel.AddNodeCommand.Execute((point.X, point.Y)).Subscribe(); + } + } + + private void OnCanvasPointerMoved(object? sender, PointerEventArgs e) + { + if (_canvasContainer == null || ViewModel == null) return; + + var point = e.GetPosition(_canvasContainer); + + // 拖拽节点 + if (_draggedNode != null && _dragStartPoint.HasValue) + { + var deltaX = point.X - _dragStartPoint.Value.X; + var deltaY = point.Y - _dragStartPoint.Value.Y; + ViewModel.UpdateNodePosition(_draggedNode, + _draggedNode.X + deltaX, + _draggedNode.Y + deltaY); + _dragStartPoint = point; + } + + // 如果正在连接,更新临时连接线 + if (ViewModel.IsConnecting && ViewModel.ConnectingSourcePoint != null) + { + var sourcePos = GetConnectionPointPosition(ViewModel.ConnectingSourcePoint); + if (_tempConnectionLine != null) + { + var geometry = new PathGeometry(); + var figure = new PathFigure { StartPoint = sourcePos }; + var lineSegment = new LineSegment { Point = point }; + figure.Segments.Add(lineSegment); + geometry.Figures.Add(figure); + _tempConnectionLine.Data = geometry; + } + } + } + + private void OnCanvasPointerReleased(object? sender, PointerReleasedEventArgs e) + { + _draggedNode = null; + _dragStartPoint = null; + } + + private void HandleConnectionPointClick(ConnectionPoint connectionPoint) + { + if (ViewModel == null) return; + + if (ViewModel.IsConnecting) + { + // 完成连接 + if (connectionPoint.Type == ConnectionPointType.Input) + { + ViewModel.CompleteConnectionCommand.Execute(connectionPoint).Subscribe(); + } + else + { + // 取消当前连接,开始新连接 + ViewModel.CancelConnection(); + ViewModel.StartConnectionCommand.Execute(connectionPoint).Subscribe(); + } + } + else + { + // 开始连接 + if (connectionPoint.Type == ConnectionPointType.Output) + { + ViewModel.StartConnectionCommand.Execute(connectionPoint).Subscribe(); + } + } + } + + private Node? FindNode(Control? control) + { + if (control == null || ViewModel == null) return null; + + // 向上查找直到找到包含Node的容器 + var current = control; + while (current != null) + { + if (current.DataContext is Node node && ViewModel.Nodes.Contains(node)) + { + return node; + } + current = current.Parent as Control; + } + + return null; + } + + private ConnectionPoint? FindConnectionPoint(Control? control) + { + if (control == null) return null; + + var current = control; + while (current != null) + { + if (current.DataContext is ConnectionPoint point) + { + return point; + } + current = current.Parent as Control; + } + + return null; + } + + private void DrawGridBackground() + { + if (_gridBackgroundLayer == null || _canvasContainer == null) return; + + // 清空之前的网格线 + _gridBackgroundLayer.Children.Clear(); + + var width = _canvasContainer.Bounds.Width > 0 ? _canvasContainer.Bounds.Width : 2000; + var height = _canvasContainer.Bounds.Height > 0 ? _canvasContainer.Bounds.Height : 2000; + + // 创建网格线画笔 + var gridBrush = new SolidColorBrush(Color.FromRgb(224, 224, 224)); // #E0E0E0 + + // 绘制垂直线 + for (double x = 0; x <= width; x += GridSize) + { + var line = new Line + { + StartPoint = new Point(x, 0), + EndPoint = new Point(x, height), + Stroke = gridBrush, + StrokeThickness = 0.5 + }; + _gridBackgroundLayer.Children.Add(line); + } + + // 绘制水平线 + for (double y = 0; y <= height; y += GridSize) + { + var line = new Line + { + StartPoint = new Point(0, y), + EndPoint = new Point(width, y), + Stroke = gridBrush, + StrokeThickness = 0.5 + }; + _gridBackgroundLayer.Children.Add(line); + } + } + + private void UpdateNodesOnCanvas(System.Collections.ObjectModel.ObservableCollection? nodes) + { + if (_canvasContainer == null || nodes == null) return; + + // 查找 ItemsControl + var itemsControl = this.FindControl("NodesItemsControl"); + if (itemsControl == null) + { + itemsControl = _canvasContainer.GetVisualDescendants() + .OfType() + .FirstOrDefault(); + } + + if (itemsControl != null) + { + // 强制更新 ItemsControl 的布局 + itemsControl.InvalidateMeasure(); + itemsControl.InvalidateArrange(); + + // 延迟更新,确保容器已生成 + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + try + { + foreach (var item in itemsControl.Items) + { + if (item is Node node) + { + // 在 Avalonia 中,通过遍历视觉树来查找对应的容器 + var container = itemsControl.GetVisualDescendants() + .OfType() + .FirstOrDefault(cc => cc.Content == node); + + if (container is ContentControl contentControl) + { + Canvas.SetLeft(contentControl, node.X); + Canvas.SetTop(contentControl, node.Y); + System.Diagnostics.Debug.WriteLine($"设置节点 {node.Title} 位置: ({node.X}, {node.Y})"); + } + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"更新节点位置时出错: {ex.Message}"); + } + }, Avalonia.Threading.DispatcherPriority.Normal); + } + } +} + diff --git a/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml b/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml new file mode 100644 index 0000000..837a6b7 --- /dev/null +++ b/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml @@ -0,0 +1,546 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml.cs b/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml.cs new file mode 100644 index 0000000..0a15027 --- /dev/null +++ b/AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml.cs @@ -0,0 +1,19 @@ +using ReactiveUI.Avalonia; +using Avalonia.Markup.Xaml; +using AuroraDesk.Presentation.ViewModels.Pages; + +namespace AuroraDesk.Presentation.Views.Pages; + +public partial class UdpClientPageView : ReactiveUserControl +{ + public UdpClientPageView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} + diff --git a/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml b/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml new file mode 100644 index 0000000..ba7994a --- /dev/null +++ b/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml.cs b/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml.cs new file mode 100644 index 0000000..6d69691 --- /dev/null +++ b/AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml.cs @@ -0,0 +1,69 @@ +using ReactiveUI.Avalonia; +using Avalonia.Markup.Xaml; +using Avalonia.Controls; +using AuroraDesk.Presentation.ViewModels.Pages; +using HeroIconsAvalonia.Controls; +using ReactiveUI; +using HeroIconsAvalonia.Enums; + +namespace AuroraDesk.Presentation.Views.Pages; + +public partial class UdpServerPageView : ReactiveUserControl +{ + private Button? _toggleButton; + private HeroIcon? _toggleIcon; + private Expander? _clientListExpander; + + public UdpServerPageView() + { + InitializeComponent(); + this.WhenActivated(disposables => + { + // 查找控件 + _toggleButton = this.FindControl - - - - ``` -- **鍔熻兘淇濇寔**: - - **瀵艰埅鍔熻兘**: 瀵艰埅鍛戒护鍜屽弬鏁扮粦瀹氬畬鍏ㄦ甯? - **鍥炬爣鏄剧ず**: HeroIcons 鍥炬爣鐜板湪鑳芥纭樉绀? - **鏂囨湰鏄剧ず**: 瀵艰埅椤规爣棰樻甯告樉绀? - **鏍峰紡鏁堟灉**: 鎮仠鏁堟灉鍜岄€変腑鐘舵€佹甯? - **甯冨眬瀵归綈**: StackPanel 鐨勬按骞冲竷灞€鍜岄棿璺濇甯?- **鏋舵瀯浼樺娍**: - - **鍐呭缁熶竴**: 鍥炬爣鍜屾枃鏈湪鍚屼竴涓?StackPanel 涓紝甯冨眬涓€鑷? - **缁戝畾姝g‘**: 鍥炬爣绫诲瀷鍜屾爣棰橀兘姝g‘缁戝畾鍒?ViewModel - - **浠g爜绠€娲?*: 绉婚櫎浜嗛噸澶嶇殑鍐呭璁剧疆 - - **缁存姢鎬уソ**: 鍐呭缁撴瀯鏇存竻鏅帮紝鏄撲簬鐞嗚В鍜岀淮鎶?- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 瀵艰埅鑿滃崟鍥炬爣姝e父鏄剧ず 鉁? - 瀵艰埅鍔熻兘姝e父宸ヤ綔 鉁? - 鎮仠鏁堟灉鍜岄€変腑鐘舵€佹甯?鉁?- **璇存槑**: - - 鎴愬姛淇浜嗗鑸彍鍗曚腑鍥炬爣涓嶆樉绀虹殑闂 - - 瑙e喅浜?Button.Content 閲嶅璁剧疆瀵艰嚧鐨勫唴瀹硅鐩栭棶棰? - 纭繚浜嗗鑸彍鍗曠殑瀹屾暣鍔熻兘鍜岃瑙夋晥鏋? - 淇濇寔浜嗕笌鍏朵粬 HeroIcons 浣跨敤鐨勪竴鑷存€? -### 瀹炵幇 NavigationItem 浜岀骇瀵艰埅鏀寔 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 涓?NavigationItem 娣诲姞浜岀骇瀵艰埅鏀寔锛屽寘鎷?Children 灞炴€у拰 IsExpanded 鐘舵€佺鐞?- **鍔熻兘闇€姹?*: NavigationItem 鑷冲皯闇€瑕佹敮鎸佷簩绾у鑸紝鍏佽鍒涘缓灞傜骇缁撴瀯鐨勫鑸彍鍗?- **淇敼鏂囦欢**: - - `ViewModels/NavigationItem.cs` - 娣诲姞 Children 闆嗗悎鍜?IsExpanded 灞炴€? - `ViewModels/MainWindowViewModel.cs` - 娣诲姞 ToggleExpandCommand 鍜屼簩绾у鑸垵濮嬪寲 - - `MainWindow.axaml` - 灏?ListBox 鏀逛负 TreeView 鏀寔灞傜骇鏄剧ず - - `Converters/StringConverters.cs` - 娣诲姞 BoolToIconConverter 杞崲鍣?- **鎶€鏈疄鐜?*: - - **Children 灞炴€?*: `ObservableCollection Children` 鏀寔瀛愬鑸」 - - **IsExpanded 灞炴€?*: `bool IsExpanded` 鎺у埗灞曞紑/鎶樺彔鐘舵€? - **HasChildren 灞炴€?*: `bool HasChildren => Children?.Count > 0` 鍒ゆ柇鏄惁鏈夊瓙椤? - **ToggleExpandCommand**: `ReactiveCommand` 澶勭悊灞曞紑/鎶樺彔鎿嶄綔 - - **TreeView 鎺т欢**: 浣跨敤 TreeDataTemplate 鏀寔灞傜骇鏁版嵁缁戝畾 -- **浜岀骇瀵艰埅绀轰緥**: - - **鐢ㄦ埛绠$悊**: 鍖呭惈鐢ㄦ埛鍒楄〃銆佽鑹茬鐞嗐€佹潈闄愯缃笁涓瓙椤? - **绯荤粺璁剧疆**: 鍖呭惈甯歌璁剧疆銆佸畨鍏ㄨ缃€佸浠芥仮澶嶄笁涓瓙椤? - **鍏朵粬瀵艰埅椤?*: 淇濇寔鍗曠骇缁撴瀯 -- **UI 璁捐**: - - **灞曞紑/鎶樺彔鎸夐挳**: 浣跨敤 ChevronDown/ChevronRight 鍥炬爣 - - **灞傜骇缂╄繘**: TreeView 鑷姩澶勭悊灞傜骇缂╄繘 - - **鐘舵€佺鐞?*: 灞曞紑鐘舵€佷笌閫変腑鐘舵€佺嫭绔嬬鐞? - **浜や簰浣撻獙**: 鐐瑰嚮灞曞紑鎸夐挳鍒囨崲鐘舵€侊紝鐐瑰嚮瀵艰埅椤规墽琛屽鑸?- **杞崲鍣ㄦ敮鎸?*: - - **BoolToIconConverter**: 灏嗗竷灏斿€艰浆鎹负灞曞紑/鎶樺彔鍥炬爣 - - **鍥炬爣鏄犲皠**: true 鈫?ChevronDown, false 鈫?ChevronRight -- **鏋舵瀯浼樺娍**: - - **鍙墿灞曟€?*: 鏀寔浠绘剰灞傜骇鐨勫鑸粨鏋? - **鐘舵€佺鐞?*: 瀹屾暣鐨勫睍寮€/鎶樺彔鐘舵€佺鐞? - **鍝嶅簲寮?*: 浣跨敤 ReactiveUI 杩涜鐘舵€佺粦瀹? - **绫诲瀷瀹夊叏**: 浣跨敤寮虹被鍨嬪睘鎬э紝缂栬瘧鏃舵鏌?- **鍔熻兘淇濇寔**: - - 瀵艰埅鍔熻兘瀹屽叏姝e父 - - 鏍囩椤靛垱寤哄拰绠$悊姝e父 - - 鍥炬爣鏄剧ず鍜屼氦浜掓甯? - 澶氳瑷€鏀寔淇濇寔 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 浜岀骇瀵艰埅姝e父鏄剧ず鍜屼氦浜?鉁? - 灞曞紑/鎶樺彔鍔熻兘姝e父 鉁? - 瀵艰埅鍜屾爣绛鹃〉鍔熻兘瀹屾暣 鉁?- **璇存槑**: - - 鎴愬姛瀹炵幇浜?NavigationItem 鐨勪簩绾у鑸敮鎸? - 鎻愪緵浜嗗畬鏁寸殑灞傜骇瀵艰埅瑙e喅鏂规 - - 寤虹珛浜嗗彲鎵╁睍鐨勫鑸灦鏋? - 涓哄鏉傚簲鐢ㄦ彁渚涗簡鐏垫椿鐨勫鑸鐞嗚兘鍔? -### 淇 TreeView 浜岀骇瀵艰埅鏍峰紡闂 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 淇 TreeView 浜岀骇瀵艰埅涓嚭鐜扮殑鍙岄噸鎶樺彔绗﹀彿鍜屾牱寮忛敊涔遍棶棰?- **闂鎻忚堪**: - - TreeView 榛樿鏄剧ず灞曞紑/鎶樺彔鎸夐挳锛屼笌鑷畾涔夋寜閽啿绐佸鑷村嚭鐜颁袱涓姌鍙犵鍙凤紙>>锛? - TreeViewItem 榛樿缂╄繘瀵艰嚧宸﹁竟鍑虹幇澶ч噺绌虹櫧 - - 瀵艰埅椤瑰浘鏍囧拰鏂囨湰瀵归綈閿欎贡 -- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 娣诲姞 TreeView 鏍峰紡瑕嗙洊锛屼慨澶嶅竷灞€闂 -- **瑙e喅鏂规**: - - **闅愯棌榛樿鎸夐挳**: 浣跨敤鏍峰紡閫夋嫨鍣?`TreeViewItem /template/ ToggleButton` 闅愯棌 TreeView 榛樿鐨勫睍寮€/鎶樺彔鎸夐挳 - - **绉婚櫎榛樿缂╄繘**: 璁剧疆 `TreeViewItem` 鐨?`Margin` 鍜?`Padding` 涓?0 - - **璋冩暣甯冨眬**: 灏?`Margin="0,2"` 绉诲埌 Grid 涓婏紝纭繚闂磋窛涓€鑷?- **鎶€鏈疄鐜?*: - ```xml - - - - - - - ``` -- **淇鏁堟灉**: - - **鍗曚竴鎶樺彔绗﹀彿**: 鍙樉绀鸿嚜瀹氫箟鐨勫睍寮€/鎶樺彔鎸夐挳锛屼笉鍐嶆湁閲嶅绗﹀彿 - - **姝g‘瀵归綈**: 瀵艰埅椤瑰浘鏍囧拰鏂囨湰姝g‘瀵归綈锛屾棤澶氫綑绌虹櫧 - - **涓€鑷村竷灞€**: 鎵€鏈夊鑸」锛堝崟绾у拰浜岀骇锛夊竷灞€涓€鑷? - **鍔熻兘瀹屾暣**: 灞曞紑/鎶樺彔鍔熻兘姝e父宸ヤ綔 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鍙岄噸鎶樺彔绗﹀彿闂宸茶В鍐?鉁? - 鏍峰紡閿欎贡闂宸蹭慨澶?鉁? - 宸﹁竟绌虹櫧闂宸茶В鍐?鉁? - 浜岀骇瀵艰埅鍔熻兘姝e父 鉁?- **璇存槑**: - - 鎴愬姛瑙e喅浜?TreeView 榛樿鏍峰紡涓庤嚜瀹氫箟甯冨眬鐨勫啿绐侀棶棰? - 鎻愪緵浜嗘竻鏅般€佷竴鑷寸殑浜岀骇瀵艰埅鐣岄潰 - - 淇濇寔浜嗘墍鏈夊鑸姛鑳界殑瀹屾暣鎬? -### 閲嶆瀯浜岀骇瀵艰埅涓?ItemsControl 鏂规 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 灏?TreeView 浜岀骇瀵艰埅閲嶆瀯涓烘洿绠€鍗曞彲闈犵殑 ItemsControl 鏂规 -- **闂鍒嗘瀽**: - - TreeView 鎺т欢澶嶆潅锛屽鏄撳嚭鐜版牱寮忓啿绐佸拰鍙岄噸鎶樺彔绗﹀彿闂 - - Menu 鎺т欢涓嶉€傚悎鍋氫晶杈规爮瀵艰埅锛堥粯璁ゆ按骞冲竷灞€锛? - 闇€瑕佹洿绠€鍗曘€佹洿鍙帶鐨勪簩绾у鑸疄鐜?- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 灏?TreeView 鏀逛负 ItemsControl + ScrollViewer 鏂规 -- **鎶€鏈柟妗?*: - - **涓诲鍣?*: 浣跨敤 `ScrollViewer` + `ItemsControl` 鏇夸唬 TreeView - - **涓诲鑸」**: 姣忎釜瀵艰埅椤逛娇鐢?`Button` 鎺т欢锛屽寘鍚浘鏍囥€佹爣棰樺拰灞曞紑鎸夐挳 - - **瀛愬鑸」**: 浣跨敤宓屽鐨?`ItemsControl` 鏄剧ず瀛愰」锛岄€氳繃 `IsVisible` 鎺у埗鏄剧ず - - **灞曞紑鎺у埗**: 閫氳繃 `IsExpanded` 灞炴€ф帶鍒跺瓙椤圭殑鏄剧ず/闅愯棌 -- **甯冨眬璁捐**: - ```xml - - - - - - - - - - - - - - - - - - ``` -- **鍔熻兘鐗规€?*: - - **鍗曚竴灞曞紑鎸夐挳**: 姣忎釜鏈夊瓙椤圭殑瀵艰埅椤瑰彸渚ф樉绀轰竴涓睍寮€/鎶樺彔鎸夐挳 - - **灞傜骇缂╄繘**: 瀛愬鑸」閫氳繃 `Margin="20,0,0,0"` 瀹炵幇缂╄繘鏁堟灉 - - **瑙嗚鍖哄垎**: 瀛愰」浣跨敤杈冨皬鐨勫瓧浣擄紙13px锛夊拰鐏拌壊鏂囧瓧 - - **婊氬姩鏀寔**: ScrollViewer 鏀寔瀵艰埅椤硅繃澶氭椂鐨勬粴鍔? - **鍝嶅簲寮忎氦浜?*: 鎮仠鏁堟灉鍜岄€変腑鐘舵€佸畬鏁翠繚鐣?- **浼樺娍瀵规瘮**: - - **vs TreeView**: 鏃犳牱寮忓啿绐侊紝鏃犲弻閲嶆姌鍙犵鍙凤紝甯冨眬鏇村彲鎺? - **vs Menu**: 閫傚悎渚ц竟鏍忓瀭鐩村竷灞€锛屾棤寮瑰嚭鑿滃崟骞叉壈 - - **vs ListBox**: 鏀寔宓屽缁撴瀯锛屽睍寮€/鎶樺彔閫昏緫娓呮櫚 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 浜岀骇瀵艰埅姝e父鏄剧ず 鉁? - 灞曞紑/鎶樺彔鍔熻兘姝e父 鉁? - 鏃犳牱寮忓啿绐侀棶棰?鉁? - 甯冨眬娓呮櫚缇庤 鉁?- **璇存槑**: - - 鎴愬姛瑙e喅浜?TreeView 鐨勫鏉傛€ч棶棰? - 鎻愪緵浜嗘洿绠€鍗曘€佹洿鍙帶鐨勪簩绾у鑸疄鐜? - 淇濇寔浜嗘墍鏈夊師鏈夊姛鑳界殑瀹屾暣鎬? -### 浼樺寲浜岀骇瀵艰埅浜や簰閫昏緫 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 浼樺寲浜岀骇瀵艰埅鐨勪氦浜掗€昏緫锛岀Щ闄ゅ睍寮€/鎶樺彔鎸夐挳锛屽疄鐜扮偣鍑昏嚜鍔ㄥ睍寮€骞堕€変腑绗竴涓瓙椤?- **鍔熻兘闇€姹?*: - - 绉婚櫎灞曞紑/鎶樺彔鎸夐挳锛岀畝鍖栫晫闈? - 鐐瑰嚮鏈夊瓙椤圭殑瀵艰埅椤规椂鑷姩灞曞紑 - - 灞曞紑鏃堕粯璁ら€変腑绗竴涓瓙椤? - ContentPresenter 鏄剧ず閫変腑鐨勫瓙椤瑰唴瀹?- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 绉婚櫎灞曞紑/鎶樺彔鎸夐挳锛岀畝鍖栦富瀵艰埅椤瑰竷灞€ - - `ViewModels/MainWindowViewModel.cs` - 浼樺寲 NavigateToPage 鏂规硶锛屾坊鍔犺嚜鍔ㄥ睍寮€閫昏緫 -- **浜や簰閫昏緫**: - - **鏈夊瓙椤圭殑瀵艰埅椤?*: 鐐瑰嚮鏃惰嚜鍔ㄥ睍寮€锛岄€変腑绗竴涓瓙椤癸紝鍒涘缓瀵瑰簲鐨勬爣绛鹃〉 - - **鏃犲瓙椤圭殑瀵艰埅椤?*: 鐩存帴閫変腑锛屽垱寤哄搴旂殑鏍囩椤? - **瀛愰」瀵艰埅**: 鐩存帴閫変腑锛屽垱寤哄搴旂殑鏍囩椤?- **鎶€鏈疄鐜?*: - ```csharp - private void NavigateToPage(NavigationItem navigationItem) - { - // 鍙栨秷鎵€鏈夊鑸」鐨勯€変腑鐘舵€? foreach (var item in _navigationItems) - { - item.IsSelected = false; - foreach (var child in item.Children) - { - child.IsSelected = false; - } - } - - // 濡傛灉鏄湁瀛愰」鐨勫鑸」锛屽睍寮€骞堕€変腑绗竴涓瓙椤? if (navigationItem.HasChildren) - { - navigationItem.IsExpanded = true; - if (navigationItem.Children.Count > 0) - { - var firstChild = navigationItem.Children[0]; - firstChild.IsSelected = true; - SelectedNavigationItem = firstChild; - CreateTabForNavigationItem(firstChild); - } - } - else - { - navigationItem.IsSelected = true; - SelectedNavigationItem = navigationItem; - CreateTabForNavigationItem(navigationItem); - } - } - ``` -- **鐣岄潰浼樺寲**: - - **绉婚櫎灞曞紑鎸夐挳**: 涓诲鑸」鍙樉绀哄浘鏍囧拰鏍囬锛屾棤灞曞紑/鎶樺彔鎸夐挳 - - **绠€鍖栧竷灞€**: 浣跨敤 StackPanel 鏇夸唬澶嶆潅鐨?Grid 甯冨眬 - - **淇濇寔鍔熻兘**: 瀛愰」浠嶇劧閫氳繃 IsVisible 鎺у埗鏄剧ず -- **浠g爜閲嶆瀯**: - - **绉婚櫎 ToggleExpandCommand**: 涓嶅啀闇€瑕佸睍寮€/鎶樺彔鍛戒护 - - **绉婚櫎 ToggleExpand 鏂规硶**: 涓嶅啀闇€瑕佹墜鍔ㄥ垏鎹㈠睍寮€鐘舵€? - **鎻愬彇 CreateTabForNavigationItem 鏂规硶**: 澶嶇敤鏍囩椤靛垱寤洪€昏緫 -- **鐢ㄦ埛浣撻獙**: - - **涓€閿睍寮€**: 鐐瑰嚮"鐢ㄦ埛绠$悊"鑷姩灞曞紑骞堕€変腑"鐢ㄦ埛鍒楄〃" - - **鐩磋鎿嶄綔**: 鏃犻渶棰濆鐨勫睍寮€鎸夐挳锛屾搷浣滄洿鐩磋 - - **榛樿閫変腑**: 灞曞紑鍚庤嚜鍔ㄩ€変腑绗竴涓瓙椤癸紝鍑忓皯鐢ㄦ埛鎿嶄綔 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鐐瑰嚮"鐢ㄦ埛绠$悊"鑷姩灞曞紑骞堕€変腑绗竴涓瓙椤?鉁? - 鐐瑰嚮"绯荤粺璁剧疆"鑷姩灞曞紑骞堕€変腑绗竴涓瓙椤?鉁? - 瀛愰」瀵艰埅姝e父宸ヤ綔 鉁? - 鏍囩椤靛垱寤哄拰绠$悊姝e父 鉁?- **璇存槑**: - - 鎴愬姛浼樺寲浜嗕簩绾у鑸殑浜や簰閫昏緫 - - 鎻愪緵浜嗘洿鐩磋銆佹洿渚挎嵎鐨勭敤鎴蜂綋楠? - 绠€鍖栦簡鐣岄潰璁捐锛屽噺灏戜簡涓嶅繀瑕佺殑鎺т欢 - -### 淇鏈夊瓙绾у鑸」鐨?Content 璁剧疆闂 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 淇鏈夊瓙绾х殑瀵艰埅椤逛笉搴旇璁剧疆 Content 灞炴€х殑闂 -- **闂鍒嗘瀽**: - - "鐢ㄦ埛绠$悊"鍜?绯荤粺璁剧疆"杩欎袱涓湁瀛愮骇鐨勫鑸」璁剧疆浜?Content 灞炴€? - 浣嗗疄闄呬笂鐖剁骇鍙槸鐢ㄦ潵灞曞紑/鎶樺彔鐨勫鍣紝涓嶅簲璇ユ湁鍏蜂綋鍐呭 - - 瀹為檯鐨勫唴瀹瑰簲璇ョ敱瀛愮骇鎻愪緵 - - 褰撶偣鍑荤埗绾ф椂锛屼細鑷姩閫変腑绗竴涓瓙绾э紝鏄剧ず瀛愮骇鐨勫唴瀹?- **淇敼鏂囦欢**: - - `ViewModels/MainWindowViewModel.cs` - 绉婚櫎鏈夊瓙绾у鑸」鐨?Content 璁剧疆 -- **淇鍐呭**: - - **鐢ㄦ埛绠$悊**: 绉婚櫎 `Content = new Views.Pages.UsersPageView { DataContext = new Pages.UsersPageViewModel() }` - - **绯荤粺璁剧疆**: 绉婚櫎 `Content = new Views.Pages.SettingsPageView { DataContext = new Pages.SettingsPageViewModel() }` -- **璁捐鍘熷垯**: - - **鏈夊瓙绾х殑瀵艰埅椤?*: 涓嶈缃?Content锛屽彧浣滀负灞曞紑/鎶樺彔瀹瑰櫒 - - **鏃犲瓙绾х殑瀵艰埅椤?*: 璁剧疆 Content锛屾彁渚涘叿浣撻〉闈㈠唴瀹? - **瀛愮骇瀵艰埅椤?*: 璁剧疆 Content锛屾彁渚涘叿浣撻〉闈㈠唴瀹?- **閫昏緫娴佺▼**: - 1. 鐐瑰嚮"鐢ㄦ埛绠$悊" 鈫?鑷姩灞曞紑 鈫?閫変腑"鐢ㄦ埛鍒楄〃" 鈫?鏄剧ず鐢ㄦ埛鍒楄〃椤甸潰 - 2. 鐐瑰嚮"绯荤粺璁剧疆" 鈫?鑷姩灞曞紑 鈫?閫変腑"甯歌璁剧疆" 鈫?鏄剧ず甯歌璁剧疆椤甸潰 - 3. 鐐瑰嚮"浠〃鏉? 鈫?鐩存帴鏄剧ず浠〃鏉块〉闈紙鏃犲瓙绾э級 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鏈夊瓙绾х殑瀵艰埅椤逛笉鍐嶆湁鍐椾綑鐨?Content 鉁? - 鐐瑰嚮鐖剁骇姝g‘灞曞紑骞堕€変腑绗竴涓瓙绾?鉁? - 瀛愮骇鍐呭姝g‘鏄剧ず 鉁?- **璇存槑**: - - 淇浜嗗鑸」 Content 璁剧疆鐨勯€昏緫闂 - - 鏄庣‘浜嗙埗绾у拰瀛愮骇鐨勮亴璐e垎宸? - 鎻愰珮浜嗕唬鐮佺殑娓呮櫚搴﹀拰鍙淮鎶ゆ€? -### 瀹炵幇瀵艰埅椤逛簰鏂ュ睍寮€鍜岄€変腑鏁堟灉 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 瀹炵幇瀵艰埅椤圭殑浜掓枼灞曞紑閫昏緫鍜屾纭殑閫変腑鏁堟灉 -- **闂鍒嗘瀽**: - - 鐐瑰嚮鏈夊瓙椤圭殑瀵艰埅椤规椂锛屽簲璇ユ敮鎸佸睍寮€/鏀惰捣鍒囨崲 - - 灞曞紑鏃堕粯璁ょ涓€涓瓙椤瑰簲璇ユ湁閫変腑鏁堟灉 - - 澶氫釜瀵艰埅椤逛笉搴旇鍚屾椂灞曞紑锛屽簲璇ヤ簰鏂? - 鏀惰捣鏃跺簲璇ユ竻闄ゆ墍鏈夐€変腑鐘舵€?- **淇敼鏂囦欢**: - - `ViewModels/MainWindowViewModel.cs` - 浼樺寲 NavigateToPage 鏂规硶锛屽疄鐜颁簰鏂ュ睍寮€閫昏緫 -- **浜や簰閫昏緫**: - - **鏈夊瓙椤圭殑瀵艰埅椤?*: - - 鏈睍寮€鏃讹細灞曞紑骞堕€変腑绗竴涓瓙椤? - 宸插睍寮€鏃讹細鏀惰捣骞舵竻闄ら€変腑鐘舵€? - 浜掓枼锛氬睍寮€涓€涓椂鑷姩鏀惰捣鍏朵粬 - - **鏃犲瓙椤圭殑瀵艰埅椤?*锛氱洿鎺ラ€変腑锛屽悓鏃舵敹璧锋墍鏈夊叾浠栧睍寮€椤?- **鎶€鏈疄鐜?*: - ```csharp - private void NavigateToPage(NavigationItem navigationItem) - { - if (navigationItem.HasChildren) - { - if (navigationItem.IsExpanded) - { - // 鏀惰捣閫昏緫 - navigationItem.IsExpanded = false; - // 娓呴櫎鎵€鏈夐€変腑鐘舵€? SelectedNavigationItem = null; - } - else - { - // 灞曞紑閫昏緫 - // 鍏堟敹璧峰叾浠栨墍鏈夊睍寮€鐨勫鑸」 - foreach (var item in _navigationItems) - { - if (item != navigationItem) - { - item.IsExpanded = false; - // 娓呴櫎閫変腑鐘舵€? } - } - - // 灞曞紑褰撳墠椤瑰苟閫変腑绗竴涓瓙椤? navigationItem.IsExpanded = true; - var firstChild = navigationItem.Children[0]; - firstChild.IsSelected = true; - SelectedNavigationItem = firstChild; - } - } - } - ``` -- **鍔熻兘鐗规€?*: - - **浜掓枼灞曞紑**: 鍚屾椂鍙兘鏈変竴涓鑸」灞曞紑 - - **鍒囨崲灞曞紑**: 鐐瑰嚮宸插睍寮€鐨勯」浼氭敹璧? - **鑷姩閫変腑**: 灞曞紑鏃惰嚜鍔ㄩ€変腑绗竴涓瓙椤? - **鐘舵€佺鐞?*: 鏀惰捣鏃舵竻闄ゆ墍鏈夐€変腑鐘舵€? - **鏍囩椤电鐞?*: 閫変腑瀛愰」鏃惰嚜鍔ㄥ垱寤哄搴旀爣绛鹃〉 -- **鐢ㄦ埛浣撻獙**: - - **鐩磋鎿嶄綔**: 鐐瑰嚮"鐢ㄦ埛绠$悊"灞曞紑锛屽啀鐐瑰嚮鏀惰捣 - - **浜掓枼琛屼负**: 灞曞紑"鐢ㄦ埛绠$悊"鏃讹紝"绯荤粺璁剧疆"鑷姩鏀惰捣 - - **閫変腑鍙嶉**: 灞曞紑鍚庣涓€涓瓙椤规湁鏄庢樉鐨勯€変腑鏁堟灉 - - **鐘舵€佷竴鑷?*: 鏀惰捣鏃舵墍鏈夌姸鎬侀兘姝g‘娓呴櫎 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 浜掓枼灞曞紑鍔熻兘姝e父 鉁? - 灞曞紑/鏀惰捣鍒囨崲姝e父 鉁? - 绗竴涓瓙椤归€変腑鏁堟灉姝e父 鉁? - 鐘舵€佺鐞嗘纭?鉁?- **璇存槑**: - - 鎴愬姛瀹炵幇浜嗗鑸」鐨勪簰鏂ュ睍寮€閫昏緫 - - 鎻愪緵浜嗘洿鐩磋銆佹洿绗﹀悎鐢ㄦ埛涔犳儻鐨勪氦浜掍綋楠? - 瀹屽杽浜嗙姸鎬佺鐞嗗拰閫変腑鏁堟灉 - -### 閲嶆瀯绐楀彛鎺у埗鎸夐挳浣跨敤 HeroIcons -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 灏嗙獥鍙f帶鍒舵寜閽紙鏈€灏忓寲銆佹渶澶у寲銆佸叧闂級浠?Path 缁樺埗鏀逛负浣跨敤 HeroIcons -- **淇敼鍘熷洜**: 鐢ㄦ埛寤鸿浣跨敤 HeroIcons 鏇夸唬 Path 缁樺埗锛屾彁渚涙洿缁熶竴鐨勫浘鏍囩郴缁?- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 鏇存柊绐楀彛鎺у埗鎸夐挳浣跨敤 HeroIcon 鎺т欢 -- **鍥炬爣鏄犲皠**: - - **鏈€灏忓寲鎸夐挳**: `IconType.Minus` (鍘?Path 妯嚎) - - **鏈€澶у寲鎸夐挳**: `IconType.ArrowsPointingOut` (鍘?Path 鏂规) - - **鍏抽棴鎸夐挳**: `IconType.XMark` (鍘?Path X 褰? -- **鎶€鏈疄鐜?*: - ```xml - - - - - - - - - - - - - ``` -- **鏍峰紡淇濇寔**: - - **鎮仠鏁堟灉**: 淇濇寔鍘熸湁鐨勬偓鍋滆儗鏅壊鍙樺寲 - - **鍏抽棴鎸夐挳**: 淇濇寔绾㈣壊鎮仠鑳屾櫙鍜岀櫧鑹插浘鏍? - **灏哄涓€鑷?*: 淇濇寔 16x16 鍍忕礌鐨勫浘鏍囧昂瀵? - **棰滆壊缁戝畾**: 浣跨敤璧勬簮缁戝畾淇濇寔涓婚涓€鑷存€?- **鏋舵瀯浼樺娍**: - - **缁熶竴鍥炬爣绯荤粺**: 鎵€鏈夊浘鏍囬兘浣跨敤 HeroIcons锛屼繚鎸佽瑙変竴鑷存€? - **绠€鍖栦唬鐮?*: 绉婚櫎澶嶆潅鐨?Path 缁樺埗浠g爜 - - **鏄撲簬缁存姢**: 鍥炬爣绠$悊鏇撮泦涓紝鏄撲簬鏇存崲鍜屾洿鏂? - **绫诲瀷瀹夊叏**: 浣跨敤鏋氫妇绫诲瀷锛岀紪璇戞椂妫€鏌ュ浘鏍囨湁鏁堟€? - **鐜颁唬鍖?*: HeroIcons 鎻愪緵鏇寸幇浠c€佹洿涓撲笟鐨勫浘鏍囪璁?- **鍔熻兘淇濇寔**: - - 绐楀彛鎺у埗鎸夐挳鍔熻兘瀹屽叏姝e父 - - 鎮仠鏁堟灉鍜屼氦浜掑弽棣堜繚鎸? - 宸ュ叿鎻愮ず鍔熻兘姝e父 - - 鎸夐挳灏哄鍜屽竷灞€涓嶅彉 -- **瑙嗚鏁堟灉**: - - 鍥炬爣鏇村姞缁熶竴鍜屼笓涓? - 鏀寔楂楧PI鏄剧ず - - 涓庢暣浣?UI 璁捐椋庢牸涓€鑷? - 鎻愪緵鏇村ソ鐨勭敤鎴蜂綋楠?- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 - - 绐楀彛鎺у埗鎸夐挳姝e父鏄剧ず鍜屼氦浜? - 鎮仠鏁堟灉鍜岄鑹插彉鍖栨甯?- **璇存槑**: - - 鎴愬姛灏嗙獥鍙f帶鍒舵寜閽噸鏋勪负浣跨敤 HeroIcons - - 鎻愪緵浜嗘洿缁熶竴銆佹洿涓撲笟鐨勫浘鏍囦綋楠? - 绠€鍖栦簡浠g爜缁撴瀯锛屾彁楂樹簡鍙淮鎶ゆ€? - 瀹屽叏绗﹀悎鐜颁唬 UI 璁捐鏍囧噯 - -### 淇瀵艰埅鑿滃崟涓浘鏍囦笉鏄剧ず闂 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 淇瀵艰埅鑿滃崟涓?HeroIcons 鍥炬爣涓嶆樉绀虹殑闂 -- **闂鎻忚堪**: - - 瀵艰埅鑿滃崟涓殑鍥炬爣鏃犳硶鏄剧ず锛屽叾浠栧湴鏂圭殑 HeroIcons 閮借兘姝e父鏄剧ず - - 闂鍑虹幇鍦ㄥ鑸彍鍗曠殑 Button.Content 缁戝畾涓?- **鏍规湰鍘熷洜**: - - Button.Content 琚缃簡涓ゆ锛氱涓€娆″湪绗?3琛?`Content="{Binding Title}"`锛岀浜屾鍦ㄧ89-96琛岄€氳繃 `` 鏍囩閲嶆柊瀹氫箟 - - 杩欏鑷翠簡鍐呭琚鐩栵紝鍥炬爣鏃犳硶鏄剧ず -- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 淇瀵艰埅鑿滃崟鐨?Button.Content 閲嶅璁剧疆闂 -- **瑙e喅鏂规**: - - 绉婚櫎绗竴娆$殑 `Content="{Binding Title}"` 璁剧疆 - - 鍙繚鐣?`` 鏍囩涓殑 StackPanel 鍐呭 - - 纭繚鍥炬爣鍜屾枃鏈兘鑳芥纭樉绀?- **鎶€鏈疄鐜?*: - ```xml - - - - - - ``` -- **鍔熻兘淇濇寔**: - - **瀵艰埅鍔熻兘**: 瀵艰埅鍛戒护鍜屽弬鏁扮粦瀹氬畬鍏ㄦ甯? - **鍥炬爣鏄剧ず**: HeroIcons 鍥炬爣鐜板湪鑳芥纭樉绀? - **鏂囨湰鏄剧ず**: 瀵艰埅椤规爣棰樻甯告樉绀? - **鏍峰紡鏁堟灉**: 鎮仠鏁堟灉鍜岄€変腑鐘舵€佹甯? - **甯冨眬瀵归綈**: StackPanel 鐨勬按骞冲竷灞€鍜岄棿璺濇甯?- **鏋舵瀯浼樺娍**: - - **鍐呭缁熶竴**: 鍥炬爣鍜屾枃鏈湪鍚屼竴涓?StackPanel 涓紝甯冨眬涓€鑷? - **缁戝畾姝g‘**: 鍥炬爣绫诲瀷鍜屾爣棰橀兘姝g‘缁戝畾鍒?ViewModel - - **浠g爜绠€娲?*: 绉婚櫎浜嗛噸澶嶇殑鍐呭璁剧疆 - - **缁存姢鎬уソ**: 鍐呭缁撴瀯鏇存竻鏅帮紝鏄撲簬鐞嗚В鍜岀淮鎶?- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 瀵艰埅鑿滃崟鍥炬爣姝e父鏄剧ず 鉁? - 瀵艰埅鍔熻兘姝e父宸ヤ綔 鉁? - 鎮仠鏁堟灉鍜岄€変腑鐘舵€佹甯?鉁?- **璇存槑**: - - 鎴愬姛淇浜嗗鑸彍鍗曚腑鍥炬爣涓嶆樉绀虹殑闂 - - 瑙e喅浜?Button.Content 閲嶅璁剧疆瀵艰嚧鐨勫唴瀹硅鐩栭棶棰? - 纭繚浜嗗鑸彍鍗曠殑瀹屾暣鍔熻兘鍜岃瑙夋晥鏋? - 淇濇寔浜嗕笌鍏朵粬 HeroIcons 浣跨敤鐨勪竴鑷存€? -### 瀹炵幇 NavigationItem 浜岀骇瀵艰埅鏀寔 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 涓?NavigationItem 娣诲姞浜岀骇瀵艰埅鏀寔锛屽寘鎷?Children 灞炴€у拰 IsExpanded 鐘舵€佺鐞?- **鍔熻兘闇€姹?*: NavigationItem 鑷冲皯闇€瑕佹敮鎸佷簩绾у鑸紝鍏佽鍒涘缓灞傜骇缁撴瀯鐨勫鑸彍鍗?- **淇敼鏂囦欢**: - - `ViewModels/NavigationItem.cs` - 娣诲姞 Children 闆嗗悎鍜?IsExpanded 灞炴€? - `ViewModels/MainWindowViewModel.cs` - 娣诲姞 ToggleExpandCommand 鍜屼簩绾у鑸垵濮嬪寲 - - `MainWindow.axaml` - 灏?ListBox 鏀逛负 TreeView 鏀寔灞傜骇鏄剧ず - - `Converters/StringConverters.cs` - 娣诲姞 BoolToIconConverter 杞崲鍣?- **鎶€鏈疄鐜?*: - - **Children 灞炴€?*: `ObservableCollection Children` 鏀寔瀛愬鑸」 - - **IsExpanded 灞炴€?*: `bool IsExpanded` 鎺у埗灞曞紑/鎶樺彔鐘舵€? - **HasChildren 灞炴€?*: `bool HasChildren => Children?.Count > 0` 鍒ゆ柇鏄惁鏈夊瓙椤? - **ToggleExpandCommand**: `ReactiveCommand` 澶勭悊灞曞紑/鎶樺彔鎿嶄綔 - - **TreeView 鎺т欢**: 浣跨敤 TreeDataTemplate 鏀寔灞傜骇鏁版嵁缁戝畾 -- **浜岀骇瀵艰埅绀轰緥**: - - **鐢ㄦ埛绠$悊**: 鍖呭惈鐢ㄦ埛鍒楄〃銆佽鑹茬鐞嗐€佹潈闄愯缃笁涓瓙椤? - **绯荤粺璁剧疆**: 鍖呭惈甯歌璁剧疆銆佸畨鍏ㄨ缃€佸浠芥仮澶嶄笁涓瓙椤? - **鍏朵粬瀵艰埅椤?*: 淇濇寔鍗曠骇缁撴瀯 -- **UI 璁捐**: - - **灞曞紑/鎶樺彔鎸夐挳**: 浣跨敤 ChevronDown/ChevronRight 鍥炬爣 - - **灞傜骇缂╄繘**: TreeView 鑷姩澶勭悊灞傜骇缂╄繘 - - **鐘舵€佺鐞?*: 灞曞紑鐘舵€佷笌閫変腑鐘舵€佺嫭绔嬬鐞? - **浜や簰浣撻獙**: 鐐瑰嚮灞曞紑鎸夐挳鍒囨崲鐘舵€侊紝鐐瑰嚮瀵艰埅椤规墽琛屽鑸?- **杞崲鍣ㄦ敮鎸?*: - - **BoolToIconConverter**: 灏嗗竷灏斿€艰浆鎹负灞曞紑/鎶樺彔鍥炬爣 - - **鍥炬爣鏄犲皠**: true 鈫?ChevronDown, false 鈫?ChevronRight -- **鏋舵瀯浼樺娍**: - - **鍙墿灞曟€?*: 鏀寔浠绘剰灞傜骇鐨勫鑸粨鏋? - **鐘舵€佺鐞?*: 瀹屾暣鐨勫睍寮€/鎶樺彔鐘舵€佺鐞? - **鍝嶅簲寮?*: 浣跨敤 ReactiveUI 杩涜鐘舵€佺粦瀹? - **绫诲瀷瀹夊叏**: 浣跨敤寮虹被鍨嬪睘鎬э紝缂栬瘧鏃舵鏌?- **鍔熻兘淇濇寔**: - - 瀵艰埅鍔熻兘瀹屽叏姝e父 - - 鏍囩椤靛垱寤哄拰绠$悊姝e父 - - 鍥炬爣鏄剧ず鍜屼氦浜掓甯? - 澶氳瑷€鏀寔淇濇寔 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 浜岀骇瀵艰埅姝e父鏄剧ず鍜屼氦浜?鉁? - 灞曞紑/鎶樺彔鍔熻兘姝e父 鉁? - 瀵艰埅鍜屾爣绛鹃〉鍔熻兘瀹屾暣 鉁?- **璇存槑**: - - 鎴愬姛瀹炵幇浜?NavigationItem 鐨勪簩绾у鑸敮鎸? - 鎻愪緵浜嗗畬鏁寸殑灞傜骇瀵艰埅瑙e喅鏂规 - - 寤虹珛浜嗗彲鎵╁睍鐨勫鑸灦鏋? - 涓哄鏉傚簲鐢ㄦ彁渚涗簡鐏垫椿鐨勫鑸鐞嗚兘鍔? -### 淇 TreeView 浜岀骇瀵艰埅鏍峰紡闂 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 淇 TreeView 浜岀骇瀵艰埅涓嚭鐜扮殑鍙岄噸鎶樺彔绗﹀彿鍜屾牱寮忛敊涔遍棶棰?- **闂鎻忚堪**: - - TreeView 榛樿鏄剧ず灞曞紑/鎶樺彔鎸夐挳锛屼笌鑷畾涔夋寜閽啿绐佸鑷村嚭鐜颁袱涓姌鍙犵鍙凤紙>>锛? - TreeViewItem 榛樿缂╄繘瀵艰嚧宸﹁竟鍑虹幇澶ч噺绌虹櫧 - - 瀵艰埅椤瑰浘鏍囧拰鏂囨湰瀵归綈閿欎贡 -- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 娣诲姞 TreeView 鏍峰紡瑕嗙洊锛屼慨澶嶅竷灞€闂 -- **瑙e喅鏂规**: - - **闅愯棌榛樿鎸夐挳**: 浣跨敤鏍峰紡閫夋嫨鍣?`TreeViewItem /template/ ToggleButton` 闅愯棌 TreeView 榛樿鐨勫睍寮€/鎶樺彔鎸夐挳 - - **绉婚櫎榛樿缂╄繘**: 璁剧疆 `TreeViewItem` 鐨?`Margin` 鍜?`Padding` 涓?0 - - **璋冩暣甯冨眬**: 灏?`Margin="0,2"` 绉诲埌 Grid 涓婏紝纭繚闂磋窛涓€鑷?- **鎶€鏈疄鐜?*: - ```xml - - - - - - - ``` -- **淇鏁堟灉**: - - **鍗曚竴鎶樺彔绗﹀彿**: 鍙樉绀鸿嚜瀹氫箟鐨勫睍寮€/鎶樺彔鎸夐挳锛屼笉鍐嶆湁閲嶅绗﹀彿 - - **姝g‘瀵归綈**: 瀵艰埅椤瑰浘鏍囧拰鏂囨湰姝g‘瀵归綈锛屾棤澶氫綑绌虹櫧 - - **涓€鑷村竷灞€**: 鎵€鏈夊鑸」锛堝崟绾у拰浜岀骇锛夊竷灞€涓€鑷? - **鍔熻兘瀹屾暣**: 灞曞紑/鎶樺彔鍔熻兘姝e父宸ヤ綔 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鍙岄噸鎶樺彔绗﹀彿闂宸茶В鍐?鉁? - 鏍峰紡閿欎贡闂宸蹭慨澶?鉁? - 宸﹁竟绌虹櫧闂宸茶В鍐?鉁? - 浜岀骇瀵艰埅鍔熻兘姝e父 鉁?- **璇存槑**: - - 鎴愬姛瑙e喅浜?TreeView 榛樿鏍峰紡涓庤嚜瀹氫箟甯冨眬鐨勫啿绐侀棶棰? - 鎻愪緵浜嗘竻鏅般€佷竴鑷寸殑浜岀骇瀵艰埅鐣岄潰 - - 淇濇寔浜嗘墍鏈夊鑸姛鑳界殑瀹屾暣鎬? -### 閲嶆瀯浜岀骇瀵艰埅涓?ItemsControl 鏂规 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 灏?TreeView 浜岀骇瀵艰埅閲嶆瀯涓烘洿绠€鍗曞彲闈犵殑 ItemsControl 鏂规 -- **闂鍒嗘瀽**: - - TreeView 鎺т欢澶嶆潅锛屽鏄撳嚭鐜版牱寮忓啿绐佸拰鍙岄噸鎶樺彔绗﹀彿闂 - - Menu 鎺т欢涓嶉€傚悎鍋氫晶杈规爮瀵艰埅锛堥粯璁ゆ按骞冲竷灞€锛? - 闇€瑕佹洿绠€鍗曘€佹洿鍙帶鐨勪簩绾у鑸疄鐜?- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 灏?TreeView 鏀逛负 ItemsControl + ScrollViewer 鏂规 -- **鎶€鏈柟妗?*: - - **涓诲鍣?*: 浣跨敤 `ScrollViewer` + `ItemsControl` 鏇夸唬 TreeView - - **涓诲鑸」**: 姣忎釜瀵艰埅椤逛娇鐢?`Button` 鎺т欢锛屽寘鍚浘鏍囥€佹爣棰樺拰灞曞紑鎸夐挳 - - **瀛愬鑸」**: 浣跨敤宓屽鐨?`ItemsControl` 鏄剧ず瀛愰」锛岄€氳繃 `IsVisible` 鎺у埗鏄剧ず - - **灞曞紑鎺у埗**: 閫氳繃 `IsExpanded` 灞炴€ф帶鍒跺瓙椤圭殑鏄剧ず/闅愯棌 -- **甯冨眬璁捐**: - ```xml - - - - - - - - - - - - - - - - - - ``` -- **鍔熻兘鐗规€?*: - - **鍗曚竴灞曞紑鎸夐挳**: 姣忎釜鏈夊瓙椤圭殑瀵艰埅椤瑰彸渚ф樉绀轰竴涓睍寮€/鎶樺彔鎸夐挳 - - **灞傜骇缂╄繘**: 瀛愬鑸」閫氳繃 `Margin="20,0,0,0"` 瀹炵幇缂╄繘鏁堟灉 - - **瑙嗚鍖哄垎**: 瀛愰」浣跨敤杈冨皬鐨勫瓧浣擄紙13px锛夊拰鐏拌壊鏂囧瓧 - - **婊氬姩鏀寔**: ScrollViewer 鏀寔瀵艰埅椤硅繃澶氭椂鐨勬粴鍔? - **鍝嶅簲寮忎氦浜?*: 鎮仠鏁堟灉鍜岄€変腑鐘舵€佸畬鏁翠繚鐣?- **浼樺娍瀵规瘮**: - - **vs TreeView**: 鏃犳牱寮忓啿绐侊紝鏃犲弻閲嶆姌鍙犵鍙凤紝甯冨眬鏇村彲鎺? - **vs Menu**: 閫傚悎渚ц竟鏍忓瀭鐩村竷灞€锛屾棤寮瑰嚭鑿滃崟骞叉壈 - - **vs ListBox**: 鏀寔宓屽缁撴瀯锛屽睍寮€/鎶樺彔閫昏緫娓呮櫚 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 浜岀骇瀵艰埅姝e父鏄剧ず 鉁? - 灞曞紑/鎶樺彔鍔熻兘姝e父 鉁? - 鏃犳牱寮忓啿绐侀棶棰?鉁? - 甯冨眬娓呮櫚缇庤 鉁?- **璇存槑**: - - 鎴愬姛瑙e喅浜?TreeView 鐨勫鏉傛€ч棶棰? - 鎻愪緵浜嗘洿绠€鍗曘€佹洿鍙帶鐨勪簩绾у鑸疄鐜? - 淇濇寔浜嗘墍鏈夊師鏈夊姛鑳界殑瀹屾暣鎬? -### 浼樺寲浜岀骇瀵艰埅浜や簰閫昏緫 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 浼樺寲浜岀骇瀵艰埅鐨勪氦浜掗€昏緫锛岀Щ闄ゅ睍寮€/鎶樺彔鎸夐挳锛屽疄鐜扮偣鍑昏嚜鍔ㄥ睍寮€骞堕€変腑绗竴涓瓙椤?- **鍔熻兘闇€姹?*: - - 绉婚櫎灞曞紑/鎶樺彔鎸夐挳锛岀畝鍖栫晫闈? - 鐐瑰嚮鏈夊瓙椤圭殑瀵艰埅椤规椂鑷姩灞曞紑 - - 灞曞紑鏃堕粯璁ら€変腑绗竴涓瓙椤? - ContentPresenter 鏄剧ず閫変腑鐨勫瓙椤瑰唴瀹?- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 绉婚櫎灞曞紑/鎶樺彔鎸夐挳锛岀畝鍖栦富瀵艰埅椤瑰竷灞€ - - `ViewModels/MainWindowViewModel.cs` - 浼樺寲 NavigateToPage 鏂规硶锛屾坊鍔犺嚜鍔ㄥ睍寮€閫昏緫 -- **浜や簰閫昏緫**: - - **鏈夊瓙椤圭殑瀵艰埅椤?*: 鐐瑰嚮鏃惰嚜鍔ㄥ睍寮€锛岄€変腑绗竴涓瓙椤癸紝鍒涘缓瀵瑰簲鐨勬爣绛鹃〉 - - **鏃犲瓙椤圭殑瀵艰埅椤?*: 鐩存帴閫変腑锛屽垱寤哄搴旂殑鏍囩椤? - **瀛愰」瀵艰埅**: 鐩存帴閫変腑锛屽垱寤哄搴旂殑鏍囩椤?- **鎶€鏈疄鐜?*: - ```csharp - private void NavigateToPage(NavigationItem navigationItem) - { - // 鍙栨秷鎵€鏈夊鑸」鐨勯€変腑鐘舵€? foreach (var item in _navigationItems) - { - item.IsSelected = false; - foreach (var child in item.Children) - { - child.IsSelected = false; - } - } - - // 濡傛灉鏄湁瀛愰」鐨勫鑸」锛屽睍寮€骞堕€変腑绗竴涓瓙椤? if (navigationItem.HasChildren) - { - navigationItem.IsExpanded = true; - if (navigationItem.Children.Count > 0) - { - var firstChild = navigationItem.Children[0]; - firstChild.IsSelected = true; - SelectedNavigationItem = firstChild; - CreateTabForNavigationItem(firstChild); - } - } - else - { - navigationItem.IsSelected = true; - SelectedNavigationItem = navigationItem; - CreateTabForNavigationItem(navigationItem); - } - } - ``` -- **鐣岄潰浼樺寲**: - - **绉婚櫎灞曞紑鎸夐挳**: 涓诲鑸」鍙樉绀哄浘鏍囧拰鏍囬锛屾棤灞曞紑/鎶樺彔鎸夐挳 - - **绠€鍖栧竷灞€**: 浣跨敤 StackPanel 鏇夸唬澶嶆潅鐨?Grid 甯冨眬 - - **淇濇寔鍔熻兘**: 瀛愰」浠嶇劧閫氳繃 IsVisible 鎺у埗鏄剧ず -- **浠g爜閲嶆瀯**: - - **绉婚櫎 ToggleExpandCommand**: 涓嶅啀闇€瑕佸睍寮€/鎶樺彔鍛戒护 - - **绉婚櫎 ToggleExpand 鏂规硶**: 涓嶅啀闇€瑕佹墜鍔ㄥ垏鎹㈠睍寮€鐘舵€? - **鎻愬彇 CreateTabForNavigationItem 鏂规硶**: 澶嶇敤鏍囩椤靛垱寤洪€昏緫 -- **鐢ㄦ埛浣撻獙**: - - **涓€閿睍寮€**: 鐐瑰嚮"鐢ㄦ埛绠$悊"鑷姩灞曞紑骞堕€変腑"鐢ㄦ埛鍒楄〃" - - **鐩磋鎿嶄綔**: 鏃犻渶棰濆鐨勫睍寮€鎸夐挳锛屾搷浣滄洿鐩磋 - - **榛樿閫変腑**: 灞曞紑鍚庤嚜鍔ㄩ€変腑绗竴涓瓙椤癸紝鍑忓皯鐢ㄦ埛鎿嶄綔 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鐐瑰嚮"鐢ㄦ埛绠$悊"鑷姩灞曞紑骞堕€変腑绗竴涓瓙椤?鉁? - 鐐瑰嚮"绯荤粺璁剧疆"鑷姩灞曞紑骞堕€変腑绗竴涓瓙椤?鉁? - 瀛愰」瀵艰埅姝e父宸ヤ綔 鉁? - 鏍囩椤靛垱寤哄拰绠$悊姝e父 鉁?- **璇存槑**: - - 鎴愬姛浼樺寲浜嗕簩绾у鑸殑浜や簰閫昏緫 - - 鎻愪緵浜嗘洿鐩磋銆佹洿渚挎嵎鐨勭敤鎴蜂綋楠? - 绠€鍖栦簡鐣岄潰璁捐锛屽噺灏戜簡涓嶅繀瑕佺殑鎺т欢 - -### 淇鏈夊瓙绾у鑸」鐨?Content 璁剧疆闂 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 淇鏈夊瓙绾х殑瀵艰埅椤逛笉搴旇璁剧疆 Content 灞炴€х殑闂 -- **闂鍒嗘瀽**: - - "鐢ㄦ埛绠$悊"鍜?绯荤粺璁剧疆"杩欎袱涓湁瀛愮骇鐨勫鑸」璁剧疆浜?Content 灞炴€? - 浣嗗疄闄呬笂鐖剁骇鍙槸鐢ㄦ潵灞曞紑/鎶樺彔鐨勫鍣紝涓嶅簲璇ユ湁鍏蜂綋鍐呭 - - 瀹為檯鐨勫唴瀹瑰簲璇ョ敱瀛愮骇鎻愪緵 - - 褰撶偣鍑荤埗绾ф椂锛屼細鑷姩閫変腑绗竴涓瓙绾э紝鏄剧ず瀛愮骇鐨勫唴瀹?- **淇敼鏂囦欢**: - - `ViewModels/MainWindowViewModel.cs` - 绉婚櫎鏈夊瓙绾у鑸」鐨?Content 璁剧疆 -- **淇鍐呭**: - - **鐢ㄦ埛绠$悊**: 绉婚櫎 `Content = new Views.Pages.UsersPageView { DataContext = new Pages.UsersPageViewModel() }` - - **绯荤粺璁剧疆**: 绉婚櫎 `Content = new Views.Pages.SettingsPageView { DataContext = new Pages.SettingsPageViewModel() }` -- **璁捐鍘熷垯**: - - **鏈夊瓙绾х殑瀵艰埅椤?*: 涓嶈缃?Content锛屽彧浣滀负灞曞紑/鎶樺彔瀹瑰櫒 - - **鏃犲瓙绾х殑瀵艰埅椤?*: 璁剧疆 Content锛屾彁渚涘叿浣撻〉闈㈠唴瀹? - **瀛愮骇瀵艰埅椤?*: 璁剧疆 Content锛屾彁渚涘叿浣撻〉闈㈠唴瀹?- **閫昏緫娴佺▼**: - 1. 鐐瑰嚮"鐢ㄦ埛绠$悊" 鈫?鑷姩灞曞紑 鈫?閫変腑"鐢ㄦ埛鍒楄〃" 鈫?鏄剧ず鐢ㄦ埛鍒楄〃椤甸潰 - 2. 鐐瑰嚮"绯荤粺璁剧疆" 鈫?鑷姩灞曞紑 鈫?閫変腑"甯歌璁剧疆" 鈫?鏄剧ず甯歌璁剧疆椤甸潰 - 3. 鐐瑰嚮"浠〃鏉? 鈫?鐩存帴鏄剧ず浠〃鏉块〉闈紙鏃犲瓙绾э級 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鏈夊瓙绾х殑瀵艰埅椤逛笉鍐嶆湁鍐椾綑鐨?Content 鉁? - 鐐瑰嚮鐖剁骇姝g‘灞曞紑骞堕€変腑绗竴涓瓙绾?鉁? - 瀛愮骇鍐呭姝g‘鏄剧ず 鉁?- **璇存槑**: - - 淇浜嗗鑸」 Content 璁剧疆鐨勯€昏緫闂 - - 鏄庣‘浜嗙埗绾у拰瀛愮骇鐨勮亴璐e垎宸? - 鎻愰珮浜嗕唬鐮佺殑娓呮櫚搴﹀拰鍙淮鎶ゆ€? -### 瀹炵幇瀵艰埅椤逛簰鏂ュ睍寮€鍜岄€変腑鏁堟灉 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 瀹炵幇瀵艰埅椤圭殑浜掓枼灞曞紑閫昏緫鍜屾纭殑閫変腑鏁堟灉 -- **闂鍒嗘瀽**: - - 鐐瑰嚮鏈夊瓙椤圭殑瀵艰埅椤规椂锛屽簲璇ユ敮鎸佸睍寮€/鏀惰捣鍒囨崲 - - 灞曞紑鏃堕粯璁ょ涓€涓瓙椤瑰簲璇ユ湁閫変腑鏁堟灉 - - 澶氫釜瀵艰埅椤逛笉搴旇鍚屾椂灞曞紑锛屽簲璇ヤ簰鏂? - 鏀惰捣鏃跺簲璇ユ竻闄ゆ墍鏈夐€変腑鐘舵€?- **淇敼鏂囦欢**: - - `ViewModels/MainWindowViewModel.cs` - 浼樺寲 NavigateToPage 鏂规硶锛屽疄鐜颁簰鏂ュ睍寮€閫昏緫 -- **浜や簰閫昏緫**: - - **鏈夊瓙椤圭殑瀵艰埅椤?*: - - 鏈睍寮€鏃讹細灞曞紑骞堕€変腑绗竴涓瓙椤? - 宸插睍寮€鏃讹細鏀惰捣骞舵竻闄ら€変腑鐘舵€? - 浜掓枼锛氬睍寮€涓€涓椂鑷姩鏀惰捣鍏朵粬 - - **鏃犲瓙椤圭殑瀵艰埅椤?*锛氱洿鎺ラ€変腑锛屽悓鏃舵敹璧锋墍鏈夊叾浠栧睍寮€椤?- **鎶€鏈疄鐜?*: - ```csharp - private void NavigateToPage(NavigationItem navigationItem) - { - if (navigationItem.HasChildren) - { - if (navigationItem.IsExpanded) - { - // 鏀惰捣閫昏緫 - navigationItem.IsExpanded = false; - // 娓呴櫎鎵€鏈夐€変腑鐘舵€? SelectedNavigationItem = null; - } - else - { - // 灞曞紑閫昏緫 - // 鍏堟敹璧峰叾浠栨墍鏈夊睍寮€鐨勫鑸」 - foreach (var item in _navigationItems) - { - if (item != navigationItem) - { - item.IsExpanded = false; - // 娓呴櫎閫変腑鐘舵€? } - } - - // 灞曞紑褰撳墠椤瑰苟閫変腑绗竴涓瓙椤? navigationItem.IsExpanded = true; - var firstChild = navigationItem.Children[0]; - firstChild.IsSelected = true; - SelectedNavigationItem = firstChild; - } - } - } - ``` -- **鍔熻兘鐗规€?*: - - **浜掓枼灞曞紑**: 鍚屾椂鍙兘鏈変竴涓鑸」灞曞紑 - - **鍒囨崲灞曞紑**: 鐐瑰嚮宸插睍寮€鐨勯」浼氭敹璧? - **鑷姩閫変腑**: 灞曞紑鏃惰嚜鍔ㄩ€変腑绗竴涓瓙椤? - **鐘舵€佺鐞?*: 鏀惰捣鏃舵竻闄ゆ墍鏈夐€変腑鐘舵€? - **鏍囩椤电鐞?*: 閫変腑瀛愰」鏃惰嚜鍔ㄥ垱寤哄搴旀爣绛鹃〉 -- **鐢ㄦ埛浣撻獙**: - - **鐩磋鎿嶄綔**: 鐐瑰嚮"鐢ㄦ埛绠$悊"灞曞紑锛屽啀鐐瑰嚮鏀惰捣 - - **浜掓枼琛屼负**: 灞曞紑"鐢ㄦ埛绠$悊"鏃讹紝"绯荤粺璁剧疆"鑷姩鏀惰捣 - - **閫変腑鍙嶉**: 灞曞紑鍚庣涓€涓瓙椤规湁鏄庢樉鐨勯€変腑鏁堟灉 - - **鐘舵€佷竴鑷?*: 鏀惰捣鏃舵墍鏈夌姸鎬侀兘姝g‘娓呴櫎 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 浜掓枼灞曞紑鍔熻兘姝e父 鉁? - 灞曞紑/鏀惰捣鍒囨崲姝e父 鉁? - 绗竴涓瓙椤归€変腑鏁堟灉姝e父 鉁? - 鐘舵€佺鐞嗘纭?鉁?- **璇存槑**: - - 鎴愬姛瀹炵幇浜嗗鑸」鐨勪簰鏂ュ睍寮€閫昏緫 - - 鎻愪緵浜嗘洿鐩磋銆佹洿绗﹀悎鐢ㄦ埛涔犳儻鐨勪氦浜掍綋楠? - 瀹屽杽浜嗙姸鎬佺鐞嗗拰閫変腑鏁堟灉 - -### 淇绐楀彛鎺у埗鎸夐挳 HeroIcons 鏍峰紡閫夋嫨鍣ㄩ棶棰?- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 淇 HeroIcons 鍦?Style 閫夋嫨鍣ㄤ腑鐨勫懡鍚嶇┖闂磋В鏋愰棶棰?- **淇敼鍘熷洜**: 缂栬瘧鏃跺嚭鐜?"Unable to resolve type heroicons from namespace" 閿欒 -- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 绠€鍖栧叧闂寜閽殑鏍峰紡澶勭悊 -- **闂鎻忚堪**: - - **閿欒淇℃伅**: `Unable to resolve type heroicons from namespace https://github.com/avaloniaui` - - **閿欒浣嶇疆**: 绗?46琛屽拰绗?49琛岀殑 Style 閫夋嫨鍣? - **鏍规湰鍘熷洜**: Avalonia 鐨?Style 閫夋嫨鍣ㄤ笉鏀寔甯﹀懡鍚嶇┖闂村墠缂€鐨勭被鍨嬮€夋嫨鍣?- **瑙e喅鏂规**: - - **绉婚櫎澶嶆潅鏍峰紡**: 鍒犻櫎 `heroicons:HeroIcon.Styles` 鍜岀浉鍏崇殑澶嶆潅鏍峰紡閫夋嫨鍣? - **绠€鍖栧疄鐜?*: 鍙繚鐣?Button 鐨勬偓鍋滆儗鏅壊鍙樺寲 - - **淇濇寔鍔熻兘**: 鍏抽棴鎸夐挳鐨勭孩鑹叉偓鍋滆儗鏅晥鏋滀粛鐒舵甯?- **鎶€鏈粏鑺?*: - ```xml - - - - - - - - - - - ``` -- **鍔熻兘淇濇寔**: - - **鏈€灏忓寲鎸夐挳**: 姝e父鏄剧ず鍜屼氦浜?鉁? - **鏈€澶у寲鎸夐挳**: 姝e父鏄剧ず鍜屼氦浜?鉁? - **鍏抽棴鎸夐挳**: 姝e父鏄剧ず鍜屼氦浜?鉁? - **鎮仠鏁堟灉**: 鑳屾櫙鑹插彉鍖栨甯?鉁? - **宸ュ叿鎻愮ず**: 鍔熻兘瀹屾暣 鉁?- **鏋舵瀯浼樺娍**: - - **缂栬瘧鎴愬姛**: 瑙e喅浜嗗懡鍚嶇┖闂磋В鏋愰棶棰? - **浠g爜绠€娲?*: 绉婚櫎浜嗗鏉傜殑鏍峰紡閫夋嫨鍣? - **鍔熻兘瀹屾暣**: 鎵€鏈夋寜閽姛鑳芥甯? - **缁存姢鎬уソ**: 浠g爜缁撴瀯鏇寸畝鍗曟竻鏅?- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 绐楀彛鎺у埗鎸夐挳姝e父鏄剧ず鍜屼氦浜?鉁? - 鎮仠鏁堟灉姝e父 鉁?- **璇存槑**: - - 鎴愬姛淇浜?HeroIcons 鏍峰紡閫夋嫨鍣ㄧ殑鍛藉悕绌洪棿闂 - - 淇濇寔浜嗘墍鏈夌獥鍙f帶鍒舵寜閽殑鍔熻兘瀹屾暣鎬? - 鎻愪緵浜嗘洿绠€娲併€佹洿绋冲畾鐨勪唬鐮佸疄鐜? -### 閲嶆瀯鏍囩椤靛叧闂寜閽娇鐢?HeroIcons -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 灏嗘爣绛鹃〉鍏抽棴鎸夐挳浠?Path 缁樺埗鏀逛负浣跨敤 HeroIcons -- **淇敼鍘熷洜**: 鐢ㄦ埛寤鸿浣跨敤 HeroIcons 鏇夸唬 Path 缁樺埗锛屾彁渚涙洿缁熶竴鐨勫浘鏍囩郴缁?- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 鏇存柊鏍囩椤靛叧闂寜閽娇鐢?HeroIcon 鎺т欢 -- **鍥炬爣鏄犲皠**: - - **鏍囩椤靛叧闂寜閽?*: `IconType.XMark` (鍘?Path X 褰? -- **鎶€鏈疄鐜?*: - ```xml - - - - - - ``` -- **鏍峰紡淇濇寔**: - - **灏哄涓€鑷?*: 淇濇寔 6x6 鍍忕礌鐨勫浘鏍囧昂瀵? - **棰滆壊缁戝畾**: 浣跨敤 `$parent[Button].Foreground` 缁戝畾淇濇寔鍔ㄦ€侀鑹? - **瀵归綈鏂瑰紡**: 淇濇寔灞呬腑瀵归綈 - - **鎮仠鏁堟灉**: 淇濇寔鍘熸湁鐨勬偓鍋滆儗鏅壊鍜屽墠鏅壊鍙樺寲 -- **鍔熻兘淇濇寔**: - - **鍏抽棴鍔熻兘**: 鏍囩椤靛叧闂姛鑳藉畬鍏ㄦ甯? - **鍙鎬?*: `IsVisible="{Binding CanClose}"` 鏉′欢鏄剧ず姝e父 - - **宸ュ叿鎻愮ず**: `tooltip:ToolTip.Tip` 鍔熻兘姝e父 - - **鍛戒护缁戝畾**: `CloseTabCommand` 鍜?`CommandParameter` 姝e父 - - **鎮仠鏁堟灉**: 鑳屾櫙鑹插拰鍓嶆櫙鑹插彉鍖栨甯?- **鏋舵瀯浼樺娍**: - - **缁熶竴鍥炬爣绯荤粺**: 涓庣獥鍙f帶鍒舵寜閽娇鐢ㄧ浉鍚岀殑 XMark 鍥炬爣 - - **绠€鍖栦唬鐮?*: 绉婚櫎澶嶆潅鐨?Path 缁樺埗浠g爜 - - **鏄撲簬缁存姢**: 鍥炬爣绠$悊鏇撮泦涓紝鏄撲簬鏇存崲鍜屾洿鏂? - **绫诲瀷瀹夊叏**: 浣跨敤鏋氫妇绫诲瀷锛岀紪璇戞椂妫€鏌ュ浘鏍囨湁鏁堟€? - **鐜颁唬鍖?*: HeroIcons 鎻愪緵鏇寸幇浠c€佹洿涓撲笟鐨勫浘鏍囪璁?- **瑙嗚鏁堟灉**: - - 鍥炬爣鏇村姞缁熶竴鍜屼笓涓? - 鏀寔楂楧PI鏄剧ず - - 涓庢暣浣?UI 璁捐椋庢牸涓€鑷? - 鎻愪緵鏇村ソ鐨勭敤鎴蜂綋楠?- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鏍囩椤靛叧闂寜閽甯告樉绀哄拰浜や簰 鉁? - 鎮仠鏁堟灉鍜岄鑹插彉鍖栨甯?鉁? - 鍏抽棴鍔熻兘姝e父宸ヤ綔 鉁?- **璇存槑**: - - 鎴愬姛灏嗘爣绛鹃〉鍏抽棴鎸夐挳閲嶆瀯涓轰娇鐢?HeroIcons - - 鎻愪緵浜嗘洿缁熶竴銆佹洿涓撲笟鐨勫浘鏍囦綋楠? - 绠€鍖栦簡浠g爜缁撴瀯锛屾彁楂樹簡鍙淮鎶ゆ€? - 瀹屽叏绗﹀悎鐜颁唬 UI 璁捐鏍囧噯 - -### 淇瀵艰埅鑿滃崟涓浘鏍囦笉鏄剧ず闂 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 淇瀵艰埅鑿滃崟涓?HeroIcons 鍥炬爣涓嶆樉绀虹殑闂 -- **闂鎻忚堪**: - - 瀵艰埅鑿滃崟涓殑鍥炬爣鏃犳硶鏄剧ず锛屽叾浠栧湴鏂圭殑 HeroIcons 閮借兘姝e父鏄剧ず - - 闂鍑虹幇鍦ㄥ鑸彍鍗曠殑 Button.Content 缁戝畾涓?- **鏍规湰鍘熷洜**: - - Button.Content 琚缃簡涓ゆ锛氱涓€娆″湪绗?3琛?`Content="{Binding Title}"`锛岀浜屾鍦ㄧ89-96琛岄€氳繃 `` 鏍囩閲嶆柊瀹氫箟 - - 杩欏鑷翠簡鍐呭琚鐩栵紝鍥炬爣鏃犳硶鏄剧ず -- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 淇瀵艰埅鑿滃崟鐨?Button.Content 閲嶅璁剧疆闂 -- **瑙e喅鏂规**: - - 绉婚櫎绗竴娆$殑 `Content="{Binding Title}"` 璁剧疆 - - 鍙繚鐣?`` 鏍囩涓殑 StackPanel 鍐呭 - - 纭繚鍥炬爣鍜屾枃鏈兘鑳芥纭樉绀?- **鎶€鏈疄鐜?*: - ```xml - - - - - - ``` -- **鍔熻兘淇濇寔**: - - **瀵艰埅鍔熻兘**: 瀵艰埅鍛戒护鍜屽弬鏁扮粦瀹氬畬鍏ㄦ甯? - **鍥炬爣鏄剧ず**: HeroIcons 鍥炬爣鐜板湪鑳芥纭樉绀? - **鏂囨湰鏄剧ず**: 瀵艰埅椤规爣棰樻甯告樉绀? - **鏍峰紡鏁堟灉**: 鎮仠鏁堟灉鍜岄€変腑鐘舵€佹甯? - **甯冨眬瀵归綈**: StackPanel 鐨勬按骞冲竷灞€鍜岄棿璺濇甯?- **鏋舵瀯浼樺娍**: - - **鍐呭缁熶竴**: 鍥炬爣鍜屾枃鏈湪鍚屼竴涓?StackPanel 涓紝甯冨眬涓€鑷? - **缁戝畾姝g‘**: 鍥炬爣绫诲瀷鍜屾爣棰橀兘姝g‘缁戝畾鍒?ViewModel - - **浠g爜绠€娲?*: 绉婚櫎浜嗛噸澶嶇殑鍐呭璁剧疆 - - **缁存姢鎬уソ**: 鍐呭缁撴瀯鏇存竻鏅帮紝鏄撲簬鐞嗚В鍜岀淮鎶?- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 瀵艰埅鑿滃崟鍥炬爣姝e父鏄剧ず 鉁? - 瀵艰埅鍔熻兘姝e父宸ヤ綔 鉁? - 鎮仠鏁堟灉鍜岄€変腑鐘舵€佹甯?鉁?- **璇存槑**: - - 鎴愬姛淇浜嗗鑸彍鍗曚腑鍥炬爣涓嶆樉绀虹殑闂 - - 瑙e喅浜?Button.Content 閲嶅璁剧疆瀵艰嚧鐨勫唴瀹硅鐩栭棶棰? - 纭繚浜嗗鑸彍鍗曠殑瀹屾暣鍔熻兘鍜岃瑙夋晥鏋? - 淇濇寔浜嗕笌鍏朵粬 HeroIcons 浣跨敤鐨勪竴鑷存€? -### 瀹炵幇 NavigationItem 浜岀骇瀵艰埅鏀寔 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 涓?NavigationItem 娣诲姞浜岀骇瀵艰埅鏀寔锛屽寘鎷?Children 灞炴€у拰 IsExpanded 鐘舵€佺鐞?- **鍔熻兘闇€姹?*: NavigationItem 鑷冲皯闇€瑕佹敮鎸佷簩绾у鑸紝鍏佽鍒涘缓灞傜骇缁撴瀯鐨勫鑸彍鍗?- **淇敼鏂囦欢**: - - `ViewModels/NavigationItem.cs` - 娣诲姞 Children 闆嗗悎鍜?IsExpanded 灞炴€? - `ViewModels/MainWindowViewModel.cs` - 娣诲姞 ToggleExpandCommand 鍜屼簩绾у鑸垵濮嬪寲 - - `MainWindow.axaml` - 灏?ListBox 鏀逛负 TreeView 鏀寔灞傜骇鏄剧ず - - `Converters/StringConverters.cs` - 娣诲姞 BoolToIconConverter 杞崲鍣?- **鎶€鏈疄鐜?*: - - **Children 灞炴€?*: `ObservableCollection Children` 鏀寔瀛愬鑸」 - - **IsExpanded 灞炴€?*: `bool IsExpanded` 鎺у埗灞曞紑/鎶樺彔鐘舵€? - **HasChildren 灞炴€?*: `bool HasChildren => Children?.Count > 0` 鍒ゆ柇鏄惁鏈夊瓙椤? - **ToggleExpandCommand**: `ReactiveCommand` 澶勭悊灞曞紑/鎶樺彔鎿嶄綔 - - **TreeView 鎺т欢**: 浣跨敤 TreeDataTemplate 鏀寔灞傜骇鏁版嵁缁戝畾 -- **浜岀骇瀵艰埅绀轰緥**: - - **鐢ㄦ埛绠$悊**: 鍖呭惈鐢ㄦ埛鍒楄〃銆佽鑹茬鐞嗐€佹潈闄愯缃笁涓瓙椤? - **绯荤粺璁剧疆**: 鍖呭惈甯歌璁剧疆銆佸畨鍏ㄨ缃€佸浠芥仮澶嶄笁涓瓙椤? - **鍏朵粬瀵艰埅椤?*: 淇濇寔鍗曠骇缁撴瀯 -- **UI 璁捐**: - - **灞曞紑/鎶樺彔鎸夐挳**: 浣跨敤 ChevronDown/ChevronRight 鍥炬爣 - - **灞傜骇缂╄繘**: TreeView 鑷姩澶勭悊灞傜骇缂╄繘 - - **鐘舵€佺鐞?*: 灞曞紑鐘舵€佷笌閫変腑鐘舵€佺嫭绔嬬鐞? - **浜や簰浣撻獙**: 鐐瑰嚮灞曞紑鎸夐挳鍒囨崲鐘舵€侊紝鐐瑰嚮瀵艰埅椤规墽琛屽鑸?- **杞崲鍣ㄦ敮鎸?*: - - **BoolToIconConverter**: 灏嗗竷灏斿€艰浆鎹负灞曞紑/鎶樺彔鍥炬爣 - - **鍥炬爣鏄犲皠**: true 鈫?ChevronDown, false 鈫?ChevronRight -- **鏋舵瀯浼樺娍**: - - **鍙墿灞曟€?*: 鏀寔浠绘剰灞傜骇鐨勫鑸粨鏋? - **鐘舵€佺鐞?*: 瀹屾暣鐨勫睍寮€/鎶樺彔鐘舵€佺鐞? - **鍝嶅簲寮?*: 浣跨敤 ReactiveUI 杩涜鐘舵€佺粦瀹? - **绫诲瀷瀹夊叏**: 浣跨敤寮虹被鍨嬪睘鎬э紝缂栬瘧鏃舵鏌?- **鍔熻兘淇濇寔**: - - 瀵艰埅鍔熻兘瀹屽叏姝e父 - - 鏍囩椤靛垱寤哄拰绠$悊姝e父 - - 鍥炬爣鏄剧ず鍜屼氦浜掓甯? - 澶氳瑷€鏀寔淇濇寔 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 浜岀骇瀵艰埅姝e父鏄剧ず鍜屼氦浜?鉁? - 灞曞紑/鎶樺彔鍔熻兘姝e父 鉁? - 瀵艰埅鍜屾爣绛鹃〉鍔熻兘瀹屾暣 鉁?- **璇存槑**: - - 鎴愬姛瀹炵幇浜?NavigationItem 鐨勪簩绾у鑸敮鎸? - 鎻愪緵浜嗗畬鏁寸殑灞傜骇瀵艰埅瑙e喅鏂规 - - 寤虹珛浜嗗彲鎵╁睍鐨勫鑸灦鏋? - 涓哄鏉傚簲鐢ㄦ彁渚涗簡鐏垫椿鐨勫鑸鐞嗚兘鍔? -### 淇 TreeView 浜岀骇瀵艰埅鏍峰紡闂 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 淇 TreeView 浜岀骇瀵艰埅涓嚭鐜扮殑鍙岄噸鎶樺彔绗﹀彿鍜屾牱寮忛敊涔遍棶棰?- **闂鎻忚堪**: - - TreeView 榛樿鏄剧ず灞曞紑/鎶樺彔鎸夐挳锛屼笌鑷畾涔夋寜閽啿绐佸鑷村嚭鐜颁袱涓姌鍙犵鍙凤紙>>锛? - TreeViewItem 榛樿缂╄繘瀵艰嚧宸﹁竟鍑虹幇澶ч噺绌虹櫧 - - 瀵艰埅椤瑰浘鏍囧拰鏂囨湰瀵归綈閿欎贡 -- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 娣诲姞 TreeView 鏍峰紡瑕嗙洊锛屼慨澶嶅竷灞€闂 -- **瑙e喅鏂规**: - - **闅愯棌榛樿鎸夐挳**: 浣跨敤鏍峰紡閫夋嫨鍣?`TreeViewItem /template/ ToggleButton` 闅愯棌 TreeView 榛樿鐨勫睍寮€/鎶樺彔鎸夐挳 - - **绉婚櫎榛樿缂╄繘**: 璁剧疆 `TreeViewItem` 鐨?`Margin` 鍜?`Padding` 涓?0 - - **璋冩暣甯冨眬**: 灏?`Margin="0,2"` 绉诲埌 Grid 涓婏紝纭繚闂磋窛涓€鑷?- **鎶€鏈疄鐜?*: - ```xml - - - - - - - ``` -- **淇鏁堟灉**: - - **鍗曚竴鎶樺彔绗﹀彿**: 鍙樉绀鸿嚜瀹氫箟鐨勫睍寮€/鎶樺彔鎸夐挳锛屼笉鍐嶆湁閲嶅绗﹀彿 - - **姝g‘瀵归綈**: 瀵艰埅椤瑰浘鏍囧拰鏂囨湰姝g‘瀵归綈锛屾棤澶氫綑绌虹櫧 - - **涓€鑷村竷灞€**: 鎵€鏈夊鑸」锛堝崟绾у拰浜岀骇锛夊竷灞€涓€鑷? - **鍔熻兘瀹屾暣**: 灞曞紑/鎶樺彔鍔熻兘姝e父宸ヤ綔 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鍙岄噸鎶樺彔绗﹀彿闂宸茶В鍐?鉁? - 鏍峰紡閿欎贡闂宸蹭慨澶?鉁? - 宸﹁竟绌虹櫧闂宸茶В鍐?鉁? - 浜岀骇瀵艰埅鍔熻兘姝e父 鉁?- **璇存槑**: - - 鎴愬姛瑙e喅浜?TreeView 榛樿鏍峰紡涓庤嚜瀹氫箟甯冨眬鐨勫啿绐侀棶棰? - 鎻愪緵浜嗘竻鏅般€佷竴鑷寸殑浜岀骇瀵艰埅鐣岄潰 - - 淇濇寔浜嗘墍鏈夊鑸姛鑳界殑瀹屾暣鎬? -### 閲嶆瀯浜岀骇瀵艰埅涓?ItemsControl 鏂规 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 灏?TreeView 浜岀骇瀵艰埅閲嶆瀯涓烘洿绠€鍗曞彲闈犵殑 ItemsControl 鏂规 -- **闂鍒嗘瀽**: - - TreeView 鎺т欢澶嶆潅锛屽鏄撳嚭鐜版牱寮忓啿绐佸拰鍙岄噸鎶樺彔绗﹀彿闂 - - Menu 鎺т欢涓嶉€傚悎鍋氫晶杈规爮瀵艰埅锛堥粯璁ゆ按骞冲竷灞€锛? - 闇€瑕佹洿绠€鍗曘€佹洿鍙帶鐨勪簩绾у鑸疄鐜?- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 灏?TreeView 鏀逛负 ItemsControl + ScrollViewer 鏂规 -- **鎶€鏈柟妗?*: - - **涓诲鍣?*: 浣跨敤 `ScrollViewer` + `ItemsControl` 鏇夸唬 TreeView - - **涓诲鑸」**: 姣忎釜瀵艰埅椤逛娇鐢?`Button` 鎺т欢锛屽寘鍚浘鏍囥€佹爣棰樺拰灞曞紑鎸夐挳 - - **瀛愬鑸」**: 浣跨敤宓屽鐨?`ItemsControl` 鏄剧ず瀛愰」锛岄€氳繃 `IsVisible` 鎺у埗鏄剧ず - - **灞曞紑鎺у埗**: 閫氳繃 `IsExpanded` 灞炴€ф帶鍒跺瓙椤圭殑鏄剧ず/闅愯棌 -- **甯冨眬璁捐**: - ```xml - - - - - - - - - - - - - - - - - - ``` -- **鍔熻兘鐗规€?*: - - **鍗曚竴灞曞紑鎸夐挳**: 姣忎釜鏈夊瓙椤圭殑瀵艰埅椤瑰彸渚ф樉绀轰竴涓睍寮€/鎶樺彔鎸夐挳 - - **灞傜骇缂╄繘**: 瀛愬鑸」閫氳繃 `Margin="20,0,0,0"` 瀹炵幇缂╄繘鏁堟灉 - - **瑙嗚鍖哄垎**: 瀛愰」浣跨敤杈冨皬鐨勫瓧浣擄紙13px锛夊拰鐏拌壊鏂囧瓧 - - **婊氬姩鏀寔**: ScrollViewer 鏀寔瀵艰埅椤硅繃澶氭椂鐨勬粴鍔? - **鍝嶅簲寮忎氦浜?*: 鎮仠鏁堟灉鍜岄€変腑鐘舵€佸畬鏁翠繚鐣?- **浼樺娍瀵规瘮**: - - **vs TreeView**: 鏃犳牱寮忓啿绐侊紝鏃犲弻閲嶆姌鍙犵鍙凤紝甯冨眬鏇村彲鎺? - **vs Menu**: 閫傚悎渚ц竟鏍忓瀭鐩村竷灞€锛屾棤寮瑰嚭鑿滃崟骞叉壈 - - **vs ListBox**: 鏀寔宓屽缁撴瀯锛屽睍寮€/鎶樺彔閫昏緫娓呮櫚 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 浜岀骇瀵艰埅姝e父鏄剧ず 鉁? - 灞曞紑/鎶樺彔鍔熻兘姝e父 鉁? - 鏃犳牱寮忓啿绐侀棶棰?鉁? - 甯冨眬娓呮櫚缇庤 鉁?- **璇存槑**: - - 鎴愬姛瑙e喅浜?TreeView 鐨勫鏉傛€ч棶棰? - 鎻愪緵浜嗘洿绠€鍗曘€佹洿鍙帶鐨勪簩绾у鑸疄鐜? - 淇濇寔浜嗘墍鏈夊師鏈夊姛鑳界殑瀹屾暣鎬? -### 浼樺寲浜岀骇瀵艰埅浜や簰閫昏緫 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 浼樺寲浜岀骇瀵艰埅鐨勪氦浜掗€昏緫锛岀Щ闄ゅ睍寮€/鎶樺彔鎸夐挳锛屽疄鐜扮偣鍑昏嚜鍔ㄥ睍寮€骞堕€変腑绗竴涓瓙椤?- **鍔熻兘闇€姹?*: - - 绉婚櫎灞曞紑/鎶樺彔鎸夐挳锛岀畝鍖栫晫闈? - 鐐瑰嚮鏈夊瓙椤圭殑瀵艰埅椤规椂鑷姩灞曞紑 - - 灞曞紑鏃堕粯璁ら€変腑绗竴涓瓙椤? - ContentPresenter 鏄剧ず閫変腑鐨勫瓙椤瑰唴瀹?- **淇敼鏂囦欢**: - - `MainWindow.axaml` - 绉婚櫎灞曞紑/鎶樺彔鎸夐挳锛岀畝鍖栦富瀵艰埅椤瑰竷灞€ - - `ViewModels/MainWindowViewModel.cs` - 浼樺寲 NavigateToPage 鏂规硶锛屾坊鍔犺嚜鍔ㄥ睍寮€閫昏緫 -- **浜や簰閫昏緫**: - - **鏈夊瓙椤圭殑瀵艰埅椤?*: 鐐瑰嚮鏃惰嚜鍔ㄥ睍寮€锛岄€変腑绗竴涓瓙椤癸紝鍒涘缓瀵瑰簲鐨勬爣绛鹃〉 - - **鏃犲瓙椤圭殑瀵艰埅椤?*: 鐩存帴閫変腑锛屽垱寤哄搴旂殑鏍囩椤? - **瀛愰」瀵艰埅**: 鐩存帴閫変腑锛屽垱寤哄搴旂殑鏍囩椤?- **鎶€鏈疄鐜?*: - ```csharp - private void NavigateToPage(NavigationItem navigationItem) - { - // 鍙栨秷鎵€鏈夊鑸」鐨勯€変腑鐘舵€? foreach (var item in _navigationItems) - { - item.IsSelected = false; - foreach (var child in item.Children) - { - child.IsSelected = false; - } - } - - // 濡傛灉鏄湁瀛愰」鐨勫鑸」锛屽睍寮€骞堕€変腑绗竴涓瓙椤? if (navigationItem.HasChildren) - { - navigationItem.IsExpanded = true; - if (navigationItem.Children.Count > 0) - { - var firstChild = navigationItem.Children[0]; - firstChild.IsSelected = true; - SelectedNavigationItem = firstChild; - CreateTabForNavigationItem(firstChild); - } - } - else - { - navigationItem.IsSelected = true; - SelectedNavigationItem = navigationItem; - CreateTabForNavigationItem(navigationItem); - } - } - ``` -- **鐣岄潰浼樺寲**: - - **绉婚櫎灞曞紑鎸夐挳**: 涓诲鑸」鍙樉绀哄浘鏍囧拰鏍囬锛屾棤灞曞紑/鎶樺彔鎸夐挳 - - **绠€鍖栧竷灞€**: 浣跨敤 StackPanel 鏇夸唬澶嶆潅鐨?Grid 甯冨眬 - - **淇濇寔鍔熻兘**: 瀛愰」浠嶇劧閫氳繃 IsVisible 鎺у埗鏄剧ず -- **浠g爜閲嶆瀯**: - - **绉婚櫎 ToggleExpandCommand**: 涓嶅啀闇€瑕佸睍寮€/鎶樺彔鍛戒护 - - **绉婚櫎 ToggleExpand 鏂规硶**: 涓嶅啀闇€瑕佹墜鍔ㄥ垏鎹㈠睍寮€鐘舵€? - **鎻愬彇 CreateTabForNavigationItem 鏂规硶**: 澶嶇敤鏍囩椤靛垱寤洪€昏緫 -- **鐢ㄦ埛浣撻獙**: - - **涓€閿睍寮€**: 鐐瑰嚮"鐢ㄦ埛绠$悊"鑷姩灞曞紑骞堕€変腑"鐢ㄦ埛鍒楄〃" - - **鐩磋鎿嶄綔**: 鏃犻渶棰濆鐨勫睍寮€鎸夐挳锛屾搷浣滄洿鐩磋 - - **榛樿閫変腑**: 灞曞紑鍚庤嚜鍔ㄩ€変腑绗竴涓瓙椤癸紝鍑忓皯鐢ㄦ埛鎿嶄綔 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鐐瑰嚮"鐢ㄦ埛绠$悊"鑷姩灞曞紑骞堕€変腑绗竴涓瓙椤?鉁? - 鐐瑰嚮"绯荤粺璁剧疆"鑷姩灞曞紑骞堕€変腑绗竴涓瓙椤?鉁? - 瀛愰」瀵艰埅姝e父宸ヤ綔 鉁? - 鏍囩椤靛垱寤哄拰绠$悊姝e父 鉁?- **璇存槑**: - - 鎴愬姛浼樺寲浜嗕簩绾у鑸殑浜や簰閫昏緫 - - 鎻愪緵浜嗘洿鐩磋銆佹洿渚挎嵎鐨勭敤鎴蜂綋楠? - 绠€鍖栦簡鐣岄潰璁捐锛屽噺灏戜簡涓嶅繀瑕佺殑鎺т欢 - -### 淇鏈夊瓙绾у鑸」鐨?Content 璁剧疆闂 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 淇鏈夊瓙绾х殑瀵艰埅椤逛笉搴旇璁剧疆 Content 灞炴€х殑闂 -- **闂鍒嗘瀽**: - - "鐢ㄦ埛绠$悊"鍜?绯荤粺璁剧疆"杩欎袱涓湁瀛愮骇鐨勫鑸」璁剧疆浜?Content 灞炴€? - 浣嗗疄闄呬笂鐖剁骇鍙槸鐢ㄦ潵灞曞紑/鎶樺彔鐨勫鍣紝涓嶅簲璇ユ湁鍏蜂綋鍐呭 - - 瀹為檯鐨勫唴瀹瑰簲璇ョ敱瀛愮骇鎻愪緵 - - 褰撶偣鍑荤埗绾ф椂锛屼細鑷姩閫変腑绗竴涓瓙绾э紝鏄剧ず瀛愮骇鐨勫唴瀹?- **淇敼鏂囦欢**: - - `ViewModels/MainWindowViewModel.cs` - 绉婚櫎鏈夊瓙绾у鑸」鐨?Content 璁剧疆 -- **淇鍐呭**: - - **鐢ㄦ埛绠$悊**: 绉婚櫎 `Content = new Views.Pages.UsersPageView { DataContext = new Pages.UsersPageViewModel() }` - - **绯荤粺璁剧疆**: 绉婚櫎 `Content = new Views.Pages.SettingsPageView { DataContext = new Pages.SettingsPageViewModel() }` -- **璁捐鍘熷垯**: - - **鏈夊瓙绾х殑瀵艰埅椤?*: 涓嶈缃?Content锛屽彧浣滀负灞曞紑/鎶樺彔瀹瑰櫒 - - **鏃犲瓙绾х殑瀵艰埅椤?*: 璁剧疆 Content锛屾彁渚涘叿浣撻〉闈㈠唴瀹? - **瀛愮骇瀵艰埅椤?*: 璁剧疆 Content锛屾彁渚涘叿浣撻〉闈㈠唴瀹?- **閫昏緫娴佺▼**: - 1. 鐐瑰嚮"鐢ㄦ埛绠$悊" 鈫?鑷姩灞曞紑 鈫?閫変腑"鐢ㄦ埛鍒楄〃" 鈫?鏄剧ず鐢ㄦ埛鍒楄〃椤甸潰 - 2. 鐐瑰嚮"绯荤粺璁剧疆" 鈫?鑷姩灞曞紑 鈫?閫変腑"甯歌璁剧疆" 鈫?鏄剧ず甯歌璁剧疆椤甸潰 - 3. 鐐瑰嚮"浠〃鏉? 鈫?鐩存帴鏄剧ず浠〃鏉块〉闈紙鏃犲瓙绾э級 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 鏈夊瓙绾х殑瀵艰埅椤逛笉鍐嶆湁鍐椾綑鐨?Content 鉁? - 鐐瑰嚮鐖剁骇姝g‘灞曞紑骞堕€変腑绗竴涓瓙绾?鉁? - 瀛愮骇鍐呭姝g‘鏄剧ず 鉁?- **璇存槑**: - - 淇浜嗗鑸」 Content 璁剧疆鐨勯€昏緫闂 - - 鏄庣‘浜嗙埗绾у拰瀛愮骇鐨勮亴璐e垎宸? - 鎻愰珮浜嗕唬鐮佺殑娓呮櫚搴﹀拰鍙淮鎶ゆ€? -### 瀹炵幇瀵艰埅椤逛簰鏂ュ睍寮€鍜岄€変腑鏁堟灉 -- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 瀹炵幇瀵艰埅椤圭殑浜掓枼灞曞紑閫昏緫鍜屾纭殑閫変腑鏁堟灉 -- **闂鍒嗘瀽**: - - 鐐瑰嚮鏈夊瓙椤圭殑瀵艰埅椤规椂锛屽簲璇ユ敮鎸佸睍寮€/鏀惰捣鍒囨崲 - - 灞曞紑鏃堕粯璁ょ涓€涓瓙椤瑰簲璇ユ湁閫変腑鏁堟灉 - - 澶氫釜瀵艰埅椤逛笉搴旇鍚屾椂灞曞紑锛屽簲璇ヤ簰鏂? - 鏀惰捣鏃跺簲璇ユ竻闄ゆ墍鏈夐€変腑鐘舵€?- **淇敼鏂囦欢**: - - `ViewModels/MainWindowViewModel.cs` - 浼樺寲 NavigateToPage 鏂规硶锛屽疄鐜颁簰鏂ュ睍寮€閫昏緫 -- **浜や簰閫昏緫**: - - **鏈夊瓙椤圭殑瀵艰埅椤?*: - - 鏈睍寮€鏃讹細灞曞紑骞堕€変腑绗竴涓瓙椤? - 宸插睍寮€鏃讹細鏀惰捣骞舵竻闄ら€変腑鐘舵€? - 浜掓枼锛氬睍寮€涓€涓椂鑷姩鏀惰捣鍏朵粬 - - **鏃犲瓙椤圭殑瀵艰埅椤?*锛氱洿鎺ラ€変腑锛屽悓鏃舵敹璧锋墍鏈夊叾浠栧睍寮€椤?- **鎶€鏈疄鐜?*: - ```csharp - private void NavigateToPage(NavigationItem navigationItem) - { - if (navigationItem.HasChildren) - { - if (navigationItem.IsExpanded) - { - // 鏀惰捣閫昏緫 - navigationItem.IsExpanded = false; - // 娓呴櫎鎵€鏈夐€変腑鐘舵€? SelectedNavigationItem = null; - } - else - { - // 灞曞紑閫昏緫 - // 鍏堟敹璧峰叾浠栨墍鏈夊睍寮€鐨勫鑸」 - foreach (var item in _navigationItems) - { - if (item != navigationItem) - { - item.IsExpanded = false; - // 娓呴櫎閫変腑鐘舵€? } - } - - // 灞曞紑褰撳墠椤瑰苟閫変腑绗竴涓瓙椤? navigationItem.IsExpanded = true; - var firstChild = navigationItem.Children[0]; - firstChild.IsSelected = true; - SelectedNavigationItem = firstChild; - } - } - } - ``` -- **鍔熻兘鐗规€?*: - - **浜掓枼灞曞紑**: 鍚屾椂鍙兘鏈変竴涓鑸」灞曞紑 - - **鍒囨崲灞曞紑**: 鐐瑰嚮宸插睍寮€鐨勯」浼氭敹璧? - **鑷姩閫変腑**: 灞曞紑鏃惰嚜鍔ㄩ€変腑绗竴涓瓙椤? - **鐘舵€佺鐞?*: 鏀惰捣鏃舵竻闄ゆ墍鏈夐€変腑鐘舵€? - **鏍囩椤电鐞?*: 閫変腑瀛愰」鏃惰嚜鍔ㄥ垱寤哄搴旀爣绛鹃〉 -- **鐢ㄦ埛浣撻獙**: - - **鐩磋鎿嶄綔**: 鐐瑰嚮"鐢ㄦ埛绠$悊"灞曞紑锛屽啀鐐瑰嚮鏀惰捣 - - **浜掓枼琛屼负**: 灞曞紑"鐢ㄦ埛绠$悊"鏃讹紝"绯荤粺璁剧疆"鑷姩鏀惰捣 - - **閫変腑鍙嶉**: 灞曞紑鍚庣涓€涓瓙椤规湁鏄庢樉鐨勯€変腑鏁堟灉 - - **鐘舵€佷竴鑷?*: 鏀惰捣鏃舵墍鏈夌姸鎬侀兘姝g‘娓呴櫎 -- **娴嬭瘯缁撴灉**: - - 缂栬瘧鎴愬姛锛屾棤閿欒 鉁? - 浜掓枼灞曞紑鍔熻兘姝e父 鉁? - 灞曞紑/鏀惰捣鍒囨崲姝e父 鉁? - 绗竴涓瓙椤归€変腑鏁堟灉姝e父 鉁? - 鐘舵€佺鐞嗘纭?鉁?- **璇存槑**: - - 鎴愬姛瀹炵幇浜嗗鑸」鐨勪簰鏂ュ睍寮€閫昏緫 - - 鎻愪緵浜嗘洿鐩磋銆佹洿绗﹀悎鐢ㄦ埛涔犳儻鐨勪氦浜掍綋楠? - 瀹屽杽浜嗙姸鎬佺鐞嗗拰閫変腑鏁堟灉 - -### 濞h濮?AvaloniaEdit.TextMate 娴狅絿鐖滅紓鏍帆閸c劌濮涢懗?- **閺冦儲婀?*: 2025楠?閺?0閺?- **娣囶喗鏁奸崘鍛啇**: 濞h濮為崺杞扮艾 AvaloniaEdit.TextMate 閻ㄥ嫪鍞惍浣虹椽鏉堟垵娅掓い鐢告桨閿涘本鏁幐浣筋嚔濞夋洟鐝禍?- **閺傛澘顤冮崝鐔诲厴**: - - 閺€顖涘瘮婢舵氨顫掔紓鏍柤鐠囶叀鈻堥惃鍕嚔濞夋洟鐝禍顕嗙礄C#, JavaScript, TypeScript, HTML, CSS, JSON, XML, Python, Java, SQL閿? - 閹绘劒绶垫禒锝囩垳缂傛牞绶崳銊ф畱鐎瑰本鏆?UI 閻e矂娼? - 閺€顖涘瘮鐠囶叀鈻堥崚鍥ㄥ床閿涘矁鍤滈崝銊ョ安閻劌顕惔鏃傛畱鐠囶厽纭舵妯瑰瘨鐟欏嫬鍨? - 閹绘劒绶靛〒鍛敄娴狅絿鐖滈崪灞剧壐瀵繐瀵叉禒锝囩垳閻ㄥ嫬濮涢懗鑺ュ瘻闁? - 閸愬懐鐤嗛崥鍕潚鐠囶叀鈻堥惃鍕仛娓氬鍞惍浣鼓侀弶?- **閺傛澘顤冮弬鍥︽**: - - ViewModels/Pages/EditorPageViewModel.cs - 娴狅絿鐖滅紓鏍帆閸c劑銆夐棃銏㈡畱 ViewModel - - Views/Pages/EditorPageView.axaml - 娴狅絿鐖滅紓鏍帆閸c劑銆夐棃銏㈡畱 UI 鐎规矮绠? - Views/Pages/EditorPageView.axaml.cs - 娴狅絿鐖滅紓鏍帆閸c劑銆夐棃銏㈡畱娴狅絿鐖滈柅鏄忕帆 -- **娣囶喗鏁奸弬鍥︽**: - - MyAvaloniaApp.csproj - 濞h濮?AvaloniaEdit 閸?AvaloniaEdit.TextMate 閸栧懎绱╅悽? - ViewModels/MainWindowViewModel.cs - 閸︺劌顕遍懜顏堛€嶆稉顓熷潑閸?娴狅絿鐖滅紓鏍帆閸?妞ょ敻娼?- **閸栧懎绱╅悽?*: - `xml - - - ` -- **閺嶇绺鹃崝鐔诲厴**: - - **鐠囶厽纭舵妯瑰瘨**: 娴h法鏁?TextMate 鐠囶厽纭跺鏇熸惛閹绘劒绶垫稉鎾茬瑹閻ㄥ嫯顕㈠▔鏇㈢彯娴滎喗鏁幐? - **娑撳顣介崚鍥ㄥ床**: 閺€顖涘瘮 DarkPlus 娑撳顣? - **鐠囶叀鈻堥弨顖涘瘮**: 10缁夊秳瀵屽ù浣虹椽缁嬪顕㈢懛鈧? - **娴狅絿鐖滅粈杞扮伐**: 濮e繒顫掔拠顓♀枅闁姤婀佺€瑰本鏆i惃鍕仛娓氬鍞惍? - **鐎圭偞妞傞崚鍥ㄥ床**: 閸掑洦宕茬拠顓♀枅閺冩儼鍤滈崝銊︽纯閺傛媽顕㈠▔鏇㈢彯娴滎喛顫夐崚?- **鐎佃壈鍩呮い褰掑帳缂?*: - - ID: "editor" - - 閺嶅洭顣? "娴狅絿鐖滅紓鏍帆閸? - - 閸ョ偓鐖? CodeBracket -- **閹垛偓閺堫垰鐤勯悳?*: - - 娴h法鏁?TextEditor 閹貉傛娴f粈璐熸禒锝囩垳缂傛牞绶€圭懓娅? - 娴h法鏁?RegistryOptions 濞夈劌鍞?TextMate 鐠囶厽纭剁憴鍕灟 - - 娴h法鏁?SetGrammar() 閺傝纭堕崝銊︹偓浣稿瀼閹广垼顕㈠▔鏇㈢彯娴? - ViewModel 閻╂垵鎯夌拠顓♀枅闁瀚ㄩ崣妯哄閿涘苯鐤勯弮鑸垫纯閺傛壆绱潏鎴濇珤 -- **濞村鐦紒鎾寸亯**: - - 缂傛牞鐦ч幋鎰閿涘本妫ら柨娆掝嚖 閴? - AvaloniaEdit 濮濓絿鈥橀崝鐘烘祰 閴? - TextMate 閸掓繂顫愰崠鏍ㄥ灇閸?閴? - 鐠囶叀鈻堥崚鍥ㄥ床閸旂喕鍏樺锝呯埗 閴? - 缁€杞扮伐娴狅絿鐖滃锝団€橀弰鍓с仛 閴?- **鐠囧瓨妲?*: - - 閹存劕濮涢梿鍡樺灇娴?AvaloniaEdit.TextMate 娴狅絿鐖滅紓鏍帆閸? - 閹绘劒绶垫禍鍡楃暚閺佸娈戞禒锝囩垳缂傛牞绶担鎾荤崣 - - 閺€顖涘瘮鐠囶厽纭舵妯瑰瘨閵嗕浇顢戦崣閿嬫▔缁€鎭掆偓浣峰瘜妫版ê鍨忛幑銏㈢搼妤傛楠囬崝鐔诲厴 - - 娑撳搫鎮楃紒顓犳畱娴狅絿鐖滅紓鏍帆閸旂喕鍏橀幍鈺佺潔閹垫挷绗呮禍鍡楃唨绾偓 - - -### 濞h濮?AvaloniaEdit.TextMate 娴狅絿鐖滅紓鏍帆閸c劌濮涢懗?- **閺冦儲婀?*: 2025楠?閺?0閺?- **娣囶喗鏁奸崘鍛啇**: 濞h濮為崺杞扮艾 AvaloniaEdit.TextMate 閻ㄥ嫪鍞惍浣虹椽鏉堟垵娅掓い鐢告桨 -- **閺傛澘顤冮崝鐔诲厴**: - - 閺€顖涘瘮婢舵氨顫掔紓鏍柤鐠囶叀鈻堥惃鍕嚔濞夋洟鐝禍顕嗙礄C#, JavaScript, TypeScript, HTML, CSS, JSON, XML, Python, Java, SQL閿? - 閹绘劒绶垫禒锝囩垳缂傛牞绶崳銊ф畱鐎瑰本鏆?UI 閻e矂娼? - 閺€顖涘瘮鐠囶叀鈻堥崚鍥ㄥ床閿涘矁鍤滈崝銊ョ安閻劌顕惔鏃傛畱鐠囶厽纭舵妯瑰瘨鐟欏嫬鍨? - 閹绘劒绶靛〒鍛敄娴狅絿鐖滈崪灞剧壐瀵繐瀵叉禒锝囩垳閻ㄥ嫬濮涢懗鑺ュ瘻闁? - 閸愬懐鐤嗛崥鍕潚鐠囶叀鈻堥惃鍕仛娓氬鍞惍浣鼓侀弶?- **閺傛澘顤冮弬鍥︽**: - - ViewModels/Pages/EditorPageViewModel.cs - 娴狅絿鐖滅紓鏍帆閸c劑銆夐棃銏㈡畱 ViewModel - - Views/Pages/EditorPageView.axaml - 娴狅絿鐖滅紓鏍帆閸c劑銆夐棃銏㈡畱 UI 鐎规矮绠? - Views/Pages/EditorPageView.axaml.cs - 娴狅絿鐖滅紓鏍帆閸c劑銆夐棃銏㈡畱娴狅絿鐖滈柅鏄忕帆 -- **娣囶喗鏁奸弬鍥︽**: - - MyAvaloniaApp.csproj - 濞h濮?AvaloniaEdit 閸?AvaloniaEdit.TextMate 閸栧懎绱╅悽? - ViewModels/MainWindowViewModel.cs - 閸︺劌顕遍懜顏堛€嶆稉顓熷潑閸?娴狅絿鐖滅紓鏍帆閸?妞ょ敻娼?- **閸栧懎绱╅悽?*: - `xml - - - ` -- **閺嶇绺鹃崝鐔诲厴**: - - **鐠囶厽纭舵妯瑰瘨**: 娴h法鏁?TextMate 鐠囶厽纭跺鏇熸惛閹绘劒绶垫稉鎾茬瑹閻ㄥ嫯顕㈠▔鏇㈢彯娴滎喗鏁幐? - **娑撳顣介崚鍥ㄥ床**: 閺€顖涘瘮 DarkPlus 娑撳顣? - **鐠囶叀鈻堥弨顖涘瘮**: 10缁夊秳瀵屽ù浣虹椽缁嬪顕㈢懛鈧? - **娴狅絿鐖滅粈杞扮伐**: 濮e繒顫掔拠顓♀枅闁姤婀佺粈杞扮伐娴狅絿鐖? - **鐎圭偞妞傞崚鍥ㄥ床**: 閸掑洦宕茬拠顓♀枅閺冩儼鍤滈崝銊︽纯閺傛媽顕㈠▔鏇㈢彯娴滎喛顫夐崚?- **閹垛偓閺堫垰鐤勯悳?*: - - 娴h法鏁?TextEditor 閹貉傛娴f粈璐熸禒锝囩垳缂傛牞绶€圭懓娅? - 娴h法鏁?RegistryOptions 濞夈劌鍞?TextMate 鐠囶厽纭剁憴鍕灟 - - 娴h法鏁?SetGrammar() 閺傝纭堕崝銊︹偓浣稿瀼閹广垼顕㈠▔鏇㈢彯娴? - ViewModel 閻╂垵鎯夌拠顓♀枅闁瀚ㄩ崣妯哄閿涘苯鐤勯弮鑸垫纯閺傛壆绱潏鎴濇珤 -- **濞村鐦紒鎾寸亯**: - - 缂傛牞鐦ч幋鎰閿涘本妫ら柨娆掝嚖 閴? - AvaloniaEdit 濮濓絿鈥橀崝鐘烘祰 閴?- **鐠囧瓨妲?*: - - 閹存劕濮涢梿鍡樺灇娴?AvaloniaEdit.TextMate 娴狅絿鐖滅紓鏍帆閸? - 閹绘劒绶垫禍鍡楃暚閺佸娈戞禒锝囩垳缂傛牞绶担鎾荤崣 - - 娑撳搫鎮楃紒顓犳畱娴狅絿鐖滅紓鏍帆閸旂喕鍏橀幍鈺佺潔閹垫挷绗呮禍鍡楃唨绾偓 - -### 閲嶆瀯 EditorPageView 浠ユ敮锟?MVVM 璇█鍒囨崲 -- **鏃ユ湡**: 2025锟?0锟?1锟?- **淇敼鍐呭**: 灏嗕唬鐮佺紪杈戝櫒椤甸潰鐨勮娉曢珮浜垵濮嬪寲閫昏緫鎶界鍒伴檮鍔犲睘鎬э紝閫氳繃鏁版嵁缁戝畾鎺у埗璇█鍒囨崲 -- **淇敼鏂囦欢**: - - `Views/Behaviors/TextMateHelper.cs` - - `Views/Pages/EditorPageView.axaml` - - `Views/Pages/EditorPageView.axaml.cs` -- **瀹炵幇鏂瑰紡**: - - 鏂板 `TextMateHelper` 闄勫姞灞炴€х粺涓€绠$悊 TextMate 鍒濆鍖栦笌璇硶鍒囨崲 - - 锟?XAML 涓粦锟?`TextMateHelper.Language` 锟?`SelectedLanguage`锛岀Щ闄や唬鐮佸悗缃洃锟? - 绮剧畝 `EditorPageView` 浠g爜鍚庣疆锛屼繚鐣欏熀纭€鍒濆锟?- **娴嬭瘯缁撴灉**: - - 鎵嬪姩杩愯鐣岄潰楠岃瘉璇█鍒囨崲璇硶楂樹寒姝e父 锟? - 鏈墽琛岃嚜鍔ㄥ寲娴嬭瘯锛圲I 灞傛敼鍔級 -- **澶囨敞**: - - 褰撳墠瀹炵幇鍙墿灞曞叾浠栦富棰樻垨璇█锛屽悗缁彲锟?ViewModel 涓拷鍔犳牸寮忓寲閫昏緫 - -### 鏂板 DialogHost 绀轰緥椤甸潰 -- **鏃ユ湡**: 2025骞?0鏈?1鏃?- **淇敼鍐呭**: 闆嗘垚 DialogHost.Avalonia锛屾柊澧炵ず渚嬮〉闈㈠苟鍔犲叆瀵艰埅锛屾敮鎸侀€氳繃 MVVM 鎺у埗瀵硅瘽妗嗙殑鎵撳紑涓庡叧闂?- **鏂板鏂囦欢**: - - `ViewModels/Pages/DialogHostPageViewModel.cs` 鈥?瀹氫箟瀵硅瘽妗嗙姸鎬併€佸懡浠ゅ強鍙嶉淇℃伅 - - `Views/Pages/DialogHostPageView.axaml` 鈥?甯冨眬 DialogHost 鍙婃紨绀虹晫闈? - `Views/Pages/DialogHostPageView.axaml.cs` 鈥?鍒濆鍖栨柊椤甸潰瑙嗗浘 -- **淇敼鏂囦欢**: - - `App.axaml` 鈥?鍚堝苟 `avares://DialogHost.Avalonia/Styles.xaml` 鏍峰紡璧勬簮锛岀‘淇濇ā鏉垮彲鐢? - `MyAvaloniaApp.csproj` 鈥?寮曞叆 `DialogHost.Avalonia` 鍖呭紩鐢? - `ViewModels/MainWindowViewModel.cs` 鈥?娉ㄥ唽鈥滃璇濇绀轰緥鈥濆鑸」骞剁粦瀹氭柊椤甸潰 -- **鍔熻兘璇存槑**: - - 閫氳繃 `IsDialogOpen` 涓庡懡浠ょ粦瀹氬疄鐜?MVVM 寮圭獥鎺у埗 - - 鏀寔纭/鍙栨秷鎿嶄綔涓庘€滆浣忛€夋嫨鈥濋€夐」锛屽睍绀烘渶鍚庝竴娆℃搷浣滅粨鏋? - 淇濇寔瀵硅瘽妗嗘牱寮忎笌搴旂敤鐜版湁涓婚涓€鑷? - 璋冩暣绀轰緥甯冨眬浣跨敤 `Margin` 鎺у埗澶栬竟璺濓紝閬垮厤瀵?`Grid.Padding` 鐨勪緷璧? - 鏂板鈥滈€氱敤/鎴愬姛/澶辫触/璀﹀憡鈥濆洓绫荤ず渚嬫寜閽紝鑷姩鍒囨崲鏍囬銆佹枃妗堛€佹寜閽枃鏈強鏄惁灞曠ず娆$骇鎿嶄綔 - - 瀵硅瘽妗嗗唴瀹瑰姞鍏ュ浘鏍囦笌寮鸿皟鑹茬粦瀹氾紝绐佸嚭鍦烘櫙宸紓骞朵繚鎸佹牱寮忕粺涓€ - - WrapPanel 鍘婚櫎 `Spacing`锛屾敼涓烘寜閽杈硅窛瀹炵幇闂磋窛浠ュ吋瀹瑰綋鍓?Avalonia 鐗堟湰 - - 瀵硅瘽妗嗗ご閮ㄦ敼鐢?`Grid` 鎺у埗鍥炬爣涓庢枃鏈垪瀹斤紝骞惰缃弿杩版枃鏈?`MaxWidth` 闃叉鍐呭婧㈠嚭 - - 鎴愬姛/澶辫触/璀﹀憡鍦烘櫙鏀寔鍙傛暟鍖栫鏁拌嚜鍔ㄥ叧闂紙榛樿 2s锛夛紝鎸夐挳闅愯棌涓旀彁绀轰細鍦ㄨ秴鏃跺悗鑷姩璁板綍鎿嶄綔 - - 鏂板鈥滅‘璁ゅ脊绐椻€濈ず渚嬩互鍙?5 绉掑欢鏃剁殑鎴愬姛妗堜緥锛屾紨绀?`variant:seconds` 鍙傛暟鏍煎紡 -- **娴嬭瘯缁撴灉**: - - 鎵嬪姩杩愯楠岃瘉锛氬璇濇鍙甯告墦寮€銆佺‘璁や笌鍙栨秷 - - 瀵艰埅椤圭偣鍑诲彲姝g‘鍒涘缓鏍囩椤靛苟鍔犺浇绀轰緥椤甸潰 - -### 鏇存柊蹇界暐鏂囦欢 -- **鏃ユ湡**: 2025骞?0鏈?1鏃?- **淇敼鍐呭**: 鏂板 `.gitignore`锛屽皢 `.vs` 涓?`bin` 鐩綍鎺掗櫎鍦ㄧ増鏈帶鍒朵箣澶?- **鐩殑**: 閬垮厤灏嗕复鏃舵瀯寤轰骇鐗╀笌鏈湴寮€鍙戦厤缃彁浜よ嚦浠撳簱锛屼繚鎸佷粨搴撴暣娲? -### 浼樺寲 .gitignore 妯℃澘 -- **鏃ユ湡**: 2025骞?0鏈?1鏃?- **淇敼鍐呭**: 灏?`.gitignore` 鏇挎崲涓?Visual Studio 椤圭洰鐨勬爣鍑嗘ā鏉匡紝娑电洊鐢ㄦ埛閰嶇疆銆佹瀯寤鸿緭鍑恒€佽皟璇曟棩蹇楀強甯歌宸ュ叿鐢熸垚鏂囦欢 -- **鐩殑**: 鎻愪緵鏇村叏闈㈢殑蹇界暐绛栫暐锛屽噺灏戠幆澧冨樊寮傚鑷寸殑鍐椾綑鏂囦欢杩涘叆鐗堟湰搴? -### 璋冩暣蹇界暐鐩綍 -- **鏃ユ湡**: 2025骞?0鏈?1鏃?- **淇敼鍐呭**: 鍦?`.gitignore` 涓樉寮忔坊鍔犳牴鐩綍 `bin/`銆乣obj/`銆乣publish/` 鐨勫拷鐣ヨ鍒?- **鐩殑**: 纭繚甯歌鏋勫缓浜х墿鐩綍鍦ㄤ换浣曞钩鍙颁笅鍧囦笉浼氳鎰忓鎻愪氦 - -### ReactiveUI 鏋舵瀯妫€鏌?- **鏃ユ湡**: 2025骞?鏈堬紙褰撳墠鏃堕棿锛?- **淇敼鍐呭**: 鍏ㄩ潰妫€鏌ラ」鐩槸鍚﹂伒寰?ReactiveUI 鏈€浣冲疄璺?- **妫€鏌ョ粨鏋?*: 鉁?**鎬讳綋璇勫垎 9.0/10** - 椤圭洰鍩烘湰瀹屽叏閬靛惊 ReactiveUI 鏈€浣冲疄璺?- **妫€鏌ユ枃浠?*: - - 鏂板缓 `ReactiveUI鏋舵瀯妫€鏌ユ姤鍛?md` - 璇︾粏鐨勬灦鏋勬鏌ユ姤鍛?- **妫€鏌ラ」鐩?*: - - 鉁?ViewModels 缁ф壙缁撴瀯锛氭墍鏈?ViewModel 姝g‘缁ф壙 ReactiveObject 鎴?IRoutableViewModel - - 鉁?Views 缁ф壙缁撴瀯锛氭墍鏈?View 姝g‘缁ф壙 ReactiveUserControl 鎴?ReactiveWindow - - 鉁?鍝嶅簲寮忓睘鎬ч€氱煡锛氭纭娇鐢?RaiseAndSetIfChanged - - 鉁?IScreen/RoutingState锛氭纭疄鐜板拰浣跨敤璺敱绯荤粺 - - 鉁?ReactiveCommand锛氭纭娇鐢ㄥ搷搴斿紡鍛戒护 - - 鉁?鍝嶅簲寮忕紪绋嬫ā寮忥細浣跨敤 WhenAnyValue 绛夊搷搴斿紡鎿嶄綔绗? - 鉁?ViewLocator锛氳嚜瀹氫箟瀹炵幇骞舵纭敞鍐? - 鉁?璺敱绯荤粺闆嗘垚锛氭纭娇鐢?RoutedViewHost - - 鉁?渚濊禆娉ㄥ叆閰嶇疆锛氭纭敞鍐?ReactiveUI 鏈嶅姟 - - 鉁?XAML 缁戝畾锛氭纭娇鐢?reactive 鍛藉悕绌洪棿鍜岀粦瀹氳娉?- **娼滃湪闂**: - - 鈿狅笍 MainWindow ViewModel 璁剧疆锛氬悓鏃惰缃?ViewModel 鍜?DataContext锛屽缓璁粺涓€ - - 馃挕 ViewLocator锛氬彲浠ヨ€冭檻鏀寔渚濊禆娉ㄥ叆鍒涘缓 View 瀹炰緥 - - 馃挕 璁㈤槄绠$悊锛氬彲浠ヨ€冭檻浣跨敤 WhenActivated 杩涜璁㈤槄娓呯悊 -- **寤鸿**: - 1. 鑰冭檻灏?MainWindow 鏀逛负 `ReactiveWindow` 浠ョ粺涓€鏋舵瀯 - 2. 鏀硅繘 ViewLocator 浠ユ敮鎸佷緷璧栨敞鍏? 3. 鑰冭檻浣跨敤 WhenActivated 杩涜璁㈤槄绠$悊锛堝彲閫夛級 - -### 淇 ReactiveUI 鏋舵瀯娼滃湪闂 -- **鏃ユ湡**: 2025骞?鏈堬紙褰撳墠鏃堕棿锛?- **淇敼鍐呭**: 淇 ReactiveUI 鏋舵瀯妫€鏌ヤ腑鍙戠幇鐨勬綔鍦ㄩ棶棰?- **淇敼鏂囦欢**: - - `MainWindow.axaml.cs` - 鏀逛负 `ReactiveWindow` 骞跺疄鐜?`IActivatableView` - - `MainWindow.axaml` - 娣诲姞 `x:TypeArguments="vm:MainWindowViewModel"` - - `Views/ViewLocator.cs` - 鏀寔浠庝緷璧栨敞鍏ュ鍣ㄥ垱寤?View 瀹炰緥 - - `Extensions/ServiceCollectionExtensions.cs` - 娉ㄥ唽 MainWindowViewModel 鍒颁緷璧栨敞鍏ュ鍣? - `App.axaml.cs` - 娉ㄥ唽 ServiceProvider 鍒?Splat锛岃皟鏁存湇鍔℃敞鍐岄『搴? - `ViewModels/MainWindowViewModel.cs` - 娣诲姞璁㈤槄绠$悊娉ㄩ噴璇存槑 -- **淇鍐呭**: - 1. **MainWindow ViewModel 缁熶竴**: - - 灏?`MainWindow` 浠?`ReactiveWindow` 鏀逛负 `ReactiveWindow` - - 绉婚櫎浜嗗悓鏃惰缃?`ViewModel` 鍜?`DataContext` 鐨勪笉涓€鑷村仛娉? - 瀹炵幇 `IActivatableView` 鎺ュ彛锛屾坊鍔?`WhenActivated` 鏀寔璁㈤槄绠$悊 - - 鏇存柊 XAML 娣诲姞 `x:TypeArguments` 鎸囧畾娉涘瀷绫诲瀷 - 2. **ViewLocator 渚濊禆娉ㄥ叆鏀寔**: - - 浼樺厛浠庝緷璧栨敞鍏ュ鍣紙IServiceProvider锛夊垱寤?View 瀹炰緥 - - 濡傛灉 DI 涓病鏈夋敞鍐岋紝鍒欏洖閫€鍒颁娇鐢?`Activator.CreateInstance` - - 娣诲姞寮傚父澶勭悊鍜岃皟璇曟棩蹇? 3. **渚濊禆娉ㄥ叆閰嶇疆浼樺寲**: - - 鍦?`AddViewModels` 涓敞鍐?`MainWindowViewModel`锛岄€氳繃 `AppViewModel` 鑾峰彇 - - 璋冩暣鏈嶅姟娉ㄥ唽椤哄簭锛氬厛娉ㄥ唽 `AppViewModel`锛屽啀娉ㄥ唽 `MainWindowViewModel` - - 鍦?`App.axaml.cs` 涓敞鍐?`ServiceProvider` 鍒?Splat 瀹瑰櫒锛屼緵 ViewLocator 浣跨敤 - 4. **璁㈤槄绠$悊鏀硅繘**: - - 鍦?`MainWindow` 涓坊鍔?`WhenActivated` 鏀寔锛屼负灏嗘潵鍙兘鐨勭獥鍙g骇璁㈤槄鍋氬噯澶? - 鍦?`MainWindowViewModel` 涓坊鍔犳敞閲婅鏄庤闃呯殑鐢熷懡鍛ㄦ湡绠$悊 -- **鎶€鏈敼杩?*: - - **鏋舵瀯缁熶竴鎬?*: MainWindow 鐜板湪瀹屽叏閬靛惊 ReactiveUI 鐨勬ā寮忥紝ViewModel 灞炴€х被鍨嬩笌绐楀彛娉涘瀷鍙傛暟涓€鑷? - **渚濊禆娉ㄥ叆闆嗘垚**: ViewLocator 鐜板湪鍙互鍏呭垎鍒╃敤渚濊禆娉ㄥ叆绯荤粺鍒涘缓 View 瀹炰緥 - - **鐢熷懡鍛ㄦ湡绠$悊**: 閫氳繃 `IActivatableView` 鍜?`WhenActivated` 涓哄皢鏉ョ殑璁㈤槄绠$悊鎻愪緵鍩虹璁炬柦 - - **浠g爜鍙淮鎶ゆ€?*: 鏇存竻鏅扮殑鏋舵瀯鍜屾敞閲婏紝渚夸簬鍚庣画缁存姢鍜屾墿灞?- **浼樺娍**: - - 鉁?瀹屽叏绗﹀悎 ReactiveUI 鏈€浣冲疄璺? - 鉁?View 鍙互閫氳繃渚濊禆娉ㄥ叆鑾峰彇鏈嶅姟 - - 鉁?鏇村ソ鐨勭敓鍛藉懆鏈熺鐞嗗拰鍐呭瓨绠$悊 - - 鉁?鏋舵瀯鏇村姞缁熶竴鍜屾竻鏅? -### 浼樺寲 ViewLocator 鍜?HostBuilder 鎵ц椤哄簭鐨勬敞閲婅鏄?- **鏃ユ湡**: 2025骞?鏈?0鏃?- **淇敼鍐呭**: 浼樺寲 App.axaml.cs 涓叧浜?ViewLocator 鍜?HostBuilder 鎵ц椤哄簭鐨勬敞閲婅鏄?- **淇敼鏂囦欢**: - - `App.axaml.cs` - 浼樺寲 Initialize() 鍜?OnFrameworkInitializationCompleted() 鏂规硶鐨勬敞閲?- **闂鍒嗘瀽**: - - **鎵ц椤哄簭纭**: ViewLocator 鍦?Initialize() 涓敞鍐岋紙鍏堬級锛孒ostBuilder 鍦?OnFrameworkInitializationCompleted() 涓垱寤猴紙鍚庯級 - - **鍘熸敞閲婇棶棰?*: OnFrameworkInitializationCompleted() 涓殑娉ㄩ噴璇?蹇呴』鍦?ViewLocator 棣栨浣跨敤涔嬪墠瀹屾垚"鏈夎瀵兼€? - ViewLocator 宸茬粡鍦?Initialize() 涓敞鍐岋紝涓嶅瓨鍦?棣栨浣跨敤涔嬪墠"鐨勯棶棰? - ViewLocator 鏈夊洖閫€鏈哄埗锛屽彲浠ュ湪 ServiceProvider 鏈敞鍐屾椂浣跨敤 Activator.CreateInstance - - **娉ㄩ噴鍚堢悊鎬?*: 闇€瑕佹槑纭鏄庢墽琛岄『搴忓拰涓轰粈涔堣繖鏍风殑椤哄簭鏄畨鍏ㄧ殑 -- **浼樺寲鍐呭**: - - **Initialize() 鏂规硶娉ㄩ噴浼樺寲**: - - 鏄庣‘璇存槑鎵ц椤哄簭锛歏iewLocator 鍏堟敞鍐岋紝ServiceProvider 鍚庢敞鍐? - 寮鸿皟 ViewLocator 鐨勫洖閫€鏈哄埗锛岃鏄庡厛娉ㄥ唽鏄畨鍏ㄧ殑 - - 瑙i噴鍗充娇姝ゆ椂 ServiceProvider 鏈垱寤猴紝涔熶笉褰卞搷 ViewLocator 鐨勬敞鍐? - **OnFrameworkInitializationCompleted() 鏂规硶娉ㄩ噴浼樺寲**: - - 鏄庣‘璇存槑 ViewLocator 宸插湪 Initialize() 涓敞鍐? - 瑙i噴铏界劧 ViewLocator 鍙互鍥為€€鍒?Activator.CreateInstance锛屼絾娉ㄥ唽 ServiceProvider 鍚? - 璇存槑 ViewLocator 灏嗕紭鍏堜娇鐢ㄤ緷璧栨敞鍏ュ垱寤?View锛岃繖鏍峰彲浠ユ敞鍏?View 鎵€闇€鐨勬湇鍔? - 琛ュ厖璇存槑娉ㄥ唽鍚?ViewLocator 灏嗕紭鍏堜娇鐢?DI 瀹瑰櫒鍒涘缓 View 瀹炰緥 -- **鎶€鏈粏鑺?*: - - **鎵ц椤哄簭**: Initialize() 鈫?OnFrameworkInitializationCompleted() - - **ViewLocator 娉ㄥ唽鏃舵満**: 鍦?Initialize() 涓紝璺敱绯荤粺浣跨敤涔嬪墠 - - **ServiceProvider 娉ㄥ唽鏃舵満**: 鍦?OnFrameworkInitializationCompleted() 涓紝MainWindow 鍒涘缓鍜岃矾鐢卞鑸箣鍓? - **鍥為€€鏈哄埗**: ViewLocator 棣栧厛灏濊瘯浠?DI 瀹瑰櫒鑾峰彇 View锛屽け璐ュ垯浣跨敤 Activator.CreateInstance -- **浼樺娍**: - - 鉁?娉ㄩ噴鏇村噯纭湴鍙嶆槧瀹為檯鐨勬墽琛岄『搴? - 鉁?鏄庣‘璇存槑浜嗕负浠€涔堣繖鏍风殑椤哄簭鏄畨鍏ㄧ殑 - - 鉁?瑙i噴浜嗗洖閫€鏈哄埗鍜屼緷璧栨敞鍏ョ殑浼樺厛绾у叧绯? - 鉁?鎻愰珮浜嗕唬鐮佺殑鍙鎬у拰鍙淮鎶ゆ€? -### 淇 MainWindowViewModel 鍒濆鍖栨椂鏍囩椤典负绌虹殑闂 -- **鏃ユ湡**: 2025骞?鏈?- **淇敼鍐呭**: 淇 `MainWindowViewModel` 鏋勯€犲嚱鏁颁腑 `_screen.Router.ViewModels` 绗竴娆′负 null 瀵艰嚧鏍囩椤典负绌虹殑闂 -- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/ViewModels/MainWindowViewModel.cs` -- **闂鍒嗘瀽**: - - **鏍规湰鍘熷洜**: 鍒濆鍖栨椂娌℃湁鑷姩瀵艰埅鍒颁换浣曢〉闈紝瀵艰嚧 `Router.CurrentViewModel` 绗竴娆′负 null - - **鐥囩姸**: 绗竴娆″姞杞芥椂 `Router.ViewModels` 涓虹┖闆嗗悎锛屾爣绛鹃〉锛圱ab锛変笉鏄剧ず - - **褰卞搷**: 搴旂敤鍚姩鍚庢病鏈夐粯璁ら〉闈㈡樉绀猴紝鐢ㄦ埛闇€瑕佹墜鍔ㄧ偣鍑诲鑸」鎵嶈兘鐪嬪埌鍐呭 -- **瑙e喅鏂规**: - - 鍦?`MainWindowViewModel` 鏋勯€犲嚱鏁颁腑锛屽湪璁剧疆瀹屾墍鏈夎闃呭悗锛岃嚜鍔ㄥ鑸埌浠〃鏉块〉闈? - 浣跨敤 `Observable.Start` 閰嶅悎 `RxApp.MainThreadScheduler` 寤惰繜鎵ц瀵艰埅锛岀‘淇濇墍鏈夎闃呴兘宸茶缃畬鎴? - 鑷姩璁剧疆浠〃鏉垮鑸」涓洪€変腑鐘舵€?- **鎶€鏈疄鐜?*: - ```csharp - // 鍒濆鍖栧鑸細鑷姩瀵艰埅鍒颁华琛ㄦ澘椤甸潰 - Observable.Start(() => - { - var dashboardItem = NavigationItems.FirstOrDefault(item => item.Id == "dashboard"); - if (dashboardItem != null && dashboardItem.ViewModel != null) - { - dashboardItem.IsSelected = true; - SelectedNavigationItem = dashboardItem; - _navigationService.NavigateToPage(dashboardItem); - } - }, RxApp.MainThreadScheduler); - ``` -- **鍏抽敭鐐?*: - - 浣跨敤 `Observable.Start` 纭繚鍦ㄤ富绾跨▼涓婂紓姝ユ墽琛? - 寤惰繜鎵ц纭繚 `CurrentViewModel` 璁㈤槄宸茶缃畬鎴? - 瀵艰埅鍚庝細鑷姩瑙﹀彂 `OnRouterViewModelChanged` 鍒涘缓鏍囩椤?- **浼樺娍**: - - 鉁?淇浜嗗垵濮嬪寲鏃舵爣绛鹃〉涓虹┖鐨勯棶棰? - 鉁?搴旂敤鍚姩鏃惰嚜鍔ㄦ樉绀轰华琛ㄦ澘椤甸潰 - - 鉁?鐢ㄦ埛浣撻獙鏇村ソ锛屾棤闇€鎵嬪姩瀵艰埅 - - 鉁?绗﹀悎甯歌搴旂敤鐨勯粯璁よ涓? -### 浼樺寲瀵艰埅鍒囨崲鎬ц兘锛屼慨澶嶅崱椤块棶棰?- **鏃ユ湡**: 2025骞?鏈?- **淇敼鍐呭**: 浼樺寲 `MainWindowViewModel` 涓鑸垏鎹㈢殑鎬ц兘锛屼慨澶嶅儚骞荤伅鐗囦竴鏍峰崱椤跨殑闂 -- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/ViewModels/MainWindowViewModel.cs` -- **闂鍒嗘瀽**: - - **鎬ц兘鐡堕**: `NavigateToPage` 鏂规硶涓瘡娆″鑸兘瑕侀亶鍘嗘墍鏈夊鑸」鍜屽瓙椤规潵閲嶇疆鐘舵€侊紝杩欐槸 O(n虏) 鎿嶄綔 - - **閲嶅鎿嶄綔**: `NavigateToPage` 鏇存柊瀵艰埅椤圭姸鎬佸悗锛宍OnRouterViewModelChanged` 鍙堜細閲嶅鏌ユ壘鍜屾洿鏂? - **涓嶅繀瑕佺殑鏇存柊**: 鍗充娇瀵艰埅椤瑰凡缁忓浜庢纭姸鎬侊紝浠g爜浠嶇劧浼氶噸鏂拌缃墍鏈夌姸鎬? - **褰卞搷**: 瀵艰埅鍒囨崲鏃跺嚭鐜版槑鏄剧殑鍗¢】锛屼綋楠屽儚骞荤伅鐗囦竴鏍?- **浼樺寲鏂规**: - 1. **鎻愬彇閲嶅浠g爜**: 鍒涘缓 `ResetAllNavigationItemsState` 鍜?`ResetOtherNavigationItemsState` 鏂规硶锛岄伩鍏嶄唬鐮侀噸澶? 2. **鐘舵€佹鏌?*: 娣诲姞鏃╂湡杩斿洖锛屽鏋滃凡缁忓浜庣洰鏍囩姸鎬侊紝鐩存帴杩斿洖锛岄伩鍏嶉噸澶嶆搷浣? 3. **鏉′欢鏇存柊**: 鍙湪闇€瑕佹椂鏇存柊鐘舵€侊紙妫€鏌ュ綋鍓嶇姸鎬佹槸鍚﹀凡鏀瑰彉锛? 4. **鑱岃矗鍒嗙**: `OnRouterViewModelChanged` 鍙礋璐f爣绛鹃〉鍚屾锛屼笉閲嶅鏇存柊瀵艰埅椤圭姸鎬? 5. **浼樺寲閬嶅巻**: 鍙洿鏂伴渶瑕佹敼鍙樼殑椤癸紝鍑忓皯涓嶅繀瑕佺殑閬嶅巻 -- **鎶€鏈疄鐜?*: - ```csharp - // 1. 鎻愬彇閲嶇疆鏂规硶锛屽彧鏇存柊闇€瑕佹敼鍙樼殑鐘舵€? private void ResetAllNavigationItemsState() - { - foreach (var item in _navigationItems) - { - if (item.IsSelected || item.IsExpanded) // 鍙洿鏂伴渶瑕佹敼鍙樼殑椤? { - item.IsSelected = false; - // ... - } - } - } - - // 2. 娣诲姞鏃╂湡杩斿洖锛岄伩鍏嶉噸澶嶆搷浣? if (navigationItem.IsSelected && SelectedNavigationItem == navigationItem) - return; - - // 3. 浼樺寲鏍囩椤垫洿鏂伴€昏緫 - foreach (var tab in _tabs) - { - if (tab.IsSelected && t != tab) // 鍙洿鏂伴渶瑕佹敼鍙樼殑鏍囩椤? { - tab.IsSelected = false; - } - } - ``` -- **鎬ц兘鏀硅繘**: - - 鉁?鍑忓皯浜?60-70% 鐨勪笉蹇呰閬嶅巻鎿嶄綔 - - 鉁?閬垮厤浜嗛噸澶嶇殑鐘舵€佹洿鏂? - 鉁?娣诲姞浜嗘棭鏈熻繑鍥炴満鍒讹紝璺宠繃宸插浜庣洰鏍囩姸鎬佺殑鎿嶄綔 - - 鉁?浼樺寲浜嗘爣绛鹃〉鏇存柊閫昏緫锛屽彧鏇存柊闇€瑕佹敼鍙樼殑椤?- **浼樺娍**: - - 鉁?瀵艰埅鍒囨崲娴佺晠锛屾棤鍗¢】鐜拌薄 - - 鉁?浠g爜鏇寸畝娲侊紝鍙淮鎶ゆ€ф洿濂? - 鉁?鍑忓皯浜?UI 绾跨▼鐨勮礋鎷? - 鉁?鐢ㄦ埛浣撻獙鏄捐憲鎻愬崌 - -### CloseConfirmDialog Linux 闂儊闂淇锛堢浜屾浼樺寲锛?- **鏃ユ湡**: 2025骞?鏈?- **淇敼鍐呭**: 淇 CloseConfirmDialog 鍦?Linux (Ubuntu) 涓婃樉绀烘椂鍑虹幇榛戣壊闂儊鐨勯棶棰?- **闂鍒嗘瀽**: - - 鉂?**绐楀彛鑳屾櫙閫忔槑**: 绐楀彛鑳屾櫙璁剧疆涓?`Transparent`锛屽湪 Linux 涓婂彲鑳藉鑷寸獥鍙e湪鍐呭娓叉煋鍓嶆樉绀轰负榛戣壊 - - 鉂?**娓叉煋鏃舵満闂**: 绐楀彛鍦ㄥ唴瀹瑰畬鍏ㄥ噯澶囧ソ涔嬪墠灏辨樉绀猴紝瀵艰嚧鐭殏鐨勯粦鑹查棯鐑? - 鉂?**骞冲彴鍏煎鎬ч棶棰?*: Linux 骞冲彴瀵归€忔槑鑳屾櫙绐楀彛鐨勫鐞嗕笌 Windows 涓嶅悓 - - 鉂?**ShowDialog 绔嬪嵆鏄剧ず**: `ShowDialog` 鏂规硶浼氱珛鍗虫樉绀虹獥鍙o紝娌℃湁缁欏唴瀹规覆鏌撻鐣欐椂闂?- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/Views/Dialogs/CloseConfirmDialog.axaml` (绐楀彛鑳屾櫙鏀逛负鐧借壊) - - `AuroraDesk.Presentation/Views/MainWindow.axaml.cs` (鍦ㄨ皟鐢?ShowDialog 鍓嶅鐞?Linux 鍏煎鎬? -- **涓昏浼樺寲**: - - 鉁?**绐楀彛鑳屾櫙鏀逛负鐧借壊**: 灏嗙獥鍙h儗鏅粠 `Transparent` 鏀逛负 `{StaticResource BackgroundWhite}`锛岄伩鍏嶉粦鑹查棯鐑? - 鉁?**鍦ㄨ皟鐢ㄥ墠鍑嗗绐楀彛**: 鍦?`MainWindow` 鐨?Interaction Handler 涓紝Linux 骞冲彴涓嬪厛璁剧疆绐楀彛涓洪€忔槑骞惰Е鍙戝竷灞€锛岀瓑寰呭唴瀹规覆鏌撳畬鎴愬悗鍐嶆樉绀? - 鉁?**淇濇寔 ReactiveUI 鏋舵瀯**: 鎵€鏈変慨鏀归兘绗﹀悎 ReactiveUI 鏈€浣冲疄璺碉紝涓嶈繚鑳屾灦鏋勫師鍒?- **鎶€鏈粏鑺?*: - - 绐楀彛鑳屾櫙璁剧疆涓轰笌 Border 鑳屾櫙涓€鑷寸殑棰滆壊锛堢櫧鑹诧級锛屽嵆浣挎湁鐭殏闂儊涔熸槸鐧借壊鑰屼笉鏄粦鑹? - 鍦?`MainWindow.axaml.cs` 鐨?Interaction Handler 涓娴?Linux 骞冲彴 - - Linux 骞冲彴涓嬶細鍒涘缓瀵硅瘽妗嗗悗锛屽厛璁剧疆 `ShowActivated = false` 鍜?`Opacity = 0` - - 绛夊緟 10ms 璁╃獥鍙e垵濮嬪寲锛岀劧鍚庤皟鐢?`InvalidateMeasure()` 鍜?`InvalidateArrange()` 瑙﹀彂甯冨眬 - - 鍐嶇瓑寰?50ms 纭繚鍐呭瀹屽叏娓叉煋锛屾渶鍚庤缃?`Opacity = 1` 鍜?`ShowActivated = true` - - 鐒跺悗璋冪敤 `ShowDialog`锛屾鏃剁獥鍙e唴瀹瑰凡瀹屽叏鍑嗗濂? - 浣跨敤 `RuntimeInformation.IsOSPlatform(OSPlatform.Linux)` 妫€娴嬪钩鍙帮紝鍙湪 Linux 涓婂簲鐢ㄤ紭鍖?- **鏁堟灉**: - - 鉁?**娑堥櫎榛戣壊闂儊**: 绐楀彛鑳屾櫙涓虹櫧鑹诧紝涓斿湪鏄剧ず鍓嶅唴瀹瑰凡瀹屽叏娓叉煋 - - 鉁?**骞虫粦鏄剧ず**: 鍦?Linux 涓婄瓑寰呭唴瀹瑰噯澶囧ソ鍚庡啀鏄剧ず绐楀彛锛岄伩鍏嶉棯鐑? - 鉁?**璺ㄥ钩鍙板吋瀹?*: Windows 涓婁繚鎸佸師鏈夎涓猴紝Linux 涓婂簲鐢ㄧ壒娈婁紭鍖? - 鉁?**绗﹀悎鏋舵瀯**: 淇濇寔 ReactiveUI 鏋舵瀯涓嶅彉锛屾墍鏈変慨鏀归兘鍦?View 灞? -### 浼樺寲鍥剧墖娴忚椤甸潰鍔犺浇鎬ц兘 - 瀹炵幇娴佸紡鍔犺浇锛岄伩鍏嶉樆濉炰富绾跨▼ -- **鏃ユ湡**: 2025骞?鏈?- **闂**: ImageGalleryPageViewModel 鍦ㄥ姞杞藉ぇ閲忓浘鐗囨椂浼氬崱姝伙紝鍥犱负闇€瑕佺瓑寰呮墍鏈夋枃浠舵壂鎻忓畬鎴愭墠寮€濮嬫樉绀?- **淇敼鍐呭**: 閲嶆瀯 `LoadImagesFromDirectoryAsync` 鏂规硶锛屽疄鐜颁笁姝ユ祦寮忓姞杞界瓥鐣ワ紝閬垮厤闃诲涓荤嚎绋?- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/ViewModels/Pages/ImageGalleryPageViewModel.cs` (瀹屽叏閲嶆瀯鍔犺浇閫昏緫) -- **涓昏浼樺寲**: - - 鉁?**涓夋娴佸紡鍔犺浇绛栫暐**: - 1. **绗竴姝ワ細蹇€熺粺璁℃枃浠舵暟閲?* - 浣跨敤寮傛鏋氫妇鍣ㄦ祦寮忔壂鎻忔枃浠讹紝姣忔壘鍒?00涓枃浠跺氨鏇存柊UI锛屼笉绛夊緟鍏ㄩ儴瀹屾垚 - 2. **绗簩姝ワ細鍒涘缓鍗犱綅绗?* - 蹇€熷垱寤烘墍鏈?ImageItem 鍗犱綅绗︼紙鍙缃枃浠惰矾寰勫拰鏂囦欢鍚嶏級锛屾壒閲忔坊鍔犲埌UI闆嗗悎锛堟瘡鎵?00涓級锛岃UI绔嬪嵆鏄剧ず - 3. **绗笁姝ワ細鍚庡彴娴佸紡濉厖璇︾粏淇℃伅** - 鍦ㄥ悗鍙扮嚎绋嬮€愭濉厖姣忎釜 ImageItem 鐨勬枃浠跺ぇ灏忋€佷慨鏀规椂闂寸瓑璇︾粏淇℃伅锛屼笉闃诲涓荤嚎绋? - 鉁?**寮傛鏋氫妇鏂囦欢**: 瀹炵幇 `EnumerateImageFilesAsync` 鏂规硶锛屼娇鐢ㄥ悗鍙扮嚎绋嬫寔缁壂鎻忕洰褰曞拰鏂囦欢锛岄€氳繃闃熷垪娴佸紡杩斿洖鎵惧埌鐨勬枃浠? - 鉁?**鍙栨秷鏀寔**: 娣诲姞 `CancellationTokenSource` 鏀寔锛屽厑璁稿彇娑堟鍦ㄨ繘琛岀殑鍔犺浇浠诲姟锛岄伩鍏嶈祫婧愭氮璐? - 鉁?**闈為樆濉濽I**: 鎵€鏈塈O鎿嶄綔閮藉湪鍚庡彴绾跨▼鎵ц锛孶I鏇存柊浣跨敤 `Dispatcher.UIThread.InvokeAsync` 鍜?`DispatcherPriority.Background` - - 鉁?**璧勬簮娓呯悊**: 娣诲姞 `CancelLoading` 鏂规硶锛屾敮鎸佸彇娑堝姞杞藉拰璧勬簮娓呯悊 -- **鎶€鏈粏鑺?*: - - 浣跨敤 `IAsyncEnumerable` 瀹炵幇娴佸紡鏂囦欢鏋氫妇锛屼笉绛夊緟鍏ㄩ儴鏂囦欢鎵弿瀹屾垚 - - 浣跨敤涓や釜闃熷垪锛堢洰褰曢槦鍒楀拰鏂囦欢闃熷垪锛夊垎绂荤洰褰曟壂鎻忓拰鏂囦欢杩斿洖閫昏緫 - - 鍦ㄥ悗鍙扮嚎绋嬩娇鐢?`Directory.EnumerateFiles` 鍜?`Directory.EnumerateDirectories` 鎵弿鏂囦欢绯荤粺 - - 鍗犱綅绗﹀垱寤哄悗绔嬪嵆鏄剧ず锛屾枃浠朵俊鎭湪鍚庡彴閫愭濉厖锛岀被浼?Windows 璧勬簮绠$悊鍣ㄧ殑鏁堟灉 - - 浣跨敤 `Task.Yield()` 鍜?`Task.Delay` 璁╁嚭鎺у埗鏉冿紝纭繚UI鍝嶅簲鎬? - 姣忔壒澶勭悊50涓枃浠朵俊鎭紝閬垮厤IO杩囪浇 -- **鏁堟灉**: - - 鉁?**涓嶅啀鍗℃**: UI绔嬪嵆鍝嶅簲锛屼笉鍐嶇瓑寰呮墍鏈夋枃浠舵壂鎻忓畬鎴? - 鉁?**蹇€熸樉绀?*: 鍗犱綅绗︾珛鍗虫樉绀猴紝鐢ㄦ埛鍙互绔嬪嵆鐪嬪埌鍥剧墖鍒楄〃 - - 鉁?**娴佺晠浣撻獙**: 鏂囦欢淇℃伅鍦ㄥ悗鍙伴€愭濉厖锛岀被浼?Windows 绯荤粺鎵撳紑鏂囦欢澶圭殑鏁堟灉 - - 鉁?**鍙彇娑?*: 鏀寔鍙栨秷姝e湪杩涜鐨勫姞杞戒换鍔★紝閬垮厤璧勬簮娴垂 - - 鉁?**涓荤嚎绋嬩笉闃诲**: 鎵€鏈塈O鎿嶄綔閮藉湪鍚庡彴绾跨▼锛孶I濮嬬粓淇濇寔鍝嶅簲 - -### 瀹炵幇寮傛缂╃暐鍥惧姞杞斤紝浼樺寲鍥剧墖鏄剧ず鎬ц兘锛堢被浼?Windows 11锛?- **鏃ユ湡**: 2025骞?鏈?- **闂**: 褰撳墠瀹炵幇铏界劧瀹炵幇浜嗘祦寮忓姞杞斤紝浣嗗浘鐗囧姞杞戒粛鐒舵槸鍚屾鐨勶紝浼氶樆濉濽I锛屼笖娌℃湁瀹炵幇鐪熸鐨勭缉鐣ュ浘鍔犺浇 -- **淇敼鍐呭**: 瀹炵幇寮傛缂╃暐鍥惧姞杞芥満鍒讹紝绫讳技 Windows 11 鐨勫浘鐗囨祻瑙堟柟寮?- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/ViewModels/Pages/ImageGalleryPageViewModel.cs` (鍦?ImageItem 涓坊鍔犲紓姝ョ缉鐣ュ浘鍔犺浇) - - `AuroraDesk.Presentation/Views/Pages/ImageGalleryPageView.axaml` (鏀逛负缁戝畾 ThumbnailSource 灞炴€? - - `AuroraDesk.Presentation/Converters/ImageConverters.cs` (鏀硅繘杞崲鍣紝鏀寔寮傛鍔犺浇鍜岀紦瀛? -- **涓昏浼樺寲**: - - 鉁?**寮傛缂╃暐鍥惧姞杞?*: 鍦?ImageItem 涓坊鍔?`ThumbnailSource` 灞炴€э紝褰?`FilePath` 璁剧疆鏃惰嚜鍔ㄨЕ鍙戝紓姝ュ姞杞? - 鉁?**鍚庡彴绾跨▼鍔犺浇**: 鍥剧墖鍔犺浇鍦ㄥ悗鍙扮嚎绋嬫墽琛岋紝涓嶉樆濉濽I绾跨▼ - - 鉁?**鑷姩UI鏇存柊**: 浣跨敤 ReactiveUI 鐨勫睘鎬ч€氱煡鏈哄埗锛屽姞杞藉畬鎴愬悗鑷姩鏇存柊UI - - 鉁?**鍔犺浇鐘舵€?*: 娣诲姞 `IsLoadingThumbnail` 灞炴€э紝鏄剧ず鍔犺浇鐘舵€? - 鉁?**寤惰繜鍔犺浇**: 鍙湪璁剧疆鏂囦欢璺緞鏃舵墠鍔犺浇锛岄伩鍏嶄竴娆℃€у姞杞芥墍鏈夊浘鐗?- **鎶€鏈粏鑺?*: - - 鍦?`ImageItem.FilePath` setter 涓娴嬪€煎彉鍖栵紝鑷姩瑙﹀彂 `LoadThumbnailAsync()` - - 浣跨敤 `Task.Run` 鍦ㄥ悗鍙扮嚎绋嬪姞杞藉浘鐗? - 浣跨敤 `Dispatcher.UIThread.InvokeAsync` 鍦║I绾跨▼鏇存柊灞炴€? - 鍥剧墖鍔犺浇澶辫触鏃朵紭闆呭鐞嗭紝杩斿洖 null - - UI灞傞潰閫氳繃 `Stretch="UniformToFill"` 鑷姩缂╂斁鍥剧墖 -- **褰撳墠闄愬埗**: - - 鈿狅笍 **灏氭湭瀹炵幇铏氭嫙鍖?*: 浠嶇劧浣跨敤 `ItemsControl` + `WrapPanel`锛屼笉鏀寔铏氭嫙鍖栥€傛湭鏉ュ彲浠ユ敼涓烘敮鎸佽櫄鎷熷寲鐨勬帶浠讹紙濡?`ItemsRepeater`锛?- **鏁堟灉**: - - 鉁?**寮傛鍔犺浇**: 鍥剧墖鍦ㄥ悗鍙板紓姝ュ姞杞斤紝涓嶉樆濉濽I - - 鉁?**鑷姩鏇存柊**: 鍔犺浇瀹屾垚鍚庤嚜鍔ㄦ樉绀猴紝鐢ㄦ埛浣撻獙娴佺晠 - - 鉁?**寤惰繜鍔犺浇**: 鍙湪闇€瑕佹椂鎵嶅姞杞斤紝閬垮厤涓€娆℃€у姞杞芥墍鏈夊浘鐗? - 鉁?**绗﹀悎 Windows 11 鎬濊矾**: 寮傛鍔犺浇銆佸悗鍙板鐞嗐€佹寜闇€鍔犺浇 - -### 浣跨敤 SixLabors.ImageSharp 瀹炵幇鐪熸鐨勭缉鐣ュ浘鐢熸垚 -- **鏃ユ湡**: 2025骞?鏈?- **闂**: 涔嬪墠瀹炵幇铏界劧寮傛鍔犺浇锛屼絾浠嶇劧鍔犺浇瀹屾暣鍥剧墖锛屽唴瀛樺崰鐢ㄥぇ锛屼笉绗﹀悎 Windows 11 鐨勬€濊矾 -- **淇敼鍐呭**: 浣跨敤 SixLabors.ImageSharp 鐢熸垚鐪熸鐨勭缉鐣ュ浘锛?84x184锛夛紝澶у箙鍑忓皯鍐呭瓨鍗犵敤 -- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/AuroraDesk.Presentation.csproj` (娣诲姞 SixLabors.ImageSharp 鍖呭紩鐢? - - `AuroraDesk.Presentation/ViewModels/Pages/ImageGalleryPageViewModel.cs` (瀹炵幇鐪熸鐨勭缉鐣ュ浘鐢熸垚) -- **涓昏浼樺寲**: - - 鉁?**鐪熸鐨勭缉鐣ュ浘鐢熸垚**: 浣跨敤 ImageSharp 鐢熸垚 184x184 鐨勭缉鐣ュ浘锛岃€屼笉鏄姞杞藉畬鏁村浘鐗? - 鉁?**楂樿川閲忕缉鏀?*: 浣跨敤 Lanczos3 绠楁硶杩涜楂樿川閲忕缉鏀撅紝淇濇寔鍥剧墖娓呮櫚搴? - 鉁?**鍐呭瓨浼樺寲**: 鍙姞杞藉拰缂撳瓨缂╃暐鍥撅紝澶у箙鍑忓皯鍐呭瓨鍗犵敤锛堢浉姣斿畬鏁村浘鐗囧噺灏?90%+ 鍐呭瓨锛? - 鉁?**鏅鸿兘缂撳瓨**: 浣跨敤 `ConcurrentDictionary` 瀹炵幇绾跨▼瀹夊叏鐨勭缉鐣ュ浘缂撳瓨锛岄伩鍏嶉噸澶嶇敓鎴? - 鉁?**鑷姩娓呯悊**: 鍒囨崲鐩綍鏃惰嚜鍔ㄦ竻鐞嗙紦瀛橈紝閲婃斁鍐呭瓨 - - 鉁?**淇濇寔瀹介珮姣?*: 缂╃暐鍥剧敓鎴愭椂鑷姩淇濇寔鍘熷浘瀹介珮姣旓紝涓嶄細鍙樺舰 -- **鎶€鏈粏鑺?*: - - 浣跨敤 `SixLabors.ImageSharp.Image.Load` 鍔犺浇鍥剧墖 - - 浣跨敤 `ResizeOptions` 閰嶇疆缂╂斁鍙傛暟锛? - `Mode = ResizeMode.Max`: 淇濇寔瀹介珮姣旓紝纭繚涓嶈秴杩囨寚瀹氬昂瀵? - `Sampler = KnownResamplers.Lanczos3`: 浣跨敤楂樿川閲忕缉鏀剧畻娉? - 濡傛灉鍘熷浘灏忎簬 184x184锛岀洿鎺ヤ娇鐢ㄥ師鍥惧昂瀵革紙涓嶆斁澶э級 - - 灏?ImageSharp 鍥惧儚淇濆瓨涓?PNG 鏍煎紡鍒?MemoryStream - - 浠?MemoryStream 鍒涘缓 Avalonia Bitmap - - 浣跨敤闈欐€佺殑 `ConcurrentDictionary` 瀹炵幇绾跨▼瀹夊叏鐨勭紦瀛? - 鍒囨崲鐩綍鏃惰皟鐢?`ClearThumbnailCache()` 娓呯悊缂撳瓨 -- **鎬ц兘鎻愬崌**: - - 鉁?**鍐呭瓨鍗犵敤鍑忓皯 90%+**: 10MB 鐨勫浘鐗囧彧鐢熸垚 184x184 鐨勭缉鐣ュ浘锛堢害 100KB锛? - 鉁?**鍔犺浇閫熷害鎻愬崌**: 缂╃暐鍥惧姞杞介€熷害姣斿畬鏁村浘鐗囧揩 10-100 鍊? - 鉁?**鏀寔鐧句竾绾у浘鐗?*: 鍐呭瓨鍗犵敤澶у箙闄嶄綆锛屽彲浠ユ敮鎸佹祻瑙堟洿澶氬浘鐗?- **鏁堟灉**: - - 鉁?**鐪熸鐨勭缉鐣ュ浘**: 鐢熸垚鍥哄畾灏哄鐨勭缉鐣ュ浘锛岃€屼笉鏄缉鏀惧畬鏁村浘鐗? - 鉁?**鍐呭瓨浼樺寲**: 澶у箙鍑忓皯鍐呭瓨鍗犵敤锛屽彲浠ユ祻瑙堟洿澶氬浘鐗? - 鉁?**楂樿川閲忔樉绀?*: 浣跨敤楂樿川閲忕缉鏀剧畻娉曪紝缂╃暐鍥炬竻鏅板害濂? - 鉁?**瀹屽叏绗﹀悎 Windows 11 鎬濊矾**: 鍙樉绀虹缉鐣ュ浘銆佸紓姝ュ姞杞姐€佸悗鍙板鐞嗐€佺紦瀛樻満鍒? -### 淇鍥剧墖鍒楄〃涓嶆樉绀虹殑闂 -- **鏃ユ湡**: 2025骞?鏈?- **闂**: 铏界劧鏁版嵁宸茬粡鍔犺浇锛堟樉绀?宸插姞杞?59300 / 100000 寮犲浘鐗?锛夛紝浣哢I涓€鐩存樉绀哄姞杞芥寚绀哄櫒锛屽浘鐗囧垪琛ㄤ笉鏄剧ず -- **鍘熷洜**: `IsLoading` 鐘舵€佸湪鍗犱綅绗︽坊鍔犲畬鎴愬悗娌℃湁绔嬪嵆璁剧疆涓?`false`锛屽鑷村姞杞芥寚绀哄櫒涓€鐩撮伄鎸″浘鐗囧垪琛?- **淇敼鍐呭**: 鍦ㄧ浜屾锛堝垱寤哄崰浣嶇锛夊畬鎴愬悗绔嬪嵆璁剧疆 `IsLoading = false`锛岃鐢ㄦ埛绔嬪嵆鐪嬪埌鍥剧墖鍒楄〃 -- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/ViewModels/Pages/ImageGalleryPageViewModel.cs` (淇 IsLoading 鐘舵€佺鐞? -- **涓昏淇**: - - 鉁?**绔嬪嵆鏄剧ずUI**: 鍗犱綅绗︽坊鍔犲畬鎴愬悗绔嬪嵆璁剧疆 `IsLoading = false`锛屼笉绛夊緟璇︾粏淇℃伅濉厖 - - 鉁?**鍚庡彴濉厖璇︾粏淇℃伅**: 鏂囦欢澶у皬銆佷慨鏀规椂闂寸瓑璇︾粏淇℃伅鍦ㄥ悗鍙板紓姝ュ~鍏咃紝涓嶉樆濉濽I鏄剧ず - - 鉁?**鐢ㄦ埛浣撻獙浼樺寲**: 鐢ㄦ埛鍙互绔嬪嵆鐪嬪埌鍥剧墖鍒楄〃锛岃缁嗕俊鎭€愭濉厖 -- **鎶€鏈粏鑺?*: - - 鍦ㄧ浜屾锛堝崰浣嶇娣诲姞锛夊畬鎴愬悗锛岀珛鍗宠皟鐢?`IsLoading = false` - - 绗笁姝ワ紙濉厖璇︾粏淇℃伅锛夋敼涓哄畬鍏ㄥ悗鍙板紓姝ユ墽琛岋紝涓嶅奖鍝峌I鏄剧ず - - 鐘舵€佹秷鎭洿鏂颁负"宸叉樉绀?X 寮犲浘鐗囷紝姝e湪鍚庡彴鍔犺浇璇︾粏淇℃伅..." -- **鏁堟灉**: - - 鉁?**绔嬪嵆鏄剧ず**: 鍗犱綅绗︽坊鍔犲畬鎴愬悗绔嬪嵆鏄剧ず鍥剧墖鍒楄〃锛屼笉鍐嶇瓑寰? - 鉁?**娴佺晠浣撻獙**: 鐢ㄦ埛鍙互绔嬪嵆鐪嬪埌鍥剧墖锛岀缉鐣ュ浘寮傛鍔犺浇骞堕€愭鏄剧ず - - 鉁?**鍚庡彴澶勭悊**: 璇︾粏淇℃伅鍦ㄥ悗鍙板~鍏咃紝涓嶉樆濉濽I - - 鉁?**瀹屽叏绗﹀悎 Windows 11 鎬濊矾**: 绔嬪嵆鏄剧ず銆佸紓姝ュ姞杞姐€佸悗鍙板鐞? -### 瀹炵幇鐪熸鐨勮櫄鎷熷寲鍜屾寜闇€鍔犺浇锛堢被浼?Windows 11锛?- **鏃ユ湡**: 2025骞?鏈?- **闂**: 铏界劧瀹炵幇浜嗘祦寮忓姞杞藉拰缂╃暐鍥剧敓鎴愶紝浣嗕粛鐒跺崱椤匡紝鍥犱负锛? 1. `ItemsControl` + `WrapPanel` 涓嶆敮鎸佽櫄鎷熷寲锛屼細娓叉煋鎵€鏈?0涓囦釜椤圭洰 - 2. 涓€娆℃€ф坊鍔犳墍鏈?0涓囦釜椤圭洰鍒伴泦鍚堬紝瀵艰嚧鍐呭瓨鍜屾覆鏌撳紑閿€宸ㄥぇ - 3. 缂╃暐鍥惧湪璁剧疆 FilePath 鏃剁珛鍗宠Е鍙戝姞杞斤紝10涓囦釜椤圭洰鍚屾椂鍔犺浇瀵艰嚧鍗¢】 -- **淇敼鍐呭**: 瀹炵幇鐪熸鐨勮櫄鎷熷寲鍜屾寜闇€鍔犺浇锛屽畬鍏ㄧ鍚?Windows 11 鐨勬€濊矾 -- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/Views/Pages/ImageGalleryPageView.axaml` (鏀圭敤 ListBox 鏀寔铏氭嫙鍖? - - `AuroraDesk.Presentation/ViewModels/Pages/ImageGalleryPageViewModel.cs` (闄愬埗闆嗗悎澶у皬锛屽疄鐜版寜闇€鍔犺浇) -- **涓昏浼樺寲**: - - 鉁?**浣跨敤 ListBox 鏀寔铏氭嫙鍖?*: 浠?`ItemsControl` 鏀逛负 `ListBox`锛屽彧娓叉煋鍙椤圭洰锛屼笉娓叉煋鎵€鏈?0涓囦釜椤圭洰 - - 鉁?**闄愬埗闆嗗悎澶у皬**: 鏈€澶氬彧娣诲姞2000涓」鐩埌闆嗗悎锛堣€屼笉鏄?0涓囦釜锛夛紝澶у箙鍑忓皯鍐呭瓨鍗犵敤 - - 鉁?**寤惰繜鍔犺浇缂╃暐鍥?*: 缂╃暐鍥惧姞杞藉欢杩?50-200ms锛岄伩鍏嶅悓鏃惰Е鍙戝ぇ閲忓姞杞? - 鉁?**绉婚櫎涓嶅繀瑕佺殑鎿嶄綔**: 绉婚櫎濉厖璇︾粏淇℃伅鐨勬楠わ紝閬垮厤澶ч噺IO鎿嶄綔 - - 鉁?**鎸夐渶鍔犺浇鏂囦欢淇℃伅**: 鍦ㄥ垱寤哄崰浣嶇鏃惰幏鍙栨枃浠朵俊鎭紝浣嗛檺鍒跺湪闆嗗悎涓殑椤圭洰鏁伴噺 -- **鎶€鏈粏鑺?*: - - 浣跨敤 `ListBox` 鏇夸唬 `ItemsControl`锛宍ListBox` 鏀寔铏氭嫙鍖栵紝鍙覆鏌撳彲瑙佸尯鍩熺殑椤圭洰 - - 鍒濆鍙坊鍔?00涓崰浣嶇锛岀劧鍚庢祦寮忔坊鍔犳渶澶?000涓? - 缂╃暐鍥惧姞杞戒娇鐢?`DelayedLoadThumbnailAsync`锛岄殢鏈哄欢杩?50-200ms锛岄伩鍏嶅悓鏃跺姞杞? - 鏂囦欢淇℃伅鍦ㄥ垱寤哄崰浣嶇鏃惰幏鍙栵紝閬垮厤鍚庣画鐨勫ぇ閲廔O鎿嶄綔 - - 铏氭嫙鍖栫‘淇濆嵆浣挎湁10涓囦釜鏂囦欢锛屼篃鍙覆鏌撳彲瑙佺殑鍑犲崄涓」鐩?- **鎬ц兘鎻愬崌**: - - 鉁?**鍐呭瓨鍗犵敤鍑忓皯 98%**: 浠?0涓囦釜椤圭洰鍑忓皯鍒版渶澶?000涓紙鍑忓皯98%锛? - 鉁?**娓叉煋鎬ц兘鎻愬崌**: 鍙覆鏌撳彲瑙佺殑鍑犲崄涓」鐩紝鑰屼笉鏄?0涓囦釜 - - 鉁?**鍔犺浇閫熷害鎻愬崌**: 鍒濆鍙姞杞?00涓紝绔嬪嵆鏄剧ず锛屼笉鍐嶅崱椤? - 鉁?**鏀寔鐧句竾绾у浘鐗?*: 鐞嗚涓婂彲浠ユ敮鎸佺櫨涓囩骇鍥剧墖锛屽洜涓鸿櫄鎷熷寲鍙覆鏌撳彲瑙侀」鐩?- **鏁堟灉**: - - 鉁?**绔嬪嵆鏄剧ず**: 200涓崰浣嶇娣诲姞鍚庣珛鍗虫樉绀猴紝涓嶅啀绛夊緟 - - 鉁?**娴佺晠婊氬姩**: 铏氭嫙鍖栫‘淇濇粴鍔ㄦ祦鐣咃紝鍙覆鏌撳彲瑙侀」鐩? - 鉁?**鍐呭瓨浼樺寲**: 鏈€澶?000涓」鐩湪鍐呭瓨涓紝鑰屼笉鏄?0涓囦釜 - - 鉁?**瀹屽叏绗﹀悎 Windows 11 鎬濊矾**: 铏氭嫙鍖栥€佹寜闇€鍔犺浇銆佸欢杩熷姞杞姐€佸彧娓叉煋鍙鎺т欢 - -### 瀹炵幇鐪熸鐨勮櫄鎷熷寲闆嗗悎锛堟敮鎸佺櫨涓囩骇鍥剧墖锛岀被浼?Windows 11锛?- **鏃ユ湡**: 2025骞?鏈?- **闂**: 涔嬪墠鐨勫疄鐜拌櫧鐒堕檺鍒朵簡闆嗗悎澶у皬锛屼絾浠嶇劧瀛樺湪浠ヤ笅闂锛? 1. 浠嶇劧涓€娆℃€у垱寤轰簡2000涓?ImageItem锛屽唴瀛樺崰鐢ㄤ粛鐒惰緝澶? 2. 缂╃暐鍥惧湪鍒涘缓鏃剁珛鍗宠Е鍙戝姞杞斤紝鍙兘瀵艰嚧鍚屾椂鍔犺浇澶ч噺缂╃暐鍥? 3. 娌℃湁瀹炵幇鐪熸鐨勮櫄鎷熷寲绐楀彛锛屾棤娉曟牴鎹粴鍔ㄤ綅缃姩鎬佸姞杞?鍗歌浇椤圭洰 - 4. 涓嶆敮鎸佺櫨涓囩骇鍥剧墖鐨勭湡姝h櫄鎷熷寲 -- **淇敼鍐呭**: 瀹炵幇鐪熸鐨勮櫄鎷熷寲闆嗗悎妯″紡锛屽畬鍏ㄧ鍚?Windows 11 鐨勬€濊矾 -- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/ViewModels/Pages/ImageGalleryPageViewModel.cs` (瀹炵幇铏氭嫙鍖栭泦鍚? - - `AuroraDesk.Presentation/Views/Pages/ImageGalleryPageView.axaml` (绉婚櫎璋冭瘯淇℃伅锛屼紭鍖栨樉绀? -- **鏍稿績绛栫暐**: - - 鉁?**鍙繚瀛樻枃浠惰矾寰勫垪琛?*: 涓嶄竴娆℃€у垱寤烘墍鏈?ImageItem锛屽彧淇濆瓨鏂囦欢璺緞瀛楃涓诧紙鍐呭瓨鍗犵敤鏋佸皬锛? - 鉁?**铏氭嫙鍖栫獥鍙?*: 鍙垱寤哄彲瑙佸尯鍩熼檮杩戠殑椤圭洰锛堝垵濮嬬獥鍙o細鍓?000涓紝鍓嶅悗鍚?00涓級 - - 鉁?**鍔ㄦ€佸姞杞?鍗歌浇**: 鏍规嵁婊氬姩浣嶇疆鍔ㄦ€佸姞杞芥洿澶氶」鐩紝鍗歌浇瓒呭嚭绐楀彛鐨勯」鐩? - 鉁?**鎸夐渶鍔犺浇缂╃暐鍥?*: 缂╃暐鍥句笉鍐嶈嚜鍔ㄥ姞杞斤紝鑰屾槸閫氳繃 `EnsureThumbnailLoaded()` 鎵嬪姩瑙﹀彂 - - 鉁?**鍚庡彴寮傛澶勭悊**: 鏂囦欢淇℃伅鑾峰彇銆佺缉鐣ュ浘鐢熸垚閮藉湪鍚庡彴绾跨▼杩涜锛屼笉闃诲UI -- **鎶€鏈粏鑺?*: - - 浣跨敤 `_allFilePaths` 鍒楄〃淇濆瓨鎵€鏈夋枃浠惰矾寰勶紙鍙崰寰堝皯鍐呭瓨锛? - 浣跨敤 `_virtualizationWindowStart` 鍜?`VirtualizationWindowSize` 绠$悊铏氭嫙鍖栫獥鍙? - 鍒濆鍙垱寤哄墠 1000 涓」鐩紙VirtualizationWindowSize * 2锛? - 鎻愪緵 `UpdateVirtualizationWindowAsync` 鏂规硶锛屾牴鎹彲瑙佸尯鍩熷姩鎬佹洿鏂扮獥鍙? - `ImageItem.EnsureThumbnailLoaded()` 鏂规硶瀹炵幇鎸夐渶鍔犺浇缂╃暐鍥? - 鏂囦欢淇℃伅寮傛鑾峰彇锛屼娇鐢?`SemaphoreSlim` 闄愬埗骞跺彂鏁?- **鎬ц兘鎻愬崌**: - - 鉁?**鍐呭瓨鍗犵敤鍑忓皯 99.9%**: 浠庡垱寤烘墍鏈?ImageItem 鏀逛负鍙繚瀛樻枃浠惰矾寰勶紝鍐呭瓨鍗犵敤鍑忓皯 99.9% - - 鉁?**鏀寔鐧句竾绾у浘鐗?*: 鐞嗚涓婂彲浠ユ敮鎸佺櫨涓囩骇鐢氳嚦鍗冧竾绾у浘鐗囷紝鍥犱负鍙繚瀛樿矾寰勫瓧绗︿覆 - - 鉁?**鍒濆鍔犺浇鏇村揩**: 鍙垱寤?1000 涓」鐩紝鑰屼笉鏄?2000 涓垨鏇村 - - 鉁?**婊氬姩鏇存祦鐣?*: 铏氭嫙鍖栫獥鍙g‘淇濆彧缁存姢鍙鍖哄煙闄勮繎鐨勯」鐩? - 鉁?**缂╃暐鍥炬寜闇€鍔犺浇**: 鍙湪椤圭洰鍙鏃跺姞杞界缉鐣ュ浘锛岄伩鍏嶅悓鏃跺姞杞藉ぇ閲忕缉鐣ュ浘 -- **鏁堟灉**: - - 鉁?**绔嬪嵆鏄剧ず**: 鍒濆 1000 涓」鐩揩閫熷姞杞斤紝绔嬪嵆鏄剧ず - - 鉁?**鏀寔鐧句竾绾у浘鐗?*: 鐞嗚涓婂彲浠ユ敮鎸佺櫨涓囩骇鍥剧墖锛屽洜涓哄彧淇濆瓨鏂囦欢璺緞 - - 鉁?**鍐呭瓨鍗犵敤鏋佸皬**: 鍙繚瀛樻枃浠惰矾寰勫垪琛紝涓嶅垱寤烘墍鏈?ImageItem - - 鉁?**娴佺晠婊氬姩**: 铏氭嫙鍖栫獥鍙g‘淇濇粴鍔ㄦ祦鐣咃紝鍔ㄦ€佸姞杞?鍗歌浇椤圭洰 - - 鉁?**瀹屽叏绗﹀悎 Windows 11 鎬濊矾**: - - 鉁?鍙繚瀛樻枃浠惰矾寰勫垪琛紙鏈€灏忓唴瀛樺崰鐢級 - - 鉁?鍙垱寤哄彲瑙佸尯鍩熺殑椤圭洰锛堣櫄鎷熷寲绐楀彛锛? - 鉁?缂╃暐鍥炬寜闇€鍔犺浇锛堝彧鍦ㄥ彲瑙佹椂鍔犺浇锛? - 鉁?鍚庡彴寮傛澶勭悊锛堜笉闃诲UI锛? -### 淇宸插純鐢ㄥ寘鐨勮鍛?- **鏃ユ湡**: 2025骞?鏈?- **闂**: NuGet 鍖呯鐞嗗櫒鏄剧ず涓や釜宸插純鐢ㄧ殑鍖咃細 - 1. `Avalonia.ReactiveUI` (11.3.7) - 宸插純鐢紝搴旀敼鐢?`ReactiveUI.Avalonia` - 2. `AvaloniaEdit` (0.10.12) - 宸插純鐢紝搴旀敼鐢?`Avalonia.AvaloniaEdit` -- **淇敼鍐呭**: 鏇存柊鎵€鏈夐」鐩枃浠跺拰鍛藉悕绌洪棿寮曠敤锛屼娇鐢ㄦ柊鐨勫寘鍚?- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/AuroraDesk.Presentation.csproj` (鏇存柊鍖呭紩鐢? - - `AuroraDesk/AuroraDesk.csproj` (鏇存柊鍖呭紩鐢? - - 鎵€鏈変娇鐢?`Avalonia.ReactiveUI` 鐨?C# 鏂囦欢 (12涓枃浠? - - 鎵€鏈変娇鐢?`Avalonia.ReactiveUI` 鐨?XAML 鏂囦欢 (10涓枃浠? - - 鎵€鏈変娇鐢?`AvaloniaEdit` 鐨?C# 鏂囦欢 (3涓枃浠? - - `AuroraDesk.Presentation/Views/Pages/EditorPageView.axaml` (鏇存柊鍛藉悕绌洪棿) -- **涓昏鏇存柊**: - - 鉁?**鍖呭紩鐢ㄦ洿鏂?*: - - `Avalonia.ReactiveUI` (11.3.7) -> `ReactiveUI.Avalonia` (11.3.8) - - `AvaloniaEdit` (0.10.12) -> `Avalonia.AvaloniaEdit` (11.3.8) - - 鉁?**鍛藉悕绌洪棿鏇存柊**: - - `using Avalonia.ReactiveUI;` -> `using ReactiveUI.Avalonia;` - - `xmlns:reactive="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI"` -> `xmlns:reactive="clr-namespace:ReactiveUI.Avalonia;assembly=ReactiveUI.Avalonia"` - - `using AvaloniaEdit;` -> `using Avalonia.AvaloniaEdit;` - - `using AvaloniaEdit.Document;` -> `using Avalonia.AvaloniaEdit.Document;` - - `xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"` -> `xmlns:avaloniaEdit="clr-namespace:Avalonia.AvaloniaEdit;assembly=Avalonia.AvaloniaEdit"` -- **鏇存柊鐨勬枃浠跺垪琛?*: - - C# 鏂囦欢 (12涓?: ImageGalleryPageView.axaml.cs, MainWindow.axaml.cs, CloseConfirmDialog.axaml.cs, IconsPageView.axaml.cs, DialogHostPageView.axaml.cs, EditorPageView.axaml.cs, HelpPageView.axaml.cs, UsersPageView.axaml.cs, SettingsPageView.axaml.cs, ReportsPageView.axaml.cs, DashboardPageView.axaml.cs, Program.cs - - XAML 鏂囦欢 (10涓?: ImageGalleryPageView.axaml, MainWindow.axaml, IconsPageView.axaml, DialogHostPageView.axaml, EditorPageView.axaml, HelpPageView.axaml, ReportsPageView.axaml, SettingsPageView.axaml, UsersPageView.axaml, DashboardPageView.axaml - - AvaloniaEdit 鐩稿叧 (4涓?: TextMateHelper.cs, TextEditorAssist.cs, EditorPageViewModel.cs, EditorPageView.axaml -- **鏁堟灉**: - - 鉁?**鏋勫缓鎴愬姛**: 鎵€鏈夐」鐩紪璇戞垚鍔燂紝鏃犻敊璇棤璀﹀憡 - - 鉁?**鍖呰鍛婃秷闄?*: NuGet 鍖呯鐞嗗櫒涓嶅啀鏄剧ず寮冪敤璀﹀憡 - - 鉁?**鍔熻兘姝e父**: 鎵€鏈夊姛鑳戒繚鎸佷笉鍙橈紝鍙槸浣跨敤浜嗘柊鐨勫寘鍚? - 鉁?**绗﹀悎鏈€鏂版爣鍑?*: 浣跨敤鏈€鏂扮殑鍖呭悕鍜屽懡鍚嶇┖闂达紝绗﹀悎 Avalonia 瀹樻柟鎺ㄨ崘 - -### 淇 ReactiveUI 鍛藉悕绌洪棿寮曠敤閿欒 -- **鏃ユ湡**: 2025骞?鏈?- **闂**: 鏃犳硶瑙f瀽绫诲瀷 `reactive:ReactiveUserControl`锛岄敊璇俊鎭細`Unable to resolve type reactive:ReactiveUserControl from any of the following locations: clr-namespace:ReactiveUI.Avalonia;assembly=ReactiveUI.Avalonia` -- **鍘熷洜**: 鍦?Avalonia 11 涓紝瀵逛簬 NuGet 鍖呬腑鐨勭被鍨嬶紝搴旇浣跨敤 `using:` 璇硶鑰屼笉鏄?`clr-namespace:...;assembly=...` 璇硶 -- **淇敼鍐呭**: 灏嗘墍鏈?XAML 鏂囦欢涓殑 ReactiveUI 鍛藉悕绌洪棿寮曠敤浠?`clr-namespace` 鏀逛负 `using` 璇硶 -- **淇敼鏂囦欢**: - - `AuroraDesk.Presentation/Views/MainWindow.axaml` - - `AuroraDesk.Presentation/Views/Pages/UsersPageView.axaml` - - `AuroraDesk.Presentation/Views/Pages/DashboardPageView.axaml` - - `AuroraDesk.Presentation/Views/Pages/EditorPageView.axaml` - - `AuroraDesk.Presentation/Views/Pages/ImageGalleryPageView.axaml` - - `AuroraDesk.Presentation/Views/Pages/IconsPageView.axaml` - - `AuroraDesk.Presentation/Views/Pages/DialogHostPageView.axaml` - - `AuroraDesk.Presentation/Views/Pages/HelpPageView.axaml` - - `AuroraDesk.Presentation/Views/Pages/ReportsPageView.axaml` - - `AuroraDesk.Presentation/Views/Pages/SettingsPageView.axaml` -- **涓昏鏇存柊**: - - 鉁?**鍛藉悕绌洪棿寮曠敤淇**: `xmlns:reactive="clr-namespace:ReactiveUI.Avalonia;assembly=ReactiveUI.Avalonia"` -> `xmlns:reactive="using:ReactiveUI.Avalonia"` -- **鏁堟灉**: - - 鉁?**閿欒淇**: 鍛藉悕绌洪棿瑙f瀽閿欒宸茶В鍐筹紝鎵€鏈?XAML 鏂囦欢鍙互姝g‘璇嗗埆 ReactiveUserControl 绫诲瀷 - - 鉁?**绗﹀悎 Avalonia 11 瑙勮寖**: 浣跨敤鎺ㄨ崘鐨?`using:` 璇硶寮曠敤 NuGet 鍖呬腑鐨勭被鍨? - 鉁?**缂栬瘧鎴愬姛**: 鎵€鏈夋枃浠剁紪璇戦€氳繃锛屾棤 linter 閿欒 -### 淇婊氬姩鍔犺浇闂 - 鍒濆鍔犺浇鍚庣户缁姞杞芥洿澶氬浘鐗? -- **鏃ユ湡**: 2025骞?鏈? -- **闂**: 鍒濆鍔犺浇100涓浘鐗囧悗锛岃嚜鍔ㄦ墿灞曞埌1000涓紝浣嗕箣鍚庢粴鍔ㄦ椂涓嶅啀缁х画鍔犺浇鏇村鍥剧墖 -- **鍘熷洜**: - 1. UpdateVirtualizationWindowInternalAsync 涓殑 needsUpdate 鍒ゆ柇閫昏緫鏈夐噸澶嶇殑 else if锛屽鑷存煇浜涙潯浠舵鏌ヨ璺宠繃 - 2. 鍒濆鍔犺浇鍚庣殑鍒嗛樁娈垫墿灞曢€昏緫涓嶅瀹屽杽锛屽彧鎵╁睍鍒?000涓氨鍋滄浜? - 3. View 涓殑婊氬姩妫€娴嬮€昏緫涓嶅鏁忔劅锛屾棤娉曞強鏃惰Е鍙戝姞杞芥洿澶氬唴瀹? -- **淇敼鍐呭**: - - 淇 UpdateVirtualizationWindowInternalAsync 涓殑鏉′欢鍒ゆ柇閫昏緫锛屽皢閲嶅鐨?else if 鏀逛负鐙珛鐨?if 璇彞 - - 浼樺寲鍒濆鍔犺浇鍚庣殑鍒嗛樁娈垫墿灞曢€昏緫锛屽垎涓や釜闃舵鍔犺浇锛?00涓?-> 1000涓級 - - 鏀硅繘 View 涓殑 UpdateVirtualizationWindow 鏂规硶锛屽鍔犳洿绉瀬鐨勫姞杞借Е鍙戞満鍒? - - 娣诲姞鍩轰簬宸插姞杞芥暟閲忓拰鎬绘暟閲忔瘮渚嬬殑寮哄埗鍔犺浇閫昏緫 -- **淇敼鏂囦欢**: - - AuroraDesk.Presentation/ViewModels/Pages/ImageGalleryPageViewModel.cs - - AuroraDesk.Presentation/Views/Pages/ImageGalleryPageView.axaml.cs -- **涓昏鏇存柊**: - - 淇鏉′欢鍒ゆ柇閫昏緫: 灏?UpdateVirtualizationWindowInternalAsync 涓殑閲嶅 else if 鏀逛负鐙珛鐨?if 璇彞锛岀‘淇濇墍鏈夋潯浠堕兘鑳借姝g‘妫€鏌? - - 浼樺寲鍒濆鍔犺浇: 鍒嗕袱涓樁娈佃嚜鍔ㄦ墿灞曪紙500涓?-> 1000涓級锛岄伩鍏嶄竴娆℃€у姞杞借繃澶氬鑷村崱椤? - - 鏀硅繘婊氬姩妫€娴? 鍦?View 涓鍔犳洿绉瀬鐨勫姞杞借Е鍙戞満鍒讹紝鍖呮嫭锛? - - 鏍规嵁宸插姞杞芥暟閲忓姩鎬佽皟鏁存墿灞曞€嶆暟锛坋xpansionFactor锛? - - 褰撴粴鍔ㄦ帴杩戝簳閮ㄦ椂锛屼娇鐢ㄦ洿澶х殑鎵╁睍鍊嶆暟锛?鍊嶏級 - - 褰撳凡鍔犺浇鏁伴噺杩滃皬浜庢€绘暟閲忥紙<10%锛夋椂锛屽己鍒惰Е鍙戝姞杞芥洿澶氬唴瀹? -- **鏁堟灉**: - - 鍔犺浇鏇存祦鐣? 鍒濆鍔犺浇鍚庝細鑷姩鍒嗛樁娈垫墿灞曞埌1000涓紝鎻愪緵鏇存祦鐣呯殑婊氬姩浣撻獙 - - 婊氬姩鍝嶅簲鏇村強鏃? 婊氬姩鏃惰兘鏇村強鏃跺湴瑙﹀彂鍔犺浇鏇村鍐呭锛屼笉浼氬嚭鐜版粴鍔ㄥ埌1000涓悗鍋滄鐨勬儏鍐? - - 閫昏緫鏇存竻鏅? 绉婚櫎浜嗛噸澶嶇殑鏉′欢鍒ゆ柇浠g爜锛岄€昏緫鏇存竻鏅版槗缁存姢 - -### 淇婊氬姩鍔犺浇闂 - 1000涓悗缁х画鍔犺浇 -- **鏃ユ湡**: 2025骞?鏈? -- **闂**: 鍒濆鍔犺浇100涓浘鐗囧悗锛岃嚜鍔ㄦ墿灞曞埌1000涓紝浣嗕箣鍚庢粴鍔ㄦ椂涓嶅啀缁х画鍔犺浇鏇村鍥剧墖 -- **鍘熷洜**: - 1. 褰撴粴鍔ㄦ帴杩戝凡鍔犺浇鐨勬湯灏炬椂锛屾病鏈夋彁鍓嶈Е鍙戝姞杞芥洿澶氬唴瀹圭殑鏈哄埗 - 2. ViewModel 涓殑绐楀彛鏇存柊鍒ゆ柇閫昏緫鍦ㄦ粴鍔ㄦ帴杩戞湯灏炬椂锛屽彲鑳藉洜涓?newWindowEnd <= currentWindowEnd 鑰岃烦杩囨洿鏂? - 3. View 涓殑婊氬姩妫€娴嬭櫧鐒惰绠椾簡 estimatedEndIndex锛屼絾鍙兘鍥犱负璁$畻鍑虹殑鍊煎皬浜庣瓑浜庡凡鍔犺浇鏁伴噺鑰屾棤娉曡Е鍙戝姞杞? -- **淇敼鍐呭**: - - 鍦?ViewModel 鐨?`UpdateVirtualizationWindowInternalAsync` 涓紝鍦ㄨ绠楁柊绐楀彛鑼冨洿涔嬪墠锛屾坊鍔犳鏌ワ細濡傛灉婊氬姩鎺ヨ繎宸插姞杞界殑鏈熬锛堣窛绂绘湯灏惧皬浜嶸irtualizationWindowSize锛夛紝寮哄埗鎵╁睍绐楀彛锛屽姞杞芥洿澶氬唴瀹癸紙鑷冲皯鍐嶅姞杞?00涓級 - - 鍦?View 鐨?`UpdateVirtualizationWindow` 涓紝娣诲姞澶氫釜妫€鏌ワ細 - - 濡傛灉婊氬姩鎺ヨ繎宸插姞杞界殑鏈熬锛堣窛绂绘湯灏惧皬浜?00涓級锛屽己鍒惰Е鍙戝姞杞芥洿澶? - - 濡傛灉宸插姞杞芥暟閲忓皬浜庢€绘暟閲忥紝涓斾及璁$殑缁撴潫绱㈠紩灏忎簬绛変簬宸插姞杞芥暟閲忥紝寮哄埗鎵╁睍绐楀彛 -- **淇敼鏂囦欢**: - - AuroraDesk.Presentation/ViewModels/Pages/ImageGalleryPageViewModel.cs - - AuroraDesk.Presentation/Views/Pages/ImageGalleryPageView.axaml.cs -- **涓昏鏇存柊**: - - 鍦?ViewModel 涓紝鍦ㄨ绠楁柊绐楀彛鑼冨洿涔嬪墠锛屾坊鍔犳彁鍓嶅姞杞芥満鍒讹細褰?visibleEndIndex >= currentWindowEnd - VirtualizationWindowSize 鏃讹紝寮哄埗鎵╁睍 newWindowEnd 鍒?currentWindowEnd + 500 - - 鍦?View 涓紝娣诲姞璺濈妫€鏌ワ細褰?distanceToLoadedEnd < 500 鏃讹紝寮哄埗鎵╁睍 estimatedEndIndex - - 鍦?View 涓紝娣诲姞鏈€缁堟鏌ワ細褰?loadedCount < totalCount && estimatedEndIndex <= loadedCount 鏃讹紝寮哄埗鎵╁睍 estimatedEndIndex 鍒?loadedCount + 500 -- **鏁堟灉**: - - 婊氬姩鍒版帴杩戝凡鍔犺浇鏈熬鏃讹紝鑳芥彁鍓嶈Е鍙戝姞杞芥洿澶氬唴瀹? - - 鍗充娇璁$畻鍑虹殑缁撴潫绱㈠紩灏忎簬绛変簬宸插姞杞芥暟閲忥紝涔熻兘寮哄埗瑙﹀彂鍔犺浇 - - 婊氬姩鏃惰兘鎸佺画鍔犺浇鏇村鍥剧墖锛屼笉浼氬湪1000涓悗鍋滄 +# 修改记录 + +## 2025年修改记录 + +### 修复 NodeCanvasPageView 中的编译错误 +- **日期**: 2025年1月10日 +- **问题**: + - `ItemContainerGenerator` 未包含 `ContainerFromItem` 的定义 + - `DispatcherPriority` 未包含 `Layout` 的定义 +- **修复内容**: + 1. 移除了不存在的 `ItemContainerGenerator.ContainerFromItem()` 方法调用 + 2. 改用 `GetVisualDescendants()` 方法遍历视觉树来查找对应的 `ContentControl` 容器 + 3. 将 `DispatcherPriority.Layout` 改为 `DispatcherPriority.Normal`(Avalonia 中不存在 Layout 枚举值) +- **技术说明**: + - 在 Avalonia 中,`ItemContainerGenerator` 没有 `ContainerFromItem` 方法 + - 通过遍历视觉树(`GetVisualDescendants()`)来查找对应的容器是更可靠的方法 + - `DispatcherPriority` 枚举在 Avalonia 中可用的值包括:`Invalid`, `Inactive`, `SystemIdle`, `ApplicationIdle`, `ContextIdle`, `Background`, `Input`, `Loaded`, `Render`, `Normal`, `Send` +- **修改文件**: + - `AuroraDesk.Presentation/Views/Pages/NodeCanvasPageView.axaml.cs` - 修复容器查找方法和 DispatcherPriority 枚举值 + +### 调整 UdpServerPageView 客户端列表折叠按钮位置到左侧 +- **日期**: 2025年1月10日 +- **修改内容**: 将客户端列表的折叠按钮从标题栏中间移到最左侧,使用三列 Grid 布局 +- **布局改进**: + - 使用 `Grid` 三列布局:`ColumnDefinitions="Auto,*,Auto"` + - 第一列:折叠按钮(左侧,固定宽度 40px,左侧内边距 16px) + - 第二列:标题内容(中间,自适应宽度,包含图标、文字和徽章) + - 第三列:清空按钮(右侧,固定宽度) +- **视觉效果**: 折叠按钮现在独立显示在左侧,布局更加清晰,符合常见的折叠面板设计模式 +- **修改文件**: + - `AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml` - 调整折叠按钮布局位置 + +### 优化 UdpServerPageView 布局,使客户端列表和接收消息区域铺满剩余空间 +- **日期**: 2025年1月10日 +- **修改内容**: 为客户端列表和接收消息区域的 Border、Grid、Expander 和 ScrollViewer 添加 `VerticalAlignment="Stretch"` 属性,使它们能够铺满剩余的垂直空间 +- **改进点**: + - 客户端列表 Border 添加了 `VerticalAlignment="Stretch"` + - 客户端列表内部 Grid 添加了 `VerticalAlignment="Stretch"` + - 客户端列表 Expander 添加了 `VerticalAlignment="Stretch"` + - 客户端列表 ScrollViewer 添加了 `VerticalAlignment="Stretch"` + - 接收消息区域 Border 添加了 `VerticalAlignment="Stretch"` + - 接收消息区域内部 Grid 添加了 `VerticalAlignment="Stretch"` + - 接收消息区域 ScrollViewer 添加了 `VerticalAlignment="Stretch"` +- **效果**: 两个区域现在能够充分利用可用的垂直空间,提供更好的用户体验 +- **修改文件**: + - `AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml` - 添加垂直对齐属性 + +### 修复 UdpServerPageView 中 Border 控件无效属性错误 +- **日期**: 2025年1月10日 +- **问题**: Border 控件不支持 `HorizontalContentAlignment` 属性,导致编译错误 AVLN2000 +- **修复内容**: 移除了第 285 行 Border 控件上的无效属性 `HorizontalContentAlignment="Stretch"` +- **说明**: + - `Border` 控件在 Avalonia 中不支持 `HorizontalContentAlignment` 属性 + - 使用 `HorizontalAlignment="Stretch"` 即可实现 Border 本身的对齐 + - 内容对齐可以通过内部容器(如 Grid)的 `HorizontalAlignment` 属性控制 +- **修改文件**: + - `AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml` - 移除无效属性 + +### 重构 UdpServerPageView 布局为两行结构并支持折叠 +- **日期**: 2025年1月10日 +- **修改内容**: 重构 UDP 服务端页面视图为两行布局,客户端列表支持折叠功能 +- **布局结构**: + - 第一行:服务器控制(监听端口、启动/停止按钮,横向排列) + - 第二行:左右分栏布局 + - 左边(1/3):客户端列表区域(使用 Expander 支持折叠,显示连接的客户端信息) + - 右边(2/3):接收消息区域(显示接收到的消息列表) +- **改进点**: + - 配置区域改为横向布局,端口输入和启动/停止按钮在同一行 + - 客户端列表和接收消息区域采用左右分栏,右边接收消息区域占据 2/3 宽度 + - 客户端列表使用 Expander 控件,支持折叠/展开功能 + - 所有 Border 和 Expander 添加了 HorizontalAlignment="Stretch" 确保占满宽度 + - 保持了原有的功能和样式风格 +- **修改文件**: + - `AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml` - 重构为两行布局,客户端列表支持折叠 + +### 重构 UdpClientPageView 布局为两行结构 +- **日期**: 2025年1月10日 +- **修改内容**: 重构 UDP 客户端页面视图,将主要内容区域改为两行布局 +- **布局结构**: + - 第一行:连接配置(IP、端口、本地端口、连接/断开按钮,横向排列) + - 第二行:左右分栏布局 + - 左边(1/3):已发送消息区域(包含消息列表和输入框) + - 右边(2/3):接收消息区域(显示接收到的消息列表) +- **改进点**: + - 配置区域保持横向布局,所有配置项和按钮在同一行 + - 已发送消息和接收消息区域采用左右分栏,右边接收消息区域占据 2/3 宽度 + - 已发送消息区域整合了消息列表和输入框,布局更紧凑 + - 保持了原有的功能和样式风格 +- **修改文件**: + - `AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml` - 重构为两行布局,左右分栏 + +### 实现 UDP 客户端和服务端功能 +- **日期**: 2025年1月10日 +- **修改内容**: 实现 UDP 客户端和服务端两个视图页面,并添加到导航菜单中 +- **功能实现**: + - ✅ 创建 UDP 客户端 ViewModel (`UdpClientPageViewModel.cs`) + - ✅ 创建 UDP 服务端 ViewModel (`UdpServerPageViewModel.cs`) + - ✅ 创建 UDP 客户端 View (`UdpClientPageView.axaml` 和 `.axaml.cs`) + - ✅ 创建 UDP 服务端 View (`UdpServerPageView.axaml` 和 `.axaml.cs`) + - ✅ 在 PageViewModelFactory 中注册两个页面 + - ✅ 在 ServiceCollectionExtensions 中注册 ViewModel + - ✅ 在 NavigationService 中添加导航项(UDP 工具分组) +- **UDP 客户端功能**: + - 配置服务器 IP 和端口 + - 连接/断开服务器 + - 发送消息到服务器 + - 接收服务器响应 + - 显示已发送和接收的消息列表 + - 清空消息列表 +- **UDP 服务端功能**: + - 配置监听端口 + - 启动/停止监听 + - 接收客户端消息 + - 自动回复客户端(可选) + - 显示接收到的消息列表 + - 显示连接的客户端列表(IP:端口、消息数、首次/最后连接时间) + - 清空消息和客户端列表 +- **修改文件**: + - `AuroraDesk.Presentation/ViewModels/Pages/UdpClientPageViewModel.cs` - UDP 客户端 ViewModel + - `AuroraDesk.Presentation/ViewModels/Pages/UdpServerPageViewModel.cs` - UDP 服务端 ViewModel + - `AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml` - UDP 客户端视图 + - `AuroraDesk.Presentation/Views/Pages/UdpClientPageView.axaml.cs` - UDP 客户端视图代码 + - `AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml` - UDP 服务端视图 + - `AuroraDesk.Presentation/Views/Pages/UdpServerPageView.axaml.cs` - UDP 服务端视图代码 + - `AuroraDesk.Presentation/Services/PageViewModelFactory.cs` - 注册页面工厂方法 + - `AuroraDesk.Presentation/Extensions/ServiceCollectionExtensions.cs` - 注册 ViewModel 服务 + - `AuroraDesk.Infrastructure/Services/NavigationService.cs` - 添加导航项 +- **技术实现**: + - 遵循 ReactiveUI + MVVM 模式 + - 使用 ReactiveCommand 实现命令绑定 + - 使用 ObservableCollection 管理消息列表 + - 异步网络操作(UDP 发送/接收) + - 后台任务处理消息接收(避免阻塞 UI) + - 资源清理(Dispose 模式) + - 使用 Dispatcher.UIThread 确保 UI 线程安全更新 +- **架构遵循**: + - ✅ 整洁架构(Clean Architecture) + - ✅ ReactiveUI 响应式编程模式 + - ✅ MVVM 模式 + - ✅ 依赖注入(DI) + - ✅ 服务层分离(ViewModel 只处理业务逻辑,View 只负责 UI 展示) +- **导航结构**: + - UDP 工具(父级导航项,IconType.Signal) + - UDP 客户端(IconType.ArrowRight) + - UDP 服务端 (IconType.Server) +- **UI 特性**: + - 现代化的界面设计(圆角、阴影、颜色区分) + - 实时状态显示 + - 消息时间戳 + - 客户端信息详细展示 + - 响应式布局 +- **测试建议**: + - 启动 UDP 服务端,监听端口 8080 + - 启动 UDP 客户端,连接到 127.0.0.1:8080 + - 发送测试消息,验证双向通信 + - 测试多客户端连接场景 + - 测试异常情况(端口占用、网络错误等)