using HandyControl.Controls; using HandyControl.Tools.Extension; using System.Collections.ObjectModel; using System.IO; using System.Text.Json; using System.Windows.Threading; using YY.Admin.Core; using YY.Admin.Core.Entity; using YY.Admin.Core.Services; using YY.Admin.Services.Service; using YY.Admin.Views.WeightRecord; namespace YY.Admin.ViewModels.WeightRecord; /// /// 地磅称重操作页面 ViewModel /// 左侧:串口模拟实时数据(重量 + 车牌);右侧:称重信息录入表单。 /// public class WeightRecordOperationViewModel : BaseViewModel { private readonly IWeightRecordService _weightRecordService; private readonly IJeecgDictSyncService _dictSyncService; private readonly IVehicleService _vehicleService; private readonly IMixerMaterialService _mixerMaterialService; private readonly string _unitCacheFilePath; // ─── 车辆档案匹配 ─── private MesXslVehicle? _matchedVehicle; private string? _lastLookedUpPlate; private string _vehicleLookupStatus = "None"; // None | Searching | Matched | NotFound public string VehicleLookupStatus { get => _vehicleLookupStatus; set { SetProperty(ref _vehicleLookupStatus, value); RaisePropertyChanged(nameof(VehicleMatchText)); RaisePropertyChanged(nameof(ShowVehicleMatchHint)); } } public bool ShowVehicleMatchHint => _vehicleLookupStatus != "None"; public string VehicleMatchText => _vehicleLookupStatus switch { "Searching" => "正在匹配车辆档案...", "Matched" => $"✓ 已匹配车辆档案({_matchedVehicle?.VehicleBelongText})", "NotFound" => "未在档案库中找到,保存后将自动录入", _ => string.Empty }; // ─── 串口模拟定时器 ─── private readonly DispatcherTimer _serialTimer; private readonly Random _rnd = new(); private double _baseWeight = 35000.0; // 基准重量(模拟车辆在磅上) private int _stableCountdown = 0; // 稳定倒计时(连续稳定帧数) private bool _vehiclePresent = false; // 是否有车辆在磅上 private int _vehicleDetectCooldown = 0; // 车辆检测冷却帧 private bool _isApplyingSelectedRecord; // ─── 实时数据属性 ─── private double _currentWeight; public double CurrentWeight { get => _currentWeight; set { SetProperty(ref _currentWeight, value); RaisePropertyChanged(nameof(CurrentWeightDisplay)); } } public string CurrentWeightDisplay => $"{CurrentWeight:N2}"; private bool _isWeightStable; public bool IsWeightStable { get => _isWeightStable; set => SetProperty(ref _isWeightStable, value); } private string _serialStatusText = "正在连接..."; public string SerialStatusText { get => _serialStatusText; set => SetProperty(ref _serialStatusText, value); } private bool _isSerialConnected; public bool IsSerialConnected { get => _isSerialConnected; set => SetProperty(ref _isSerialConnected, value); } private string _detectedPlate = string.Empty; public string DetectedPlate { get => _detectedPlate; set => SetProperty(ref _detectedPlate, value); } private bool _hasPlate; public bool HasPlate { get => _hasPlate; set => SetProperty(ref _hasPlate, value); } public ObservableCollection OperationLogs { get; } = new(); // ─── 采集状态 ─── private bool _grossWeightCaptured; public bool GrossWeightCaptured { get => _grossWeightCaptured; set => SetProperty(ref _grossWeightCaptured, value); } private bool _tareWeightCaptured; public bool TareWeightCaptured { get => _tareWeightCaptured; set => SetProperty(ref _tareWeightCaptured, value); } /// /// 手动输入毛重/皮重(仅用于测试联调,正式过磅请关闭)。 /// private bool _isManualWeightEntry; public bool IsManualWeightEntry { get => _isManualWeightEntry; set { if (!SetProperty(ref _isManualWeightEntry, value)) return; CaptureGrossWeightCommand.RaiseCanExecuteChanged(); CaptureTareWeightCommand.RaiseCanExecuteChanged(); } } // ─── 表单绑定属性 ─── private DateTime _weighDate = DateTime.Today; public DateTime WeighDate { get => _weighDate; set => SetProperty(ref _weighDate, value); } private string? _inoutDirection = "1"; public string? InoutDirection { get => _inoutDirection; set { if (!SetProperty(ref _inoutDirection, value)) return; RaisePropertyChanged(nameof(CaptureGrossButtonText)); RaisePropertyChanged(nameof(CaptureTareButtonText)); CaptureGrossWeightCommand.RaiseCanExecuteChanged(); CaptureTareWeightCommand.RaiseCanExecuteChanged(); if (_isApplyingSelectedRecord) return; if (SelectedRecentWeightRecord != null) { ClearFormByDirectionSwitch(); } TryApplyCachedUnitByDirection(value); _ = LoadRecentWeightRecordsAsync(PlateNumber); } } public string CaptureGrossButtonText => "采集毛重"; public string CaptureTareButtonText => "采集皮重"; private string? _plateNumber; public string? PlateNumber { get => _plateNumber; set { if (!SetProperty(ref _plateNumber, value)) return; if (_isApplyingSelectedRecord) return; _ = LoadRecentWeightRecordsAsync(value); var trimmed = value?.Trim() ?? string.Empty; if (trimmed.Length >= 6) _ = LookupVehicleByPlateAsync(trimmed); else ClearVehicleMatch(); } } // ─── 发货/收货单位(含选择器状态) ─── private MesXslSupplier? _selectedSupplier; private MesXslCustomer? _selectedCustomer; private string? _cachedInboundReceiverUnit; private string? _cachedOutboundSenderUnit; public bool HasSelectedSupplier => _selectedSupplier != null; public bool HasSelectedCustomer => _selectedCustomer != null; public string SenderUnitDisplay => _selectedSupplier != null ? $"[{_selectedSupplier.SupplierCode}] {_selectedSupplier.SupplierName}" : (!string.IsNullOrWhiteSpace(_senderUnit) ? _senderUnit : "点击右侧「选择」从供应商列表中选取"); public string ReceiverUnitDisplay => _selectedCustomer != null ? $"[{_selectedCustomer.CustomerCode}] {_selectedCustomer.CustomerName}" : (!string.IsNullOrWhiteSpace(_receiverUnit) ? _receiverUnit : "点击右侧「选择」从客户列表中选取"); private string? _senderUnit; public string? SenderUnit { get => _senderUnit; set { SetProperty(ref _senderUnit, value); RaisePropertyChanged(nameof(SenderUnitDisplay)); } } private string? _receiverUnit; public string? ReceiverUnit { get => _receiverUnit; set { SetProperty(ref _receiverUnit, value); RaisePropertyChanged(nameof(ReceiverUnitDisplay)); } } // ─── 密炼物料选择 ─── private string? _mixerMaterialIds; private string? _mixerMaterialNames; private bool _isMixerMaterialManual; public bool IsMixerMaterialManual { get => _isMixerMaterialManual; set { if (!SetProperty(ref _isMixerMaterialManual, value)) return; // 切换为手动填写时,清空 ID,避免保存时出现“名称手填 + ID遗留”不一致 if (value) { _mixerMaterialIds = null; RaisePropertyChanged(nameof(HasSelectedMixerMaterials)); } RaisePropertyChanged(nameof(IsMixerMaterialReadOnly)); OpenMixerMaterialPickerCommand.RaiseCanExecuteChanged(); } } public bool IsMixerMaterialReadOnly => !IsMixerMaterialManual; public string? MixerMaterialNames { get => _mixerMaterialNames; set { if (!SetProperty(ref _mixerMaterialNames, value)) return; // 手动填写时,名称由人工输入,ID 不再有效 if (IsMixerMaterialManual) { _mixerMaterialIds = null; } RaisePropertyChanged(nameof(MixerMaterialDisplay)); RaisePropertyChanged(nameof(HasSelectedMixerMaterials)); } } public string MixerMaterialDisplay => !string.IsNullOrWhiteSpace(_mixerMaterialNames) ? _mixerMaterialNames : "点击右侧「选择」选取密炼物料(可多选)"; public bool HasSelectedMixerMaterials => !string.IsNullOrWhiteSpace(_mixerMaterialNames); private double? _grossWeight; public double? GrossWeight { get => _grossWeight; set { if (!SetProperty(ref _grossWeight, value)) return; // 测试用手动录入:有有效数值即视为已填写(用于「已采集」提示与保存) if (IsManualWeightEntry) GrossWeightCaptured = value.HasValue && value.Value > 0; RecalcNetWeight(); } } private double? _tareWeight; public double? TareWeight { get => _tareWeight; set { if (!SetProperty(ref _tareWeight, value)) return; if (IsManualWeightEntry) TareWeightCaptured = value.HasValue && value.Value > 0; RecalcNetWeight(); } } private double? _netWeight; public double? NetWeight { get => _netWeight; set => SetProperty(ref _netWeight, value); } public string NetWeightDisplay => NetWeight.HasValue ? $"{NetWeight.Value:N2}" : "—"; private string? _driverName; public string? DriverName { get => _driverName; set => SetProperty(ref _driverName, value); } private string? _driverPhone; public string? DriverPhone { get => _driverPhone; set => SetProperty(ref _driverPhone, value); } // ─── 字典 ─── public ObservableCollection> InoutDirectionOptions { get; } = new(); public ObservableCollection RecentWeightRecords { get; } = new(); private bool _isPlateNumberLocked; public bool IsPlateNumberLocked { get => _isPlateNumberLocked; set => SetProperty(ref _isPlateNumberLocked, value); } private WeightRecordSimpleItem? _selectedRecentWeightRecord; public WeightRecordSimpleItem? SelectedRecentWeightRecord { get => _selectedRecentWeightRecord; set { if (!SetProperty(ref _selectedRecentWeightRecord, value)) return; IsPlateNumberLocked = value?.Source?.Id != null; if (value == null) return; ApplySelectedRecordToForm(value); } } // ─── 命令 ─── public DelegateCommand CaptureGrossWeightCommand { get; } public DelegateCommand CaptureTareWeightCommand { get; } public DelegateCommand SaveCommand { get; } public DelegateCommand ClearCommand { get; } public DelegateCommand UseDetectedPlateCommand { get; } public DelegateCommand OpenSupplierPickerCommand { get; } public DelegateCommand OpenCustomerPickerCommand { get; } public DelegateCommand OpenMixerMaterialPickerCommand { get; } public DelegateCommand ClearSupplierCommand { get; } public DelegateCommand ClearCustomerCommand { get; } public DelegateCommand ClearMixerMaterialCommand { get; } public WeightRecordOperationViewModel( IWeightRecordService weightRecordService, IJeecgDictSyncService dictSyncService, IVehicleService vehicleService, IMixerMaterialService mixerMaterialService, IContainerExtension container, IRegionManager regionManager) : base(container, regionManager) { _weightRecordService = weightRecordService; _dictSyncService = dictSyncService; _vehicleService = vehicleService; _mixerMaterialService = mixerMaterialService; var cacheDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "YY.Admin", "cache"); Directory.CreateDirectory(cacheDir); _unitCacheFilePath = Path.Combine(cacheDir, "weight-record-unit-cache.json"); LoadUnitCache(); CaptureGrossWeightCommand = new DelegateCommand(CaptureGrossWeight, CanCaptureGrossWeight) .ObservesProperty(() => IsWeightStable) .ObservesProperty(() => IsManualWeightEntry) .ObservesProperty(() => GrossWeightCaptured) .ObservesProperty(() => TareWeightCaptured); CaptureTareWeightCommand = new DelegateCommand(CaptureTareWeight, CanCaptureTareWeight) .ObservesProperty(() => IsWeightStable) .ObservesProperty(() => IsManualWeightEntry) .ObservesProperty(() => GrossWeightCaptured) .ObservesProperty(() => TareWeightCaptured); SaveCommand = new DelegateCommand(async () => await SaveAsync()); ClearCommand = new DelegateCommand(ClearForm); UseDetectedPlateCommand = new DelegateCommand(() => PlateNumber = DetectedPlate, () => HasPlate) .ObservesProperty(() => HasPlate); OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync()); OpenCustomerPickerCommand = new DelegateCommand(async () => await OpenCustomerPickerAsync()); OpenMixerMaterialPickerCommand = new DelegateCommand(async () => await OpenMixerMaterialPickerAsync(), () => !IsMixerMaterialManual) .ObservesProperty(() => IsMixerMaterialManual); ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection); ClearCustomerCommand = new DelegateCommand(ClearCustomerSelection); ClearMixerMaterialCommand = new DelegateCommand(ClearMixerMaterialSelection); _ = LoadDictOptionsAsync(); _ = LoadRecentWeightRecordsAsync(PlateNumber); TryApplyCachedUnitByDirection(InoutDirection); // 启动串口模拟定时器(200ms 刷新一次,约 5Hz) _serialTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) }; _serialTimer.Tick += OnSerialTimerTick; _serialTimer.Start(); // 模拟1秒后串口连接成功 var connectTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; connectTimer.Tick += (s, e) => { ((DispatcherTimer)s!).Stop(); IsSerialConnected = true; SerialStatusText = "COM3 9600bps 已连接"; AddLog("串口连接成功 COM3"); // 3秒后模拟第一辆车到来 var vehicleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) }; vehicleTimer.Tick += (s2, e2) => { ((DispatcherTimer)s2!).Stop(); SimulateVehicleArrival(); }; vehicleTimer.Start(); }; connectTimer.Start(); } private void OnSerialTimerTick(object? sender, EventArgs e) { if (!IsSerialConnected) return; if (_vehiclePresent) { // 车辆在磅上:在基准重量附近波动 var noise = (_rnd.NextDouble() - 0.5) * 80; CurrentWeight = Math.Max(0, _baseWeight + noise); // 稳定检测:连续10帧波动小于30kg视为稳定 if (Math.Abs(noise) < 30) _stableCountdown = Math.Min(_stableCountdown + 1, 10); else _stableCountdown = Math.Max(_stableCountdown - 2, 0); IsWeightStable = _stableCountdown >= 8; } else { // 无车辆:重量归零并稳定 CurrentWeight = Math.Max(0, CurrentWeight - 500); if (CurrentWeight < 50) { CurrentWeight = 0; IsWeightStable = true; } else IsWeightStable = false; } // 车辆检测冷却计数 if (_vehicleDetectCooldown > 0) _vehicleDetectCooldown--; } private void SimulateVehicleArrival() { _vehiclePresent = true; _stableCountdown = 0; // 同一张单第二次上磅:模拟重量与已采值协调(先毛后皮 → 皮重小于毛重;先皮后毛 → 毛重大于皮重) PickBaseWeightForCurrentWeighingScenario(); AddLog("检测到车辆入场"); // 模拟摄像头识别车牌(2秒后回传) var plateTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; plateTimer.Tick += (s, e) => { ((DispatcherTimer)s!).Stop(); var plates = new[] { "粤A88888", "粤B12345", "粤C98765", "湘A66666", "川B55555" }; DetectedPlate = plates[_rnd.Next(plates.Length)]; HasPlate = true; AddLog($"车牌识别:{DetectedPlate}"); }; plateTimer.Start(); // 模拟15秒后车辆离开(可手动操作) var leaveTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; leaveTimer.Tick += (s, e) => { ((DispatcherTimer)s!).Stop(); if (_vehiclePresent) SimulateVehicleLeave(); }; leaveTimer.Start(); } /// /// 按当前单据已填毛重/皮重生成模拟磅台基准重量,保证净重合理。 /// private void PickBaseWeightForCurrentWeighingScenario() { if (HasEffectiveWeighValue(GrossWeight) && !HasEffectiveWeighValue(TareWeight)) { var g = GrossWeight!.Value; // 皮重需明显小于毛重,留出净重空间 var maxTare = Math.Max(0, g - 30); var minTare = Math.Min(maxTare * 0.22, maxTare - 80); minTare = Math.Max(0, minTare); if (maxTare - minTare < 25) minTare = Math.Max(0, maxTare * 0.45); if (maxTare - minTare < 1) maxTare = Math.Max(minTare + 1, g - 5); _baseWeight = minTare + _rnd.NextDouble() * Math.Max(1e-6, maxTare - minTare); } else if (HasEffectiveWeighValue(TareWeight) && !HasEffectiveWeighValue(GrossWeight)) { var t = TareWeight!.Value; var lower = t + Math.Max(100, t * 0.05); var span = 8000 + _rnd.NextDouble() * 12000; var upper = Math.Min(65000, lower + span); if (upper - lower < 500) upper = lower + 500; _baseWeight = lower + _rnd.NextDouble() * (upper - lower); } else { _baseWeight = _rnd.Next(18000, 65000); // 18~65 吨随机(首次过磅或两称已齐) } } private void SimulateVehicleLeave() { _vehiclePresent = false; _stableCountdown = 0; HasPlate = false; AddLog("车辆离场"); // 8秒后下一辆车 if (_vehicleDetectCooldown <= 0) { _vehicleDetectCooldown = 40; // 8秒 / 200ms = 40帧 var nextTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(8) }; nextTimer.Tick += (s, e) => { ((DispatcherTimer)s!).Stop(); SimulateVehicleArrival(); }; nextTimer.Start(); } } private void CaptureGrossWeight() { var weight = Math.Round(CurrentWeight, 2); GrossWeight = weight; GrossWeightCaptured = true; AddLog($"采集毛重:{GrossWeight:N2} kg"); } private void CaptureTareWeight() { var weight = Math.Round(CurrentWeight, 2); TareWeight = weight; TareWeightCaptured = true; AddLog($"采集皮重:{TareWeight:N2} kg"); } private bool CanCaptureGrossWeight() { if (GrossWeightCaptured) return false; // 手动测试模式:无需等稳定即可点采集 if (IsManualWeightEntry) return true; return IsWeightStable; } private bool CanCaptureTareWeight() { if (TareWeightCaptured) return false; if (IsManualWeightEntry) return true; return IsWeightStable; } private void RecalcNetWeight() { // 与后端一致:0 或占位视为“未称”,避免仅毛重时 net=tare(0) 即毛重、单据误判完成 if (HasEffectiveWeighValue(GrossWeight) && HasEffectiveWeighValue(TareWeight)) { NetWeight = Math.Round(GrossWeight!.Value - TareWeight!.Value, 2); RaisePropertyChanged(nameof(NetWeightDisplay)); } else { NetWeight = null; RaisePropertyChanged(nameof(NetWeightDisplay)); } } private static bool HasEffectiveWeighValue(double? kg) => kg.HasValue && kg.Value > 0; private async Task SaveAsync() { if (string.IsNullOrWhiteSpace(PlateNumber)) { HandyControl.Controls.MessageBox.Warning("车牌号不能为空!"); return; } var selectedSource = SelectedRecentWeightRecord?.Source; var isCompleteSelectedRecord = !string.IsNullOrWhiteSpace(selectedSource?.Id); var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal); if (!isCompleteSelectedRecord) { if (isOutbound && !HasEffectiveWeighValue(TareWeight)) { HandyControl.Controls.MessageBox.Warning("出厂流程请先采集皮重!"); return; } if (!isOutbound && !HasEffectiveWeighValue(GrossWeight)) { HandyControl.Controls.MessageBox.Warning("进厂流程请先采集毛重!"); return; } } else { var needSecondWeightCaptured = isOutbound ? HasEffectiveWeighValue(GrossWeight) : HasEffectiveWeighValue(TareWeight); if (!needSecondWeightCaptured) { HandyControl.Controls.MessageBox.Warning(isOutbound ? "当前为已称皮重单据补毛重,必须先采集毛重!" : "当前为已称毛重单据补皮重,必须先采集皮重!"); return; } } var entity = new MesXslWeightRecord { Id = selectedSource?.Id, BillNo = selectedSource?.BillNo, WeighDate = WeighDate, InoutDirection = InoutDirection, PlateNumber = PlateNumber, SenderUnit = SenderUnit, ReceiverUnit = ReceiverUnit, GrossWeight = GrossWeight, TareWeight = TareWeight, NetWeight = NetWeight, DriverName = DriverName, DriverPhone = DriverPhone, MixerMaterialIds = _mixerMaterialIds, MixerMaterialNames = _mixerMaterialNames, // 勾选“手动”=2;未勾选=1(自动) MaterialType = IsMixerMaterialManual ? "2" : "1" }; try { var ok = isCompleteSelectedRecord ? await _weightRecordService.EditAsync(entity) : await _weightRecordService.AddAsync(entity); if (ok) { CacheUnitByDirection(); if (isCompleteSelectedRecord) { Growl.Success("皮重回填成功,单据已更新为称重完成!"); AddLog(isOutbound ? $"更新成功:{entity.BillNo ?? PlateNumber},毛重 {GrossWeight:N2} kg" : $"更新成功:{entity.BillNo ?? PlateNumber},皮重 {TareWeight:N2} kg"); } else { Growl.Success("磅单保存成功!"); AddLog($"保存成功:{PlateNumber},净重 {NetWeight:N2} kg"); } _ = UpsertVehicleAsync(); await LoadRecentWeightRecordsAsync(PlateNumber); // 保存后重置重量相关字段,保留日期/方向 GrossWeight = null; TareWeight = null; NetWeight = null; GrossWeightCaptured = false; TareWeightCaptured = false; SelectedRecentWeightRecord = null; IsPlateNumberLocked = false; PlateNumber = null; DetectedPlate = string.Empty; HasPlate = false; SenderUnit = null; ReceiverUnit = null; DriverName = null; DriverPhone = null; ClearSupplierSelection(); ClearCustomerSelection(); ClearMixerMaterialSelection(); ClearVehicleMatch(); IsManualWeightEntry = false; // 保存后立即按当前方向回填缓存,避免必须重进页面才显示 TryApplyCachedUnitByDirection(InoutDirection); RaisePropertyChanged(nameof(NetWeightDisplay)); } else { Growl.Error("磅单保存失败!"); } } catch (Exception ex) { Growl.Error($"保存失败:{ex.Message}"); } } private void ClearForm() { var confirm = System.Windows.MessageBox.Show( "确认清空所有填写内容?", "确认清空", System.Windows.MessageBoxButton.OKCancel, System.Windows.MessageBoxImage.Question); if (confirm != System.Windows.MessageBoxResult.OK) return; WeighDate = DateTime.Today; InoutDirection = "1"; PlateNumber = null; SenderUnit = null; ReceiverUnit = null; GrossWeight = null; TareWeight = null; NetWeight = null; DriverName = null; DriverPhone = null; GrossWeightCaptured = false; TareWeightCaptured = false; DetectedPlate = string.Empty; HasPlate = false; SelectedRecentWeightRecord = null; IsPlateNumberLocked = false; ClearSupplierSelection(); ClearCustomerSelection(); ClearMixerMaterialSelection(); ClearVehicleMatch(); IsManualWeightEntry = false; RaisePropertyChanged(nameof(NetWeightDisplay)); AddLog("表单已清空"); } private void CacheUnitByDirection() { var changed = false; // 按需求:进厂缓存收货单位;出厂缓存发货单位 if (string.Equals(InoutDirection, "1", StringComparison.Ordinal)) { if (!string.IsNullOrWhiteSpace(ReceiverUnit) && !string.Equals(_cachedInboundReceiverUnit, ReceiverUnit, StringComparison.Ordinal)) { _cachedInboundReceiverUnit = ReceiverUnit; changed = true; } } else if (string.Equals(InoutDirection, "2", StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(SenderUnit) && !string.Equals(_cachedOutboundSenderUnit, SenderUnit, StringComparison.Ordinal)) { _cachedOutboundSenderUnit = SenderUnit; changed = true; } if (changed) { SaveUnitCache(); } } private void TryApplyCachedUnitByDirection(string? inoutDirection) { // 切换方向时动态切换缓存展示: // 进厂只展示“收货单位缓存”,出厂只展示“发货单位缓存” if (string.Equals(inoutDirection, "1", StringComparison.Ordinal)) { // 进厂:清空发货单位展示 _selectedSupplier = null; SenderUnit = null; RaisePropertyChanged(nameof(SenderUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedSupplier)); // 进厂:带出收货单位缓存(无缓存则清空) if (!string.IsNullOrWhiteSpace(_cachedInboundReceiverUnit)) { ReceiverUnit = _cachedInboundReceiverUnit; AddLog($"已带出进厂收货单位缓存:{_cachedInboundReceiverUnit}"); } else { _selectedCustomer = null; ReceiverUnit = null; RaisePropertyChanged(nameof(ReceiverUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedCustomer)); } return; } if (string.Equals(inoutDirection, "2", StringComparison.Ordinal)) { // 出厂:清空收货单位展示 _selectedCustomer = null; ReceiverUnit = null; RaisePropertyChanged(nameof(ReceiverUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedCustomer)); // 出厂:带出发货单位缓存(无缓存则清空) if (!string.IsNullOrWhiteSpace(_cachedOutboundSenderUnit)) { SenderUnit = _cachedOutboundSenderUnit; AddLog($"已带出出厂发货单位缓存:{_cachedOutboundSenderUnit}"); } else { _selectedSupplier = null; SenderUnit = null; RaisePropertyChanged(nameof(SenderUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedSupplier)); } } } private void LoadUnitCache() { try { if (!File.Exists(_unitCacheFilePath)) return; var json = File.ReadAllText(_unitCacheFilePath); var cache = JsonSerializer.Deserialize(json); if (cache == null) return; _cachedInboundReceiverUnit = string.IsNullOrWhiteSpace(cache.InboundReceiverUnit) ? null : cache.InboundReceiverUnit; _cachedOutboundSenderUnit = string.IsNullOrWhiteSpace(cache.OutboundSenderUnit) ? null : cache.OutboundSenderUnit; } catch { // 缓存读取失败不影响主流程 } } private void SaveUnitCache() { try { var cache = new UnitDirectionCache { InboundReceiverUnit = _cachedInboundReceiverUnit, OutboundSenderUnit = _cachedOutboundSenderUnit, }; var json = JsonSerializer.Serialize(cache); File.WriteAllText(_unitCacheFilePath, json); } catch { // 缓存写入失败不影响主流程 } } private async Task OpenSupplierPickerAsync() { SupplierPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await HandyControl.Controls.Dialog.Show() .Initialize(vm => { pickerVm = vm; }) .GetResultAsync(); } catch { return; } if (!confirmed || pickerVm?.SelectedSupplier == null) return; var supplier = pickerVm.SelectedSupplier; _selectedSupplier = supplier; SenderUnit = supplier.SupplierShortName ?? supplier.SupplierName; RaisePropertyChanged(nameof(SenderUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedSupplier)); AddLog($"已选发货单位:{supplier.SupplierName}"); } private async Task OpenCustomerPickerAsync() { CustomerPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await HandyControl.Controls.Dialog.Show() .Initialize(vm => { pickerVm = vm; }) .GetResultAsync(); } catch { return; } if (!confirmed || pickerVm?.SelectedCustomer == null) return; var customer = pickerVm.SelectedCustomer; _selectedCustomer = customer; ReceiverUnit = customer.CustomerShortName ?? customer.CustomerName; RaisePropertyChanged(nameof(ReceiverUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedCustomer)); AddLog($"已选收货单位:{customer.CustomerName}"); } private void ClearSupplierSelection() { _selectedSupplier = null; SenderUnit = null; RaisePropertyChanged(nameof(SenderUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedSupplier)); } private void ClearCustomerSelection() { _selectedCustomer = null; ReceiverUnit = null; RaisePropertyChanged(nameof(ReceiverUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedCustomer)); } private async Task OpenMixerMaterialPickerAsync() { MixerMaterialPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await HandyControl.Controls.Dialog.Show() .Initialize(vm => { pickerVm = vm; }) .GetResultAsync(); } catch { return; } if (!confirmed || pickerVm == null) return; var selected = pickerVm.SelectedMaterials; if (selected.Count == 0) return; _mixerMaterialIds = string.Join(";", selected.Select(m => m.Source.Id ?? "")); MixerMaterialNames = string.Join(";", selected.Select(m => m.Source.MaterialName ?? "")); if (IsMixerMaterialManual) { IsMixerMaterialManual = false; } RaisePropertyChanged(nameof(MixerMaterialDisplay)); RaisePropertyChanged(nameof(HasSelectedMixerMaterials)); AddLog($"已选密炼物料:{_mixerMaterialNames}"); } private void ClearMixerMaterialSelection() { _mixerMaterialIds = null; MixerMaterialNames = null; IsMixerMaterialManual = false; RaisePropertyChanged(nameof(MixerMaterialDisplay)); RaisePropertyChanged(nameof(HasSelectedMixerMaterials)); } private void AddLog(string message) { var entry = $"{DateTime.Now:HH:mm:ss} {message}"; OperationLogs.Insert(0, entry); while (OperationLogs.Count > 8) OperationLogs.RemoveAt(OperationLogs.Count - 1); } private async Task LoadDictOptionsAsync() { try { var options = await _dictSyncService.GetDictOptionsAsync("xslmes_inout_direction"); InoutDirectionOptions.Clear(); foreach (var item in options) InoutDirectionOptions.Add(item); if (InoutDirectionOptions.Count == 0) AddDefaultDirectionOptions(); } catch { AddDefaultDirectionOptions(); } } private void AddDefaultDirectionOptions() { InoutDirectionOptions.Clear(); InoutDirectionOptions.Add(new KeyValuePair("进厂", "1")); InoutDirectionOptions.Add(new KeyValuePair("出厂", "2")); } private async Task LoadRecentWeightRecordsAsync(string? plateNumber) { try { var plate = (plateNumber ?? string.Empty).Trim(); RecentWeightRecords.Clear(); if (string.IsNullOrWhiteSpace(plate)) { SelectedRecentWeightRecord = null; return; } // 按进出方向查询当天待补称单据:进厂=已称毛重,出厂=已称皮重。 var page = await _weightRecordService.PageAsync(1, 100, filterPlateNumber: plate); var today = DateTime.Today; var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal); bool effT(double? w) => w.HasValue && w.Value > 0; var candidates = page.Records .Where(r => string.Equals((r.PlateNumber ?? string.Empty).Trim(), plate, StringComparison.OrdinalIgnoreCase) && r.WeighDate.HasValue && r.WeighDate.Value.Date == today && (isOutbound ? (effT(r.TareWeight) && !effT(r.GrossWeight)) : (effT(r.GrossWeight) && !effT(r.TareWeight)))) .OrderByDescending(r => r.CreateTime ?? DateTime.MinValue) .ToList(); foreach (var record in candidates) { RecentWeightRecords.Add(new WeightRecordSimpleItem { Source = record, BillNo = record.BillNo ?? "-", PlateNumber = record.PlateNumber ?? "-", FirstWeightDisplay = isOutbound ? (HasEffectiveWeighValue(record.TareWeight) ? $"{record.TareWeight!.Value:N2}" : "-") : (HasEffectiveWeighValue(record.GrossWeight) ? $"{record.GrossWeight!.Value:N2}" : "-") }); } SelectedRecentWeightRecord = null; } catch { // 查询失败时保持现有列表,不中断主流程 } } private void ApplySelectedRecordToForm(WeightRecordSimpleItem selected) { if (selected.Source == null) return; // 带入历史磅单时,不触发车辆重新查询 _matchedVehicle = null; _lastLookedUpPlate = selected.Source.PlateNumber?.Trim(); VehicleLookupStatus = "None"; _isApplyingSelectedRecord = true; try { // 自动带入基础信息,并按进出方向保留"待补称"的第二次称重位。 WeighDate = selected.Source.WeighDate ?? DateTime.Today; InoutDirection = selected.Source.InoutDirection; PlateNumber = selected.Source.PlateNumber; _selectedSupplier = null; _selectedCustomer = null; SenderUnit = selected.Source.SenderUnit; ReceiverUnit = selected.Source.ReceiverUnit; RaisePropertyChanged(nameof(SenderUnitDisplay)); RaisePropertyChanged(nameof(ReceiverUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedSupplier)); RaisePropertyChanged(nameof(HasSelectedCustomer)); DriverName = selected.Source.DriverName; DriverPhone = selected.Source.DriverPhone; _mixerMaterialIds = selected.Source.MixerMaterialIds; MixerMaterialNames = selected.Source.MixerMaterialNames; IsMixerMaterialManual = string.Equals(selected.Source.MaterialType, "2", StringComparison.Ordinal); RaisePropertyChanged(nameof(MixerMaterialDisplay)); RaisePropertyChanged(nameof(HasSelectedMixerMaterials)); var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal); if (isOutbound) { TareWeight = selected.Source.TareWeight; TareWeightCaptured = HasEffectiveWeighValue(TareWeight); GrossWeight = null; GrossWeightCaptured = false; } else { GrossWeight = selected.Source.GrossWeight; GrossWeightCaptured = HasEffectiveWeighValue(GrossWeight); TareWeight = null; TareWeightCaptured = false; } AddLog($"已带入榜单:{selected.BillNo}"); } finally { _isApplyingSelectedRecord = false; } } private void ClearFormByDirectionSwitch() { // 已选榜单后切换进出方向,清空数据防止串单。 WeighDate = DateTime.Today; PlateNumber = null; SenderUnit = null; ReceiverUnit = null; DriverName = null; DriverPhone = null; GrossWeight = null; TareWeight = null; NetWeight = null; GrossWeightCaptured = false; TareWeightCaptured = false; SelectedRecentWeightRecord = null; IsPlateNumberLocked = false; ClearSupplierSelection(); ClearCustomerSelection(); ClearMixerMaterialSelection(); ClearVehicleMatch(); IsManualWeightEntry = false; RaisePropertyChanged(nameof(NetWeightDisplay)); AddLog("已切换进出方向,当前表单数据已清空"); } // ─── 车辆档案查询与回写 ─── private async Task LookupVehicleByPlateAsync(string plate) { if (string.Equals(plate, _lastLookedUpPlate, StringComparison.OrdinalIgnoreCase)) return; _lastLookedUpPlate = plate; VehicleLookupStatus = "Searching"; try { var result = await _vehicleService.PageAsync(1, 1, plateNumber: plate); var vehicle = result.Records.FirstOrDefault(v => string.Equals(v.PlateNumber?.Trim(), plate, StringComparison.OrdinalIgnoreCase) && v.Status != "1"); if (vehicle != null) { _matchedVehicle = vehicle; VehicleLookupStatus = "Matched"; ApplyVehicleToForm(vehicle); } else { _matchedVehicle = null; VehicleLookupStatus = "NotFound"; } } catch { _matchedVehicle = null; VehicleLookupStatus = "None"; } } private void ApplyVehicleToForm(MesXslVehicle vehicle) { // 司机信息优先带出 if (!string.IsNullOrWhiteSpace(vehicle.DriverName)) DriverName = vehicle.DriverName; if (!string.IsNullOrWhiteSpace(vehicle.DriverPhone)) DriverPhone = vehicle.DriverPhone; // 根据车辆归属自动判断进出方向与发收货单位 if (vehicle.VehicleBelong == "2") // 供应商 → 进厂 { if (!_isApplyingSelectedRecord) InoutDirection = "1"; _selectedSupplier = null; SenderUnit = vehicle.SupplierShortName ?? vehicle.SupplierName; RaisePropertyChanged(nameof(SenderUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedSupplier)); } else if (vehicle.VehicleBelong == "1") // 客户 → 出厂 { if (!_isApplyingSelectedRecord) InoutDirection = "2"; _selectedCustomer = null; ReceiverUnit = vehicle.CustomerShortName; RaisePropertyChanged(nameof(ReceiverUnitDisplay)); RaisePropertyChanged(nameof(HasSelectedCustomer)); } // VehicleBelong == "3"(本公司):方向不定,不自动切换 AddLog($"车辆档案匹配:{vehicle.PlateNumber}({vehicle.VehicleBelongText})"); } private void ClearVehicleMatch() { _matchedVehicle = null; _lastLookedUpPlate = null; VehicleLookupStatus = "None"; } private async Task UpsertVehicleAsync() { if (string.IsNullOrWhiteSpace(PlateNumber)) return; try { if (_matchedVehicle != null) { // 已有档案:检查司机、供应商、客户是否需要回写 bool driverChanged = !string.Equals(DriverName, _matchedVehicle.DriverName) || !string.Equals(DriverPhone, _matchedVehicle.DriverPhone); // 操作人员通过弹窗选了供应商,且与档案不同(含原来为空的情况) bool supplierChanged = _matchedVehicle.VehicleBelong == "2" && _selectedSupplier != null && _selectedSupplier.Id != _matchedVehicle.SupplierId; // 操作人员通过弹窗选了客户,且与档案不同(含原来为空的情况) bool customerChanged = _matchedVehicle.VehicleBelong == "1" && _selectedCustomer != null && _selectedCustomer.Id != _matchedVehicle.CustomerIds; if (!driverChanged && !supplierChanged && !customerChanged) return; await _vehicleService.EditAsync(new MesXslVehicle { Id = _matchedVehicle.Id, PlateNumber = _matchedVehicle.PlateNumber, VehicleBelong = _matchedVehicle.VehicleBelong, TareWeightKg = _matchedVehicle.TareWeightKg, LoadCapacity = _matchedVehicle.LoadCapacity, SupplierId = supplierChanged ? _selectedSupplier!.Id : _matchedVehicle.SupplierId, SupplierName = supplierChanged ? _selectedSupplier!.SupplierName : _matchedVehicle.SupplierName, SupplierShortName = supplierChanged ? _selectedSupplier!.SupplierShortName : _matchedVehicle.SupplierShortName, CustomerIds = customerChanged ? _selectedCustomer!.Id : _matchedVehicle.CustomerIds, CustomerShortName = customerChanged ? (_selectedCustomer!.CustomerShortName ?? _selectedCustomer.CustomerName) : _matchedVehicle.CustomerShortName, DriverName = DriverName, DriverPhone = DriverPhone, Status = _matchedVehicle.Status, TenantId = _matchedVehicle.TenantId, Version = _matchedVehicle.Version, }); var parts = new List(); if (driverChanged) parts.Add("司机信息"); if (supplierChanged) parts.Add($"供应商→{_selectedSupplier!.SupplierName}"); if (customerChanged) parts.Add($"客户→{_selectedCustomer!.CustomerName}"); AddLog($"车辆档案已更新:{PlateNumber}({string.Join("、", parts)})"); } else { // 只有明确查询过且未找到时才新建,避免从最近榜单带入时重复新建 if (VehicleLookupStatus != "NotFound") return; var vehicleBelong = InoutDirection == "2" ? "1" : "2"; // 出厂→客户 进厂→供应商 var newVehicle = new MesXslVehicle { PlateNumber = PlateNumber?.Trim(), VehicleBelong = vehicleBelong, SupplierId = _selectedSupplier?.Id, SupplierName = _selectedSupplier?.SupplierName, SupplierShortName = _selectedSupplier?.SupplierShortName, CustomerIds = _selectedCustomer?.Id, CustomerShortName = _selectedCustomer?.CustomerShortName ?? (InoutDirection == "2" ? ReceiverUnit : null), DriverName = DriverName, DriverPhone = DriverPhone, Status = "0", }; await _vehicleService.AddAsync(newVehicle); AddLog($"已新建车辆档案:{PlateNumber}"); } } catch { // 车辆回写失败不影响磅单保存,静默处理 } } protected override void CleanUp() { base.CleanUp(); _serialTimer.Stop(); } } public class UnitDirectionCache { public string? InboundReceiverUnit { get; set; } public string? OutboundSenderUnit { get; set; } } public class WeightRecordSimpleItem { public MesXslWeightRecord? Source { get; set; } public string BillNo { get; set; } = "-"; public string PlateNumber { get; set; } = "-"; public string FirstWeightDisplay { get; set; } = "-"; }