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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml.cs b/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml.cs
new file mode 100644
index 0000000..316870b
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Base;
+
+public partial class EntityEditWindow : Window
+{
+ private object? _entity;
+ private TaskCompletionSource? _tcs;
+
+ public EntityEditWindow()
+ {
+ InitializeComponent();
+ CancelBtn.Click += (_, _) => CloseWith(false);
+ SaveBtn.Click += (_, _) => CloseWith(true);
+ }
+
+ public Task ShowDialog(object entity, List fields, bool isEdit, Window owner)
+ {
+ _entity = entity;
+ Title = isEdit ? "编辑" : "新增";
+ BuildForm(fields);
+ _tcs = new TaskCompletionSource();
+ ShowDialog(owner);
+ return _tcs.Task;
+ }
+
+ private void BuildForm(List fields)
+ {
+ FormPanel.Children.Clear();
+ if (_entity is null) return;
+ var type = _entity.GetType();
+
+ foreach (var field in fields)
+ {
+ var prop = type.GetProperty(field.PropertyName);
+ if (prop is null) continue;
+ var value = prop.GetValue(_entity);
+
+ // 每行一个字段容器: 标题 + 输入框
+ var cell = new Border
+ {
+ Width = 340,
+ Padding = new Thickness(0, 4, 10, 4),
+ };
+
+ var row = new Grid
+ {
+ ColumnDefinitions = new ColumnDefinitions("100,*"),
+ };
+
+ var label = new TextBlock
+ {
+ Text = field.DisplayName + ":",
+ FontSize = 12,
+ Foreground = Brush.Parse("#8B9BB5"),
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ row.Children.Add(label);
+
+ Control input;
+ if (field.FieldType == FieldType.CheckBox)
+ {
+ var cb = new CheckBox
+ {
+ IsChecked = value is int iv ? iv == 1 : (value as bool? ?? false),
+ IsEnabled = !field.IsReadOnly,
+ Foreground = Brush.Parse("#DDE4F0"),
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ cb.IsCheckedChanged += (_, _) =>
+ prop.SetValue(_entity, cb.IsChecked == true ? 1 : 0);
+ input = cb;
+ }
+ else
+ {
+ var tb = new TextBox
+ {
+ Text = value?.ToString() ?? "",
+ IsReadOnly = field.IsReadOnly,
+ Watermark = field.DisplayName,
+ Background = Brush.Parse("#0C1622"),
+ Foreground = Brush.Parse("#DDE4F0"),
+ BorderBrush = Brush.Parse("#1A2F4A"),
+ };
+ if (field.FieldType == FieldType.Number)
+ {
+ tb.TextChanged += (_, _) =>
+ {
+ if (int.TryParse(tb.Text, out var num))
+ prop.SetValue(_entity, num);
+ };
+ }
+ else
+ {
+ tb.TextChanged += (_, _) =>
+ prop.SetValue(_entity, tb.Text);
+ }
+ input = tb;
+ }
+ Grid.SetColumn(input, 1);
+ row.Children.Add(input);
+
+ cell.Child = row;
+ FormPanel.Children.Add(cell);
+ }
+ }
+
+ private void CloseWith(bool result)
+ {
+ _tcs?.TrySetResult(result);
+ Close();
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml b/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml
new file mode 100644
index 0000000..c8e0a49
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml.cs b/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml.cs
new file mode 100644
index 0000000..9ff0eac
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Base;
+
+public partial class LocationInfoListView : UserControl
+{
+ public LocationInfoListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Base/MaterialInfoListView.axaml b/Sln.Wcs.UI/Views/Base/MaterialInfoListView.axaml
new file mode 100644
index 0000000..ad78f03
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Base/MaterialInfoListView.axaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Base/MaterialInfoListView.axaml.cs b/Sln.Wcs.UI/Views/Base/MaterialInfoListView.axaml.cs
new file mode 100644
index 0000000..b6ed06c
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Base/MaterialInfoListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Base;
+
+public partial class MaterialInfoListView : UserControl
+{
+ public MaterialInfoListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml b/Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml
new file mode 100644
index 0000000..0eaea2c
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml.cs b/Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml.cs
new file mode 100644
index 0000000..56670c2
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Base;
+
+public partial class StoreInfoListView : UserControl
+{
+ public StoreInfoListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Device/DeviceHostListView.axaml b/Sln.Wcs.UI/Views/Device/DeviceHostListView.axaml
new file mode 100644
index 0000000..b4d28d1
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Device/DeviceHostListView.axaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Device/DeviceHostListView.axaml.cs b/Sln.Wcs.UI/Views/Device/DeviceHostListView.axaml.cs
new file mode 100644
index 0000000..7231b5b
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Device/DeviceHostListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Device;
+
+public partial class DeviceHostListView : UserControl
+{
+ public DeviceHostListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml b/Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml
new file mode 100644
index 0000000..a918276
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml.cs b/Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml.cs
new file mode 100644
index 0000000..01b40a6
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Device;
+
+public partial class DeviceInfoListView : UserControl
+{
+ public DeviceInfoListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Device/DeviceParamListView.axaml b/Sln.Wcs.UI/Views/Device/DeviceParamListView.axaml
new file mode 100644
index 0000000..a54e72a
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Device/DeviceParamListView.axaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Device/DeviceParamListView.axaml.cs b/Sln.Wcs.UI/Views/Device/DeviceParamListView.axaml.cs
new file mode 100644
index 0000000..49de851
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Device/DeviceParamListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Device;
+
+public partial class DeviceParamListView : UserControl
+{
+ public DeviceParamListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/HomePageView.axaml b/Sln.Wcs.UI/Views/HomePageView.axaml
new file mode 100644
index 0000000..d1d5ed0
--- /dev/null
+++ b/Sln.Wcs.UI/Views/HomePageView.axaml
@@ -0,0 +1,253 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/HomePageView.axaml.cs b/Sln.Wcs.UI/Views/HomePageView.axaml.cs
new file mode 100644
index 0000000..0fe4669
--- /dev/null
+++ b/Sln.Wcs.UI/Views/HomePageView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace Sln.Wcs.UI.Views;
+
+public partial class HomePageView : UserControl
+{
+ public HomePageView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/Sln.Wcs.UI/Views/MainWindow.axaml b/Sln.Wcs.UI/Views/MainWindow.axaml
new file mode 100644
index 0000000..b22fb0d
--- /dev/null
+++ b/Sln.Wcs.UI/Views/MainWindow.axaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/MainWindow.axaml.cs b/Sln.Wcs.UI/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..e1a2e41
--- /dev/null
+++ b/Sln.Wcs.UI/Views/MainWindow.axaml.cs
@@ -0,0 +1,177 @@
+using System;
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Sln.Wcs.UI.ViewModels;
+
+namespace Sln.Wcs.UI.Views;
+
+public partial class MainWindow : Window
+{
+ private readonly NavigationViewModel _navVm;
+ private readonly List _openPopups = new();
+
+ public MainWindow(NavigationViewModel navigationVm)
+ {
+ InitializeComponent();
+ _navVm = navigationVm;
+ DataContext = _navVm;
+
+ _navVm.PageChanged += OnPageChanged;
+ BuildMenu();
+ _navVm.LoadDefaultPage();
+ }
+
+ private void BuildMenu()
+ {
+ MenuContainer.Children.Clear();
+ foreach (var item in _navVm.TopMenuItems)
+ {
+ var btn = new Button
+ {
+ Content = item.Label,
+ Background = Brushes.Transparent,
+ Foreground = Brush.Parse("#8B9BB5"),
+ FontSize = 13,
+ Padding = new Thickness(14, 12),
+ Cursor = new Cursor(StandardCursorType.Hand),
+ BorderThickness = new Thickness(0),
+ };
+
+ if (item.Action != null)
+ {
+ btn.Click += (_, _) => item.Action();
+ }
+
+ if (item.Children != null && item.Children.Count > 0)
+ {
+ var popup = new Popup
+ {
+ PlacementTarget = btn,
+ Placement = PlacementMode.Bottom,
+ IsLightDismissEnabled = false,
+ };
+
+ var popupBorder = new Border
+ {
+ Background = Brush.Parse("#0F1620"),
+ BorderBrush = Brush.Parse("#1A2F4A"),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(4),
+ MinWidth = 160,
+ };
+
+ var stack = new StackPanel { Margin = new Thickness(4) };
+ foreach (var sub in item.Children)
+ {
+ var subBtn = new Button
+ {
+ Content = sub.Label,
+ Background = Brushes.Transparent,
+ Foreground = Brush.Parse("#8B9BB5"),
+ FontSize = 12,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ HorizontalContentAlignment = HorizontalAlignment.Left,
+ Padding = new Thickness(12, 8),
+ BorderThickness = new Thickness(0),
+ Cursor = new Cursor(StandardCursorType.Hand),
+ };
+ subBtn.Click += (_, _) =>
+ {
+ popup.Close();
+ sub.Action();
+ };
+ subBtn.PointerEntered += (_, _) =>
+ subBtn.Background = Brush.Parse("#1B3A5C");
+ subBtn.PointerExited += (_, _) =>
+ subBtn.Background = Brushes.Transparent;
+ stack.Children.Add(subBtn);
+ }
+ popupBorder.Child = stack;
+ popup.Child = popupBorder;
+
+ // Hover 展开,离开按钮或 popup 时关闭
+ var closeTimer = new System.Timers.Timer(200) { AutoReset = false };
+ bool mouseInPopup = false;
+ bool mouseInButton = false;
+
+ popupBorder.PointerEntered += (_, _) => { mouseInPopup = true; closeTimer.Stop(); };
+ popupBorder.PointerExited += (_, _) => { mouseInPopup = false; TryClose(); };
+
+ btn.PointerEntered += (_, _) =>
+ {
+ mouseInButton = true;
+ closeTimer.Stop();
+ btn.Background = Brush.Parse("#1B3A5C");
+ btn.Foreground = Brush.Parse("#4FC3F7");
+ CloseAllPopups();
+ popup.Open();
+ };
+ btn.PointerExited += (_, _) =>
+ {
+ mouseInButton = false;
+ TryClose();
+ };
+
+ void TryClose()
+ {
+ closeTimer.Stop();
+ closeTimer.Start();
+ closeTimer.Elapsed += (_, _) =>
+ {
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ if (!mouseInButton && !mouseInPopup)
+ {
+ popup.Close();
+ }
+ });
+ };
+ }
+
+ popup.Closed += (_, _) =>
+ {
+ btn.Background = Brushes.Transparent;
+ btn.Foreground = Brush.Parse("#8B9BB5");
+ };
+ _openPopups.Add(popup);
+ }
+ else
+ {
+ btn.PointerEntered += (_, _) =>
+ {
+ btn.Background = Brush.Parse("#1B3A5C");
+ btn.Foreground = Brush.Parse("#4FC3F7");
+ };
+ btn.PointerExited += (_, _) =>
+ {
+ btn.Background = Brushes.Transparent;
+ btn.Foreground = Brush.Parse("#8B9BB5");
+ };
+ }
+
+ MenuContainer.Children.Add(btn);
+ }
+ }
+
+ private void CloseAllPopups()
+ {
+ foreach (var p in _openPopups)
+ p.Close();
+ }
+
+ private void OnPageChanged(Control? view)
+ {
+ if (view != null)
+ {
+ view.HorizontalAlignment = HorizontalAlignment.Stretch;
+ view.VerticalAlignment = VerticalAlignment.Stretch;
+ ContentArea.Child = view;
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Path/PathDetailsListView.axaml b/Sln.Wcs.UI/Views/Path/PathDetailsListView.axaml
new file mode 100644
index 0000000..2c188dd
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Path/PathDetailsListView.axaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Path/PathDetailsListView.axaml.cs b/Sln.Wcs.UI/Views/Path/PathDetailsListView.axaml.cs
new file mode 100644
index 0000000..cf2cfe5
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Path/PathDetailsListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Path;
+
+public partial class PathDetailsListView : UserControl
+{
+ public PathDetailsListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Path/PathInfoListView.axaml b/Sln.Wcs.UI/Views/Path/PathInfoListView.axaml
new file mode 100644
index 0000000..4a21e31
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Path/PathInfoListView.axaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Path/PathInfoListView.axaml.cs b/Sln.Wcs.UI/Views/Path/PathInfoListView.axaml.cs
new file mode 100644
index 0000000..61f515b
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Path/PathInfoListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Path;
+
+public partial class PathInfoListView : UserControl
+{
+ public PathInfoListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Task/TaskDetailListView.axaml b/Sln.Wcs.UI/Views/Task/TaskDetailListView.axaml
new file mode 100644
index 0000000..2cd6347
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Task/TaskDetailListView.axaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Task/TaskDetailListView.axaml.cs b/Sln.Wcs.UI/Views/Task/TaskDetailListView.axaml.cs
new file mode 100644
index 0000000..bf79830
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Task/TaskDetailListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Task;
+
+public partial class TaskDetailListView : UserControl
+{
+ public TaskDetailListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml b/Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml
new file mode 100644
index 0000000..a233226
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml.cs b/Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml.cs
new file mode 100644
index 0000000..8a402c2
--- /dev/null
+++ b/Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Sln.Wcs.UI.ViewModels.Base;
+
+namespace Sln.Wcs.UI.Views.Task;
+
+public partial class TaskQueueListView : UserControl
+{
+ public TaskQueueListView()
+ {
+ InitializeComponent();
+ }
+
+ private void Edit_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("EditCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+
+ private void Delete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Button btn && btn.Tag != null && DataContext is ICrudPageViewModel vm)
+ {
+ vm.GetType().GetProperty("SelectedItem")?.SetValue(vm, btn.Tag);
+ (vm.GetType().GetProperty("DeleteCommand")?.GetValue(vm) as System.Windows.Input.ICommand)?.Execute(null);
+ }
+ }
+}
diff --git a/Sln.Wcs.UI/app.manifest b/Sln.Wcs.UI/app.manifest
new file mode 100644
index 0000000..526a6e6
--- /dev/null
+++ b/Sln.Wcs.UI/app.manifest
@@ -0,0 +1,9 @@
+
+
+
+
+ true
+ PerMonitorV2
+
+
+
diff --git a/Sln.Wcs.UI/appsettings.json b/Sln.Wcs.UI/appsettings.json
new file mode 100644
index 0000000..ebaa832
--- /dev/null
+++ b/Sln.Wcs.UI/appsettings.json
@@ -0,0 +1,18 @@
+{
+ "exclude": [
+ "**/bin",
+ "**/bower_components",
+ "**/jspm_packages",
+ "**/node_modules",
+ "**/obj",
+ "**/platforms"
+ ],
+ "apollo": {
+ "AppId": "SlnWcs",
+ "Env": "DEV",
+ "MetaServer": "http://119.45.202.115:4320",
+ "ConfigServer": [
+ "http://119.45.202.115:4320"
+ ]
+ }
+}
diff --git a/Sln.Wcs.sln b/Sln.Wcs.sln
index ea55fe1..95b01c1 100644
--- a/Sln.Wcs.sln
+++ b/Sln.Wcs.sln
@@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.HoistDispatcher", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.HikRoBotDispatcher", "Sln.Wcs.HikRoBotDispatcher\Sln.Wcs.HikRoBotDispatcher.csproj", "{A6387485-6B2E-4AA2-8FD1-F02AA5C5100C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.UI", "Sln.Wcs.UI\Sln.Wcs.UI.csproj", "{1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -98,6 +100,10 @@ Global
{A6387485-6B2E-4AA2-8FD1-F02AA5C5100C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6387485-6B2E-4AA2-8FD1-F02AA5C5100C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6387485-6B2E-4AA2-8FD1-F02AA5C5100C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE