From 210f3614eaf53cf454d7fc481030049167746265 Mon Sep 17 00:00:00 2001
From: geht <2947093423@qq.com>
Date: Wed, 13 May 2026 14:06:13 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9D=A1=E7=A0=81=E5=85=83?=
=?UTF-8?q?=E7=B4=A0=E7=9A=84=E5=A1=AB=E5=85=85=E9=80=89=E9=A1=B9=EF=BC=8C?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=E6=97=A0=E6=8D=9F=E5=A1=AB=E6=BB=A1=E5=A4=96?=
=?UTF-8?q?=E5=B1=82=E5=AE=B9=E5=99=A8=E7=9A=84=E9=85=8D=E7=BD=AE=E3=80=82?=
=?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E6=B8=B2=E6=9F=93=E9=80=BB?=
=?UTF-8?q?=E8=BE=91=E5=92=8C=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=8F=90=E5=8D=87?=
=?UTF-8?q?=E6=9D=A1=E7=A0=81=E6=98=BE=E7=A4=BA=E7=9A=84=E7=81=B5=E6=B4=BB?=
=?UTF-8?q?=E6=80=A7=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../native/components/PropertiesPanel.vue | 7 +
.../components/elements/BarcodeElement.vue | 2 +
.../template/native/core/barcodeRenderer.ts | 12 +-
.../template/native/core/printRenderer.ts | 1 +
.../views/print/template/native/core/types.ts | 9 +
.../Service/Print/NativePrintRenderService.cs | 259 +++++++++++++-----
6 files changed, 221 insertions(+), 69 deletions(-)
diff --git a/jeecgboot-vue3/src/views/print/template/native/components/PropertiesPanel.vue b/jeecgboot-vue3/src/views/print/template/native/components/PropertiesPanel.vue
index c14f03c..a8f5caa 100644
--- a/jeecgboot-vue3/src/views/print/template/native/components/PropertiesPanel.vue
+++ b/jeecgboot-vue3/src/views/print/template/native/components/PropertiesPanel.vue
@@ -225,6 +225,13 @@
un-checked-children="隐藏条码下文字"
@update:checked="updateField('displayValue', $event)"
/>
+
,让浏览器把字符间距自动拉伸到条码宽度
if (opts.displayValue && opts.justifyText) {
applyJustifyTextAlign(svg);
diff --git a/jeecgboot-vue3/src/views/print/template/native/core/printRenderer.ts b/jeecgboot-vue3/src/views/print/template/native/core/printRenderer.ts
index 7d9f9ea..d6086bd 100644
--- a/jeecgboot-vue3/src/views/print/template/native/core/printRenderer.ts
+++ b/jeecgboot-vue3/src/views/print/template/native/core/printRenderer.ts
@@ -558,6 +558,7 @@ export async function renderNativePrintHtml(schema: NativeTemplateSchema, data:
lineWidth: (item as any).lineWidth,
barHeight: (item as any).barHeight,
textAlign: (item as any).textAlign,
+ fillCell: (item as any).fillCell === true,
});
if (svgStr) {
// 让 SVG 100% 充满定位 div;preserveAspectRatio 在 SVG 内部已设为 xMidYMid meet,
diff --git a/jeecgboot-vue3/src/views/print/template/native/core/types.ts b/jeecgboot-vue3/src/views/print/template/native/core/types.ts
index 937f783..62278b5 100644
--- a/jeecgboot-vue3/src/views/print/template/native/core/types.ts
+++ b/jeecgboot-vue3/src/views/print/template/native/core/types.ts
@@ -73,6 +73,15 @@ export interface NativeImageElement extends NativeElementBase {
export interface NativeCodeElement extends NativeElementBase {
type: 'qrcode' | 'barcode';
value: string;
+ /**
+ * 是否无损铺满整个编辑框(条码):
+ * - false / undefined(默认):preserveAspectRatio="xMidYMid meet",保比例居中,
+ * 扁容器中上下会留白,但条宽比例严格 1:1,扫码兼容性最佳。
+ * - true:preserveAspectRatio="none",SVG 矢量按容器宽高拉伸铺满;
+ * 放大缩小不会失真(仍是矢量),但条宽/条高比会随容器变化,
+ * 适合需要"卡片整格塞满"的小票/标签场景。
+ */
+ fillCell?: boolean;
}
export interface NativeTableColumn {
diff --git a/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs b/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs
index 99dffbc..25aca51 100644
--- a/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs
+++ b/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs
@@ -345,8 +345,12 @@ public static class NativePrintRenderService
var lineWidthPx = Math.Max(1, (int)Math.Round(el["lineWidth"]?.GetValue
() ?? 2d));
var barHeightPx = Math.Max(10, (int)Math.Round(el["barHeight"]?.GetValue() ?? 60d));
var barFontSize = Math.Max(8, (int)Math.Round(el["fontSize"]?.GetValue() ?? 14d));
+ // 与后端对齐:优先读元素级 fillCell;兼容旧模板的 style.fillCell。
+ // 均未配置时默认 false(保比例居中)。
+ var fillCellNode = el["fillCell"] ?? el["style"]?["fillCell"];
+ var fillCell = string.Equals(ReadAsString(fillCellNode, "false"), "true", StringComparison.OrdinalIgnoreCase);
- var inner = BuildBarcodeSvgInner(value, format, displayValue, textAlign, lineWidthPx, barHeightPx, barFontSize);
+ var inner = BuildBarcodeSvgInner(value, format, displayValue, textAlign, lineWidthPx, barHeightPx, barFontSize, fillCell);
var wrapStyle = "display:flex;align-items:center;justify-content:center;overflow:hidden;";
@@ -426,38 +430,53 @@ public static class NativePrintRenderService
}
///
- /// 把 ZXing 输出的 SVG 转换为"由外层 CSS 控制尺寸 + 保持原始条宽比例"的形式:
- /// 1) 若原 SVG 无 viewBox,用原 width/height 派生(让 preserveAspectRatio 生效);
- /// 2) 删除根 svg 节点的 width/height 属性(移交给 CSS);
- /// 3) 强制 preserveAspectRatio="xMidYMid meet"(与 web 端 jsbarcode 一致);
- /// 4) 重新挂上 width="100%" height="100%",确保 div 100% 铺满。
- /// 防护层:若 ZXing 输出的 SVG 已带 viewBox,直接复用;否则从 width/height 推导。
+ /// 把 ZXing 输出的 SVG 转换为"由外层 CSS 控制尺寸 + 可配置铺满策略"的形式:
+ /// 1) 强制清除根 svg 节点上原有的 width / height / viewBox / preserveAspectRatio 属性
+ /// (不再依赖 ZXing 自身输出格式,无论它用单引号、双引号、无引号都覆写);
+ /// 2) 用调用方传入的 viewBoxWidth × viewBoxHeight 作为 viewBox,保证容器内
+ /// 条码比例与 BuildBarcodeSvgInner 计算的画布比例一致;
+ /// 3) preserveAspectRatio:fillCell=true → "none"(按容器拉伸铺满,矢量缩放无损);
+ /// fillCell=false → "xMidYMid meet"(保比例居中,与 web jsbarcode 默认一致);
+ /// 4) 重新挂上 width="100%" height="100%",确保外层 div 100% 铺满。
+ /// 这里不再"尝试从原 SVG 派生 viewBox",否则 regex 不匹配 ZXing 实际输出(单引号 /
+ /// 无属性 / 顺序差异)时,viewBox 会被漏掉,导致 SVG 被 CSS 双向拉伸,条码视觉变粗、变高。
///
- private static string? NormalizeBarcodeSvg(string svg)
+ private static string? NormalizeBarcodeSvg(string svg, bool fillCell = false,
+ double viewBoxWidth = 0, double viewBoxHeight = 0)
{
if (string.IsNullOrWhiteSpace(svg)) return null;
+ var targetAspect = fillCell ? "none" : "xMidYMid meet";
return Regex.Replace(svg, @"", RegexOptions.IgnoreCase);
+ }
+
+ ///
+ /// 收紧条码水平 viewBox,裁掉左右静区;仅调整 x/width,不改 y/height。
+ ///
+ private static string TightenBarcodeHorizontalViewBox(string svg, double fallbackWidth)
+ {
+ var (x0, y0, vbW, vbH) = ParseViewBoxOrDefault(svg, fallbackWidth);
+ var (inkLeft, inkRight) = ResolveBarcodeInkBounds(svg, vbW);
+ var inkW = inkRight - inkLeft;
+ if (inkW <= 1e-6) return svg;
+
+ var newX = x0 + inkLeft;
+ var newViewBox = $"viewBox=\"{newX.ToString("0.###", CultureInfo.InvariantCulture)} {y0.ToString("0.###", CultureInfo.InvariantCulture)} {inkW.ToString("0.###", CultureInfo.InvariantCulture)} {vbH.ToString("0.###", CultureInfo.InvariantCulture)}\"";
+ return Regex.Replace(svg, @"viewBox\s*=\s*""[^""]+""", newViewBox, RegexOptions.IgnoreCase);
+ }
+
+ private static (double x0, double y0, double w, double h) ParseViewBoxOrDefault(string svg, double fallbackWidth = 200, double fallbackHeight = 60)
+ {
+ var m = Regex.Match(svg ?? string.Empty, @"viewBox\s*=\s*""([^""]+)""", RegexOptions.IgnoreCase);
+ if (m.Success)
+ {
+ var parts = Regex.Split(m.Groups[1].Value.Trim(), @"[\s,]+");
if (parts.Length >= 4
- && double.TryParse(parts[2], NumberStyles.Any, CultureInfo.InvariantCulture, out var w) && w > 0)
+ && double.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var x0)
+ && double.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var y0)
+ && double.TryParse(parts[2], NumberStyles.Any, CultureInfo.InvariantCulture, out var w)
+ && double.TryParse(parts[3], NumberStyles.Any, CultureInfo.InvariantCulture, out var h)
+ && w > 0 && h > 0)
{
- vbWidth = w;
+ return (x0, y0, w, h);
}
}
- if (vbWidth <= 0) return svg; // 拿不到 viewBox 宽度时不动,保持 ZXing 默认居中
+ return (0d, 0d, Math.Max(1d, fallbackWidth), Math.Max(1d, fallbackHeight));
+ }
- return Regex.Replace(svg, @"]*)>", m =>
+ ///
+ /// 从 SVG 中提取条码“黑色墨迹”的左右边界。
+ /// 优先扫描 <rect>,忽略白色/透明填充;失败时回退到整块 viewBox。
+ ///
+ private static (double left, double right) ResolveBarcodeInkBounds(string svg, double fallbackWidth)
+ {
+ if (string.IsNullOrWhiteSpace(svg))
+ return (0d, Math.Max(1d, fallbackWidth));
+
+ var rects = Regex.Matches(svg, @"]*>", RegexOptions.IgnoreCase);
+ double minX = double.PositiveInfinity;
+ double maxX = double.NegativeInfinity;
+
+ foreach (Match m in rects)
{
- var attrs = m.Groups[1].Value;
- attrs = Regex.Replace(attrs, @"\s(x|text-anchor|textLength|lengthAdjust)\s*=\s*""[^""]*""", string.Empty, RegexOptions.IgnoreCase);
- var widthStr = vbWidth.ToString("0.###", CultureInfo.InvariantCulture);
+ var tag = m.Value;
- return align switch
- {
- "left" => $"",
- "right" => $"",
- "justify" => $"",
- _ => m.Value,
- };
- }, RegexOptions.IgnoreCase);
+ // 跳过明显的白底/透明背景 rect
+ var fill = ReadSvgAttr(tag, "fill")?.Trim().ToLowerInvariant();
+ if (fill is "none" or "transparent" or "#fff" or "#ffffff" or "white")
+ continue;
+
+ var xs = ReadSvgAttr(tag, "x");
+ var ws = ReadSvgAttr(tag, "width");
+ if (!TryParseSvgNumber(xs, out var x) || !TryParseSvgNumber(ws, out var w) || w <= 0)
+ continue;
+
+ if (x < minX) minX = x;
+ if (x + w > maxX) maxX = x + w;
+ }
+
+ if (double.IsInfinity(minX) || double.IsInfinity(maxX) || maxX <= minX)
+ return (0d, Math.Max(1d, fallbackWidth));
+
+ return (minX, maxX);
+ }
+
+ private static string? ReadSvgAttr(string tag, string attr)
+ {
+ if (string.IsNullOrWhiteSpace(tag) || string.IsNullOrWhiteSpace(attr))
+ return null;
+ var mm = Regex.Match(tag, $@"\b{Regex.Escape(attr)}\s*=\s*(""([^""]*)""|'([^']*)')", RegexOptions.IgnoreCase);
+ if (!mm.Success) return null;
+ return !string.IsNullOrEmpty(mm.Groups[2].Value) ? mm.Groups[2].Value : mm.Groups[3].Value;
+ }
+
+ private static bool TryParseSvgNumber(string? raw, out double value)
+ {
+ value = 0;
+ if (string.IsNullOrWhiteSpace(raw)) return false;
+ var s = raw.Trim();
+ // 支持 "123", "123.45", "123px";百分比不参与墨迹边界计算
+ if (s.EndsWith("%", StringComparison.Ordinal)) return false;
+ s = Regex.Replace(s, @"[^\d\.\-+eE]", "");
+ return double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out value);
}
///
@@ -1617,9 +1740,9 @@ public static class NativePrintRenderService
}
if (t == "barcode")
{
- var fmt = ReadAsString(col?["barcodeFormat"]);
- var dv = !string.Equals(ReadAsString(col?["displayValue"]), "false", StringComparison.OrdinalIgnoreCase);
- var bFontSize = Math.Max(8, (int)Math.Round(col?["barcodeFontSize"]?.GetValue() ?? 14d));
+ var fmt = ReadAsString(col?["barcodeFormat"]);
+ var dv = !string.Equals(ReadAsString(col?["displayValue"]), "false", StringComparison.OrdinalIgnoreCase);
+ var bFontSize = Math.Max(8, (int)Math.Round(col?["barcodeFontSize"]?.GetValue() ?? 14d));
var svg = BuildBarcodeCellSvg(value, fmt, dv, null, bFontSize);
return $"{svg}
";
}