Files
qhmes/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryEditDialogViewModel.cs

771 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}