@jcyao/print-sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +106 -0
- package/README.md +198 -20
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +487 -84
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +488 -83
- package/dist/index.js.map +1 -1
- package/dist/printEngine/constants.d.ts +2 -2
- package/dist/printEngine/renderers/PageNumberRenderer.d.ts +22 -0
- package/dist/printEngine/renderers/index.d.ts +1 -0
- package/dist/printEngine.d.ts +19 -3
- package/dist/sdk.d.ts +2 -2
- package/dist/types.d.ts +25 -0
- package/dist/utils/resourceLoader.d.ts +19 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -50,8 +50,8 @@ const TABLE_DEFAULT = {
|
|
|
50
50
|
HEADER_HEIGHT: 10,
|
|
51
51
|
/** 数据行最小高度(mm) */
|
|
52
52
|
MIN_ROW_HEIGHT: 8,
|
|
53
|
-
/**
|
|
54
|
-
ROW_HEIGHT_FACTOR: 1.
|
|
53
|
+
/** 行高计算系数(用于 min-height,实际高度由内容撑开) */
|
|
54
|
+
ROW_HEIGHT_FACTOR: 1.0,
|
|
55
55
|
};
|
|
56
56
|
/**
|
|
57
57
|
* 样式默认值
|
|
@@ -548,7 +548,7 @@ function buildPositionStyle(xMm, yMm, widthMm, heightMm, mmToPx = 3.78) {
|
|
|
548
548
|
styles.width = `${widthMm * mmToPx}px`;
|
|
549
549
|
}
|
|
550
550
|
if (heightMm !== undefined) {
|
|
551
|
-
styles.
|
|
551
|
+
styles.height = `${heightMm * mmToPx}px`;
|
|
552
552
|
}
|
|
553
553
|
return styles;
|
|
554
554
|
}
|
|
@@ -606,7 +606,11 @@ class TableRenderer {
|
|
|
606
606
|
// 表头配置,过滤隐藏列
|
|
607
607
|
const allColumns = (props === null || props === void 0 ? void 0 : props.columns) || [];
|
|
608
608
|
const visibleColumns = allColumns.filter((col) => !col.hidden);
|
|
609
|
-
|
|
609
|
+
// 支持分页传入的 _showHeader 标记,优先于 props.showHeader
|
|
610
|
+
const explicitShowHeader = props && typeof props._showHeader === 'boolean'
|
|
611
|
+
? props._showHeader
|
|
612
|
+
: undefined;
|
|
613
|
+
const showHeader = explicitShowHeader !== undefined ? explicitShowHeader : (props === null || props === void 0 ? void 0 : props.showHeader) !== false;
|
|
610
614
|
const bordered = (props === null || props === void 0 ? void 0 : props.bordered) !== false;
|
|
611
615
|
// 计算表格宽度
|
|
612
616
|
let tableWidthMm;
|
|
@@ -635,9 +639,17 @@ class TableRenderer {
|
|
|
635
639
|
// 备用:使用默认值
|
|
636
640
|
tableWidthMm = COMPONENT_DEFAULT_SIZE.TABLE_WIDTH;
|
|
637
641
|
}
|
|
642
|
+
// ✅ 检查 xMm 是否已超过可用宽度
|
|
643
|
+
const availableWidth = context.pageInfo
|
|
644
|
+
? context.pageInfo.widthMm - context.pageInfo.marginMm.left - context.pageInfo.marginMm.right
|
|
645
|
+
: COMPONENT_DEFAULT_SIZE.TABLE_WIDTH;
|
|
646
|
+
if (xMm >= availableWidth) {
|
|
647
|
+
console.error(`[TableRenderer] 表格位置错误:xMm(${xMm.toFixed(2)}mm) 已超过页面可用宽度(${availableWidth.toFixed(2)}mm),` +
|
|
648
|
+
`表格将无法正常显示。请调整表格的 x 位置,使其小于 ${availableWidth.toFixed(2)}mm`);
|
|
649
|
+
}
|
|
638
650
|
// 最小宽度保护
|
|
639
651
|
if (tableWidthMm < 10) {
|
|
640
|
-
console.warn(
|
|
652
|
+
console.warn(`[TableRenderer] 表格宽度过小 (${tableWidthMm.toFixed(2)}mm),强制设置为最小宽度 10mm`);
|
|
641
653
|
tableWidthMm = 10;
|
|
642
654
|
}
|
|
643
655
|
console.log(tableWidthMm, "tableWidthMm");
|
|
@@ -649,20 +661,26 @@ class TableRenderer {
|
|
|
649
661
|
// 单元格样式
|
|
650
662
|
const cellBorder = bordered ? `border: 1px solid ${TABLE_STYLE_DEFAULT.BORDER_COLOR};` : '';
|
|
651
663
|
const cellPadding = `padding: ${TABLE_STYLE_DEFAULT.CELL_PADDING};`;
|
|
652
|
-
const cellTextStyle = `white-space: normal; word-break: break-word; line-height: ${STYLE_DEFAULT.LINE_HEIGHT}; vertical-align:
|
|
664
|
+
const cellTextStyle = `white-space: normal; word-break: break-word; line-height: ${STYLE_DEFAULT.LINE_HEIGHT}; vertical-align: middle;`;
|
|
653
665
|
const textAlign = (style === null || style === void 0 ? void 0 : style.textAlign) || 'left'; // 对齐方式
|
|
666
|
+
// ✅ 计算均分列宽(简化方案:按列数均分表格宽度)
|
|
667
|
+
const colCount = visibleColumns.length || 1;
|
|
668
|
+
const colWidthPercent = (100 / colCount).toFixed(2);
|
|
654
669
|
// ✅ 计算表头和数据行的高度(mm 转 px)
|
|
655
|
-
|
|
656
|
-
const
|
|
670
|
+
// 使用全局常量 ROW_HEIGHT_FACTOR,实际高度由内容自然撑开(min-height)
|
|
671
|
+
const headerHeightPx = TABLE_DEFAULT.HEADER_HEIGHT * context.mmToPx;
|
|
672
|
+
const rowHeightPx = TABLE_DEFAULT.MIN_ROW_HEIGHT * TABLE_DEFAULT.ROW_HEIGHT_FACTOR * context.mmToPx;
|
|
657
673
|
// 渲染表头
|
|
658
674
|
let headerHtml = '';
|
|
659
675
|
if (showHeader && visibleColumns.length > 0) {
|
|
660
676
|
const headerCells = visibleColumns
|
|
661
677
|
.map((col) => {
|
|
662
678
|
const title = col.title || col.dataIndex;
|
|
663
|
-
|
|
679
|
+
// 使用百分比宽度实现均分,min-height 允许自然扩展
|
|
680
|
+
return `<th style="${cellBorder} ${cellPadding} ${cellTextStyle} background: ${TABLE_STYLE_DEFAULT.HEADER_BACKGROUND}; font-weight: 600; text-align: ${textAlign}; width: ${colWidthPercent}%; min-height: ${headerHeightPx}px; box-sizing: border-box;">${title}</th>`;
|
|
664
681
|
})
|
|
665
682
|
.join('');
|
|
683
|
+
// 表头使用固定高度,表体使用 min-height
|
|
666
684
|
headerHtml = `<thead class="table-header-repeat"><tr style="height: ${headerHeightPx}px;">${headerCells}</tr></thead>`;
|
|
667
685
|
}
|
|
668
686
|
// 渲染表体
|
|
@@ -673,17 +691,19 @@ class TableRenderer {
|
|
|
673
691
|
const cells = visibleColumns
|
|
674
692
|
.map((col) => {
|
|
675
693
|
const value = row[col.dataIndex] || '';
|
|
676
|
-
|
|
694
|
+
// 使用百分比宽度,min-height 允许内容换行时自然扩展
|
|
695
|
+
return `<td style="${cellBorder} ${cellPadding} ${cellTextStyle} text-align: ${textAlign}; width: ${colWidthPercent}%; min-height: ${rowHeightPx}px; box-sizing: border-box;">${value}</td>`;
|
|
677
696
|
})
|
|
678
697
|
.join('');
|
|
679
|
-
|
|
698
|
+
// 行使用 min-height 而非固定 height
|
|
699
|
+
return `<tr style="min-height: ${rowHeightPx}px;">${cells}</tr>`;
|
|
680
700
|
})
|
|
681
701
|
.join('');
|
|
682
702
|
bodyHtml = `<tbody>${rows}</tbody>`;
|
|
683
703
|
}
|
|
684
704
|
else {
|
|
685
705
|
const colspan = visibleColumns.length || 1;
|
|
686
|
-
bodyHtml = `<tbody><tr style="height: ${rowHeightPx}px;"><td colspan="${colspan}" style="${cellBorder} ${cellPadding} text-align: center; color: #999; height: ${rowHeightPx}px; box-sizing: border-box;">暂无数据</td></tr></tbody>`;
|
|
706
|
+
bodyHtml = `<tbody><tr style="min-height: ${rowHeightPx}px;"><td colspan="${colspan}" style="${cellBorder} ${cellPadding} text-align: center; color: #999; min-height: ${rowHeightPx}px; box-sizing: border-box;">暂无数据</td></tr></tbody>`;
|
|
687
707
|
}
|
|
688
708
|
// 渲染合计行(新增)
|
|
689
709
|
const showSummary = (props === null || props === void 0 ? void 0 : props.showSummary) === true;
|
|
@@ -698,14 +718,14 @@ class TableRenderer {
|
|
|
698
718
|
? props._totalData
|
|
699
719
|
: tableData;
|
|
700
720
|
const summaryHtml = shouldShowSummary
|
|
701
|
-
? this.renderSummary(summaryData, visibleColumns, props, cellBorder, cellPadding, cellTextStyle, rowHeightPx, textAlign)
|
|
721
|
+
? this.renderSummary(summaryData, visibleColumns, props, cellBorder, cellPadding, cellTextStyle, rowHeightPx, textAlign, colWidthPercent)
|
|
702
722
|
: '';
|
|
703
723
|
return `<table class="print-table" style="${tableStyleStr}">${headerHtml}${bodyHtml}${summaryHtml}</table>`;
|
|
704
724
|
}
|
|
705
725
|
/**
|
|
706
726
|
* 渲染合计行
|
|
707
727
|
*/
|
|
708
|
-
renderSummary(data, columns, props, cellBorder, cellPadding, cellTextStyle, rowHeightPx, defaultTextAlign) {
|
|
728
|
+
renderSummary(data, columns, props, cellBorder, cellPadding, cellTextStyle, rowHeightPx, defaultTextAlign, colWidthPercent = 'auto') {
|
|
709
729
|
if (!columns.length)
|
|
710
730
|
return '';
|
|
711
731
|
const summaryLabel = props.summaryLabel || '合计';
|
|
@@ -728,7 +748,8 @@ class TableRenderer {
|
|
|
728
748
|
${cellPadding}
|
|
729
749
|
${cellTextStyle}
|
|
730
750
|
text-align: ${col.align || defaultTextAlign};
|
|
731
|
-
|
|
751
|
+
width: ${colWidthPercent}%;
|
|
752
|
+
min-height: ${rowHeightPx}px;
|
|
732
753
|
box-sizing: border-box;
|
|
733
754
|
background: ${bgColor};
|
|
734
755
|
font-weight: ${fontWeight};
|
|
@@ -736,7 +757,7 @@ class TableRenderer {
|
|
|
736
757
|
`.trim().replace(/\s+/g, ' ');
|
|
737
758
|
return `<td style="${cellStyle}">${content}</td>`;
|
|
738
759
|
}).join('');
|
|
739
|
-
return `<tfoot><tr style="height: ${rowHeightPx}px;">${cells}</tr></tfoot>`;
|
|
760
|
+
return `<tfoot><tr style="min-height: ${rowHeightPx}px;">${cells}</tr></tfoot>`;
|
|
740
761
|
}
|
|
741
762
|
/**
|
|
742
763
|
* 计算单列合计值(使用 Decimal.js 解决精度问题)
|
|
@@ -782,25 +803,38 @@ class TableRenderer {
|
|
|
782
803
|
}
|
|
783
804
|
}
|
|
784
805
|
catch (error) {
|
|
785
|
-
console.error('
|
|
806
|
+
console.error('[TableRenderer] 合计计算错误:', error);
|
|
807
|
+
// ✅ 返回友好的错误提示,而不是静默失败
|
|
808
|
+
return '计算错误';
|
|
809
|
+
}
|
|
810
|
+
// ✅ 格式化前检查结果是否有效
|
|
811
|
+
if (!result || typeof result.toFixed !== 'function') {
|
|
812
|
+
console.warn('[TableRenderer] 合计结果无效:', result);
|
|
786
813
|
return '-';
|
|
787
814
|
}
|
|
788
815
|
// 格式化
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
816
|
+
try {
|
|
817
|
+
const precision = (_a = summary.precision) !== null && _a !== void 0 ? _a : 2;
|
|
818
|
+
const formatted = result.toFixed(precision);
|
|
819
|
+
const prefix = summary.prefix || '';
|
|
820
|
+
const suffix = summary.suffix || '';
|
|
821
|
+
return `${prefix}${formatted}${suffix}`;
|
|
822
|
+
}
|
|
823
|
+
catch (formatError) {
|
|
824
|
+
console.error('[TableRenderer] 格式化合计结果失败:', formatError);
|
|
825
|
+
return '-';
|
|
826
|
+
}
|
|
794
827
|
}
|
|
795
828
|
calculateHeight(component, context) {
|
|
796
829
|
var _a, _b, _c;
|
|
797
|
-
//
|
|
830
|
+
// 表格高度:简单估算(用于初始布局计算,实际分页使用 measureTableRowHeights)
|
|
798
831
|
if ((_a = component.binding) === null || _a === void 0 ? void 0 : _a.path) {
|
|
799
832
|
const data = context.getValueByPath(component.binding.path);
|
|
800
833
|
if (Array.isArray(data) && data.length > 0) {
|
|
801
834
|
const headerHeight = ((_b = component.props) === null || _b === void 0 ? void 0 : _b.showHeader) !== false ? TABLE_DEFAULT.HEADER_HEIGHT : 0;
|
|
802
|
-
|
|
803
|
-
const
|
|
835
|
+
// 使用基础行高(不乘系数),实际高度由渲染后测量决定
|
|
836
|
+
const rowHeight = TABLE_DEFAULT.MIN_ROW_HEIGHT;
|
|
837
|
+
const summaryHeight = ((_c = component.props) === null || _c === void 0 ? void 0 : _c.showSummary) === true ? rowHeight : 0;
|
|
804
838
|
return headerHeight + data.length * rowHeight + summaryHeight;
|
|
805
839
|
}
|
|
806
840
|
}
|
|
@@ -822,13 +856,13 @@ class ImageRenderer {
|
|
|
822
856
|
const imageSrc = value || (props === null || props === void 0 ? void 0 : props.src) || '';
|
|
823
857
|
// 容器定位样式
|
|
824
858
|
const positionStyles = buildPositionStyle(layout.xMm || 0, layout.yMm || 0, layout.widthMm || COMPONENT_DEFAULT_SIZE.IMAGE_WIDTH, layout.heightMm || COMPONENT_DEFAULT_SIZE.IMAGE_HEIGHT, context.mmToPx);
|
|
825
|
-
const containerStyles = Object.assign(Object.assign({}, positionStyles), {
|
|
859
|
+
const containerStyles = Object.assign(Object.assign({}, positionStyles), { overflow: 'hidden' });
|
|
826
860
|
const containerStyleStr = buildStyleString(containerStyles);
|
|
827
|
-
//
|
|
861
|
+
// 图片样式:严格按模板尺寸渲染,避免自适应导致排版错乱
|
|
828
862
|
const imageStyle = `
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
object-fit: ${(style === null || style === void 0 ? void 0 : style.objectFit) || '
|
|
863
|
+
width: 100%;
|
|
864
|
+
height: 100%;
|
|
865
|
+
object-fit: ${(style === null || style === void 0 ? void 0 : style.objectFit) || 'fill'};
|
|
832
866
|
`;
|
|
833
867
|
if (imageSrc) {
|
|
834
868
|
return `<div style="${containerStyleStr}"><img src="${imageSrc}" style="${imageStyle}" alt="" /></div>`;
|
|
@@ -1189,6 +1223,11 @@ class PrintEngine {
|
|
|
1189
1223
|
* 计算表头高度(mm)
|
|
1190
1224
|
*/
|
|
1191
1225
|
calculateTableHeaderHeight(comp) {
|
|
1226
|
+
var _a;
|
|
1227
|
+
// 如果用户设置 showHeader: false,则表头高度为 0
|
|
1228
|
+
if (((_a = comp.props) === null || _a === void 0 ? void 0 : _a.showHeader) === false) {
|
|
1229
|
+
return 0;
|
|
1230
|
+
}
|
|
1192
1231
|
// 使用 TABLE_DEFAULT.HEADER_HEIGHT 作为表头高度
|
|
1193
1232
|
return TABLE_DEFAULT.HEADER_HEIGHT;
|
|
1194
1233
|
}
|
|
@@ -1199,14 +1238,108 @@ class PrintEngine {
|
|
|
1199
1238
|
// 使用 MIN_ROW_HEIGHT * ROW_HEIGHT_FACTOR 作为行高
|
|
1200
1239
|
return TABLE_DEFAULT.MIN_ROW_HEIGHT * TABLE_DEFAULT.ROW_HEIGHT_FACTOR;
|
|
1201
1240
|
}
|
|
1241
|
+
/**
|
|
1242
|
+
* 渲染页码(根据页面配置)
|
|
1243
|
+
*/
|
|
1244
|
+
renderPageNumber(pageNumber, totalPages) {
|
|
1245
|
+
var _a, _b, _c, _d;
|
|
1246
|
+
const { page } = this.template;
|
|
1247
|
+
const pageNumberConfig = page.pageNumber;
|
|
1248
|
+
// 如果未启用页码或缺少页码信息,返回空
|
|
1249
|
+
if (!(pageNumberConfig === null || pageNumberConfig === void 0 ? void 0 : pageNumberConfig.enabled) || pageNumber === undefined || totalPages === undefined) {
|
|
1250
|
+
return '';
|
|
1251
|
+
}
|
|
1252
|
+
console.log(`[PrintEngine] 渲染页码: pageNumber=${pageNumber}, totalPages=${totalPages}`, pageNumberConfig);
|
|
1253
|
+
// 格式化页码文本
|
|
1254
|
+
const format = pageNumberConfig.format || 'slash';
|
|
1255
|
+
const prefix = pageNumberConfig.prefix || '';
|
|
1256
|
+
const suffix = pageNumberConfig.suffix || '';
|
|
1257
|
+
const separator = pageNumberConfig.separator || '/';
|
|
1258
|
+
let pageText = '';
|
|
1259
|
+
if (format === 'simple') {
|
|
1260
|
+
pageText = `${pageNumber}`;
|
|
1261
|
+
}
|
|
1262
|
+
else if (format === 'text') {
|
|
1263
|
+
pageText = `第${pageNumber}页 共${totalPages}页`;
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
pageText = `${pageNumber}${separator}${totalPages}`;
|
|
1267
|
+
}
|
|
1268
|
+
pageText = `${prefix}${pageText}${suffix}`;
|
|
1269
|
+
// 计算位置
|
|
1270
|
+
const { widthMm, heightMm } = this.getPageSize();
|
|
1271
|
+
const { position, offsetX = 0, offsetY = 0, style = {} } = pageNumberConfig;
|
|
1272
|
+
const fontSize = style.fontSize || 12;
|
|
1273
|
+
const color = style.color || '#666';
|
|
1274
|
+
const fontWeight = style.fontWeight || 'normal';
|
|
1275
|
+
// 根据 position 计算 x, y 坐标
|
|
1276
|
+
let xMm = 0;
|
|
1277
|
+
let yMm = 0;
|
|
1278
|
+
const pageNumberWidth = 20; // 页码宽度 mm
|
|
1279
|
+
const pageNumberHeight = 6; // 页码高度 mm
|
|
1280
|
+
const marginTop = ((_a = page.marginMm) === null || _a === void 0 ? void 0 : _a.top) || 0;
|
|
1281
|
+
const marginRight = ((_b = page.marginMm) === null || _b === void 0 ? void 0 : _b.right) || 0;
|
|
1282
|
+
const marginBottom = ((_c = page.marginMm) === null || _c === void 0 ? void 0 : _c.bottom) || 0;
|
|
1283
|
+
const marginLeft = ((_d = page.marginMm) === null || _d === void 0 ? void 0 : _d.left) || 0;
|
|
1284
|
+
switch (position) {
|
|
1285
|
+
case 'top-left':
|
|
1286
|
+
xMm = marginLeft;
|
|
1287
|
+
yMm = marginTop;
|
|
1288
|
+
break;
|
|
1289
|
+
case 'top-center':
|
|
1290
|
+
xMm = (widthMm - pageNumberWidth) / 2;
|
|
1291
|
+
yMm = marginTop;
|
|
1292
|
+
break;
|
|
1293
|
+
case 'top-right':
|
|
1294
|
+
xMm = widthMm - marginRight - pageNumberWidth;
|
|
1295
|
+
yMm = marginTop;
|
|
1296
|
+
break;
|
|
1297
|
+
case 'bottom-left':
|
|
1298
|
+
xMm = marginLeft;
|
|
1299
|
+
yMm = heightMm - marginBottom - pageNumberHeight;
|
|
1300
|
+
break;
|
|
1301
|
+
case 'bottom-center':
|
|
1302
|
+
xMm = (widthMm - pageNumberWidth) / 2;
|
|
1303
|
+
yMm = heightMm - marginBottom - pageNumberHeight;
|
|
1304
|
+
break;
|
|
1305
|
+
case 'bottom-right':
|
|
1306
|
+
default:
|
|
1307
|
+
xMm = widthMm - marginRight - pageNumberWidth;
|
|
1308
|
+
yMm = heightMm - marginBottom - pageNumberHeight;
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
// 应用偏移
|
|
1312
|
+
xMm += offsetX;
|
|
1313
|
+
yMm += offsetY;
|
|
1314
|
+
// 转换为 px
|
|
1315
|
+
const xPx = xMm * this.mmToPx;
|
|
1316
|
+
const yPx = yMm * this.mmToPx;
|
|
1317
|
+
const widthPx = pageNumberWidth * this.mmToPx;
|
|
1318
|
+
const heightPx = pageNumberHeight * this.mmToPx;
|
|
1319
|
+
// 生成 HTML
|
|
1320
|
+
const alignStyle = position.includes('left') ? 'left' : position.includes('right') ? 'right' : 'center';
|
|
1321
|
+
const justifyContent = alignStyle === 'left' ? 'flex-start' : alignStyle === 'right' ? 'flex-end' : 'center';
|
|
1322
|
+
return `<div style="position: absolute; left: ${xPx}px; top: ${yPx}px; width: ${widthPx}px; height: ${heightPx}px; font-size: ${fontSize}px; color: ${color}; font-weight: ${fontWeight}; display: flex; align-items: center; justify-content: ${justifyContent};">${this.escapeHtml(pageText)}</div>`;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* HTML 转义
|
|
1326
|
+
*/
|
|
1327
|
+
escapeHtml(text) {
|
|
1328
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
1329
|
+
}
|
|
1202
1330
|
/**
|
|
1203
1331
|
* 渲染单个页面(直接渲染,不做智能布局)
|
|
1332
|
+
* @param components 组件列表
|
|
1333
|
+
* @param pageNumber 当前页码(可选)
|
|
1334
|
+
* @param totalPages 总页数(可选)
|
|
1204
1335
|
*/
|
|
1205
|
-
renderSinglePage(components) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1336
|
+
renderSinglePage(components, pageNumber, totalPages) {
|
|
1337
|
+
console.log(`[PrintEngine] renderSinglePage: 页码=${pageNumber}, 总页数=${totalPages}, 组件数=${components.length}`);
|
|
1338
|
+
// 渲染所有组件
|
|
1339
|
+
const componentsHTML = components.map(comp => this.renderComponent(comp)).join('');
|
|
1340
|
+
// 如果页面配置启用了页码,在固定位置渲染页码
|
|
1341
|
+
const pageNumberHTML = this.renderPageNumber(pageNumber, totalPages);
|
|
1342
|
+
return componentsHTML + pageNumberHTML;
|
|
1210
1343
|
}
|
|
1211
1344
|
/**
|
|
1212
1345
|
* 虚拟分页:基于相对间距的流式布局
|
|
@@ -1215,7 +1348,7 @@ class PrintEngine {
|
|
|
1215
1348
|
* 2. 按顺序累加高度,遇到表格就拆分
|
|
1216
1349
|
* 3. 换页时从 marginTop 开始,忽略原 gap
|
|
1217
1350
|
*/
|
|
1218
|
-
calculatePages(components) {
|
|
1351
|
+
async calculatePages(components) {
|
|
1219
1352
|
var _a, _b, _c;
|
|
1220
1353
|
const { page } = this.template;
|
|
1221
1354
|
const { heightMm } = this.getPageSize();
|
|
@@ -1235,8 +1368,10 @@ class PrintEngine {
|
|
|
1235
1368
|
return { comp, gap: (comp.layout.yMm || 0) };
|
|
1236
1369
|
}
|
|
1237
1370
|
const prevComp = sortedComponents[index - 1];
|
|
1371
|
+
// ✅ 使用设计高度计算相对间距(保留负数,表示组件重叠)
|
|
1372
|
+
// 注意:表格的实际高度会在 splitTableWithGap 中重新计算
|
|
1238
1373
|
const prevBottom = (prevComp.layout.yMm || 0) + (prevComp.layout.heightMm || 0);
|
|
1239
|
-
const gap = (comp.layout.yMm || 0) - prevBottom;
|
|
1374
|
+
const gap = (comp.layout.yMm || 0) - prevBottom; // 保留负数,表示组件重叠
|
|
1240
1375
|
return { comp, gap };
|
|
1241
1376
|
});
|
|
1242
1377
|
// 3. 遍历组件,累加高度
|
|
@@ -1246,8 +1381,13 @@ class PrintEngine {
|
|
|
1246
1381
|
let currentPageHeight = marginTop; // 当前页的累计高度
|
|
1247
1382
|
let isFirstComponentInPage = true; // 标记当前页是否是第一个组件
|
|
1248
1383
|
for (let i = 0; i < componentsWithGaps.length; i++) {
|
|
1249
|
-
const { comp, gap } = componentsWithGaps[i];
|
|
1384
|
+
const { comp, gap: designGap } = componentsWithGaps[i];
|
|
1250
1385
|
const compHeightMm = comp.layout.heightMm || 50;
|
|
1386
|
+
// ✅ 使用设计时的相对间距(保留负数,表示组件重叠)
|
|
1387
|
+
// designGap 是设计时计算的:组件B.yMm - (组件A.yMm + 组件A.heightMm)
|
|
1388
|
+
// 这代表了设计意图中的"组件A底部到组件B顶部的间距"
|
|
1389
|
+
// 负数表示组件重叠,这是设计时允许的布局方式
|
|
1390
|
+
const actualGap = designGap;
|
|
1251
1391
|
// 边界检查:组件高度接近页面高度时输出警告
|
|
1252
1392
|
if (compHeightMm > availableHeightMm * 0.8) {
|
|
1253
1393
|
console.warn(`组件 ${comp.id} (${comp.type}) 高度 ${compHeightMm.toFixed(2)}mm 接近页面可用高度 ${availableHeightMm.toFixed(2)}mm,可能影响分页效果`);
|
|
@@ -1256,14 +1396,16 @@ class PrintEngine {
|
|
|
1256
1396
|
if (comp.type === 'table' && ((_c = comp.binding) === null || _c === void 0 ? void 0 : _c.path)) {
|
|
1257
1397
|
const tableData = context.getValueByPath(comp.binding.path);
|
|
1258
1398
|
if (Array.isArray(tableData) && tableData.length > 0) {
|
|
1259
|
-
const result = this.splitTableWithGap(comp, tableData,
|
|
1399
|
+
const result = await this.splitTableWithGap(comp, tableData, actualGap, isFirstComponentInPage, availableHeightMm, currentPageHeight, pages, currentPage);
|
|
1260
1400
|
currentPage = result.currentPage;
|
|
1261
|
-
|
|
1262
|
-
|
|
1401
|
+
// ✅ 对于紧跟表格的组件,使用表格实际底部位置作为参考
|
|
1402
|
+
// 这样可以确保无论表格是否跨页,后续组件与表格的相对间距保持一致
|
|
1403
|
+
currentPageHeight = result.lastTableFragmentBottom;
|
|
1404
|
+
isFirstComponentInPage = false; // 表格后的组件不是页面第一个组件
|
|
1263
1405
|
}
|
|
1264
1406
|
else {
|
|
1265
1407
|
// 空表格:按普通组件处理
|
|
1266
|
-
if (this.shouldBreakPage(currentPageHeight, compHeightMm,
|
|
1408
|
+
if (this.shouldBreakPage(currentPageHeight, compHeightMm, actualGap, availableHeightMm, isFirstComponentInPage) && currentPage.length > 0) {
|
|
1267
1409
|
// 换页
|
|
1268
1410
|
pages.push(currentPage);
|
|
1269
1411
|
currentPage = [];
|
|
@@ -1279,7 +1421,7 @@ class PrintEngine {
|
|
|
1279
1421
|
}
|
|
1280
1422
|
else {
|
|
1281
1423
|
// 同一页:应用 gap
|
|
1282
|
-
currentPageHeight +=
|
|
1424
|
+
currentPageHeight += actualGap;
|
|
1283
1425
|
compCopy.layout.yMm = currentPageHeight;
|
|
1284
1426
|
currentPageHeight += compHeightMm;
|
|
1285
1427
|
}
|
|
@@ -1290,7 +1432,7 @@ class PrintEngine {
|
|
|
1290
1432
|
// 4.2 普通组件:按相对间距累加高度
|
|
1291
1433
|
else {
|
|
1292
1434
|
// 使用辅助方法判断是否需要换页
|
|
1293
|
-
if (this.shouldBreakPage(currentPageHeight, compHeightMm,
|
|
1435
|
+
if (this.shouldBreakPage(currentPageHeight, compHeightMm, actualGap, availableHeightMm, isFirstComponentInPage) && currentPage.length > 0) {
|
|
1294
1436
|
// 换页
|
|
1295
1437
|
pages.push(currentPage);
|
|
1296
1438
|
currentPage = [];
|
|
@@ -1300,13 +1442,15 @@ class PrintEngine {
|
|
|
1300
1442
|
// 创建组件副本,避免修改原数据
|
|
1301
1443
|
const compCopy = Object.assign(Object.assign({}, comp), { layout: Object.assign({}, comp.layout) });
|
|
1302
1444
|
if (isFirstComponentInPage) {
|
|
1303
|
-
//
|
|
1445
|
+
// 新页面第一个组件:应用 gap(从页面顶部开始的相对间距)
|
|
1446
|
+
// 注意:即使在新页面,也应该保持设计时的相对间距
|
|
1447
|
+
currentPageHeight += actualGap;
|
|
1304
1448
|
compCopy.layout.yMm = currentPageHeight;
|
|
1305
1449
|
currentPageHeight += compHeightMm;
|
|
1306
1450
|
}
|
|
1307
1451
|
else {
|
|
1308
1452
|
// 同一页:应用 gap
|
|
1309
|
-
currentPageHeight +=
|
|
1453
|
+
currentPageHeight += actualGap;
|
|
1310
1454
|
compCopy.layout.yMm = currentPageHeight;
|
|
1311
1455
|
currentPageHeight += compHeightMm;
|
|
1312
1456
|
}
|
|
@@ -1321,24 +1465,140 @@ class PrintEngine {
|
|
|
1321
1465
|
// 6. 返回分页结果
|
|
1322
1466
|
return pages.length > 0 ? pages : [components];
|
|
1323
1467
|
}
|
|
1468
|
+
/**
|
|
1469
|
+
* 测量表格实际行高(渲染后测量方案)
|
|
1470
|
+
* 将表格渲染到隐藏容器,测量表头、数据行和合计行的实际高度
|
|
1471
|
+
*/
|
|
1472
|
+
async measureTableRowHeights(tableComponent, tableData) {
|
|
1473
|
+
var _a;
|
|
1474
|
+
// 检查是否在浏览器环境
|
|
1475
|
+
if (typeof document === 'undefined') {
|
|
1476
|
+
// 服务器端:使用估算值
|
|
1477
|
+
const baseRowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1478
|
+
return {
|
|
1479
|
+
headerHeight: this.calculateTableHeaderHeight(tableComponent),
|
|
1480
|
+
rowHeights: tableData.map(() => baseRowHeight),
|
|
1481
|
+
summaryHeight: baseRowHeight
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
const renderer = this.renderers.get('table');
|
|
1485
|
+
if (!renderer) {
|
|
1486
|
+
const baseRowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1487
|
+
return {
|
|
1488
|
+
headerHeight: this.calculateTableHeaderHeight(tableComponent),
|
|
1489
|
+
rowHeights: tableData.map(() => baseRowHeight),
|
|
1490
|
+
summaryHeight: baseRowHeight
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
// 计算与 TableRenderer 一致的表格宽度
|
|
1494
|
+
const context = this.createRenderContext();
|
|
1495
|
+
const xMm = tableComponent.layout.xMm || 0;
|
|
1496
|
+
let tableWidthMm;
|
|
1497
|
+
if (tableComponent.layout.widthMm) {
|
|
1498
|
+
tableWidthMm = tableComponent.layout.widthMm;
|
|
1499
|
+
if (context.pageInfo) {
|
|
1500
|
+
const availableWidth = context.pageInfo.widthMm - context.pageInfo.marginMm.left - context.pageInfo.marginMm.right;
|
|
1501
|
+
const totalOccupied = xMm + tableWidthMm;
|
|
1502
|
+
if (totalOccupied > availableWidth) {
|
|
1503
|
+
tableWidthMm = availableWidth - xMm;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
else if (context.pageInfo) {
|
|
1508
|
+
const availableWidth = context.pageInfo.widthMm - context.pageInfo.marginMm.left - context.pageInfo.marginMm.right;
|
|
1509
|
+
tableWidthMm = availableWidth - xMm;
|
|
1510
|
+
}
|
|
1511
|
+
else {
|
|
1512
|
+
tableWidthMm = COMPONENT_DEFAULT_SIZE.TABLE_WIDTH;
|
|
1513
|
+
}
|
|
1514
|
+
// 创建隐藏测量容器
|
|
1515
|
+
const measureContainer = document.createElement('div');
|
|
1516
|
+
measureContainer.style.cssText = `
|
|
1517
|
+
position: absolute;
|
|
1518
|
+
visibility: hidden;
|
|
1519
|
+
left: -9999px;
|
|
1520
|
+
top: 0;
|
|
1521
|
+
width: ${tableWidthMm}mm;
|
|
1522
|
+
pointer-events: none;
|
|
1523
|
+
`;
|
|
1524
|
+
// 创建完整的表格组件用于测量(包含合计行)
|
|
1525
|
+
// 使用用户实际的 showHeader 和 bordered 设置,确保测量准确
|
|
1526
|
+
const measureComponent = Object.assign(Object.assign({}, tableComponent), { props: Object.assign(Object.assign({}, tableComponent.props), { _pageData: tableData, _showHeader: ((_a = tableComponent.props) === null || _a === void 0 ? void 0 : _a.showHeader) !== false, _isLastPage: true }) });
|
|
1527
|
+
// 渲染表格
|
|
1528
|
+
const tableHtml = renderer.render(measureComponent, context);
|
|
1529
|
+
measureContainer.innerHTML = `
|
|
1530
|
+
<div style="width: ${tableWidthMm}mm; position: relative;">
|
|
1531
|
+
${tableHtml}
|
|
1532
|
+
</div>
|
|
1533
|
+
`;
|
|
1534
|
+
document.body.appendChild(measureContainer);
|
|
1535
|
+
try {
|
|
1536
|
+
// ✅ 测量表头高度(如果存在)
|
|
1537
|
+
let headerHeight = 0;
|
|
1538
|
+
const headerRow = measureContainer.querySelector('thead tr');
|
|
1539
|
+
if (headerRow) {
|
|
1540
|
+
headerHeight = headerRow.offsetHeight / this.mmToPx;
|
|
1541
|
+
}
|
|
1542
|
+
// 测量数据行高度
|
|
1543
|
+
const rows = measureContainer.querySelectorAll('tbody tr');
|
|
1544
|
+
const rowHeights = [];
|
|
1545
|
+
rows.forEach((row) => {
|
|
1546
|
+
const heightPx = row.offsetHeight;
|
|
1547
|
+
const heightMm = heightPx / this.mmToPx;
|
|
1548
|
+
rowHeights.push(heightMm);
|
|
1549
|
+
});
|
|
1550
|
+
// ✅ 测量合计行高度(如果存在)
|
|
1551
|
+
let summaryHeight = 0;
|
|
1552
|
+
const summaryRow = measureContainer.querySelector('tfoot tr');
|
|
1553
|
+
if (summaryRow) {
|
|
1554
|
+
summaryHeight = summaryRow.offsetHeight / this.mmToPx;
|
|
1555
|
+
}
|
|
1556
|
+
// 如果测量失败,使用估算值
|
|
1557
|
+
if (rowHeights.length === 0) {
|
|
1558
|
+
const baseRowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1559
|
+
return {
|
|
1560
|
+
headerHeight: this.calculateTableHeaderHeight(tableComponent),
|
|
1561
|
+
rowHeights: tableData.map(() => baseRowHeight),
|
|
1562
|
+
summaryHeight: baseRowHeight
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
return { headerHeight, rowHeights, summaryHeight };
|
|
1566
|
+
}
|
|
1567
|
+
finally {
|
|
1568
|
+
// 确保无论成功或失败,都清理测量容器
|
|
1569
|
+
if (measureContainer.parentNode) {
|
|
1570
|
+
document.body.removeChild(measureContainer);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1324
1574
|
/**
|
|
1325
1575
|
* 表格跨页拆分(基于相对间距)
|
|
1326
|
-
* 支持 repeatHeader
|
|
1576
|
+
* 支持 repeatHeader 配置、渲染后测量、空表格检查
|
|
1327
1577
|
*/
|
|
1328
|
-
splitTableWithGap(tableComponent, tableData, gap, isFirstComponentInPage, availableHeightMm, currentPageHeight, pages, currentPage) {
|
|
1578
|
+
async splitTableWithGap(tableComponent, tableData, gap, isFirstComponentInPage, availableHeightMm, currentPageHeight, pages, currentPage) {
|
|
1329
1579
|
var _a, _b, _c, _d, _e;
|
|
1330
1580
|
// 读取配置:是否重复表头(默认 true)
|
|
1331
1581
|
const repeatHeader = ((_b = (_a = tableComponent.props) === null || _a === void 0 ? void 0 : _a.pagination) === null || _b === void 0 ? void 0 : _b.repeatHeader) !== false;
|
|
1332
|
-
// 使用精确计算的高度
|
|
1333
|
-
const headerHeight = this.calculateTableHeaderHeight(tableComponent);
|
|
1334
|
-
const rowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1335
1582
|
const marginTop = ((_c = this.template.page.marginMm) === null || _c === void 0 ? void 0 : _c.top) || 0;
|
|
1583
|
+
// 记录初始页面数,用于判断表格是否跨页
|
|
1584
|
+
const initialPagesLength = pages.length;
|
|
1336
1585
|
// 空表格检查
|
|
1337
1586
|
if (tableData.length === 0) {
|
|
1338
1587
|
console.info('表格无数据,跳过渲染');
|
|
1339
|
-
return { currentPage, currentPageHeight };
|
|
1588
|
+
return { currentPage, currentPageHeight, isTableSplitAcrossPages: false, lastTableFragmentBottom: currentPageHeight };
|
|
1589
|
+
}
|
|
1590
|
+
// ✅ 渲染后测量:获取表头、数据行和合计行的实际高度
|
|
1591
|
+
let { headerHeight, rowHeights, summaryHeight: measuredSummaryHeight } = await this.measureTableRowHeights(tableComponent, tableData);
|
|
1592
|
+
// 检查测量结果是否有效(防止所有列 hidden 等情况导致长度不一致)
|
|
1593
|
+
if (rowHeights.length !== tableData.length) {
|
|
1594
|
+
console.warn(`[PrintEngine] 表格测量结果异常:rowHeights.length (${rowHeights.length}) != tableData.length (${tableData.length}),` +
|
|
1595
|
+
`可能所有列均为 hidden,使用估算行高继续分页`);
|
|
1596
|
+
// 使用估算行高替换测量结果
|
|
1597
|
+
const fallbackRowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1598
|
+
rowHeights = tableData.map(() => fallbackRowHeight);
|
|
1340
1599
|
}
|
|
1341
1600
|
let remainingData = [...tableData];
|
|
1601
|
+
let remainingRowHeights = [...rowHeights];
|
|
1342
1602
|
let workingPage = [...currentPage];
|
|
1343
1603
|
let workingPageHeight = currentPageHeight;
|
|
1344
1604
|
let isFirstFragment = true;
|
|
@@ -1350,11 +1610,22 @@ class PrintEngine {
|
|
|
1350
1610
|
if (isFirstFragment && !isFirstComponentInPage) {
|
|
1351
1611
|
remainingHeight -= gap;
|
|
1352
1612
|
}
|
|
1353
|
-
// 计算能放多少行
|
|
1354
1613
|
// 如果 repeatHeader = false 且是第一个片段,则表头已经在第一页了,后续页不需要表头
|
|
1355
1614
|
const needHeader = isFirstFragment || repeatHeader;
|
|
1356
|
-
|
|
1357
|
-
|
|
1615
|
+
let availableForRows = remainingHeight - (needHeader ? headerHeight : 0);
|
|
1616
|
+
// ✅ 使用实际测量的行高计算能放多少行
|
|
1617
|
+
let rowsCanFit = 0;
|
|
1618
|
+
let accumulatedHeight = 0;
|
|
1619
|
+
for (let i = 0; i < remainingRowHeights.length; i++) {
|
|
1620
|
+
const rowHeight = remainingRowHeights[i];
|
|
1621
|
+
if (accumulatedHeight + rowHeight <= availableForRows) {
|
|
1622
|
+
accumulatedHeight += rowHeight;
|
|
1623
|
+
rowsCanFit++;
|
|
1624
|
+
}
|
|
1625
|
+
else {
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1358
1629
|
// 确保至少有 1 行数据(避免只有表头的空页面)
|
|
1359
1630
|
if (rowsCanFit <= 0) {
|
|
1360
1631
|
// 当前页放不下,换页
|
|
@@ -1368,25 +1639,28 @@ class PrintEngine {
|
|
|
1368
1639
|
}
|
|
1369
1640
|
// 取出当前页能放的数据
|
|
1370
1641
|
const dataForThisPage = remainingData.slice(0, rowsCanFit);
|
|
1642
|
+
const rowHeightsForThisPage = remainingRowHeights.slice(0, rowsCanFit);
|
|
1371
1643
|
remainingData = remainingData.slice(rowsCanFit);
|
|
1644
|
+
remainingRowHeights = remainingRowHeights.slice(rowsCanFit);
|
|
1372
1645
|
// 判断是否为最后一页(用于合计行)
|
|
1373
1646
|
const isLastPage = remainingData.length === 0;
|
|
1374
1647
|
// 创建当前页的表格片段
|
|
1375
1648
|
const tableFragmentYMm = isFirstFragment
|
|
1376
1649
|
? (isFirstComponentInPage ? workingPageHeight : workingPageHeight + gap)
|
|
1377
1650
|
: marginTop;
|
|
1378
|
-
const tableFragment = Object.assign(Object.assign({}, tableComponent), { layout: Object.assign(Object.assign({}, tableComponent.layout), { yMm: tableFragmentYMm }), props: Object.assign(Object.assign({}, tableComponent.props), { _pageData: dataForThisPage, _showHeader: needHeader, _isLastPage: isLastPage, _totalData: tableData
|
|
1651
|
+
const tableFragment = Object.assign(Object.assign({}, tableComponent), { layout: Object.assign(Object.assign({}, tableComponent.layout), { yMm: tableFragmentYMm }), props: Object.assign(Object.assign({}, tableComponent.props), { _pageData: dataForThisPage, _showHeader: needHeader, _isLastPage: isLastPage, _totalData: tableData, _rowHeights: rowHeightsForThisPage // 传递实际行高到渲染器
|
|
1379
1652
|
}) });
|
|
1380
1653
|
workingPage.push(tableFragment);
|
|
1381
1654
|
// 计算合计行高度(如果启用合计功能)
|
|
1382
1655
|
const showSummary = ((_d = tableComponent.props) === null || _d === void 0 ? void 0 : _d.showSummary) === true;
|
|
1383
1656
|
const summaryMode = ((_e = tableComponent.props) === null || _e === void 0 ? void 0 : _e.summaryMode) || 'total';
|
|
1384
|
-
const shouldShowSummaryOnThisPage = showSummary && (summaryMode === 'page' ||
|
|
1385
|
-
(summaryMode === 'total' && isLastPage)
|
|
1386
|
-
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1657
|
+
const shouldShowSummaryOnThisPage = showSummary && (summaryMode === 'page' ||
|
|
1658
|
+
(summaryMode === 'total' && isLastPage));
|
|
1659
|
+
// ✅ 使用测量的合计行高度,如果没有测量值则使用平均行高
|
|
1660
|
+
const avgRowHeight = rowHeightsForThisPage.reduce((a, b) => a + b, 0) / rowHeightsForThisPage.length;
|
|
1661
|
+
const summaryHeight = shouldShowSummaryOnThisPage ? (measuredSummaryHeight || avgRowHeight) : 0;
|
|
1662
|
+
// 更新当前页高度(使用实际测量的行高累加)
|
|
1663
|
+
const tableFragmentHeight = (needHeader ? headerHeight : 0) + accumulatedHeight + summaryHeight;
|
|
1390
1664
|
if (isFirstFragment && !isFirstComponentInPage) {
|
|
1391
1665
|
workingPageHeight += gap + tableFragmentHeight;
|
|
1392
1666
|
}
|
|
@@ -1401,15 +1675,19 @@ class PrintEngine {
|
|
|
1401
1675
|
isFirstFragment = false;
|
|
1402
1676
|
}
|
|
1403
1677
|
}
|
|
1678
|
+
// 判断表格是否跨页(即是否产生了多个页面片段)
|
|
1679
|
+
const isTableSplitAcrossPages = pages.length > initialPagesLength;
|
|
1404
1680
|
return {
|
|
1405
1681
|
currentPage: workingPage,
|
|
1406
|
-
currentPageHeight: workingPageHeight
|
|
1682
|
+
currentPageHeight: workingPageHeight,
|
|
1683
|
+
isTableSplitAcrossPages, // ✅ 返回表格是否跨页的信息
|
|
1684
|
+
lastTableFragmentBottom: workingPageHeight // ✅ 返回最后一个表格片段的底部位置
|
|
1407
1685
|
};
|
|
1408
1686
|
}
|
|
1409
1687
|
/**
|
|
1410
1688
|
* 生成打印 HTML
|
|
1411
1689
|
*/
|
|
1412
|
-
generatePrintHTML() {
|
|
1690
|
+
async generatePrintHTML() {
|
|
1413
1691
|
var _a, _b, _c, _d;
|
|
1414
1692
|
const { page, components } = this.template;
|
|
1415
1693
|
const { widthMm, heightMm } = this.getPageSize();
|
|
@@ -1438,11 +1716,13 @@ class PrintEngine {
|
|
|
1438
1716
|
});
|
|
1439
1717
|
}
|
|
1440
1718
|
// 标准页面模式:虚拟分页,生成多个独立的页面
|
|
1441
|
-
const pages = this.calculatePages(components);
|
|
1719
|
+
const pages = await this.calculatePages(components);
|
|
1720
|
+
const totalPages = pages.length;
|
|
1442
1721
|
// 渲染每个页面
|
|
1443
1722
|
const pagesHTML = pages.map((pageComponents, index) => {
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1723
|
+
const pageNumber = index + 1;
|
|
1724
|
+
const pageContent = this.renderSinglePage(pageComponents, pageNumber, totalPages);
|
|
1725
|
+
return `<div class="print-page" data-page="${pageNumber}">${pageContent}</div>`;
|
|
1446
1726
|
}).join('');
|
|
1447
1727
|
const styles = generatePrintPageStyles({
|
|
1448
1728
|
pageWidthMm: widthMm,
|
|
@@ -1469,8 +1749,8 @@ function createPrintEngine(template, data) {
|
|
|
1469
1749
|
/**
|
|
1470
1750
|
* 生成完整打印 HTML
|
|
1471
1751
|
*/
|
|
1472
|
-
generatePrintHTML() {
|
|
1473
|
-
return engine.generatePrintHTML();
|
|
1752
|
+
async generatePrintHTML() {
|
|
1753
|
+
return await engine.generatePrintHTML();
|
|
1474
1754
|
},
|
|
1475
1755
|
/**
|
|
1476
1756
|
* 注册自定义渲染器
|
|
@@ -1487,11 +1767,114 @@ function createPrintEngine(template, data) {
|
|
|
1487
1767
|
};
|
|
1488
1768
|
}
|
|
1489
1769
|
|
|
1770
|
+
/**
|
|
1771
|
+
* 资源加载工具
|
|
1772
|
+
* 用于等待图片、二维码、条形码等异步资源加载完成
|
|
1773
|
+
*/
|
|
1774
|
+
/**
|
|
1775
|
+
* 等待文档中所有图片加载完成
|
|
1776
|
+
* @param doc 文档对象(可以是 window.document 或 iframe.contentDocument)
|
|
1777
|
+
* @param timeout 超时时间(毫秒),默认 10000ms
|
|
1778
|
+
* @returns Promise<void>
|
|
1779
|
+
*/
|
|
1780
|
+
async function waitForImagesLoaded(doc, timeout = 10000) {
|
|
1781
|
+
const images = Array.from(doc.querySelectorAll('img'));
|
|
1782
|
+
if (images.length === 0) {
|
|
1783
|
+
// 没有图片,直接返回
|
|
1784
|
+
return Promise.resolve();
|
|
1785
|
+
}
|
|
1786
|
+
return new Promise((resolve, reject) => {
|
|
1787
|
+
let loadedCount = 0;
|
|
1788
|
+
let errorCount = 0;
|
|
1789
|
+
const totalImages = images.length;
|
|
1790
|
+
// 超时处理
|
|
1791
|
+
const timeoutId = setTimeout(() => {
|
|
1792
|
+
console.warn(`图片加载超时,已加载 ${loadedCount}/${totalImages},失败 ${errorCount} 张`);
|
|
1793
|
+
resolve(); // 超时也算完成,避免无限等待
|
|
1794
|
+
}, timeout);
|
|
1795
|
+
// 检查是否所有图片都已完成
|
|
1796
|
+
const checkComplete = () => {
|
|
1797
|
+
if (loadedCount + errorCount >= totalImages) {
|
|
1798
|
+
clearTimeout(timeoutId);
|
|
1799
|
+
if (errorCount > 0) {
|
|
1800
|
+
console.warn(`有 ${errorCount} 张图片加载失败`);
|
|
1801
|
+
}
|
|
1802
|
+
resolve();
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
// 为每个图片添加加载监听
|
|
1806
|
+
images.forEach((img) => {
|
|
1807
|
+
// 如果图片已经加载完成(cached)
|
|
1808
|
+
if (img.complete) {
|
|
1809
|
+
if (img.naturalHeight !== 0) {
|
|
1810
|
+
loadedCount++;
|
|
1811
|
+
}
|
|
1812
|
+
else {
|
|
1813
|
+
errorCount++;
|
|
1814
|
+
}
|
|
1815
|
+
checkComplete();
|
|
1816
|
+
}
|
|
1817
|
+
else {
|
|
1818
|
+
// 监听 load 和 error 事件
|
|
1819
|
+
img.addEventListener('load', () => {
|
|
1820
|
+
loadedCount++;
|
|
1821
|
+
checkComplete();
|
|
1822
|
+
});
|
|
1823
|
+
img.addEventListener('error', (e) => {
|
|
1824
|
+
errorCount++;
|
|
1825
|
+
console.error('图片加载失败:', img.src, e);
|
|
1826
|
+
checkComplete();
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
// 立即检查一次(可能所有图片都已缓存)
|
|
1831
|
+
checkComplete();
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* 等待所有打印资源加载完成
|
|
1836
|
+
* 包括:图片、二维码(已转base64)、条形码(已转base64)
|
|
1837
|
+
* 注意:二维码和条形码在渲染时已同步生成为 base64,所以主要等待外部图片
|
|
1838
|
+
* @param doc 文档对象
|
|
1839
|
+
* @param timeout 超时时间(毫秒),默认 10000ms
|
|
1840
|
+
*/
|
|
1841
|
+
async function waitForPrintResourcesReady(doc, timeout = 10000) {
|
|
1842
|
+
// 目前只需要等待图片加载
|
|
1843
|
+
// 二维码和条形码在 QRCodeRenderer 和 BarcodeRenderer 中已同步生成为 base64
|
|
1844
|
+
return waitForImagesLoaded(doc, timeout);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1490
1847
|
/**
|
|
1491
1848
|
* 打印 SDK 核心类
|
|
1492
1849
|
* 提供完整的打印功能封装
|
|
1493
1850
|
* 解耦设计:直接接收模板数据,不依赖模板服务
|
|
1494
1851
|
*/
|
|
1852
|
+
/**
|
|
1853
|
+
* ✅ 使用 DOMParser 提取 HTML body 内容(比正则更健壮)
|
|
1854
|
+
* @param html 完整的 HTML 字符串
|
|
1855
|
+
* @returns body 内的内容,如果解析失败返回 null
|
|
1856
|
+
*/
|
|
1857
|
+
function extractBodyContent(html) {
|
|
1858
|
+
var _a, _b, _c;
|
|
1859
|
+
try {
|
|
1860
|
+
// 使用 DOMParser 解析 HTML
|
|
1861
|
+
const parser = new DOMParser();
|
|
1862
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
1863
|
+
// 获取 body 内容
|
|
1864
|
+
const bodyContent = (_b = (_a = doc.body) === null || _a === void 0 ? void 0 : _a.innerHTML) === null || _b === void 0 ? void 0 : _b.trim();
|
|
1865
|
+
if (!bodyContent) {
|
|
1866
|
+
console.warn('[PrintSDK] 无法提取 body 内容');
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
return bodyContent;
|
|
1870
|
+
}
|
|
1871
|
+
catch (error) {
|
|
1872
|
+
console.error('[PrintSDK] 解析 HTML 失败:', error);
|
|
1873
|
+
// 兜底:使用正则提取
|
|
1874
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
1875
|
+
return ((_c = bodyMatch === null || bodyMatch === void 0 ? void 0 : bodyMatch[1]) === null || _c === void 0 ? void 0 : _c.trim()) || null;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1495
1878
|
class PrintSDK {
|
|
1496
1879
|
// 无需配置和缓存,完全解耦
|
|
1497
1880
|
/**
|
|
@@ -1511,9 +1894,11 @@ class PrintSDK {
|
|
|
1511
1894
|
if (!printWindow) {
|
|
1512
1895
|
throw new Error('Failed to open print window. Please check browser settings.');
|
|
1513
1896
|
}
|
|
1514
|
-
const html = engine.generatePrintHTML();
|
|
1897
|
+
const html = await engine.generatePrintHTML();
|
|
1515
1898
|
printWindow.document.write(html);
|
|
1516
1899
|
printWindow.document.close();
|
|
1900
|
+
// 等待所有图片加载完成后再打印
|
|
1901
|
+
await waitForImagesLoaded(printWindow.document);
|
|
1517
1902
|
printWindow.print();
|
|
1518
1903
|
}
|
|
1519
1904
|
else {
|
|
@@ -1523,19 +1908,35 @@ class PrintSDK {
|
|
|
1523
1908
|
iframe.style.top = '-9999px';
|
|
1524
1909
|
iframe.style.left = '-9999px';
|
|
1525
1910
|
document.body.appendChild(iframe);
|
|
1526
|
-
const html = engine.generatePrintHTML();
|
|
1911
|
+
const html = await engine.generatePrintHTML();
|
|
1527
1912
|
const iframeDoc = (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document;
|
|
1528
1913
|
if (!iframeDoc) {
|
|
1529
1914
|
throw new Error('Failed to access iframe document');
|
|
1530
1915
|
}
|
|
1531
1916
|
iframeDoc.write(html);
|
|
1532
1917
|
iframeDoc.close();
|
|
1533
|
-
//
|
|
1918
|
+
// 等待所有图片加载完成后再打印
|
|
1919
|
+
// 注意:二维码和条形码已同步生成为base64,主要等待外部图片资源
|
|
1920
|
+
await waitForImagesLoaded(iframeDoc);
|
|
1921
|
+
// ✅ 监听打印完成事件后再移除 iframe
|
|
1922
|
+
const cleanup = () => {
|
|
1923
|
+
if (iframe.parentNode) {
|
|
1924
|
+
document.body.removeChild(iframe);
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
// 优先使用 afterprint 事件(现代浏览器支持)
|
|
1928
|
+
if (iframe.contentWindow) {
|
|
1929
|
+
iframe.contentWindow.addEventListener('afterprint', cleanup, { once: true });
|
|
1930
|
+
}
|
|
1931
|
+
// 触发打印
|
|
1534
1932
|
(_b = iframe.contentWindow) === null || _b === void 0 ? void 0 : _b.print();
|
|
1535
|
-
//
|
|
1933
|
+
// 兜底:如果 afterprint 事件未触发(如用户取消打印),5秒后清理
|
|
1536
1934
|
setTimeout(() => {
|
|
1537
|
-
|
|
1538
|
-
|
|
1935
|
+
if (iframe.parentNode) {
|
|
1936
|
+
console.warn('[PrintSDK] afterprint 事件未触发,执行兜底清理');
|
|
1937
|
+
cleanup();
|
|
1938
|
+
}
|
|
1939
|
+
}, 5000);
|
|
1539
1940
|
}
|
|
1540
1941
|
}
|
|
1541
1942
|
/**
|
|
@@ -1562,7 +1963,7 @@ class PrintSDK {
|
|
|
1562
1963
|
*/
|
|
1563
1964
|
async generateHTML(template, data) {
|
|
1564
1965
|
const engine = createPrintEngine(template, data);
|
|
1565
|
-
return engine.generatePrintHTML();
|
|
1966
|
+
return await engine.generatePrintHTML();
|
|
1566
1967
|
}
|
|
1567
1968
|
/**
|
|
1568
1969
|
* 批量打印(同模板多数据)
|
|
@@ -1591,11 +1992,11 @@ class PrintSDK {
|
|
|
1591
1992
|
onProgress === null || onProgress === void 0 ? void 0 : onProgress(progress);
|
|
1592
1993
|
try {
|
|
1593
1994
|
const engine = createPrintEngine(template, data);
|
|
1594
|
-
const html = engine.generatePrintHTML();
|
|
1595
|
-
// 提取 <body>
|
|
1596
|
-
const
|
|
1597
|
-
if (
|
|
1598
|
-
allPagesHTML.push(
|
|
1995
|
+
const html = await engine.generatePrintHTML();
|
|
1996
|
+
// ✅ 提取 <body> 标签中的内容(使用 DOMParser 代替正则,更健壮)
|
|
1997
|
+
const bodyContent = extractBodyContent(html);
|
|
1998
|
+
if (bodyContent) {
|
|
1999
|
+
allPagesHTML.push(bodyContent);
|
|
1599
2000
|
}
|
|
1600
2001
|
progress.completed++;
|
|
1601
2002
|
onProgress === null || onProgress === void 0 ? void 0 : onProgress(progress);
|
|
@@ -1633,7 +2034,8 @@ class PrintSDK {
|
|
|
1633
2034
|
}
|
|
1634
2035
|
printWindow.document.write(fullHTML);
|
|
1635
2036
|
printWindow.document.close();
|
|
1636
|
-
//
|
|
2037
|
+
// 等待所有图片加载完成后再打印
|
|
2038
|
+
await waitForImagesLoaded(printWindow.document);
|
|
1637
2039
|
printWindow.print();
|
|
1638
2040
|
}
|
|
1639
2041
|
else {
|
|
@@ -1649,7 +2051,8 @@ class PrintSDK {
|
|
|
1649
2051
|
}
|
|
1650
2052
|
iframeDoc.write(fullHTML);
|
|
1651
2053
|
iframeDoc.close();
|
|
1652
|
-
//
|
|
2054
|
+
// 等待所有图片加载完成后再打印
|
|
2055
|
+
await waitForImagesLoaded(iframeDoc);
|
|
1653
2056
|
(_b = iframe.contentWindow) === null || _b === void 0 ? void 0 : _b.print();
|
|
1654
2057
|
// 打印完成后移除 iframe
|
|
1655
2058
|
setTimeout(() => {
|
|
@@ -1699,4 +2102,6 @@ exports.getAllPipes = getAllPipes;
|
|
|
1699
2102
|
exports.getExecutor = getExecutor;
|
|
1700
2103
|
exports.getPageSizeFromConfig = getPageSizeFromConfig;
|
|
1701
2104
|
exports.registerExecutor = registerExecutor;
|
|
2105
|
+
exports.waitForImagesLoaded = waitForImagesLoaded;
|
|
2106
|
+
exports.waitForPrintResourcesReady = waitForPrintResourcesReady;
|
|
1702
2107
|
//# sourceMappingURL=index.js.map
|