gm-printer 10.41.9 → 10.42.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.
@@ -3,6 +3,8 @@ import { action, observable, computed } from 'mobx'
3
3
  import {
4
4
  getSumTrHeight,
5
5
  isMultiTable,
6
+ getMultiNumber,
7
+ getDataKey,
6
8
  caclSingleDetailsPageHeight,
7
9
  getArrayMid,
8
10
  getOverallOrderTrHeight
@@ -11,6 +13,20 @@ import { isIndependentRolDataKey } from '../common/data_key_util'
11
13
  import _ from 'lodash'
12
14
  import Big from 'big.js'
13
15
  import { Tip } from '../components'
16
+ import {
17
+ fillEmptyRowIndex,
18
+ fillPartialLastRow,
19
+ getMaxSerial,
20
+ detectColNumber,
21
+ ensureRowSerialKeys,
22
+ flattenHorizontalPackedRows,
23
+ repackVerticalTableByPageRanges,
24
+ packVerticalNewspaper,
25
+ packHorizontalRowMajor,
26
+ createEmptyRows,
27
+ createEmptyRowSkeleton,
28
+ resolveVerticalEmptyCount
29
+ } from './multi_column'
14
30
 
15
31
  export const TR_BASE_HEIGHT = 23
16
32
 
@@ -48,6 +64,8 @@ class PrinterStore {
48
64
  /** 当前页剩余空白高度 */
49
65
  @observable remainPageHeight = 0
50
66
  @observable isSave = false
67
+ /** data._table 非 observable,变更后递增以驱动预览重渲 */
68
+ @observable dataRevision = 0
51
69
 
52
70
  data = {}
53
71
 
@@ -217,6 +235,19 @@ class PrinterStore {
217
235
  this.config = config || {}
218
236
  }
219
237
 
238
+ /**
239
+ * 签名区高度:优先 DOM 测高,否则用配置 style.height。
240
+ * 打印在 iframe 内时测高常为 0,若只用 this.height.sign 会导致末页不扣签名、空行多填。
241
+ */
242
+ getSignHeight() {
243
+ const measured = this.height?.sign
244
+ if (measured && measured > 0) return measured
245
+ const raw = this.config?.sign?.style?.height
246
+ if (raw == null || raw === '') return 0
247
+ const parsed = parseFloat(raw)
248
+ return Number.isFinite(parsed) ? parsed : 0
249
+ }
250
+
220
251
  /** 处理采购单的明细
221
252
  一行有多个明细, 并且采购明细是换行展示, 明细数据超多的情况则需要分割到不同页面中
222
253
  比如: 一行有20行明细,当前页只能放下17行明细 ,剩下的3行明细需要放到下一页进行展示 */
@@ -357,9 +388,12 @@ class PrinterStore {
357
388
  /** 当前page页面的最小高度 */
358
389
  const currentPageMinimumHeight =
359
390
  allPagesHaveThisHeight + allTableHaveThisHeight
391
+ // 最后一项是表格时,末页仍要渲染签名:装行可用高度预留签名,避免空行顶进签名区
392
+ const isLastContent = index === this.config.contents.length - 1
393
+ const signReserve = isLastContent ? this.getSignHeight() : 0
360
394
  /** 当前page可容纳的table高度 */
361
395
  let pageAccomodateTableHeight = roundHeight(
362
- pageHeight - currentPageHeight
396
+ pageHeight - currentPageHeight - signReserve
363
397
  )
364
398
  // 拷贝一份行高数组,避免 splice 触发 observable 副作用
365
399
  const heights = [
@@ -492,9 +526,9 @@ class PrinterStore {
492
526
  }
493
527
 
494
528
  begin = end
495
- // 开启新一页,重置页面高度
529
+ // 开启新一页,重置页面高度(末项表格仍预留签名)
496
530
  pageAccomodateTableHeight = roundHeight(
497
- pageHeight - allPagesHaveThisHeight
531
+ pageHeight - allPagesHaveThisHeight - signReserve
498
532
  )
499
533
  currentTableHeight = allTableHaveThisHeight
500
534
  currentPageHeight = currentPageMinimumHeight
@@ -534,7 +568,7 @@ class PrinterStore {
534
568
 
535
569
  // 如果是最后一页,必须要加上sign的高度,否则会重叠
536
570
  if (index === contentsLength - 1) {
537
- currentPageHeight += heightMap?.sign || 0
571
+ currentPageHeight += this.getSignHeight()
538
572
  }
539
573
 
540
574
  if (currentPageHeight <= pageHeight) {
@@ -557,9 +591,88 @@ class PrinterStore {
557
591
  }
558
592
  pages.push(page)
559
593
  this.pages = pages
560
- this.remainPageHeight = Math.round(pageHeight - currentPageHeight)
594
+
595
+ // 末页签名:panel 分支会在「最后一项是 panel」时扣 sign;
596
+ // 最后一项是 table 时上面不会扣,导致 remain 偏大、空行多填溢页
597
+ // 打印 iframe 内 height.sign 可能为 0,必须用 getSignHeight(含配置兜底)
598
+ const lastContent = contents[contentsLength - 1]
599
+ if (lastContent?.type === 'table') {
600
+ currentPageHeight += this.getSignHeight()
601
+ }
602
+
603
+ this.remainPageHeight = Math.max(
604
+ 0,
605
+ Math.round(pageHeight - currentPageHeight)
606
+ )
561
607
  this.isSave = false
562
608
  this.config.isSave = false
609
+ this.repackVerticalMultiByPages()
610
+ }
611
+
612
+ getTablePageRanges(contentIndex, rows) {
613
+ const ranges = []
614
+ this.pages.forEach(page => {
615
+ page.forEach(panel => {
616
+ if (panel.type === 'table' && panel.index === contentIndex) {
617
+ ranges.push({ begin: panel.begin, end: panel.end })
618
+ }
619
+ })
620
+ })
621
+ if (!ranges.length || !rows?.length) return ranges
622
+ const last = ranges[ranges.length - 1]
623
+ let end = last.end
624
+ while (end < rows.length && rows[end]?._isEmptyData) {
625
+ end++
626
+ }
627
+ last.end = end
628
+ return ranges
629
+ }
630
+
631
+ @action
632
+ repackVerticalMultiByPages() {
633
+ if (!this.pages?.length) return
634
+ this.config.contents.forEach((content, contentIndex) => {
635
+ if (content?.type !== 'table' || !isMultiTable(content.dataKey)) {
636
+ return
637
+ }
638
+ const dataKey = content.dataKey
639
+ const isVertical =
640
+ content.arrange === 'vertical' || /_vertical$/.test(dataKey)
641
+ if (!isVertical) return
642
+
643
+ const sourceKey = dataKey.replace(/_vertical$/, '')
644
+ const verticalKey = /_vertical$/.test(dataKey)
645
+ ? dataKey
646
+ : `${sourceKey}_vertical`
647
+ const colNumber = getMultiNumber(sourceKey)
648
+ const verticalRows = this.data._table[verticalKey]
649
+ if (!verticalRows?.length) return
650
+
651
+ const singleKey = sourceKey.replace(/_multi3?/, '')
652
+ const sourceRows =
653
+ this.data._table[singleKey] || this.data._table[sourceKey] || []
654
+ const sourceColNumber = isMultiTable(singleKey)
655
+ ? getMultiNumber(singleKey)
656
+ : isMultiTable(sourceKey)
657
+ ? colNumber
658
+ : 1
659
+ const sourceItems = flattenHorizontalPackedRows(
660
+ sourceRows,
661
+ sourceColNumber
662
+ )
663
+ // 无商品时仍可能需要给空行按列编序号
664
+ if (!sourceItems.length && !this.fillIndex) return
665
+
666
+ const pageRanges = this.getTablePageRanges(contentIndex, verticalRows)
667
+ this.data._table[verticalKey] = repackVerticalTableByPageRanges(
668
+ verticalRows,
669
+ colNumber,
670
+ pageRanges,
671
+ this.fillIndex,
672
+ sourceItems
673
+ )
674
+ this.dataRevision += 1
675
+ })
563
676
  }
564
677
 
565
678
  @action
@@ -670,7 +783,7 @@ class PrinterStore {
670
783
  }
671
784
 
672
785
  templateTable(text, dataKey, index, pageIndex) {
673
- // 做好保护,出错就返回 text
786
+ // 做好保护,出错就返回空串(勿回传原始 {{}},否则单元格会露出模板)
674
787
  try {
675
788
  let list = this.data._table[dataKey] || this.data._table.orders
676
789
 
@@ -679,18 +792,34 @@ class PrinterStore {
679
792
  list = _.uniqBy(list, '序号')
680
793
  }
681
794
 
795
+ const colNumber = detectColNumber(dataKey, list)
796
+ const sample = (list || []).find(item => item && !item._isEmptyData)
797
+ let row = list?.[index]
798
+ // 渲染 end 偶发超过数据长度时,用空行骨架兜底,避免 列=undefined 抛错露出 {{列.xxx_MULTI_SUFFIX}}
799
+ if (!row) {
800
+ if (!this.isAutoFilling) return ''
801
+ list = (list || []).slice()
802
+ while (list.length <= index) {
803
+ list.push(createEmptyRowSkeleton(sample, colNumber))
804
+ }
805
+ row = list[index]
806
+ }
807
+ row =
808
+ ensureRowSerialKeys(list, index, colNumber, this.fillIndex, dataKey) ||
809
+ row
810
+
682
811
  return _.template(text, {
683
812
  interpolate: /{{([\s\S]+?)}}/g
684
813
  })({
685
814
  ...this.data.common,
686
815
 
687
- [i18next.t('列')]: list[index],
816
+ [i18next.t('列')]: row,
688
817
  [i18next.t('当前页码')]: pageIndex + 1,
689
818
  [i18next.t('页码总数')]: Math.max(1, this.pages.length),
690
819
  price: price // 提供一个价格处理函数
691
820
  })
692
821
  } catch (err) {
693
- return text
822
+ return ''
694
823
  }
695
824
  }
696
825
 
@@ -763,41 +892,176 @@ class PrinterStore {
763
892
  const tr_count = Math.floor(
764
893
  this.remainPageHeight / this.computedTableCustomerRowHeight
765
894
  )
895
+ if (tr_count <= 0) return []
766
896
 
767
- const filledData = {
768
- _isEmptyData: true // 表示是填充的空白数据
769
- }
770
- _.map(tableData[0], (val, key) => {
771
- filledData[key] = ''
897
+ const sample = (tableData || []).find(item => item && !item._isEmptyData)
898
+ const colNumber = detectColNumber(
899
+ this.tableConfig?.dataKey || autoFillConfig?.dataKey,
900
+ tableData
901
+ )
902
+ return createEmptyRows(tr_count, sample, colNumber)
903
+ }
904
+
905
+ /** 空行高度只由 getNormalTableBodyHeights 虚拟追加,禁止写入 body.heights(会双计溢页) */
906
+ @action
907
+ trimTableBodyHeightsToRealRows(dataKey) {
908
+ if (!dataKey || !this.tablesInfo) return
909
+ this.config.contents.forEach((content, index) => {
910
+ if (content?.type !== 'table') return
911
+ const resolved = getDataKey(content.dataKey, content.arrange)
912
+ if (resolved !== dataKey && content.dataKey !== dataKey) return
913
+
914
+ const name = `contents.table.${index}`
915
+ const info = this.tablesInfo[name]
916
+ if (!info?.body?.heights) return
917
+
918
+ const rows = this.data._table[resolved] || this.data._table[dataKey] || []
919
+ const realCount = rows.filter(row => row && !row._isEmptyData).length
920
+ if (info.body.heights.length > realCount) {
921
+ info.body.heights = info.body.heights.slice(0, realCount)
922
+ }
772
923
  })
773
- console.log(tr_count, 'tr_count')
924
+ }
925
+
926
+ /** 多栏表缺失时(如双栏切三栏),从单栏/低栏源表现场打包 */
927
+ ensureMultiTableData(dataKey) {
928
+ const existing = this.data._table[dataKey]
929
+ if (existing?.length) return existing
930
+ if (!isMultiTable(dataKey)) return existing || null
931
+
932
+ const isVertical = /_vertical$/.test(dataKey)
933
+ const sourceKey = dataKey.replace(/_vertical$/, '')
934
+ const colNumber = getMultiNumber(sourceKey)
935
+ const singleKey = sourceKey.replace(/_multi3?/, '')
936
+
937
+ let sourceRows = this.data._table[singleKey]
938
+ let sourceColNumber = 1
939
+ if (!sourceRows?.length && /multi3/.test(sourceKey)) {
940
+ const dualKey = sourceKey.replace('multi3', 'multi')
941
+ sourceRows = this.data._table[dualKey]
942
+ sourceColNumber = 2
943
+ }
944
+ if (!sourceRows?.length) {
945
+ sourceRows = this.data._table[sourceKey]
946
+ sourceColNumber = isMultiTable(sourceKey) ? colNumber : 1
947
+ }
948
+ if (!sourceRows?.length) {
949
+ this.data._table[dataKey] = []
950
+ return []
951
+ }
952
+
953
+ const sourceItems = flattenHorizontalPackedRows(sourceRows, sourceColNumber)
954
+ const baseRowCount = sourceItems.length
955
+ ? Math.max(1, Math.ceil(sourceItems.length / colNumber))
956
+ : 0
957
+ const packed = !baseRowCount
958
+ ? []
959
+ : isVertical
960
+ ? packVerticalNewspaper(sourceItems, colNumber, baseRowCount, false)
961
+ : packHorizontalRowMajor(sourceItems, colNumber)
962
+ this.data._table[dataKey] = packed
963
+ return packed
964
+ }
774
965
 
775
- return Array(tr_count).fill(filledData)
966
+ fillTableEmptyIndex(dataKey) {
967
+ let tableData = this.data._table[dataKey]
968
+ if (!tableData && isMultiTable(dataKey)) {
969
+ tableData = this.ensureMultiTableData(dataKey)
970
+ }
971
+ if (!tableData) return
972
+ const emptyTable = tableData.filter(item => item._isEmptyData)
973
+ const table = tableData.filter(item => !item._isEmptyData)
974
+ const toFillEmptyCount =
975
+ emptyTable.length === 0 ? this.getFilledTableData(table).length : 0
976
+ const emptyCount =
977
+ emptyTable.length > 0 ? emptyTable.length : toFillEmptyCount
978
+
979
+ // 纵向多栏:源商品按对半行落位 + 追加空行;左列优先由 repack 按「数据行+空行」整段完成
980
+ if (/_vertical$/.test(dataKey)) {
981
+ const sourceKey = dataKey.replace(/_vertical$/, '')
982
+ const singleKey = sourceKey.replace(/_multi3?/, '')
983
+ const colNumber = getMultiNumber(sourceKey)
984
+ // 横向源表缺失时先生成(双栏→三栏常见)
985
+ if (!this.data._table[sourceKey]?.length) {
986
+ this.ensureMultiTableData(sourceKey)
987
+ }
988
+ const sourceRows =
989
+ this.data._table[singleKey] || this.data._table[sourceKey] || []
990
+ const sourceColNumber = isMultiTable(singleKey)
991
+ ? getMultiNumber(singleKey)
992
+ : isMultiTable(sourceKey)
993
+ ? colNumber
994
+ : 1
995
+ const sourceItems = flattenHorizontalPackedRows(
996
+ sourceRows,
997
+ sourceColNumber
998
+ )
999
+ const baseRowCount = sourceItems.length
1000
+ ? Math.max(1, Math.ceil(sourceItems.length / colNumber))
1001
+ : 0
1002
+ // 钉死总行:left-first repack 后 _isEmptyData 变少,不能用 emptyTable.length 回推
1003
+ const emptyCount = resolveVerticalEmptyCount(
1004
+ tableData.length,
1005
+ baseRowCount,
1006
+ emptyTable.length > 0
1007
+ ? emptyTable.length
1008
+ : this.getFilledTableData(table).length,
1009
+ emptyTable.length > 0
1010
+ )
1011
+ const dataRows = baseRowCount
1012
+ ? packVerticalNewspaper(sourceItems, colNumber, baseRowCount, false)
1013
+ : []
1014
+ const sample = dataRows[0] || sourceRows[0]
1015
+ const emptyRows = createEmptyRows(emptyCount, sample, colNumber)
1016
+ this.data._table[dataKey] = dataRows.concat(emptyRows)
1017
+ return
1018
+ }
1019
+
1020
+ const colNumber = detectColNumber(dataKey, tableData)
1021
+ const templates =
1022
+ emptyCount === 0
1023
+ ? []
1024
+ : emptyTable.length > 0
1025
+ ? emptyTable
1026
+ : this.getFilledTableData(table)
1027
+ const withPartial = fillPartialLastRow(table, colNumber, this.fillIndex)
1028
+ const lastKey = getMaxSerial(withPartial, colNumber) + 1
1029
+ const sample = table[0]
1030
+ const fillList = templates.map((item, index) =>
1031
+ fillEmptyRowIndex(
1032
+ {
1033
+ ...createEmptyRowSkeleton(sample, colNumber),
1034
+ ...item,
1035
+ _isEmptyData: true
1036
+ },
1037
+ index,
1038
+ lastKey,
1039
+ colNumber,
1040
+ this.fillIndex
1041
+ )
1042
+ )
1043
+ withPartial.push(...fillList)
1044
+ this.data._table[dataKey] = withPartial
776
1045
  }
777
1046
 
778
1047
  @action.bound
779
1048
  changeTableData() {
780
1049
  const { autoFillConfig } = this.config
781
1050
  if (!this.isAutoFilling) return
782
- const dataKey = autoFillConfig?.dataKey
783
- const table = this.data._table[dataKey]?.filter(item => !item._isEmptyData)
784
- const emptyTable = this.data._table[dataKey]?.filter(
785
- item => item._isEmptyData
786
- )
787
- const lastKey = _.findLast(table, item => item?.序号)?.序号 + 1
788
- const fillList = _.map(
789
- emptyTable?.length === 0 ? this.getFilledTableData(table) : emptyTable,
790
- (item, index) => {
791
- if (this.fillIndex) {
792
- return { ...item, 序号: index + lastKey }
793
- } else {
794
- return { ...item, 序号: '' }
795
- }
1051
+ const dataKey = this.tableConfig?.dataKey || autoFillConfig?.dataKey
1052
+ if (!dataKey) return
1053
+ this.fillTableEmptyIndex(dataKey)
1054
+ // 测高只含商品行;空行高度交给 getNormalTableBodyHeights,避免写入 heights 双计
1055
+ this.trimTableBodyHeightsToRealRows(dataKey)
1056
+ if (isMultiTable(dataKey)) {
1057
+ const verticalKey = getDataKey(dataKey, 'vertical')
1058
+ if (!this.data._table[verticalKey]) {
1059
+ this.data._table[verticalKey] = []
796
1060
  }
797
- )
798
- console.log(fillList, 'fillList')
799
- table.push(...fillList)
800
- this.data._table[dataKey] = table
1061
+ this.fillTableEmptyIndex(verticalKey)
1062
+ this.trimTableBodyHeightsToRealRows(verticalKey)
1063
+ }
1064
+ this.repackVerticalMultiByPages()
801
1065
  }
802
1066
  }
803
1067
 
@@ -360,15 +360,11 @@ class Table extends React.Component {
360
360
  }
361
361
 
362
362
  // 如果项为空对象展现一个占满一行的td
363
- const isItemNone = !_.keys(tableData[i]).length
363
+ const isItemNone = !_.keys(tableData[i] || {}).length
364
364
  return (
365
365
  <tr style={{ height: `${customerRowHeight}px` }} key={i}>
366
- {isItemNone ? (
367
- !printerStore?.isAutoFilling ? (
368
- <td colSpan='99' />
369
- ) : (
370
- <></>
371
- )
366
+ {isItemNone && !printerStore?.isAutoFilling ? (
367
+ <td colSpan='99' />
372
368
  ) : (
373
369
  _.map(columns, (col, j) => {
374
370
  const rowSpan = printerStore.templateTableRowSpan(
package/src/util.js CHANGED
@@ -2,19 +2,23 @@ import _ from 'lodash'
2
2
  import Big from 'big.js'
3
3
 
4
4
  function getHeight(el) {
5
- const styles = window.getComputedStyle(el)
5
+ // 打印渲染在 iframe 内时,必须用元素所属 document 的 window,
6
+ // 否则 parent window.getComputedStyle 可能得到错误值、测高为 0
7
+ const view = (el.ownerDocument && el.ownerDocument.defaultView) || window
8
+ const styles = view.getComputedStyle(el)
6
9
  const height = el.offsetHeight
7
- const borderTopWidth = parseFloat(styles.borderTopWidth)
8
- const borderBottomWidth = parseFloat(styles.borderBottomWidth)
9
- const paddingTop = parseFloat(styles.paddingTop)
10
- const paddingBottom = parseFloat(styles.paddingBottom)
10
+ const borderTopWidth = parseFloat(styles.borderTopWidth) || 0
11
+ const borderBottomWidth = parseFloat(styles.borderBottomWidth) || 0
12
+ const paddingTop = parseFloat(styles.paddingTop) || 0
13
+ const paddingBottom = parseFloat(styles.paddingBottom) || 0
11
14
  return (
12
15
  height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom
13
16
  )
14
17
  }
15
18
 
16
19
  function getWidth(el) {
17
- const styles = window.getComputedStyle(el)
20
+ const view = (el.ownerDocument && el.ownerDocument.defaultView) || window
21
+ const styles = view.getComputedStyle(el)
18
22
  // 取width保留小数点用 getBoundClientRect
19
23
  const style = el.getBoundingClientRect()
20
24
  const width = Math.round(style.width * 100) / 100 // 保留两位小数并四舍五入
@@ -82,13 +86,12 @@ function exchange(arr, target, source) {
82
86
  return arr
83
87
  }
84
88
 
85
- let timer
86
-
87
89
  function afterImgAndSvgLoaded(callback, $printer) {
88
90
  const $imgList = $printer.querySelectorAll('img')
89
91
  const $svgList = $printer.querySelectorAll('svg')
90
92
 
91
- clearTimeout(timer)
93
+ // timer 为函数内局部变量:computePages(导出测高)与 doPrint(打印)可能并发调用本函数,
94
+ // 若共享模块级 timer,后调用方会 clearTimeout 掉前一方的轮询,导致其 callback 永不执行
92
95
 
93
96
  const everyThingIsOk =
94
97
  _.every($imgList, img => img.complete) &&
@@ -96,7 +99,7 @@ function afterImgAndSvgLoaded(callback, $printer) {
96
99
  if (everyThingIsOk) {
97
100
  callback()
98
101
  } else {
99
- timer = setTimeout(afterImgAndSvgLoaded.bind(this, callback, $printer), 300)
102
+ setTimeout(afterImgAndSvgLoaded.bind(this, callback, $printer), 300)
100
103
  }
101
104
  }
102
105