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.Threading.Tasks; 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 int _previewVersion; private bool _isRefreshingPrinters; private bool _suppressPrinterSave; /// Loaded 中批量赋值选中行时跳过 Setter 内的去抖预览,再由 Loaded 单次立即刷新。 private bool _suppressPreviewSchedule; 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))); // 仅用属性变更触发预览刷新,避免 SelectionChanged + 绑定 双重 Navigate 造成卡顿 if (!_suppressPreviewSchedule) { SchedulePreviewNavigate(skipDebounce: false); } } } 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; try { _suppressPreviewSchedule = true; if (SelectedPlanItem == null && PlanItems.Count > 0) { SelectedPlanItem = PlanItems[0]; } } finally { _suppressPreviewSchedule = false; } SchedulePreviewNavigate(skipDebounce: true); } 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) { Interlocked.Increment(ref _previewVersion); 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; } } /// /// RenderToHtml 在 UI 线程会阻塞鼠标/选中反馈;移至后台线程并做短去抖合并连点。 /// private async void SchedulePreviewNavigate(bool skipDebounce) { if (!_webViewReady) { return; } var token = Interlocked.Increment(ref _previewVersion); try { if (!skipDebounce) { await Task.Delay(45).ConfigureAwait(true); if (token != _previewVersion) { return; } } var row = SelectedPlanItem; if (row == null) { PreviewWebView.NavigateToString( "请先选择左侧卡片记录"); return; } string html; try { var capturedRow = row; html = await Task.Run(() => _previewHtmlBuilder(capturedRow)).ConfigureAwait(true); } catch (Exception ex) { html = "模板预览失败:" + System.Net.WebUtility.HtmlEncode(ex.Message) + ""; } if (token != _previewVersion || !ReferenceEquals(SelectedPlanItem, row)) { return; } PreviewWebView.NavigateToString(html); } catch { /* 窗口关闭或调度异常时忽略 */ } } 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; } }