From f0c14d8a4b4aa410e007a68a560fc6617b3a9b91 Mon Sep 17 00:00:00 2001
From: geht <2947093423@qq.com>
Date: Thu, 14 May 2026 15:52:00 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8E=9F=E6=9D=90=E6=96=99?=
=?UTF-8?q?=E5=8D=A1=E7=89=87=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91=EF=BC=8C?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E6=89=93=E5=8D=B0=E9=A2=84=E8=A7=88=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E5=8D=A1=E7=89=87=E6=A8=A1?=
=?UTF-8?q?=E6=9D=BF=E7=9A=84=E5=8A=A0=E8=BD=BD=E5=92=8C=E6=89=93=E5=8D=B0?=
=?UTF-8?q?=E3=80=82=E9=87=8D=E6=9E=84=E7=9B=B8=E5=85=B3=E6=96=B9=E6=B3=95?=
=?UTF-8?q?=E4=BB=A5=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?=
=?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=94=9F=E6=88=90=E7=9A=84=E5=8D=A1?=
=?UTF-8?q?=E7=89=87=E6=95=B0=E6=8D=AE=E6=9C=89=E6=95=88=EF=BC=8C=E5=B9=B6?=
=?UTF-8?q?=E5=A4=84=E7=90=86=E6=89=93=E5=8D=B0=E5=BC=82=E5=B8=B8=E6=83=85?=
=?UTF-8?q?=E5=86=B5=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../RawMaterialEntryOperationViewModel.cs | 288 ++++++++++++-
.../RawMaterialCardGenerateConfirmWindow.xaml | 184 +++++++++
...wMaterialCardGenerateConfirmWindow.xaml.cs | 389 ++++++++++++++++++
3 files changed, 855 insertions(+), 6 deletions(-)
create mode 100644 yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml
create mode 100644 yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml.cs
diff --git a/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs
index c38b8fa..7e36603 100644
--- a/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs
+++ b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs
@@ -11,6 +11,7 @@ using YY.Admin.Core.Services;
using YY.Admin.Infrastructure.Print;
using YY.Admin.Services.Service;
using YY.Admin.Services.Service.Print;
+using YY.Admin.Views.RawMaterialEntry;
namespace YY.Admin.ViewModels.RawMaterialEntry;
@@ -22,6 +23,7 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
private const int TodayListFetchSize = 5000;
private const string RawMaterialEntryBizCode = "1900000000000000530";
private const string RawMaterialEntryTemplateCode = "MES_RAW_MATERIAL_ENTRY";
+ private const string RawMaterialCardTemplateCode = "MES_RAW_MATERIAL_CARD";
private readonly IRawMaterialCardService _rawMaterialCardService;
private readonly IPrintDotService _printDotService;
private readonly IPrintBizTemplateBindService _printBizTemplateBindService;
@@ -151,6 +153,11 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
private string _previewTemplateName = "原料入场记录";
private string _previewTemplateCode = RawMaterialEntryTemplateCode;
private string _previewFieldMappingJson = "[]";
+ private bool _rawMaterialCardTemplateLoaded;
+ private string _rawMaterialCardTemplateJson = string.Empty;
+ private string _rawMaterialCardTemplateName = "原材料卡片";
+ private string _rawMaterialCardTemplateCode = RawMaterialCardTemplateCode;
+ private string _rawMaterialCardFieldMappingJson = "[]";
private bool _isPrintPreviewExpanded = true;
/// 右侧下方「入场标签打印预览」折叠面板是否展开。
@@ -694,18 +701,58 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
}
}
+ // 续号起点:已有卡片数 = Σ HasCard==true 行的 Portions;新增卡片从其后续编
+ var alreadyGenerated = SplitCodeDetails
+ .Where(d => d.HasCard)
+ .Sum(d => d.Portions ?? 0);
+ var plannedCards = BuildPlannedRawMaterialCards(Entry, pendingRows, alreadyGenerated + 1);
+ if (plannedCards.Count == 0)
+ {
+ HandyControl.Controls.MessageBox.Warning("未生成有效的原材料卡片预览数据,请检查拆码明细后重试。");
+ return;
+ }
+
+ if (!await EnsureRawMaterialCardTemplateLoadedAsync())
+ {
+ HandyControl.Controls.MessageBox.Warning("未找到原材料卡片打印模板,请先同步“业务打印绑定/打印模板”后再试。");
+ return;
+ }
+
+ var confirmWindow = new RawMaterialCardGenerateConfirmWindow(
+ plannedCards,
+ _rawMaterialCardTemplateName,
+ _rawMaterialCardTemplateCode,
+ _printDotService,
+ row =>
+ {
+ var matched = plannedCards.FirstOrDefault(p =>
+ string.Equals(p.DetailId, row.DetailId, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(p.Card.Barcode, row.Card.Barcode, StringComparison.OrdinalIgnoreCase));
+ var target = matched ?? plannedCards[Math.Clamp(row.Index - 1, 0, plannedCards.Count - 1)];
+ return BuildRawMaterialCardPreviewHtml(target);
+ })
+ {
+ Owner = Application.Current.MainWindow,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner
+ };
+ var confirmed = confirmWindow.ShowDialog() == true;
+ if (!confirmed) return;
+ var selectedPrinterName = confirmWindow.SelectedPrinterName;
+ if (string.IsNullOrWhiteSpace(selectedPrinterName))
+ {
+ HandyControl.Controls.MessageBox.Warning("未选择打印机,已取消打印。");
+ return;
+ }
+
BeginActionBusy("生成中...");
try
{
IsLoading = true;
- // 续号起点:已有卡片数 = Σ HasCard==true 行的 Portions;新增卡片从其后续编
- var alreadyGenerated = SplitCodeDetails
- .Where(d => d.HasCard)
- .Sum(d => d.Portions ?? 0);
var globalIndex = alreadyGenerated + 1;
var baseBarcode = Entry.Barcode ?? "";
var failCount = 0;
var newCardCount = 0;
+ var generatedCards = new List();
// 按行收集成功标记:只要该行所有份都加卡成功,行 HasCard=true;
// 中途任一份失败则保留 HasCard=false,下次再点「生成」时会重试该行
var rowSuccessMap = new Dictionary();
@@ -740,7 +787,11 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
TenantId = Entry.TenantId
};
var ok = await _rawMaterialCardService.AddAsync(card);
- if (ok) newCardCount++;
+ if (ok)
+ {
+ newCardCount++;
+ generatedCards.Add(card);
+ }
else { failCount++; rowAllOk = false; }
globalIndex++;
}
@@ -769,8 +820,17 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
RaisePropertyChanged(nameof(CanGenerateCards));
RaisePropertyChanged(nameof(CanResplit));
+ if (newCardCount > 0 && generatedCards.Count > 0)
+ {
+ var printError = await PrintGeneratedRawMaterialCardsAsync(selectedPrinterName, generatedCards);
+ if (!string.IsNullOrWhiteSpace(printError))
+ {
+ HandyControl.Controls.MessageBox.Warning($"卡片已生成 {newCardCount} 张,但打印存在异常:{printError}");
+ }
+ }
+
if (failCount == 0)
- HandyControl.Controls.MessageBox.Success($"已生成 {newCardCount} 张原材料卡片(累计 {alreadyGenerated + newCardCount} 张),打印状态已更新为「已打印」!");
+ HandyControl.Controls.MessageBox.Success($"已生成并打印 {newCardCount} 张原材料卡片(累计 {alreadyGenerated + newCardCount} 张),打印状态已更新为「已打印」!");
else
HandyControl.Controls.MessageBox.Warning($"本次共尝试生成 {newCardCount + failCount} 张,成功 {newCardCount} 张,失败 {failCount} 张。失败的行未标记为「已打印」,可检查网络后再次点击「生成原材料卡片」重试。");
}
@@ -785,6 +845,215 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
}
}
+ private List BuildPlannedRawMaterialCards(
+ MesXslRawMaterialEntry entry,
+ IReadOnlyList pendingRows,
+ int startIndex)
+ {
+ var list = new List();
+ var cursor = startIndex;
+ var baseBarcode = entry.Barcode ?? string.Empty;
+ foreach (var detail in pendingRows)
+ {
+ var portions = detail.Portions ?? 0;
+ for (var i = 0; i < portions; i++)
+ {
+ var card = BuildRawMaterialCardFromDetail(entry, detail, baseBarcode + cursor.ToString("D3"));
+ list.Add(new RawMaterialCardGeneratePlanItem
+ {
+ Card = card,
+ DetailId = detail.Id,
+ SourceRowNo = SplitCodeDetails.IndexOf(detail) + 1
+ });
+ cursor++;
+ }
+ }
+ return list;
+ }
+
+ private static MesXslRawMaterialCard BuildRawMaterialCardFromDetail(
+ MesXslRawMaterialEntry entry,
+ RawMaterialSplitDetailItem detail,
+ string barcode)
+ {
+ return new MesXslRawMaterialCard
+ {
+ SplitDetailId = detail.Id,
+ Barcode = barcode,
+ 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
+ };
+ }
+
+ private async Task EnsureRawMaterialCardTemplateLoadedAsync()
+ {
+ if (_rawMaterialCardTemplateLoaded && !string.IsNullOrWhiteSpace(_rawMaterialCardTemplateJson))
+ {
+ 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.TemplateCode, RawMaterialCardTemplateCode, StringComparison.OrdinalIgnoreCase))
+ ?? bindList.FirstOrDefault(x => (x.BizName ?? string.Empty).Contains("原材料卡片", StringComparison.OrdinalIgnoreCase));
+ if (bind == null) return false;
+
+ _rawMaterialCardFieldMappingJson = string.IsNullOrWhiteSpace(bind.FieldMappingJson) ? "[]" : bind.FieldMappingJson!;
+ _rawMaterialCardTemplateCode = string.IsNullOrWhiteSpace(bind.TemplateCode) ? RawMaterialCardTemplateCode : 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, _rawMaterialCardTemplateCode, StringComparison.OrdinalIgnoreCase));
+ if (tpl == null || string.IsNullOrWhiteSpace(tpl.TemplateJson)) return false;
+
+ _rawMaterialCardTemplateJson = tpl.TemplateJson!;
+ _rawMaterialCardTemplateName = string.IsNullOrWhiteSpace(tpl.TemplateName) ? "原材料卡片" : tpl.TemplateName!;
+ _rawMaterialCardTemplateLoaded = true;
+ return true;
+ }
+
+ private string BuildRawMaterialCardPreviewHtml(RawMaterialCardGeneratePlanItem planItem)
+ {
+ try
+ {
+ var data = BuildRawMaterialCardPrintData(planItem.Card);
+ return NativePrintRenderService.RenderToHtml(_rawMaterialCardTemplateJson, data, enableScreenAutoFit: false);
+ }
+ catch (Exception ex)
+ {
+ return BuildPrintPreviewErrorHtml($"模板预览失败:{ex.Message}");
+ }
+ }
+
+ private JsonObject BuildRawMaterialCardPrintData(MesXslRawMaterialCard card)
+ {
+ JsonObject printData = new();
+ JsonNode? bizRoot = JsonSerializer.SerializeToNode(card, PreviewSnapshotJsonOpts);
+ try
+ {
+ var mappingNode = JsonNode.Parse(string.IsNullOrWhiteSpace(_rawMaterialCardFieldMappingJson) ? "[]" : _rawMaterialCardFieldMappingJson) as JsonArray;
+ if (mappingNode != null)
+ {
+ foreach (var rule in mappingNode)
+ {
+ if (rule is not JsonObject obj) continue;
+ var templateField = obj["templateField"]?.GetValue()?.Trim();
+ if (string.IsNullOrWhiteSpace(templateField)) continue;
+ var bizField = obj["bizField"]?.GetValue()?.Trim();
+ JsonNode? val = string.IsNullOrWhiteSpace(bizField)
+ ? JsonValue.Create(string.Empty)
+ : ResolvePath(bizRoot, bizField!);
+ var normalized = NormalizePreviewNodeValue(val);
+ SetPath(printData, templateField!, normalized);
+ }
+ }
+ }
+ catch
+ {
+ // 映射失败时降级为模板字段补空,不中断后续流程
+ }
+
+ try
+ {
+ var root = JsonNode.Parse(_rawMaterialCardTemplateJson);
+ var fields = new HashSet(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 async Task PrintGeneratedRawMaterialCardsAsync(string printerName, IReadOnlyList cards)
+ {
+ if (cards.Count == 0) return null;
+ if (string.IsNullOrWhiteSpace(printerName)) return "未选择打印机";
+ if (!await EnsureRawMaterialCardTemplateLoadedAsync()) return "未找到原材料卡片打印模板";
+
+ var tpl = BuildPrintTemplateForRawMaterialCard(_rawMaterialCardTemplateJson);
+ try
+ {
+ var idx = 0;
+ foreach (var card in cards)
+ {
+ idx++;
+ BeginActionBusy($"打印中({idx}/{cards.Count})...");
+ var data = BuildRawMaterialCardPrintData(card);
+ var html = NativePrintRenderService.RenderToHtml(_rawMaterialCardTemplateJson, data);
+ var pdfBase64 = await HtmlToPdfRenderer.RenderAsync(
+ html,
+ tpl.PaperWidthMm ?? 210d,
+ tpl.PaperHeightMm ?? 297d);
+ var jobName = string.IsNullOrWhiteSpace(card.Barcode) ? "原材料卡片" : $"原材料卡片-{card.Barcode}";
+ await _printDotService.PrintAsync(printerName, pdfBase64, jobName, 1);
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex.Message;
+ }
+ }
+
+ private static PrintTemplate BuildPrintTemplateForRawMaterialCard(string? templateJson)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(templateJson) || templateJson == "{}")
+ return new PrintTemplate { TemplateName = "原材料卡片", TemplateCode = RawMaterialCardTemplateCode };
+ 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 = RawMaterialCardTemplateCode,
+ PaperWidthMm = w,
+ PaperHeightMm = h,
+ PaperOrientation = w > h ? "横向" : "纵向",
+ };
+ }
+ catch
+ {
+ return new PrintTemplate { TemplateName = "原材料卡片", TemplateCode = RawMaterialCardTemplateCode };
+ }
+ }
+
private void BeginActionBusy(string text)
{
ActionBusyText = string.IsNullOrWhiteSpace(text) ? "处理中..." : text;
@@ -1152,4 +1421,11 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
public double? ExpandedWidth { get; set; }
public bool? IsExpanded { get; set; }
}
+
+ public sealed class RawMaterialCardGeneratePlanItem
+ {
+ public required MesXslRawMaterialCard Card { get; init; }
+ public required string DetailId { get; init; }
+ public int SourceRowNo { get; init; }
+ }
}
diff --git a/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml
new file mode 100644
index 0000000..ef6abc6
--- /dev/null
+++ b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml.cs b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml.cs
new file mode 100644
index 0000000..21f01d0
--- /dev/null
+++ b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml.cs
@@ -0,0 +1,389 @@
+using System.Collections.ObjectModel;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls.Primitives;
+using YY.Admin.Core.Services;
+using YY.Admin.ViewModels.RawMaterialEntry;
+
+namespace YY.Admin.Views.RawMaterialEntry;
+
+///
+/// 需显式实现 ,否则 WPF 绑定不会订阅 PropertyChanged,
+/// 界面会一直停留在属性初始值(例如 PrinterStatus 的「加载打印机中…」)。
+///
+public partial class RawMaterialCardGenerateConfirmWindow : HandyControl.Controls.Window, INotifyPropertyChanged
+{
+ private const int PrinterLoadTimeoutMs = 8000;
+ private const double DefaultLeftRatio = 0.7d;
+ private const double MinLeftRatio = 0.2d;
+ private const double MaxLeftRatio = 0.8d;
+ private static readonly JsonSerializerOptions LayoutJsonOpts = new() { WriteIndented = true };
+ private static string LayoutFilePath => Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "YY.Admin",
+ "raw-material-card-generate-confirm-layout.json");
+
+ public ObservableCollection PlanItems { get; } = new();
+
+ public string HeaderText { get; }
+ public string TemplateText { get; }
+ public ObservableCollection Printers { get; } = new();
+ private readonly Func _previewHtmlBuilder;
+ private readonly IPrintDotService _printDotService;
+ private bool _webViewReady;
+ private bool _isRefreshingPrinters;
+ private bool _suppressPrinterSave;
+ private string? _preferredPrinterNameOnLoad;
+
+ private string _printerStatus = "加载打印机中...";
+ public string PrinterStatus
+ {
+ get => _printerStatus;
+ set
+ {
+ if (string.Equals(_printerStatus, value, StringComparison.Ordinal)) return;
+ _printerStatus = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PrinterStatus)));
+ }
+ }
+
+ private PrintDotPrinter? _selectedPrinter;
+ public PrintDotPrinter? SelectedPrinter
+ {
+ get => _selectedPrinter;
+ set
+ {
+ if (ReferenceEquals(_selectedPrinter, value)) return;
+ _selectedPrinter = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedPrinter)));
+ if (_suppressPrinterSave) return;
+ SaveLayout(dto => dto.SelectedPrinterName = value?.Name ?? string.Empty);
+ }
+ }
+
+ public string SelectedPrinterName => SelectedPrinter?.Name?.Trim() ?? string.Empty;
+
+ private RawMaterialCardGeneratePlanRow? _selectedPlanItem;
+ public RawMaterialCardGeneratePlanRow? SelectedPlanItem
+ {
+ get => _selectedPlanItem;
+ set
+ {
+ if (ReferenceEquals(_selectedPlanItem, value)) return;
+ _selectedPlanItem = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedPlanItem)));
+ }
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public RawMaterialCardGenerateConfirmWindow(
+ IReadOnlyList planItems,
+ string templateName,
+ string templateCode,
+ IPrintDotService printDotService,
+ Func previewHtmlBuilder)
+ {
+ InitializeComponent();
+ _printDotService = printDotService;
+ _previewHtmlBuilder = previewHtmlBuilder;
+ HeaderText = $"共 {planItems.Count} 张,左侧展示即将生成的原材料卡片,右侧展示业务关联打印模板预览。";
+ TemplateText = $"模板:{templateName}({templateCode})";
+ var idx = 0;
+ foreach (var item in planItems)
+ {
+ idx++;
+ PlanItems.Add(new RawMaterialCardGeneratePlanRow
+ {
+ Index = idx,
+ SourceRowNo = item.SourceRowNo,
+ DetailId = item.DetailId,
+ Card = item.Card
+ });
+ }
+
+ DataContext = this;
+ Loaded += OnLoadedAsync;
+ Closing += OnClosing;
+ }
+
+ private async void OnLoadedAsync(object? sender, RoutedEventArgs e)
+ {
+ var layout = LoadSavedLayout();
+ ApplyPaneRatio(layout.LeftPaneRatio ?? DefaultLeftRatio);
+ ApplySavedColumnWidths(layout.ColumnWidths);
+ _preferredPrinterNameOnLoad = layout.SelectedPrinterName;
+ await RefreshPrintersAsync(verbose: false);
+ try
+ {
+ await PreviewWebView.EnsureCoreWebView2Async();
+ _webViewReady = true;
+ if (SelectedPlanItem == null && PlanItems.Count > 0)
+ {
+ SelectedPlanItem = PlanItems[0];
+ }
+ RenderSelectedPreview();
+ }
+ catch
+ {
+ PreviewWebView.NavigateToString("模板预览加载失败");
+ }
+ }
+
+ private async void RefreshPrintersButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ await RefreshPrintersAsync(verbose: true);
+ }
+
+ private void MainSplitter_OnDragCompleted(object sender, DragCompletedEventArgs e)
+ {
+ SavePaneRatio(GetCurrentLeftRatio());
+ }
+
+ private double GetCurrentLeftRatio()
+ {
+ var left = LeftPaneCol.ActualWidth;
+ var right = RightPaneCol.ActualWidth;
+ var total = left + right;
+ if (total <= 0.1d) return DefaultLeftRatio;
+ return Math.Clamp(left / total, MinLeftRatio, MaxLeftRatio);
+ }
+
+ private void ApplyPaneRatio(double ratio)
+ {
+ var leftRatio = Math.Clamp(ratio, MinLeftRatio, MaxLeftRatio);
+ LeftPaneCol.Width = new GridLength(leftRatio, GridUnitType.Star);
+ RightPaneCol.Width = new GridLength(1d - leftRatio, GridUnitType.Star);
+ }
+
+ private static ConfirmWindowLayoutDto LoadSavedLayout()
+ {
+ try
+ {
+ if (!File.Exists(LayoutFilePath)) return new ConfirmWindowLayoutDto();
+ var json = File.ReadAllText(LayoutFilePath);
+ var dto = JsonSerializer.Deserialize(json, LayoutJsonOpts);
+ if (dto is null) return new ConfirmWindowLayoutDto();
+ if (dto.LeftPaneRatio is > 0 and < 1)
+ dto.LeftPaneRatio = Math.Clamp(dto.LeftPaneRatio.Value, MinLeftRatio, MaxLeftRatio);
+ return dto;
+ }
+ catch
+ {
+ // 布局缓存异常时使用默认比例,不影响主流程
+ }
+ return new ConfirmWindowLayoutDto();
+ }
+
+ private static void SavePaneRatio(double leftRatio)
+ {
+ SaveLayout(dto => { dto.LeftPaneRatio = Math.Clamp(leftRatio, MinLeftRatio, MaxLeftRatio); });
+ }
+
+ private static void SaveLayout(Action mutator)
+ {
+ try
+ {
+ var dir = Path.GetDirectoryName(LayoutFilePath);
+ if (!string.IsNullOrWhiteSpace(dir))
+ {
+ Directory.CreateDirectory(dir);
+ }
+
+ var dto = LoadSavedLayout();
+ mutator(dto);
+ File.WriteAllText(LayoutFilePath, JsonSerializer.Serialize(dto, LayoutJsonOpts));
+ }
+ catch
+ {
+ // 写入失败不阻断页面交互
+ }
+ }
+
+ private void ApplySavedColumnWidths(Dictionary? columnWidths)
+ {
+ if (columnWidths is null || columnWidths.Count == 0) return;
+ for (var index = 0; index < PlanGrid.Columns.Count; index++)
+ {
+ var column = PlanGrid.Columns[index];
+ var key = GetColumnWidthKey(column, index);
+ if (!columnWidths.TryGetValue(key, out var cachedWidth) || cachedWidth <= 0) continue;
+ var minWidth = column.MinWidth > 0 ? column.MinWidth : 40d;
+ column.Width = new System.Windows.Controls.DataGridLength(
+ Math.Max(cachedWidth, minWidth),
+ System.Windows.Controls.DataGridLengthUnitType.Pixel);
+ }
+ }
+
+ private void SaveCurrentColumnWidths()
+ {
+ if (PlanGrid.Columns.Count == 0) return;
+ var widths = new Dictionary(StringComparer.Ordinal);
+ for (var index = 0; index < PlanGrid.Columns.Count; index++)
+ {
+ var column = PlanGrid.Columns[index];
+ var width = column.ActualWidth;
+ if (double.IsNaN(width) || double.IsInfinity(width) || width <= 0) continue;
+ widths[GetColumnWidthKey(column, index)] = width;
+ }
+
+ if (widths.Count == 0) return;
+ SaveLayout(dto => dto.ColumnWidths = widths);
+ }
+
+ private static string GetColumnWidthKey(System.Windows.Controls.DataGridColumn column, int index)
+ {
+ var header = column.Header?.ToString();
+ if (string.IsNullOrWhiteSpace(header))
+ return $"col-{index.ToString(CultureInfo.InvariantCulture)}";
+ return $"col-{index.ToString(CultureInfo.InvariantCulture)}-{header.Trim()}";
+ }
+
+ private void OnClosing(object? sender, CancelEventArgs e)
+ {
+ SavePaneRatio(GetCurrentLeftRatio());
+ SaveCurrentColumnWidths();
+ }
+
+ private async Task RefreshPrintersAsync(bool verbose)
+ {
+ if (_isRefreshingPrinters) return;
+ _isRefreshingPrinters = true;
+ const string loadingText = "加载打印机中...";
+ PrinterStatus = verbose ? "刷新打印机中..." : loadingText;
+ try
+ {
+ using var cts = new CancellationTokenSource();
+ var fetchTask = _printDotService.GetPrintersAsync(cts.Token);
+ var timeoutTask = Task.Delay(PrinterLoadTimeoutMs);
+ var completedTask = await Task.WhenAny(fetchTask, timeoutTask);
+ if (!ReferenceEquals(completedTask, fetchTask))
+ {
+ cts.Cancel();
+ throw new TimeoutException("获取打印机列表超时");
+ }
+
+ var list = await fetchTask;
+ Printers.Clear();
+ foreach (var printer in list) Printers.Add(printer);
+ // 先更新状态,避免后续本地缓存逻辑异常导致“加载中”残留
+ PrinterStatus = list.Count > 0 ? $"共 {list.Count} 台打印机" : "未检测到打印机";
+
+ var preferred = _preferredPrinterNameOnLoad;
+ if (string.IsNullOrWhiteSpace(preferred))
+ {
+ preferred = SelectedPrinter?.Name;
+ }
+
+ var match = list.FirstOrDefault(p => string.Equals(p.Name, preferred, StringComparison.OrdinalIgnoreCase))
+ ?? list.FirstOrDefault(p => p.IsDefault)
+ ?? list.FirstOrDefault();
+
+ _suppressPrinterSave = true;
+ SelectedPrinter = match;
+ _suppressPrinterSave = false;
+
+ if (match is not null)
+ {
+ SaveLayout(dto => dto.SelectedPrinterName = match.Name);
+ }
+ _preferredPrinterNameOnLoad = null;
+ }
+ catch (OperationCanceledException)
+ {
+ Printers.Clear();
+ _suppressPrinterSave = true;
+ SelectedPrinter = null;
+ _suppressPrinterSave = false;
+ PrinterStatus = "加载打印机超时,请检查 PrintDot 后重试";
+ }
+ catch (TimeoutException)
+ {
+ Printers.Clear();
+ _suppressPrinterSave = true;
+ SelectedPrinter = null;
+ _suppressPrinterSave = false;
+ PrinterStatus = "加载打印机超时,请检查 PrintDot 后重试";
+ }
+ catch (Exception ex)
+ {
+ Printers.Clear();
+ _suppressPrinterSave = true;
+ SelectedPrinter = null;
+ _suppressPrinterSave = false;
+ PrinterStatus = verbose ? $"打印机连接失败:{ex.Message}" : "打印机连接失败";
+ }
+ finally
+ {
+ // 兜底:如果实际有打印机但状态仍是“加载中”,强制纠正状态文本
+ if (string.Equals(PrinterStatus, loadingText, StringComparison.Ordinal) && Printers.Count > 0)
+ {
+ PrinterStatus = $"共 {Printers.Count} 台打印机";
+ }
+ _isRefreshingPrinters = false;
+ }
+ }
+
+ private void PlanGrid_OnSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
+ {
+ RenderSelectedPreview();
+ }
+
+ private void RenderSelectedPreview()
+ {
+ if (!_webViewReady) return;
+ if (SelectedPlanItem == null)
+ {
+ PreviewWebView.NavigateToString("请先选择左侧卡片记录");
+ return;
+ }
+
+ try
+ {
+ var html = _previewHtmlBuilder.Invoke(SelectedPlanItem);
+ PreviewWebView.NavigateToString(html);
+ }
+ catch
+ {
+ PreviewWebView.NavigateToString("模板预览加载失败");
+ }
+ }
+
+ private void CancelButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void ConfirmButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (string.IsNullOrWhiteSpace(SelectedPrinterName))
+ {
+ HandyControl.Controls.MessageBox.Warning("请先在当前弹窗选择打印机,再执行“生成并打印”。");
+ return;
+ }
+ DialogResult = true;
+ Close();
+ }
+}
+
+internal sealed class ConfirmWindowLayoutDto
+{
+ public double? LeftPaneRatio { get; set; }
+ public Dictionary? ColumnWidths { get; set; }
+ public string? SelectedPrinterName { get; set; }
+}
+
+public sealed class RawMaterialCardGeneratePlanRow
+{
+ public int Index { get; init; }
+ public int SourceRowNo { get; init; }
+ public required string DetailId { get; init; }
+ public required YY.Admin.Core.Entity.MesXslRawMaterialCard Card { get; init; }
+}