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