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