From a97aca845e23aae2c7f670d29fcd298623b7978f Mon Sep 17 00:00:00 2001 From: WenJY Date: Thu, 11 Jun 2026 14:54:39 +0800 Subject: [PATCH] =?UTF-8?q?add=20-=20=E6=B7=BB=E5=8A=A0=20UI=20=E7=95=8C?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 6 +- Sln.Wcs.UI/App.axaml | 13 + Sln.Wcs.UI/App.axaml.cs | 110 ++++++++ Sln.Wcs.UI/Program.cs | 17 ++ Sln.Wcs.UI/Sln.Wcs.UI.csproj | 48 ++++ Sln.Wcs.UI/ViewLocator.cs | 30 +++ .../ViewModels/Base/CrudPageViewModel.cs | 140 ++++++++++ Sln.Wcs.UI/ViewModels/Base/FieldConfig.cs | 18 ++ .../ViewModels/Base/LocationInfoViewModel.cs | 39 +++ .../ViewModels/Base/MaterialInfoViewModel.cs | 32 +++ .../ViewModels/Base/StoreInfoViewModel.cs | 29 ++ .../ViewModels/Device/DeviceHostViewModel.cs | 33 +++ .../ViewModels/Device/DeviceInfoViewModel.cs | 33 +++ .../ViewModels/Device/DeviceParamViewModel.cs | 34 +++ Sln.Wcs.UI/ViewModels/HomePageViewModel.cs | 15 ++ Sln.Wcs.UI/ViewModels/MainViewModel.cs | 109 ++++++++ Sln.Wcs.UI/ViewModels/NavigationViewModel.cs | 117 ++++++++ .../ViewModels/Path/PathDetailsViewModel.cs | 32 +++ .../ViewModels/Path/PathInfoViewModel.cs | 33 +++ .../ViewModels/Task/TaskDetailViewModel.cs | 40 +++ .../ViewModels/Task/TaskQueueViewModel.cs | 39 +++ Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml | 28 ++ .../Views/Base/EntityEditWindow.axaml.cs | 120 +++++++++ .../Views/Base/LocationInfoListView.axaml | 52 ++++ .../Views/Base/LocationInfoListView.axaml.cs | 31 +++ .../Views/Base/MaterialInfoListView.axaml | 44 +++ .../Views/Base/MaterialInfoListView.axaml.cs | 31 +++ Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml | 38 +++ .../Views/Base/StoreInfoListView.axaml.cs | 31 +++ .../Views/Device/DeviceHostListView.axaml | 49 ++++ .../Views/Device/DeviceHostListView.axaml.cs | 31 +++ .../Views/Device/DeviceInfoListView.axaml | 46 ++++ .../Views/Device/DeviceInfoListView.axaml.cs | 31 +++ .../Views/Device/DeviceParamListView.axaml | 48 ++++ .../Views/Device/DeviceParamListView.axaml.cs | 31 +++ Sln.Wcs.UI/Views/HomePageView.axaml | 253 ++++++++++++++++++ Sln.Wcs.UI/Views/HomePageView.axaml.cs | 11 + Sln.Wcs.UI/Views/MainWindow.axaml | 41 +++ Sln.Wcs.UI/Views/MainWindow.axaml.cs | 177 ++++++++++++ .../Views/Path/PathDetailsListView.axaml | 42 +++ .../Views/Path/PathDetailsListView.axaml.cs | 31 +++ Sln.Wcs.UI/Views/Path/PathInfoListView.axaml | 44 +++ .../Views/Path/PathInfoListView.axaml.cs | 31 +++ .../Views/Task/TaskDetailListView.axaml | 54 ++++ .../Views/Task/TaskDetailListView.axaml.cs | 31 +++ Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml | 52 ++++ .../Views/Task/TaskQueueListView.axaml.cs | 31 +++ Sln.Wcs.UI/app.manifest | 9 + Sln.Wcs.UI/appsettings.json | 18 ++ Sln.Wcs.sln | 6 + 50 files changed, 2408 insertions(+), 1 deletion(-) create mode 100644 Sln.Wcs.UI/App.axaml create mode 100644 Sln.Wcs.UI/App.axaml.cs create mode 100644 Sln.Wcs.UI/Program.cs create mode 100644 Sln.Wcs.UI/Sln.Wcs.UI.csproj create mode 100644 Sln.Wcs.UI/ViewLocator.cs create mode 100644 Sln.Wcs.UI/ViewModels/Base/CrudPageViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Base/FieldConfig.cs create mode 100644 Sln.Wcs.UI/ViewModels/Base/LocationInfoViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Base/MaterialInfoViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Base/StoreInfoViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Device/DeviceHostViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Device/DeviceInfoViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Device/DeviceParamViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/HomePageViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/MainViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/NavigationViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Path/PathDetailsViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Path/PathInfoViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Task/TaskDetailViewModel.cs create mode 100644 Sln.Wcs.UI/ViewModels/Task/TaskQueueViewModel.cs create mode 100644 Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml create mode 100644 Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml create mode 100644 Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Base/MaterialInfoListView.axaml create mode 100644 Sln.Wcs.UI/Views/Base/MaterialInfoListView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml create mode 100644 Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Device/DeviceHostListView.axaml create mode 100644 Sln.Wcs.UI/Views/Device/DeviceHostListView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml create mode 100644 Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Device/DeviceParamListView.axaml create mode 100644 Sln.Wcs.UI/Views/Device/DeviceParamListView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/HomePageView.axaml create mode 100644 Sln.Wcs.UI/Views/HomePageView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/MainWindow.axaml create mode 100644 Sln.Wcs.UI/Views/MainWindow.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Path/PathDetailsListView.axaml create mode 100644 Sln.Wcs.UI/Views/Path/PathDetailsListView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Path/PathInfoListView.axaml create mode 100644 Sln.Wcs.UI/Views/Path/PathInfoListView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Task/TaskDetailListView.axaml create mode 100644 Sln.Wcs.UI/Views/Task/TaskDetailListView.axaml.cs create mode 100644 Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml create mode 100644 Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml.cs create mode 100644 Sln.Wcs.UI/app.manifest create mode 100644 Sln.Wcs.UI/appsettings.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 164ca10..ec2d8c5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,11 @@ "Bash(dotnet build *)", "Bash(git -C \"/Users/wenxiansheng/Public/WorkSpace/Mesnac/项目资料/研发项目/基于多场景应用的 WCS 通用平台研发/程序设计/wcs_core\" log --all --oneline --grep=\"HikRobot\" --grep=\"HikRoBot\")", "Bash(git -C \"/Users/wenxiansheng/Public/WorkSpace/Mesnac/项目资料/研发项目/基于多场景应用的 WCS 通用平台研发/程序设计/wcs_core\" log --all --oneline -S \"HikRobot\")", - "Bash(git -C \"/Users/wenxiansheng/Public/WorkSpace/Mesnac/项目资料/研发项目/基于多场景应用的 WCS 通用平台研发/程序设计/wcs_core\" log --all --oneline -S \"HikRoBot\")" + "Bash(git -C \"/Users/wenxiansheng/Public/WorkSpace/Mesnac/项目资料/研发项目/基于多场景应用的 WCS 通用平台研发/程序设计/wcs_core\" log --all --oneline -S \"HikRoBot\")", + "Bash(dotnet --version)", + "Bash(dotnet sln *)", + "Bash(dotnet clean *)", + "Bash(cat)" ] } } diff --git a/Sln.Wcs.UI/App.axaml b/Sln.Wcs.UI/App.axaml new file mode 100644 index 0000000..783f573 --- /dev/null +++ b/Sln.Wcs.UI/App.axaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/Sln.Wcs.UI/App.axaml.cs b/Sln.Wcs.UI/App.axaml.cs new file mode 100644 index 0000000..c5133fd --- /dev/null +++ b/Sln.Wcs.UI/App.axaml.cs @@ -0,0 +1,110 @@ +using System; +using System.Reflection; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Com.Ctrip.Framework.Apollo; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NeoSmart.Caching.Sqlite; +using Sln.Wcs.Repository; +using Sln.Wcs.Serilog; +using Sln.Wcs.UI.ViewModels; +using Sln.Wcs.UI.Views; +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; + +namespace Sln.Wcs.UI; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // ---- 独立初始化 WCS 后端服务 ---- + var services = new ServiceCollection(); + + // 1. Apollo 配置中心 + var basePath = AppContext.BaseDirectory; + var localConfig = new ConfigurationBuilder() + .SetBasePath(basePath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + + services.AddSingleton(localConfig); + + var apolloConfigSection = localConfig.GetSection("apollo"); + var apolloConfig = new ConfigurationBuilder() + .SetBasePath(basePath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddApollo(apolloConfigSection) + .AddDefault() + .Build(); + + // 替换为 Apollo 增强配置 + services.Remove(new ServiceDescriptor(typeof(IConfiguration), localConfig)); + services.AddSingleton(apolloConfig); + + // 2. 加载程序集并扫描注册 DI + var assemblies = new[] + { + Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Common.dll")), + Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Cache.dll")), + Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Repository.dll")), + }; + + services.Scan(scan => scan.FromAssemblies(assemblies) + .AddClasses() + .AsImplementedInterfaces() + .AsSelf() + .WithTransientLifetime()); + + // 3. 注册日志 + services.AddSingleton(typeof(SerilogHelper)); + + // 4. SqlSugar + FusionCache + services.AddSqlSugarSetup(); + services.AddFusionCache() + .WithSerializer(new FusionCacheNewtonsoftJsonSerializer()) + .WithDistributedCache(new SqliteCache(new SqliteCacheOptions + { + CachePath = apolloConfig["cachePath"]! + })); + + // 5. 注册 ViewModel + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + var serviceProvider = services.BuildServiceProvider(); + + // ---- 启动后初始化 ---- + + // 启用 Serilog + serviceProvider.UseSerilogExtensions(); + var config = serviceProvider.GetRequiredService(); + var log = serviceProvider.GetRequiredService(); + log.Info($"系统启动成功,日志存放位置:{config["logPath"]}"); + + // 创建主窗口 + desktop.MainWindow = new MainWindow(serviceProvider.GetRequiredService()); + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/Sln.Wcs.UI/Program.cs b/Sln.Wcs.UI/Program.cs new file mode 100644 index 0000000..3c0af79 --- /dev/null +++ b/Sln.Wcs.UI/Program.cs @@ -0,0 +1,17 @@ +using Avalonia; +using System; + +namespace Sln.Wcs.UI; + +sealed class Program +{ + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/Sln.Wcs.UI/Sln.Wcs.UI.csproj b/Sln.Wcs.UI/Sln.Wcs.UI.csproj new file mode 100644 index 0000000..fee3f5c --- /dev/null +++ b/Sln.Wcs.UI/Sln.Wcs.UI.csproj @@ -0,0 +1,48 @@ + + + + WinExe + net8.0 + enable + enable + true + app.manifest + true + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Sln.Wcs.UI/ViewLocator.cs b/Sln.Wcs.UI/ViewLocator.cs new file mode 100644 index 0000000..2b56951 --- /dev/null +++ b/Sln.Wcs.UI/ViewLocator.cs @@ -0,0 +1,30 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using CommunityToolkit.Mvvm.ComponentModel; +using System; + +namespace Sln.Wcs.UI; + +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? param) + { + if (param is null) + return new TextBlock { Text = "DataContext is null" }; + + var viewName = param.GetType().FullName! + .Replace("ViewModels", "Views") + .Replace("ViewModel", "Window"); + + var type = Type.GetType(viewName); + if (type is null) + return new TextBlock { Text = $"View not found: {viewName}" }; + + return (Control)Activator.CreateInstance(type)!; + } + + public bool Match(object? data) + { + return data is ObservableObject; + } +} diff --git a/Sln.Wcs.UI/ViewModels/Base/CrudPageViewModel.cs b/Sln.Wcs.UI/ViewModels/Base/CrudPageViewModel.cs new file mode 100644 index 0000000..23f2dda --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Base/CrudPageViewModel.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Sln.Wcs.Repository.service.@base; +using Sln.Wcs.UI.Views.Base; + +namespace Sln.Wcs.UI.ViewModels.Base; + +public interface ICrudPageViewModel +{ + void Load(); + Avalonia.Controls.Control CreateView(); +} + +public abstract partial class CrudPageViewModel : ObservableObject, ICrudPageViewModel where T : class, new() +{ + protected readonly IBaseService _service; + + [ObservableProperty] + private ObservableCollection _items = new(); + + [ObservableProperty] + private T? _selectedItem; + + [ObservableProperty] + private string _searchText = string.Empty; + + [ObservableProperty] + private string _pageTitle = string.Empty; + + [ObservableProperty] + private string _statusText = string.Empty; + + public abstract List FieldConfigs { get; } + public abstract Avalonia.Controls.Control CreateView(); + + /// + /// Expression to search by name/code field. Override to customize. + /// Returns null to skip filtering (load all). + /// + protected abstract Expression>? BuildSearchExpression(string search); + + protected CrudPageViewModel(IBaseService service) + { + _service = service; + AddCommand = new AsyncRelayCommand(Add); + EditCommand = new AsyncRelayCommand(Edit); + } + + [RelayCommand] + public void Load() + { + Console.WriteLine($"[CRUD] Load 调用, T={typeof(T).Name}, SearchText='{SearchText}'"); + try + { + List list; + if (string.IsNullOrWhiteSpace(SearchText)) + { + Console.WriteLine($"[CRUD] 调用 _service.Query() 无过滤"); + list = _service.Query(); + } + else + { + Console.WriteLine($"[CRUD] 调用 _service.Query(expression)"); + var exp = BuildSearchExpression(SearchText); + list = exp != null ? _service.Query(exp) : _service.Query(); + } + Console.WriteLine($"[CRUD] _service.Query() 返回 {list.Count} 条记录"); + Items = new ObservableCollection(list); + StatusText = $"共 {list.Count} 条记录"; + Console.WriteLine($"[CRUD] Items 已设置, Count={Items.Count}"); + } + catch (Exception ex) + { + Console.WriteLine($"[CRUD] Load 异常: {ex}"); + StatusText = $"加载失败: {ex.Message}"; + } + } + + public AsyncRelayCommand AddCommand { get; } + public AsyncRelayCommand EditCommand { get; } + + private async System.Threading.Tasks.Task Add() + { + var entity = new T(); + var editor = new EntityEditWindow(); + var result = await editor.ShowDialog(entity, FieldConfigs, false, GetMainWindow()); + if (result) + { + _service.Insert(entity); + Load(); + StatusText = "新增成功"; + } + } + + private async System.Threading.Tasks.Task Edit() + { + if (SelectedItem is null) return; + var editor = new EntityEditWindow(); + var result = await editor.ShowDialog(SelectedItem, FieldConfigs, true, GetMainWindow()); + if (result) + { + _service.Update(SelectedItem); + Load(); + StatusText = "编辑成功"; + } + } + + [RelayCommand] + private void Delete() + { + if (SelectedItem is null) return; + var prop = typeof(T).GetProperty("objId") ?? typeof(T).GetProperty("ObjId"); + if (prop is null) return; + var id = prop.GetValue(SelectedItem); + _service.DeleteById(id!); + Load(); + StatusText = "删除成功"; + } + + [RelayCommand] + private void Search() + { + Load(); + } + + private Avalonia.Controls.Window GetMainWindow() + { + return (Avalonia.Controls.Window)Avalonia.Application.Current! + .ApplicationLifetime!.GetType() + .GetProperty("MainWindow")! + .GetValue(Avalonia.Application.Current.ApplicationLifetime)!; + } +} diff --git a/Sln.Wcs.UI/ViewModels/Base/FieldConfig.cs b/Sln.Wcs.UI/ViewModels/Base/FieldConfig.cs new file mode 100644 index 0000000..226c7dc --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Base/FieldConfig.cs @@ -0,0 +1,18 @@ +namespace Sln.Wcs.UI.ViewModels.Base; + +public class FieldConfig +{ + public string PropertyName { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public FieldType FieldType { get; set; } = FieldType.Text; + public bool IsReadOnly { get; set; } + public bool IsRequired { get; set; } +} + +public enum FieldType +{ + Text, + Number, + Combo, + CheckBox +} diff --git a/Sln.Wcs.UI/ViewModels/Base/LocationInfoViewModel.cs b/Sln.Wcs.UI/ViewModels/Base/LocationInfoViewModel.cs new file mode 100644 index 0000000..85f2ccb --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Base/LocationInfoViewModel.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Base; + +public class LocationInfoViewModel : CrudPageViewModel +{ + public LocationInfoViewModel(IBaseLocationInfoService service) : base(service) + { + PageTitle = "库位信息管理"; + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "locationCode", DisplayName = "库位编号" }, + new() { PropertyName = "locationName", DisplayName = "库位名称" }, + new() { PropertyName = "locationArea", DisplayName = "库位区域" }, + new() { PropertyName = "storeCode", DisplayName = "所属仓库" }, + new() { PropertyName = "locationRows", DisplayName = "排", FieldType = FieldType.Number }, + new() { PropertyName = "locationColumns", DisplayName = "列", FieldType = FieldType.Number }, + new() { PropertyName = "locationLayers", DisplayName = "层", FieldType = FieldType.Number }, + new() { PropertyName = "agvPosition", DisplayName = "AGV定位" }, + new() { PropertyName = "materialCode", DisplayName = "物料编号" }, + new() { PropertyName = "palletBarcode", DisplayName = "托盘条码" }, + new() { PropertyName = "stackCount", DisplayName = "库存数量" }, + new() { PropertyName = "locationStatus", DisplayName = "库位状态", FieldType = FieldType.Number }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => (x.locationCode != null && x.locationCode.Contains(search)) + || (x.locationName != null && x.locationName.Contains(search)); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Base.LocationInfoListView(); +} diff --git a/Sln.Wcs.UI/ViewModels/Base/MaterialInfoViewModel.cs b/Sln.Wcs.UI/ViewModels/Base/MaterialInfoViewModel.cs new file mode 100644 index 0000000..29f47a5 --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Base/MaterialInfoViewModel.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Base; + +public class MaterialInfoViewModel : CrudPageViewModel +{ + public MaterialInfoViewModel(IBaseMaterialInfoService service) : base(service) + { + PageTitle = "物料信息管理"; + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "materialCode", DisplayName = "物料编号", IsRequired = true }, + new() { PropertyName = "materialName", DisplayName = "物料名称" }, + new() { PropertyName = "materialType", DisplayName = "物料类型" }, + new() { PropertyName = "materialBarcode", DisplayName = "物料条码" }, + new() { PropertyName = "minStorageCycle", DisplayName = "最短存放周期", FieldType = FieldType.Number }, + new() { PropertyName = "maxStorageCycle", DisplayName = "最长存放周期", FieldType = FieldType.Number }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => x.materialCode.Contains(search) || (x.materialName != null && x.materialName.Contains(search)); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Base.MaterialInfoListView(); +} diff --git a/Sln.Wcs.UI/ViewModels/Base/StoreInfoViewModel.cs b/Sln.Wcs.UI/ViewModels/Base/StoreInfoViewModel.cs new file mode 100644 index 0000000..7b8f37a --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Base/StoreInfoViewModel.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Base; + +public class StoreInfoViewModel : CrudPageViewModel +{ + public StoreInfoViewModel(IBaseStoreInfoService service) : base(service) + { + PageTitle = "仓库信息管理"; + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "storeCode", DisplayName = "仓库编号", IsRequired = true }, + new() { PropertyName = "storeName", DisplayName = "仓库名称" }, + new() { PropertyName = "storeType", DisplayName = "仓库类型", FieldType = FieldType.Number }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => x.storeCode.Contains(search) || (x.storeName != null && x.storeName.Contains(search)); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Base.StoreInfoListView(); +} diff --git a/Sln.Wcs.UI/ViewModels/Device/DeviceHostViewModel.cs b/Sln.Wcs.UI/ViewModels/Device/DeviceHostViewModel.cs new file mode 100644 index 0000000..dac2bd0 --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Device/DeviceHostViewModel.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Device; + +public class DeviceHostViewModel : CrudPageViewModel +{ + public DeviceHostViewModel(IBaseDeviceHostService service) : base(service) + { + PageTitle = "设备主机管理"; + System.Console.WriteLine("[VM] DeviceHostViewModel 构造, service 类型: " + service.GetType().FullName); + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "hostCode", DisplayName = "主机编号", IsRequired = true }, + new() { PropertyName = "hostName", DisplayName = "主机名称" }, + new() { PropertyName = "hostType", DisplayName = "主机类型", FieldType = FieldType.Number }, + new() { PropertyName = "hostIP", DisplayName = "主机IP" }, + new() { PropertyName = "hostPort", DisplayName = "主机端口", FieldType = FieldType.Number }, + new() { PropertyName = "hostPath", DisplayName = "主机路径" }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => x.hostCode.Contains(search) || x.hostName.Contains(search); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Device.DeviceHostListView(); +} diff --git a/Sln.Wcs.UI/ViewModels/Device/DeviceInfoViewModel.cs b/Sln.Wcs.UI/ViewModels/Device/DeviceInfoViewModel.cs new file mode 100644 index 0000000..8fa1b03 --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Device/DeviceInfoViewModel.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Device; + +public class DeviceInfoViewModel : CrudPageViewModel +{ + public DeviceInfoViewModel(IBaseDeviceInfoService service) : base(service) + { + PageTitle = "设备信息管理"; + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "deviceCode", DisplayName = "设备编号", IsRequired = true }, + new() { PropertyName = "deviceName", DisplayName = "设备名称" }, + new() { PropertyName = "deviceAlias", DisplayName = "设备别名" }, + new() { PropertyName = "deviceSerialNo", DisplayName = "设备序号", FieldType = FieldType.Number }, + new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Number }, + new() { PropertyName = "deviceStatus", DisplayName = "设备状态", FieldType = FieldType.Number }, + new() { PropertyName = "hostCode", DisplayName = "主机编号", IsRequired = true }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => x.deviceCode.Contains(search) || (x.deviceName != null && x.deviceName.Contains(search)); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Device.DeviceInfoListView(); +} diff --git a/Sln.Wcs.UI/ViewModels/Device/DeviceParamViewModel.cs b/Sln.Wcs.UI/ViewModels/Device/DeviceParamViewModel.cs new file mode 100644 index 0000000..95be9d2 --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Device/DeviceParamViewModel.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Device; + +public class DeviceParamViewModel : CrudPageViewModel +{ + public DeviceParamViewModel(IBaseDeviceParamService service) : base(service) + { + PageTitle = "设备参数管理"; + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "paramCode", DisplayName = "参数编号", IsRequired = true }, + new() { PropertyName = "deviceCode", DisplayName = "设备编号", IsRequired = true }, + new() { PropertyName = "paramName", DisplayName = "参数名称" }, + new() { PropertyName = "paramAddress", DisplayName = "参数地址" }, + new() { PropertyName = "paramType", DisplayName = "参数类型" }, + new() { PropertyName = "paramValue", DisplayName = "参数值", FieldType = FieldType.Number }, + new() { PropertyName = "operationType", DisplayName = "操作类型" }, + new() { PropertyName = "operationFrequency", DisplayName = "操作频率(ms)" }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => x.paramCode.Contains(search) || x.paramName.Contains(search); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Device.DeviceParamListView(); +} diff --git a/Sln.Wcs.UI/ViewModels/HomePageViewModel.cs b/Sln.Wcs.UI/ViewModels/HomePageViewModel.cs new file mode 100644 index 0000000..6a9f77a --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/HomePageViewModel.cs @@ -0,0 +1,15 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Sln.Wcs.UI.ViewModels; + +public partial class HomePageViewModel : ObservableObject +{ + [ObservableProperty] + private string _title = "基于多场景应用的 WCS 通用平台"; + + [ObservableProperty] + private string _version = "V1.0.0"; + + [ObservableProperty] + private string _description = "基于 .NET 8.0 + Avalonia UI 构建的跨平台仓库控制系统,支持多种硬件设备协议(海康 AGV、提升机、西门子/汇川 PLC),提供统一的调度、监控和管理能力。"; +} diff --git a/Sln.Wcs.UI/ViewModels/MainViewModel.cs b/Sln.Wcs.UI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..1bedb99 --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/MainViewModel.cs @@ -0,0 +1,109 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Configuration; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.Serilog; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; + +namespace Sln.Wcs.UI.ViewModels; + +public partial class MainViewModel : ObservableObject +{ + private readonly SerilogHelper _log; + private readonly IConfiguration _config; + private readonly IBaseDeviceInfoService _deviceInfoService; + private readonly ISqlSugarClient _db; + + [ObservableProperty] + private string _appTitle = "基于多场景应用的 WCS 通用平台"; + + [ObservableProperty] + private string _appVersion = "V1.0.0"; + + [ObservableProperty] + private string _statusMessage = "系统就绪,点击'加载设备'获取设备列表"; + + [ObservableProperty] + private int _deviceCount; + + [ObservableProperty] + private ObservableCollection _devices = new(); + + public MainViewModel( + SerilogHelper log, + IConfiguration config, + IBaseDeviceInfoService deviceInfoService, + ISqlSugarClient db) + { + _log = log; + _config = config; + _deviceInfoService = deviceInfoService; + _db = db; + LoadDevicesCommand = new AsyncRelayCommand(LoadDevicesAsync); + } + + public AsyncRelayCommand LoadDevicesCommand { get; } + + private async System.Threading.Tasks.Task LoadDevicesAsync() + { + StatusMessage = "正在加载设备列表..."; + try + { + var devices = await System.Threading.Tasks.Task.Run(() => + _deviceInfoService.GetDeviceInfos(x => x.isFlag == 1).ToList()); + + Devices.Clear(); + foreach (var d in devices) + { + Devices.Add(new DeviceSummary + { + DeviceCode = d.deviceCode, + DeviceName = d.deviceName ?? "", + DeviceType = ParseDeviceType(d.deviceType), + HostCode = d.hostCode, + IsFlag = d.isFlag == 1 + }); + } + + DeviceCount = Devices.Count; + StatusMessage = $"已加载 {DeviceCount} 台设备"; + _log.Info($"UI: 设备列表加载完成,共{DeviceCount}台"); + } + catch (Exception ex) + { + StatusMessage = $"加载设备失败: {ex.Message}"; + _log.Error($"UI: 设备列表加载失败 - {ex.Message}"); + } + } + + [RelayCommand] + private void RefreshStatus() + { + var apolloEnv = _config["apollo:Env"] ?? "未知"; + var logPath = _config["logPath"] ?? "未配置"; + StatusMessage = $"Apollo环境: {apolloEnv} | 日志路径: {logPath} | 已连接设备: {DeviceCount}"; + } + + private static string ParseDeviceType(int? deviceType) => deviceType switch + { + 0 => "输送线", + 1 => "AGV", + 2 => "提升机", + _ => "未知" + }; +} + +public class DeviceSummary +{ + public string DeviceCode { get; set; } = string.Empty; + public string DeviceName { get; set; } = string.Empty; + public string DeviceType { get; set; } = string.Empty; + public string HostCode { get; set; } = string.Empty; + public bool IsFlag { get; set; } +} diff --git a/Sln.Wcs.UI/ViewModels/NavigationViewModel.cs b/Sln.Wcs.UI/ViewModels/NavigationViewModel.cs new file mode 100644 index 0000000..2c4e602 --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/NavigationViewModel.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Sln.Wcs.UI.ViewModels.Base; +using Sln.Wcs.UI.ViewModels.Device; +using Sln.Wcs.UI.ViewModels.Path; +using Sln.Wcs.UI.ViewModels.Task; +using Sln.Wcs.UI.Views; + +namespace Sln.Wcs.UI.ViewModels; + +public partial class NavigationViewModel : ObservableObject +{ + private readonly IServiceProvider _sp; + private readonly Dictionary _vmCache = new(); + private readonly Dictionary _viewCache = new(); + private Control? _homeView; + + public ObservableCollection TopMenuItems { get; } = new(); + + public event Action? PageChanged; + + public NavigationViewModel(IServiceProvider sp) + { + _sp = sp; + BuildMenu(); + } + + public void LoadDefaultPage() => ShowHome(); + + private void BuildMenu() + { + TopMenuItems.Clear(); + TopMenuItems.Add(new TopMenuItem("首页", ShowHome)); + TopMenuItems.Add(new TopMenuItem("基础数据", new List + { + new("库位信息", () => NavigateTo()), + new("物料信息", () => NavigateTo()), + new("仓库信息", () => NavigateTo()), + })); + TopMenuItems.Add(new TopMenuItem("设备管理", new List + { + new("设备主机", () => NavigateTo()), + new("设备信息", () => NavigateTo()), + new("设备参数", () => NavigateTo()), + })); + TopMenuItems.Add(new TopMenuItem("路径管理", new List + { + new("路径信息", () => NavigateTo()), + new("路径明细", () => NavigateTo()), + })); + TopMenuItems.Add(new TopMenuItem("任务管理", new List + { + new("任务队列", () => NavigateTo()), + new("任务明细", () => NavigateTo()), + })); + } + + private void ShowHome() + { + if (_homeView == null) + { + var vm = _sp.GetRequiredService(); + _homeView = new HomePageView { DataContext = vm }; + } + PageChanged?.Invoke(_homeView); + } + + private void NavigateTo() where T : ICrudPageViewModel + { + var type = typeof(T); + if (!_vmCache.TryGetValue(type, out var vm)) + { + vm = _sp.GetRequiredService(); + _vmCache[type] = vm; + } + if (!_viewCache.TryGetValue(type, out var view)) + { + view = vm.CreateView(); + view.DataContext = vm; + _viewCache[type] = view; + } + PageChanged?.Invoke(view); + vm.Load(); + } +} + +public class TopMenuItem +{ + public string Label { get; set; } + public List? Children { get; set; } + public Action? Action { get; set; } + + public TopMenuItem(string label, Action action) + { + Label = label; Action = action; + } + + public TopMenuItem(string label, List children) + { + Label = label; Children = children; + } +} + +public class SubMenuItem +{ + public string Label { get; set; } + public Action Action { get; set; } + + public SubMenuItem(string label, Action action) + { + Label = label; Action = action; + } +} diff --git a/Sln.Wcs.UI/ViewModels/Path/PathDetailsViewModel.cs b/Sln.Wcs.UI/ViewModels/Path/PathDetailsViewModel.cs new file mode 100644 index 0000000..194fc6a --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Path/PathDetailsViewModel.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Path; + +public class PathDetailsViewModel : CrudPageViewModel +{ + public PathDetailsViewModel(IBasePathDetailsService service) : base(service) + { + PageTitle = "路径明细管理"; + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "pathCode", DisplayName = "路径编号" }, + new() { PropertyName = "pathName", DisplayName = "路径名称" }, + new() { PropertyName = "startPoint", DisplayName = "起点" }, + new() { PropertyName = "endPoint", DisplayName = "终点" }, + new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Number }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => (x.pathCode != null && x.pathCode.Contains(search)) + || (x.pathName != null && x.pathName.Contains(search)); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Path.PathDetailsListView(); +} diff --git a/Sln.Wcs.UI/ViewModels/Path/PathInfoViewModel.cs b/Sln.Wcs.UI/ViewModels/Path/PathInfoViewModel.cs new file mode 100644 index 0000000..9325ef3 --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Path/PathInfoViewModel.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Path; + +public class PathInfoViewModel : CrudPageViewModel +{ + public PathInfoViewModel(IBasePathInfoService service) : base(service) + { + PageTitle = "路径信息管理"; + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "pathCode", DisplayName = "路径编号" }, + new() { PropertyName = "pathName", DisplayName = "路径名称" }, + new() { PropertyName = "pathType", DisplayName = "路径类型", FieldType = FieldType.Number }, + new() { PropertyName = "pathCategory", DisplayName = "路径类别", FieldType = FieldType.Number }, + new() { PropertyName = "startPoint", DisplayName = "起点" }, + new() { PropertyName = "endPoint", DisplayName = "终点" }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => (x.pathCode != null && x.pathCode.Contains(search)) + || (x.pathName != null && x.pathName.Contains(search)); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Path.PathInfoListView(); +} diff --git a/Sln.Wcs.UI/ViewModels/Task/TaskDetailViewModel.cs b/Sln.Wcs.UI/ViewModels/Task/TaskDetailViewModel.cs new file mode 100644 index 0000000..9e018d0 --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Task/TaskDetailViewModel.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Task; + +public class TaskDetailViewModel : CrudPageViewModel +{ + public TaskDetailViewModel(ILiveTaskDetailService service) : base(service) + { + PageTitle = "任务明细管理"; + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "taskCode", DisplayName = "任务编号" }, + new() { PropertyName = "pathCode", DisplayName = "路径编号" }, + new() { PropertyName = "materialCode", DisplayName = "物料编号" }, + new() { PropertyName = "palletBarcode", DisplayName = "托盘条码" }, + new() { PropertyName = "materialBarcode", DisplayName = "物料条码" }, + new() { PropertyName = "materialCount", DisplayName = "物料数量", FieldType = FieldType.Number }, + new() { PropertyName = "taskType", DisplayName = "任务类型", FieldType = FieldType.Number }, + new() { PropertyName = "taskCategory", DisplayName = "任务类别", FieldType = FieldType.Number }, + new() { PropertyName = "startPoint", DisplayName = "起始位置" }, + new() { PropertyName = "endPoint", DisplayName = "结束位置" }, + new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Number }, + new() { PropertyName = "isValidate", DisplayName = "校验物料", FieldType = FieldType.CheckBox }, + new() { PropertyName = "taskStatus", DisplayName = "任务状态", FieldType = FieldType.Number }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => (x.taskCode != null && x.taskCode.Contains(search)) + || (x.materialCode != null && x.materialCode.Contains(search)); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Task.TaskDetailListView(); +} diff --git a/Sln.Wcs.UI/ViewModels/Task/TaskQueueViewModel.cs b/Sln.Wcs.UI/ViewModels/Task/TaskQueueViewModel.cs new file mode 100644 index 0000000..685b8af --- /dev/null +++ b/Sln.Wcs.UI/ViewModels/Task/TaskQueueViewModel.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Sln.Wcs.UI.ViewModels.Base; + +namespace Sln.Wcs.UI.ViewModels.Task; + +public class TaskQueueViewModel : CrudPageViewModel +{ + public TaskQueueViewModel(ILiveTaskQueueService service) : base(service) + { + PageTitle = "任务队列管理"; + } + + public override List FieldConfigs => new() + { + new() { PropertyName = "taskCode", DisplayName = "任务编号" }, + new() { PropertyName = "materialCode", DisplayName = "物料编号" }, + new() { PropertyName = "palletBarcode", DisplayName = "托盘条码" }, + new() { PropertyName = "materialBarcode", DisplayName = "物料条码" }, + new() { PropertyName = "materialCount", DisplayName = "物料数量", FieldType = FieldType.Number }, + new() { PropertyName = "taskType", DisplayName = "任务类型", FieldType = FieldType.Number }, + new() { PropertyName = "taskCategory", DisplayName = "任务类别", FieldType = FieldType.Number }, + new() { PropertyName = "startPoint", DisplayName = "起始位置" }, + new() { PropertyName = "endPoint", DisplayName = "结束位置" }, + new() { PropertyName = "pathCode", DisplayName = "路径编号" }, + new() { PropertyName = "taskStatus", DisplayName = "任务状态", FieldType = FieldType.Number }, + new() { PropertyName = "taskSteps", DisplayName = "任务步骤", FieldType = FieldType.Number }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + protected override Expression>? BuildSearchExpression(string search) + => x => (x.taskCode != null && x.taskCode.Contains(search)) + || (x.materialCode != null && x.materialCode.Contains(search)); + + public override Avalonia.Controls.Control CreateView() => new Sln.Wcs.UI.Views.Task.TaskQueueListView(); +} diff --git a/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml b/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml new file mode 100644 index 0000000..dff2e85 --- /dev/null +++ b/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml @@ -0,0 +1,28 @@ + + + + + + + + +