新增打印模板管理功能,包含免密接口和实时通知机制,支持桌面端打印模板的查询和列表展示。更新相关控制器、服务和视图,优化用户体验并增强系统的实时数据同步能力。

This commit is contained in:
geht
2026-05-12 18:29:03 +08:00
parent f5ba828eff
commit fcedc66f7a
32 changed files with 2788 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ using YY.Admin.EventBus;
using YY.Admin.Filter;
using YY.Admin.Module;
using YY.Admin.Properties;
using YY.Admin.Services.Service.Print;
using YY.Admin.Setup;
using YY.Admin.ViewModels;
using YY.Admin.Views;
@@ -96,6 +97,9 @@ namespace YY.Admin
_logger.Information("应用程序已启动");
// 加载 PrintDot 本地设置(使 PrintDotService 在任何页面调用前已有配置)
PrintDotSettings.Load();
// 启动断联续传同步模块
_syncModule.OnInitialized(Container);
}

View File

@@ -162,6 +162,10 @@ public class StompWebSocketService : ISignalRService
await SendFrameAsync(
BuildSubscribeFrame("sub-mes-warehouse-areas", "/topic/sync/mes-warehouse-areas"),
cancellationToken).ConfigureAwait(false);
// 打印模板变更:订阅 /topic/sync/print-templates
await SendFrameAsync(
BuildSubscribeFrame("sub-print-templates", "/topic/sync/print-templates"),
cancellationToken).ConfigureAwait(false);
// 订阅服务端 PONG 回复(应用层假在线检测)
await SendFrameAsync(

View File

@@ -0,0 +1,93 @@
using System.IO;
using System.Text;
using System.Windows;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
namespace YY.Admin.Infrastructure.Print;
/// <summary>
/// 使用隐藏 WebView2 窗口将 HTML 字符串渲染为 PDF base64。
/// 所有调用必须最终在 WPF UI 线程执行,由内部 Dispatcher 保证。
/// </summary>
public static class HtmlToPdfRenderer
{
public static Task<string> RenderAsync(string html, double widthMm = 210, double heightMm = 297)
{
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
Application.Current.Dispatcher.InvokeAsync(async () =>
{
try
{
var result = await RenderOnUiThreadAsync(html, widthMm, heightMm);
tcs.SetResult(result);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
});
return tcs.Task;
}
private static async Task<string> RenderOnUiThreadAsync(string html, double widthMm, double heightMm)
{
var tempHtml = Path.ChangeExtension(Path.GetTempFileName(), ".html");
var tempPdf = Path.ChangeExtension(Path.GetTempFileName(), ".pdf");
Window? win = null;
try
{
File.WriteAllText(tempHtml, html, Encoding.UTF8);
var env = await CoreWebView2Environment.CreateAsync();
var wv = new WebView2();
win = new Window
{
Width = 1,
Height = 1,
Left = -99999,
Top = -99999,
ShowInTaskbar = false,
WindowStyle = WindowStyle.None,
Visibility = Visibility.Visible
};
win.Content = wv;
win.Show();
await wv.EnsureCoreWebView2Async(env);
var navTcs = new TaskCompletionSource();
wv.CoreWebView2.NavigationCompleted += (_, _) => navTcs.TrySetResult();
wv.CoreWebView2.Navigate(new Uri(tempHtml).AbsoluteUri);
await navTcs.Task;
// 短暂等待 JS/CSS 完成渲染
await Task.Delay(200);
var settings = env.CreatePrintSettings();
settings.PageWidth = widthMm / 25.4;
settings.PageHeight = heightMm / 25.4;
settings.MarginTop = 0;
settings.MarginBottom = 0;
settings.MarginLeft = 0;
settings.MarginRight = 0;
settings.ShouldPrintHeaderAndFooter = false;
settings.ShouldPrintBackgrounds = true;
await wv.CoreWebView2.PrintToPdfAsync(tempPdf, settings);
var pdfBytes = File.ReadAllBytes(tempPdf);
return Convert.ToBase64String(pdfBytes);
}
finally
{
win?.Close();
try { File.Delete(tempHtml); } catch { }
try { File.Delete(tempPdf); } catch { }
}
}
}

View File

@@ -16,6 +16,7 @@ using YY.Admin.Views.WeightRecord;
using YY.Admin.Views.RawMaterialCard;
using YY.Admin.Views.WarehouseArea;
using YY.Admin.Views.RawMaterialEntry;
using YY.Admin.Views.Print;
namespace YY.Admin
{
@@ -81,6 +82,10 @@ namespace YY.Admin
containerRegistry.RegisterForNavigation<RawMaterialCardListView>();
// 库区管理
containerRegistry.RegisterForNavigation<WarehouseAreaListView>();
// 打印设置
containerRegistry.RegisterForNavigation<PrintSettingsView>();
// 打印模板列表
containerRegistry.RegisterForNavigation<PrintTemplateListView>();
}
}
public class DialogWindow : Window, IDialogWindow

View File

@@ -25,6 +25,7 @@ using YY.Admin.Services.Service.Vehicle;
using YY.Admin.Services.Service.Warehouse;
using YY.Admin.Services.Service.WarehouseArea;
using YY.Admin.Services.Service.WeightRecord;
using YY.Admin.Services.Service.Print;
namespace YY.Admin.Module;
@@ -74,6 +75,10 @@ public class SyncModule : IModule
containerRegistry.RegisterSingleton<DictSyncCoordinator>();
// 统一轮询管理器(修改 SyncPollManager.PollInterval 即可调整所有模块的轮询间隔)
containerRegistry.RegisterSingleton<SyncPollManager>();
// 打印服务PrintDot 桥接器 + 打印模板(含 STOMP 实时同步 + 本地缓存)
containerRegistry.RegisterSingleton<IPrintDotService, PrintDotService>();
containerRegistry.RegisterSingleton<IPrintTemplateService, PrintTemplateService>();
containerRegistry.RegisterSingleton<PrintTemplateSyncCoordinator>();
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<DisconnectGuardHandler>();
@@ -140,6 +145,8 @@ public class SyncModule : IModule
_ = containerProvider.Resolve<CategorySyncCoordinator>();
// 强制实例化数据字典同步协调器
_ = containerProvider.Resolve<DictSyncCoordinator>();
// 强制实例化打印模板同步协调器
_ = containerProvider.Resolve<PrintTemplateSyncCoordinator>();
}
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()

View File

@@ -140,7 +140,18 @@ namespace YY.Admin.ViewModels.Control
// 已实现页面:库区管理
["WarehouseAreaListView"] = "WarehouseAreaListView",
["/xslmes/mesXslWarehouseArea"] = "WarehouseAreaListView",
["mesXslWarehouseArea"] = "WarehouseAreaListView"
["mesXslWarehouseArea"] = "WarehouseAreaListView",
// 已实现页面:打印设置
["PrintSettingsView"] = "PrintSettingsView",
["/system/printSettings"] = "PrintSettingsView",
["printSettings"] = "PrintSettingsView",
// 已实现页面:打印模板
["PrintTemplateListView"] = "PrintTemplateListView",
["/platform/print"] = "PrintTemplateListView",
["print"] = "PrintTemplateListView",
["printTemplate"] = "PrintTemplateListView"
};
private MenuItem? _selectedMenuItem;

View File

@@ -0,0 +1,142 @@
using System.Collections.ObjectModel;
using System.Windows;
using Prism.Commands;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service.Print;
using YY.Admin.Core;
namespace YY.Admin.ViewModels.Print;
public class PrintSettingsViewModel : BaseViewModel
{
private readonly IPrintDotService _printDotService;
private readonly IPrintTemplateService _printTemplateService;
// ── 连接设置 ──────────────────────────────────────────────────────────
private string _wsUrl = "ws://127.0.0.1:1122/ws";
public string WsUrl
{
get => _wsUrl;
set => SetProperty(ref _wsUrl, value);
}
// ── 打印机列表 ──────────────────────────────────────────────────────────
public ObservableCollection<PrintDotPrinter> Printers { get; } = new();
private PrintDotPrinter? _selectedPrinter;
public PrintDotPrinter? SelectedPrinter
{
get => _selectedPrinter;
set => SetProperty(ref _selectedPrinter, value);
}
// ── 模板列表 ────────────────────────────────────────────────────────────
public ObservableCollection<PrintTemplate> Templates { get; } = new();
// ── 状态 ────────────────────────────────────────────────────────────────
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
private string _statusMessage = string.Empty;
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
// ── 命令 ────────────────────────────────────────────────────────────────
public DelegateCommand TestConnectionCommand { get; }
public DelegateCommand SaveCommand { get; }
public DelegateCommand RefreshTemplatesCommand { get; }
public PrintSettingsViewModel(
IPrintDotService printDotService,
IPrintTemplateService printTemplateService,
IContainerExtension container,
IRegionManager regionManager) : base(container, regionManager)
{
_printDotService = printDotService;
_printTemplateService = printTemplateService;
TestConnectionCommand = new DelegateCommand(async () => await TestConnectionAsync());
SaveCommand = new DelegateCommand(SaveSettings);
RefreshTemplatesCommand = new DelegateCommand(async () => await RefreshTemplatesAsync());
// 加载已保存的设置
var saved = PrintDotSettings.Load();
WsUrl = saved.WsUrl;
if (!string.IsNullOrWhiteSpace(saved.SelectedPrinter))
_selectedPrinter = new PrintDotPrinter(saved.SelectedPrinter, false);
}
private async Task TestConnectionAsync()
{
IsBusy = true;
StatusMessage = "正在连接...";
Printers.Clear();
try
{
// 临时用当前输入的 URL 测试
PrintDotSettings.Current!.WsUrl = WsUrl;
var list = await _printDotService.GetPrintersAsync();
foreach (var p in list) Printers.Add(p);
// 还原保存的已选打印机
var saved = PrintDotSettings.Load();
var match = list.FirstOrDefault(p => p.Name == saved.SelectedPrinter);
SelectedPrinter = match ?? (list.Count > 0 ? list[0] : null);
StatusMessage = $"连接成功,共 {list.Count} 台打印机";
}
catch (Exception ex)
{
StatusMessage = $"连接失败:{ex.Message}";
}
finally
{
IsBusy = false;
}
}
private void SaveSettings()
{
var settings = new PrintDotSettings
{
WsUrl = WsUrl.Trim(),
SelectedPrinter = SelectedPrinter?.Name ?? string.Empty
};
settings.Save();
StatusMessage = "设置已保存";
MessageBox.Show("PrintDot 设置已保存", "保存成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
private async Task RefreshTemplatesAsync()
{
IsBusy = true;
Templates.Clear();
try
{
var list = await _printTemplateService.ListAsync();
foreach (var t in list) Templates.Add(t);
StatusMessage = $"已加载 {list.Count} 个打印模板";
}
catch (Exception ex)
{
StatusMessage = $"加载模板失败:{ex.Message}";
}
finally
{
IsBusy = false;
}
}
}

View File

@@ -0,0 +1,201 @@
using System.Collections.ObjectModel;
using System.Windows;
using Prism.Commands;
using Prism.Events;
using YY.Admin.Core;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Events;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service.Print;
using YY.Admin.Views.Print;
namespace YY.Admin.ViewModels.Print;
public class PrintTemplateListViewModel : BaseViewModel
{
private readonly IPrintTemplateService _printTemplateService;
private readonly IEventAggregator _eventAggregator;
private SubscriptionToken? _changeToken;
private List<PrintTemplate> _allTemplates = new();
public ObservableCollection<PrintTemplate> Templates { get; } = new();
private string _statusMessage = string.Empty;
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
private string? _filterCode;
public string? FilterCode
{
get => _filterCode;
set => SetProperty(ref _filterCode, value);
}
private string? _filterName;
public string? FilterName
{
get => _filterName;
set => SetProperty(ref _filterName, value);
}
private string? _filterCategory;
public string? FilterCategory
{
get => _filterCategory;
set => SetProperty(ref _filterCategory, value);
}
public DelegateCommand SearchCommand { get; }
public DelegateCommand ResetCommand { get; }
public DelegateCommand<PrintTemplate> PreviewCommand { get; }
public PrintTemplateListViewModel(
IPrintTemplateService printTemplateService,
IEventAggregator eventAggregator,
IContainerExtension container,
IRegionManager regionManager) : base(container, regionManager)
{
_printTemplateService = printTemplateService;
_eventAggregator = eventAggregator;
SearchCommand = new DelegateCommand(ApplyFilter);
PreviewCommand = new DelegateCommand<PrintTemplate>(ShowPreview);
ResetCommand = new DelegateCommand(() =>
{
FilterCode = null;
FilterName = null;
FilterCategory = null;
ApplyFilter();
});
_changeToken = _eventAggregator
.GetEvent<PrintTemplateChangedEvent>()
.Subscribe(_ => { RefreshSilentlyAsync().ConfigureAwait(false); }, ThreadOption.UIThread);
// 先用缓存立即填充,再后台静默刷新
ShowCached();
_ = RefreshSilentlyAsync();
}
private void ShowCached()
{
var cached = _printTemplateService.GetCached();
if (cached.Count == 0) return;
_allTemplates = cached.ToList();
ApplyFilter();
UpdateStatus();
}
private async Task RefreshSilentlyAsync()
{
try
{
var list = await _printTemplateService.RefreshCacheAsync().ConfigureAwait(false);
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
_allTemplates = list.ToList();
ApplyFilter();
UpdateStatus();
});
}
catch
{
// 静默失败:保留当前缓存内容不动,不显示错误
var cached = _printTemplateService.GetCached();
if (cached.Count > 0 && _allTemplates.Count == 0)
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
_allTemplates = cached.ToList();
ApplyFilter();
UpdateStatus();
});
}
}
}
private void ApplyFilter()
{
IEnumerable<PrintTemplate> result = _allTemplates;
if (!string.IsNullOrWhiteSpace(FilterCode))
result = result.Where(t => (t.TemplateCode ?? string.Empty)
.Contains(FilterCode, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(FilterName))
result = result.Where(t => (t.TemplateName ?? string.Empty)
.Contains(FilterName, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(FilterCategory))
result = result.Where(t => (t.Category ?? string.Empty)
.Contains(FilterCategory, StringComparison.OrdinalIgnoreCase));
var filtered = result.ToList();
// 原地差量更新,避免滚动位置重置和闪烁
for (int i = Templates.Count - 1; i >= 0; i--)
{
if (!filtered.Any(t => t.Id == Templates[i].Id))
Templates.RemoveAt(i);
}
for (int i = 0; i < filtered.Count; i++)
{
var item = filtered[i];
var existingIdx = -1;
for (int j = 0; j < Templates.Count; j++)
{
if (Templates[j].Id == item.Id) { existingIdx = j; break; }
}
if (existingIdx < 0)
Templates.Insert(i, item);
else
{
if (existingIdx != i) Templates.Move(existingIdx, i);
Templates[i] = item;
}
}
}
private void UpdateStatus()
{
var hasFilter = !string.IsNullOrWhiteSpace(FilterCode)
|| !string.IsNullOrWhiteSpace(FilterName)
|| !string.IsNullOrWhiteSpace(FilterCategory);
StatusMessage = hasFilter
? $"筛选结果 {Templates.Count} / {_allTemplates.Count} 个"
: _allTemplates.Count > 0
? $"共 {_allTemplates.Count} 个模板"
: "暂无模板";
}
private void ShowPreview(PrintTemplate template)
{
if (template == null) return;
ShowPreviewAsync(template);
}
private async Task ShowPreviewAsync(PrintTemplate template)
{
// 列表缓存可能不含 templateJson大字段按需通过 queryByCode 单独拉取
var json = template.TemplateJson;
if (string.IsNullOrWhiteSpace(json) || json == "{}")
{
try
{
var full = await _printTemplateService.GetByCodeAsync(template.TemplateCode ?? "");
json = full?.TemplateJson;
}
catch { /* 保持 json 为 null预览窗口显示"尚未设计" */ }
}
var win = new PrintPreviewWindow(template, json)
{
Owner = Application.Current.MainWindow
};
win.Show();
}
}

View File

@@ -0,0 +1,125 @@
<hc:Window x:Class="YY.Admin.Views.Print.PrintPreviewWindow"
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="1200" Height="760"
MinWidth="900" MinHeight="560"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize"
Background="White"
BorderBrush="#f0f0f0"
BorderThickness="1"
Title="打印预览">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 顶部工具栏 -->
<Border Grid.Row="0"
Background="#fafafa"
BorderBrush="#f0f0f0"
BorderThickness="0,0,0,1">
<Grid Margin="16,0">
<StackPanel VerticalAlignment="Center">
<TextBlock x:Name="TbTemplateName"
FontSize="14" FontWeight="SemiBold"
Foreground="#333333"/>
<TextBlock x:Name="TbTemplateCode"
FontSize="11" Foreground="#888888" Margin="0,1,0,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
VerticalAlignment="Center">
<TextBlock x:Name="TbStatus"
FontSize="12" Foreground="#888888"
VerticalAlignment="Center" Margin="0,0,16,0"/>
<Button Content="关闭" Click="CloseButton_Click"
Width="72" Height="30" FontSize="13"
Style="{StaticResource ButtonDefault}"/>
</StackPanel>
</Grid>
</Border>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="360"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧参数 JSON 区 -->
<Border Grid.Column="0"
BorderBrush="#f0f0f0"
BorderThickness="0,0,1,0"
Background="#fafafa"
Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="参数JSON"
FontSize="13"
FontWeight="SemiBold"
Foreground="#333333"/>
<StackPanel Grid.Row="1"
Orientation="Horizontal"
Margin="0,8,0,8">
<Button Content="根据画布生成"
Click="GenerateMockJson_Click"
Height="28"
Padding="10,0"
FontSize="12"
Style="{StaticResource ButtonDefault}"/>
<Button Content="重新渲染"
Click="RenderByParamJson_Click"
Margin="8,0,0,0"
Height="28"
Padding="10,0"
FontSize="12"
Style="{StaticResource ButtonPrimary}"/>
</StackPanel>
<TextBox x:Name="TbParamJson"
Grid.Row="2"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
FontFamily="Consolas"
FontSize="12"
TextWrapping="NoWrap"
BorderBrush="#d9d9d9"
BorderThickness="1"/>
</Grid>
</Border>
<GridSplitter Grid.Column="1"
Width="8"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ResizeBehavior="PreviousAndNext"
ShowsPreview="True"
Background="#f0f0f0"
Cursor="SizeWE"/>
<!-- 右侧 WebView2 预览区 -->
<wv2:WebView2 Grid.Column="2" x:Name="WebView" DefaultBackgroundColor="Transparent"/>
</Grid>
</Grid>
</hc:Window>

View File

@@ -0,0 +1,240 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Windows;
using YY.Admin.Core.Entity;
using YY.Admin.Services.Service.Print;
namespace YY.Admin.Views.Print;
public partial class PrintPreviewWindow : HandyControl.Controls.Window
{
private readonly string _templateJson;
public PrintPreviewWindow(PrintTemplate template, string? templateJson)
{
InitializeComponent();
_templateJson = templateJson ?? string.Empty;
TbTemplateName.Text = template.TemplateName ?? "(未命名)";
TbTemplateCode.Text = $"编码:{template.TemplateCode} " +
$"尺寸:{template.PaperWidthMm ?? 210}×{template.PaperHeightMm ?? 297} mm " +
$"方向:{template.PaperOrientation ?? ""}";
TbParamJson.Text = BuildMockParamJson(_templateJson);
Loaded += async (_, _) => await LoadPreviewAsync();
}
private async Task LoadPreviewAsync()
{
try
{
TbStatus.Text = "加载中…";
await WebView.EnsureCoreWebView2Async();
if (string.IsNullOrWhiteSpace(_templateJson) || _templateJson == "{}")
{
WebView.NavigateToString(BuildEmptyHtml());
TbStatus.Text = "尚未设计模板内容";
return;
}
await RenderCurrentParamJsonAsync();
}
catch (Exception ex)
{
TbStatus.Text = $"预览失败:{ex.Message}";
}
}
private static string BuildEmptyHtml() => """
<!DOCTYPE html>
<html><head><meta charset="utf-8"/>
<style>
body { margin:0; background:#525659; display:flex;
align-items:center; justify-content:center; height:100vh;
font-family:"Microsoft YaHei",Arial,sans-serif; }
.card { background:#fff; border-radius:8px; padding:48px 64px;
text-align:center; box-shadow:0 6px 24px rgba(0,0,0,.4); }
.icon { font-size:48px; color:#ccc; margin-bottom:16px; }
.tip { font-size:15px; color:#888; }
</style></head>
<body>
<div class="card">
<div class="icon">📄</div>
<div class="tip"></div>
</div>
</body></html>
""";
/// <summary>
/// 左侧参数 JSON 重新渲染预览(与后端预览一致:用参数 JSON 驱动模板绑定字段)。
/// </summary>
private async Task RenderCurrentParamJsonAsync()
{
try
{
TbStatus.Text = "渲染中…";
JsonObject dataObj;
var text = TbParamJson.Text?.Trim();
if (string.IsNullOrWhiteSpace(text))
{
dataObj = new JsonObject();
}
else
{
var node = JsonNode.Parse(text);
if (node is not JsonObject obj)
{
WebView.NavigateToString(BuildErrorHtml("参数JSON必须是对象JSON Object"));
TbStatus.Text = "参数JSON格式错误";
return;
}
dataObj = obj;
}
var html = NativePrintRenderService.RenderToHtml(_templateJson, dataObj);
WebView.NavigateToString(html);
TbStatus.Text = string.Empty;
}
catch (Exception ex)
{
WebView.NavigateToString(BuildErrorHtml(ex.Message));
TbStatus.Text = $"渲染失败:{ex.Message}";
}
await Task.CompletedTask;
}
/// <summary>
/// 根据模板绑定字段生成参数 JSON便于用户直接编辑并预览
/// </summary>
private static string BuildMockParamJson(string templateJson)
{
if (string.IsNullOrWhiteSpace(templateJson) || templateJson == "{}")
return "{}";
try
{
var root = JsonNode.Parse(templateJson);
var obj = new JsonObject();
var elements = root?["elements"]?.AsArray() ?? new JsonArray();
var fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var el in elements.OfType<JsonObject>())
{
var type = (el["type"]?.ToString() ?? string.Empty).Trim();
if (type is "table" or "detailTable")
{
var source = (el["source"]?.ToString() ?? "mainTable").Trim();
if (!obj.ContainsKey(source))
{
var rows = new JsonArray();
var columns = el["columns"]?.AsArray() ?? new JsonArray();
for (var i = 1; i <= 8; i++)
{
var row = new JsonObject();
foreach (var col in columns.OfType<JsonObject>())
{
var field = (col["bindField"]?.ToString() ?? col["field"]?.ToString() ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(field)) continue;
var contentType = (col["contentType"]?.ToString() ?? "text").Trim().ToLowerInvariant();
row[field] = contentType switch
{
"number" => i * 123.45,
"amount" => i * 24567.89,
"qrcode" => $"QR_{field}_{i}",
"barcode" => $"BAR_{field}_{i}",
"image" => $"https://picsum.photos/seed/{Uri.EscapeDataString(field + "_" + i)}/260/120",
_ => $"{field}_示例值_{i}"
};
fields.Add(field);
}
rows.Add(row);
}
obj[source] = rows;
}
}
var bind = (el["bindField"]?.ToString() ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(bind))
fields.Add(bind);
// 提取 text 中的 {{field}} 占位符(支持内嵌)
var text = el["text"]?.ToString() ?? string.Empty;
foreach (Match m in Regex.Matches(text, @"\{\{\s*([\w\.]+)\s*\}\}"))
{
var key = m.Groups[1].Value.Trim();
if (!string.IsNullOrWhiteSpace(key))
fields.Add(key);
}
CollectBindFields(el["cells"], fields);
}
foreach (var f in fields.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
{
if (f.Equals("pageNo", StringComparison.OrdinalIgnoreCase) || f.Equals("totalPages", StringComparison.OrdinalIgnoreCase))
continue;
if (!obj.ContainsKey(f))
obj[f] = $"{f}_示例值";
}
return obj.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
}
catch
{
return "{}";
}
}
private static void CollectBindFields(JsonNode? node, ISet<string> fields)
{
if (node == null) return;
if (node is JsonObject o)
{
if (o.TryGetPropertyValue("bindField", out var bindNode))
{
var bind = bindNode?.ToString()?.Trim();
if (!string.IsNullOrWhiteSpace(bind))
fields.Add(bind);
}
foreach (var kv in o)
CollectBindFields(kv.Value, fields);
return;
}
if (node is JsonArray arr)
{
foreach (var it in arr)
CollectBindFields(it, fields);
}
}
private static string BuildErrorHtml(string message)
{
var esc = message.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
return "<html><head><meta charset=\"utf-8\"/><style>"
+ "body{margin:0;background:#525659;display:flex;align-items:center;"
+ "justify-content:center;height:100vh;font-family:'Microsoft YaHei',Arial,sans-serif;}"
+ ".card{background:#fff;border-radius:8px;padding:32px 48px;text-align:center;"
+ "box-shadow:0 6px 24px rgba(0,0,0,.4);max-width:560px;}"
+ "</style></head><body><div class=\"card\">"
+ "<div style=\"font-size:40px;margin-bottom:12px\">⚠️</div>"
+ "<div style=\"font-size:13px;color:#e74c3c;word-break:break-all;\">渲染失败:" + esc + "</div>"
+ "</div></body></html>";
}
private async void RenderByParamJson_Click(object sender, RoutedEventArgs e)
{
await RenderCurrentParamJsonAsync();
}
private async void GenerateMockJson_Click(object sender, RoutedEventArgs e)
{
TbParamJson.Text = BuildMockParamJson(_templateJson);
await RenderCurrentParamJsonAsync();
}
private void CloseButton_Click(object sender, RoutedEventArgs e) => Close();
}

View File

@@ -0,0 +1,96 @@
<UserControl x:Class="YY.Admin.Views.Print.PrintSettingsView"
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">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="24" MaxWidth="680">
<!-- 标题 -->
<TextBlock Text="打印设置" FontSize="18" FontWeight="Bold" Margin="0,0,0,20"/>
<!-- PrintDot 连接配置 -->
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" CornerRadius="4" Padding="16" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="PrintDot 桥接器连接" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,12"/>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="WebSocket 地址" VerticalAlignment="Center" FontSize="13"/>
<TextBox Grid.Column="1" Text="{Binding WsUrl, UpdateSourceTrigger=PropertyChanged}"
hc:InfoElement.Placeholder="ws://192.168.x.x:1122/ws"
FontSize="13" Padding="6,4"/>
</Grid>
<TextBlock Text="格式ws://&lt;IP&gt;:1122/ws支持局域网任意 IP" FontSize="11"
Foreground="{DynamicResource SecondaryTextBrush}" Margin="120,0,0,12"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<Button Content="测试连接并获取打印机" Command="{Binding TestConnectionCommand}"
Style="{StaticResource ButtonPrimary}" Padding="12,6" FontSize="13" Margin="0,0,8,0"/>
<Button Content="保存设置" Command="{Binding SaveCommand}"
Style="{StaticResource ButtonDefault}" Padding="12,6" FontSize="13"/>
</StackPanel>
<!-- 状态提示 -->
<TextBlock Text="{Binding StatusMessage}" FontSize="12" Foreground="{DynamicResource InfoBrush}"
Visibility="{Binding StatusMessage, Converter={StaticResource String2VisibilityConverter}}"/>
</StackPanel>
</Border>
<!-- 打印机列表 -->
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" CornerRadius="4" Padding="16" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="可用打印机" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,12"/>
<ListBox ItemsSource="{Binding Printers}" SelectedItem="{Binding SelectedPrinter}"
MaxHeight="180" ScrollViewer.VerticalScrollBarVisibility="Auto">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="4,2">
<TextBlock Text="{Binding Name}" FontSize="13"/>
<TextBlock Text=" (默认)" FontSize="11" Foreground="{DynamicResource InfoBrush}"
Visibility="{Binding IsDefault, Converter={StaticResource Boolean2VisibilityConverter}}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Margin="0,8,0,0" FontSize="12" Foreground="{DynamicResource SecondaryTextBrush}">
已选打印机:<Run Text="{Binding SelectedPrinter.Name, FallbackValue='(未选择)'}"/>
</TextBlock>
</StackPanel>
</Border>
<!-- 打印模板列表 -->
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" CornerRadius="4" Padding="16">
<StackPanel>
<DockPanel Margin="0,0,0,12">
<Button DockPanel.Dock="Right" Content="刷新模板" Command="{Binding RefreshTemplatesCommand}"
Style="{StaticResource ButtonDefault}" Padding="10,4" FontSize="12"/>
<TextBlock Text="打印模板" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
</DockPanel>
<DataGrid ItemsSource="{Binding Templates}" AutoGenerateColumns="False"
IsReadOnly="True" MaxHeight="240" GridLinesVisibility="Horizontal"
HeadersVisibility="Column" FontSize="12">
<DataGrid.Columns>
<DataGridTextColumn Header="模板编码" Binding="{Binding TemplateCode}" Width="160"/>
<DataGridTextColumn Header="模板名称" Binding="{Binding TemplateName}" Width="*"/>
<DataGridTextColumn Header="分类" Binding="{Binding Category}" Width="80"/>
<DataGridTextColumn Header="纸宽(mm)" Binding="{Binding PaperWidthMm}" Width="70"/>
<DataGridTextColumn Header="纸高(mm)" Binding="{Binding PaperHeightMm}" Width="70"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Border>
<!-- 忙碌指示 -->
<hc:LoadingCircle Visibility="{Binding IsBusy, Converter={StaticResource Boolean2VisibilityConverter}}"
HorizontalAlignment="Center" Margin="0,16,0,0"/>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace YY.Admin.Views.Print;
public partial class PrintSettingsView : UserControl
{
public PrintSettingsView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,125 @@
<UserControl x:Class="YY.Admin.Views.Print.PrintTemplateListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<Grid Style="{StaticResource BaseViewStyle}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 搜索条件区域 -->
<Border Grid.Row="0" CornerRadius="4" Margin="0 0 -10 0">
<hc:Row>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:TextBox Text="{Binding FilterCode, UpdateSourceTrigger=PropertyChanged}"
Margin="0 0 10 10"
hc:InfoElement.Title="模板编码"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="65"
hc:InfoElement.Placeholder="请输入模板编码"
hc:InfoElement.ShowClearButton="True"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:TextBox Text="{Binding FilterName, UpdateSourceTrigger=PropertyChanged}"
Margin="0 0 10 10"
hc:InfoElement.Title="模板名称"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="65"
hc:InfoElement.Placeholder="请输入模板名称"
hc:InfoElement.ShowClearButton="True"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:TextBox Text="{Binding FilterCategory, UpdateSourceTrigger=PropertyChanged}"
Margin="0 0 10 10"
hc:InfoElement.Title="分类"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="65"
hc:InfoElement.Placeholder="请输入分类"
hc:InfoElement.ShowClearButton="True"/>
</hc:Col>
</hc:Row>
</Border>
<!-- 操作工具栏 -->
<Border Grid.Row="1" Margin="0,10">
<hc:UniformSpacingPanel Spacing="10">
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding SearchCommand}">
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="Search"/>
<TextBlock Text="搜索" Style="{StaticResource IconButtonStyle}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource ButtonDefault}" Command="{Binding ResetCommand}">
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="Refresh"/>
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
</StackPanel>
</Button>
</hc:UniformSpacingPanel>
</Border>
<!-- 数据表格 -->
<DataGrid Grid.Row="2"
ItemsSource="{Binding Templates}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="False"
SelectionMode="Extended"
SelectionUnit="FullRow"
RowHeaderWidth="55"
GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#FFEDEDED"
VerticalGridLinesBrush="Transparent"
HeadersVisibility="All"
ColumnHeaderStyle="{StaticResource CusDataGridColumnHeaderStyle}"
Style="{StaticResource CusDataGridStyle}"
hc:DataGridAttach.ShowSelectAllButton="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
<DataGrid.Columns>
<DataGridTextColumn Header="模板编码" Binding="{Binding TemplateCode}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="160"/>
<DataGridTextColumn Header="模板名称" Binding="{Binding TemplateName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="*"/>
<DataGridTextColumn Header="分类" Binding="{Binding Category}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
<DataGridTextColumn Header="纸宽(mm)" Binding="{Binding PaperWidthMm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
<DataGridTextColumn Header="纸高(mm)" Binding="{Binding PaperHeightMm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
<DataGridTextColumn Header="方向" Binding="{Binding PaperOrientation}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="80"/>
<DataGridTextColumn Header="备注" Binding="{Binding Remark}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="180"/>
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat=yyyy-MM-dd HH:mm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
<DataGridTemplateColumn Header="操作" Width="72" CanUserSort="False" CanUserResize="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="预览"
Command="{Binding DataContext.PreviewCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
CommandParameter="{Binding}"
Style="{StaticResource ButtonPrimary}"
Padding="0" Height="26" FontSize="12"
Margin="4,0"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<!-- 底部状态栏 -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
<TextBlock Text="{Binding StatusMessage}"
VerticalAlignment="Center"
Foreground="{DynamicResource SecondaryTextBrush}"/>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace YY.Admin.Views.Print;
public partial class PrintTemplateListView : UserControl
{
public PrintTemplateListView()
{
InitializeComponent();
}
}

View File

@@ -44,6 +44,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3065.39" />
<PackageReference Include="Microsoft.Extensions.Http">
<Version>10.0.7</Version>
</PackageReference>