diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7635d3a..be750ac 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -21,7 +21,22 @@ "Bash(dotnet new *)", "Bash(dotnet add *)", "Bash(python3 *)", - "Bash(git restore *)" + "Bash(git restore *)", + "Bash(sort -t'\"' -k2 -n)", + "Bash(git checkout *)", + "Bash(sed -n '41,65p' Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml)", + "Bash(sed -n '58,75p' Sln.Wcs.UI/Views/Task/TaskQueueListView.axaml)", + "Bash(sed -n '11,28p' Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml)", + "Bash(sed -n '42,50p' Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml)", + "Bash(sed -n '64,68p' Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml)", + "Bash(sed -n '82,86p' Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml)", + "Bash(sed -n '62,67p' Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml)", + "Bash(sed -n '80,84p' Sln.Wcs.UI/Views/Base/StoreInfoListView.axaml)", + "Bash(sed -i '' 's|Grid\\\\.Column=\"1\" Text=\"{Binding SlidePanelTitle}\"|Grid.Column=\"0\" Text=\"{Binding SlidePanelTitle}\"|g' Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml)", + "Bash(sed -i '' 's|Grid\\\\.Column=\"2\" Content=\"新增\" Command=\"{Binding AddParamCommand}\"|Grid.Column=\"1\" Content=\"新增\" Command=\"{Binding AddParamCommand}\"|g' Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml)", + "Bash(sed -i '' 's|Grid\\\\.Column=\"3\" Content=\"✕\" Click=\"ClosePanel_Click\"|Grid.Column=\"2\" Content=\"✕\" Click=\"ClosePanel_Click\"|g' Sln.Wcs.UI/Views/Device/DeviceInfoListView.axaml)", + "Bash(sed -i '' 's|Grid\\\\.Column=\"1\" Text=\"{Binding SlidePanelTitle}\"|Grid.Column=\"0\" Text=\"{Binding SlidePanelTitle}\"|g' Sln.Wcs.UI/Views/Path/PathInfoListView.axaml)", + "Bash(sed -i '' 's|Grid\\\\.Column=\"2\" Content=\"新增\" Command=\"{Binding AddDetailCommand}\"|Grid.Column=\"1\" Content=\"新增\" Command=\"{Binding AddDetailCommand}\"|g' Sln.Wcs.UI/Views/Path/PathInfoListView.axaml)" ] } } diff --git a/Sln.Wcs.HikRoBotServer/Program.cs b/Sln.Wcs.HikRoBotServer/Program.cs index f3cded7..45a3b79 100644 --- a/Sln.Wcs.HikRoBotServer/Program.cs +++ b/Sln.Wcs.HikRoBotServer/Program.cs @@ -103,9 +103,23 @@ api.MapGet("/task/status", (string taskCode) => return Results.Ok(new { time = DateTime.Now, status = "ok" , taskStatus = taskStatus }); }); +//0524779AA0550094 +// AGV 等待点 +// api.MapPost("/robot/wait", (HikRoBotWaitRequest req) => +// { +// return Results.Ok(new { time = DateTime.Now, status = "ok" , agvPosition = "0524779AA0550094" }); +// }); + api.MapGet("/health", () => Results.Ok(new { time = DateTime.Now, status = "ok" })); log.Info("HikRoBotServer 就绪: http://localhost:5200/swagger"); app.Run(); record ReceiveTaskRequest(string TaskCode, string StartPoint, string EndPoint); + +/// +/// 请求参数 +/// +/// WCS 下发的任务编号:同 submit 接口 +/// 提升机编号:15 栋入库-1#Hoist ;15栋出库-2#Hoist;14 栋提升机-3#Host;13 栋提升机-4#Hoist; +record HikRoBotWaitRequest(string TaskCode, string HoistCode); diff --git a/Sln.Wcs.HoistDispatcher/HoistDispatchHub.cs b/Sln.Wcs.HoistDispatcher/HoistDispatchHub.cs index 98d3cfb..e28a526 100644 --- a/Sln.Wcs.HoistDispatcher/HoistDispatchHub.cs +++ b/Sln.Wcs.HoistDispatcher/HoistDispatchHub.cs @@ -112,8 +112,32 @@ public class HoistDispatchHub out string startLocation); TryParsePointCode(liveTaskDetail.endPoint, out string endBuilding, out string endFloor, out string endLocation); - int endPoint = Convert.ToInt32(deviceInfo.deviceSerialNo + startFloor + endFloor); + + if (startBuilding.Contains("15") && endBuilding.Contains("15")) + { + if (startFloor == "1") + { + startFloor = deviceInfo.deviceSerialNo == 1 ? "13" : "12"; + } + else + { + startFloor = $"0{startFloor}"; + } + if (endFloor == "1") + { + endFloor = deviceInfo.deviceSerialNo == 1 ? "13" : "12"; + } + else + { + endFloor = $"0{endFloor}"; + } + } + + int endPoint = Convert.ToInt32(deviceInfo.deviceSerialNo + startFloor + endFloor); + + + //调用适配层下发 提升机调度任务 SetHoistTaskResultDto res = _hoistAdapter.SetHoistTask(new SetHoistTaskDto() { @@ -164,7 +188,7 @@ public class HoistDispatchHub } } } - + /// /// 刷新设备参数:根据设备参数地址通过 PLC 获取参数值 /// hostCode为空时获取配置的所有参数值 diff --git a/Sln.Wcs.Model/Domain/BaseDeviceHost.cs b/Sln.Wcs.Model/Domain/BaseDeviceHost.cs index cf7e280..5a07098 100644 --- a/Sln.Wcs.Model/Domain/BaseDeviceHost.cs +++ b/Sln.Wcs.Model/Domain/BaseDeviceHost.cs @@ -103,4 +103,7 @@ public class BaseDeviceHost /// [SugarColumn(ColumnName = "remark")] public string remark { get; set; } + + [SugarColumn(IsIgnore = true)] + public int RowIndex { get; set; } } \ No newline at end of file diff --git a/Sln.Wcs.Model/Domain/BaseDeviceInfo.cs b/Sln.Wcs.Model/Domain/BaseDeviceInfo.cs index f6b50a2..145caa6 100644 --- a/Sln.Wcs.Model/Domain/BaseDeviceInfo.cs +++ b/Sln.Wcs.Model/Domain/BaseDeviceInfo.cs @@ -123,5 +123,16 @@ namespace Sln.Wcs.Model.Domain [SugarColumn(IsIgnore = true)] [Navigate(NavigateType.OneToMany, nameof(BaseDeviceParam.deviceCode), nameof(deviceCode))] public List< BaseDeviceParam> deviceParams { get; set; } + + [SugarColumn(IsIgnore = true)] + public string deviceTypeText + { + get { return deviceType switch { 0 => "输送线", 1 => "AGV", 2 => "提升机", _ => "--" }; } + } + [SugarColumn(IsIgnore = true)] + public string deviceStatusText + { + get { return deviceStatus switch { 0 => "正常", 1 => "在忙", 2 => "异常", _ => "--" }; } + } } } diff --git a/Sln.Wcs.Model/Domain/BaseDeviceParam.cs b/Sln.Wcs.Model/Domain/BaseDeviceParam.cs index 22037d4..ea8eed9 100644 --- a/Sln.Wcs.Model/Domain/BaseDeviceParam.cs +++ b/Sln.Wcs.Model/Domain/BaseDeviceParam.cs @@ -118,4 +118,7 @@ public class BaseDeviceParam [SugarColumn(ColumnName = "remark")] public string remark { get; set; } + + [SugarColumn(IsIgnore = true)] + public int RowIndex { get; set; } } \ No newline at end of file diff --git a/Sln.Wcs.Model/Domain/BasePathDetails.cs b/Sln.Wcs.Model/Domain/BasePathDetails.cs index 624f00b..39edaee 100644 --- a/Sln.Wcs.Model/Domain/BasePathDetails.cs +++ b/Sln.Wcs.Model/Domain/BasePathDetails.cs @@ -98,5 +98,14 @@ namespace Sln.Wcs.Model.Domain /// [SugarColumn(ColumnName = "remark")] public string remark { get; set; } + + [SugarColumn(IsIgnore = true)] + public string deviceTypeText + { + get { return deviceType switch { 0 => "输送线", 1 => "AGV", 2 => "提升机", _ => "--" }; } + } + + [SugarColumn(IsIgnore = true)] + public int RowIndex { get; set; } } } diff --git a/Sln.Wcs.Model/Domain/BasePathInfo.cs b/Sln.Wcs.Model/Domain/BasePathInfo.cs index a5eae50..0bea36e 100644 --- a/Sln.Wcs.Model/Domain/BasePathInfo.cs +++ b/Sln.Wcs.Model/Domain/BasePathInfo.cs @@ -113,5 +113,8 @@ namespace Sln.Wcs.Model.Domain [SugarColumn(IsIgnore = true)] [Navigate(NavigateType.OneToMany, nameof(BasePathDetails.pathCode), nameof(pathCode))] public List pathDetails { get; set; } + + [SugarColumn(IsIgnore = true)] + public int RowIndex { get; set; } } } diff --git a/Sln.Wcs.Model/Domain/LiveTaskDetail.cs b/Sln.Wcs.Model/Domain/LiveTaskDetail.cs index 1570882..4d9891e 100644 --- a/Sln.Wcs.Model/Domain/LiveTaskDetail.cs +++ b/Sln.Wcs.Model/Domain/LiveTaskDetail.cs @@ -161,6 +161,14 @@ public class LiveTaskDetail [SugarColumn(ColumnName = "execution_mode")] public int? executionMode { get; set; } + /// + /// Desc:执行设备编号 + /// Default: + /// Nullable:True + /// + [SugarColumn(ColumnName = "exec_device")] + public string execDevice { get; set; } + /// /// Desc:备注 /// Default: @@ -168,4 +176,33 @@ public class LiveTaskDetail /// [SugarColumn(ColumnName = "remark")] public string remark { get; set; } + + [SugarColumn(IsIgnore = true)] + public string taskTypeText + { + get { return taskType switch { 1 => "入库", 2 => "出库", _ => "--" }; } + } + [SugarColumn(IsIgnore = true)] + public string taskCategoryText + { + get { return taskCategory switch { 1 => "包材", 2 => "成品", 3 => "托盘", _ => "--" }; } + } + [SugarColumn(IsIgnore = true)] + public string taskStatusText + { + get { return taskStatus switch { 1 => "待执行", 2 => "执行中", 3 => "已完成", _ => "--" }; } + } + [SugarColumn(IsIgnore = true)] + public string executionModeText + { + get { return executionMode switch { 0 => "自动", 1 => "手动", _ => "--" }; } + } + [SugarColumn(IsIgnore = true)] + public string deviceTypeText + { + get { return deviceType switch { 0 => "输送线", 1 => "AGV", 2 => "提升机", _ => "--" }; } + } + + [SugarColumn(IsIgnore = true)] + public int RowIndex { get; set; } } \ No newline at end of file diff --git a/Sln.Wcs.Model/Domain/LiveTaskQueue.cs b/Sln.Wcs.Model/Domain/LiveTaskQueue.cs index 20d3f87..d7138bf 100644 --- a/Sln.Wcs.Model/Domain/LiveTaskQueue.cs +++ b/Sln.Wcs.Model/Domain/LiveTaskQueue.cs @@ -167,4 +167,40 @@ public class LiveTaskQueue [SugarColumn(IsIgnore = true)] [Navigate(NavigateType.OneToMany, nameof(LiveTaskDetail.taskCode), nameof(taskCode))] public List taskDetails { get; set; } + + [SugarColumn(IsIgnore = true)] + public string taskTypeText + { + get + { + return taskType switch { 1 => "入库", 2 => "出库", _ => "--" }; + } + } + [SugarColumn(IsIgnore = true)] + public string taskCategoryText + { + get + { + return taskCategory switch { 1 => "包材", 2 => "成品", 3 => "托盘", _ => "--" }; + } + } + [SugarColumn(IsIgnore = true)] + public string taskStatusText + { + get + { + return taskStatus switch { 1 => "待执行", 2 => "执行中", 3 => "已完成", _ => "--" }; + } + } + [SugarColumn(IsIgnore = true)] + public string executionModeText + { + get + { + return executionMode switch { 0 => "自动", 1 => "手动", _ => "--" }; + } + } + + [SugarColumn(IsIgnore = true)] + public int RowIndex { get; set; } } \ No newline at end of file diff --git a/Sln.Wcs.UI/App.axaml b/Sln.Wcs.UI/App.axaml index ce86296..31b7d96 100644 --- a/Sln.Wcs.UI/App.axaml +++ b/Sln.Wcs.UI/App.axaml @@ -1,6 +1,7 @@ @@ -9,6 +10,7 @@ + diff --git a/Sln.Wcs.UI/ViewModels/Base/CrudPageViewModel.cs b/Sln.Wcs.UI/ViewModels/Base/CrudPageViewModel.cs index f35f9c0..2a7d6d5 100644 --- a/Sln.Wcs.UI/ViewModels/Base/CrudPageViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Base/CrudPageViewModel.cs @@ -69,6 +69,11 @@ public abstract partial class CrudPageViewModel : ObservableObject, ICrudPage var exp = BuildSearchExpression(SearchText); list = exp != null ? _service.Query(exp) : _service.Query(); } + for (int i = 0; i < list.Count; i++) + { + var rowProp = typeof(T).GetProperty("RowIndex"); + rowProp?.SetValue(list[i], i + 1); + } Items = new ObservableCollection(list); StatusText = $"共 {list.Count} 条记录"; } diff --git a/Sln.Wcs.UI/ViewModels/Base/FieldConfig.cs b/Sln.Wcs.UI/ViewModels/Base/FieldConfig.cs index 226c7dc..6277aa1 100644 --- a/Sln.Wcs.UI/ViewModels/Base/FieldConfig.cs +++ b/Sln.Wcs.UI/ViewModels/Base/FieldConfig.cs @@ -7,6 +7,13 @@ public class FieldConfig public FieldType FieldType { get; set; } = FieldType.Text; public bool IsReadOnly { get; set; } public bool IsRequired { get; set; } + public List Options { get; set; } = new(); +} + +public class ComboOption +{ + public object? Value { get; set; } + public string Display { get; set; } = string.Empty; } public enum FieldType @@ -16,3 +23,61 @@ public enum FieldType Combo, CheckBox } + +public static class StandardOptions +{ + public static readonly List TaskType = new() + { + new() { Value = 1, Display = "1 - 入库" }, + new() { Value = 2, Display = "2 - 出库" }, + }; + + public static readonly List TaskCategory = new() + { + new() { Value = 1, Display = "1 - 包材" }, + new() { Value = 2, Display = "2 - 成品" }, + new() { Value = 3, Display = "3 - 托盘" }, + }; + + public static readonly List TaskStatus = new() + { + new() { Value = 1, Display = "1 - 待执行" }, + new() { Value = 2, Display = "2 - 执行中" }, + new() { Value = 3, Display = "3 - 已完成" }, + }; + + public static readonly List ExecutionMode = new() + { + new() { Value = 0, Display = "0 - 自动" }, + new() { Value = 1, Display = "1 - 手动" }, + }; + + public static readonly List DeviceType = new() + { + new() { Value = 0, Display = "0 - 输送线" }, + new() { Value = 1, Display = "1 - AGV" }, + new() { Value = 2, Display = "2 - 提升机" }, + }; + + public static readonly List DeviceStatus = new() + { + new() { Value = 0, Display = "0 - 正常" }, + new() { Value = 1, Display = "1 - 在忙" }, + new() { Value = 2, Display = "2 - 异常" }, + }; + + public static readonly List LocationStatus = new() + { + new() { Value = 0, Display = "0 - 未使用" }, + new() { Value = 1, Display = "1 - 已使用" }, + new() { Value = 2, Display = "2 - 锁库" }, + new() { Value = 3, Display = "3 - 异常" }, + }; + + public static readonly List OperationType = new() + { + new() { Value = "0", Display = "0 - 默认读写" }, + new() { Value = "1", Display = "1 - 只读" }, + new() { Value = "2", Display = "2 - 只写" }, + }; +} diff --git a/Sln.Wcs.UI/ViewModels/Base/LocationInfoViewModel.cs b/Sln.Wcs.UI/ViewModels/Base/LocationInfoViewModel.cs index 85f2ccb..50abd81 100644 --- a/Sln.Wcs.UI/ViewModels/Base/LocationInfoViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Base/LocationInfoViewModel.cs @@ -26,7 +26,7 @@ public class LocationInfoViewModel : CrudPageViewModel new() { PropertyName = "materialCode", DisplayName = "物料编号" }, new() { PropertyName = "palletBarcode", DisplayName = "托盘条码" }, new() { PropertyName = "stackCount", DisplayName = "库存数量" }, - new() { PropertyName = "locationStatus", DisplayName = "库位状态", FieldType = FieldType.Number }, + new() { PropertyName = "locationStatus", DisplayName = "库位状态", FieldType = FieldType.Combo, Options = StandardOptions.LocationStatus }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, new() { PropertyName = "remark", DisplayName = "备注" }, }; diff --git a/Sln.Wcs.UI/ViewModels/Base/StoreInfoViewModel.cs b/Sln.Wcs.UI/ViewModels/Base/StoreInfoViewModel.cs index 7b8f37a..f99e1ca 100644 --- a/Sln.Wcs.UI/ViewModels/Base/StoreInfoViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Base/StoreInfoViewModel.cs @@ -1,15 +1,32 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq.Expressions; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using Sln.Wcs.Model.Domain; using Sln.Wcs.Repository.service; using Sln.Wcs.UI.ViewModels.Base; +using Sln.Wcs.UI.Views.Base; namespace Sln.Wcs.UI.ViewModels.Base; -public class StoreInfoViewModel : CrudPageViewModel +public partial class StoreInfoViewModel : CrudPageViewModel { - public StoreInfoViewModel(IBaseStoreInfoService service) : base(service) + private readonly IBaseLocationInfoService _locationService; + private BaseStoreInfo? _currentStore; + + [ObservableProperty] + private bool _isPanelOpen; + + [ObservableProperty] + private ObservableCollection _storeLocations = new(); + + [ObservableProperty] + private string _slidePanelTitle = string.Empty; + + public StoreInfoViewModel(IBaseStoreInfoService service, IBaseLocationInfoService locationService) : base(service) { + _locationService = locationService; PageTitle = "仓库信息管理"; } @@ -26,4 +43,84 @@ public class StoreInfoViewModel : CrudPageViewModel => 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(); + + public void LoadLocationsData(BaseStoreInfo store) + { + _currentStore = store; + LoadLocations(); + SlidePanelTitle = $"库位列表 - {store.storeCode}"; + } + + [RelayCommand] + public void ClosePanel() + { + IsPanelOpen = false; + } + + [RelayCommand] + private async System.Threading.Tasks.Task AddLocation() + { + if (_currentStore is null) return; + var entity = new BaseLocationInfo { storeCode = _currentStore.storeCode }; + var editor = new EntityEditWindow(); + var result = await editor.ShowDialog(entity, LocationFieldConfigs, false, GetMainWindow()); + if (result) + { + _locationService.Insert(entity); + LoadLocations(); + } + } + + public async System.Threading.Tasks.Task EditLocationAsync(BaseLocationInfo location) + { + var editor = new EntityEditWindow(); + var result = await editor.ShowDialog(location, LocationFieldConfigs, true, GetMainWindow()); + if (result) + { + _locationService.Update(location); + LoadLocations(); + } + } + + public async System.Threading.Tasks.Task DeleteLocationAsync(BaseLocationInfo location) + { + var dlg = new Sln.Wcs.UI.Views.Base.ConfirmDialog(); + if (!await dlg.ShowDialog("确定要删除该库位吗?", GetMainWindow())) return; + _locationService.DeleteById(location.objId); + LoadLocations(); + } + + private void LoadLocations() + { + if (_currentStore is null) return; + var list = _locationService.Query(x => x.storeCode == _currentStore.storeCode); + for (int i = 0; i < list.Count; i++) list[i].RowIndex = i + 1; + StoreLocations = new ObservableCollection(list); + } + + public List LocationFieldConfigs => new() + { + new() { PropertyName = "locationCode", DisplayName = "库位编号", IsRequired = true }, + new() { PropertyName = "storeCode", DisplayName = "仓库编号", IsRequired = true, IsReadOnly = true }, + new() { PropertyName = "locationName", DisplayName = "库位名称" }, + new() { PropertyName = "locationArea", DisplayName = "库位区域" }, + new() { PropertyName = "locationRows", DisplayName = "排", FieldType = FieldType.Number }, + new() { PropertyName = "locationColumns", DisplayName = "列", FieldType = FieldType.Number }, + new() { PropertyName = "locationLayers", DisplayName = "层", FieldType = FieldType.Number }, + new() { PropertyName = "locationStatus", DisplayName = "库位状态", FieldType = FieldType.Combo, Options = StandardOptions.LocationStatus }, + new() { PropertyName = "agvPosition", DisplayName = "AGV定位" }, + new() { PropertyName = "materialCode", DisplayName = "物料编号" }, + new() { PropertyName = "palletBarcode", DisplayName = "托盘条码" }, + new() { PropertyName = "stackCount", DisplayName = "库存数量" }, + new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, + new() { PropertyName = "remark", DisplayName = "备注" }, + }; + + 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/Device/DeviceInfoViewModel.cs b/Sln.Wcs.UI/ViewModels/Device/DeviceInfoViewModel.cs index 962cee9..12f8ca5 100644 --- a/Sln.Wcs.UI/ViewModels/Device/DeviceInfoViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Device/DeviceInfoViewModel.cs @@ -36,8 +36,8 @@ public partial class DeviceInfoViewModel : CrudPageViewModel 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 = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Combo, Options = StandardOptions.DeviceType }, + new() { PropertyName = "deviceStatus", DisplayName = "设备状态", FieldType = FieldType.Combo, Options = StandardOptions.DeviceStatus }, new() { PropertyName = "hostCode", DisplayName = "主机编号", IsRequired = true }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, new() { PropertyName = "remark", DisplayName = "备注" }, @@ -98,6 +98,7 @@ public partial class DeviceInfoViewModel : CrudPageViewModel { if (_currentDevice is null) return; var list = _paramService.Query(x => x.deviceCode == _currentDevice.deviceCode); + for (int i = 0; i < list.Count; i++) list[i].RowIndex = i + 1; DeviceParams = new ObservableCollection(list); } @@ -109,7 +110,7 @@ public partial class DeviceInfoViewModel : CrudPageViewModel new() { PropertyName = "paramAddress", DisplayName = "参数地址", IsRequired = true }, new() { PropertyName = "paramType", DisplayName = "参数类型" }, new() { PropertyName = "paramValue", DisplayName = "参数值", FieldType = FieldType.Number }, - new() { PropertyName = "operationType", DisplayName = "操作类型" }, + new() { PropertyName = "operationType", DisplayName = "操作类型", FieldType = FieldType.Combo, Options = StandardOptions.OperationType }, new() { PropertyName = "operationFrequency", DisplayName = "操作频率(ms)" }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, new() { PropertyName = "remark", DisplayName = "备注" }, diff --git a/Sln.Wcs.UI/ViewModels/Device/DeviceParamViewModel.cs b/Sln.Wcs.UI/ViewModels/Device/DeviceParamViewModel.cs index 95be9d2..d5e656b 100644 --- a/Sln.Wcs.UI/ViewModels/Device/DeviceParamViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Device/DeviceParamViewModel.cs @@ -21,7 +21,7 @@ public class DeviceParamViewModel : CrudPageViewModel new() { PropertyName = "paramAddress", DisplayName = "参数地址" }, new() { PropertyName = "paramType", DisplayName = "参数类型" }, new() { PropertyName = "paramValue", DisplayName = "参数值", FieldType = FieldType.Number }, - new() { PropertyName = "operationType", DisplayName = "操作类型" }, + new() { PropertyName = "operationType", DisplayName = "操作类型", FieldType = FieldType.Combo, Options = StandardOptions.OperationType }, new() { PropertyName = "operationFrequency", DisplayName = "操作频率(ms)" }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, new() { PropertyName = "remark", DisplayName = "备注" }, diff --git a/Sln.Wcs.UI/ViewModels/Path/PathDetailsViewModel.cs b/Sln.Wcs.UI/ViewModels/Path/PathDetailsViewModel.cs index 194fc6a..49bd9e1 100644 --- a/Sln.Wcs.UI/ViewModels/Path/PathDetailsViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Path/PathDetailsViewModel.cs @@ -19,7 +19,7 @@ public class PathDetailsViewModel : CrudPageViewModel new() { PropertyName = "pathName", DisplayName = "路径名称" }, new() { PropertyName = "startPoint", DisplayName = "起点" }, new() { PropertyName = "endPoint", DisplayName = "终点" }, - new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Number }, + new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Combo, Options = StandardOptions.DeviceType }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, new() { PropertyName = "remark", DisplayName = "备注" }, }; diff --git a/Sln.Wcs.UI/ViewModels/Path/PathInfoViewModel.cs b/Sln.Wcs.UI/ViewModels/Path/PathInfoViewModel.cs index 386297c..ed1cfcd 100644 --- a/Sln.Wcs.UI/ViewModels/Path/PathInfoViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Path/PathInfoViewModel.cs @@ -34,8 +34,8 @@ public partial class PathInfoViewModel : CrudPageViewModel { new() { PropertyName = "pathCode", DisplayName = "路径编号" }, new() { PropertyName = "pathName", DisplayName = "路径名称" }, - new() { PropertyName = "pathType", DisplayName = "路径类型", FieldType = FieldType.Number }, - new() { PropertyName = "pathCategory", DisplayName = "路径类别", FieldType = FieldType.Number }, + new() { PropertyName = "pathType", DisplayName = "路径类型", FieldType = FieldType.Combo, Options = StandardOptions.TaskType }, + new() { PropertyName = "pathCategory", DisplayName = "路径类别", FieldType = FieldType.Combo, Options = StandardOptions.TaskCategory }, new() { PropertyName = "startPoint", DisplayName = "起点" }, new() { PropertyName = "endPoint", DisplayName = "终点" }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, @@ -98,6 +98,7 @@ public partial class PathInfoViewModel : CrudPageViewModel { if (_currentPath is null) return; var list = _detailService.Query(x => x.pathCode == _currentPath.pathCode); + for (int i = 0; i < list.Count; i++) list[i].RowIndex = i + 1; DetailItems = new ObservableCollection(list); } @@ -107,7 +108,7 @@ public partial class PathInfoViewModel : CrudPageViewModel new() { PropertyName = "pathName", DisplayName = "路径名称" }, new() { PropertyName = "startPoint", DisplayName = "起点", IsRequired = true }, new() { PropertyName = "endPoint", DisplayName = "终点", IsRequired = true }, - new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Number }, + new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Combo, Options = StandardOptions.DeviceType }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, new() { PropertyName = "remark", DisplayName = "备注" }, }; diff --git a/Sln.Wcs.UI/ViewModels/Task/CreateTaskViewModel.cs b/Sln.Wcs.UI/ViewModels/Task/CreateTaskViewModel.cs index 940e91f..691207e 100644 --- a/Sln.Wcs.UI/ViewModels/Task/CreateTaskViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Task/CreateTaskViewModel.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -24,11 +25,41 @@ public partial class CreateTaskViewModel : ObservableObject [ObservableProperty] private string _endLocation = "01"; // ---- 任务属性 ---- - [ObservableProperty] private int _taskType = 1; // 1-入库 2-出库 - [ObservableProperty] private int _taskCategory = 1; // 1-包材 2-成品 3-托盘 + [ObservableProperty] private int _taskType = 1; + [ObservableProperty] private int _taskCategory = 1; [ObservableProperty] private string _palletBarcode = string.Empty; [ObservableProperty] private string _materialCode = string.Empty; + // ---- 下拉选项 ---- + public ObservableCollection TaskTypeOptions { get; } = new() + { + new() { Value = 1, Display = "1 - 入库" }, + new() { Value = 2, Display = "2 - 出库" }, + }; + + public ObservableCollection TaskCategoryOptions { get; } = new() + { + new() { Value = 1, Display = "1 - 包材" }, + new() { Value = 2, Display = "2 - 成品" }, + new() { Value = 3, Display = "3 - 托盘" }, + }; + + [ObservableProperty] + private TaskAttributeOption _selectedTaskType; + + [ObservableProperty] + private TaskAttributeOption _selectedTaskCategory; + + partial void OnSelectedTaskTypeChanged(TaskAttributeOption? value) + { + if (value is not null) TaskType = value.Value; + } + + partial void OnSelectedTaskCategoryChanged(TaskAttributeOption? value) + { + if (value is not null) TaskCategory = value.Value; + } + // ---- 手动操作选项 ---- [ObservableProperty] private bool _manualPutIn; [ObservableProperty] private bool _manualTakeOut; @@ -41,6 +72,8 @@ public partial class CreateTaskViewModel : ObservableObject { _taskCreateService = taskCreateService; _taskQueueService = taskQueueService; + _selectedTaskType = TaskTypeOptions[0]; + _selectedTaskCategory = TaskCategoryOptions[0]; } public Avalonia.Controls.Control CreateView() => new Views.Task.CreateTaskView(); @@ -85,3 +118,9 @@ public partial class CreateTaskViewModel : ObservableObject } } } + +public class TaskAttributeOption +{ + public int Value { get; set; } + public string Display { get; set; } = string.Empty; +} diff --git a/Sln.Wcs.UI/ViewModels/Task/ManualTaskViewModel.cs b/Sln.Wcs.UI/ViewModels/Task/ManualTaskViewModel.cs index d918995..b2836b7 100644 --- a/Sln.Wcs.UI/ViewModels/Task/ManualTaskViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Task/ManualTaskViewModel.cs @@ -19,11 +19,6 @@ public partial class ManualTaskViewModel : ObservableObject private readonly ILiveTaskDetailService _taskDetailService; private readonly HttpClient _http; private readonly SerilogHelper _logger; - - private FreeHoistResponse? _allocatedHoist; - - private Dictionary hoistDic = new Dictionary(); - public string PageTitle => "手动执行任务"; [ObservableProperty] @@ -35,6 +30,9 @@ public partial class ManualTaskViewModel : ObservableObject [ObservableProperty] private ObservableCollection _details = new(); + [ObservableProperty] + private LiveTaskDetail? _selectedDetail; + [ObservableProperty] private string _statusText = string.Empty; @@ -53,6 +51,17 @@ public partial class ManualTaskViewModel : ObservableObject [ObservableProperty] private string _dockingPoint = "--"; + public ObservableCollection HostOptions { get; } = new() + { + new() { Name = "15栋入库提升机", Code = "1#Host" }, + new() { Name = "15栋出库提升机", Code = "2#Host" }, + new() { Name = "14栋提升机", Code = "3#Host" }, + new() { Name = "13栋提升机", Code = "4#Host" } + }; + + [ObservableProperty] + private HostOption _selectedHostOption; + public ManualTaskViewModel( ILiveTaskQueueService taskQueueService, ILiveTaskDetailService taskDetailService, @@ -62,6 +71,7 @@ public partial class ManualTaskViewModel : ObservableObject _taskDetailService = taskDetailService; _http = new HttpClient { Timeout = System.TimeSpan.FromSeconds(5) }; _logger = logger; + _selectedHostOption = HostOptions[0]; } public Avalonia.Controls.Control CreateView() => new Views.Task.ManualTaskView(); @@ -75,6 +85,7 @@ public partial class ManualTaskViewModel : ObservableObject private void RefreshTaskList() { List list = _taskQueueService.getLiveTaskQueues(x=>x.executionMode == 1 && x.isFlag == 1); + for (int i = 0; i < list.Count; i++) list[i].RowIndex = i + 1; Tasks = new ObservableCollection(list); StatusText = list.Count > 0 ? $"查询到 {list.Count} 条待执行手动任务" @@ -84,18 +95,35 @@ public partial class ManualTaskViewModel : ObservableObject partial void OnSelectedTaskChanged(LiveTaskQueue? value) { TaskExecuted = false; - _allocatedHoist = null; + SelectedDetail = null; HoistInfo = "--"; DockingPoint = "--"; if (value is not null) LoadDetails(value.taskCode); } + partial void OnSelectedDetailChanged(LiveTaskDetail? value) + { + if (value is not null && !string.IsNullOrWhiteSpace(value.execDevice)) + { + var parts = value.execDevice.Split('_'); + if (parts.Length == 2) + { + HoistInfo = parts[0]; + DockingPoint = parts[1]; + return; + } + } + HoistInfo = "--"; + DockingPoint = "--"; + } + private void LoadDetails(string taskCode) { var detailList = _taskDetailService.Query(x => x.taskCode == taskCode) .OrderBy(d => d.objId) .ToList(); + for (int i = 0; i < detailList.Count; i++) detailList[i].RowIndex = i + 1; Details = new ObservableCollection(detailList); //StatusText = $"任务 {taskCode},共 {detailList.Count} 条明细"; } @@ -118,10 +146,16 @@ public partial class ManualTaskViewModel : ObservableObject return; } - var hoistDetail = Details.FirstOrDefault(d => d.deviceType == 2); - if (hoistDetail is null) + if (SelectedDetail is null) { - StatusText = "当前任务未包含提升机步骤,无需手动执行"; + StatusText = "请先在明细列表中选择一条提升机明细"; + return; + } + + var hoistDetail = SelectedDetail; + if (hoistDetail.deviceType != 2) + { + StatusText = "所选明细不是提升机步骤,请选择设备类型为提升机的明细"; return; } @@ -130,8 +164,26 @@ public partial class ManualTaskViewModel : ObservableObject try { + // 1. 获取空闲提升机 - string hostCode = "1#Host"; + string hostCode = SelectedHostOption?.Code ?? "1#Host"; + if (hoistDetail.taskType == 2 && hoistDetail.taskCategory == 2 && hoistDetail.startPoint.Contains("15#")) + { + hostCode = "2#Host"; + } + else + { + if (hoistDetail.startPoint.Contains("13#")) + { + hostCode = "4#Host"; + }else if (hoistDetail.startPoint.Contains("14#")) + { + hostCode = "3#Host"; + }else if (hoistDetail.startPoint.Contains("15#")) + { + hostCode = "1#Host"; + } + } var freeUrl = $"{HoistBaseUrl}/api/hoist/free?hostCode={Uri.EscapeDataString(hostCode)}"; var freeRes = await _http.GetFromJsonAsync(freeUrl); @@ -143,12 +195,8 @@ public partial class ManualTaskViewModel : ObservableObject return; } - _allocatedHoist = freeRes; - HoistInfo = $"{freeRes.deviceName ?? freeRes.deviceCode} (空闲)"; - DockingPoint = $"{freeRes.deviceSerialNo}" ?? "--"; - StatusText = $"已获取空闲提升机 {freeRes.deviceName ?? freeRes.deviceCode},正在下发任务..."; - hoistDic.Add(SelectedTask.taskCode,freeRes); + // 2. 下发提升机调度任务 var dispatchBody = new { @@ -176,7 +224,11 @@ public partial class ManualTaskViewModel : ObservableObject _taskQueueService.Update(SelectedTask); hoistDetail.taskStatus = 2; + hoistDetail.execDevice = $"{freeRes.hostCode}_{freeRes.deviceSerialNo}"; _taskDetailService.Update(hoistDetail); + + HoistInfo = freeRes.hostCode.ToString(); // HoistInfo = freeRes.hostCode; + DockingPoint = freeRes.deviceSerialNo.ToString(); }); TaskExecuted = true; @@ -188,7 +240,6 @@ public partial class ManualTaskViewModel : ObservableObject catch (HttpRequestException ex) { TaskExecuted = false; - _allocatedHoist = null; HoistInfo = "--"; StatusText = $"与提升机调度中心通信失败: {ex.Message}"; _logger.Error($"HoistServer 通信失败: {ex.Message}"); @@ -196,7 +247,6 @@ public partial class ManualTaskViewModel : ObservableObject catch (Exception ex) { TaskExecuted = false; - _allocatedHoist = null; HoistInfo = "--"; StatusText = $"任务执行失败: {ex.Message}"; _logger.Error($"手动任务执行失败: {ex.Message}"); @@ -219,18 +269,29 @@ public partial class ManualTaskViewModel : ObservableObject return; } - hoistDic.TryGetValue(SelectedTask.taskCode, out _allocatedHoist); - - if (_allocatedHoist is null) + if (SelectedDetail is null) { - StatusText = "请先执行任务获取空闲提升机,再确认物料到位"; + StatusText = "请先在明细列表中选择一条明细"; return; } - var hoistDetail = Details.FirstOrDefault(d => d.deviceType == 2); - if (hoistDetail is null) + var hoistDetail = SelectedDetail; + + if (hoistDetail.deviceType != 2) { - StatusText = "未找到提升机任务明细"; + StatusText = "所选明细不是提升机步骤,请选择设备类型为提升机的明细"; + return; + } + + if (SelectedTask.taskStatus != 2) + { + StatusText = "当前任务未在执行中,请先执行任务"; + return; + } + + if (string.IsNullOrWhiteSpace(hoistDetail.execDevice)) + { + StatusText = "当前任务未分配执行设备,请先执行任务"; return; } @@ -239,10 +300,14 @@ public partial class ManualTaskViewModel : ObservableObject try { + var execParts = hoistDetail.execDevice!.Split('_'); + var execHostCode = execParts[0]; + var execSerialNo = int.Parse(execParts[1]); + var body = new { - hostCode = _allocatedHoist.hostCode, - serialNo = _allocatedHoist.deviceSerialNo, + hostCode = execHostCode, + serialNo = execSerialNo, taskCode = hoistDetail.taskCode, palletBarcode = hoistDetail.palletBarcode ?? "", startPoint = hoistDetail.startPoint, @@ -302,4 +367,10 @@ public partial class ManualTaskViewModel : ObservableObject public bool success { get; set; } public string? msg { get; set; } } + + public class HostOption + { + public string Name { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + } } diff --git a/Sln.Wcs.UI/ViewModels/Task/TaskDetailViewModel.cs b/Sln.Wcs.UI/ViewModels/Task/TaskDetailViewModel.cs index 9e018d0..75cfe0f 100644 --- a/Sln.Wcs.UI/ViewModels/Task/TaskDetailViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Task/TaskDetailViewModel.cs @@ -21,13 +21,14 @@ public class TaskDetailViewModel : CrudPageViewModel 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 = "taskType", DisplayName = "任务类型", FieldType = FieldType.Combo, Options = StandardOptions.TaskType }, + new() { PropertyName = "taskCategory", DisplayName = "任务类别", FieldType = FieldType.Combo, Options = StandardOptions.TaskCategory }, new() { PropertyName = "startPoint", DisplayName = "起始位置" }, new() { PropertyName = "endPoint", DisplayName = "结束位置" }, - new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Number }, + new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Combo, Options = StandardOptions.DeviceType }, + new() { PropertyName = "execDevice", DisplayName = "执行设备" }, new() { PropertyName = "isValidate", DisplayName = "校验物料", FieldType = FieldType.CheckBox }, - new() { PropertyName = "taskStatus", DisplayName = "任务状态", FieldType = FieldType.Number }, + new() { PropertyName = "taskStatus", DisplayName = "任务状态", FieldType = FieldType.Combo, Options = StandardOptions.TaskStatus }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, new() { PropertyName = "remark", DisplayName = "备注" }, }; diff --git a/Sln.Wcs.UI/ViewModels/Task/TaskQueueViewModel.cs b/Sln.Wcs.UI/ViewModels/Task/TaskQueueViewModel.cs index 496c0a0..d25bb36 100644 --- a/Sln.Wcs.UI/ViewModels/Task/TaskQueueViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Task/TaskQueueViewModel.cs @@ -37,12 +37,12 @@ public partial class TaskQueueViewModel : CrudPageViewModel 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 = "taskType", DisplayName = "任务类型", FieldType = FieldType.Combo, Options = StandardOptions.TaskType }, + new() { PropertyName = "taskCategory", DisplayName = "任务类别", FieldType = FieldType.Combo, Options = StandardOptions.TaskCategory }, new() { PropertyName = "startPoint", DisplayName = "起始位置" }, new() { PropertyName = "endPoint", DisplayName = "结束位置" }, new() { PropertyName = "pathCode", DisplayName = "路径编号" }, - new() { PropertyName = "taskStatus", DisplayName = "任务状态", FieldType = FieldType.Number }, + new() { PropertyName = "taskStatus", DisplayName = "任务状态", FieldType = FieldType.Combo, Options = StandardOptions.TaskStatus }, new() { PropertyName = "taskSteps", DisplayName = "任务步骤", FieldType = FieldType.Number }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, new() { PropertyName = "remark", DisplayName = "备注" }, @@ -104,6 +104,7 @@ public partial class TaskQueueViewModel : CrudPageViewModel { if (_currentTask is null) return; var list = _detailService.Query(x => x.taskCode == _currentTask.taskCode); + for (int i = 0; i < list.Count; i++) list[i].RowIndex = i + 1; DetailItems = new ObservableCollection(list); } @@ -114,9 +115,10 @@ public partial class TaskQueueViewModel : CrudPageViewModel new() { PropertyName = "materialCode", DisplayName = "物料编号" }, new() { PropertyName = "startPoint", DisplayName = "起始位置" }, new() { PropertyName = "endPoint", DisplayName = "结束位置" }, - new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Number }, + new() { PropertyName = "deviceType", DisplayName = "设备类型", FieldType = FieldType.Combo, Options = StandardOptions.DeviceType }, + new() { PropertyName = "execDevice", DisplayName = "执行设备" }, new() { PropertyName = "isValidate", DisplayName = "校验物料", FieldType = FieldType.CheckBox }, - new() { PropertyName = "taskStatus", DisplayName = "任务状态", FieldType = FieldType.Number }, + new() { PropertyName = "taskStatus", DisplayName = "任务状态", FieldType = FieldType.Combo, Options = StandardOptions.TaskStatus }, new() { PropertyName = "isFlag", DisplayName = "启用", FieldType = FieldType.CheckBox }, new() { PropertyName = "remark", DisplayName = "备注" }, }; diff --git a/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml.cs b/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml.cs index 4495595..35d1fb0 100644 --- a/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml.cs +++ b/Sln.Wcs.UI/Views/Base/EntityEditWindow.axaml.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Styling; @@ -88,6 +89,42 @@ public partial class EntityEditWindow : Window prop.SetValue(_entity, cb.IsChecked == true ? 1 : 0); input = cb; } + else if (field.FieldType == FieldType.Combo) + { + var combo = new ComboBox + { + ItemsSource = field.Options, + DisplayMemberBinding = new Binding("Display"), + IsEnabled = !field.IsReadOnly, + Background = tbBg, + Foreground = tbFg, + BorderBrush = tbBorder, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(3), + Padding = new Thickness(8, 5), + FontSize = 12, + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + + // 选中当前值对应的选项 + ComboOption? selected = null; + foreach (ComboOption opt in field.Options) + { + if (Equals(opt.Value, value)) + { + selected = opt; + break; + } + } + combo.SelectedItem = selected; + + combo.SelectionChanged += (_, _) => + { + if (combo.SelectedItem is ComboOption opt) + prop.SetValue(_entity, opt.Value); + }; + input = combo; + } else { var tb = new TextBox diff --git a/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml b/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml index 91fa5ec..1246081 100644 --- a/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml +++ b/Sln.Wcs.UI/Views/Base/LocationInfoListView.axaml @@ -8,38 +8,40 @@