新增原材料卡片生成逻辑,优化打印预览功能,支持卡片模板的加载和打印。重构相关方法以提升用户体验,确保生成的卡片数据有效,并处理打印异常情况。

This commit is contained in:
geht
2026-05-14 15:52:00 +08:00
parent 687b9bebed
commit f0c14d8a4b
3 changed files with 855 additions and 6 deletions

View File

@@ -0,0 +1,184 @@
<hc:Window x:Class="YY.Admin.Views.RawMaterialEntry.RawMaterialCardGenerateConfirmWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
Width="1416"
Height="864"
MinWidth="1176"
MinHeight="744"
Title="生成并打印原材料卡片"
ResizeMode="CanResize">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="即将生成的原材料卡片"
FontSize="16"
FontWeight="SemiBold"/>
<TextBlock Text="{Binding HeaderText}"
Margin="0,4,0,0"
FontSize="12"
Foreground="#8C8C8C"/>
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="打印机:"
VerticalAlignment="Center"
Margin="0,0,6,0"/>
<ComboBox Width="300"
Height="34"
ItemsSource="{Binding Printers}"
SelectedItem="{Binding SelectedPrinter, Mode=TwoWay}"
DisplayMemberPath="Name"
VerticalContentAlignment="Center"/>
<Button Content="刷新打印机"
Height="34"
Margin="8,0,8,0"
Padding="12,0"
Click="RefreshPrintersButton_OnClick"/>
<TextBlock Text="{Binding PrinterStatus}"
VerticalAlignment="Center"
Foreground="#595959"/>
</StackPanel>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftPaneCol" Width="7*"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition x:Name="RightPaneCol" Width="3*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
BorderBrush="#E5E6EB"
BorderThickness="1"
CornerRadius="4"
Padding="0"
Background="#FFFFFF">
<DataGrid x:Name="PlanGrid"
ItemsSource="{Binding PlanItems}"
SelectedItem="{Binding SelectedPlanItem, Mode=TwoWay}"
AutoGenerateColumns="False"
HeadersVisibility="Column"
CanUserResizeColumns="True"
CanUserReorderColumns="False"
CanUserSortColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
IsReadOnly="True"
GridLinesVisibility="None"
HorizontalGridLinesBrush="Transparent"
VerticalGridLinesBrush="Transparent"
BorderThickness="0"
RowHeaderWidth="0"
SelectionMode="Single"
SelectionUnit="FullRow"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionChanged="PlanGrid_OnSelectionChanged">
<DataGrid.Resources>
<Style x:Key="CenteredCellTextStyle" TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="TextAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="MinHeight" Value="36"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Margin" Value="0,0,0,3"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Background" Value="#FFFFFF"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Padding" Value="8,10"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="TextBlock.TextAlignment" Value="Center"/>
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="#" Binding="{Binding Index}" Width="70" MinWidth="60" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
<DataGridTextColumn Header="条码" Binding="{Binding Card.Barcode}" Width="220" MinWidth="170" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
<DataGridTextColumn Header="来源行" Binding="{Binding SourceRowNo}" Width="90" MinWidth="70" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
<DataGridTextColumn Header="物料名称" Binding="{Binding Card.MaterialName}" Width="220" MinWidth="160" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
<DataGridTextColumn Header="批次号" Binding="{Binding Card.BatchNo}" Width="220" MinWidth="160" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
<DataGridTextColumn Header="库位" Binding="{Binding Card.WarehouseArea}" Width="150" MinWidth="100" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
</DataGrid.Columns>
</DataGrid>
</Border>
<GridSplitter Grid.Column="1"
Width="10"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
ResizeBehavior="PreviousAndNext"
Cursor="SizeWE"
DragCompleted="MainSplitter_OnDragCompleted"/>
<Border Grid.Column="2"
BorderBrush="#E5E6EB"
BorderThickness="1"
CornerRadius="4"
Background="#FFFFFF">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="10,8,10,8">
<TextBlock Text="打印模板预览" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Text="{Binding TemplateText}" Margin="0,4,0,0" FontSize="12" Foreground="#8C8C8C"/>
</StackPanel>
<wv2:WebView2 x:Name="PreviewWebView" Grid.Row="1" DefaultBackgroundColor="#FFFFFFFF"/>
</Grid>
</Border>
</Grid>
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,12,0,0">
<Button Content="取消"
Width="92"
Height="34"
Margin="0,0,10,0"
Click="CancelButton_OnClick"
Style="{StaticResource ButtonDefault}"/>
<Button Content="生成并打印"
Width="120"
Height="34"
Click="ConfirmButton_OnClick"
Style="{StaticResource ButtonPrimary}"/>
</StackPanel>
</Grid>
</hc:Window>

View File

@@ -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;
/// <summary>
/// 需显式实现 <see cref="INotifyPropertyChanged"/>,否则 WPF 绑定不会订阅 PropertyChanged
/// 界面会一直停留在属性初始值(例如 PrinterStatus 的「加载打印机中…」)。
/// </summary>
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<RawMaterialCardGeneratePlanRow> PlanItems { get; } = new();
public string HeaderText { get; }
public string TemplateText { get; }
public ObservableCollection<PrintDotPrinter> Printers { get; } = new();
private readonly Func<RawMaterialCardGeneratePlanRow, string> _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<RawMaterialEntryOperationViewModel.RawMaterialCardGeneratePlanItem> planItems,
string templateName,
string templateCode,
IPrintDotService printDotService,
Func<RawMaterialCardGeneratePlanRow, string> 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("<html><body style='font-family:Microsoft YaHei;padding:24px;'>模板预览加载失败</body></html>");
}
}
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<ConfirmWindowLayoutDto>(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<ConfirmWindowLayoutDto> 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<string, double>? 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<string, double>(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("<html><body style='font-family:Microsoft YaHei;padding:24px;'>请先选择左侧卡片记录</body></html>");
return;
}
try
{
var html = _previewHtmlBuilder.Invoke(SelectedPlanItem);
PreviewWebView.NavigateToString(html);
}
catch
{
PreviewWebView.NavigateToString("<html><body style='font-family:Microsoft YaHei;padding:24px;'>模板预览加载失败</body></html>");
}
}
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<string, double>? 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; }
}