771 lines
29 KiB
C#
771 lines
29 KiB
C#
using HandyControl.Controls;
|
||
using HandyControl.Tools.Extension;
|
||
using System.Collections.Specialized;
|
||
using System.ComponentModel;
|
||
using System.Collections.ObjectModel;
|
||
using System.Globalization;
|
||
using YY.Admin.Core;
|
||
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 模式选中项
|
||
protected string? _pendingMaterialId;
|
||
|
||
private MesXslRawMaterialEntry? _entry;
|
||
public MesXslRawMaterialEntry? Entry
|
||
{
|
||
get => _entry;
|
||
set => SetProperty(ref _entry, value);
|
||
}
|
||
|
||
protected MesMixerMaterial? _selectedMaterial;
|
||
public MesMixerMaterial? SelectedMaterial
|
||
{
|
||
get => _selectedMaterial;
|
||
set
|
||
{
|
||
if (!SetProperty(ref _selectedMaterial, value) || value == null || Entry == null) return;
|
||
Entry.MaterialId = value.Id;
|
||
Entry.MaterialCode = value.MaterialCode;
|
||
Entry.MaterialName = value.MaterialName;
|
||
Entry.ManufacturerMaterialName = value.AliasName;
|
||
RecalculateShelfLife(value);
|
||
RaisePropertyChanged(nameof(Entry));
|
||
RaisePropertyChanged(nameof(SelectedMaterialDisplay));
|
||
RaisePropertyChanged(nameof(HasSelectedMaterial));
|
||
|
||
// 新增模式自动生成条码/批次号
|
||
if (IsAddMode && !string.IsNullOrEmpty(value.MaterialCode))
|
||
_ = AutoGenerateBarcodeAsync(value.MaterialCode);
|
||
}
|
||
}
|
||
|
||
private bool _isGenerating;
|
||
public bool IsGenerating
|
||
{
|
||
get => _isGenerating;
|
||
set => SetProperty(ref _isGenerating, value);
|
||
}
|
||
|
||
public bool IsAddMode => string.IsNullOrWhiteSpace(Entry?.Id);
|
||
public string DialogTitle => IsAddMode ? "新增原料入场记录" : "编辑原料入场记录";
|
||
public string SelectedMaterialDisplay => _selectedMaterial == null
|
||
? "请选择密炼物料"
|
||
: $"[{_selectedMaterial.MaterialCode}] {_selectedMaterial.MaterialName}";
|
||
public bool HasSelectedMaterial => _selectedMaterial != null;
|
||
|
||
public string? IsSpecialAdoptionValue
|
||
{
|
||
get => Entry?.IsSpecialAdoption;
|
||
set
|
||
{
|
||
if (Entry == null || Entry.IsSpecialAdoption == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Entry.IsSpecialAdoption = value;
|
||
if (!string.Equals(value, "1", StringComparison.Ordinal))
|
||
{
|
||
Entry.SpecialAdoptionOperator = null;
|
||
Entry.SpecialAdoptionTime = null;
|
||
Entry.SpecialAdoptionReason = null;
|
||
}
|
||
|
||
RaisePropertyChanged(nameof(Entry));
|
||
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
|
||
}
|
||
}
|
||
|
||
public double? TotalWeightInput
|
||
{
|
||
get => Entry?.TotalWeight;
|
||
set
|
||
{
|
||
if (Entry == null || Entry.TotalWeight == value) return;
|
||
Entry.TotalWeight = value;
|
||
RaisePropertyChanged(nameof(Entry));
|
||
}
|
||
}
|
||
|
||
public ObservableCollection<MesMixerMaterial> MaterialOptions { get; } = new();
|
||
public ObservableCollection<KeyValuePair<string, string>> TestResultOptions { get; } = new();
|
||
public ObservableCollection<KeyValuePair<string, string>> TestStatusOptions { get; } = new();
|
||
public ObservableCollection<KeyValuePair<string, string>> PrintFlagOptions { get; } = new();
|
||
public ObservableCollection<KeyValuePair<string, string>> StockBalanceOptions { get; } = new();
|
||
public ObservableCollection<KeyValuePair<string, string>> IsSpecialAdoptionOptions { get; } = new();
|
||
public ObservableCollection<KeyValuePair<string, string>> StatusOptions { get; } = new();
|
||
public ObservableCollection<RawMaterialSplitDetailItem> SplitCodeDetails { get; } = new();
|
||
public double SplitCodeTableHeight => CalculateSplitCodeTableHeight();
|
||
public string SplitTotalPortionsDisplay => JoinSplitValue(item => item.Portions?.ToString(CultureInfo.InvariantCulture), true);
|
||
public string SplitPortionWeightDisplay => JoinSplitValue(item => FormatNullableDecimal(item.PortionWeight), true);
|
||
public string SplitPortionPackagesDisplay => JoinSplitValue(item => item.PortionPackages?.ToString(CultureInfo.InvariantCulture), true);
|
||
|
||
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 DelegateCommand ResetCommand { get; }
|
||
public DelegateCommand AddSplitDetailCommand { get; }
|
||
public DelegateCommand<RawMaterialSplitDetailItem> RemoveSplitDetailCommand { get; }
|
||
public DelegateCommand OpenMaterialPickerCommand { get; }
|
||
public DelegateCommand ClearMaterialCommand { get; }
|
||
public DelegateCommand OpenWeightRecordPickerCommand { get; }
|
||
public DelegateCommand ClearWeightRecordCommand { get; }
|
||
public DelegateCommand OpenSupplierPickerCommand { get; }
|
||
public DelegateCommand ClearSupplierCommand { get; }
|
||
|
||
public RawMaterialEntryEditDialogViewModel(
|
||
IRawMaterialEntryService entryService,
|
||
IJeecgDictSyncService dictSyncService,
|
||
IMixerMaterialService mixerMaterialService,
|
||
IContainerExtension container,
|
||
IRegionManager regionManager) : base(container, regionManager)
|
||
{
|
||
_entryService = entryService;
|
||
_dictSyncService = dictSyncService;
|
||
_mixerMaterialService = mixerMaterialService;
|
||
SaveCommand = new DelegateCommand(async () => await SaveAsync());
|
||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||
ResetCommand = new DelegateCommand(InitializeForAdd);
|
||
AddSplitDetailCommand = new DelegateCommand(AddSplitDetailRow);
|
||
RemoveSplitDetailCommand = new DelegateCommand<RawMaterialSplitDetailItem>(RemoveSplitDetailRow);
|
||
OpenMaterialPickerCommand = new DelegateCommand(async () => await OpenMaterialPickerAsync());
|
||
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();
|
||
}
|
||
|
||
private async Task LoadAllAsync()
|
||
{
|
||
await Task.WhenAll(LoadDictOptionsAsync(), LoadMaterialOptionsAsync());
|
||
}
|
||
|
||
protected async Task LoadMaterialOptionsAsync()
|
||
{
|
||
try
|
||
{
|
||
// 每次打开弹窗都主动拉一次,确保直接写库的数据也能同步到
|
||
await _mixerMaterialService.SyncFromRemoteAsync();
|
||
var result = await _mixerMaterialService.PageAsync(1, 2000);
|
||
MaterialOptions.Clear();
|
||
foreach (var m in result.Records)
|
||
MaterialOptions.Add(m);
|
||
|
||
// Edit 模式:物料加载完后回填选中项
|
||
if (_pendingMaterialId != null)
|
||
{
|
||
var match = MaterialOptions.FirstOrDefault(m =>
|
||
string.Equals(m.Id, _pendingMaterialId, StringComparison.OrdinalIgnoreCase));
|
||
if (match != null)
|
||
SelectedMaterial = match;
|
||
_pendingMaterialId = null;
|
||
}
|
||
}
|
||
catch { /* 无法加载物料列表时不阻断表单 */ }
|
||
}
|
||
|
||
private async Task LoadDictOptionsAsync()
|
||
{
|
||
try
|
||
{
|
||
var testResultOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_test_result");
|
||
var testStatusOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_test_status");
|
||
var printFlagOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_print_flag");
|
||
var ynOpts = await _dictSyncService.GetDictOptionsAsync("yn");
|
||
var statusOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_entry_status");
|
||
|
||
PopulateOptions(TestResultOptions, testResultOpts, new[]
|
||
{
|
||
new KeyValuePair<string, string>("未检", "0"),
|
||
new KeyValuePair<string, string>("合格", "1"),
|
||
new KeyValuePair<string, string>("不合格", "2"),
|
||
});
|
||
PopulateOptions(TestStatusOptions, testStatusOpts, new[]
|
||
{
|
||
new KeyValuePair<string, string>("送样", "0"),
|
||
new KeyValuePair<string, string>("已批准", "1"),
|
||
});
|
||
PopulateOptions(PrintFlagOptions, printFlagOpts, new[]
|
||
{
|
||
new KeyValuePair<string, string>("未打印", "0"),
|
||
new KeyValuePair<string, string>("已打印", "1"),
|
||
});
|
||
PopulateOptions(StockBalanceOptions, ynOpts, new[]
|
||
{
|
||
new KeyValuePair<string, string>("否", "0"),
|
||
new KeyValuePair<string, string>("是", "1"),
|
||
});
|
||
PopulateOptions(IsSpecialAdoptionOptions, ynOpts, new[]
|
||
{
|
||
new KeyValuePair<string, string>("否", "0"),
|
||
new KeyValuePair<string, string>("是", "1"),
|
||
});
|
||
PopulateOptions(StatusOptions, statusOpts, Array.Empty<KeyValuePair<string, string>>());
|
||
ApplyDefaultEntryStatusForAdd();
|
||
ApplyHiddenFieldDefaultsForAdd();
|
||
}
|
||
catch
|
||
{
|
||
FillFallbackOptions();
|
||
ApplyDefaultEntryStatusForAdd();
|
||
ApplyHiddenFieldDefaultsForAdd();
|
||
}
|
||
}
|
||
|
||
private static void PopulateOptions(
|
||
ObservableCollection<KeyValuePair<string, string>> target,
|
||
IEnumerable<KeyValuePair<string, string>> items,
|
||
IEnumerable<KeyValuePair<string, string>> fallback)
|
||
{
|
||
target.Clear();
|
||
var list = items.ToList();
|
||
foreach (var item in list.Count > 0 ? list : fallback.ToList())
|
||
target.Add(item);
|
||
}
|
||
|
||
private void FillFallbackOptions()
|
||
{
|
||
PopulateOptions(TestResultOptions, Array.Empty<KeyValuePair<string, string>>(), new[]
|
||
{
|
||
new KeyValuePair<string, string>("未检", "0"),
|
||
new KeyValuePair<string, string>("合格", "1"),
|
||
new KeyValuePair<string, string>("不合格", "2"),
|
||
});
|
||
PopulateOptions(TestStatusOptions, Array.Empty<KeyValuePair<string, string>>(), new[]
|
||
{
|
||
new KeyValuePair<string, string>("送样", "0"),
|
||
new KeyValuePair<string, string>("已批准", "1"),
|
||
});
|
||
PopulateOptions(PrintFlagOptions, Array.Empty<KeyValuePair<string, string>>(), new[]
|
||
{
|
||
new KeyValuePair<string, string>("未打印", "0"),
|
||
new KeyValuePair<string, string>("已打印", "1"),
|
||
});
|
||
var ynDefault = new[] { new KeyValuePair<string, string>("否", "0"), new KeyValuePair<string, string>("是", "1") };
|
||
PopulateOptions(StockBalanceOptions, Array.Empty<KeyValuePair<string, string>>(), ynDefault);
|
||
PopulateOptions(IsSpecialAdoptionOptions, Array.Empty<KeyValuePair<string, string>>(), ynDefault);
|
||
}
|
||
|
||
protected async Task AutoGenerateBarcodeAsync(string materialCode)
|
||
{
|
||
IsGenerating = true;
|
||
try
|
||
{
|
||
var code = await _entryService.GenerateBarcodeAsync(materialCode);
|
||
if (!string.IsNullOrEmpty(code) && Entry != null)
|
||
{
|
||
Entry.Barcode = code;
|
||
Entry.BatchNo = code;
|
||
RaisePropertyChanged(nameof(Entry));
|
||
}
|
||
}
|
||
finally { IsGenerating = false; }
|
||
}
|
||
|
||
public virtual void InitializeForAdd()
|
||
{
|
||
_selectedMaterial = null;
|
||
RaisePropertyChanged(nameof(SelectedMaterial));
|
||
RaisePropertyChanged(nameof(SelectedMaterialDisplay));
|
||
RaisePropertyChanged(nameof(HasSelectedMaterial));
|
||
Entry = new MesXslRawMaterialEntry
|
||
{
|
||
EntryTime = DateTime.Now,
|
||
IsSpecialAdoption = "0"
|
||
};
|
||
InitializeSplitCodeDetailsFromEntry();
|
||
ApplyDefaultEntryStatusForAdd();
|
||
ApplyHiddenFieldDefaultsForAdd();
|
||
RaisePropertyChanged(nameof(IsAddMode));
|
||
RaisePropertyChanged(nameof(DialogTitle));
|
||
RaisePropertyChanged(nameof(TotalWeightInput));
|
||
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
|
||
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
||
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
||
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
|
||
}
|
||
|
||
public void InitializeForEdit(MesXslRawMaterialEntry entry)
|
||
{
|
||
Entry = new MesXslRawMaterialEntry
|
||
{
|
||
Id = entry.Id, Barcode = entry.Barcode, BatchNo = entry.BatchNo, EntryTime = entry.EntryTime,
|
||
WeightRecordId = entry.WeightRecordId, BillNo = entry.BillNo,
|
||
MaterialId = entry.MaterialId, MaterialCode = entry.MaterialCode, MaterialName = entry.MaterialName,
|
||
SupplyCustomer = entry.SupplyCustomer, SupplierId = entry.SupplierId, SupplierName = entry.SupplierName,
|
||
ManufacturerMaterialName = entry.ManufacturerMaterialName,
|
||
ShelfLife = entry.ShelfLife, TotalWeight = entry.TotalWeight, TotalPortions = entry.TotalPortions,
|
||
PortionWeight = entry.PortionWeight, PortionPackages = entry.PortionPackages,
|
||
TestResult = entry.TestResult, TestStatus = entry.TestStatus, PrintFlag = entry.PrintFlag,
|
||
StockBalance = entry.StockBalance, WarehouseLocation = entry.WarehouseLocation,
|
||
UnloadOperator = entry.UnloadOperator, IsSpecialAdoption = entry.IsSpecialAdoption,
|
||
SpecialAdoptionOperator = entry.SpecialAdoptionOperator, SpecialAdoptionTime = entry.SpecialAdoptionTime,
|
||
SpecialAdoptionReason = entry.SpecialAdoptionReason, Status = entry.Status, Remark = entry.Remark,
|
||
TenantId = entry.TenantId,
|
||
};
|
||
InitializeSplitCodeDetailsFromEntry();
|
||
|
||
// 若物料列表已加载则直接回填,否则记录 pending 等加载完后回填
|
||
if (MaterialOptions.Count > 0)
|
||
{
|
||
_selectedMaterial = MaterialOptions.FirstOrDefault(m =>
|
||
string.Equals(m.Id, entry.MaterialId, StringComparison.OrdinalIgnoreCase));
|
||
RaisePropertyChanged(nameof(SelectedMaterial));
|
||
RaisePropertyChanged(nameof(SelectedMaterialDisplay));
|
||
RaisePropertyChanged(nameof(HasSelectedMaterial));
|
||
}
|
||
else
|
||
{
|
||
_pendingMaterialId = entry.MaterialId;
|
||
}
|
||
|
||
RaisePropertyChanged(nameof(IsAddMode));
|
||
RaisePropertyChanged(nameof(DialogTitle));
|
||
RaisePropertyChanged(nameof(TotalWeightInput));
|
||
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
|
||
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
||
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
||
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
|
||
}
|
||
|
||
protected virtual async Task SaveAsync()
|
||
{
|
||
if (Entry == null) return;
|
||
try
|
||
{
|
||
ApplySplitDetailsToEntry();
|
||
ApplyDefaultEntryStatusForAdd();
|
||
ApplyHiddenFieldDefaultsForAdd();
|
||
bool ok;
|
||
if (IsAddMode)
|
||
{
|
||
ok = await _entryService.AddAsync(Entry);
|
||
if (ok) HandyControl.Controls.MessageBox.Success("新增成功!");
|
||
else { HandyControl.Controls.MessageBox.Error("新增失败!"); return; }
|
||
}
|
||
else
|
||
{
|
||
ok = await _entryService.EditAsync(Entry);
|
||
if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return; }
|
||
}
|
||
Result = ok;
|
||
if (IsAddMode && CloseAction == null)
|
||
{
|
||
// 独立新增页面:保存成功后自动清空表单,便于连续录入
|
||
InitializeForAdd();
|
||
return;
|
||
}
|
||
|
||
CloseAction?.Invoke();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandyControl.Controls.MessageBox.Error($"操作失败:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
private async Task OpenWeightRecordPickerAsync()
|
||
{
|
||
WeightRecordPickerDialogViewModel? pickerVm = null;
|
||
bool confirmed;
|
||
try
|
||
{
|
||
confirmed = await HandyControl.Controls.Dialog.Show<WeightRecordPickerDialogView>()
|
||
.Initialize<WeightRecordPickerDialogViewModel>(vm =>
|
||
{
|
||
pickerVm = vm;
|
||
vm.Initialize(Entry?.BillNo);
|
||
})
|
||
.GetResultAsync<bool>();
|
||
}
|
||
catch
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!confirmed || pickerVm?.SelectedRecord == null || Entry == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var selected = pickerVm.SelectedRecord;
|
||
Entry.WeightRecordId = selected.Id;
|
||
Entry.BillNo = selected.BillNo;
|
||
Entry.SupplierName = selected.SenderUnit;
|
||
Entry.SupplierId = null;
|
||
RaisePropertyChanged(nameof(Entry));
|
||
}
|
||
|
||
private async Task OpenMaterialPickerAsync()
|
||
{
|
||
RawMaterialEntryMaterialPickerDialogViewModel? pickerVm = null;
|
||
bool confirmed;
|
||
try
|
||
{
|
||
confirmed = await HandyControl.Controls.Dialog.Show<RawMaterialEntryMaterialPickerDialogView>()
|
||
.Initialize<RawMaterialEntryMaterialPickerDialogViewModel>(vm =>
|
||
{
|
||
pickerVm = vm;
|
||
vm.Initialize(Entry?.MaterialCode, Entry?.MaterialName);
|
||
})
|
||
.GetResultAsync<bool>();
|
||
}
|
||
catch
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!confirmed || pickerVm?.SelectedMaterial == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
SelectedMaterial = pickerVm.SelectedMaterial;
|
||
}
|
||
|
||
private void ClearMaterialSelection()
|
||
{
|
||
_selectedMaterial = null;
|
||
RaisePropertyChanged(nameof(SelectedMaterial));
|
||
RaisePropertyChanged(nameof(SelectedMaterialDisplay));
|
||
RaisePropertyChanged(nameof(HasSelectedMaterial));
|
||
|
||
if (Entry == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Entry.MaterialId = null;
|
||
Entry.MaterialCode = null;
|
||
Entry.MaterialName = null;
|
||
Entry.ManufacturerMaterialName = null;
|
||
Entry.ShelfLife = null;
|
||
RaisePropertyChanged(nameof(Entry));
|
||
}
|
||
|
||
private void ClearWeightRecordSelection()
|
||
{
|
||
if (Entry == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Entry.WeightRecordId = null;
|
||
Entry.BillNo = null;
|
||
Entry.SupplierId = null;
|
||
Entry.SupplierName = null;
|
||
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)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Entry.ShelfLife = DateTime.Now.Date.AddDays(material.ShelfLifeDays.Value).ToString("yyyy-MM-dd");
|
||
}
|
||
|
||
protected void ApplyDefaultEntryStatusForAdd()
|
||
{
|
||
if (!IsAddMode || Entry == null || !string.IsNullOrWhiteSpace(Entry.Status))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var pending = StatusOptions.FirstOrDefault(x =>
|
||
string.Equals(x.Key?.Trim(), "待处理", StringComparison.OrdinalIgnoreCase));
|
||
if (!string.IsNullOrWhiteSpace(pending.Value))
|
||
{
|
||
Entry.Status = pending.Value;
|
||
return;
|
||
}
|
||
|
||
// 字典未就绪时使用常见默认值
|
||
Entry.Status = "0";
|
||
}
|
||
|
||
/// <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();
|
||
|
||
// 三个字段是按拆码明细多行拼接的字符串(末尾带 /),解析回明细列表
|
||
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++)
|
||
{
|
||
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());
|
||
RaisePropertyChanged(nameof(SplitCodeTableHeight));
|
||
}
|
||
|
||
private void RemoveSplitDetailRow(RawMaterialSplitDetailItem? item)
|
||
{
|
||
if (item == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
SplitCodeDetails.Remove(item);
|
||
if (SplitCodeDetails.Count == 0)
|
||
{
|
||
SplitCodeDetails.Add(new RawMaterialSplitDetailItem());
|
||
}
|
||
RaisePropertyChanged(nameof(SplitCodeTableHeight));
|
||
}
|
||
|
||
private void OnSplitCodeDetailsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||
{
|
||
if (e.OldItems != null)
|
||
{
|
||
foreach (var oldItem in e.OldItems.OfType<RawMaterialSplitDetailItem>())
|
||
{
|
||
oldItem.PropertyChanged -= OnSplitDetailItemPropertyChanged;
|
||
}
|
||
}
|
||
|
||
if (e.NewItems != null)
|
||
{
|
||
foreach (var newItem in e.NewItems.OfType<RawMaterialSplitDetailItem>())
|
||
{
|
||
newItem.PropertyChanged += OnSplitDetailItemPropertyChanged;
|
||
}
|
||
}
|
||
|
||
RaiseSplitDisplayPropertyChanged();
|
||
RaisePropertyChanged(nameof(SplitCodeTableHeight));
|
||
}
|
||
|
||
private void OnSplitDetailItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||
{
|
||
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.Portions)
|
||
or nameof(RawMaterialSplitDetailItem.PortionWeight)
|
||
or nameof(RawMaterialSplitDetailItem.PortionPackages))
|
||
{
|
||
RaiseSplitDisplayPropertyChanged();
|
||
}
|
||
}
|
||
|
||
private void RaiseSplitDisplayPropertyChanged()
|
||
{
|
||
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
||
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
||
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
|
||
}
|
||
|
||
private string JoinSplitValue(Func<RawMaterialSplitDetailItem, string?> selector, bool withTrailingSlash)
|
||
{
|
||
var values = SplitCodeDetails
|
||
.Select(selector)
|
||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||
.Select(x => x!.Trim())
|
||
.ToList();
|
||
if (values.Count == 0)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var joined = string.Join("/", values);
|
||
return withTrailingSlash ? $"{joined}/" : joined;
|
||
}
|
||
|
||
private static string? FormatNullableDecimal(double? value)
|
||
{
|
||
if (!value.HasValue)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return value.Value.ToString("0.##", CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把 SplitCodeDetails 全部明细行的「份数 / 每份重量 / 每份包数」按 "x/y/z/" 拼接后持久化到 Entry,
|
||
/// 库位仍取首行(业务上库位是单值)。与 SplitTotalPortionsDisplay 等只读展示属性同规则。
|
||
/// </summary>
|
||
private void ApplySplitDetailsToEntry()
|
||
{
|
||
if (Entry == null) return;
|
||
|
||
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()
|
||
{
|
||
// 与拆码明细 XAML 行高保持一致:表头 40px、数据行 44px
|
||
const double headerHeight = 40d;
|
||
const double rowHeight = 44d;
|
||
const double framePadding = 16d;
|
||
const double minRows = 1d;
|
||
const double maxRows = 6d;
|
||
var rowCount = Math.Clamp(SplitCodeDetails.Count, (int)minRows, (int)maxRows);
|
||
return headerHeight + rowCount * rowHeight + framePadding;
|
||
}
|
||
}
|
||
|
||
public class RawMaterialSplitDetailItem : BindableBase
|
||
{
|
||
private int? _portions;
|
||
public int? Portions
|
||
{
|
||
get => _portions;
|
||
set => SetProperty(ref _portions, value);
|
||
}
|
||
|
||
private double? _portionWeight;
|
||
public double? PortionWeight
|
||
{
|
||
get => _portionWeight;
|
||
set => SetProperty(ref _portionWeight, value);
|
||
}
|
||
|
||
private int? _portionPackages;
|
||
public int? PortionPackages
|
||
{
|
||
get => _portionPackages;
|
||
set => SetProperty(ref _portionPackages, value);
|
||
}
|
||
|
||
private string? _warehouseLocation;
|
||
public string? WarehouseLocation
|
||
{
|
||
get => _warehouseLocation;
|
||
set => SetProperty(ref _warehouseLocation, value);
|
||
}
|
||
}
|