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.
@@ -0,0 +1,461 @@
1
+ import { MULTI_SUFFIX, MULTI_SUFFIX3 } from '../config'
2
+ import { getMultiNumber, isMultiTable } from '../util'
3
+
4
+ export const INDEX_KEY = '序号'
5
+
6
+ const SPECIAL_ROW_KEYS = [
7
+ '_special',
8
+ '_diyCategorySubtotal',
9
+ '_diyTagSubtotal',
10
+ '_customTr'
11
+ ]
12
+
13
+ export const getColSuffix = colIndex => {
14
+ if (colIndex <= 0) return ''
15
+ if (colIndex === 1) return MULTI_SUFFIX
16
+ return MULTI_SUFFIX3
17
+ }
18
+
19
+ export const getSerialKeys = colNumber =>
20
+ Array.from({ length: colNumber }, (_, c) => INDEX_KEY + getColSuffix(c))
21
+
22
+ /** 栏数:dataKey 的 multi/multi3,并用行上是否已有 _MULTI_SUFFIX 字段兜底 */
23
+ export const detectColNumber = (dataKey, rows) => {
24
+ let n = isMultiTable(dataKey) ? getMultiNumber(dataKey) : 1
25
+ const sample = (rows || []).find(
26
+ row => row && !row._isEmptyData && !isSpecialTableRow(row)
27
+ )
28
+ if (sample) {
29
+ const keys = Object.keys(sample)
30
+ if (keys.some(key => key.endsWith(MULTI_SUFFIX3))) {
31
+ n = Math.max(n, 3)
32
+ } else if (keys.some(key => key.endsWith(MULTI_SUFFIX))) {
33
+ n = Math.max(n, 2)
34
+ }
35
+ }
36
+ return n
37
+ }
38
+
39
+ export const isSpecialTableRow = row =>
40
+ !!(row && SPECIAL_ROW_KEYS.some(key => row[key]))
41
+
42
+ export const isFilledIndexValue = value => value !== '' && value != null
43
+
44
+ export const toSerialNumber = value => {
45
+ if (!isFilledIndexValue(value)) return null
46
+ const n = Number(value)
47
+ return Number.isFinite(n) ? n : null
48
+ }
49
+
50
+ const splitKeyCol = key => {
51
+ if (key.endsWith(MULTI_SUFFIX3)) {
52
+ return { col: 2, base: key.slice(0, -MULTI_SUFFIX3.length) }
53
+ }
54
+ if (key.endsWith(MULTI_SUFFIX)) {
55
+ return { col: 1, base: key.slice(0, -MULTI_SUFFIX.length) }
56
+ }
57
+ return { col: 0, base: key }
58
+ }
59
+
60
+ /** 把多栏打包行拆成各列的原始行数据 */
61
+ export const splitPackedRow = (row, colNumber) => {
62
+ const cols = Array.from({ length: colNumber }, () => ({}))
63
+ Object.keys(row || {}).forEach(key => {
64
+ if (key === '_isEmptyData') return
65
+ const { col, base } = splitKeyCol(key)
66
+ if (col < colNumber) {
67
+ cols[col][base] = row[key]
68
+ }
69
+ })
70
+ return cols
71
+ }
72
+
73
+ const isRealItem = item => isFilledIndexValue(item?.[INDEX_KEY])
74
+
75
+ /** 去掉多栏后缀字段,避免旧右列残留被 assign 进新行 */
76
+ export const stripMultiSuffixFields = item => {
77
+ if (!item || typeof item !== 'object') return item
78
+ const next = {}
79
+ Object.keys(item).forEach(key => {
80
+ if (key === '_isEmptyData') return
81
+ if (key.endsWith(MULTI_SUFFIX) || key.endsWith(MULTI_SUFFIX3)) return
82
+ next[key] = item[key]
83
+ })
84
+ return next
85
+ }
86
+
87
+ /** 横向多栏:每行从左到右拆出商品,得到原始顺序 */
88
+ export const flattenHorizontalPackedRows = (rows, colNumber) => {
89
+ const items = []
90
+ const cols = Math.max(colNumber || 1, 1)
91
+ ;(rows || []).forEach(row => {
92
+ if (!row || row._isEmptyData || isSpecialTableRow(row)) return
93
+ if (cols <= 1) {
94
+ items.push(stripMultiSuffixFields(row))
95
+ return
96
+ }
97
+ splitPackedRow(row, cols).forEach(item => {
98
+ if (isRealItem(item)) items.push(stripMultiSuffixFields(item))
99
+ })
100
+ })
101
+ return items
102
+ }
103
+
104
+ /** 纵向整单打包:左列全部 + 右列全部,还原商品原始顺序 */
105
+ export const flattenVerticalSegment = (rows, colNumber) => {
106
+ const columns = Array.from({ length: colNumber }, () => [])
107
+ rows.forEach(row => {
108
+ if (isSpecialTableRow(row)) return
109
+ splitPackedRow(row, colNumber).forEach((item, col) => {
110
+ if (isRealItem(item)) {
111
+ columns[col].push(stripMultiSuffixFields(item))
112
+ }
113
+ })
114
+ })
115
+ return columns.reduce((list, col) => list.concat(col), [])
116
+ }
117
+
118
+ const assignItemToRow = (row, item, suffix) => {
119
+ const clean = stripMultiSuffixFields(item) || {}
120
+ Object.keys(clean).forEach(key => {
121
+ row[key + suffix] = clean[key]
122
+ })
123
+ }
124
+
125
+ /**
126
+ * 按「先填满当前页左列,再填右列」打包。
127
+ * row r 的第 c 列对应 items[c * rowCount + r]
128
+ * 空单元格补序号也按列优先(先下后右),与商品落位一致。
129
+ * @param {object} [options]
130
+ * @param {number} [options.serialStart] 填充序号的起始值(含)
131
+ */
132
+ export const packVerticalNewspaper = (
133
+ items,
134
+ colNumber,
135
+ rowCount,
136
+ fillIndex = false,
137
+ options = {}
138
+ ) => {
139
+ const { serialStart } = options
140
+ const rows = Array.from({ length: rowCount }, () => ({}))
141
+ let maxSerial = 0
142
+ ;(items || []).forEach(item => {
143
+ const n = toSerialNumber(item?.[INDEX_KEY])
144
+ if (n != null && n > maxSerial) maxSerial = n
145
+ })
146
+ if (serialStart != null && serialStart - 1 > maxSerial) {
147
+ maxSerial = serialStart - 1
148
+ }
149
+
150
+ for (let c = 0; c < colNumber; c++) {
151
+ for (let r = 0; r < rowCount; r++) {
152
+ const item = (items || [])[c * rowCount + r]
153
+ if (!isRealItem(item)) continue
154
+ assignItemToRow(rows[r], item, getColSuffix(c))
155
+ }
156
+ }
157
+
158
+ if (fillIndex) {
159
+ let nextSerial = maxSerial
160
+ for (let c = 0; c < colNumber; c++) {
161
+ for (let r = 0; r < rowCount; r++) {
162
+ const key = INDEX_KEY + getColSuffix(c)
163
+ if (isFilledIndexValue(rows[r][key])) {
164
+ const n = toSerialNumber(rows[r][key])
165
+ if (n != null && n > nextSerial) nextSerial = n
166
+ continue
167
+ }
168
+ rows[r][key] = ++nextSerial
169
+ }
170
+ }
171
+ } else {
172
+ for (let c = 0; c < colNumber; c++) {
173
+ for (let r = 0; r < rowCount; r++) {
174
+ const key = INDEX_KEY + getColSuffix(c)
175
+ if (!isFilledIndexValue(rows[r][key])) {
176
+ rows[r][key] = ''
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ rows.forEach((row, r) => {
183
+ let hasRealItem = false
184
+ for (let c = 0; c < colNumber; c++) {
185
+ if (isRealItem((items || [])[c * rowCount + r])) {
186
+ hasRealItem = true
187
+ break
188
+ }
189
+ }
190
+ if (!hasRealItem) {
191
+ row._isEmptyData = true
192
+ }
193
+ })
194
+ return rows
195
+ }
196
+
197
+ /**
198
+ * 横向多栏:每行从左到右落位(row r 的第 c 列 ← items[r * colNumber + c])
199
+ * 用于缺省 multi/multi3 表时从单栏商品生成打包数据。
200
+ */
201
+ export const packHorizontalRowMajor = (items, colNumber) => {
202
+ const cols = Math.max(colNumber || 1, 1)
203
+ const list = items || []
204
+ const rowCount = list.length ? Math.ceil(list.length / cols) : 0
205
+ const rows = Array.from({ length: rowCount }, () => ({}))
206
+ for (let r = 0; r < rowCount; r++) {
207
+ for (let c = 0; c < cols; c++) {
208
+ const item = list[r * cols + c]
209
+ if (!isRealItem(item)) continue
210
+ assignItemToRow(rows[r], item, getColSuffix(c))
211
+ }
212
+ }
213
+ return rows
214
+ }
215
+
216
+ export const getMaxSerial = (rows, colNumber) => {
217
+ let max = 0
218
+ const keys = getSerialKeys(colNumber)
219
+ ;(rows || []).forEach(row => {
220
+ keys.forEach(key => {
221
+ const n = toSerialNumber(row?.[key])
222
+ if (n != null && n > max) max = n
223
+ })
224
+ })
225
+ return max
226
+ }
227
+
228
+ /** 空白行补齐每一栏的序号 */
229
+ export const fillEmptyRowIndex = (
230
+ item,
231
+ index,
232
+ lastKey,
233
+ colNumber,
234
+ fillIndex
235
+ ) => {
236
+ const next = { ...item }
237
+ getSerialKeys(colNumber).forEach((key, col) => {
238
+ next[key] = fillIndex ? lastKey + index * colNumber + col : ''
239
+ })
240
+ return next
241
+ }
242
+
243
+ /**
244
+ * 按样板行生成空行骨架(拷贝字段为 ''),避免多栏模板读 undefined 抛错。
245
+ * 并预留各栏序号字段。
246
+ */
247
+ export const createEmptyRowSkeleton = (sampleRow, colNumber = 1) => {
248
+ const skeleton = { _isEmptyData: true }
249
+ if (sampleRow && typeof sampleRow === 'object') {
250
+ Object.keys(sampleRow).forEach(key => {
251
+ if (key === '_isEmptyData') return
252
+ skeleton[key] = ''
253
+ })
254
+ }
255
+ const cols = Math.max(colNumber || 1, 1)
256
+ getSerialKeys(cols).forEach(key => {
257
+ if (skeleton[key] === undefined) skeleton[key] = ''
258
+ })
259
+ return skeleton
260
+ }
261
+
262
+ export const createEmptyRows = (count, sampleRow, colNumber = 1) => {
263
+ if (count <= 0) return []
264
+ const skeleton = createEmptyRowSkeleton(sampleRow, colNumber)
265
+ return Array.from({ length: count }, () => ({ ...skeleton }))
266
+ }
267
+
268
+ /**
269
+ * 纵向多栏空行数:
270
+ * - 仍有 _isEmptyData 时:用「总行 - 对半商品行」钉死(开关填充序号,避免 repack 吃空行)
271
+ * - 已无空行标记时:走 fallback(remain/行高),供多栏/排列切换清空后重补
272
+ * (left-fill 会留下无 _isEmptyData 的多余商品行,不能当总行钉死)
273
+ */
274
+ export const resolveVerticalEmptyCount = (
275
+ tableLength,
276
+ baseRowCount,
277
+ fallbackEmptyCount = 0,
278
+ hasEmptyMarked = false
279
+ ) => {
280
+ const base = Math.max(0, baseRowCount || 0)
281
+ const pinned = Math.max(0, (tableLength || 0) - base)
282
+ if (hasEmptyMarked && pinned > 0) return pinned
283
+ return Math.max(0, fallbackEmptyCount || 0)
284
+ }
285
+
286
+ /**
287
+ * 渲染/插值前补齐当前行每一栏序号。
288
+ * 纵向表(_vertical)序号由 repack 统一生成,这里不再按横向公式补,避免断号/残留错觉。
289
+ */
290
+ export const ensureRowSerialKeys = (
291
+ list,
292
+ index,
293
+ colNumber,
294
+ fillIndex,
295
+ dataKey
296
+ ) => {
297
+ const row = list?.[index]
298
+ if (!row || !fillIndex || colNumber < 1 || isSpecialTableRow(row)) {
299
+ return row
300
+ }
301
+ // 纵向多栏:信任 repack 结果
302
+ if (dataKey && /_vertical$/.test(dataKey)) {
303
+ return row
304
+ }
305
+
306
+ const keys = getSerialKeys(colNumber)
307
+ const missing = keys.filter(key => !isFilledIndexValue(row[key]))
308
+ if (!missing.length) return row
309
+
310
+ const realRows = (list || []).filter(
311
+ item => item && !item._isEmptyData && !isSpecialTableRow(item)
312
+ )
313
+
314
+ if (row._isEmptyData) {
315
+ let max = getMaxSerial(realRows, colNumber)
316
+ const lastReal = realRows[realRows.length - 1]
317
+ if (lastReal) {
318
+ keys.forEach(key => {
319
+ if (!isFilledIndexValue(lastReal[key])) max += 1
320
+ })
321
+ }
322
+ const emptyIndex = (list || [])
323
+ .slice(0, index)
324
+ .filter(item => item?._isEmptyData).length
325
+ const next = { ...row }
326
+ keys.forEach((key, col) => {
327
+ next[key] = max + 1 + emptyIndex * colNumber + col
328
+ })
329
+ return next
330
+ }
331
+
332
+ const lastRealIndex = (list || []).reduce((acc, item, i) => {
333
+ if (item && !item._isEmptyData && !isSpecialTableRow(item)) return i
334
+ return acc
335
+ }, -1)
336
+ if (index !== lastRealIndex) return row
337
+
338
+ let max = getMaxSerial(realRows, colNumber)
339
+ const next = { ...row }
340
+ keys.forEach(key => {
341
+ if (!isFilledIndexValue(next[key])) {
342
+ next[key] = ++max
343
+ }
344
+ })
345
+ return next
346
+ }
347
+
348
+ /** 最后一行右侧空单元格续编序号 */
349
+ export const fillPartialLastRow = (rows, colNumber, fillIndex) => {
350
+ if (!fillIndex || !rows?.length || colNumber <= 1) return rows
351
+ const lastIndex = rows.length - 1
352
+ const last = rows[lastIndex]
353
+ if (!last || last._isEmptyData || isSpecialTableRow(last)) return rows
354
+
355
+ let max = getMaxSerial(rows, colNumber)
356
+ const next = { ...last }
357
+ let changed = false
358
+ getSerialKeys(colNumber).forEach(key => {
359
+ if (!isFilledIndexValue(next[key])) {
360
+ next[key] = ++max
361
+ changed = true
362
+ }
363
+ })
364
+ if (!changed) return rows
365
+ const result = rows.slice()
366
+ result[lastIndex] = next
367
+ return result
368
+ }
369
+
370
+ /** 整段按列优先重编序号(先左列上→下,再右列),数据行与空行连续不断号 */
371
+ export const renumberSegmentSerialsColumnMajor = (
372
+ rows,
373
+ begin,
374
+ end,
375
+ colNumber,
376
+ startSerial = 1
377
+ ) => {
378
+ let next = startSerial
379
+ for (let c = 0; c < colNumber; c++) {
380
+ for (let r = begin; r < end; r++) {
381
+ const row = rows[r]
382
+ if (!row || isSpecialTableRow(row)) continue
383
+ const key = INDEX_KEY + getColSuffix(c)
384
+ rows[r] = { ...row, [key]: next++ }
385
+ }
386
+ }
387
+ return next
388
+ }
389
+
390
+ /**
391
+ * 按分页区间把纵向多栏数据改成「每页先左后右」。
392
+ * - 商品按序号灌满当前段左列再排右列(含空行的整段行数)
393
+ * - 每一行都是新建对象,清除旧右列残留
394
+ * - fillIndex 时整段按列重编序号,有数据/无数据同一套规则
395
+ */
396
+ export const repackVerticalTableByPageRanges = (
397
+ rows,
398
+ colNumber,
399
+ pageRanges,
400
+ fillIndex = false,
401
+ sourceItems
402
+ ) => {
403
+ if (!rows?.length || !pageRanges?.length || colNumber < 2) return rows
404
+ const items = (sourceItems && sourceItems.length
405
+ ? sourceItems
406
+ : flattenVerticalSegment(rows, colNumber)
407
+ ).map(stripMultiSuffixFields)
408
+
409
+ if (!items.length && !fillIndex) return rows
410
+
411
+ // 全新数组,避免与旧行对象共享引用
412
+ const result = new Array(rows.length)
413
+ for (let k = 0; k < rows.length; k++) {
414
+ result[k] = rows[k]
415
+ }
416
+
417
+ let consumed = 0
418
+ let nextSerial = 1
419
+ pageRanges.forEach(({ begin, end }) => {
420
+ const packEnd = Math.min(end, rows.length)
421
+ let i = begin
422
+ while (i < packEnd) {
423
+ if (isSpecialTableRow(rows[i])) {
424
+ result[i] = rows[i]
425
+ i++
426
+ continue
427
+ }
428
+ let j = i
429
+ while (j < packEnd && !isSpecialTableRow(rows[j])) {
430
+ j++
431
+ }
432
+
433
+ const rowCount = j - i
434
+ if (rowCount <= 0) {
435
+ i = j
436
+ continue
437
+ }
438
+
439
+ const capacity = rowCount * colNumber
440
+ const chunk = items.slice(consumed, consumed + capacity)
441
+ const packed = packVerticalNewspaper(chunk, colNumber, rowCount, false)
442
+ packed.forEach((row, offset) => {
443
+ result[i + offset] = row
444
+ })
445
+ consumed += chunk.length
446
+
447
+ if (fillIndex) {
448
+ nextSerial = renumberSegmentSerialsColumnMajor(
449
+ result,
450
+ i,
451
+ j,
452
+ colNumber,
453
+ nextSerial
454
+ )
455
+ }
456
+
457
+ i = j
458
+ }
459
+ })
460
+ return result
461
+ }
@@ -11,7 +11,7 @@ import Table from './table'
11
11
  import LongPrintTable from './long_print_table'
12
12
  import MergePage from './merge_page'
13
13
  import { LONG_PRINT } from '../config'
14
- import Big from 'big.js'
14
+ import { getDataKey } from '../util'
15
15
 
16
16
  /** 当前 panel 是否为该表格 content 在全部分页中的最后一次出现 */
17
17
  const isLastTablePanel = (pages, pageIndex, panelIndexInPage, tableIndex) => {
@@ -119,6 +119,8 @@ class Printer extends React.Component {
119
119
  const batchPrintConfig = config.batchPrintConfig
120
120
  // didMount 代表第一次渲染完成,先分页再 ready,避免空 pages 触发 renderPage
121
121
  if (batchPrintConfig !== 2) {
122
+ // 先分页拿到剩余高度,再补空行,最后按补空后的数据重新分页
123
+ // (若先分页后补空却不重算,panel.end 仍是补空前行数,空行/后半数据会被裁掉)
122
124
  printerStore.computedPages()
123
125
  if (config.autoFillConfig?.checked) {
124
126
  this.props.printerStore.setAutofillConfig(
@@ -128,6 +130,7 @@ class Printer extends React.Component {
128
130
  config.autoFillConfig?.fillIndex || false
129
131
  )
130
132
  printerStore.changeTableData()
133
+ printerStore.computedPages()
131
134
  }
132
135
  // 获取剩余空白高度,传到editor
133
136
  getremainpageHeight && getremainpageHeight(printerStore.remainPageHeight)
@@ -158,7 +161,10 @@ class Printer extends React.Component {
158
161
  switch (content.type) {
159
162
  case 'table':
160
163
  // eslint-disable-next-line no-case-declarations
161
- const list = printerStore.data._table[content.dataKey]
164
+ const list =
165
+ printerStore.data._table[
166
+ getDataKey(content.dataKey, content.arrange)
167
+ ] || printerStore.data._table[content.dataKey]
162
168
  return (
163
169
  <Table
164
170
  key={`contents.table.${index}`}
@@ -245,7 +251,9 @@ class Printer extends React.Component {
245
251
 
246
252
  renderPage() {
247
253
  const { printerStore, isSomeSubtotalTr } = this.props
248
- const { config, remainPageHeight, isAutoFilling } = printerStore
254
+ const { config, isAutoFilling } = printerStore
255
+ // 订阅 dataRevision,保证 _table 重排后预览会刷新
256
+ const dataRevision = printerStore.dataRevision
249
257
  return (
250
258
  <>
251
259
  {_.map(printerStore.pages, (page, i) => {
@@ -254,29 +262,40 @@ class Printer extends React.Component {
254
262
  return null
255
263
  }
256
264
  return (
257
- <Page key={i}>
265
+ <Page key={`${i}-${dataRevision}`}>
258
266
  <Header config={config.header} pageIndex={i} />
259
267
  {_.map(page, (panel, ii) => {
260
268
  const content = config.contents[panel.index]
261
269
  const autoFillConfig = config?.autoFillConfig || {}
262
- const isAutofillConfig =
270
+ const resolvedDataKey = getDataKey(
271
+ content?.dataKey,
272
+ content?.arrange
273
+ )
274
+ const tableRows =
275
+ printerStore.data._table?.[resolvedDataKey] ||
276
+ printerStore.data._table?.[content?.dataKey] ||
277
+ []
278
+ const isAutofillTable =
263
279
  isLastPage &&
264
280
  isAutoFilling &&
265
- panel.end &&
266
- content?.dataKey === autoFillConfig?.dataKey
267
- const endList = isAutofillConfig
268
- ? Math.floor(
269
- remainPageHeight /
270
- printerStore.computedTableCustomerRowHeight
271
- )
272
- : 0
273
- const end = +Big(panel?.end || 0).add(endList)
281
+ panel.end != null &&
282
+ panel.type === 'table' &&
283
+ (content?.dataKey === autoFillConfig?.dataKey ||
284
+ content?.dataKey === printerStore.tableConfig?.dataKey ||
285
+ autoFillConfig?.region === `contents.table.${panel.index}`)
286
+ // 空行必须已写入 _table(数据驱动);渲染不得用剩余高度「虚造」行,
287
+ // 否则 list[i] 为 undefined → 模板抛错露出 {{列.xxx_MULTI_SUFFIX}}
288
+ let end = panel.end
289
+ if (isAutofillTable) {
290
+ end = Math.max(panel.end || 0, tableRows.length)
291
+ }
292
+ end = Math.min(end || 0, tableRows.length)
274
293
 
275
294
  switch (panel.type) {
276
295
  case 'table':
277
296
  return (
278
297
  <Table
279
- key={`contents.table.${panel.index}.${ii}`}
298
+ key={`contents.table.${panel.index}.${ii}.${dataRevision}`}
280
299
  name={`contents.table.${panel.index}`}
281
300
  config={content}
282
301
  range={{