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