支持模板预览/字段字典、产品绑定 LabelTemplateId、订单 Pending→Ready 生成 PDF,以及 order/print、批量与仓内 printUrl。 Co-authored-by: Cursor <cursoragent@cursor.com>
202 lines
6.4 KiB
C#
202 lines
6.4 KiB
C#
using System.Globalization;
|
||
using System.Text.Json;
|
||
using Fake.Application;
|
||
using Fake.DependencyInjection;
|
||
|
||
namespace Oms.Application.Labeling.Hiprint;
|
||
|
||
/// <summary>
|
||
/// 将 hiprint TemplateJson 转为中间绘制 IR。
|
||
/// </summary>
|
||
public sealed class HiprintTemplateParser : ITransientDependency
|
||
{
|
||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||
{
|
||
PropertyNameCaseInsensitive = true,
|
||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||
AllowTrailingCommas = true
|
||
};
|
||
|
||
public LabelPageIr Parse(string templateJson, IReadOnlyDictionary<string, object?> printData)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(templateJson))
|
||
{
|
||
throw new ArgumentException("模板 JSON 为空", nameof(templateJson));
|
||
}
|
||
|
||
var doc = JsonSerializer.Deserialize<HiprintTemplateDocument>(templateJson, JsonOptions)
|
||
?? throw new BusinessException("模板 JSON 无法解析");
|
||
|
||
var panel = doc.Panels.OrderBy(x => x.Index).FirstOrDefault()
|
||
?? throw new BusinessException("模板缺少 panels");
|
||
|
||
var widthMm = panel.Width > 0 ? panel.Width : 100f;
|
||
var heightMm = panel.Height > 0 ? panel.Height : 150f;
|
||
var ops = new List<LabelDrawOp>();
|
||
|
||
foreach (var element in panel.PrintElements)
|
||
{
|
||
var op = MapElement(element, printData);
|
||
if (op != null)
|
||
{
|
||
ops.Add(op);
|
||
}
|
||
}
|
||
|
||
return new LabelPageIr
|
||
{
|
||
WidthMm = widthMm,
|
||
HeightMm = heightMm,
|
||
Ops = ops
|
||
};
|
||
}
|
||
|
||
private static LabelDrawOp? MapElement(
|
||
HiprintPrintElement element,
|
||
IReadOnlyDictionary<string, object?> printData)
|
||
{
|
||
var type = element.PrintElementType?.Type?.Trim().ToLowerInvariant() ?? "text";
|
||
var opt = element.Options;
|
||
var left = opt.Left;
|
||
var top = opt.Top;
|
||
var width = Math.Max(opt.Width, 1f);
|
||
var height = Math.Max(opt.Height, 1f);
|
||
|
||
switch (type)
|
||
{
|
||
case "hline":
|
||
return new LabelLineOp
|
||
{
|
||
LeftPt = left,
|
||
TopPt = top,
|
||
WidthPt = width,
|
||
HeightPt = Math.Max(opt.BorderWidth ?? 1f, 0.5f),
|
||
Vertical = false,
|
||
ColorHex = opt.Color,
|
||
ThicknessPt = opt.BorderWidth ?? 1f
|
||
};
|
||
case "vline":
|
||
return new LabelLineOp
|
||
{
|
||
LeftPt = left,
|
||
TopPt = top,
|
||
WidthPt = Math.Max(opt.BorderWidth ?? 1f, 0.5f),
|
||
HeightPt = height,
|
||
Vertical = true,
|
||
ColorHex = opt.Color,
|
||
ThicknessPt = opt.BorderWidth ?? 1f
|
||
};
|
||
case "rect":
|
||
return new LabelRectOp
|
||
{
|
||
LeftPt = left,
|
||
TopPt = top,
|
||
WidthPt = width,
|
||
HeightPt = height,
|
||
BorderColorHex = opt.Color,
|
||
BorderWidthPt = opt.BorderWidth ?? 1f
|
||
};
|
||
case "image":
|
||
return new LabelImageOp
|
||
{
|
||
LeftPt = left,
|
||
TopPt = top,
|
||
WidthPt = width,
|
||
HeightPt = height,
|
||
Src = ResolveValue(opt.Field, opt.Src ?? opt.Title, printData)
|
||
};
|
||
case "longtext":
|
||
return BuildTextOp(opt, left, top, width, height, printData, wrap: true);
|
||
case "text":
|
||
default:
|
||
{
|
||
var textType = opt.TextType?.Trim().ToLowerInvariant();
|
||
if (textType is "barcode" or "qrcode")
|
||
{
|
||
var content = ResolveValue(opt.Field, opt.Title, printData);
|
||
if (string.IsNullOrWhiteSpace(content))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return new LabelBarcodeOp
|
||
{
|
||
LeftPt = left,
|
||
TopPt = top,
|
||
WidthPt = width,
|
||
HeightPt = height,
|
||
Content = content,
|
||
QrCode = textType == "qrcode"
|
||
};
|
||
}
|
||
|
||
return BuildTextOp(opt, left, top, width, height, printData, wrap: false);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static LabelTextOp? BuildTextOp(
|
||
HiprintElementOptions opt,
|
||
float left,
|
||
float top,
|
||
float width,
|
||
float height,
|
||
IReadOnlyDictionary<string, object?> printData,
|
||
bool wrap)
|
||
{
|
||
var value = ResolveValue(opt.Field, null, printData);
|
||
string text;
|
||
if (!string.IsNullOrWhiteSpace(opt.Field))
|
||
{
|
||
// 有 field:默认只打字段值;hideTitle=false 且有 title 时拼「标题:值」
|
||
if (opt.HideTitle == false && !string.IsNullOrWhiteSpace(opt.Title) && !string.IsNullOrWhiteSpace(value))
|
||
{
|
||
text = $"{opt.Title}:{value}";
|
||
}
|
||
else
|
||
{
|
||
text = value ?? string.Empty;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
text = opt.Title ?? string.Empty;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(text))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var bold = string.Equals(opt.FontWeight, "bold", StringComparison.OrdinalIgnoreCase)
|
||
|| opt.FontWeight is "700" or "600";
|
||
|
||
return new LabelTextOp
|
||
{
|
||
LeftPt = left,
|
||
TopPt = top,
|
||
WidthPt = width,
|
||
HeightPt = height,
|
||
Text = text,
|
||
FontSizePt = opt.FontSize is > 0 ? opt.FontSize.Value : 10f,
|
||
Bold = bold,
|
||
ColorHex = opt.Color,
|
||
Align = (opt.TextAlign ?? "left").ToLowerInvariant(),
|
||
Wrap = wrap
|
||
};
|
||
}
|
||
|
||
private static string? ResolveValue(
|
||
string? field,
|
||
string? fallback,
|
||
IReadOnlyDictionary<string, object?> printData)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(field) && printData.TryGetValue(field, out var raw) && raw != null)
|
||
{
|
||
return Convert.ToString(raw, CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
return fallback;
|
||
}
|
||
}
|