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