@jcyao/print-sdk 1.0.1 → 1.1.1
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 +13 -5
- package/dist/index.esm.js +313 -69
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +313 -69
- package/dist/index.js.map +1 -1
- package/dist/printEngine/constants.d.ts +2 -2
- package/dist/printEngine.d.ts +8 -3
- package/package.json +2 -1
package/dist/index.esm.js
CHANGED
|
@@ -48,8 +48,8 @@ const TABLE_DEFAULT = {
|
|
|
48
48
|
HEADER_HEIGHT: 10,
|
|
49
49
|
/** 数据行最小高度(mm) */
|
|
50
50
|
MIN_ROW_HEIGHT: 8,
|
|
51
|
-
/**
|
|
52
|
-
ROW_HEIGHT_FACTOR: 1.
|
|
51
|
+
/** 行高计算系数(用于 min-height,实际高度由内容撑开) */
|
|
52
|
+
ROW_HEIGHT_FACTOR: 1.0,
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
55
|
* 样式默认值
|
|
@@ -584,6 +584,25 @@ class TextRenderer {
|
|
|
584
584
|
/**
|
|
585
585
|
* 表格组件渲染器
|
|
586
586
|
*/
|
|
587
|
+
/**
|
|
588
|
+
* 根据数据路径从对象中取值
|
|
589
|
+
* 支持嵌套路径,如:'product.name' => obj.product.name
|
|
590
|
+
* @param obj 数据对象
|
|
591
|
+
* @param path 属性路径,支持点号分隔的嵌套路径
|
|
592
|
+
* @returns 属性值,路径不存在时返回 undefined
|
|
593
|
+
*/
|
|
594
|
+
function getByPath(obj, path) {
|
|
595
|
+
if (!obj || !path)
|
|
596
|
+
return undefined;
|
|
597
|
+
const keys = path.split('.');
|
|
598
|
+
let value = obj;
|
|
599
|
+
for (const key of keys) {
|
|
600
|
+
if (value === null || value === undefined)
|
|
601
|
+
return undefined;
|
|
602
|
+
value = value[key];
|
|
603
|
+
}
|
|
604
|
+
return value;
|
|
605
|
+
}
|
|
587
606
|
class TableRenderer {
|
|
588
607
|
constructor() {
|
|
589
608
|
this.type = 'table';
|
|
@@ -604,7 +623,11 @@ class TableRenderer {
|
|
|
604
623
|
// 表头配置,过滤隐藏列
|
|
605
624
|
const allColumns = (props === null || props === void 0 ? void 0 : props.columns) || [];
|
|
606
625
|
const visibleColumns = allColumns.filter((col) => !col.hidden);
|
|
607
|
-
|
|
626
|
+
// 支持分页传入的 _showHeader 标记,优先于 props.showHeader
|
|
627
|
+
const explicitShowHeader = props && typeof props._showHeader === 'boolean'
|
|
628
|
+
? props._showHeader
|
|
629
|
+
: undefined;
|
|
630
|
+
const showHeader = explicitShowHeader !== undefined ? explicitShowHeader : (props === null || props === void 0 ? void 0 : props.showHeader) !== false;
|
|
608
631
|
const bordered = (props === null || props === void 0 ? void 0 : props.bordered) !== false;
|
|
609
632
|
// 计算表格宽度
|
|
610
633
|
let tableWidthMm;
|
|
@@ -633,9 +656,17 @@ class TableRenderer {
|
|
|
633
656
|
// 备用:使用默认值
|
|
634
657
|
tableWidthMm = COMPONENT_DEFAULT_SIZE.TABLE_WIDTH;
|
|
635
658
|
}
|
|
659
|
+
// ✅ 检查 xMm 是否已超过可用宽度
|
|
660
|
+
const availableWidth = context.pageInfo
|
|
661
|
+
? context.pageInfo.widthMm - context.pageInfo.marginMm.left - context.pageInfo.marginMm.right
|
|
662
|
+
: COMPONENT_DEFAULT_SIZE.TABLE_WIDTH;
|
|
663
|
+
if (xMm >= availableWidth) {
|
|
664
|
+
console.error(`[TableRenderer] 表格位置错误:xMm(${xMm.toFixed(2)}mm) 已超过页面可用宽度(${availableWidth.toFixed(2)}mm),` +
|
|
665
|
+
`表格将无法正常显示。请调整表格的 x 位置,使其小于 ${availableWidth.toFixed(2)}mm`);
|
|
666
|
+
}
|
|
636
667
|
// 最小宽度保护
|
|
637
668
|
if (tableWidthMm < 10) {
|
|
638
|
-
console.warn(
|
|
669
|
+
console.warn(`[TableRenderer] 表格宽度过小 (${tableWidthMm.toFixed(2)}mm),强制设置为最小宽度 10mm`);
|
|
639
670
|
tableWidthMm = 10;
|
|
640
671
|
}
|
|
641
672
|
console.log(tableWidthMm, "tableWidthMm");
|
|
@@ -647,20 +678,26 @@ class TableRenderer {
|
|
|
647
678
|
// 单元格样式
|
|
648
679
|
const cellBorder = bordered ? `border: 1px solid ${TABLE_STYLE_DEFAULT.BORDER_COLOR};` : '';
|
|
649
680
|
const cellPadding = `padding: ${TABLE_STYLE_DEFAULT.CELL_PADDING};`;
|
|
650
|
-
const cellTextStyle = `white-space: normal; word-break: break-word; line-height: ${STYLE_DEFAULT.LINE_HEIGHT}; vertical-align:
|
|
681
|
+
const cellTextStyle = `white-space: normal; word-break: break-word; line-height: ${STYLE_DEFAULT.LINE_HEIGHT}; vertical-align: middle;`;
|
|
651
682
|
const textAlign = (style === null || style === void 0 ? void 0 : style.textAlign) || 'left'; // 对齐方式
|
|
683
|
+
// ✅ 计算均分列宽(简化方案:按列数均分表格宽度)
|
|
684
|
+
const colCount = visibleColumns.length || 1;
|
|
685
|
+
const colWidthPercent = (100 / colCount).toFixed(2);
|
|
652
686
|
// ✅ 计算表头和数据行的高度(mm 转 px)
|
|
653
|
-
|
|
654
|
-
const
|
|
687
|
+
// 使用全局常量 ROW_HEIGHT_FACTOR,实际高度由内容自然撑开(min-height)
|
|
688
|
+
const headerHeightPx = TABLE_DEFAULT.HEADER_HEIGHT * context.mmToPx;
|
|
689
|
+
const rowHeightPx = TABLE_DEFAULT.MIN_ROW_HEIGHT * TABLE_DEFAULT.ROW_HEIGHT_FACTOR * context.mmToPx;
|
|
655
690
|
// 渲染表头
|
|
656
691
|
let headerHtml = '';
|
|
657
692
|
if (showHeader && visibleColumns.length > 0) {
|
|
658
693
|
const headerCells = visibleColumns
|
|
659
694
|
.map((col) => {
|
|
660
695
|
const title = col.title || col.dataIndex;
|
|
661
|
-
|
|
696
|
+
// 使用百分比宽度实现均分,min-height 允许自然扩展
|
|
697
|
+
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>`;
|
|
662
698
|
})
|
|
663
699
|
.join('');
|
|
700
|
+
// 表头使用固定高度,表体使用 min-height
|
|
664
701
|
headerHtml = `<thead class="table-header-repeat"><tr style="height: ${headerHeightPx}px;">${headerCells}</tr></thead>`;
|
|
665
702
|
}
|
|
666
703
|
// 渲染表体
|
|
@@ -670,18 +707,21 @@ class TableRenderer {
|
|
|
670
707
|
.map((row) => {
|
|
671
708
|
const cells = visibleColumns
|
|
672
709
|
.map((col) => {
|
|
673
|
-
|
|
674
|
-
|
|
710
|
+
var _a;
|
|
711
|
+
const value = (_a = getByPath(row, col.dataIndex)) !== null && _a !== void 0 ? _a : '';
|
|
712
|
+
// 使用百分比宽度,min-height 允许内容换行时自然扩展
|
|
713
|
+
return `<td style="${cellBorder} ${cellPadding} ${cellTextStyle} text-align: ${textAlign}; width: ${colWidthPercent}%; min-height: ${rowHeightPx}px; box-sizing: border-box;">${value}</td>`;
|
|
675
714
|
})
|
|
676
715
|
.join('');
|
|
677
|
-
|
|
716
|
+
// 行使用 min-height 而非固定 height
|
|
717
|
+
return `<tr style="min-height: ${rowHeightPx}px;">${cells}</tr>`;
|
|
678
718
|
})
|
|
679
719
|
.join('');
|
|
680
720
|
bodyHtml = `<tbody>${rows}</tbody>`;
|
|
681
721
|
}
|
|
682
722
|
else {
|
|
683
723
|
const colspan = visibleColumns.length || 1;
|
|
684
|
-
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>`;
|
|
724
|
+
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>`;
|
|
685
725
|
}
|
|
686
726
|
// 渲染合计行(新增)
|
|
687
727
|
const showSummary = (props === null || props === void 0 ? void 0 : props.showSummary) === true;
|
|
@@ -696,14 +736,14 @@ class TableRenderer {
|
|
|
696
736
|
? props._totalData
|
|
697
737
|
: tableData;
|
|
698
738
|
const summaryHtml = shouldShowSummary
|
|
699
|
-
? this.renderSummary(summaryData, visibleColumns, props, cellBorder, cellPadding, cellTextStyle, rowHeightPx, textAlign)
|
|
739
|
+
? this.renderSummary(summaryData, visibleColumns, props, cellBorder, cellPadding, cellTextStyle, rowHeightPx, textAlign, colWidthPercent)
|
|
700
740
|
: '';
|
|
701
741
|
return `<table class="print-table" style="${tableStyleStr}">${headerHtml}${bodyHtml}${summaryHtml}</table>`;
|
|
702
742
|
}
|
|
703
743
|
/**
|
|
704
744
|
* 渲染合计行
|
|
705
745
|
*/
|
|
706
|
-
renderSummary(data, columns, props, cellBorder, cellPadding, cellTextStyle, rowHeightPx, defaultTextAlign) {
|
|
746
|
+
renderSummary(data, columns, props, cellBorder, cellPadding, cellTextStyle, rowHeightPx, defaultTextAlign, colWidthPercent = 'auto') {
|
|
707
747
|
if (!columns.length)
|
|
708
748
|
return '';
|
|
709
749
|
const summaryLabel = props.summaryLabel || '合计';
|
|
@@ -726,7 +766,8 @@ class TableRenderer {
|
|
|
726
766
|
${cellPadding}
|
|
727
767
|
${cellTextStyle}
|
|
728
768
|
text-align: ${col.align || defaultTextAlign};
|
|
729
|
-
|
|
769
|
+
width: ${colWidthPercent}%;
|
|
770
|
+
min-height: ${rowHeightPx}px;
|
|
730
771
|
box-sizing: border-box;
|
|
731
772
|
background: ${bgColor};
|
|
732
773
|
font-weight: ${fontWeight};
|
|
@@ -734,7 +775,7 @@ class TableRenderer {
|
|
|
734
775
|
`.trim().replace(/\s+/g, ' ');
|
|
735
776
|
return `<td style="${cellStyle}">${content}</td>`;
|
|
736
777
|
}).join('');
|
|
737
|
-
return `<tfoot><tr style="height: ${rowHeightPx}px;">${cells}</tr></tfoot>`;
|
|
778
|
+
return `<tfoot><tr style="min-height: ${rowHeightPx}px;">${cells}</tr></tfoot>`;
|
|
738
779
|
}
|
|
739
780
|
/**
|
|
740
781
|
* 计算单列合计值(使用 Decimal.js 解决精度问题)
|
|
@@ -748,7 +789,7 @@ class TableRenderer {
|
|
|
748
789
|
return '';
|
|
749
790
|
const values = data
|
|
750
791
|
.map(row => {
|
|
751
|
-
const val = row
|
|
792
|
+
const val = getByPath(row, column.dataIndex);
|
|
752
793
|
// 尝试转换为数字,失败则返回 null
|
|
753
794
|
const num = Number(val);
|
|
754
795
|
return isNaN(num) ? null : num;
|
|
@@ -780,25 +821,38 @@ class TableRenderer {
|
|
|
780
821
|
}
|
|
781
822
|
}
|
|
782
823
|
catch (error) {
|
|
783
|
-
console.error('
|
|
824
|
+
console.error('[TableRenderer] 合计计算错误:', error);
|
|
825
|
+
// ✅ 返回友好的错误提示,而不是静默失败
|
|
826
|
+
return '计算错误';
|
|
827
|
+
}
|
|
828
|
+
// ✅ 格式化前检查结果是否有效
|
|
829
|
+
if (!result || typeof result.toFixed !== 'function') {
|
|
830
|
+
console.warn('[TableRenderer] 合计结果无效:', result);
|
|
784
831
|
return '-';
|
|
785
832
|
}
|
|
786
833
|
// 格式化
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
834
|
+
try {
|
|
835
|
+
const precision = (_a = summary.precision) !== null && _a !== void 0 ? _a : 2;
|
|
836
|
+
const formatted = result.toFixed(precision);
|
|
837
|
+
const prefix = summary.prefix || '';
|
|
838
|
+
const suffix = summary.suffix || '';
|
|
839
|
+
return `${prefix}${formatted}${suffix}`;
|
|
840
|
+
}
|
|
841
|
+
catch (formatError) {
|
|
842
|
+
console.error('[TableRenderer] 格式化合计结果失败:', formatError);
|
|
843
|
+
return '-';
|
|
844
|
+
}
|
|
792
845
|
}
|
|
793
846
|
calculateHeight(component, context) {
|
|
794
847
|
var _a, _b, _c;
|
|
795
|
-
//
|
|
848
|
+
// 表格高度:简单估算(用于初始布局计算,实际分页使用 measureTableRowHeights)
|
|
796
849
|
if ((_a = component.binding) === null || _a === void 0 ? void 0 : _a.path) {
|
|
797
850
|
const data = context.getValueByPath(component.binding.path);
|
|
798
851
|
if (Array.isArray(data) && data.length > 0) {
|
|
799
852
|
const headerHeight = ((_b = component.props) === null || _b === void 0 ? void 0 : _b.showHeader) !== false ? TABLE_DEFAULT.HEADER_HEIGHT : 0;
|
|
800
|
-
|
|
801
|
-
const
|
|
853
|
+
// 使用基础行高(不乘系数),实际高度由渲染后测量决定
|
|
854
|
+
const rowHeight = TABLE_DEFAULT.MIN_ROW_HEIGHT;
|
|
855
|
+
const summaryHeight = ((_c = component.props) === null || _c === void 0 ? void 0 : _c.showSummary) === true ? rowHeight : 0;
|
|
802
856
|
return headerHeight + data.length * rowHeight + summaryHeight;
|
|
803
857
|
}
|
|
804
858
|
}
|
|
@@ -1187,6 +1241,11 @@ class PrintEngine {
|
|
|
1187
1241
|
* 计算表头高度(mm)
|
|
1188
1242
|
*/
|
|
1189
1243
|
calculateTableHeaderHeight(comp) {
|
|
1244
|
+
var _a;
|
|
1245
|
+
// 如果用户设置 showHeader: false,则表头高度为 0
|
|
1246
|
+
if (((_a = comp.props) === null || _a === void 0 ? void 0 : _a.showHeader) === false) {
|
|
1247
|
+
return 0;
|
|
1248
|
+
}
|
|
1190
1249
|
// 使用 TABLE_DEFAULT.HEADER_HEIGHT 作为表头高度
|
|
1191
1250
|
return TABLE_DEFAULT.HEADER_HEIGHT;
|
|
1192
1251
|
}
|
|
@@ -1307,7 +1366,7 @@ class PrintEngine {
|
|
|
1307
1366
|
* 2. 按顺序累加高度,遇到表格就拆分
|
|
1308
1367
|
* 3. 换页时从 marginTop 开始,忽略原 gap
|
|
1309
1368
|
*/
|
|
1310
|
-
calculatePages(components) {
|
|
1369
|
+
async calculatePages(components) {
|
|
1311
1370
|
var _a, _b, _c;
|
|
1312
1371
|
const { page } = this.template;
|
|
1313
1372
|
const { heightMm } = this.getPageSize();
|
|
@@ -1327,8 +1386,10 @@ class PrintEngine {
|
|
|
1327
1386
|
return { comp, gap: (comp.layout.yMm || 0) };
|
|
1328
1387
|
}
|
|
1329
1388
|
const prevComp = sortedComponents[index - 1];
|
|
1389
|
+
// ✅ 使用设计高度计算相对间距(保留负数,表示组件重叠)
|
|
1390
|
+
// 注意:表格的实际高度会在 splitTableWithGap 中重新计算
|
|
1330
1391
|
const prevBottom = (prevComp.layout.yMm || 0) + (prevComp.layout.heightMm || 0);
|
|
1331
|
-
const gap = (comp.layout.yMm || 0) - prevBottom;
|
|
1392
|
+
const gap = (comp.layout.yMm || 0) - prevBottom; // 保留负数,表示组件重叠
|
|
1332
1393
|
return { comp, gap };
|
|
1333
1394
|
});
|
|
1334
1395
|
// 3. 遍历组件,累加高度
|
|
@@ -1338,8 +1399,13 @@ class PrintEngine {
|
|
|
1338
1399
|
let currentPageHeight = marginTop; // 当前页的累计高度
|
|
1339
1400
|
let isFirstComponentInPage = true; // 标记当前页是否是第一个组件
|
|
1340
1401
|
for (let i = 0; i < componentsWithGaps.length; i++) {
|
|
1341
|
-
const { comp, gap } = componentsWithGaps[i];
|
|
1402
|
+
const { comp, gap: designGap } = componentsWithGaps[i];
|
|
1342
1403
|
const compHeightMm = comp.layout.heightMm || 50;
|
|
1404
|
+
// ✅ 使用设计时的相对间距(保留负数,表示组件重叠)
|
|
1405
|
+
// designGap 是设计时计算的:组件B.yMm - (组件A.yMm + 组件A.heightMm)
|
|
1406
|
+
// 这代表了设计意图中的"组件A底部到组件B顶部的间距"
|
|
1407
|
+
// 负数表示组件重叠,这是设计时允许的布局方式
|
|
1408
|
+
const actualGap = designGap;
|
|
1343
1409
|
// 边界检查:组件高度接近页面高度时输出警告
|
|
1344
1410
|
if (compHeightMm > availableHeightMm * 0.8) {
|
|
1345
1411
|
console.warn(`组件 ${comp.id} (${comp.type}) 高度 ${compHeightMm.toFixed(2)}mm 接近页面可用高度 ${availableHeightMm.toFixed(2)}mm,可能影响分页效果`);
|
|
@@ -1348,14 +1414,16 @@ class PrintEngine {
|
|
|
1348
1414
|
if (comp.type === 'table' && ((_c = comp.binding) === null || _c === void 0 ? void 0 : _c.path)) {
|
|
1349
1415
|
const tableData = context.getValueByPath(comp.binding.path);
|
|
1350
1416
|
if (Array.isArray(tableData) && tableData.length > 0) {
|
|
1351
|
-
const result = this.splitTableWithGap(comp, tableData,
|
|
1417
|
+
const result = await this.splitTableWithGap(comp, tableData, actualGap, isFirstComponentInPage, availableHeightMm, currentPageHeight, pages, currentPage);
|
|
1352
1418
|
currentPage = result.currentPage;
|
|
1353
|
-
|
|
1354
|
-
|
|
1419
|
+
// ✅ 对于紧跟表格的组件,使用表格实际底部位置作为参考
|
|
1420
|
+
// 这样可以确保无论表格是否跨页,后续组件与表格的相对间距保持一致
|
|
1421
|
+
currentPageHeight = result.lastTableFragmentBottom;
|
|
1422
|
+
isFirstComponentInPage = false; // 表格后的组件不是页面第一个组件
|
|
1355
1423
|
}
|
|
1356
1424
|
else {
|
|
1357
1425
|
// 空表格:按普通组件处理
|
|
1358
|
-
if (this.shouldBreakPage(currentPageHeight, compHeightMm,
|
|
1426
|
+
if (this.shouldBreakPage(currentPageHeight, compHeightMm, actualGap, availableHeightMm, isFirstComponentInPage) && currentPage.length > 0) {
|
|
1359
1427
|
// 换页
|
|
1360
1428
|
pages.push(currentPage);
|
|
1361
1429
|
currentPage = [];
|
|
@@ -1371,7 +1439,7 @@ class PrintEngine {
|
|
|
1371
1439
|
}
|
|
1372
1440
|
else {
|
|
1373
1441
|
// 同一页:应用 gap
|
|
1374
|
-
currentPageHeight +=
|
|
1442
|
+
currentPageHeight += actualGap;
|
|
1375
1443
|
compCopy.layout.yMm = currentPageHeight;
|
|
1376
1444
|
currentPageHeight += compHeightMm;
|
|
1377
1445
|
}
|
|
@@ -1382,7 +1450,7 @@ class PrintEngine {
|
|
|
1382
1450
|
// 4.2 普通组件:按相对间距累加高度
|
|
1383
1451
|
else {
|
|
1384
1452
|
// 使用辅助方法判断是否需要换页
|
|
1385
|
-
if (this.shouldBreakPage(currentPageHeight, compHeightMm,
|
|
1453
|
+
if (this.shouldBreakPage(currentPageHeight, compHeightMm, actualGap, availableHeightMm, isFirstComponentInPage) && currentPage.length > 0) {
|
|
1386
1454
|
// 换页
|
|
1387
1455
|
pages.push(currentPage);
|
|
1388
1456
|
currentPage = [];
|
|
@@ -1392,13 +1460,15 @@ class PrintEngine {
|
|
|
1392
1460
|
// 创建组件副本,避免修改原数据
|
|
1393
1461
|
const compCopy = Object.assign(Object.assign({}, comp), { layout: Object.assign({}, comp.layout) });
|
|
1394
1462
|
if (isFirstComponentInPage) {
|
|
1395
|
-
//
|
|
1463
|
+
// 新页面第一个组件:应用 gap(从页面顶部开始的相对间距)
|
|
1464
|
+
// 注意:即使在新页面,也应该保持设计时的相对间距
|
|
1465
|
+
currentPageHeight += actualGap;
|
|
1396
1466
|
compCopy.layout.yMm = currentPageHeight;
|
|
1397
1467
|
currentPageHeight += compHeightMm;
|
|
1398
1468
|
}
|
|
1399
1469
|
else {
|
|
1400
1470
|
// 同一页:应用 gap
|
|
1401
|
-
currentPageHeight +=
|
|
1471
|
+
currentPageHeight += actualGap;
|
|
1402
1472
|
compCopy.layout.yMm = currentPageHeight;
|
|
1403
1473
|
currentPageHeight += compHeightMm;
|
|
1404
1474
|
}
|
|
@@ -1413,24 +1483,140 @@ class PrintEngine {
|
|
|
1413
1483
|
// 6. 返回分页结果
|
|
1414
1484
|
return pages.length > 0 ? pages : [components];
|
|
1415
1485
|
}
|
|
1486
|
+
/**
|
|
1487
|
+
* 测量表格实际行高(渲染后测量方案)
|
|
1488
|
+
* 将表格渲染到隐藏容器,测量表头、数据行和合计行的实际高度
|
|
1489
|
+
*/
|
|
1490
|
+
async measureTableRowHeights(tableComponent, tableData) {
|
|
1491
|
+
var _a;
|
|
1492
|
+
// 检查是否在浏览器环境
|
|
1493
|
+
if (typeof document === 'undefined') {
|
|
1494
|
+
// 服务器端:使用估算值
|
|
1495
|
+
const baseRowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1496
|
+
return {
|
|
1497
|
+
headerHeight: this.calculateTableHeaderHeight(tableComponent),
|
|
1498
|
+
rowHeights: tableData.map(() => baseRowHeight),
|
|
1499
|
+
summaryHeight: baseRowHeight
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
const renderer = this.renderers.get('table');
|
|
1503
|
+
if (!renderer) {
|
|
1504
|
+
const baseRowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1505
|
+
return {
|
|
1506
|
+
headerHeight: this.calculateTableHeaderHeight(tableComponent),
|
|
1507
|
+
rowHeights: tableData.map(() => baseRowHeight),
|
|
1508
|
+
summaryHeight: baseRowHeight
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
// 计算与 TableRenderer 一致的表格宽度
|
|
1512
|
+
const context = this.createRenderContext();
|
|
1513
|
+
const xMm = tableComponent.layout.xMm || 0;
|
|
1514
|
+
let tableWidthMm;
|
|
1515
|
+
if (tableComponent.layout.widthMm) {
|
|
1516
|
+
tableWidthMm = tableComponent.layout.widthMm;
|
|
1517
|
+
if (context.pageInfo) {
|
|
1518
|
+
const availableWidth = context.pageInfo.widthMm - context.pageInfo.marginMm.left - context.pageInfo.marginMm.right;
|
|
1519
|
+
const totalOccupied = xMm + tableWidthMm;
|
|
1520
|
+
if (totalOccupied > availableWidth) {
|
|
1521
|
+
tableWidthMm = availableWidth - xMm;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
else if (context.pageInfo) {
|
|
1526
|
+
const availableWidth = context.pageInfo.widthMm - context.pageInfo.marginMm.left - context.pageInfo.marginMm.right;
|
|
1527
|
+
tableWidthMm = availableWidth - xMm;
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
tableWidthMm = COMPONENT_DEFAULT_SIZE.TABLE_WIDTH;
|
|
1531
|
+
}
|
|
1532
|
+
// 创建隐藏测量容器
|
|
1533
|
+
const measureContainer = document.createElement('div');
|
|
1534
|
+
measureContainer.style.cssText = `
|
|
1535
|
+
position: absolute;
|
|
1536
|
+
visibility: hidden;
|
|
1537
|
+
left: -9999px;
|
|
1538
|
+
top: 0;
|
|
1539
|
+
width: ${tableWidthMm}mm;
|
|
1540
|
+
pointer-events: none;
|
|
1541
|
+
`;
|
|
1542
|
+
// 创建完整的表格组件用于测量(包含合计行)
|
|
1543
|
+
// 使用用户实际的 showHeader 和 bordered 设置,确保测量准确
|
|
1544
|
+
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 }) });
|
|
1545
|
+
// 渲染表格
|
|
1546
|
+
const tableHtml = renderer.render(measureComponent, context);
|
|
1547
|
+
measureContainer.innerHTML = `
|
|
1548
|
+
<div style="width: ${tableWidthMm}mm; position: relative;">
|
|
1549
|
+
${tableHtml}
|
|
1550
|
+
</div>
|
|
1551
|
+
`;
|
|
1552
|
+
document.body.appendChild(measureContainer);
|
|
1553
|
+
try {
|
|
1554
|
+
// ✅ 测量表头高度(如果存在)
|
|
1555
|
+
let headerHeight = 0;
|
|
1556
|
+
const headerRow = measureContainer.querySelector('thead tr');
|
|
1557
|
+
if (headerRow) {
|
|
1558
|
+
headerHeight = headerRow.offsetHeight / this.mmToPx;
|
|
1559
|
+
}
|
|
1560
|
+
// 测量数据行高度
|
|
1561
|
+
const rows = measureContainer.querySelectorAll('tbody tr');
|
|
1562
|
+
const rowHeights = [];
|
|
1563
|
+
rows.forEach((row) => {
|
|
1564
|
+
const heightPx = row.offsetHeight;
|
|
1565
|
+
const heightMm = heightPx / this.mmToPx;
|
|
1566
|
+
rowHeights.push(heightMm);
|
|
1567
|
+
});
|
|
1568
|
+
// ✅ 测量合计行高度(如果存在)
|
|
1569
|
+
let summaryHeight = 0;
|
|
1570
|
+
const summaryRow = measureContainer.querySelector('tfoot tr');
|
|
1571
|
+
if (summaryRow) {
|
|
1572
|
+
summaryHeight = summaryRow.offsetHeight / this.mmToPx;
|
|
1573
|
+
}
|
|
1574
|
+
// 如果测量失败,使用估算值
|
|
1575
|
+
if (rowHeights.length === 0) {
|
|
1576
|
+
const baseRowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1577
|
+
return {
|
|
1578
|
+
headerHeight: this.calculateTableHeaderHeight(tableComponent),
|
|
1579
|
+
rowHeights: tableData.map(() => baseRowHeight),
|
|
1580
|
+
summaryHeight: baseRowHeight
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
return { headerHeight, rowHeights, summaryHeight };
|
|
1584
|
+
}
|
|
1585
|
+
finally {
|
|
1586
|
+
// 确保无论成功或失败,都清理测量容器
|
|
1587
|
+
if (measureContainer.parentNode) {
|
|
1588
|
+
document.body.removeChild(measureContainer);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1416
1592
|
/**
|
|
1417
1593
|
* 表格跨页拆分(基于相对间距)
|
|
1418
|
-
* 支持 repeatHeader
|
|
1594
|
+
* 支持 repeatHeader 配置、渲染后测量、空表格检查
|
|
1419
1595
|
*/
|
|
1420
|
-
splitTableWithGap(tableComponent, tableData, gap, isFirstComponentInPage, availableHeightMm, currentPageHeight, pages, currentPage) {
|
|
1596
|
+
async splitTableWithGap(tableComponent, tableData, gap, isFirstComponentInPage, availableHeightMm, currentPageHeight, pages, currentPage) {
|
|
1421
1597
|
var _a, _b, _c, _d, _e;
|
|
1422
1598
|
// 读取配置:是否重复表头(默认 true)
|
|
1423
1599
|
const repeatHeader = ((_b = (_a = tableComponent.props) === null || _a === void 0 ? void 0 : _a.pagination) === null || _b === void 0 ? void 0 : _b.repeatHeader) !== false;
|
|
1424
|
-
// 使用精确计算的高度
|
|
1425
|
-
const headerHeight = this.calculateTableHeaderHeight(tableComponent);
|
|
1426
|
-
const rowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1427
1600
|
const marginTop = ((_c = this.template.page.marginMm) === null || _c === void 0 ? void 0 : _c.top) || 0;
|
|
1601
|
+
// 记录初始页面数,用于判断表格是否跨页
|
|
1602
|
+
const initialPagesLength = pages.length;
|
|
1428
1603
|
// 空表格检查
|
|
1429
1604
|
if (tableData.length === 0) {
|
|
1430
1605
|
console.info('表格无数据,跳过渲染');
|
|
1431
|
-
return { currentPage, currentPageHeight };
|
|
1606
|
+
return { currentPage, currentPageHeight, isTableSplitAcrossPages: false, lastTableFragmentBottom: currentPageHeight };
|
|
1607
|
+
}
|
|
1608
|
+
// ✅ 渲染后测量:获取表头、数据行和合计行的实际高度
|
|
1609
|
+
let { headerHeight, rowHeights, summaryHeight: measuredSummaryHeight } = await this.measureTableRowHeights(tableComponent, tableData);
|
|
1610
|
+
// 检查测量结果是否有效(防止所有列 hidden 等情况导致长度不一致)
|
|
1611
|
+
if (rowHeights.length !== tableData.length) {
|
|
1612
|
+
console.warn(`[PrintEngine] 表格测量结果异常:rowHeights.length (${rowHeights.length}) != tableData.length (${tableData.length}),` +
|
|
1613
|
+
`可能所有列均为 hidden,使用估算行高继续分页`);
|
|
1614
|
+
// 使用估算行高替换测量结果
|
|
1615
|
+
const fallbackRowHeight = this.calculateTableRowHeight(tableComponent);
|
|
1616
|
+
rowHeights = tableData.map(() => fallbackRowHeight);
|
|
1432
1617
|
}
|
|
1433
1618
|
let remainingData = [...tableData];
|
|
1619
|
+
let remainingRowHeights = [...rowHeights];
|
|
1434
1620
|
let workingPage = [...currentPage];
|
|
1435
1621
|
let workingPageHeight = currentPageHeight;
|
|
1436
1622
|
let isFirstFragment = true;
|
|
@@ -1442,11 +1628,22 @@ class PrintEngine {
|
|
|
1442
1628
|
if (isFirstFragment && !isFirstComponentInPage) {
|
|
1443
1629
|
remainingHeight -= gap;
|
|
1444
1630
|
}
|
|
1445
|
-
// 计算能放多少行
|
|
1446
1631
|
// 如果 repeatHeader = false 且是第一个片段,则表头已经在第一页了,后续页不需要表头
|
|
1447
1632
|
const needHeader = isFirstFragment || repeatHeader;
|
|
1448
|
-
|
|
1449
|
-
|
|
1633
|
+
let availableForRows = remainingHeight - (needHeader ? headerHeight : 0);
|
|
1634
|
+
// ✅ 使用实际测量的行高计算能放多少行
|
|
1635
|
+
let rowsCanFit = 0;
|
|
1636
|
+
let accumulatedHeight = 0;
|
|
1637
|
+
for (let i = 0; i < remainingRowHeights.length; i++) {
|
|
1638
|
+
const rowHeight = remainingRowHeights[i];
|
|
1639
|
+
if (accumulatedHeight + rowHeight <= availableForRows) {
|
|
1640
|
+
accumulatedHeight += rowHeight;
|
|
1641
|
+
rowsCanFit++;
|
|
1642
|
+
}
|
|
1643
|
+
else {
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1450
1647
|
// 确保至少有 1 行数据(避免只有表头的空页面)
|
|
1451
1648
|
if (rowsCanFit <= 0) {
|
|
1452
1649
|
// 当前页放不下,换页
|
|
@@ -1460,25 +1657,28 @@ class PrintEngine {
|
|
|
1460
1657
|
}
|
|
1461
1658
|
// 取出当前页能放的数据
|
|
1462
1659
|
const dataForThisPage = remainingData.slice(0, rowsCanFit);
|
|
1660
|
+
const rowHeightsForThisPage = remainingRowHeights.slice(0, rowsCanFit);
|
|
1463
1661
|
remainingData = remainingData.slice(rowsCanFit);
|
|
1662
|
+
remainingRowHeights = remainingRowHeights.slice(rowsCanFit);
|
|
1464
1663
|
// 判断是否为最后一页(用于合计行)
|
|
1465
1664
|
const isLastPage = remainingData.length === 0;
|
|
1466
1665
|
// 创建当前页的表格片段
|
|
1467
1666
|
const tableFragmentYMm = isFirstFragment
|
|
1468
1667
|
? (isFirstComponentInPage ? workingPageHeight : workingPageHeight + gap)
|
|
1469
1668
|
: marginTop;
|
|
1470
|
-
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
|
|
1669
|
+
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 // 传递实际行高到渲染器
|
|
1471
1670
|
}) });
|
|
1472
1671
|
workingPage.push(tableFragment);
|
|
1473
1672
|
// 计算合计行高度(如果启用合计功能)
|
|
1474
1673
|
const showSummary = ((_d = tableComponent.props) === null || _d === void 0 ? void 0 : _d.showSummary) === true;
|
|
1475
1674
|
const summaryMode = ((_e = tableComponent.props) === null || _e === void 0 ? void 0 : _e.summaryMode) || 'total';
|
|
1476
|
-
const shouldShowSummaryOnThisPage = showSummary && (summaryMode === 'page' ||
|
|
1477
|
-
(summaryMode === 'total' && isLastPage)
|
|
1478
|
-
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1675
|
+
const shouldShowSummaryOnThisPage = showSummary && (summaryMode === 'page' ||
|
|
1676
|
+
(summaryMode === 'total' && isLastPage));
|
|
1677
|
+
// ✅ 使用测量的合计行高度,如果没有测量值则使用平均行高
|
|
1678
|
+
const avgRowHeight = rowHeightsForThisPage.reduce((a, b) => a + b, 0) / rowHeightsForThisPage.length;
|
|
1679
|
+
const summaryHeight = shouldShowSummaryOnThisPage ? (measuredSummaryHeight || avgRowHeight) : 0;
|
|
1680
|
+
// 更新当前页高度(使用实际测量的行高累加)
|
|
1681
|
+
const tableFragmentHeight = (needHeader ? headerHeight : 0) + accumulatedHeight + summaryHeight;
|
|
1482
1682
|
if (isFirstFragment && !isFirstComponentInPage) {
|
|
1483
1683
|
workingPageHeight += gap + tableFragmentHeight;
|
|
1484
1684
|
}
|
|
@@ -1493,15 +1693,19 @@ class PrintEngine {
|
|
|
1493
1693
|
isFirstFragment = false;
|
|
1494
1694
|
}
|
|
1495
1695
|
}
|
|
1696
|
+
// 判断表格是否跨页(即是否产生了多个页面片段)
|
|
1697
|
+
const isTableSplitAcrossPages = pages.length > initialPagesLength;
|
|
1496
1698
|
return {
|
|
1497
1699
|
currentPage: workingPage,
|
|
1498
|
-
currentPageHeight: workingPageHeight
|
|
1700
|
+
currentPageHeight: workingPageHeight,
|
|
1701
|
+
isTableSplitAcrossPages, // ✅ 返回表格是否跨页的信息
|
|
1702
|
+
lastTableFragmentBottom: workingPageHeight // ✅ 返回最后一个表格片段的底部位置
|
|
1499
1703
|
};
|
|
1500
1704
|
}
|
|
1501
1705
|
/**
|
|
1502
1706
|
* 生成打印 HTML
|
|
1503
1707
|
*/
|
|
1504
|
-
generatePrintHTML() {
|
|
1708
|
+
async generatePrintHTML() {
|
|
1505
1709
|
var _a, _b, _c, _d;
|
|
1506
1710
|
const { page, components } = this.template;
|
|
1507
1711
|
const { widthMm, heightMm } = this.getPageSize();
|
|
@@ -1530,7 +1734,7 @@ class PrintEngine {
|
|
|
1530
1734
|
});
|
|
1531
1735
|
}
|
|
1532
1736
|
// 标准页面模式:虚拟分页,生成多个独立的页面
|
|
1533
|
-
const pages = this.calculatePages(components);
|
|
1737
|
+
const pages = await this.calculatePages(components);
|
|
1534
1738
|
const totalPages = pages.length;
|
|
1535
1739
|
// 渲染每个页面
|
|
1536
1740
|
const pagesHTML = pages.map((pageComponents, index) => {
|
|
@@ -1563,8 +1767,8 @@ function createPrintEngine(template, data) {
|
|
|
1563
1767
|
/**
|
|
1564
1768
|
* 生成完整打印 HTML
|
|
1565
1769
|
*/
|
|
1566
|
-
generatePrintHTML() {
|
|
1567
|
-
return engine.generatePrintHTML();
|
|
1770
|
+
async generatePrintHTML() {
|
|
1771
|
+
return await engine.generatePrintHTML();
|
|
1568
1772
|
},
|
|
1569
1773
|
/**
|
|
1570
1774
|
* 注册自定义渲染器
|
|
@@ -1663,6 +1867,32 @@ async function waitForPrintResourcesReady(doc, timeout = 10000) {
|
|
|
1663
1867
|
* 提供完整的打印功能封装
|
|
1664
1868
|
* 解耦设计:直接接收模板数据,不依赖模板服务
|
|
1665
1869
|
*/
|
|
1870
|
+
/**
|
|
1871
|
+
* ✅ 使用 DOMParser 提取 HTML body 内容(比正则更健壮)
|
|
1872
|
+
* @param html 完整的 HTML 字符串
|
|
1873
|
+
* @returns body 内的内容,如果解析失败返回 null
|
|
1874
|
+
*/
|
|
1875
|
+
function extractBodyContent(html) {
|
|
1876
|
+
var _a, _b, _c;
|
|
1877
|
+
try {
|
|
1878
|
+
// 使用 DOMParser 解析 HTML
|
|
1879
|
+
const parser = new DOMParser();
|
|
1880
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
1881
|
+
// 获取 body 内容
|
|
1882
|
+
const bodyContent = (_b = (_a = doc.body) === null || _a === void 0 ? void 0 : _a.innerHTML) === null || _b === void 0 ? void 0 : _b.trim();
|
|
1883
|
+
if (!bodyContent) {
|
|
1884
|
+
console.warn('[PrintSDK] 无法提取 body 内容');
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
return bodyContent;
|
|
1888
|
+
}
|
|
1889
|
+
catch (error) {
|
|
1890
|
+
console.error('[PrintSDK] 解析 HTML 失败:', error);
|
|
1891
|
+
// 兜底:使用正则提取
|
|
1892
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
1893
|
+
return ((_c = bodyMatch === null || bodyMatch === void 0 ? void 0 : bodyMatch[1]) === null || _c === void 0 ? void 0 : _c.trim()) || null;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1666
1896
|
class PrintSDK {
|
|
1667
1897
|
// 无需配置和缓存,完全解耦
|
|
1668
1898
|
/**
|
|
@@ -1682,7 +1912,7 @@ class PrintSDK {
|
|
|
1682
1912
|
if (!printWindow) {
|
|
1683
1913
|
throw new Error('Failed to open print window. Please check browser settings.');
|
|
1684
1914
|
}
|
|
1685
|
-
const html = engine.generatePrintHTML();
|
|
1915
|
+
const html = await engine.generatePrintHTML();
|
|
1686
1916
|
printWindow.document.write(html);
|
|
1687
1917
|
printWindow.document.close();
|
|
1688
1918
|
// 等待所有图片加载完成后再打印
|
|
@@ -1696,7 +1926,7 @@ class PrintSDK {
|
|
|
1696
1926
|
iframe.style.top = '-9999px';
|
|
1697
1927
|
iframe.style.left = '-9999px';
|
|
1698
1928
|
document.body.appendChild(iframe);
|
|
1699
|
-
const html = engine.generatePrintHTML();
|
|
1929
|
+
const html = await engine.generatePrintHTML();
|
|
1700
1930
|
const iframeDoc = (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document;
|
|
1701
1931
|
if (!iframeDoc) {
|
|
1702
1932
|
throw new Error('Failed to access iframe document');
|
|
@@ -1706,11 +1936,25 @@ class PrintSDK {
|
|
|
1706
1936
|
// 等待所有图片加载完成后再打印
|
|
1707
1937
|
// 注意:二维码和条形码已同步生成为base64,主要等待外部图片资源
|
|
1708
1938
|
await waitForImagesLoaded(iframeDoc);
|
|
1939
|
+
// ✅ 监听打印完成事件后再移除 iframe
|
|
1940
|
+
const cleanup = () => {
|
|
1941
|
+
if (iframe.parentNode) {
|
|
1942
|
+
document.body.removeChild(iframe);
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
// 优先使用 afterprint 事件(现代浏览器支持)
|
|
1946
|
+
if (iframe.contentWindow) {
|
|
1947
|
+
iframe.contentWindow.addEventListener('afterprint', cleanup, { once: true });
|
|
1948
|
+
}
|
|
1949
|
+
// 触发打印
|
|
1709
1950
|
(_b = iframe.contentWindow) === null || _b === void 0 ? void 0 : _b.print();
|
|
1710
|
-
//
|
|
1951
|
+
// 兜底:如果 afterprint 事件未触发(如用户取消打印),5秒后清理
|
|
1711
1952
|
setTimeout(() => {
|
|
1712
|
-
|
|
1713
|
-
|
|
1953
|
+
if (iframe.parentNode) {
|
|
1954
|
+
console.warn('[PrintSDK] afterprint 事件未触发,执行兜底清理');
|
|
1955
|
+
cleanup();
|
|
1956
|
+
}
|
|
1957
|
+
}, 5000);
|
|
1714
1958
|
}
|
|
1715
1959
|
}
|
|
1716
1960
|
/**
|
|
@@ -1737,7 +1981,7 @@ class PrintSDK {
|
|
|
1737
1981
|
*/
|
|
1738
1982
|
async generateHTML(template, data) {
|
|
1739
1983
|
const engine = createPrintEngine(template, data);
|
|
1740
|
-
return engine.generatePrintHTML();
|
|
1984
|
+
return await engine.generatePrintHTML();
|
|
1741
1985
|
}
|
|
1742
1986
|
/**
|
|
1743
1987
|
* 批量打印(同模板多数据)
|
|
@@ -1766,11 +2010,11 @@ class PrintSDK {
|
|
|
1766
2010
|
onProgress === null || onProgress === void 0 ? void 0 : onProgress(progress);
|
|
1767
2011
|
try {
|
|
1768
2012
|
const engine = createPrintEngine(template, data);
|
|
1769
|
-
const html = engine.generatePrintHTML();
|
|
1770
|
-
// 提取 <body>
|
|
1771
|
-
const
|
|
1772
|
-
if (
|
|
1773
|
-
allPagesHTML.push(
|
|
2013
|
+
const html = await engine.generatePrintHTML();
|
|
2014
|
+
// ✅ 提取 <body> 标签中的内容(使用 DOMParser 代替正则,更健壮)
|
|
2015
|
+
const bodyContent = extractBodyContent(html);
|
|
2016
|
+
if (bodyContent) {
|
|
2017
|
+
allPagesHTML.push(bodyContent);
|
|
1774
2018
|
}
|
|
1775
2019
|
progress.completed++;
|
|
1776
2020
|
onProgress === null || onProgress === void 0 ? void 0 : onProgress(progress);
|