using HandyControl.Controls; using HandyControl.Data; 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.Services.Service.MixerMaterialTareStrategy; 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 { private readonly IRawMaterialEntryService _entryService; /// 供子页面(如独立新增页右侧面板)复用列表/详情查询能力。 protected IRawMaterialEntryService EntryService => _entryService; private readonly IJeecgDictSyncService _dictSyncService; private readonly IMixerMaterialService _mixerMaterialService; private readonly IMixerMaterialTareStrategyService _tareStrategyService; private readonly ISupplierService _supplierService; private bool _suppressTareStrategyRefresh; 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)); _ = RefreshAllRowsTareStrategyAsync(); // 新增模式自动生成条码/批次号 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 ? "新增原料入场记录" : "编辑原料入场记录"; /// 已打印(PrintFlag=="1"):总重只读、不可新增明细。 public bool IsPrinted => string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal); public bool IsTotalWeightEditable => !IsPrinted; 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 DateTime? EntryTimeInput { get => Entry?.EntryTime; set { if (Entry == null || Entry.EntryTime == value) return; Entry.EntryTime = value; RaisePropertyChanged(nameof(Entry)); _ = RefreshAllRowsTareStrategyAsync(); } } /// 基础资料「托盘及皮重(合计)」= Σ 份数 × (包装物皮重 + 托盘重量)。 public double PalletTareTotalDisplay => SplitCodeDetails.Sum(row => { var portions = row.Portions ?? 0; var packaging = row.PackagingTare ?? 0d; var pallet = row.PalletWeight ?? 0d; return portions * (packaging + pallet); }); public ObservableCollection MaterialOptions { get; } = new(); public ObservableCollection> TestResultOptions { get; } = new(); public ObservableCollection> TestStatusOptions { get; } = new(); public ObservableCollection> PrintFlagOptions { get; } = new(); public ObservableCollection> StockBalanceOptions { get; } = new(); public ObservableCollection> IsSpecialAdoptionOptions { get; } = new(); public ObservableCollection> StatusOptions { get; } = new(); public ObservableCollection SplitCodeDetails { get; } = new(); private bool _suspendEmbeddedPrintPreviewAirspace; /// /// WebView2 使用独立 HWND(Airspace),会同窗体内浮在 WPF 元素之上,遮挡 HandyControl 内嵌 Dialog。 /// 原料入场独立页在弹窗期间通过绑定收起预览宿主;其它页面无 WebView2 时可忽略该属性。 /// public bool SuspendEmbeddedPrintPreviewAirspace { get => _suspendEmbeddedPrintPreviewAirspace; private set { if (!SetProperty(ref _suspendEmbeddedPrintPreviewAirspace, value)) { return; } OnSuspendEmbeddedPrintPreviewAirspaceChanged(); } } /// 子类在预览区可见性依赖此标志时,覆写以联动通知。 protected virtual void OnSuspendEmbeddedPrintPreviewAirspaceChanged() { } /// 在异步弹窗期间挂起右侧嵌入 WebView2,避免遮挡模态内容。 protected async Task SuspendEmbeddedPrintPreviewAirspaceWhileAsync(Func> action) { SuspendEmbeddedPrintPreviewAirspace = true; try { return await action(); } finally { SuspendEmbeddedPrintPreviewAirspace = false; } } 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 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; } /// 拆码明细 - 库位选择命令(弹出「库区选择」弹窗,单选)。CommandParameter 为当前行 RawMaterialSplitDetailItem。 public DelegateCommand OpenWarehouseAreaPickerCommand { get; } /// 拆码明细 - 手动选择皮重策略。 public DelegateCommand OpenTareStrategyPickerCommand { get; } /// 拆码明细 - 清除手动策略并按规则重新自动匹配。 public DelegateCommand ResetTareStrategyCommand { get; } public RawMaterialEntryEditDialogViewModel( IRawMaterialEntryService entryService, IJeecgDictSyncService dictSyncService, IMixerMaterialService mixerMaterialService, IMixerMaterialTareStrategyService tareStrategyService, ISupplierService supplierService, IContainerExtension container, IRegionManager regionManager) : base(container, regionManager) { _entryService = entryService; _dictSyncService = dictSyncService; _mixerMaterialService = mixerMaterialService; _tareStrategyService = tareStrategyService; _supplierService = supplierService; SaveCommand = new DelegateCommand(async () => await SaveAsync()); CancelCommand = new DelegateCommand(() => CloseAction?.Invoke()); ResetCommand = new DelegateCommand(InitializeForAdd); AddSplitDetailCommand = new DelegateCommand(AddSplitDetailRow, () => !IsPrinted); RemoveSplitDetailCommand = new DelegateCommand(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); OpenWarehouseAreaPickerCommand = new DelegateCommand(async row => await OpenWarehouseAreaPickerAsync(row)); OpenTareStrategyPickerCommand = new DelegateCommand(async row => await OpenTareStrategyPickerAsync(row)); ResetTareStrategyCommand = new DelegateCommand(async row => await ResetTareStrategyAsync(row)); 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("未检", "0"), new KeyValuePair("合格", "1"), new KeyValuePair("不合格", "2"), }); PopulateOptions(TestStatusOptions, testStatusOpts, new[] { new KeyValuePair("送样", "0"), new KeyValuePair("已批准", "1"), }); PopulateOptions(PrintFlagOptions, printFlagOpts, new[] { new KeyValuePair("未打印", "0"), new KeyValuePair("已打印", "1"), }); PopulateOptions(StockBalanceOptions, ynOpts, new[] { new KeyValuePair("否", "0"), new KeyValuePair("是", "1"), }); PopulateOptions(IsSpecialAdoptionOptions, ynOpts, new[] { new KeyValuePair("否", "0"), new KeyValuePair("是", "1"), }); PopulateOptions(StatusOptions, statusOpts, Array.Empty>()); ApplyDefaultEntryStatusForAdd(); ApplyHiddenFieldDefaultsForAdd(); } catch { FillFallbackOptions(); ApplyDefaultEntryStatusForAdd(); ApplyHiddenFieldDefaultsForAdd(); } } private static void PopulateOptions( ObservableCollection> target, IEnumerable> items, IEnumerable> 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>(), new[] { new KeyValuePair("未检", "0"), new KeyValuePair("合格", "1"), new KeyValuePair("不合格", "2"), }); PopulateOptions(TestStatusOptions, Array.Empty>(), new[] { new KeyValuePair("送样", "0"), new KeyValuePair("已批准", "1"), }); PopulateOptions(PrintFlagOptions, Array.Empty>(), new[] { new KeyValuePair("未打印", "0"), new KeyValuePair("已打印", "1"), }); var ynDefault = new[] { new KeyValuePair("否", "0"), new KeyValuePair("是", "1") }; PopulateOptions(StockBalanceOptions, Array.Empty>(), ynDefault); PopulateOptions(IsSpecialAdoptionOptions, Array.Empty>(), 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(EntryTimeInput)); RaisePropertyChanged(nameof(PalletTareTotalDisplay)); RaisePropertyChanged(nameof(IsSpecialAdoptionValue)); RaisePropertyChanged(nameof(SplitTotalPortionsDisplay)); RaisePropertyChanged(nameof(SplitPortionWeightDisplay)); RaisePropertyChanged(nameof(SplitPortionPackagesDisplay)); RaisePropertyChanged(nameof(IsPrinted)); RaisePropertyChanged(nameof(IsTotalWeightEditable)); AddSplitDetailCommand.RaiseCanExecuteChanged(); } 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, PalletTareTotal = entry.PalletTareTotal, TotalPortions = entry.TotalPortions, PortionWeight = entry.PortionWeight, PortionPackagingTare = entry.PortionPackagingTare, PortionPalletWeight = entry.PortionPalletWeight, PortionTareStrategyIds = entry.PortionTareStrategyIds, PortionPackages = entry.PortionPackages, PortionWarehouseLocations = entry.PortionWarehouseLocations, PortionDetailIds = entry.PortionDetailIds, PortionCardFlags = entry.PortionCardFlags, 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(EntryTimeInput)); RaisePropertyChanged(nameof(PalletTareTotalDisplay)); RaisePropertyChanged(nameof(IsSpecialAdoptionValue)); RaisePropertyChanged(nameof(SplitTotalPortionsDisplay)); RaisePropertyChanged(nameof(SplitPortionWeightDisplay)); RaisePropertyChanged(nameof(SplitPortionPackagesDisplay)); RaisePropertyChanged(nameof(IsPrinted)); RaisePropertyChanged(nameof(IsTotalWeightEditable)); AddSplitDetailCommand.RaiseCanExecuteChanged(); } /// 校验并提交新增/编辑;不弹关闭、不执行独立页的 InitializeForAdd。成功时 Result=true。 protected async Task PersistEntryCoreAsync() { if (Entry == null) return false; ApplySplitDetailsToEntry(); ApplyDefaultEntryStatusForAdd(); ApplyHiddenFieldDefaultsForAdd(); var missing = new List(); if (string.IsNullOrWhiteSpace(Entry.MaterialId)) missing.Add("密炼物料"); if (string.IsNullOrWhiteSpace(Entry.BillNo)) missing.Add("榜单号"); if (string.IsNullOrWhiteSpace(Entry.UnloadOperator)) missing.Add("卸货人"); if (string.IsNullOrWhiteSpace(Entry.SupplierName)) missing.Add("供应商名称"); if (missing.Count > 0) { HandyControl.Controls.MessageBox.Warning($"以下必填项不能为空:{string.Join("、", missing)}"); return false; } bool ok; if (IsAddMode) { ok = await _entryService.AddAsync(Entry); if (!ok) { HandyControl.Controls.MessageBox.Error("新增失败!"); return false; } } else { ok = await _entryService.EditAsync(Entry); if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return false; } } Result = ok; return true; } protected virtual async Task SaveAsync() { if (Entry == null) return; try { if (!await PersistEntryCoreAsync()) return; if (IsAddMode) { // 非模态轻提示,约 1 秒后自动消失(与独立页「保存并打印」时的体验一致) Growl.Success(new GrowlInfo { Message = "保存成功", ShowDateTime = false, WaitTime = 1 }); if (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 SuspendEmbeddedPrintPreviewAirspaceWhileAsync(() => HandyControl.Controls.Dialog.Show() .Initialize(vm => { pickerVm = vm; vm.Initialize(Entry?.BillNo); }) .GetResultAsync()); } 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; await ApplySupplierFromDisplayNameAsync(selected.SenderUnit); // 选择榜单后,自动把「剩余可入场量 = 净重 - 已入场重量」带入「总重」, // 用户仍可手动编辑;若净重为空则保留原值,避免误清空。 if (selected.NetWeight.HasValue) { var entered = selected.EnteredWeight ?? 0d; var remaining = selected.NetWeight.Value - entered; // 已入场重量可能因数据异常超过净重;UI 不展示负数,强制夹到 0。 Entry.TotalWeight = Math.Round(Math.Max(0d, remaining), 2); } RaisePropertyChanged(nameof(Entry)); RaisePropertyChanged(nameof(TotalWeightInput)); RaisePropertyChanged(nameof(EntryTimeInput)); RaisePropertyChanged(nameof(PalletTareTotalDisplay)); await RefreshAllRowsTareStrategyAsync(); } /// /// 按供应商全称/简称反查 ID(榜单「发货单位」与手动填写的供应商名称均可用)。 /// private async Task ApplySupplierFromDisplayNameAsync(string? displayName) { if (Entry == null || string.IsNullOrWhiteSpace(displayName)) { return; } var supplier = await FindSupplierByDisplayNameAsync(displayName); if (supplier == null) { return; } Entry.SupplierId = supplier.Id; Entry.SupplierName = supplier.SupplierName ?? supplier.SupplierShortName ?? displayName.Trim(); RaisePropertyChanged(nameof(Entry)); } private async Task FindSupplierByDisplayNameAsync(string? displayName) { if (string.IsNullOrWhiteSpace(displayName)) { return null; } var name = displayName.Trim(); try { var page = await _supplierService.PageAsync(1, 5000); var exact = page.Records.FirstOrDefault(s => string.Equals(s.SupplierName?.Trim(), name, StringComparison.OrdinalIgnoreCase) || string.Equals(s.SupplierShortName?.Trim(), name, StringComparison.OrdinalIgnoreCase)); if (exact != null) { return exact; } return page.Records.FirstOrDefault(s => (s.SupplierName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase) || (s.SupplierShortName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase) || name.Contains(s.SupplierShortName ?? "", StringComparison.OrdinalIgnoreCase) || name.Contains(s.SupplierName ?? "", StringComparison.OrdinalIgnoreCase)); } catch { return null; } } /// /// 皮重策略匹配前确保 SupplierId 已解析(榜单仅带出名称时补全 ID)。 /// private async Task EnsureSupplierIdForTareMatchAsync() { if (Entry == null || !string.IsNullOrWhiteSpace(Entry.SupplierId)) { return; } await ApplySupplierFromDisplayNameAsync(Entry.SupplierName); } /// /// 弹出「库区选择」弹窗,把选中的 AreaName 写回当前明细行的 WarehouseLocation。 /// 入参 row:被点击的拆码明细行;为 null 直接 return(防御性)。 /// private async Task OpenWarehouseAreaPickerAsync(RawMaterialSplitDetailItem? row) { if (row == null) { return; } WarehouseAreaPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(() => HandyControl.Controls.Dialog.Show() .Initialize(vm => { pickerVm = vm; vm.Initialize(row.WarehouseLocation); }) .GetResultAsync()); } catch (Exception ex) { // 保留 Debug 输出以便排查,但不阻断流程 System.Diagnostics.Debug.WriteLine($"[库位选择] 弹窗异常: {ex}"); return; } if (!confirmed || pickerVm?.SelectedRecord == null) { return; } // 仅回填库区名称(与后端 warehouse_location 字段保持单字符串语义)。 // 如未来需要严格关联库区 ID,可在 RawMaterialSplitDetailItem 新增 WarehouseAreaId 一并保存。 row.WarehouseLocation = pickerVm.SelectedRecord.AreaName; } private async Task OpenMaterialPickerAsync() { RawMaterialEntryMaterialPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(() => HandyControl.Controls.Dialog.Show() .Initialize(vm => { pickerVm = vm; vm.Initialize(Entry?.MaterialCode, Entry?.MaterialName); }) .GetResultAsync()); } 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)); _ = RefreshAllRowsTareStrategyAsync(); } private async Task OpenSupplierPickerAsync() { SupplierPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(() => HandyControl.Controls.Dialog.Show() .Initialize(vm => pickerVm = vm) .GetResultAsync()); } 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)); await RefreshAllRowsTareStrategyAsync(); } private void ClearSupplierSelection() { if (Entry == null) return; Entry.SupplierId = null; Entry.SupplierName = null; RaisePropertyChanged(nameof(Entry)); _ = RefreshAllRowsTareStrategyAsync(); } private async Task RefreshAllRowsTareStrategyAsync() { if (_suppressTareStrategyRefresh) return; await EnsureSupplierIdForTareMatchAsync(); foreach (var row in SplitCodeDetails.ToList()) { if (!row.IsManualTareStrategy) await ApplyAutoTareStrategyToRowAsync(row); } } private async Task ApplyAutoTareStrategyToRowAsync(RawMaterialSplitDetailItem row) { if (_suppressTareStrategyRefresh || row.IsManualTareStrategy || row.HasCard) return; try { var strategies = await _tareStrategyService.GetAllForMatchAsync(); var match = MixerMaterialTareStrategyMatcher.PickBestMatch( strategies, Entry?.MaterialId, Entry?.SupplierId, Entry?.EntryTime, row.PortionWeight); ApplyTareStrategyToRow(row, match, manual: false); } catch { ApplyTareStrategyToRow(row, null, manual: false); } RaisePalletTareTotalChanged(); } private void ApplyTareStrategyToRow( RawMaterialSplitDetailItem row, MesXslMixerMaterialTareStrategy? strategy, bool manual) { if (strategy == null) { row.PackagingTare = 0d; row.PalletWeight = 0d; row.TareStrategyId = null; row.TareStrategyDisplay = "未匹配(0)"; } else { row.PackagingTare = strategy.TareWeight.HasValue ? (double)strategy.TareWeight.Value : 0d; row.PalletWeight = strategy.PalletWeight.HasValue ? (double)strategy.PalletWeight.Value : 0d; row.TareStrategyId = strategy.Id; row.TareStrategyDisplay = BuildTareStrategyDisplay(strategy); } row.IsManualTareStrategy = manual && strategy != null; } private static string BuildTareStrategyDisplay(MesXslMixerMaterialTareStrategy strategy) { var spec = string.IsNullOrWhiteSpace(strategy.MaterialSpec) ? "-" : strategy.MaterialSpec; var pkg = strategy.TareWeight?.ToString("0.##") ?? "0"; var pallet = strategy.PalletWeight?.ToString("0.##") ?? "0"; return $"规格:{spec} 包装:{pkg} 托盘:{pallet}"; } private async Task OpenTareStrategyPickerAsync(RawMaterialSplitDetailItem? row) { if (row == null || row.HasCard) return; if (string.IsNullOrWhiteSpace(Entry?.MaterialId)) { HandyControl.Controls.MessageBox.Warning("请先选择密炼物料!"); return; } await EnsureSupplierIdForTareMatchAsync(); if (string.IsNullOrWhiteSpace(Entry?.SupplierId)) { HandyControl.Controls.MessageBox.Warning("未能匹配到供应商档案,请手动选择供应商,或检查榜单发货单位是否与供应商简称/全称一致!"); return; } TareStrategyPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(async () => { return await HandyControl.Controls.Dialog.Show() .Initialize(vm => { pickerVm = vm; vm.Initialize(Entry!.MaterialId, Entry.SupplierId, Entry.EntryTime, row.PortionWeight, row.TareStrategyId); }) .GetResultAsync(); }); } catch { return; } if (!confirmed) return; if (pickerVm?.SelectedStrategy == null) { row.IsManualTareStrategy = false; await ApplyAutoTareStrategyToRowAsync(row); return; } ApplyTareStrategyToRow(row, pickerVm.SelectedStrategy, manual: true); RaisePalletTareTotalChanged(); } private async Task ResetTareStrategyAsync(RawMaterialSplitDetailItem? row) { if (row == null || row.HasCard) return; row.IsManualTareStrategy = false; await ApplyAutoTareStrategyToRowAsync(row); } private void RaisePalletTareTotalChanged() => RaisePropertyChanged(nameof(PalletTareTotalDisplay)); 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"; } /// /// 新增模式下为已在前端隐藏的字段补充默认值,确保保存到后端时不为空: /// 检测结果=未检、打印标记=未打印、入库结存=否。 /// 字典就绪时取字典 code,未就绪时回退到约定的常用 code。 /// protected void ApplyHiddenFieldDefaultsForAdd() { if (!IsAddMode || Entry == null) { return; } if (string.IsNullOrWhiteSpace(Entry.TestResult)) { Entry.TestResult = ResolveDefaultOptionValue(TestResultOptions, "未检", "0"); } if (string.IsNullOrWhiteSpace(Entry.PrintFlag)) { Entry.PrintFlag = ResolveDefaultOptionValue(PrintFlagOptions, "未打印", "0"); } if (string.IsNullOrWhiteSpace(Entry.StockBalance)) { Entry.StockBalance = ResolveDefaultOptionValue(StockBalanceOptions, "否", "0"); } RaisePropertyChanged(nameof(Entry)); } /// 在字典选项中按 Key(显示标签)查找对应 Value(实际 code),找不到则回退到 fallback。 private static string ResolveDefaultOptionValue( ObservableCollection> 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() { _suppressTareStrategyRefresh = true; try { SplitCodeDetails.Clear(); var portionsArr = SplitJoinedValues(Entry?.TotalPortions); var weightArr = SplitJoinedValues(Entry?.PortionWeight); var packagingTareArr = SplitJoinedValues(Entry?.PortionPackagingTare); var palletWeightArr = SplitJoinedValues(Entry?.PortionPalletWeight); var strategyIdsArr = SplitJoinedValues(Entry?.PortionTareStrategyIds); var packagesArr = SplitJoinedValues(Entry?.PortionPackages); var locationsArr = SplitJoinedValues(Entry?.PortionWarehouseLocations); var idsArr = SplitJoinedValues(Entry?.PortionDetailIds); var cardFlagsArr = SplitJoinedValues(Entry?.PortionCardFlags); var fallbackToPrintFlag = cardFlagsArr.Length == 0 && string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal); var rowCount = Math.Max(1, Math.Max(Math.Max(portionsArr.Length, weightArr.Length), Math.Max(Math.Max(packagesArr.Length, locationsArr.Length), idsArr.Length))); for (var i = 0; i < rowCount; i++) { var locationFromArr = GetAt(locationsArr, i); var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr)) ? Entry?.WarehouseLocation : locationFromArr; var strategyId = GetAt(strategyIdsArr, i); var item = new RawMaterialSplitDetailItem { Portions = TryParseInt(GetAt(portionsArr, i)), PortionWeight = TryParseDouble(GetAt(weightArr, i)), PortionPackages = TryParseInt(GetAt(packagesArr, i)), WarehouseLocation = locationFallback, PackagingTare = TryParseDouble(GetAt(packagingTareArr, i)) ?? 0d, PalletWeight = TryParseDouble(GetAt(palletWeightArr, i)) ?? 0d, TareStrategyId = strategyId, IsManualTareStrategy = !string.IsNullOrWhiteSpace(strategyId), }; item.TareStrategyDisplay = string.IsNullOrWhiteSpace(strategyId) ? "未匹配(0)" : $"策略ID:{strategyId}"; var existingId = GetAt(idsArr, i); if (!string.IsNullOrWhiteSpace(existingId)) item.Id = existingId; var flagAt = GetAt(cardFlagsArr, i); if (!string.IsNullOrWhiteSpace(flagAt)) item.HasCard = string.Equals(flagAt, "1", StringComparison.Ordinal); else if (fallbackToPrintFlag && !string.IsNullOrWhiteSpace(existingId)) item.HasCard = true; SplitCodeDetails.Add(item); } RaisePropertyChanged(nameof(SplitCodeTableHeight)); RaisePalletTareTotalChanged(); } finally { _suppressTareStrategyRefresh = false; } } private static string[] SplitJoinedValues(string? value) { if (string.IsNullOrWhiteSpace(value)) return Array.Empty(); 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() { var row = new RawMaterialSplitDetailItem(); SplitCodeDetails.Add(row); RaisePropertyChanged(nameof(SplitCodeTableHeight)); _ = ApplyAutoTareStrategyToRowAsync(row); } 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()) { oldItem.PropertyChanged -= OnSplitDetailItemPropertyChanged; } } if (e.NewItems != null) { foreach (var newItem in e.NewItems.OfType()) { newItem.PropertyChanged += OnSplitDetailItemPropertyChanged; } } RaiseSplitDisplayPropertyChanged(); RaisePropertyChanged(nameof(SplitCodeTableHeight)); } private void OnSplitDetailItemPropertyChanged(object? sender, PropertyChangedEventArgs e) { // 用户修改某行的「份数」时:仅当该行「每份重量」为空才按公式自动计算 if (e.PropertyName == nameof(RawMaterialSplitDetailItem.Portions) && sender is RawMaterialSplitDetailItem row) { RecalculatePortionWeightForRow(row); } if (e.PropertyName == nameof(RawMaterialSplitDetailItem.PortionWeight) && sender is RawMaterialSplitDetailItem weightRow) { // 清空每份重量后,若份数已填则立即按公式重算(配合 PortionWeightText 空值绑定) if (!weightRow.PortionWeight.HasValue) { RecalculatePortionWeightForRow(weightRow); } if (!weightRow.IsManualTareStrategy && !weightRow.HasCard) { _ = ApplyAutoTareStrategyToRowAsync(weightRow); } } if (e.PropertyName is nameof(RawMaterialSplitDetailItem.Portions) or nameof(RawMaterialSplitDetailItem.PortionWeight) or nameof(RawMaterialSplitDetailItem.PortionPackages) or nameof(RawMaterialSplitDetailItem.PackagingTare) or nameof(RawMaterialSplitDetailItem.PalletWeight)) { RaiseSplitDisplayPropertyChanged(); } } /// /// 拆码明细某行的「份数」变化时,仅当该行「每份重量」为空时按公式重算: /// 公式:每份重量 = (总重 - 其他行 Σ份数×每份重量) / 当前行份数 /// — 已填写「每份重量」后再改份数,不覆盖用户输入; /// — 总重为空 / ≤0、当前份数 ≤0、剩余总重 ≤0 时跳过; /// — 结果四舍五入到两位小数。 /// private void RecalculatePortionWeightForRow(RawMaterialSplitDetailItem row) { if (row.PortionWeight.HasValue) { return; } if (Entry?.TotalWeight is not { } total || total <= 0d) { return; } if (row.Portions is not { } portions || portions <= 0) { return; } var otherSum = 0d; foreach (var other in SplitCodeDetails) { if (ReferenceEquals(other, row)) continue; if (other.Portions is not { } op || other.PortionWeight is not { } ow) continue; otherSum += op * ow; } var remaining = total - otherSum; if (remaining <= 0d) { return; } row.PortionWeight = Math.Round(remaining / portions, 2); } private void RaiseSplitDisplayPropertyChanged() { RaisePropertyChanged(nameof(SplitTotalPortionsDisplay)); RaisePropertyChanged(nameof(SplitPortionWeightDisplay)); RaisePropertyChanged(nameof(SplitPortionPackagesDisplay)); RaisePalletTareTotalChanged(); } private string JoinSplitValue(Func 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); } /// /// 把 SplitCodeDetails 全部明细行的「份数 / 每份重量 / 每份包数 / 库位 / 行 ID」按 "x/y/z/" 拼接后持久化到 Entry。 /// 明细行的「库位」属于行级数据(用于后续生成原材料卡片时区分库位),与 Entry.WarehouseLocation /// (基础资料整票级单值)独立保存,通过 portion_warehouse_locations 字段持久化。 /// 子类(如 OperationViewModel 的「重新拆码」)需要直接调用,因此声明为 protected。 /// protected 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.PortionPackagingTare = JoinSplitValue(it => FormatNullableDecimal(it.PackagingTare), true); Entry.PortionPalletWeight = JoinSplitValue(it => FormatNullableDecimal(it.PalletWeight), true); Entry.PortionTareStrategyIds = JoinSplitValue( it => it.IsManualTareStrategy && !string.IsNullOrWhiteSpace(it.TareStrategyId) ? it.TareStrategyId : null, true); Entry.PortionPackages = JoinSplitValue(it => it.PortionPackages?.ToString(CultureInfo.InvariantCulture), true); Entry.PortionWarehouseLocations = JoinSplitValue(it => string.IsNullOrWhiteSpace(it.WarehouseLocation) ? null : it.WarehouseLocation.Trim(), true); // 持久化每行 GUID,行序与上方四个字段对齐;JoinSplitValue 会过滤空,但 Id 在构造时即生成不会为空。 Entry.PortionDetailIds = JoinSplitValue(it => it.Id, true); // 持久化每行的「已生成卡片」标志(1=已生成 0=未生成), // 用于解决「保存后新增行被误判为已打印」的问题: // 「生成原材料卡片」只对 HasCard=false 的行加卡 → 必须把真实的 HasCard 持久化, // 否则重新加载后无法区分「已生成」与「保存后新增的未生成行」。 Entry.PortionCardFlags = JoinSplitValue(it => it.HasCard ? "1" : "0", true); Entry.PalletTareTotal = PalletTareTotalDisplay; } 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 { /// /// 拆码明细行 ID(GUID,构造时生成)。前端隐藏; /// 「生成原材料卡片」时由桌面端写入对应 MesXslRawMaterialCard.SplitDetailId; /// 「重新拆码」时按入场记录的 PortionDetailIds 反查批删关联卡片。 /// 编辑回填时若 portion_detail_ids 已有该位置的值,会覆盖默认值,确保跨次保持稳定。 /// public string Id { get; set; } = Guid.NewGuid().ToString("N"); private int? _portions; public int? Portions { get => _portions; set => SetProperty(ref _portions, value); } private double? _portionWeight; public double? PortionWeight { get => _portionWeight; set { if (SetProperty(ref _portionWeight, value)) { RaisePropertyChanged(nameof(PortionWeightText)); } } } /// /// 每份重量文本(供 TextBox 绑定;空字符串表示未填写,避免 double? 直接绑定无法清空)。 /// public string PortionWeightText { get => _portionWeight.HasValue ? _portionWeight.Value.ToString("0.##", CultureInfo.InvariantCulture) : string.Empty; set { var text = value?.Trim() ?? string.Empty; if (string.IsNullOrEmpty(text)) { PortionWeight = null; return; } if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) { PortionWeight = parsed; } } } 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); } private double? _packagingTare; public double? PackagingTare { get => _packagingTare; set => SetProperty(ref _packagingTare, value); } private double? _palletWeight; public double? PalletWeight { get => _palletWeight; set => SetProperty(ref _palletWeight, value); } private string? _tareStrategyId; public string? TareStrategyId { get => _tareStrategyId; set => SetProperty(ref _tareStrategyId, value); } private string? _tareStrategyDisplay = "未匹配(0)"; public string? TareStrategyDisplay { get => _tareStrategyDisplay; set => SetProperty(ref _tareStrategyDisplay, value); } private bool _isManualTareStrategy; /// 用户手动选择皮重策略后为 true,自动匹配不再覆盖。 public bool IsManualTareStrategy { get => _isManualTareStrategy; set => SetProperty(ref _isManualTareStrategy, value); } /// /// 该行是否已生成原材料卡片(运行时状态,不直接持久化)。 /// 加载时根据 Entry.PrintFlag + Id 是否来自持久化的 PortionDetailIds 推断; /// 「生成原材料卡片」成功后由 VM 置为 true;「重新拆码」清空集合时随之失效。 /// true → 行内输入项 / 删除 全部锁定,避免与已生成卡片数据脱节; /// false → 该行参与下次「生成原材料卡片」(继续拆码 + 续号生成)。 /// private bool _hasCard; public bool HasCard { get => _hasCard; set => SetProperty(ref _hasCard, value); } }