增强原材料卡片管理功能,新增免密接口和数据处理逻辑,支持原材料卡片的增删改查操作。更新前端视图以支持多行拆码明细拼接,优化用户体验和系统实时数据同步能力。

This commit is contained in:
geht
2026-05-11 14:32:44 +08:00
parent 936375bb2c
commit cffe32d896
49 changed files with 4594 additions and 390 deletions

View File

@@ -130,7 +130,12 @@ namespace YY.Admin.ViewModels.Control
// 已实现页面:新增原料入场记录
["RawMaterialEntryOperationView"] = "RawMaterialEntryOperationView",
["/xslmes/rawMaterialEntryOperation"] = "RawMaterialEntryOperationView",
["rawMaterialEntryOperation"] = "RawMaterialEntryOperationView"
["rawMaterialEntryOperation"] = "RawMaterialEntryOperationView",
// 已实现页面:原材料卡片
["RawMaterialCardListView"] = "RawMaterialCardListView",
["/xslmes/mesXslRawMaterialCard"] = "RawMaterialCardListView",
["mesXslRawMaterialCard"] = "RawMaterialCardListView"
};
private MenuItem? _selectedMenuItem;

View File

@@ -0,0 +1,160 @@
using HandyControl.Controls;
using HandyControl.Tools.Extension;
using System.Collections.ObjectModel;
using YY.Admin.Core;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service;
namespace YY.Admin.ViewModels.RawMaterialCard;
public class RawMaterialCardEditDialogViewModel : BaseViewModel, IDialogResultable<bool>
{
private readonly IRawMaterialCardService _cardService;
private readonly IJeecgDictSyncService _dictSyncService;
private MesXslRawMaterialCard? _card;
public MesXslRawMaterialCard? Card
{
get => _card;
set => SetProperty(ref _card, value);
}
public bool IsAddMode => string.IsNullOrWhiteSpace(Card?.Id);
public string DialogTitle => IsAddMode ? "新增原材料卡片" : "编辑原材料卡片";
public ObservableCollection<KeyValuePair<string, string>> StatusOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> TestResultOptions { get; } = new();
private bool _result;
public bool Result { get => _result; set => SetProperty(ref _result, value); }
public Action? CloseAction { get; set; }
public DelegateCommand SaveCommand { get; }
public DelegateCommand CancelCommand { get; }
public RawMaterialCardEditDialogViewModel(
IRawMaterialCardService cardService,
IJeecgDictSyncService dictSyncService,
IContainerExtension container,
IRegionManager regionManager) : base(container, regionManager)
{
_cardService = cardService;
_dictSyncService = dictSyncService;
SaveCommand = new DelegateCommand(async () => await SaveAsync());
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
_ = LoadDictOptionsAsync();
}
private async Task LoadDictOptionsAsync()
{
try
{
var statusOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_card_status");
var testResultOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_test_result");
StatusOptions.Clear();
foreach (var item in statusOpts) StatusOptions.Add(item);
if (StatusOptions.Count == 0)
{
StatusOptions.Add(new KeyValuePair<string, string>("正常", "1"));
StatusOptions.Add(new KeyValuePair<string, string>("异常", "0"));
}
TestResultOptions.Clear();
foreach (var item in testResultOpts) TestResultOptions.Add(item);
if (TestResultOptions.Count == 0)
{
TestResultOptions.Add(new KeyValuePair<string, string>("未检", "0"));
TestResultOptions.Add(new KeyValuePair<string, string>("合格", "1"));
TestResultOptions.Add(new KeyValuePair<string, string>("不合格", "2"));
}
}
catch
{
StatusOptions.Clear();
StatusOptions.Add(new KeyValuePair<string, string>("正常", "1"));
StatusOptions.Add(new KeyValuePair<string, string>("异常", "0"));
TestResultOptions.Clear();
TestResultOptions.Add(new KeyValuePair<string, string>("未检", "0"));
TestResultOptions.Add(new KeyValuePair<string, string>("合格", "1"));
TestResultOptions.Add(new KeyValuePair<string, string>("不合格", "2"));
}
}
public void InitializeForAdd()
{
Card = new MesXslRawMaterialCard
{
Status = "1",
TestResult = "0",
PriorityPickup = "0",
EntryDate = DateTime.Today
};
RaisePropertyChanged(nameof(IsAddMode));
RaisePropertyChanged(nameof(DialogTitle));
}
public void InitializeForEdit(MesXslRawMaterialCard card)
{
Card = new MesXslRawMaterialCard
{
Id = card.Id,
Barcode = card.Barcode,
BatchNo = card.BatchNo,
EntryDate = card.EntryDate,
MaterialId = card.MaterialId,
MaterialName = card.MaterialName,
MaterialDesc = card.MaterialDesc,
SupplierId = card.SupplierId,
SupplierName = card.SupplierName,
ManufacturerMaterialName = card.ManufacturerMaterialName,
ShelfLife = card.ShelfLife,
TotalWeight = card.TotalWeight,
RemainingWeight = card.RemainingWeight,
RemainingQuantity = card.RemainingQuantity,
Status = card.Status,
TestResult = card.TestResult,
WarehouseArea = card.WarehouseArea,
UnloadOperator = card.UnloadOperator,
PriorityPickup = card.PriorityPickup,
TenantId = card.TenantId,
UpdateTime = card.UpdateTime
};
RaisePropertyChanged(nameof(IsAddMode));
RaisePropertyChanged(nameof(DialogTitle));
}
private async Task SaveAsync()
{
if (Card == null) return;
if (string.IsNullOrWhiteSpace(Card.MaterialName))
{
HandyControl.Controls.MessageBox.Warning("物料名称不能为空!");
return;
}
try
{
bool ok;
if (IsAddMode)
{
ok = await _cardService.AddAsync(Card);
if (ok) HandyControl.Controls.MessageBox.Success("新增原材料卡片成功!");
else { HandyControl.Controls.MessageBox.Error("新增原材料卡片失败!"); return; }
}
else
{
ok = await _cardService.EditAsync(Card);
if (!ok) { HandyControl.Controls.MessageBox.Error("编辑原材料卡片失败!"); return; }
}
Result = ok;
CloseAction?.Invoke();
}
catch (Exception ex)
{
HandyControl.Controls.MessageBox.Error($"操作失败:{ex.Message}");
}
}
}

View File

@@ -0,0 +1,272 @@
using HandyControl.Controls;
using HandyControl.Tools.Extension;
using Prism.Events;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
using YY.Admin.Core;
using YY.Admin.Core.Events;
using YY.Admin.Core.Helper;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service;
using YY.Admin.Views.RawMaterialCard;
namespace YY.Admin.ViewModels.RawMaterialCard;
public class RawMaterialCardListViewModel : BaseViewModel
{
private readonly IRawMaterialCardService _cardService;
private readonly IJeecgDictSyncService _dictSyncService;
private SubscriptionToken? _changedToken;
private SubscriptionToken? _syncConflictToken;
private ObservableCollection<MesXslRawMaterialCard> _cards = new();
public ObservableCollection<MesXslRawMaterialCard> Cards
{
get => _cards;
set => SetProperty(ref _cards, value);
}
private long _total;
public long Total { get => _total; set => SetProperty(ref _total, value); }
private int _pageNo = 1;
public int PageNo { get => _pageNo; set => SetProperty(ref _pageNo, value); }
private int _pageSize = 20;
public int PageSize { get => _pageSize; set => SetProperty(ref _pageSize, value); }
private string? _filterBarcode;
public string? FilterBarcode { get => _filterBarcode; set => SetProperty(ref _filterBarcode, value); }
private string? _filterBatchNo;
public string? FilterBatchNo { get => _filterBatchNo; set => SetProperty(ref _filterBatchNo, value); }
private string? _filterMaterialName;
public string? FilterMaterialName { get => _filterMaterialName; set => SetProperty(ref _filterMaterialName, value); }
private string? _filterSupplierName;
public string? FilterSupplierName { get => _filterSupplierName; set => SetProperty(ref _filterSupplierName, value); }
private string? _filterStatus;
public string? FilterStatus { get => _filterStatus; set => SetProperty(ref _filterStatus, value); }
public ObservableCollection<KeyValuePair<string, string>> StatusOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> TestResultOptions { get; } = new();
public DelegateCommand SearchCommand { get; }
public DelegateCommand ResetCommand { get; }
public DelegateCommand AddCommand { get; }
public DelegateCommand<MesXslRawMaterialCard> EditCommand { get; }
public DelegateCommand<MesXslRawMaterialCard> DeleteCommand { get; }
public DelegateCommand<MesXslRawMaterialCard> TogglePriorityCommand { get; }
public DelegateCommand PrevPageCommand { get; }
public DelegateCommand NextPageCommand { get; }
public RawMaterialCardListViewModel(
IRawMaterialCardService cardService,
IJeecgDictSyncService dictSyncService,
IContainerExtension container,
IRegionManager regionManager) : base(container, regionManager)
{
_cardService = cardService;
_dictSyncService = dictSyncService;
SearchCommand = new DelegateCommand(async () => { PageNo = 1; await LoadAsync(); });
ResetCommand = new DelegateCommand(async () =>
{
FilterBarcode = null; FilterBatchNo = null; FilterMaterialName = null;
FilterSupplierName = null; FilterStatus = null; PageNo = 1;
await LoadAsync();
});
AddCommand = new DelegateCommand(async () => await ShowAddDialogAsync());
EditCommand = new DelegateCommand<MesXslRawMaterialCard>(async c => await ShowEditDialogAsync(c));
DeleteCommand = new DelegateCommand<MesXslRawMaterialCard>(async c => await DeleteAsync(c));
TogglePriorityCommand = new DelegateCommand<MesXslRawMaterialCard>(async c => await TogglePriorityAsync(c));
PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } });
NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } });
_changedToken = _eventAggregator.GetEvent<RawMaterialCardChangedEvent>()
.Subscribe(async p => await OnChangedAsync(p), ThreadOption.UIThread);
_syncConflictToken = _eventAggregator.GetEvent<SyncConflictEvent>()
.Subscribe(OnSyncConflict, ThreadOption.UIThread);
_ = InitializeAsync();
}
private async Task OnChangedAsync(RawMaterialCardChangedPayload payload)
{
if (payload.Action == "edit" && !string.IsNullOrWhiteSpace(payload.CardId))
await RefreshSingleAsync(payload.CardId!);
else
await LoadAsync();
}
private async Task RefreshSingleAsync(string cardId)
{
try
{
var updated = await _cardService.GetByIdAsync(cardId);
if (updated == null) return;
var idx = Cards.ToList().FindIndex(c => string.Equals(c.Id, cardId, StringComparison.OrdinalIgnoreCase));
if (idx >= 0) Cards[idx] = updated;
}
catch (Exception ex)
{
Debug.WriteLine($"[原材料卡片] 单条刷新失败: {ex.Message}");
}
}
private void OnSyncConflict(SyncConflictPayload payload)
{
if (!string.Equals(payload.EntityName, "原材料卡片", StringComparison.OrdinalIgnoreCase)) return;
var parts = new List<string>();
if (payload.PushedCount > 0) parts.Add($"已同步 {payload.PushedCount} 条本地改动到服务器");
if (payload.NewRecordsPushed > 0) parts.Add($"已上传 {payload.NewRecordsPushed} 条本地新增记录");
if (payload.ConflictCount > 0) parts.Add($"{payload.ConflictCount} 条记录与服务器版本冲突,已保留服务器版本");
if (parts.Count == 0) return;
var message = string.Join("\n", parts);
if (payload.ConflictCount > 0) Growl.Warning(message);
else Growl.Success(message);
}
private async Task InitializeAsync()
{
try
{
await LoadDictOptionsAsync();
await UIHelper.WaitForRenderAsync();
await LoadAsync();
}
catch (Exception ex)
{
Debug.WriteLine($"[原材料卡片] 初始化失败: {ex.Message}");
}
}
private async Task LoadDictOptionsAsync()
{
try
{
var statusOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_card_status", includeAll: true);
var testResultOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_test_result", includeAll: true);
StatusOptions.Clear();
foreach (var item in statusOpts) StatusOptions.Add(item);
if (StatusOptions.Count == 0) StatusOptions.Add(new KeyValuePair<string, string>("全部", ""));
TestResultOptions.Clear();
foreach (var item in testResultOpts) TestResultOptions.Add(item);
if (TestResultOptions.Count == 0) TestResultOptions.Add(new KeyValuePair<string, string>("全部", ""));
}
catch
{
StatusOptions.Clear();
StatusOptions.Add(new KeyValuePair<string, string>("全部", ""));
StatusOptions.Add(new KeyValuePair<string, string>("正常", "1"));
StatusOptions.Add(new KeyValuePair<string, string>("异常", "0"));
TestResultOptions.Clear();
TestResultOptions.Add(new KeyValuePair<string, string>("全部", ""));
TestResultOptions.Add(new KeyValuePair<string, string>("未检", "0"));
TestResultOptions.Add(new KeyValuePair<string, string>("合格", "1"));
TestResultOptions.Add(new KeyValuePair<string, string>("不合格", "2"));
}
}
public async Task LoadAsync()
{
try
{
IsLoading = true;
var result = await _cardService.PageAsync(PageNo, PageSize,
FilterBarcode, FilterBatchNo, FilterMaterialName, FilterSupplierName, FilterStatus);
Cards = new ObservableCollection<MesXslRawMaterialCard>(result.Records);
Total = result.Total;
}
catch (Exception ex)
{
Growl.Error($"加载原材料卡片失败:{ex.Message}");
}
finally
{
IsLoading = false;
}
}
private async Task ShowAddDialogAsync()
{
try
{
var result = await HandyControl.Controls.Dialog.Show<RawMaterialCardEditDialogView>()
.Initialize<RawMaterialCardEditDialogViewModel>(vm => vm.InitializeForAdd())
.GetResultAsync<bool>();
if (result) await LoadAsync();
}
catch (Exception ex)
{
Growl.Error($"打开新增对话框失败:{ex.Message}");
}
}
private async Task ShowEditDialogAsync(MesXslRawMaterialCard card)
{
if (card == null) return;
try
{
var result = await HandyControl.Controls.Dialog.Show<RawMaterialCardEditDialogView>()
.Initialize<RawMaterialCardEditDialogViewModel>(vm => vm.InitializeForEdit(card))
.GetResultAsync<bool>();
if (result) await LoadAsync();
}
catch (Exception ex)
{
Growl.Error($"打开编辑对话框失败:{ex.Message}");
}
}
private async Task DeleteAsync(MesXslRawMaterialCard card)
{
if (card?.Id == null) return;
var confirm = System.Windows.MessageBox.Show(
$"确定删除原材料卡片(条码:{card.Barcode})?此操作不可恢复!",
"确认删除", MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (confirm != System.Windows.MessageBoxResult.OK) return;
var ok = await _cardService.DeleteAsync(card.Id);
if (ok) { Growl.Success("删除成功!"); await LoadAsync(); }
else Growl.Error("删除失败!");
}
private async Task TogglePriorityAsync(MesXslRawMaterialCard card)
{
if (card?.Id == null) return;
var newVal = card.PriorityPickup == "1" ? "0" : "1";
var ok = await _cardService.UpdatePriorityAsync(card.Id, newVal);
if (ok)
{
card.PriorityPickup = newVal;
RaisePropertyChanged(nameof(Cards));
}
else
{
Growl.Error("优先出库设置失败!");
}
}
protected override void CleanUp()
{
base.CleanUp();
if (_changedToken != null)
{
_eventAggregator.GetEvent<RawMaterialCardChangedEvent>().Unsubscribe(_changedToken);
_changedToken = null;
}
if (_syncConflictToken != null)
{
_eventAggregator.GetEvent<SyncConflictEvent>().Unsubscribe(_syncConflictToken);
_syncConflictToken = null;
}
}
}

View File

@@ -9,17 +9,21 @@ using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service;
using YY.Admin.Views.RawMaterialEntry;
using YY.Admin.ViewModels.WeightRecord;
using YY.Admin.Views.WeightRecord;
namespace YY.Admin.ViewModels.RawMaterialEntry;
public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResultable<bool>
{
private readonly IRawMaterialEntryService _entryService;
/// <summary>供子页面(如独立新增页右侧面板)复用列表/详情查询能力。</summary>
protected IRawMaterialEntryService EntryService => _entryService;
private readonly IJeecgDictSyncService _dictSyncService;
private readonly IMixerMaterialService _mixerMaterialService;
// 加载完物料后用于回填 Edit 模式选中项
private string? _pendingMaterialId;
protected string? _pendingMaterialId;
private MesXslRawMaterialEntry? _entry;
public MesXslRawMaterialEntry? Entry
@@ -28,7 +32,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
set => SetProperty(ref _entry, value);
}
private MesMixerMaterial? _selectedMaterial;
protected MesMixerMaterial? _selectedMaterial;
public MesMixerMaterial? SelectedMaterial
{
get => _selectedMaterial;
@@ -94,19 +98,6 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
{
if (Entry == null || Entry.TotalWeight == value) return;
Entry.TotalWeight = value;
RecalculatePortionWeight();
RaisePropertyChanged(nameof(Entry));
}
}
public int? TotalPortionsInput
{
get => Entry?.TotalPortions;
set
{
if (Entry == null || Entry.TotalPortions == value) return;
Entry.TotalPortions = value;
RecalculatePortionWeight();
RaisePropertyChanged(nameof(Entry));
}
}
@@ -137,6 +128,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
public DelegateCommand ClearMaterialCommand { get; }
public DelegateCommand OpenWeightRecordPickerCommand { get; }
public DelegateCommand ClearWeightRecordCommand { get; }
public DelegateCommand OpenSupplierPickerCommand { get; }
public DelegateCommand ClearSupplierCommand { get; }
public RawMaterialEntryEditDialogViewModel(
IRawMaterialEntryService entryService,
@@ -157,6 +150,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
ClearMaterialCommand = new DelegateCommand(ClearMaterialSelection);
OpenWeightRecordPickerCommand = new DelegateCommand(async () => await OpenWeightRecordPickerAsync());
ClearWeightRecordCommand = new DelegateCommand(ClearWeightRecordSelection);
OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync());
ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection);
SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChanged;
_ = LoadAllAsync();
}
@@ -166,7 +161,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
await Task.WhenAll(LoadDictOptionsAsync(), LoadMaterialOptionsAsync());
}
private async Task LoadMaterialOptionsAsync()
protected async Task LoadMaterialOptionsAsync()
{
try
{
@@ -228,11 +223,13 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
});
PopulateOptions(StatusOptions, statusOpts, Array.Empty<KeyValuePair<string, string>>());
ApplyDefaultEntryStatusForAdd();
ApplyHiddenFieldDefaultsForAdd();
}
catch
{
FillFallbackOptions();
ApplyDefaultEntryStatusForAdd();
ApplyHiddenFieldDefaultsForAdd();
}
}
@@ -270,7 +267,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
PopulateOptions(IsSpecialAdoptionOptions, Array.Empty<KeyValuePair<string, string>>(), ynDefault);
}
private async Task AutoGenerateBarcodeAsync(string materialCode)
protected async Task AutoGenerateBarcodeAsync(string materialCode)
{
IsGenerating = true;
try
@@ -286,7 +283,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
finally { IsGenerating = false; }
}
public void InitializeForAdd()
public virtual void InitializeForAdd()
{
_selectedMaterial = null;
RaisePropertyChanged(nameof(SelectedMaterial));
@@ -299,10 +296,10 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
};
InitializeSplitCodeDetailsFromEntry();
ApplyDefaultEntryStatusForAdd();
ApplyHiddenFieldDefaultsForAdd();
RaisePropertyChanged(nameof(IsAddMode));
RaisePropertyChanged(nameof(DialogTitle));
RaisePropertyChanged(nameof(TotalWeightInput));
RaisePropertyChanged(nameof(TotalPortionsInput));
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
@@ -346,20 +343,20 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
RaisePropertyChanged(nameof(IsAddMode));
RaisePropertyChanged(nameof(DialogTitle));
RaisePropertyChanged(nameof(TotalWeightInput));
RaisePropertyChanged(nameof(TotalPortionsInput));
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
}
private async Task SaveAsync()
protected virtual async Task SaveAsync()
{
if (Entry == null) return;
try
{
ApplyFirstSplitDetailToEntry();
RecalculatePortionWeight();
ApplySplitDetailsToEntry();
ApplyDefaultEntryStatusForAdd();
ApplyHiddenFieldDefaultsForAdd();
bool ok;
if (IsAddMode)
{
@@ -481,6 +478,34 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
RaisePropertyChanged(nameof(Entry));
}
private async Task OpenSupplierPickerAsync()
{
SupplierPickerDialogViewModel? pickerVm = null;
bool confirmed;
try
{
confirmed = await HandyControl.Controls.Dialog.Show<SupplierPickerDialogView>()
.Initialize<SupplierPickerDialogViewModel>(vm => pickerVm = vm)
.GetResultAsync<bool>();
}
catch { return; }
if (!confirmed || pickerVm?.SelectedSupplier == null || Entry == null) return;
var selected = pickerVm.SelectedSupplier;
Entry.SupplierId = selected.Id;
Entry.SupplierName = selected.SupplierName;
RaisePropertyChanged(nameof(Entry));
}
private void ClearSupplierSelection()
{
if (Entry == null) return;
Entry.SupplierId = null;
Entry.SupplierName = null;
RaisePropertyChanged(nameof(Entry));
}
private void RecalculateShelfLife(MesMixerMaterial? material)
{
if (Entry == null || material?.ShelfLifeDays == null || material.ShelfLifeDays <= 0)
@@ -491,23 +516,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
Entry.ShelfLife = DateTime.Now.Date.AddDays(material.ShelfLifeDays.Value).ToString("yyyy-MM-dd");
}
private void RecalculatePortionWeight()
{
if (Entry == null)
{
return;
}
if (Entry.TotalWeight.HasValue && Entry.TotalPortions.HasValue && Entry.TotalPortions.Value > 0)
{
Entry.PortionWeight = Math.Round(Entry.TotalWeight.Value / Entry.TotalPortions.Value, 2, MidpointRounding.AwayFromZero);
return;
}
Entry.PortionWeight = null;
}
private void ApplyDefaultEntryStatusForAdd()
protected void ApplyDefaultEntryStatusForAdd()
{
if (!IsAddMode || Entry == null || !string.IsNullOrWhiteSpace(Entry.Status))
{
@@ -526,19 +535,92 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
Entry.Status = "0";
}
private void InitializeSplitCodeDetailsFromEntry()
/// <summary>
/// 新增模式下为已在前端隐藏的字段补充默认值,确保保存到后端时不为空:
/// 检测结果=未检、检测状态=送样、打印标记=未打印、入库结存=否。
/// 字典就绪时取字典 code未就绪时回退到约定的常用 code。
/// </summary>
protected void ApplyHiddenFieldDefaultsForAdd()
{
if (!IsAddMode || Entry == null)
{
return;
}
if (string.IsNullOrWhiteSpace(Entry.TestResult))
{
Entry.TestResult = ResolveDefaultOptionValue(TestResultOptions, "未检", "0");
}
if (string.IsNullOrWhiteSpace(Entry.TestStatus))
{
Entry.TestStatus = ResolveDefaultOptionValue(TestStatusOptions, "送样", "0");
}
if (string.IsNullOrWhiteSpace(Entry.PrintFlag))
{
Entry.PrintFlag = ResolveDefaultOptionValue(PrintFlagOptions, "未打印", "0");
}
if (string.IsNullOrWhiteSpace(Entry.StockBalance))
{
Entry.StockBalance = ResolveDefaultOptionValue(StockBalanceOptions, "否", "0");
}
RaisePropertyChanged(nameof(Entry));
}
/// <summary>在字典选项中按 Key显示标签查找对应 Value实际 code找不到则回退到 fallback。</summary>
private static string ResolveDefaultOptionValue(
ObservableCollection<KeyValuePair<string, string>> options,
string keyLabel,
string fallbackValue)
{
var match = options.FirstOrDefault(x =>
string.Equals(x.Key?.Trim(), keyLabel, StringComparison.OrdinalIgnoreCase));
return !string.IsNullOrWhiteSpace(match.Value) ? match.Value : fallbackValue;
}
protected void InitializeSplitCodeDetailsFromEntry()
{
SplitCodeDetails.Clear();
SplitCodeDetails.Add(new RawMaterialSplitDetailItem
// 三个字段是按拆码明细多行拼接的字符串(末尾带 /),解析回明细列表
var portionsArr = SplitJoinedValues(Entry?.TotalPortions);
var weightArr = SplitJoinedValues(Entry?.PortionWeight);
var packagesArr = SplitJoinedValues(Entry?.PortionPackages);
var rowCount = Math.Max(1, Math.Max(portionsArr.Length, Math.Max(weightArr.Length, packagesArr.Length)));
for (var i = 0; i < rowCount; i++)
{
Portions = Entry?.TotalPortions,
PortionWeight = Entry?.PortionWeight,
PortionPackages = Entry?.PortionPackages,
WarehouseLocation = Entry?.WarehouseLocation
});
SplitCodeDetails.Add(new RawMaterialSplitDetailItem
{
Portions = TryParseInt(GetAt(portionsArr, i)),
PortionWeight = TryParseDouble(GetAt(weightArr, i)),
PortionPackages = TryParseInt(GetAt(packagesArr, i)),
// 库位是单值字段,仅赋给首行
WarehouseLocation = i == 0 ? Entry?.WarehouseLocation : null,
});
}
RaisePropertyChanged(nameof(SplitCodeTableHeight));
}
private static string[] SplitJoinedValues(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return Array.Empty<string>();
return value
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Where(x => x.Length > 0)
.ToArray();
}
private static string? GetAt(string[] arr, int index) => index < arr.Length ? arr[index] : null;
private static int? TryParseInt(string? text)
=> int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v : null;
private static double? TryParseDouble(string? text)
=> double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : null;
private void AddSplitDetailRow()
{
SplitCodeDetails.Add(new RawMaterialSplitDetailItem());
@@ -625,24 +707,29 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
return value.Value.ToString("0.##", CultureInfo.InvariantCulture);
}
private void ApplyFirstSplitDetailToEntry()
/// <summary>
/// 把 SplitCodeDetails 全部明细行的「份数 / 每份重量 / 每份包数」按 "x/y/z/" 拼接后持久化到 Entry
/// 库位仍取首行(业务上库位是单值)。与 SplitTotalPortionsDisplay 等只读展示属性同规则。
/// </summary>
private void ApplySplitDetailsToEntry()
{
if (Entry == null || SplitCodeDetails.Count == 0)
{
return;
}
if (Entry == null) return;
var first = SplitCodeDetails[0];
Entry.TotalPortions = first.Portions;
Entry.PortionWeight = first.PortionWeight;
Entry.PortionPackages = first.PortionPackages;
Entry.WarehouseLocation = first.WarehouseLocation;
Entry.TotalPortions = JoinSplitValue(it => it.Portions?.ToString(CultureInfo.InvariantCulture), true);
Entry.PortionWeight = JoinSplitValue(it => FormatNullableDecimal(it.PortionWeight), true);
Entry.PortionPackages = JoinSplitValue(it => it.PortionPackages?.ToString(CultureInfo.InvariantCulture), true);
if (SplitCodeDetails.Count > 0)
{
Entry.WarehouseLocation = SplitCodeDetails[0].WarehouseLocation;
}
}
private double CalculateSplitCodeTableHeight()
{
const double headerHeight = 36d;
const double rowHeight = 36d;
// 与拆码明细 XAML 行高保持一致:表头 40px、数据行 44px
const double headerHeight = 40d;
const double rowHeight = 44d;
const double framePadding = 16d;
const double minRows = 1d;
const double maxRows = 6d;

View File

@@ -1,17 +1,288 @@
using Prism.Commands;
using System.Collections.ObjectModel;
using System.IO;
using System.Text.Json;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service;
namespace YY.Admin.ViewModels.RawMaterialEntry;
/// <summary>
/// 「新增原料入场记录」独立页面:左侧表单逻辑继承编辑 VM右侧展示当日入场简要列表并支持选中回填模板。
/// </summary>
public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel
{
private const int TodayListFetchSize = 5000;
private readonly IRawMaterialCardService _rawMaterialCardService;
private static readonly JsonSerializerOptions LayoutJsonOpts = new()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
private static string LayoutFilePath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"YY.Admin",
"raw-material-entry-add-layout.json");
private bool _suppressTodaySelectionReaction;
public ObservableCollection<MesXslRawMaterialEntry> TodayEntries { get; } = new();
private MesXslRawMaterialEntry? _selectedTodayEntry;
public MesXslRawMaterialEntry? SelectedTodayEntry
{
get => _selectedTodayEntry;
set
{
if (!SetProperty(ref _selectedTodayEntry, value)) return;
if (_suppressTodaySelectionReaction || value == null) return;
_ = ApplyTodayRowToFormAsync(value);
}
}
private bool _isRightPanelExpanded = true;
public bool IsRightPanelExpanded
{
get => _isRightPanelExpanded;
set
{
if (!SetProperty(ref _isRightPanelExpanded, value)) return;
SaveLayoutState();
}
}
private double _expandedRightPanelWidth = 280;
/// <summary>右侧面板展开时的目标宽度(像素),由 GridSplitter 拖拽结束时回写。</summary>
public double ExpandedRightPanelWidth
{
get => _expandedRightPanelWidth;
private set
{
var v = Math.Clamp(value, 200, 560);
if (SetProperty(ref _expandedRightPanelWidth, v))
SaveLayoutState();
}
}
/// <summary>仅当入场记录已保存(有 Id且存在拆码明细时允许生成原材料卡片。</summary>
public bool CanGenerateCards => !string.IsNullOrWhiteSpace(Entry?.Id) && SplitCodeDetails.Count > 0;
public DelegateCommand ToggleRightPanelCommand { get; }
public DelegateCommand RefreshTodayEntriesCommand { get; }
public DelegateCommand GenerateRawMaterialCardsCommand { get; }
public RawMaterialEntryOperationViewModel(
IRawMaterialEntryService entryService,
IJeecgDictSyncService dictSyncService,
IMixerMaterialService mixerMaterialService,
IRawMaterialCardService rawMaterialCardService,
IContainerExtension container,
IRegionManager regionManager)
: base(entryService, dictSyncService, mixerMaterialService, container, regionManager)
{
_rawMaterialCardService = rawMaterialCardService;
LoadLayoutState();
ToggleRightPanelCommand = new DelegateCommand(() => IsRightPanelExpanded = !IsRightPanelExpanded);
RefreshTodayEntriesCommand = new DelegateCommand(async () => await LoadTodayEntriesAsync());
GenerateRawMaterialCardsCommand = new DelegateCommand(async () => await GenerateRawMaterialCardsAsync());
SplitCodeDetails.CollectionChanged += (_, _) => RaisePropertyChanged(nameof(CanGenerateCards));
}
public override void InitializeForAdd()
{
_suppressTodaySelectionReaction = true;
_selectedTodayEntry = null;
RaisePropertyChanged(nameof(SelectedTodayEntry));
_suppressTodaySelectionReaction = false;
base.InitializeForAdd();
RaisePropertyChanged(nameof(CanGenerateCards));
}
/// <summary>页面首次加载时拉取「今日」列表(由 View Loaded 调用)。</summary>
public async Task LoadTodayEntriesOnFirstShowAsync() => await LoadTodayEntriesAsync();
/// <summary>用户在拖拽结束 GridSplitter 后提交实际列宽。</summary>
public void CommitRightPanelWidthFromView(double actualWidth)
{
if (!IsRightPanelExpanded || actualWidth < 1) return;
ExpandedRightPanelWidth = actualWidth;
}
private async Task LoadTodayEntriesAsync()
{
try
{
IsLoading = true;
var result = await EntryService.PageAsync(1, TodayListFetchSize);
var today = DateTime.Today;
var rows = result.Records
.Where(e => IsTodayEntry(e, today))
.OrderByDescending(e => e.EntryTime ?? e.CreateTime ?? DateTime.MinValue)
.ToList();
TodayEntries.Clear();
foreach (var r in rows)
TodayEntries.Add(r);
}
catch
{
// 列表失败不阻断左侧新增(与物料加载策略一致)
}
finally
{
IsLoading = false;
}
}
private static bool IsTodayEntry(MesXslRawMaterialEntry e, DateTime today)
{
var byEntry = e.EntryTime?.Date == today;
var byCreate = e.CreateTime?.Date == today;
return byEntry || byCreate;
}
/// <summary>
/// 点击右侧今日记录:直接以「编辑模式」加载源记录到左侧表单(保留 Id/条码/批次号),
/// 不再走「新增模板」逻辑,避免重新生成条码、覆盖原数据。
/// </summary>
private async Task ApplyTodayRowToFormAsync(MesXslRawMaterialEntry src)
{
// 复用基类 InitializeForEdit保留 Id/Barcode/BatchNo/状态等所有字段,标题自动切到「编辑原料入场记录」
base.InitializeForEdit(src);
RaisePropertyChanged(nameof(CanGenerateCards));
// 若物料列表此前未加载导致选中项未回填,则补一次拉取(与编辑弹窗逻辑一致)
if (_pendingMaterialId != null)
await LoadMaterialOptionsAsync();
}
/// <summary>编辑保存成功后:刷新右侧今日列表并切回新增态,避免连续误改同一条。</summary>
protected override async Task SaveAsync()
{
var wasEdit = !IsAddMode;
await base.SaveAsync();
RaisePropertyChanged(nameof(CanGenerateCards));
if (wasEdit && Result && CloseAction == null)
{
HandyControl.Controls.MessageBox.Success("编辑成功!");
await LoadTodayEntriesAsync();
InitializeForAdd();
}
}
private async Task GenerateRawMaterialCardsAsync()
{
if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id))
{
HandyControl.Controls.MessageBox.Warning("请先保存入场记录再生成原材料卡片!");
return;
}
if (SplitCodeDetails.Count == 0)
{
HandyControl.Controls.MessageBox.Warning("当前入场记录无拆分明细,无法生成原材料卡片!");
return;
}
try
{
IsLoading = true;
var globalIndex = 1;
var baseBarcode = Entry.Barcode ?? "";
var failCount = 0;
foreach (var detail in SplitCodeDetails)
{
var portions = detail.Portions ?? 0;
for (var i = 0; i < portions; i++)
{
var card = new MesXslRawMaterialCard
{
Barcode = baseBarcode + globalIndex.ToString("D3"),
BatchNo = Entry.BatchNo,
EntryDate = Entry.EntryTime?.Date ?? DateTime.Today,
MaterialId = Entry.MaterialId,
MaterialName = Entry.MaterialName,
SupplierId = Entry.SupplierId,
SupplierName = Entry.SupplierName,
ManufacturerMaterialName = Entry.ManufacturerMaterialName,
ShelfLife = Entry.ShelfLife,
TotalWeight = detail.PortionWeight.HasValue ? (decimal?)detail.PortionWeight.Value : null,
RemainingWeight = detail.PortionWeight.HasValue ? (decimal?)detail.PortionWeight.Value : null,
RemainingQuantity = detail.PortionPackages,
WarehouseArea = detail.WarehouseLocation,
UnloadOperator = Entry.UnloadOperator,
Status = "1",
TestResult = "0",
PriorityPickup = "0",
TenantId = Entry.TenantId
};
var ok = await _rawMaterialCardService.AddAsync(card);
if (!ok) failCount++;
globalIndex++;
}
}
// 更新打印状态为「已打印」
Entry.PrintFlag = "1";
await EntryService.EditAsync(Entry);
RaisePropertyChanged(nameof(Entry));
var total = globalIndex - 1;
if (failCount == 0)
HandyControl.Controls.MessageBox.Success($"已生成 {total} 张原材料卡片,打印状态已更新为「已打印」!");
else
HandyControl.Controls.MessageBox.Warning($"共生成 {total} 张,其中 {failCount} 张失败,请检查网络后重试!");
}
catch (Exception ex)
{
HandyControl.Controls.MessageBox.Error($"生成原材料卡片失败:{ex.Message}");
}
finally
{
IsLoading = false;
}
}
private void LoadLayoutState()
{
try
{
if (!File.Exists(LayoutFilePath)) return;
var json = File.ReadAllText(LayoutFilePath);
var dto = JsonSerializer.Deserialize<AddPageLayoutDto>(json, LayoutJsonOpts);
if (dto == null) return;
if (dto.ExpandedWidth is > 0 and < 2000)
_expandedRightPanelWidth = Math.Clamp(dto.ExpandedWidth.Value, 200, 560);
if (dto.IsExpanded.HasValue)
_isRightPanelExpanded = dto.IsExpanded.Value;
RaisePropertyChanged(nameof(ExpandedRightPanelWidth));
RaisePropertyChanged(nameof(IsRightPanelExpanded));
}
catch { /* 布局文件损坏时忽略 */ }
}
private void SaveLayoutState()
{
try
{
var dir = Path.GetDirectoryName(LayoutFilePath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
var dto = new AddPageLayoutDto
{
ExpandedWidth = _expandedRightPanelWidth,
IsExpanded = _isRightPanelExpanded
};
File.WriteAllText(LayoutFilePath, JsonSerializer.Serialize(dto, LayoutJsonOpts));
}
catch { /* 写本地失败时不影响业务 */ }
}
private sealed class AddPageLayoutDto
{
public double? ExpandedWidth { get; set; }
public bool? IsExpanded { get; set; }
}
}