289 lines
12 KiB
C#
289 lines
12 KiB
C#
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(便于用户直接编辑并预览)。
|
||
/// 与 web 端 nativeMockData.ts 保持一致:识别 mergeColumnKeys 让同组相邻行字段值相同,
|
||
/// 以便在预览中触发 rowSpan 合并显示。
|
||
/// </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);
|
||
var rng = new Random();
|
||
|
||
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 columns = el["columns"]?.AsArray() ?? new JsonArray();
|
||
var colList = columns.OfType<JsonObject>().ToList();
|
||
|
||
// 解析合并字段顺序:根据 mergeColumnKeys 按 column.key 映射到 bindField
|
||
var mergeKeys = (el["mergeColumnKeys"]?.AsArray() ?? new JsonArray())
|
||
.Select(n => n?.ToString() ?? string.Empty)
|
||
.Where(s => !string.IsNullOrEmpty(s))
|
||
.ToList();
|
||
var strictGrouping = !string.Equals(
|
||
el["strictGrouping"]?.ToString() ?? "true", "false",
|
||
StringComparison.OrdinalIgnoreCase);
|
||
|
||
var mergeFieldOrder = mergeKeys
|
||
.Select(k => colList.FirstOrDefault(c => string.Equals(c["key"]?.ToString() ?? string.Empty, k, StringComparison.Ordinal)))
|
||
.Where(c => c != null)
|
||
.Select(c => (c!["bindField"]?.ToString() ?? c!["field"]?.ToString() ?? string.Empty).Trim())
|
||
.Where(s => !string.IsNullOrEmpty(s))
|
||
.ToList();
|
||
|
||
var rows = new JsonArray();
|
||
JsonObject? prevRow = null;
|
||
for (var i = 0; i < 8; i++)
|
||
{
|
||
var row = new JsonObject();
|
||
foreach (var col in colList)
|
||
{
|
||
var field = (col["bindField"]?.ToString() ?? col["field"]?.ToString() ?? string.Empty).Trim();
|
||
if (string.IsNullOrWhiteSpace(field)) continue;
|
||
var contentType = (col["contentType"]?.ToString() ?? "text").Trim().ToLowerInvariant();
|
||
fields.Add(field);
|
||
|
||
var mergeIndex = mergeFieldOrder.IndexOf(field);
|
||
var enableMerge = mergeIndex >= 0;
|
||
|
||
if (enableMerge)
|
||
{
|
||
// 父级合并字段在 strictGrouping 下必须与前一行一致,才允许沿用前一行值
|
||
var canFollowPrev = !strictGrouping || mergeIndex == 0 || (prevRow != null &&
|
||
mergeFieldOrder.Take(mergeIndex).All(parent =>
|
||
(prevRow[parent]?.ToString() ?? string.Empty) == (row[parent]?.ToString() ?? string.Empty)));
|
||
|
||
if (i > 0 && prevRow != null && canFollowPrev && rng.NextDouble() < 0.5)
|
||
{
|
||
// 沿用前一行此字段值,从而触发合并
|
||
var prevVal = prevRow[field];
|
||
row[field] = prevVal != null ? JsonNode.Parse(prevVal.ToJsonString()) : (JsonNode?)$"{field}_合并组1";
|
||
}
|
||
else
|
||
{
|
||
// 新合并组:随机 0-3 表示组编号
|
||
row[field] = $"{field}_合并组{rng.Next(1, 5)}";
|
||
}
|
||
continue;
|
||
}
|
||
|
||
row[field] = contentType switch
|
||
{
|
||
"number" => (i + 1) * 123.45,
|
||
"amount" => (i + 1) * 24567.89,
|
||
"qrcode" => $"QR_{field}_{i + 1}",
|
||
"barcode" => $"BAR_{field}_{i + 1}",
|
||
"image" => $"https://picsum.photos/seed/{Uri.EscapeDataString(field + "_" + (i + 1))}/260/120",
|
||
_ => $"{field}_示例值_{i + 1}"
|
||
};
|
||
}
|
||
rows.Add(row);
|
||
prevRow = 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("&", "&").Replace("<", "<").Replace(">", ">");
|
||
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();
|
||
}
|