新增业务打印绑定功能,整合打印模板与业务数据的映射配置,优化打印数据生成逻辑。新增免密接口,支持桌面端打印模板的查询与列表展示,提升用户体验和系统的实时数据同步能力。同时,重构相关控制器以增强系统的可维护性和扩展性。
This commit is contained in:
@@ -151,7 +151,12 @@ namespace YY.Admin.ViewModels.Control
|
||||
["PrintTemplateListView"] = "PrintTemplateListView",
|
||||
["/platform/print"] = "PrintTemplateListView",
|
||||
["print"] = "PrintTemplateListView",
|
||||
["printTemplate"] = "PrintTemplateListView"
|
||||
["printTemplate"] = "PrintTemplateListView",
|
||||
|
||||
// 已实现页面:业务打印绑定(桌面端)
|
||||
["PrintBizTemplateBindListView"] = "PrintBizTemplateBindListView",
|
||||
["/platform/printBizBind"] = "PrintBizTemplateBindListView",
|
||||
["printBizBind"] = "PrintBizTemplateBindListView"
|
||||
};
|
||||
|
||||
private MenuItem? _selectedMenuItem;
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Views.Print;
|
||||
|
||||
namespace YY.Admin.ViewModels.Print;
|
||||
|
||||
/// <summary>业务打印绑定列表(只读,本地缓存 + 同步)</summary>
|
||||
public class PrintBizTemplateBindListViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPrintBizTemplateBindService _bindService;
|
||||
private SubscriptionToken? _changeToken;
|
||||
|
||||
private List<PrintBizTemplateBind> _all = new();
|
||||
|
||||
public ObservableCollection<PrintBizTemplateBind> Items { get; } = new();
|
||||
|
||||
private string _statusMessage = string.Empty;
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
private string? _filterBizCode;
|
||||
public string? FilterBizCode
|
||||
{
|
||||
get => _filterBizCode;
|
||||
set => SetProperty(ref _filterBizCode, value);
|
||||
}
|
||||
|
||||
private string? _filterBizName;
|
||||
public string? FilterBizName
|
||||
{
|
||||
get => _filterBizName;
|
||||
set => SetProperty(ref _filterBizName, value);
|
||||
}
|
||||
|
||||
private string? _filterTemplateCode;
|
||||
public string? FilterTemplateCode
|
||||
{
|
||||
get => _filterTemplateCode;
|
||||
set => SetProperty(ref _filterTemplateCode, value);
|
||||
}
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ResetCommand { get; }
|
||||
public DelegateCommand<PrintBizTemplateBind> DetailCommand { get; }
|
||||
|
||||
public PrintBizTemplateBindListViewModel(
|
||||
IPrintBizTemplateBindService bindService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_bindService = bindService;
|
||||
|
||||
SearchCommand = new DelegateCommand(ApplyFilter);
|
||||
ResetCommand = new DelegateCommand(() =>
|
||||
{
|
||||
FilterBizCode = null;
|
||||
FilterBizName = null;
|
||||
FilterTemplateCode = null;
|
||||
ApplyFilter();
|
||||
});
|
||||
DetailCommand = new DelegateCommand<PrintBizTemplateBind>(OpenDetail);
|
||||
|
||||
_changeToken = _eventAggregator
|
||||
.GetEvent<PrintBizTemplateBindChangedEvent>()
|
||||
.Subscribe(_ => { RefreshSilentlyAsync().ConfigureAwait(false); }, ThreadOption.UIThread);
|
||||
|
||||
ShowCached();
|
||||
_ = RefreshSilentlyAsync();
|
||||
}
|
||||
|
||||
private void ShowCached()
|
||||
{
|
||||
var cached = _bindService.GetCached();
|
||||
if (cached.Count == 0) return;
|
||||
_all = cached.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private async Task RefreshSilentlyAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await _bindService.RefreshCacheAsync().ConfigureAwait(false);
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_all = list.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
var cached = _bindService.GetCached();
|
||||
if (cached.Count > 0 && _all.Count == 0)
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_all = cached.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
IEnumerable<PrintBizTemplateBind> result = _all;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterBizCode))
|
||||
result = result.Where(x => (x.BizCode ?? string.Empty)
|
||||
.Contains(FilterBizCode, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterBizName))
|
||||
result = result.Where(x => (x.BizName ?? string.Empty)
|
||||
.Contains(FilterBizName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterTemplateCode))
|
||||
result = result.Where(x => (x.TemplateCode ?? string.Empty)
|
||||
.Contains(FilterTemplateCode, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var filtered = result.ToList();
|
||||
|
||||
for (int i = Items.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!filtered.Any(t => t.Id == Items[i].Id))
|
||||
Items.RemoveAt(i);
|
||||
}
|
||||
for (int i = 0; i < filtered.Count; i++)
|
||||
{
|
||||
var item = filtered[i];
|
||||
var existingIdx = -1;
|
||||
for (int j = 0; j < Items.Count; j++)
|
||||
{
|
||||
if (Items[j].Id == item.Id) { existingIdx = j; break; }
|
||||
}
|
||||
if (existingIdx < 0)
|
||||
Items.Insert(i, item);
|
||||
else
|
||||
{
|
||||
if (existingIdx != i) Items.Move(existingIdx, i);
|
||||
Items[i] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
var hasFilter = !string.IsNullOrWhiteSpace(FilterBizCode)
|
||||
|| !string.IsNullOrWhiteSpace(FilterBizName)
|
||||
|| !string.IsNullOrWhiteSpace(FilterTemplateCode);
|
||||
StatusMessage = hasFilter
|
||||
? $"筛选结果 {Items.Count} / {_all.Count} 条"
|
||||
: _all.Count > 0
|
||||
? $"共 {_all.Count} 条绑定(缓存于本地,可离线查看)"
|
||||
: "暂无数据(联网后将自动同步)";
|
||||
}
|
||||
|
||||
private async void OpenDetail(PrintBizTemplateBind? row)
|
||||
{
|
||||
if (row == null) return;
|
||||
PrintBizTemplateBind model = row;
|
||||
try
|
||||
{
|
||||
var fresh = await _bindService.GetByIdAsync(row.Id ?? "").ConfigureAwait(false);
|
||||
if (fresh != null) model = fresh;
|
||||
}
|
||||
catch { /* 使用列表行数据 */ }
|
||||
|
||||
// 网络请求 continuation 可能在非 STA 线程,Window 必须在 UI 线程创建
|
||||
var app = Application.Current;
|
||||
if (app?.Dispatcher == null) return;
|
||||
await app.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var win = new PrintBizTemplateBindDetailWindow(model)
|
||||
{
|
||||
Owner = app.MainWindow
|
||||
};
|
||||
win.ShowDialog();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -366,44 +366,56 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
AddSplitDetailCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
/// <summary>校验并提交新增/编辑;不弹关闭、不执行独立页的 InitializeForAdd。成功时 Result=true。</summary>
|
||||
protected async Task<bool> PersistEntryCoreAsync()
|
||||
{
|
||||
if (Entry == null) return false;
|
||||
ApplySplitDetailsToEntry();
|
||||
ApplyDefaultEntryStatusForAdd();
|
||||
ApplyHiddenFieldDefaultsForAdd();
|
||||
|
||||
var missing = new List<string>();
|
||||
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
|
||||
{
|
||||
ApplySplitDetailsToEntry();
|
||||
ApplyDefaultEntryStatusForAdd();
|
||||
ApplyHiddenFieldDefaultsForAdd();
|
||||
if (!await PersistEntryCoreAsync()) return;
|
||||
|
||||
var missing = new List<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
HandyControl.Controls.MessageBox.Success("新增成功!");
|
||||
if (CloseAction == null)
|
||||
{
|
||||
// 独立新增页面:保存成功后自动清空表单,便于连续录入
|
||||
InitializeForAdd();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CloseAction?.Invoke();
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
using Prism.Commands;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Infrastructure.Print;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
|
||||
namespace YY.Admin.ViewModels.RawMaterialEntry;
|
||||
|
||||
@@ -14,7 +20,12 @@ namespace YY.Admin.ViewModels.RawMaterialEntry;
|
||||
public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel
|
||||
{
|
||||
private const int TodayListFetchSize = 5000;
|
||||
private const string RawMaterialEntryBizCode = "1900000000000000530";
|
||||
private const string RawMaterialEntryTemplateCode = "MES_RAW_MATERIAL_ENTRY";
|
||||
private readonly IRawMaterialCardService _rawMaterialCardService;
|
||||
private readonly IPrintDotService _printDotService;
|
||||
private readonly IPrintBizTemplateBindService _printBizTemplateBindService;
|
||||
private readonly IPrintTemplateService _printTemplateService;
|
||||
|
||||
private static readonly JsonSerializerOptions LayoutJsonOpts = new()
|
||||
{
|
||||
@@ -95,25 +106,131 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
public DelegateCommand GenerateRawMaterialCardsCommand { get; }
|
||||
/// <summary>「重新拆码」:清除已生成的卡片 + 清空明细,仅在编辑态可用。</summary>
|
||||
public DelegateCommand ResplitCommand { get; }
|
||||
public DelegateCommand SaveAndPrintCommand { get; }
|
||||
public DelegateCommand RefreshPrintersCommand { get; }
|
||||
|
||||
/// <summary>PrintDot 桥接器返回的打印机列表(与打印模板页一致)。</summary>
|
||||
public ObservableCollection<PrintDotPrinter> Printers { get; } = new();
|
||||
|
||||
private bool _suppressPrinterSave;
|
||||
private PrintDotPrinter? _selectedPrinter;
|
||||
public PrintDotPrinter? SelectedPrinter
|
||||
{
|
||||
get => _selectedPrinter;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _selectedPrinter, value)) return;
|
||||
if (_suppressPrinterSave) return;
|
||||
var s = PrintDotSettings.Load();
|
||||
s.SelectedPrinter = value?.Name ?? string.Empty;
|
||||
s.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private string _printerStatus = string.Empty;
|
||||
public string PrinterStatus
|
||||
{
|
||||
get => _printerStatus;
|
||||
set => SetProperty(ref _printerStatus, value);
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions PreviewSnapshotJsonOpts = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
private readonly DispatcherTimer _printPreviewTimer;
|
||||
private string _lastPreviewSnapshot = string.Empty;
|
||||
private int _previewRequestGeneration;
|
||||
private bool _printPreviewBusy;
|
||||
private bool _previewTemplateLoaded;
|
||||
private string _previewTemplateJson = string.Empty;
|
||||
private string _previewTemplateName = "原料入场记录";
|
||||
private string _previewTemplateCode = RawMaterialEntryTemplateCode;
|
||||
private string _previewFieldMappingJson = "[]";
|
||||
|
||||
private bool _isPrintPreviewExpanded = true;
|
||||
/// <summary>右侧下方「入场标签打印预览」折叠面板是否展开。</summary>
|
||||
public bool IsPrintPreviewExpanded
|
||||
{
|
||||
get => _isPrintPreviewExpanded;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _isPrintPreviewExpanded, value)) return;
|
||||
if (value) _lastPreviewSnapshot = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private string _printPreviewStatus = string.Empty;
|
||||
/// <summary>预览区状态提示(如离线、加载中、错误摘要)。</summary>
|
||||
public string PrintPreviewStatus
|
||||
{
|
||||
get => _printPreviewStatus;
|
||||
private set => SetProperty(ref _printPreviewStatus, value);
|
||||
}
|
||||
|
||||
/// <summary>由 View 订阅,在 UI 线程将 HTML 交给 WebView2。</summary>
|
||||
public event EventHandler<string>? PrintPreviewHtmlReady;
|
||||
|
||||
private bool _isActionBusy;
|
||||
/// <summary>底部动作按钮(保存/保存并打印/生成卡片)是否处于执行中。</summary>
|
||||
public bool IsActionBusy
|
||||
{
|
||||
get => _isActionBusy;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _isActionBusy, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(IsNotActionBusy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNotActionBusy => !IsActionBusy;
|
||||
|
||||
private string _actionBusyText = "处理中...";
|
||||
/// <summary>动作执行中的遮罩提示文案。</summary>
|
||||
public string ActionBusyText
|
||||
{
|
||||
get => _actionBusyText;
|
||||
private set => SetProperty(ref _actionBusyText, value);
|
||||
}
|
||||
|
||||
public RawMaterialEntryOperationViewModel(
|
||||
IRawMaterialEntryService entryService,
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
IMixerMaterialService mixerMaterialService,
|
||||
IRawMaterialCardService rawMaterialCardService,
|
||||
IPrintDotService printDotService,
|
||||
IPrintBizTemplateBindService printBizTemplateBindService,
|
||||
IPrintTemplateService printTemplateService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager)
|
||||
: base(entryService, dictSyncService, mixerMaterialService, container, regionManager)
|
||||
{
|
||||
_rawMaterialCardService = rawMaterialCardService;
|
||||
_printDotService = printDotService;
|
||||
_printBizTemplateBindService = printBizTemplateBindService;
|
||||
_printTemplateService = printTemplateService;
|
||||
LoadLayoutState();
|
||||
ToggleRightPanelCommand = new DelegateCommand(() => IsRightPanelExpanded = !IsRightPanelExpanded);
|
||||
RefreshTodayEntriesCommand = new DelegateCommand(async () => await LoadTodayEntriesAsync());
|
||||
GenerateRawMaterialCardsCommand = new DelegateCommand(async () => await GenerateRawMaterialCardsAsync());
|
||||
ResplitCommand = new DelegateCommand(async () => await ResplitAsync(), () => CanResplit)
|
||||
.ObservesProperty(() => Entry);
|
||||
SaveAndPrintCommand = new DelegateCommand(async () => await SaveAndPrintAsync());
|
||||
RefreshPrintersCommand = new DelegateCommand(async () => await RefreshPrintersAsync(verbose: true));
|
||||
// 集合变化:批量重订阅 item.PropertyChanged 监听 HasCard/Portions,并同步刷新两个 Can*。
|
||||
SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChangedForCanFlags;
|
||||
_ = RefreshPrintersAsync(verbose: false);
|
||||
|
||||
var dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||
_printPreviewTimer = new DispatcherTimer(
|
||||
TimeSpan.FromMilliseconds(480),
|
||||
DispatcherPriority.Background,
|
||||
(_, _) => _ = OnPrintPreviewTickAsync(),
|
||||
dispatcher);
|
||||
_printPreviewTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -160,6 +277,7 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
|
||||
public override void InitializeForAdd()
|
||||
{
|
||||
_lastPreviewSnapshot = string.Empty;
|
||||
_suppressTodaySelectionReaction = true;
|
||||
_selectedTodayEntry = null;
|
||||
RaisePropertyChanged(nameof(SelectedTodayEntry));
|
||||
@@ -168,6 +286,149 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
}
|
||||
|
||||
/// <summary>保存后按业务打印绑定拉取模板与 printData,直接发送到 PrintDot 打印机(不弹预览窗)。</summary>
|
||||
private async Task SaveAndPrintAsync()
|
||||
{
|
||||
if (IsActionBusy) return;
|
||||
if (Entry == null) return;
|
||||
var wasEdit = !IsAddMode;
|
||||
var barcode = Entry.Barcode;
|
||||
BeginActionBusy("保存并打印中...");
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
if (!await PersistEntryCoreAsync()) return;
|
||||
|
||||
string? entryId = Entry.Id;
|
||||
if (string.IsNullOrWhiteSpace(entryId) && !string.IsNullOrWhiteSpace(barcode))
|
||||
{
|
||||
var page = await EntryService.PageAsync(1, 50, barcode: barcode);
|
||||
entryId = page.Records
|
||||
.FirstOrDefault(e => string.Equals(e.Barcode, barcode, StringComparison.OrdinalIgnoreCase))?.Id
|
||||
?? page.Records
|
||||
.OrderByDescending(e => e.EntryTime ?? e.CreateTime ?? DateTime.MinValue)
|
||||
.FirstOrDefault()?.Id;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(entryId))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("保存成功,但未能解析记录主键,无法打印。请从右侧列表选中该条后再试。");
|
||||
}
|
||||
else
|
||||
{
|
||||
var (templateJson, printDataJson, err) = await EntryService.PrepareNativePrintAsync(entryId);
|
||||
if (!string.IsNullOrWhiteSpace(err))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Error($"保存成功,但打印准备失败:{err}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var selectedPrinterName = SelectedPrinter?.Name?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(selectedPrinterName))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("保存成功,但未选择打印机。请先选择打印机后再试。");
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonObject? dataObj = JsonNode.Parse(printDataJson) as JsonObject;
|
||||
dataObj ??= new JsonObject();
|
||||
var html = NativePrintRenderService.RenderToHtml(templateJson, dataObj);
|
||||
var tpl = BuildPrintTemplateForEntry(templateJson);
|
||||
var pdfBase64 = await HtmlToPdfRenderer.RenderAsync(
|
||||
html,
|
||||
tpl.PaperWidthMm ?? 210d,
|
||||
tpl.PaperHeightMm ?? 297d);
|
||||
var jobName = string.IsNullOrWhiteSpace(barcode) ? "原料入场记录" : $"原料入场记录-{barcode}";
|
||||
await _printDotService.PrintAsync(selectedPrinterName, pdfBase64, jobName, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wasEdit)
|
||||
HandyControl.Controls.MessageBox.Success("编辑成功!");
|
||||
else
|
||||
HandyControl.Controls.MessageBox.Success("新增成功!");
|
||||
|
||||
Result = true;
|
||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
await LoadTodayEntriesAsync();
|
||||
InitializeForAdd();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Error($"保存或打印失败:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
EndActionBusy();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>通过 PrintDot 桥接器拉取打印机列表(与打印模板页一致)。</summary>
|
||||
private async Task RefreshPrintersAsync(bool verbose)
|
||||
{
|
||||
if (verbose) PrinterStatus = "刷新打印机中...";
|
||||
var savedName = PrintDotSettings.Load().SelectedPrinter;
|
||||
try
|
||||
{
|
||||
var list = await _printDotService.GetPrintersAsync();
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Printers.Clear();
|
||||
foreach (var p in list) Printers.Add(p);
|
||||
var match = list.FirstOrDefault(p => p.Name == savedName)
|
||||
?? list.FirstOrDefault(p => p.IsDefault)
|
||||
?? (list.Count > 0 ? list[0] : null);
|
||||
_suppressPrinterSave = true;
|
||||
SelectedPrinter = match;
|
||||
_suppressPrinterSave = false;
|
||||
PrinterStatus = list.Count > 0 ? $"共 {list.Count} 台打印机" : "未检测到打印机";
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Printers.Clear();
|
||||
_suppressPrinterSave = true;
|
||||
SelectedPrinter = null;
|
||||
_suppressPrinterSave = false;
|
||||
PrinterStatus = verbose
|
||||
? $"PrintDot 未连接:{ex.Message}"
|
||||
: "PrintDot 未连接";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static PrintTemplate BuildPrintTemplateForEntry(string? templateJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(templateJson) || templateJson == "{}")
|
||||
return new PrintTemplate { TemplateName = "原料入场记录", TemplateCode = "MES_RAW_MATERIAL_ENTRY" };
|
||||
var root = JsonDocument.Parse(templateJson).RootElement;
|
||||
double w = 210, h = 297;
|
||||
if (root.TryGetProperty("page", out var page))
|
||||
{
|
||||
if (page.TryGetProperty("width", out var wEl)) w = wEl.GetDouble();
|
||||
if (page.TryGetProperty("height", out var hEl)) h = hEl.GetDouble();
|
||||
}
|
||||
return new PrintTemplate
|
||||
{
|
||||
TemplateName = "原料入场记录",
|
||||
TemplateCode = "MES_RAW_MATERIAL_ENTRY",
|
||||
PaperWidthMm = w,
|
||||
PaperHeightMm = h,
|
||||
PaperOrientation = w > h ? "横向" : "纵向",
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new PrintTemplate { TemplateName = "原料入场记录", TemplateCode = "MES_RAW_MATERIAL_ENTRY" };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>页面首次加载时拉取「今日」列表(由 View Loaded 调用)。</summary>
|
||||
public async Task LoadTodayEntriesOnFirstShowAsync() => await LoadTodayEntriesAsync();
|
||||
|
||||
@@ -232,6 +493,7 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
// 若物料列表此前未加载导致选中项未回填,则补一次拉取(与编辑弹窗逻辑一致)
|
||||
if (_pendingMaterialId != null)
|
||||
await LoadMaterialOptionsAsync();
|
||||
_lastPreviewSnapshot = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -240,21 +502,30 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
/// </summary>
|
||||
protected override async Task SaveAsync()
|
||||
{
|
||||
if (IsActionBusy) return;
|
||||
BeginActionBusy("保存中...");
|
||||
var wasEdit = !IsAddMode;
|
||||
await base.SaveAsync();
|
||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
|
||||
if (!Result || CloseAction != null)
|
||||
try
|
||||
{
|
||||
return;
|
||||
await base.SaveAsync();
|
||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
|
||||
if (!Result || CloseAction != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadTodayEntriesAsync();
|
||||
|
||||
if (wasEdit)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Success("编辑成功!");
|
||||
InitializeForAdd();
|
||||
}
|
||||
}
|
||||
|
||||
await LoadTodayEntriesAsync();
|
||||
|
||||
if (wasEdit)
|
||||
finally
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Success("编辑成功!");
|
||||
InitializeForAdd();
|
||||
EndActionBusy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +619,7 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
/// </summary>
|
||||
private async Task GenerateRawMaterialCardsAsync()
|
||||
{
|
||||
if (IsActionBusy) return;
|
||||
if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("请先保存入场记录再生成原材料卡片!");
|
||||
@@ -395,6 +667,7 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
}
|
||||
}
|
||||
|
||||
BeginActionBusy("生成中...");
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
@@ -481,9 +754,336 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
EndActionBusy();
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginActionBusy(string text)
|
||||
{
|
||||
ActionBusyText = string.IsNullOrWhiteSpace(text) ? "处理中..." : text;
|
||||
IsActionBusy = true;
|
||||
}
|
||||
|
||||
private void EndActionBusy()
|
||||
{
|
||||
IsActionBusy = false;
|
||||
ActionBusyText = "处理中...";
|
||||
}
|
||||
|
||||
/// <summary>页面卸载时停止轮询,避免后台仍请求已释放的 WebView。</summary>
|
||||
public void StopPrintPreviewTimer() => _printPreviewTimer.Stop();
|
||||
|
||||
/// <summary>视图重新附加时恢复轮询(与 <see cref="StopPrintPreviewTimer"/> 配对)。</summary>
|
||||
public void StartPrintPreviewTimer()
|
||||
{
|
||||
if (!_printPreviewTimer.IsEnabled) _printPreviewTimer.Start();
|
||||
}
|
||||
|
||||
private async Task OnPrintPreviewTickAsync()
|
||||
{
|
||||
if (!IsPrintPreviewExpanded || Entry == null || _printPreviewBusy) return;
|
||||
try
|
||||
{
|
||||
ApplySplitDetailsToEntry();
|
||||
var snap = JsonSerializer.Serialize(Entry, PreviewSnapshotJsonOpts);
|
||||
if (string.Equals(snap, _lastPreviewSnapshot, StringComparison.Ordinal)) return;
|
||||
_lastPreviewSnapshot = snap;
|
||||
_printPreviewBusy = true;
|
||||
PrintPreviewStatus = "更新预览中…";
|
||||
var myGen = ++_previewRequestGeneration;
|
||||
if (!await EnsureLocalPreviewTemplateLoadedAsync().ConfigureAwait(false))
|
||||
{
|
||||
PostPrintPreviewHtml(BuildPrintPreviewPlaceholderHtml("未找到本地打印模板缓存,请先联网完成一次“业务打印绑定/打印模板”同步。"));
|
||||
PrintPreviewStatus = "未找到本地模板缓存";
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_previewTemplateJson) || _previewTemplateJson == "{}")
|
||||
{
|
||||
PostPrintPreviewHtml(BuildPrintPreviewPlaceholderHtml("尚未配置业务打印绑定或模板内容为空"));
|
||||
PrintPreviewStatus = "未配置模板";
|
||||
return;
|
||||
}
|
||||
var dataObj = BuildLocalPreviewData(Entry);
|
||||
if (myGen != _previewRequestGeneration) return;
|
||||
string html;
|
||||
try
|
||||
{
|
||||
html = NativePrintRenderService.RenderToHtml(_previewTemplateJson, dataObj);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
html = BuildPrintPreviewErrorHtml(ex.Message);
|
||||
PrintPreviewStatus = "渲染失败";
|
||||
PostPrintPreviewHtml(html);
|
||||
return;
|
||||
}
|
||||
PostPrintPreviewHtml(html);
|
||||
PrintPreviewStatus = string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PostPrintPreviewHtml(BuildPrintPreviewErrorHtml(ex.Message));
|
||||
PrintPreviewStatus = "预览异常";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_printPreviewBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void PostPrintPreviewHtml(string html)
|
||||
{
|
||||
var app = Application.Current;
|
||||
if (app?.Dispatcher == null)
|
||||
{
|
||||
PrintPreviewHtmlReady?.Invoke(this, html);
|
||||
return;
|
||||
}
|
||||
if (app.Dispatcher.CheckAccess()) PrintPreviewHtmlReady?.Invoke(this, html);
|
||||
else app.Dispatcher.Invoke(() => PrintPreviewHtmlReady?.Invoke(this, html));
|
||||
}
|
||||
|
||||
private static string BuildPrintPreviewErrorHtml(string message)
|
||||
{
|
||||
var esc = WebUtility.HtmlEncode(message ?? string.Empty);
|
||||
return "<html><head><meta charset=\"utf-8\"/><style>"
|
||||
+ "body{margin:0;background:#525659;display:flex;align-items:center;"
|
||||
+ "justify-content:center;height:100vh;font-family:'Microsoft YaHei',Arial,sans-serif;}"
|
||||
+ ".card{background:#fff;border-radius:8px;padding:28px 40px;text-align:center;"
|
||||
+ "box-shadow:0 6px 24px rgba(0,0,0,.4);max-width:520px;}"
|
||||
+ "</style></head><body><div class=\"card\">"
|
||||
+ "<div style=\"font-size:36px;margin-bottom:10px\">⚠️</div>"
|
||||
+ "<div style=\"font-size:13px;color:#e74c3c;word-break:break-all;\">预览:" + esc + "</div>"
|
||||
+ "</div></body></html>";
|
||||
}
|
||||
|
||||
private static string BuildPrintPreviewPlaceholderHtml(string tip)
|
||||
{
|
||||
var esc = WebUtility.HtmlEncode(tip ?? string.Empty);
|
||||
return "<html><head><meta charset=\"utf-8\"/><style>"
|
||||
+ "body{margin:0;background:#525659;display:flex;align-items:center;justify-content:center;"
|
||||
+ "height:100vh;font-family:'Microsoft YaHei',Arial,sans-serif;}"
|
||||
+ ".card{background:#fff;border-radius:8px;padding:32px 48px;text-align:center;}"
|
||||
+ "</style></head><body><div class=\"card\">"
|
||||
+ "<div style=\"font-size:14px;color:#888;\">" + esc + "</div></div></body></html>";
|
||||
}
|
||||
|
||||
private async Task<bool> EnsureLocalPreviewTemplateLoadedAsync()
|
||||
{
|
||||
if (_previewTemplateLoaded && !string.IsNullOrWhiteSpace(_previewTemplateJson))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 离线优先用本地缓存;若当前在线再尝试刷新缓存
|
||||
var bindList = _printBizTemplateBindService.GetCached();
|
||||
if (bindList.Count == 0)
|
||||
{
|
||||
try { bindList = await _printBizTemplateBindService.ListAsync().ConfigureAwait(false); } catch { /* 忽略 */ }
|
||||
}
|
||||
var bind = bindList.FirstOrDefault(x => string.Equals(x.BizCode, RawMaterialEntryBizCode, StringComparison.OrdinalIgnoreCase))
|
||||
?? bindList.FirstOrDefault(x => string.Equals(x.TemplateCode, RawMaterialEntryTemplateCode, StringComparison.OrdinalIgnoreCase))
|
||||
?? bindList.FirstOrDefault(x =>
|
||||
(x.BizName ?? string.Empty).Contains("原料入场记录", StringComparison.OrdinalIgnoreCase)
|
||||
|| (x.BizName ?? string.Empty).Contains("原材料流转卡片", StringComparison.OrdinalIgnoreCase));
|
||||
if (bind == null) return false;
|
||||
|
||||
_previewFieldMappingJson = string.IsNullOrWhiteSpace(bind.FieldMappingJson) ? "[]" : bind.FieldMappingJson!;
|
||||
_previewTemplateCode = string.IsNullOrWhiteSpace(bind.TemplateCode) ? RawMaterialEntryTemplateCode : bind.TemplateCode!;
|
||||
|
||||
var templates = _printTemplateService.GetCached();
|
||||
if (templates.Count == 0)
|
||||
{
|
||||
try { templates = await _printTemplateService.ListAsync().ConfigureAwait(false); } catch { /* 忽略 */ }
|
||||
}
|
||||
var tpl = templates.FirstOrDefault(t => !string.IsNullOrWhiteSpace(bind.TemplateId) && string.Equals(t.Id, bind.TemplateId, StringComparison.OrdinalIgnoreCase))
|
||||
?? templates.FirstOrDefault(t => string.Equals(t.TemplateCode, _previewTemplateCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (tpl == null || string.IsNullOrWhiteSpace(tpl.TemplateJson)) return false;
|
||||
|
||||
_previewTemplateJson = tpl.TemplateJson!;
|
||||
_previewTemplateName = string.IsNullOrWhiteSpace(tpl.TemplateName) ? "原料入场记录" : tpl.TemplateName!;
|
||||
_previewTemplateLoaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private JsonObject BuildLocalPreviewData(MesXslRawMaterialEntry entry)
|
||||
{
|
||||
JsonObject printData = new();
|
||||
JsonNode? bizRoot = JsonSerializer.SerializeToNode(entry, PreviewSnapshotJsonOpts);
|
||||
try
|
||||
{
|
||||
var mappingNode = JsonNode.Parse(string.IsNullOrWhiteSpace(_previewFieldMappingJson) ? "[]" : _previewFieldMappingJson) as JsonArray;
|
||||
if (mappingNode != null)
|
||||
{
|
||||
foreach (var rule in mappingNode)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (rule is not JsonObject obj) continue;
|
||||
var templateField = obj["templateField"]?.GetValue<string>()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(templateField)) continue;
|
||||
var bizField = obj["bizField"]?.GetValue<string>()?.Trim();
|
||||
JsonNode? val = string.IsNullOrWhiteSpace(bizField)
|
||||
? JsonValue.Create(string.Empty)
|
||||
: ResolvePath(bizRoot, bizField!);
|
||||
// JsonNode 不能同时挂在两个父节点,必须深拷贝后再写入 printData
|
||||
var normalized = NormalizePreviewNodeValue(val);
|
||||
SetPath(printData, templateField!, normalized);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 单条映射异常不应影响其余字段,继续后续规则
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略 mapping JSON 异常,继续尝试按模板字段补空
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var root = JsonNode.Parse(_previewTemplateJson);
|
||||
var fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
CollectTemplateBindFields(root, fields);
|
||||
foreach (var key in fields)
|
||||
{
|
||||
if (!HasPath(printData, key))
|
||||
{
|
||||
SetPath(printData, key, JsonValue.Create(string.Empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 模板异常时忽略补齐
|
||||
}
|
||||
|
||||
return printData;
|
||||
}
|
||||
|
||||
private static void CollectTemplateBindFields(JsonNode? node, HashSet<string> fields)
|
||||
{
|
||||
if (node == null) return;
|
||||
if (node is JsonObject obj)
|
||||
{
|
||||
if (obj["dataBinding"] is JsonObject db && db["params"] is JsonArray paramsArr)
|
||||
{
|
||||
foreach (var p in paramsArr)
|
||||
{
|
||||
var key = p?["key"]?.GetValue<string>()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(key)) fields.Add(key!);
|
||||
}
|
||||
}
|
||||
var bindField = obj["bindField"]?.GetValue<string>()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(bindField)) fields.Add(bindField!);
|
||||
if (obj["columns"] is JsonArray cols)
|
||||
{
|
||||
foreach (var c in cols)
|
||||
{
|
||||
var cBind = c?["bindField"]?.GetValue<string>()?.Trim();
|
||||
var cField = c?["field"]?.GetValue<string>()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(cBind)) fields.Add(cBind!);
|
||||
else if (!string.IsNullOrWhiteSpace(cField)) fields.Add(cField!);
|
||||
}
|
||||
}
|
||||
foreach (var kv in obj) CollectTemplateBindFields(kv.Value, fields);
|
||||
return;
|
||||
}
|
||||
if (node is JsonArray arr)
|
||||
{
|
||||
foreach (var item in arr) CollectTemplateBindFields(item, fields);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasPath(JsonObject root, string path)
|
||||
{
|
||||
var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries);
|
||||
JsonNode? cur = root;
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (cur is not JsonObject obj) return false;
|
||||
if (!obj.TryGetPropertyValue(parts[i], out cur)) return false;
|
||||
if (i == parts.Length - 1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static JsonNode? ResolvePath(JsonNode? root, string path)
|
||||
{
|
||||
if (root == null || string.IsNullOrWhiteSpace(path)) return null;
|
||||
var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries);
|
||||
JsonNode? cur = root;
|
||||
foreach (var p in parts)
|
||||
{
|
||||
if (cur == null) return null;
|
||||
if (cur is JsonArray arr)
|
||||
{
|
||||
if (int.TryParse(p, out var idx))
|
||||
{
|
||||
cur = idx >= 0 && idx < arr.Count ? arr[idx] : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
cur = arr.Count > 0 ? arr[0]?[p] : null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cur = cur[p];
|
||||
}
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地实时预览与后端保持一致:DateTime/DateTimeOffset 统一输出 yyyy-MM-dd HH:mm:ss(不带时区后缀)。
|
||||
/// </summary>
|
||||
private static JsonNode NormalizePreviewNodeValue(JsonNode? node)
|
||||
{
|
||||
if (node == null) return JsonValue.Create(string.Empty)!;
|
||||
|
||||
if (node is JsonValue v)
|
||||
{
|
||||
if (v.TryGetValue<DateTime>(out var dt))
|
||||
{
|
||||
return JsonValue.Create(dt.ToString("yyyy-MM-dd HH:mm:ss"))!;
|
||||
}
|
||||
if (v.TryGetValue<DateTimeOffset>(out var dto))
|
||||
{
|
||||
return JsonValue.Create(dto.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))!;
|
||||
}
|
||||
if (v.TryGetValue<string>(out var s) && !string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
// 仅处理明显的 ISO 时间串,避免误伤普通文本
|
||||
if ((s.Contains('T') || s.EndsWith("Z", StringComparison.OrdinalIgnoreCase) || s.Contains('+'))
|
||||
&& DateTimeOffset.TryParse(s, out var parsed))
|
||||
{
|
||||
return JsonValue.Create(parsed.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node.DeepClone();
|
||||
}
|
||||
|
||||
private static void SetPath(JsonObject target, string path, JsonNode value)
|
||||
{
|
||||
var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0) return;
|
||||
JsonObject cur = target;
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
if (cur[parts[i]] is not JsonObject child)
|
||||
{
|
||||
child = new JsonObject();
|
||||
cur[parts[i]] = child;
|
||||
}
|
||||
cur = child;
|
||||
}
|
||||
cur[parts[^1]] = value;
|
||||
}
|
||||
|
||||
private void LoadLayoutState()
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user