using HandyControl.Controls;
using HandyControl.Data;
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;
using YY.Admin.Views.RawMaterialEntry;
namespace YY.Admin.ViewModels.RawMaterialEntry;
///
/// 「新增原料入场记录」独立页面:左侧表单逻辑继承编辑 VM,右侧展示当日入场简要列表并支持选中回填模板。
///
public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel
{
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;
private readonly IPrintTemplateService _printTemplateService;
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 TodayEntries { get; } = new();
public IReadOnlyList DateRangeOptions { get; } =
["今日", "过去24小时", "过去48小时", "过去72小时"];
private string _selectedDateRange = "今日";
public string SelectedDateRange
{
get => _selectedDateRange;
set
{
if (SetProperty(ref _selectedDateRange, value))
_ = LoadTodayEntriesAsync();
}
}
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;
/// 右侧面板展开时的目标宽度(像素),由 GridSplitter 拖拽结束时回写。
public double ExpandedRightPanelWidth
{
get => _expandedRightPanelWidth;
private set
{
var v = Math.Clamp(value, 200, 560);
if (SetProperty(ref _expandedRightPanelWidth, v))
SaveLayoutState();
}
}
///
/// 「生成原材料卡片」按钮可用条件:
/// 入场记录已保存(有 Id)且至少存在一行「未生成卡片 + 份数>0」的明细。
/// 已打印态下用户「继续拆码」新增了行,按钮自动重新可用;全部行都已生成卡片时不可用。
///
public bool CanGenerateCards =>
!string.IsNullOrWhiteSpace(Entry?.Id)
&& SplitCodeDetails.Any(d => !d.HasCard && (d.Portions ?? 0) > 0);
public DelegateCommand ToggleRightPanelCommand { get; }
public DelegateCommand RefreshTodayEntriesCommand { get; }
public DelegateCommand GenerateRawMaterialCardsCommand { get; }
/// 「重新拆码」:清除已生成的卡片 + 清空明细,仅在编辑态可用。
public DelegateCommand ResplitCommand { get; }
public DelegateCommand SaveAndPrintCommand { get; }
public DelegateCommand RefreshPrintersCommand { get; }
public DelegateCommand ZoomOutPrintPreviewCommand { get; }
public DelegateCommand ZoomInPrintPreviewCommand { get; }
public DelegateCommand ResetPrintPreviewZoomCommand { get; }
/// PrintDot 桥接器返回的打印机列表(与打印模板页一致)。
public ObservableCollection 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 _rawMaterialCardTemplateLoaded;
private string _rawMaterialCardTemplateJson = string.Empty;
private string _rawMaterialCardTemplateName = "原材料卡片";
private string _rawMaterialCardTemplateCode = RawMaterialCardTemplateCode;
private string _rawMaterialCardFieldMappingJson = "[]";
private bool _isPrintPreviewExpanded = true;
/// 右侧下方「入场标签打印预览」折叠面板是否展开。
public bool IsPrintPreviewExpanded
{
get => _isPrintPreviewExpanded;
set
{
if (!SetProperty(ref _isPrintPreviewExpanded, value)) return;
if (value) _lastPreviewSnapshot = string.Empty;
RaisePropertyChanged(nameof(IsPrintPreviewWebAreaVisible));
}
}
/// 右侧下方预览区是否显示 WebView2 宿主(HandyControl 内嵌弹窗期间因 Airspace 临时隐藏)。
public bool IsPrintPreviewWebAreaVisible => !SuspendEmbeddedPrintPreviewAirspace && IsPrintPreviewExpanded;
protected override void OnSuspendEmbeddedPrintPreviewAirspaceChanged()
{
RaisePropertyChanged(nameof(IsPrintPreviewWebAreaVisible));
}
private string _printPreviewStatus = string.Empty;
/// 预览区状态提示(如离线、加载中、错误摘要)。
public string PrintPreviewStatus
{
get => _printPreviewStatus;
private set => SetProperty(ref _printPreviewStatus, value);
}
private const double PrintPreviewMinZoom = 0.5d;
private const double PrintPreviewMaxZoom = 2.0d;
private const double PrintPreviewDefaultZoom = 0.7d;
private const double PrintPreviewZoomStep = 0.1d;
private double _printPreviewZoomFactor = PrintPreviewDefaultZoom;
/// 打印预览缩放倍率(WebView2 ZoomFactor)。
public double PrintPreviewZoomFactor
{
get => _printPreviewZoomFactor;
set
{
var clamped = Math.Round(Math.Clamp(value, PrintPreviewMinZoom, PrintPreviewMaxZoom), 2);
if (!SetProperty(ref _printPreviewZoomFactor, clamped)) return;
RaisePropertyChanged(nameof(PrintPreviewZoomText));
}
}
/// 打印预览缩放显示文本(百分比)。
public string PrintPreviewZoomText => $"{Math.Round(PrintPreviewZoomFactor * 100):0}%";
/// 由 View 订阅,在 UI 线程将 HTML 交给 WebView2。
public event EventHandler? PrintPreviewHtmlReady;
private bool _isActionBusy;
/// 底部动作按钮(保存/保存并打印/生成卡片)是否处于执行中。
public bool IsActionBusy
{
get => _isActionBusy;
private set
{
if (SetProperty(ref _isActionBusy, value))
{
RaisePropertyChanged(nameof(IsNotActionBusy));
}
}
}
public bool IsNotActionBusy => !IsActionBusy;
private string _actionBusyText = "处理中...";
/// 动作执行中的遮罩提示文案。
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));
ZoomOutPrintPreviewCommand = new DelegateCommand(() => PrintPreviewZoomFactor -= PrintPreviewZoomStep);
ZoomInPrintPreviewCommand = new DelegateCommand(() => PrintPreviewZoomFactor += PrintPreviewZoomStep);
ResetPrintPreviewZoomCommand = new DelegateCommand(() => PrintPreviewZoomFactor = PrintPreviewDefaultZoom);
// 集合变化:批量重订阅 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();
}
///
/// 「重新拆码」按钮可用条件:入场记录已保存 + 至少有一行已生成卡片。
/// 全是新增未生成的行时,用户直接点「删除」即可,无需走「清空卡片」流程。
///
public bool CanResplit =>
!string.IsNullOrWhiteSpace(Entry?.Id)
&& SplitCodeDetails.Any(d => d.HasCard);
///
/// 集合变化时:对新增/移除的 item 维护 PropertyChanged 订阅,并刷新两个 Can* 属性。
/// HasCard / Portions 变化会让「生成原材料卡片」和「重新拆码」按钮可用性同步刷新。
///
private void OnSplitCodeDetailsCollectionChangedForCanFlags(
object? sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (RawMaterialSplitDetailItem o in e.OldItems)
o.PropertyChanged -= OnSplitDetailHasCardChanged;
}
if (e.NewItems != null)
{
foreach (RawMaterialSplitDetailItem n in e.NewItems)
n.PropertyChanged += OnSplitDetailHasCardChanged;
}
RaisePropertyChanged(nameof(CanGenerateCards));
RaisePropertyChanged(nameof(CanResplit));
ResplitCommand.RaiseCanExecuteChanged();
}
private void OnSplitDetailHasCardChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.HasCard)
or nameof(RawMaterialSplitDetailItem.Portions))
{
RaisePropertyChanged(nameof(CanGenerateCards));
RaisePropertyChanged(nameof(CanResplit));
ResplitCommand.RaiseCanExecuteChanged();
}
}
public override void InitializeForAdd()
{
_lastPreviewSnapshot = string.Empty;
_suppressTodaySelectionReaction = true;
_selectedTodayEntry = null;
RaisePropertyChanged(nameof(SelectedTodayEntry));
_suppressTodaySelectionReaction = false;
base.InitializeForAdd();
RaisePropertyChanged(nameof(CanGenerateCards));
}
/// 保存后按业务打印绑定拉取模板与 printData,直接发送到 PrintDot 打印机(不弹预览窗)。
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);
}
}
}
Growl.Success(new GrowlInfo
{
Message = "保存成功",
ShowDateTime = false,
WaitTime = 1
});
Result = true;
RaisePropertyChanged(nameof(CanGenerateCards));
await LoadTodayEntriesAsync();
InitializeForAdd();
}
catch (Exception ex)
{
HandyControl.Controls.MessageBox.Error($"保存或打印失败:{ex.Message}");
}
finally
{
IsLoading = false;
EndActionBusy();
}
}
/// 通过 PrintDot 桥接器拉取打印机列表(与打印模板页一致)。
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" };
}
}
/// 页面首次加载时拉取「今日」列表(由 View Loaded 调用)。
public async Task LoadTodayEntriesOnFirstShowAsync() => await LoadTodayEntriesAsync();
/// 用户在拖拽结束 GridSplitter 后提交实际列宽。
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);
DateTime? threshold = _selectedDateRange switch
{
"过去24小时" => DateTime.Now.AddHours(-24),
"过去48小时" => DateTime.Now.AddHours(-48),
"过去72小时" => DateTime.Now.AddHours(-72),
_ => null // "今日":按自然日过滤
};
var rows = result.Records
.Where(e => IsInRange(e, threshold))
.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 IsInRange(MesXslRawMaterialEntry e, DateTime? threshold)
{
if (threshold == null)
{
var today = DateTime.Today;
return (e.EntryTime?.Date == today) || (e.CreateTime?.Date == today);
}
return (e.EntryTime >= threshold) || (e.CreateTime >= threshold);
}
///
/// 点击右侧今日记录:直接以「编辑模式」加载源记录到左侧表单(保留 Id/条码/批次号),
/// 不再走「新增模板」逻辑,避免重新生成条码、覆盖原数据。
///
private async Task ApplyTodayRowToFormAsync(MesXslRawMaterialEntry src)
{
// 复用基类 InitializeForEdit:保留 Id/Barcode/BatchNo/状态等所有字段,标题自动切到「编辑原料入场记录」
base.InitializeForEdit(src);
RaisePropertyChanged(nameof(CanGenerateCards));
// 若物料列表此前未加载导致选中项未回填,则补一次拉取(与编辑弹窗逻辑一致)
if (_pendingMaterialId != null)
await LoadMaterialOptionsAsync();
_lastPreviewSnapshot = string.Empty;
}
///
/// 保存成功后:无论新增还是编辑,都立即刷新右侧「今日入场」列表。
/// 编辑成功额外把表单切回「新增态」,避免用户连续误改同一条;新增成功后由基类负责清空表单。
///
protected override async Task SaveAsync()
{
if (IsActionBusy) return;
BeginActionBusy("保存中...");
var wasEdit = !IsAddMode;
try
{
await base.SaveAsync();
RaisePropertyChanged(nameof(CanGenerateCards));
if (!Result || CloseAction != null)
{
return;
}
await LoadTodayEntriesAsync();
if (wasEdit)
{
Growl.Success(new GrowlInfo
{
Message = "保存成功",
ShowDateTime = false,
WaitTime = 1
});
InitializeForAdd();
}
}
finally
{
EndActionBusy();
}
}
///
/// 「重新拆码」:弹窗预提示「将清除 N 张原材料卡片」→ 确认 → 后端按 splitDetailId IN 批删
/// → 清空 SplitCodeDetails → Entry.PrintFlag=0 → EditAsync(Entry) 持久化新状态 → 刷新今日列表。
/// 流程对应「清空卡片重新生成」的业务诉求;用户可立即重新维护明细并再次「生成原材料卡片」。
///
private async Task ResplitAsync()
{
if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id))
{
HandyControl.Controls.MessageBox.Warning("请先选中一条已保存的入场记录后再「重新拆码」。");
return;
}
var detailIds = SplitCodeDetails
.Select(d => d.Id)
.Where(id => !string.IsNullOrWhiteSpace(id))
.Distinct()
.ToList();
try
{
IsLoading = true;
// dryRun 预查:统计当前明细行已生成的卡片数量
var cardCount = detailIds.Count > 0
? await _rawMaterialCardService.DeleteBySplitDetailIdsAsync(detailIds, dryRun: true)
: 0;
if (cardCount < 0)
{
HandyControl.Controls.MessageBox.Error("无法连接服务器,重新拆码已取消。");
return;
}
var confirm = HandyControl.Controls.MessageBox.Show(
$"当前入场记录已生成 {cardCount} 条原材料卡片,是否清空卡片重新生成?",
"重新拆码",
System.Windows.MessageBoxButton.OKCancel,
System.Windows.MessageBoxImage.Question);
if (confirm != System.Windows.MessageBoxResult.OK)
{
return;
}
// 真正执行批删(即使 cardCount=0 也调用一次,结果幂等)
if (cardCount > 0)
{
var deleted = await _rawMaterialCardService.DeleteBySplitDetailIdsAsync(detailIds, dryRun: false);
if (deleted < 0)
{
HandyControl.Controls.MessageBox.Error("清除已生成的原材料卡片失败,重新拆码已中止。");
return;
}
}
// 清空明细 + 重置打印标记,并把变更持久化到后端入场记录
SplitCodeDetails.Clear();
Entry.PrintFlag = "0";
ApplySplitDetailsToEntry(); // 同步把 PortionDetailIds 等清空到 Entry 上
var ok = await EntryService.EditAsync(Entry);
if (!ok)
{
HandyControl.Controls.MessageBox.Error("保存重新拆码后的入场记录失败,请刷新后重试。");
return;
}
RaisePropertyChanged(nameof(Entry));
RaisePropertyChanged(nameof(IsPrinted));
RaisePropertyChanged(nameof(IsTotalWeightEditable));
AddSplitDetailCommand.RaiseCanExecuteChanged();
RaisePropertyChanged(nameof(CanGenerateCards));
RaisePropertyChanged(nameof(CanResplit));
HandyControl.Controls.MessageBox.Success($"已清除 {cardCount} 条原材料卡片,请重新维护拆码明细。");
await LoadTodayEntriesAsync();
}
catch (Exception ex)
{
HandyControl.Controls.MessageBox.Error($"重新拆码失败:{ex.Message}");
}
finally
{
IsLoading = false;
}
}
///
/// 「生成原材料卡片」:仅处理 HasCard==false 的明细行(「继续拆码」流程的核心)。
/// 条码续号 = 已有卡片总数(Σ HasCard==true 行的 Portions)+ 1,避免与已生成卡片的条码冲突。
/// 成功生成的行置 HasCard=true,Entry.PrintFlag=1(整票级标记)。
///
private async Task GenerateRawMaterialCardsAsync()
{
if (IsActionBusy) return;
if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id))
{
HandyControl.Controls.MessageBox.Warning("请先保存入场记录再生成原材料卡片!");
return;
}
// 只处理未生成卡片的行 + 份数>0;已生成行直接跳过,避免重复加卡和条码冲突
var pendingRows = SplitCodeDetails
.Where(d => !d.HasCard && (d.Portions ?? 0) > 0)
.ToList();
if (pendingRows.Count == 0)
{
HandyControl.Controls.MessageBox.Warning("当前没有待生成卡片的拆码明细(已生成的行不会重复处理)。请先「新增明细」再生成。");
return;
}
if (!await EnsureEntryBarcodeAndBatchNoAsync())
{
return;
}
// 库位必填仅校验本批待生成行;行号取在原集合的真实索引,避免编号偏移误导
foreach (var row in pendingRows)
{
if (string.IsNullOrWhiteSpace(row.WarehouseLocation))
{
var realIndex = SplitCodeDetails.IndexOf(row);
HandyControl.Controls.MessageBox.Warning($"拆码明细第 {realIndex + 1} 行的「库位」未填写,请点击该行库位选择库区后再生成原材料卡片。");
return;
}
}
// 总重核对:拆码明细合计 vs 基础资料总重
{
var splitTotal = SplitCodeDetails.Sum(d => (d.PortionWeight ?? 0d) * (d.Portions ?? 0));
var basicTotal = Entry.TotalWeight ?? 0d;
if (Math.Abs(splitTotal - basicTotal) > 0.01d)
{
string hint;
if (splitTotal > basicTotal)
hint = $"本次打印拆码总重 {splitTotal:0.##},超出基础资料总重 {basicTotal:0.##},超出部分是否需要核对?";
else
hint = $"本次打印拆码总重 {splitTotal:0.##},少于基础资料总重 {basicTotal:0.##},剩余部分将无法继续拆码,是否继续?";
var confirm = System.Windows.MessageBox.Show(
hint,
"总重核对",
System.Windows.MessageBoxButton.OKCancel,
System.Windows.MessageBoxImage.Warning);
if (confirm != System.Windows.MessageBoxResult.OK) return;
}
}
// 续号起点:已有卡片数 = Σ 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
};
// WebView2 Airspace:模态子窗体仍可能与本窗体内 HWND 叠层异常,弹窗期间收起嵌入预览
var confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(() =>
Task.FromResult(confirmWindow.ShowDialog() == true));
if (!confirmed) return;
var selectedPrinterName = confirmWindow.SelectedPrinterName;
if (string.IsNullOrWhiteSpace(selectedPrinterName))
{
HandyControl.Controls.MessageBox.Warning("未选择打印机,已取消打印。");
return;
}
BeginActionBusy("生成中...");
try
{
IsLoading = true;
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();
foreach (var detail in pendingRows)
{
var portions = detail.Portions ?? 0;
var rowAllOk = true;
for (var i = 0; i < portions; i++)
{
var card = new MesXslRawMaterialCard
{
// 关联到当前拆码明细行 — 便于「重新拆码」按此 ID 批删
SplitDetailId = detail.Id,
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)
{
newCardCount++;
generatedCards.Add(card);
}
else { failCount++; rowAllOk = false; }
globalIndex++;
}
rowSuccessMap[detail] = rowAllOk;
}
// 把整行加卡都成功的标记 HasCard=true(触发该行 UI 自动锁定 + 删除按钮隐藏 + 打印列显示「已打印」)
foreach (var kv in rowSuccessMap)
{
if (kv.Value) kv.Key.HasCard = true;
}
// 整票级 PrintFlag:只要本批至少有一张卡片成功即置 1(与原行为兼容)。
// 关键:必须先 ApplySplitDetailsToEntry(),把刚被设为 true 的 HasCard 序列化到 PortionCardFlags,
// 否则下次重新打开时该行会回退为 false(PortionCardFlags 旧值未更新),从而被「继续拆码」流程误纳入待生成。
if (newCardCount > 0)
{
Entry.PrintFlag = "1";
ApplySplitDetailsToEntry();
await EntryService.EditAsync(Entry);
RaisePropertyChanged(nameof(Entry));
RaisePropertyChanged(nameof(IsPrinted));
RaisePropertyChanged(nameof(IsTotalWeightEditable));
AddSplitDetailCommand.RaiseCanExecuteChanged();
}
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} 张),打印状态已更新为「已打印」!");
else
HandyControl.Controls.MessageBox.Warning($"本次共尝试生成 {newCardCount + failCount} 张,成功 {newCardCount} 张,失败 {failCount} 张。失败的行未标记为「已打印」,可检查网络后再次点击「生成原材料卡片」重试。");
}
catch (Exception ex)
{
HandyControl.Controls.MessageBox.Error($"生成原材料卡片失败:{ex.Message}");
}
finally
{
IsLoading = false;
EndActionBusy();
}
}
///
/// 生成原材料卡片前确保入场记录已有条码/批次号。
/// 规则:条码优先沿用现有值;为空时调用入场服务生成(在线优先,离线本地兜底);
/// 批次号为空则回填为条码。
///
private async Task EnsureEntryBarcodeAndBatchNoAsync()
{
if (Entry == null) return false;
if (string.IsNullOrWhiteSpace(Entry.Barcode))
{
if (string.IsNullOrWhiteSpace(Entry.MaterialCode))
{
HandyControl.Controls.MessageBox.Warning("当前记录缺少物料编码,无法生成原材料卡片条码。");
return false;
}
var code = await EntryService.GenerateBarcodeAsync(Entry.MaterialCode);
if (string.IsNullOrWhiteSpace(code))
{
HandyControl.Controls.MessageBox.Warning("未能生成入场条码,请检查网络或稍后重试。");
return false;
}
Entry.Barcode = code.Trim();
}
if (string.IsNullOrWhiteSpace(Entry.BatchNo))
{
Entry.BatchNo = Entry.Barcode;
}
RaisePropertyChanged(nameof(Entry));
return true;
}
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;
IsActionBusy = true;
}
private void EndActionBusy()
{
IsActionBusy = false;
ActionBusyText = "处理中...";
}
/// 页面卸载时停止轮询,避免后台仍请求已释放的 WebView。
public void StopPrintPreviewTimer() => _printPreviewTimer.Stop();
/// 视图重新附加时恢复轮询(与 配对)。
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
{
// 实时预览关闭模板内部 fitPage 自适应,避免与 WebView2 外层缩放叠加后出现“越放大越小”。
html = NativePrintRenderService.RenderToHtml(_previewTemplateJson, dataObj, enableScreenAutoFit: false);
}
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 ""
+ "
⚠️
"
+ "
预览:" + esc + "
"
+ "
";
}
private static string BuildPrintPreviewPlaceholderHtml(string tip)
{
var esc = WebUtility.HtmlEncode(tip ?? string.Empty);
return "";
}
private async Task 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()?.Trim();
if (string.IsNullOrWhiteSpace(templateField)) continue;
var bizField = obj["bizField"]?.GetValue()?.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(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 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()?.Trim();
if (!string.IsNullOrWhiteSpace(key)) fields.Add(key!);
}
}
var bindField = obj["bindField"]?.GetValue()?.Trim();
if (!string.IsNullOrWhiteSpace(bindField)) fields.Add(bindField!);
if (obj["columns"] is JsonArray cols)
{
foreach (var c in cols)
{
var cBind = c?["bindField"]?.GetValue()?.Trim();
var cField = c?["field"]?.GetValue()?.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;
}
///
/// 本地实时预览与后端保持一致:DateTime/DateTimeOffset 统一输出 yyyy-MM-dd HH:mm:ss(不带时区后缀)。
///
private static JsonNode NormalizePreviewNodeValue(JsonNode? node)
{
if (node == null) return JsonValue.Create(string.Empty)!;
if (node is JsonValue v)
{
if (v.TryGetValue(out var dt))
{
return JsonValue.Create(dt.ToString("yyyy-MM-dd HH:mm:ss"))!;
}
if (v.TryGetValue(out var dto))
{
return JsonValue.Create(dto.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))!;
}
if (v.TryGetValue(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
{
if (!File.Exists(LayoutFilePath)) return;
var json = File.ReadAllText(LayoutFilePath);
var dto = JsonSerializer.Deserialize(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; }
}
public sealed class RawMaterialCardGeneratePlanItem
{
public required MesXslRawMaterialCard Card { get; init; }
public required string DetailId { get; init; }
public int SourceRowNo { get; init; }
}
}