|
diff --git a/jeecgboot-vue3/src/views/print/template/native/components/elements/TextElement.vue b/jeecgboot-vue3/src/views/print/template/native/components/elements/TextElement.vue
index 72bc1ee..5957f7a 100644
--- a/jeecgboot-vue3/src/views/print/template/native/components/elements/TextElement.vue
+++ b/jeecgboot-vue3/src/views/print/template/native/components/elements/TextElement.vue
@@ -36,20 +36,32 @@
return props.element.text || '';
});
- const styleObject = computed(() => ({
- fontSize: `${props.element.style?.fontSize || 12}px`,
- fontWeight: String(props.element.style?.fontWeight || 400),
- color: props.element.style?.color || '#111',
- textAlign: props.element.style?.textAlign || 'left',
- lineHeight: String(props.element.style?.lineHeight || 1.4),
- width: '100%',
- height: '100%',
- whiteSpace: 'pre-wrap',
- overflow: 'hidden',
- backgroundColor: props.element.style?.backgroundColor || 'transparent',
- display: (props.element as any)?.visible === false ? 'none' : 'block',
- borderTop: props.element.type === 'reportHeader' || props.element.type === 'reportFooter' ? '1px dashed rgba(22,119,255,0.5)' : 'none',
- borderBottom: props.element.type === 'reportHeader' || props.element.type === 'reportFooter' ? '1px dashed rgba(22,119,255,0.5)' : 'none',
- background: props.element.type === 'reportHeader' || props.element.type === 'reportFooter' ? 'rgba(22,119,255,0.06)' : props.element.style?.backgroundColor || 'transparent',
- }));
+ const styleObject = computed(() => {
+ const isBand = props.element.type === 'reportHeader' || props.element.type === 'reportFooter';
+ const bw = Math.max(0, Number(props.element.style?.borderWidth || 0));
+ const bc = props.element.style?.borderColor || '#222';
+ const normalBorderTop = bw > 0 && props.element.style?.hideBorderTop !== true ? `${bw}px solid ${bc}` : 'none';
+ const normalBorderRight = bw > 0 && props.element.style?.hideBorderRight !== true ? `${bw}px solid ${bc}` : 'none';
+ const normalBorderBottom = bw > 0 && props.element.style?.hideBorderBottom !== true ? `${bw}px solid ${bc}` : 'none';
+ const normalBorderLeft = bw > 0 && props.element.style?.hideBorderLeft !== true ? `${bw}px solid ${bc}` : 'none';
+ return {
+ fontSize: `${props.element.style?.fontSize || 12}px`,
+ fontWeight: String(props.element.style?.fontWeight || 400),
+ color: props.element.style?.color || '#111',
+ textAlign: props.element.style?.textAlign || 'left',
+ lineHeight: String(props.element.style?.lineHeight || 1.4),
+ width: '100%',
+ height: '100%',
+ boxSizing: 'border-box',
+ whiteSpace: 'pre-wrap',
+ overflow: 'hidden',
+ display: (props.element as any)?.visible === false ? 'none' : 'block',
+ // 报表头/尾保留原蓝色虚线辅助;标题/副标题/正文走用户配置四边边框
+ borderTop: isBand ? '1px dashed rgba(22,119,255,0.5)' : normalBorderTop,
+ borderRight: isBand ? 'none' : normalBorderRight,
+ borderBottom: isBand ? '1px dashed rgba(22,119,255,0.5)' : normalBorderBottom,
+ borderLeft: isBand ? 'none' : normalBorderLeft,
+ background: isBand ? 'rgba(22,119,255,0.06)' : props.element.style?.backgroundColor || 'transparent',
+ };
+ });
diff --git a/jeecgboot-vue3/src/views/print/template/native/core/barcodeRenderer.ts b/jeecgboot-vue3/src/views/print/template/native/core/barcodeRenderer.ts
new file mode 100644
index 0000000..7d9de52
--- /dev/null
+++ b/jeecgboot-vue3/src/views/print/template/native/core/barcodeRenderer.ts
@@ -0,0 +1,146 @@
+/**
+ * 共享的 1D 条码渲染工具:用 jsbarcode 生成 SVG 字符串/元素。
+ *
+ * 关键设计:
+ * - 保比例:返回的 SVG 设置 preserveAspectRatio="xMidYMid meet",CSS 100% 拉伸时
+ * 会保持原始条宽/条高比,避免被容器横向拉长后变形。
+ * - 容器铺满:内部 viewBox 由 jsbarcode 按 width/height 参数自动写入,外层只需
+ * width:100%;height:100% 即可让 SVG 在容器内最大化保比例显示。
+ * - 兼容设计器画布与打印路径(printRenderer.ts)两种使用场景:画布走 renderInto,
+ * 打印走 buildString 拼到 HTML 字符串中。
+ */
+
+export interface NativeBarcodeOptions {
+ /** Code128 / EAN13 / EAN8 / UPC / CODE39 等,默认 CODE128 */
+ format?: string;
+ /** 是否在底部显示文本,默认 true */
+ displayValue?: boolean;
+ /** 单根细线宽度(像素),默认 2 */
+ lineWidth?: number;
+ /** 条码主体高度(像素,不含文字),默认 60 */
+ barHeight?: number;
+ /** 底部文本字号(像素),默认 14 */
+ fontSize?: number;
+ /** 上下左右安静区(像素),默认 0 */
+ margin?: number;
+ /** 前景色(条),默认黑 */
+ lineColor?: string;
+ /** 背景色,默认白 */
+ background?: string;
+ /**
+ * 条码下文字对齐:center / left / right 直接交由 jsbarcode 原生 textAlign;
+ * justify(两端对齐)在 jsbarcode center 输出基础上后处理,给 加 textLength
+ * + lengthAdjust=spacing,由浏览器拉伸字符间距让文字横向铺满条码宽度。
+ */
+ textAlign?: 'left' | 'center' | 'right' | 'justify';
+}
+
+const SVG_NS = 'http://www.w3.org/2000/svg';
+
+function resolveOptions(value: string, options: NativeBarcodeOptions | undefined) {
+ const text = String(value ?? '').trim() || '0000000000';
+ const rawAlign = (options?.textAlign || 'center').toLowerCase();
+ const isJustify = rawAlign === 'justify';
+ // jsbarcode 不直接支持 justify,先按 center 出 SVG,再后处理拉伸字符间距
+ const jsAlign = (['left', 'center', 'right'] as const).includes(rawAlign as any) ? (rawAlign as 'left' | 'center' | 'right') : 'center';
+ return {
+ text,
+ format: options?.format || 'CODE128',
+ displayValue: options?.displayValue !== false,
+ width: Math.max(0.5, Number(options?.lineWidth ?? 2)),
+ height: Math.max(10, Number(options?.barHeight ?? 60)),
+ fontSize: Math.max(8, Number(options?.fontSize ?? 14)),
+ margin: Math.max(0, Number(options?.margin ?? 0)),
+ lineColor: options?.lineColor || '#000000',
+ background: options?.background || '#ffffff',
+ textAlign: jsAlign,
+ justifyText: isJustify,
+ };
+}
+
+/**
+ * 把条码渲染到一个已存在的 SVG 元素中(用于 Vue 组件内的 svg ref)。
+ * 渲染完成后会清除 svg 自身的 width/height 属性,并设置 preserveAspectRatio
+ * 以便外层 CSS 100% 高宽时仍保持原比例。
+ */
+export async function renderNativeBarcodeIntoSvg(
+ svg: SVGSVGElement,
+ value: string,
+ options?: NativeBarcodeOptions,
+): Promise {
+ const module: any = await import('jsbarcode');
+ const JsBarcode = module.default || module;
+ const opts = resolveOptions(value, options);
+ try {
+ JsBarcode(svg, opts.text, {
+ format: opts.format,
+ displayValue: opts.displayValue,
+ margin: opts.margin,
+ width: opts.width,
+ height: opts.height,
+ fontSize: opts.fontSize,
+ lineColor: opts.lineColor,
+ background: opts.background,
+ textAlign: opts.textAlign,
+ });
+ // 关键修复:jsbarcode 默认只设 width/height 属性,没有 viewBox。我们若直接移除
+ // width/height,SVG 会失去几何坐标系,preserveAspectRatio 不生效——
+ // 在设计画布(外层 flex+overflow:hidden)里浏览器会按 user units 裁切显示成"满铺",
+ // 在打印输出 div 里却按缩水的 user units 居中,造成两边视觉差异。
+ // 修复:用 jsbarcode 设置的 width/height 派生 viewBox,再交给 CSS 控制最终尺寸,
+ // preserveAspectRatio="xMidYMid meet" 才能保比例统一缩放。
+ const rawWidth = parseFloat(svg.getAttribute('width') || '0');
+ const rawHeight = parseFloat(svg.getAttribute('height') || '0');
+ if (rawWidth > 0 && rawHeight > 0 && !svg.getAttribute('viewBox')) {
+ svg.setAttribute('viewBox', `0 0 ${rawWidth} ${rawHeight}`);
+ }
+ svg.removeAttribute('width');
+ svg.removeAttribute('height');
+ svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
+ // 两端对齐:找到底部文字 ,让浏览器把字符间距自动拉伸到条码宽度
+ if (opts.displayValue && opts.justifyText) {
+ applyJustifyTextAlign(svg);
+ }
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * 把 jsbarcode 输出的底部文字()改成两端对齐:保留原 y/字号,重置 x=0、
+ * text-anchor=start,并加 textLength=条码宽 + lengthAdjust=spacing,
+ * 浏览器会自动拉伸字符间距让文字横向铺满条码宽度。
+ */
+function applyJustifyTextAlign(svg: SVGSVGElement) {
+ const texts = svg.querySelectorAll('text');
+ if (!texts.length) return;
+ // jsbarcode 输出的 SVG 第一个有 viewBox/width,取其宽度作为目标 textLength
+ const viewBox = (svg.getAttribute('viewBox') || '').split(/\s+/).map((n) => Number(n) || 0);
+ const targetWidth = viewBox.length === 4 && viewBox[2] > 0 ? viewBox[2] : Number(svg.getAttribute('width')) || 0;
+ if (!targetWidth) return;
+ texts.forEach((t) => {
+ t.setAttribute('x', '0');
+ t.setAttribute('text-anchor', 'start');
+ t.setAttribute('textLength', String(targetWidth));
+ t.setAttribute('lengthAdjust', 'spacing');
+ });
+}
+
+/**
+ * 生成可直接拼入 HTML 字符串的 `` 文本(供打印 HTML 渲染使用)。
+ * 失败时回退到带条码内容的占位 div 字符串,便于使用方区分。
+ */
+export async function buildNativeBarcodeSvgString(
+ value: string,
+ options?: NativeBarcodeOptions,
+): Promise {
+ if (typeof document === 'undefined') {
+ return '';
+ }
+ const svg = document.createElementNS(SVG_NS, 'svg') as SVGSVGElement;
+ const ok = await renderNativeBarcodeIntoSvg(svg, value, options);
+ if (!ok) return '';
+ // 让外层布局可以通过 CSS 控制宽高(width/height attribute 已在 renderInto 中清除)
+ return new XMLSerializer().serializeToString(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 837109c..7d9f9ea 100644
--- a/jeecgboot-vue3/src/views/print/template/native/core/printRenderer.ts
+++ b/jeecgboot-vue3/src/views/print/template/native/core/printRenderer.ts
@@ -7,6 +7,7 @@ import { normalizeFreeTableAnchors } from './freeTableGrid';
import { borderSidesToCssFragment, resolveFreeTableCellBorderSides } from './freeTableBorders';
import { resolveFreeTableCellLineStyleKeys } from './freeTableLineStyles';
import { resolveFreeTableColWidthsMm, resolveFreeTableRowHeightsMm } from './freeTableTracks';
+import { buildNativeBarcodeSvgString } from './barcodeRenderer';
function resolveBoundValue(element: NativeElement, data: Record) {
const bindField = (element as any).bindField;
@@ -48,10 +49,10 @@ async function renderFreeTable(element: NativeFreeTableElement, data: Record${colWidthsMm.map((cw) => ``).join('')}`;
const anchors = normalizeFreeTableAnchors(rowCount, colCount, (element as any)?.cells || []);
const body = (
@@ -79,12 +80,21 @@ async function renderFreeTable(element: NativeFreeTableElement, data: Record 1 ? ` rowspan="${rs}"` : '';
const colspanAttr = cs > 1 ? ` colspan="${cs}"` : '';
const spanW = colWidthsMm.slice(cell.col, cell.col + cs).reduce((a, b) => a + b, 0);
+ const spanH = rowHeightsMm.slice(cell.row, cell.row + rs).reduce((a, b) => a + b, 0);
+ // 行高很小(4~5mm)时固定 2mm padding 会把行撑爆,导致后续行被容器裁切。
+ // 这里改为按单元格高度自适应,保证高密度标签场景仍能完整显示所有行线。
+ const vPadMm = Math.max(0.15, Math.min(0.8, spanH * 0.08));
+ const hPadMm = Math.max(0.3, Math.min(1.2, vPadMm * 1.6));
+ // 随行密度自动收缩字体:优先保证全部行可见(与画布规则保持一致)
+ const innerHmm = Math.max(0.1, spanH - vPadMm * 2);
+ const innerHpx = innerHmm * (96 / 25.4);
+ const fitFontSize = Math.max(1, Math.min(baseFontSize, Math.floor(innerHpx * 0.82)));
const colWidthStyle = `width:${spanW}mm;`;
const sides = resolveFreeTableCellBorderSides(element, anchors, cell, cell.row, cell.col, rs, cs, rowCount, colCount);
const lineKeys = resolveFreeTableCellLineStyleKeys(element, cell.row, cell.col, rs, cs, rowCount, colCount);
@@ -93,8 +103,8 @@ async function renderFreeTable(element: NativeFreeTableElement, data: Record${bodyInnerHtml} | `;
}),
)
@@ -107,6 +117,28 @@ async function renderFreeTable(element: NativeFreeTableElement, data: Record${colgroup}${body}