gm-printer 10.41.8 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-printer",
3
- "version": "10.41.8",
3
+ "version": "10.42.0",
4
4
  "description": "diy printer",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -24,6 +24,9 @@ class EditorCutomizedConfig extends React.Component {
24
24
  handleFillIndex = value => {
25
25
  const { editStore } = this.props
26
26
  editStore.setFillIndex(value)
27
+ if (editStore.isAutoFilling) {
28
+ editStore.handleChangeTableData(true)
29
+ }
27
30
  }
28
31
 
29
32
  handleTaxFreeProductRateDisplay = e => {
@@ -2,7 +2,15 @@ import i18next from '../../locales'
2
2
  import { action, computed, observable, set, toJS } from 'mobx'
3
3
  import { pageTypeMap } from '../config'
4
4
  import _ from 'lodash'
5
- import { dispatchMsg, getBlockName, exchange, getColSpanLength } from '../util'
5
+ import {
6
+ dispatchMsg,
7
+ getBlockName,
8
+ exchange,
9
+ getColSpanLength,
10
+ isMultiTable,
11
+ getDataKey,
12
+ getMultiNumber
13
+ } from '../util'
6
14
  import { accountRadioList } from './util'
7
15
  import {
8
16
  buildDataKey,
@@ -12,6 +20,18 @@ import {
12
20
  } from './data_key_util'
13
21
  import React from 'react'
14
22
  import { createDiySummaryStoreMethods } from './diy_summary_helper'
23
+ import {
24
+ detectColNumber,
25
+ fillEmptyRowIndex,
26
+ fillPartialLastRow,
27
+ getMaxSerial,
28
+ flattenHorizontalPackedRows,
29
+ packVerticalNewspaper,
30
+ packHorizontalRowMajor,
31
+ createEmptyRows,
32
+ createEmptyRowSkeleton,
33
+ resolveVerticalEmptyCount
34
+ } from '../printer/multi_column'
15
35
 
16
36
  class EditorStore {
17
37
  @observable
@@ -207,6 +227,7 @@ class EditorStore {
207
227
  vv.columns.length +
208
228
  vv.className +
209
229
  vv.dataKey +
230
+ (vv.arrange || 'lateral') +
210
231
  vv.subtotal.show +
211
232
  vv.summaryConfig?.pageSummaryShow +
212
233
  vv.customerRowHeight
@@ -246,14 +267,54 @@ class EditorStore {
246
267
  tr_count = Math.floor(
247
268
  this.remainPageHeihgt / this.computedTableCustomerRowHeight
248
269
  )
270
+ if (tr_count <= 0) return []
249
271
 
250
- const filledData = {
251
- _isEmptyData: true // 表示是填充的空白数据
272
+ const sample = (tableData || []).find(item => item && !item._isEmptyData)
273
+ const dataKey =
274
+ this.computedTableSpecialConfig?.dataKey || autoFillConfig?.dataKey
275
+ const colNumber = detectColNumber(dataKey, tableData)
276
+ return createEmptyRows(tr_count, sample, colNumber)
277
+ }
278
+
279
+ /** 多栏表缺失时(如双栏切三栏),从单栏/双栏源表现场打包到 mockData */
280
+ @action
281
+ ensureMockMultiTable(dataKey) {
282
+ if (!dataKey || !isMultiTable(dataKey)) return
283
+ if (!this.mockData._table) {
284
+ this.mockData._table = {}
285
+ }
286
+ if (this.mockData._table[dataKey]?.length) return
287
+
288
+ const isVertical = /_vertical$/.test(dataKey)
289
+ const sourceKey = dataKey.replace(/_vertical$/, '')
290
+ const colNumber = getMultiNumber(sourceKey)
291
+ const singleKey = sourceKey.replace(/_multi3?/, '')
292
+
293
+ let sourceRows = this.mockData._table[singleKey]
294
+ let sourceColNumber = 1
295
+ if (!sourceRows?.length && /multi3/.test(sourceKey)) {
296
+ const dualKey = sourceKey.replace('multi3', 'multi')
297
+ sourceRows = this.mockData._table[dualKey]
298
+ sourceColNumber = 2
299
+ }
300
+ if (!sourceRows?.length) {
301
+ sourceRows = this.mockData._table[sourceKey]
302
+ sourceColNumber = isMultiTable(sourceKey) ? colNumber : 1
303
+ }
304
+ if (!sourceRows?.length) {
305
+ this.mockData._table[dataKey] = []
306
+ return
252
307
  }
253
- _.map(tableData[0], (val, key) => {
254
- filledData[key] = ''
255
- })
256
- return Array(tr_count).fill(filledData)
308
+
309
+ const sourceItems = flattenHorizontalPackedRows(sourceRows, sourceColNumber)
310
+ const baseRowCount = sourceItems.length
311
+ ? Math.max(1, Math.ceil(sourceItems.length / colNumber))
312
+ : 0
313
+ this.mockData._table[dataKey] = !baseRowCount
314
+ ? []
315
+ : isVertical
316
+ ? packVerticalNewspaper(sourceItems, colNumber, baseRowCount, false)
317
+ : packHorizontalRowMajor(sourceItems, colNumber)
257
318
  }
258
319
 
259
320
  @action.bound
@@ -262,9 +323,6 @@ class EditorStore {
262
323
  if (!this.selectedRegion && !autoFillConfig?.checked) return
263
324
  const dataKey =
264
325
  this.computedTableSpecialConfig?.dataKey || autoFillConfig?.dataKey
265
- // 每次都更新一下?
266
- const table = this.mockData._table[dataKey]
267
- // 赋值一下
268
326
  this.setAutoFillingConfig(isAutoFilling)
269
327
  this.setFillIndex(autoFillConfig?.fillIndex || false)
270
328
  set(this.config, {
@@ -277,24 +335,112 @@ class EditorStore {
277
335
  }
278
336
  })
279
337
 
280
- const hasHadEmptyData = _.some(table, data => data?._isEmptyData)
338
+ if (dataKey && isMultiTable(dataKey)) {
339
+ this.ensureMockMultiTable(dataKey)
340
+ this.ensureMockMultiTable(getDataKey(dataKey, 'vertical'))
341
+ }
281
342
 
282
- // 开关打开,且之前数组不包含空数据,再进行填充
343
+ const syncEmptyRows = key => {
344
+ if (!this.mockData._table?.[key]) return
345
+ const list = this.mockData._table[key]
346
+ const hasHadEmptyData = _.some(list, data => data?._isEmptyData)
347
+ if (isAutoFilling && !hasHadEmptyData) {
348
+ list.push(...this.getFilledTableData(list))
349
+ }
350
+ if (!isAutoFilling) {
351
+ this.clearExtraTableData(key)
352
+ return
353
+ }
354
+ this.mockData._table[key] = list
355
+ }
356
+
357
+ const numberEmptyRows = key => {
358
+ const list = this.mockData._table?.[key]
359
+ if (!list) return
360
+ const table = list.filter(item => !item._isEmptyData)
361
+ const emptyTable = list.filter(item => item._isEmptyData)
362
+ // 纵向表:对半数据行 + 空行占位;左列优先交给 Printer.repack
363
+ if (/_vertical$/.test(key)) {
364
+ const sourceKey = key.replace(/_vertical$/, '')
365
+ const singleKey = sourceKey.replace(/_multi3?/, '')
366
+ const colNumber = getMultiNumber(sourceKey)
367
+ const sourceRows =
368
+ this.mockData._table[singleKey] ||
369
+ this.mockData._table[sourceKey] ||
370
+ []
371
+ const sourceColNumber = isMultiTable(singleKey)
372
+ ? getMultiNumber(singleKey)
373
+ : isMultiTable(sourceKey)
374
+ ? colNumber
375
+ : 1
376
+ const sourceItems = flattenHorizontalPackedRows(
377
+ sourceRows,
378
+ sourceColNumber
379
+ )
380
+ const baseRowCount = sourceItems.length
381
+ ? Math.max(1, Math.ceil(sourceItems.length / colNumber))
382
+ : 0
383
+ // 钉死总行:repack 后 _isEmptyData 会变少,不能再用 emptyTable.length
384
+ const emptyCount = resolveVerticalEmptyCount(
385
+ list.length,
386
+ baseRowCount,
387
+ emptyTable.length > 0
388
+ ? emptyTable.length
389
+ : this.getFilledTableData(list).length,
390
+ emptyTable.length > 0
391
+ )
392
+ const dataRows = baseRowCount
393
+ ? packVerticalNewspaper(sourceItems, colNumber, baseRowCount, false)
394
+ : []
395
+ const sample = dataRows[0] || sourceRows[0]
396
+ this.mockData._table[key] = dataRows.concat(
397
+ createEmptyRows(emptyCount, sample, colNumber)
398
+ )
399
+ return
400
+ }
401
+ const colNumber = detectColNumber(key, list)
402
+ const withPartial = fillPartialLastRow(table, colNumber, this.fillIndex)
403
+ const lastKey = getMaxSerial(withPartial, colNumber) + 1
404
+ const sample = table[0]
405
+ this.mockData._table[key] = withPartial.concat(
406
+ emptyTable.map((item, index) =>
407
+ fillEmptyRowIndex(
408
+ {
409
+ ...createEmptyRowSkeleton(sample, colNumber),
410
+ ...item,
411
+ _isEmptyData: true
412
+ },
413
+ index,
414
+ lastKey,
415
+ colNumber,
416
+ this.fillIndex
417
+ )
418
+ )
419
+ )
420
+ }
283
421
 
284
- if (isAutoFilling && !hasHadEmptyData) {
285
- table.push(...this.getFilledTableData(table))
422
+ syncEmptyRows(dataKey)
423
+ if (dataKey && isMultiTable(dataKey)) {
424
+ const verticalKey = getDataKey(dataKey, 'vertical')
425
+ if (!this.mockData._table[verticalKey]) {
426
+ this.mockData._table[verticalKey] = []
427
+ }
428
+ syncEmptyRows(verticalKey)
286
429
  }
287
- if (!isAutoFilling) {
288
- this.clearExtraTableData(dataKey)
289
- return
430
+ if (isAutoFilling) {
431
+ numberEmptyRows(dataKey)
432
+ if (dataKey && isMultiTable(dataKey)) {
433
+ numberEmptyRows(getDataKey(dataKey, 'vertical'))
434
+ }
290
435
  }
291
- this.mockData._table[dataKey] = table
292
436
  }
293
437
 
294
438
  @action
295
439
  clearExtraTableData(dataKey) {
296
- const newTable = this.mockData._table[dataKey].filter(x => !x._isEmptyData)
297
- this.mockData._table[dataKey] = newTable
440
+ if (!this.mockData._table?.[dataKey]) return
441
+ this.mockData._table[dataKey] = this.mockData._table[dataKey].filter(
442
+ x => !x._isEmptyData
443
+ )
298
444
  }
299
445
 
300
446
  @action
@@ -549,12 +695,39 @@ class EditorStore {
549
695
  for (const [key, table] of Object.entries(tableData)) {
550
696
  tableData[key] = table.filter(x => !x._isEmptyData)
551
697
  }
552
- // this.setAutoFillingConfig(false)
698
+ // 纵向 left-fill 后商品行数 > 对半行且无 _isEmptyData,只滤空行会留下偏多行,
699
+ // 重挂载测高/remain 偏小,多栏切换后空行补不回来。收回到对半商品行。
700
+ Object.keys(tableData).forEach(key => {
701
+ if (!/_vertical$/.test(key) || !isMultiTable(key)) return
702
+ this.collapseVerticalMultiToBaseRows(key)
703
+ })
553
704
  set(this.mockData, {
554
705
  _table: tableData
555
706
  })
556
707
  }
557
708
 
709
+ /** 纵向多栏收到「对半」商品行,去掉 left-fill 撑开的多余行 */
710
+ @action
711
+ collapseVerticalMultiToBaseRows(key) {
712
+ const sourceKey = key.replace(/_vertical$/, '')
713
+ const singleKey = sourceKey.replace(/_multi3?/, '')
714
+ const colNumber = getMultiNumber(sourceKey)
715
+ const sourceRows =
716
+ this.mockData._table[singleKey] || this.mockData._table[sourceKey] || []
717
+ const sourceColNumber = isMultiTable(singleKey)
718
+ ? getMultiNumber(singleKey)
719
+ : isMultiTable(sourceKey)
720
+ ? colNumber
721
+ : 1
722
+ const sourceItems = flattenHorizontalPackedRows(sourceRows, sourceColNumber)
723
+ const baseRowCount = sourceItems.length
724
+ ? Math.max(1, Math.ceil(sourceItems.length / colNumber))
725
+ : 0
726
+ this.mockData._table[key] = baseRowCount
727
+ ? packVerticalNewspaper(sourceItems, colNumber, baseRowCount, false)
728
+ : []
729
+ }
730
+
558
731
  @action
559
732
  addConfigBlock(name, type, pos = {}, link = '') {
560
733
  let blocks
@@ -1023,6 +1196,13 @@ class EditorStore {
1023
1196
 
1024
1197
  this.config.contents[arr[2]].dataKey = newDataKey.join('_')
1025
1198
 
1199
+ // 双栏↔三栏等切换时,mock 可能缺对应 _table,现场兜底生成,避免整表空白
1200
+ const joinedKey = newDataKey.join('_')
1201
+ if (isMultiTable(joinedKey)) {
1202
+ this.ensureMockMultiTable(joinedKey)
1203
+ this.ensureMockMultiTable(getDataKey(joinedKey, 'vertical'))
1204
+ }
1205
+
1026
1206
  // 三级分类相关 token(商品三级分类/三级分类小计)是纯分组、标题能力,
1027
1207
  // 与每页/整单合计可共存,切换时不再强制关闭合计开关;
1028
1208
  // 其余 token(多栏/一级分类/标签等)保持历史行为:切换时强制关闭
@@ -1044,7 +1224,7 @@ class EditorStore {
1044
1224
  autoFillConfig: {
1045
1225
  ...(this.config.autoFillConfig || {}),
1046
1226
  region: this.selectedRegion || this.config.autoFillConfig?.region,
1047
- dataKey: newDataKey.join('_'),
1227
+ dataKey: joinedKey,
1048
1228
  checked: true,
1049
1229
  fillIndex: this.fillIndex
1050
1230
  }
@@ -1487,10 +1667,26 @@ class EditorStore {
1487
1667
  if (this.selectedRegion) {
1488
1668
  const arr = this.selectedRegion.split('.')
1489
1669
  if (arr.includes('table')) {
1670
+ const table = this.config.contents[arr[2]]
1490
1671
  this.config.contents[arr[2]] = {
1491
- ...this.config.contents[arr[2]],
1672
+ ...table,
1492
1673
  arrange: val
1493
1674
  }
1675
+ // 只改排列并清掉旧空行;保持填充开关,交给 Printer 重挂载后
1676
+ // 「先补空 → 再重新分页」恢复空行(勿在此处用旧 remainHeight 立刻补)
1677
+ if (this.isAutoFilling || this.config?.autoFillConfig?.checked) {
1678
+ this.clearAllTableEmptyData()
1679
+ this.setAutoFillingConfig(true)
1680
+ set(this.config, {
1681
+ autoFillConfig: {
1682
+ ...(this.config.autoFillConfig || {}),
1683
+ region: this.selectedRegion,
1684
+ dataKey: table.dataKey,
1685
+ checked: true,
1686
+ fillIndex: this.fillIndex
1687
+ }
1688
+ })
1689
+ }
1494
1690
  }
1495
1691
  }
1496
1692
  }
package/src/index.js CHANGED
@@ -20,7 +20,8 @@ import {
20
20
  getCSS,
21
21
  getPrintContainerHTML,
22
22
  doBatchFinancePrint,
23
- setPrintStyle
23
+ setPrintStyle,
24
+ computePages
24
25
  } from './printer'
25
26
  import { MULTI_SUFFIX, MULTI_SUFFIX3 } from './config'
26
27
 
@@ -49,5 +50,6 @@ export {
49
50
  MULTI_SUFFIX3,
50
51
  getPrintContainerHTML,
51
52
  doBatchFinancePrint,
52
- setPrintStyle
53
+ setPrintStyle,
54
+ computePages
53
55
  }
@@ -0,0 +1,122 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom'
3
+ import _ from 'lodash'
4
+ import { toJS } from 'mobx'
5
+ import Printer from './printer'
6
+ import getCSS from './get_css'
7
+ import { afterImgAndSvgLoaded } from '../util'
8
+ import { LONG_PRINT } from '../config'
9
+
10
+ // 与 do_print.js 的容器隔离,使用独立 iframe,避免和打印任务互相 unmount 对方内容
11
+ const printerId = '_gm-printer-compute-pages_' + Math.random()
12
+ let $printer = null
13
+
14
+ /**
15
+ * 初始化隐藏渲染容器
16
+ * 复制自 do_print.js 的 init({ isTest: true }) 分支(iframe 创建 + getCSS 注入),
17
+ * 差异仅 id 前缀与「永远隐藏」。⚠️ get_css / 容器初始化逻辑变更时两处需同步,
18
+ * 否则测量环境与打印环境漂移,分页边界不再一致
19
+ */
20
+ function init() {
21
+ if (!$printer) {
22
+ $printer = window.document.createElement('iframe')
23
+ $printer.id = printerId
24
+ $printer.style.position = 'fixed'
25
+ $printer.style.top = '0'
26
+ $printer.style.width = '100%'
27
+ // 隐藏起来,用户不可见
28
+ $printer.style.left = '-2000px'
29
+ window.document.body.appendChild($printer)
30
+
31
+ const idocument = $printer.contentDocument
32
+ idocument.open()
33
+ idocument.write('<!DOCTYPE html><html><head></head><body></body></html>')
34
+ idocument.close()
35
+
36
+ const doc = $printer.contentWindow.document
37
+
38
+ const style = doc.createElement('style')
39
+ style.appendChild(doc.createTextNode(getCSS()))
40
+ doc.head.appendChild(style)
41
+
42
+ const div = doc.createElement('div')
43
+ div.id = 'appContainer'
44
+
45
+ doc.body.appendChild(div)
46
+ }
47
+ }
48
+
49
+ /**
50
+ * 按打印分页逻辑计算分页结果(不触发打印)
51
+ *
52
+ * 场景:导出功能需要拿到与打印完全一致的分页边界
53
+ *
54
+ * 不支持的模板类型(返回空 pages,调用方需自行降级):
55
+ * - batchPrintConfig === 2(合并打印):componentDidMount 不跑 computedPages,pages 恒空
56
+ * - page.type === LONG_PRINT(长条打印):走 renderLongPage 全量渲染,pages 不对应真实布局
57
+ *
58
+ * @param {Object} options
59
+ * @param {Object} options.data 打印数据(结构同 doPrint 的 data,含 _table)
60
+ * @param {Object} options.config 模板配置(结构同 doPrint 的 config)
61
+ * @param {boolean} [options.isSomeSubtotalTr] 每页合计是否展示多个字段(对齐 doPrint 第三参,
62
+ * 影响合计行渲染与行高,进而影响分页边界)
63
+ * @returns {Promise<{pages: Array, remainPageHeight: number, tableCustomerRowHeight: number}>}
64
+ * pages: [[{type, index, begin, end}, ...], ...] 对 data._table[contents[i].dataKey] 行号的切片
65
+ * remainPageHeight: 最后一页剩余高度(供行数填充换算)
66
+ * tableCustomerRowHeight: 表格行高(供行数填充换算)
67
+ */
68
+ function computePages({ data, config, isSomeSubtotalTr }) {
69
+ // 不支持的模板类型提前告警(不阻断,返回结果交由调用方降级处理)
70
+ if (config.batchPrintConfig === 2 || config.page?.type === LONG_PRINT) {
71
+ console.warn(
72
+ '[gm-printer] computePages: 当前模板为合并打印或长条打印,computedPages 不生效,pages 将为空'
73
+ )
74
+ }
75
+
76
+ return new window.Promise(resolve => {
77
+ init()
78
+
79
+ const $app = $printer.contentWindow.document.getElementById('appContainer')
80
+ ReactDOM.unmountComponentAtNode($app)
81
+
82
+ // computedPages 处理采购明细拆行时会原地 splice data._table,
83
+ // 深拷贝一份,避免污染调用方数据
84
+ const clonedData = _.cloneDeep(data)
85
+ // computedPages 会往 config 顶层写 isSave(store.js),浅拷贝隔离开调用方的
86
+ // template 对象(stationv2 导出时同一 template 跨多个 sheet 复用)
87
+ const clonedConfig = { ...config }
88
+
89
+ // 依赖 WithStorePrinter 实例字段 printerStore(printer.js)读取分页结果,
90
+ // Printer 组件重构(store 挂载方式变化)时此处需同步
91
+ let printerRef = null
92
+ ReactDOM.render(
93
+ <Printer
94
+ ref={ref => {
95
+ printerRef = ref
96
+ }}
97
+ isSomeSubtotalTr={isSomeSubtotalTr}
98
+ config={clonedConfig}
99
+ data={clonedData}
100
+ onReady={() => {
101
+ // onReady 触发时 componentDidMount 已同步执行完 computedPages(),pages 就绪
102
+ // 模板含图片时等图片加载完成,避免图片撑高导致测量偏差
103
+ afterImgAndSvgLoaded(() => {
104
+ const store = printerRef.printerStore
105
+ const result = {
106
+ pages: toJS(store.pages),
107
+ remainPageHeight: store.remainPageHeight,
108
+ tableCustomerRowHeight: store.computedTableCustomerRowHeight
109
+ }
110
+
111
+ // 读取完成后卸载,避免 DOM 累积
112
+ ReactDOM.unmountComponentAtNode($app)
113
+ resolve(result)
114
+ }, $app)
115
+ }}
116
+ />,
117
+ $app
118
+ )
119
+ })
120
+ }
121
+
122
+ export { computePages }
@@ -7,6 +7,7 @@ import {
7
7
  doBatchFinancePrint,
8
8
  setPrintStyle
9
9
  } from './do_print'
10
+ import { computePages } from './compute_pages'
10
11
  import getCSS from './get_css'
11
12
 
12
13
  export {
@@ -17,5 +18,6 @@ export {
17
18
  getCSS,
18
19
  getPrintContainerHTML,
19
20
  doBatchFinancePrint,
20
- setPrintStyle
21
+ setPrintStyle,
22
+ computePages
21
23
  }