gm-printer 10.42.3 → 10.42.4-beta.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.42.3",
3
+ "version": "10.42.4-beta.0",
4
4
  "description": "diy printer",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -0,0 +1,153 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import _ from 'lodash'
4
+ import { observer, inject } from 'mobx-react'
5
+ import i18next from '../../locales'
6
+ import { Flex, Select, Option } from '../components'
7
+ import { Title, Gap, Radio } from './component'
8
+
9
+ // 排序方式常量,与宿主 sort_setting_util / 后端 parse_sort_setting 对齐
10
+ // product_row_sort.field:'' 不排序 / tax_rate 税率 / box_num 箱号 / custom 行号(自定义字段)
11
+ const SORT_FIELD_LIST = [
12
+ { value: '', text: i18next.t('不排序') },
13
+ { value: 'tax_rate', text: i18next.t('税率') },
14
+ { value: 'box_num', text: i18next.t('箱号') },
15
+ { value: 'custom', text: i18next.t('行号') }
16
+ ]
17
+
18
+ const SORT_DIRECTION_LIST = [
19
+ { value: 'asc', text: i18next.t('升序') },
20
+ { value: 'desc', text: i18next.t('降序') }
21
+ ]
22
+
23
+ // order_sort.type:仅商户配送单模板提供(账户模板不渲染该组)
24
+ const ORDER_SORT_LIST = [
25
+ { value: 'none', text: i18next.t('不排序') },
26
+ { value: 'asc', text: i18next.t('按订单时间升序') },
27
+ { value: 'desc', text: i18next.t('按订单时间降序') }
28
+ ]
29
+
30
+ /**
31
+ * 「排序方式」配置类:归集「商品行排序」「单据内订单顺序」两组子配置
32
+ * 读写 editStore.config.sort_setting(mobx4 读取不存在的键不建立依赖,
33
+ * 宿主传入 config 前需保证 sort_setting 键存在,无配置时为 null)
34
+ */
35
+ @inject('editStore')
36
+ @observer
37
+ class SortSettingField extends React.Component {
38
+ getProductRowSort() {
39
+ return this.props.editStore.config.sort_setting?.product_row_sort || {}
40
+ }
41
+
42
+ getOrderSortType() {
43
+ return this.props.editStore.config.sort_setting?.order_sort?.type || 'none'
44
+ }
45
+
46
+ setSortSetting(sortSetting) {
47
+ this.props.editStore.config.sort_setting = sortSetting
48
+ }
49
+
50
+ handleFieldChange = field => {
51
+ const productRowSort = { ...this.getProductRowSort(), field }
52
+ if (field !== 'custom') {
53
+ // 切离行号排序时清掉脏数据
54
+ delete productRowSort.custom_field_key
55
+ } else if (!productRowSort.custom_field_key) {
56
+ // 选中「行号」默认取第一个自定义字段(行号 1)
57
+ const customFields =
58
+ (this.props.addFields && this.props.addFields.sortCustomFields) || []
59
+ productRowSort.custom_field_key = customFields[0] && customFields[0].id
60
+ }
61
+ this.setSortSetting({
62
+ ...this.props.editStore.config.sort_setting,
63
+ product_row_sort: {
64
+ ...productRowSort,
65
+ direction: productRowSort.direction || 'asc'
66
+ }
67
+ })
68
+ }
69
+
70
+ handleDirectionChange = direction => {
71
+ this.setSortSetting({
72
+ ...this.props.editStore.config.sort_setting,
73
+ product_row_sort: { ...this.getProductRowSort(), direction }
74
+ })
75
+ }
76
+
77
+ handleOrderSortChange = type => {
78
+ this.setSortSetting({
79
+ ...this.props.editStore.config.sort_setting,
80
+ order_sort: { type }
81
+ })
82
+ }
83
+
84
+ render() {
85
+ const { showOrderSort } = this.props
86
+ const productRowSort = this.getProductRowSort()
87
+
88
+ return (
89
+ <div>
90
+ <Title title={i18next.t('排序方式')} />
91
+ <Gap height='5px' />
92
+ <Flex alignCenter className='gm-padding-top-5'>
93
+ <div>{i18next.t('商品行排序')}:</div>
94
+ <Select
95
+ className='gm-printer-edit-select'
96
+ value={productRowSort.field || ''}
97
+ onChange={this.handleFieldChange}
98
+ >
99
+ {_.map(SORT_FIELD_LIST, v => (
100
+ <Option key={v.value} value={v.value}>
101
+ {v.text}
102
+ </Option>
103
+ ))}
104
+ </Select>
105
+ </Flex>
106
+ <Flex alignCenter className='gm-padding-top-5'>
107
+ <div>{i18next.t('排序方向')}:</div>
108
+ <Select
109
+ className='gm-printer-edit-select'
110
+ value={productRowSort.direction || 'asc'}
111
+ onChange={this.handleDirectionChange}
112
+ >
113
+ {_.map(SORT_DIRECTION_LIST, v => (
114
+ <Option key={v.value} value={v.value}>
115
+ {v.text}
116
+ </Option>
117
+ ))}
118
+ </Select>
119
+ </Flex>
120
+ {showOrderSort && (
121
+ <>
122
+ <div className='gm-padding-top-5'>
123
+ {i18next.t('单据内订单顺序')}:
124
+ </div>
125
+ {_.map(ORDER_SORT_LIST, v => (
126
+ <Radio
127
+ key={v.value}
128
+ id={`sort_setting_order_${v.value}`}
129
+ value={v.text}
130
+ inputName='sort_setting_order_sort'
131
+ checked={this.getOrderSortType() === v.value}
132
+ radioChecked={this.handleOrderSortChange.bind(this, v.value)}
133
+ />
134
+ ))}
135
+ </>
136
+ )}
137
+ <Gap />
138
+ </div>
139
+ )
140
+ }
141
+ }
142
+
143
+ SortSettingField.propTypes = {
144
+ addFields: PropTypes.object.isRequired,
145
+ showOrderSort: PropTypes.bool,
146
+ editStore: PropTypes.object
147
+ }
148
+
149
+ SortSettingField.defaultProps = {
150
+ showOrderSort: false
151
+ }
152
+
153
+ export default SortSettingField
@@ -238,7 +238,7 @@ class EditorStore {
238
238
  }
239
239
  }).join('/')
240
240
  } else {
241
- return v.style ? v.style.height : ''
241
+ return v && v.style ? v.style.height : ''
242
242
  }
243
243
  }).join('_')
244
244
  }
@@ -1575,7 +1575,8 @@ class EditorStore {
1575
1575
  const table = this.config.contents[arr[2]]
1576
1576
  const prefix = getDataKeyPrefix(table.dataKey)
1577
1577
  // 切换「按商品/按商户排序」时保留已开启的商品分类、双栏后缀,避免点完分类再切排序把 category 冲掉
1578
- const baseSuffix = dataKey === 'goods' ? 'independent_rol_sku' : 'independent_rol_address'
1578
+ const baseSuffix =
1579
+ dataKey === 'goods' ? 'independent_rol_sku' : 'independent_rol_address'
1579
1580
  const nextDataKey = buildDataKey(
1580
1581
  prefix,
1581
1582
  `${baseSuffix}${getDataKeyModifierSuffix(table.dataKey)}`
@@ -1606,7 +1607,10 @@ class EditorStore {
1606
1607
 
1607
1608
  const { dataKey } = table
1608
1609
  // independent_rol_* / detail_one_row_*(含 category、multi 后缀)在下拉里都映射回「按采购明细单行」
1609
- if (isIndependentRolDataKey(dataKey) || isDetailOneRowSelectKey(dataKey)) {
1610
+ if (
1611
+ isIndependentRolDataKey(dataKey) ||
1612
+ isDetailOneRowSelectKey(dataKey)
1613
+ ) {
1610
1614
  return buildDataKey(getDataKeyPrefix(dataKey), 'detail_one_row')
1611
1615
  }
1612
1616
  return dataKey
@@ -9,6 +9,7 @@ import EditorTitle from '../common/editor_title'
9
9
  import EditorSelect from '../common/editor_select'
10
10
  import SpecialField from '../common/editor_special_field'
11
11
  import SpecialOtherField from '../common/editor_special_other_field'
12
+ import SortSettingField from '../common/editor_sort_setting'
12
13
  import EditorField from '../common/editor_edit_field'
13
14
  import EditorAddField from '../common/editor_add_field'
14
15
  import EditorPageSummary from '../common/editor_page_summary'
@@ -88,6 +89,8 @@ class Editor extends React.Component {
88
89
  <Gap height='5px' />
89
90
  <SpecialField addFields={addFields} mockData={mockData} />
90
91
  <SpecialOtherField addFields={addFields} mockData={mockData} />
92
+ <Gap height='5px' />
93
+ <SortSettingField addFields={addFields} showOrderSort />
91
94
  <EditorCutomizedConfig />
92
95
  <Gap height='5px' />
93
96
  <EditorField
@@ -7,6 +7,7 @@ import editStore from './store'
7
7
  import { observer, inject } from 'mobx-react'
8
8
  import EditorTitle from '../common/editor_title'
9
9
  import EditorSelect from '../common/editor_select'
10
+ import SortSettingField from '../common/editor_sort_setting'
10
11
  import EditorField from '../common/editor_edit_field'
11
12
  import EditorAddField from '../common/editor_add_field'
12
13
  import EditorCutomizedConfig from '../common/editor_customize_config'
@@ -55,6 +56,7 @@ class Editor extends React.Component {
55
56
  <Gap height='10px' />
56
57
  <EditorSelect />
57
58
  <Gap height='5px' />
59
+ <SortSettingField addFields={addFields} />
58
60
  <EditorCutomizedConfig />
59
61
  <Gap height='5px' />
60
62
  <EditorField showNewDate={showNewDate} />
@@ -5,6 +5,7 @@ export const INDEX_KEY = '序号'
5
5
 
6
6
  const SPECIAL_ROW_KEYS = [
7
7
  '_special',
8
+ '_specialText',
8
9
  '_diyCategorySubtotal',
9
10
  '_diyTagSubtotal',
10
11
  '_customTr'
@@ -105,7 +106,8 @@ export const flattenHorizontalPackedRows = (rows, colNumber) => {
105
106
  export const flattenVerticalSegment = (rows, colNumber) => {
106
107
  const columns = Array.from({ length: colNumber }, () => [])
107
108
  rows.forEach(row => {
108
- if (isSpecialTableRow(row)) return
109
+ // _isEmptyData 行的序号可能被填充过,必须显式跳过,否则会被当真实商品吸收
110
+ if (isSpecialTableRow(row) || row._isEmptyData) return
109
111
  splitPackedRow(row, colNumber).forEach((item, col) => {
110
112
  if (isRealItem(item)) {
111
113
  columns[col].push(stripMultiSuffixFields(item))
@@ -115,6 +117,37 @@ export const flattenVerticalSegment = (rows, colNumber) => {
115
117
  return columns.reduce((list, col) => list.concat(col), [])
116
118
  }
117
119
 
120
+ /** 按特殊行(分类小计/标题等)把整表切成若干「组」:相邻非特殊行的 run */
121
+ export const splitRowsIntoGroups = rows => {
122
+ const groups = []
123
+ let start = -1
124
+ for (let i = 0; i <= (rows || []).length; i++) {
125
+ const isBoundary = i === rows.length || isSpecialTableRow(rows[i])
126
+ if (!isBoundary && start < 0) {
127
+ start = i
128
+ }
129
+ if (isBoundary) {
130
+ if (start >= 0) {
131
+ groups.push({ begin: start, end: i })
132
+ }
133
+ start = -1
134
+ }
135
+ }
136
+ return groups
137
+ }
138
+
139
+ /**
140
+ * 源表(单栏/横向多栏)按特殊行分组后逐组拍平 → 每组一个商品队列。
141
+ * 源表不会被 repack 改写,队列是纯函数,重复执行结果不变(幂等)。
142
+ */
143
+ export const buildSourceGroupQueues = (rows, colNumber) =>
144
+ splitRowsIntoGroups(rows).map(group =>
145
+ flattenHorizontalPackedRows(
146
+ (rows || []).slice(group.begin, group.end),
147
+ colNumber
148
+ )
149
+ )
150
+
118
151
  const assignItemToRow = (row, item, suffix) => {
119
152
  const clean = stripMultiSuffixFields(item) || {}
120
153
  Object.keys(clean).forEach(key => {
@@ -441,39 +474,34 @@ export const renumberSegmentSerialsColumnMajor = (
441
474
 
442
475
  /**
443
476
  * 按分页区间把纵向多栏数据改成「每页先左后右」。
444
- * - 商品按序号灌满当前段左列再排右列(含空行的整段行数)
445
- * - 每一行都是新建对象,清除旧右列残留
446
- * - fillIndex 时整段按列重编序号,有数据/无数据同一套规则
477
+ * - 按特殊行(分类小计等)把整表切成若干组,每组一个商品队列:
478
+ * 商品只从本组队列取数,永不跨组 —— 奇数商品的组余数留在本组末段空格,
479
+ * 不会挤占下一组(修复分类合并错位)
480
+ * - 每组队列优先用外部传入的源表顺序(groupQueues;源表不被 repack 改写,
481
+ * 重复执行幂等);缺失时按「各页段贡献」从当前行拼接(重排的不动点,
482
+ * 同样幂等,但首趟不做每页重排)
483
+ * - 特殊行钉在原地;非特殊行都是新建对象,清除旧右列残留
484
+ * - fillIndex 时整段按列重编序号,有数据/无数据同一套规则,序号跨段跨组连续
447
485
  */
448
486
  export const repackVerticalTableByPageRanges = (
449
487
  rows,
450
488
  colNumber,
451
489
  pageRanges,
452
490
  fillIndex = false,
453
- sourceItems
491
+ groupQueues = null
454
492
  ) => {
455
493
  if (!rows?.length || !pageRanges?.length || colNumber < 2) return rows
456
- const items = (sourceItems && sourceItems.length
457
- ? sourceItems
458
- : flattenVerticalSegment(rows, colNumber)
459
- ).map(stripMultiSuffixFields)
460
494
 
461
- if (!items.length && !fillIndex) return rows
462
-
463
- // 全新数组,避免与旧行对象共享引用
464
- const result = new Array(rows.length)
465
- for (let k = 0; k < rows.length; k++) {
466
- result[k] = rows[k]
467
- }
495
+ const groups = splitRowsIntoGroups(rows)
496
+ // 全新数组,避免与旧行对象共享引用;特殊行沿用原引用(钉原地)
497
+ const result = rows.slice()
468
498
 
469
- let consumed = 0
470
- let nextSerial = 1
471
- pageRanges.forEach(({ begin, end }) => {
499
+ // 页 range 与组边界取交集得到页段,并记录所属组
500
+ const segments = pageRanges.reduce((list, { begin, end }) => {
472
501
  const packEnd = Math.min(end, rows.length)
473
502
  let i = begin
474
503
  while (i < packEnd) {
475
504
  if (isSpecialTableRow(rows[i])) {
476
- result[i] = rows[i]
477
505
  i++
478
506
  continue
479
507
  }
@@ -481,32 +509,60 @@ export const repackVerticalTableByPageRanges = (
481
509
  while (j < packEnd && !isSpecialTableRow(rows[j])) {
482
510
  j++
483
511
  }
484
-
485
- const rowCount = j - i
486
- if (rowCount <= 0) {
487
- i = j
488
- continue
512
+ if (j > i) {
513
+ list.push({
514
+ begin: i,
515
+ end: j,
516
+ groupIndex: groups.findIndex(g => i >= g.begin && i < g.end)
517
+ })
489
518
  }
519
+ i = j
520
+ }
521
+ return list
522
+ }, [])
523
+
524
+ const externalQueues = groupQueues || []
525
+ const queues = groups.map((_, gi) => {
526
+ const external = externalQueues[gi]
527
+ if (external && external.length) {
528
+ return external
529
+ }
530
+ return segments
531
+ .filter(s => s.groupIndex === gi)
532
+ .reduce(
533
+ (list, s) =>
534
+ list.concat(
535
+ flattenVerticalSegment(rows.slice(s.begin, s.end), colNumber)
536
+ ),
537
+ []
538
+ )
539
+ })
490
540
 
491
- const capacity = rowCount * colNumber
492
- const chunk = items.slice(consumed, consumed + capacity)
493
- const packed = packVerticalNewspaper(chunk, colNumber, rowCount, false)
494
- packed.forEach((row, offset) => {
495
- result[i + offset] = row
496
- })
497
- consumed += chunk.length
498
-
499
- if (fillIndex) {
500
- nextSerial = renumberSegmentSerialsColumnMajor(
501
- result,
502
- i,
503
- j,
504
- colNumber,
505
- nextSerial
506
- )
507
- }
541
+ if (!queues.some(q => q.length) && !fillIndex) return rows
508
542
 
509
- i = j
543
+ const consumed = groups.map(() => 0)
544
+ let nextSerial = 1
545
+ segments.forEach(({ begin, end, groupIndex }) => {
546
+ const rowCount = end - begin
547
+ const queue = queues[groupIndex]
548
+ const chunk = queue.slice(
549
+ consumed[groupIndex],
550
+ consumed[groupIndex] + rowCount * colNumber
551
+ )
552
+ consumed[groupIndex] += chunk.length
553
+ packVerticalNewspaper(chunk, colNumber, rowCount, false).forEach(
554
+ (row, offset) => {
555
+ result[begin + offset] = row
556
+ }
557
+ )
558
+ if (fillIndex) {
559
+ nextSerial = renumberSegmentSerialsColumnMajor(
560
+ result,
561
+ begin,
562
+ end,
563
+ colNumber,
564
+ nextSerial
565
+ )
510
566
  }
511
567
  })
512
568
  return result
@@ -29,7 +29,9 @@ import {
29
29
  packHorizontalRowMajor,
30
30
  createEmptyRows,
31
31
  createEmptyRowSkeleton,
32
- resolveVerticalEmptyCount
32
+ isSpecialTableRow,
33
+ splitRowsIntoGroups,
34
+ buildSourceGroupQueues
33
35
  } from './multi_column'
34
36
 
35
37
  export const TR_BASE_HEIGHT = 23
@@ -669,13 +671,28 @@ class PrinterStore {
669
671
  // 无商品时仍可能需要给空行按列编序号
670
672
  if (!sourceItems.length && !this.fillIndex) return
671
673
 
674
+ // 源表按特殊行分组 → 每组一个商品队列:商品永不跨分类小计行
675
+ const groupQueues = buildSourceGroupQueues(sourceRows, sourceColNumber)
676
+ const verticalGroups = splitRowsIntoGroups(verticalRows)
677
+ const lastGroup = verticalGroups[verticalGroups.length - 1]
678
+ const lastGroupAllEmpty =
679
+ lastGroup &&
680
+ verticalRows
681
+ .slice(lastGroup.begin, lastGroup.end)
682
+ .every(row => row?._isEmptyData)
683
+ // 组数对齐(允许纵向表多一个纯空行尾组=行数填充产物)才用源表队列;
684
+ // 否则退回「按页段贡献」兜底:最坏不做每页重排,绝不会跨组串货
685
+ const aligned =
686
+ groupQueues.length === verticalGroups.length ||
687
+ (groupQueues.length === verticalGroups.length - 1 && lastGroupAllEmpty)
688
+
672
689
  const pageRanges = this.getTablePageRanges(contentIndex, verticalRows)
673
690
  this.data._table[verticalKey] = repackVerticalTableByPageRanges(
674
691
  verticalRows,
675
692
  colNumber,
676
693
  pageRanges,
677
694
  this.fillIndex,
678
- sourceItems
695
+ aligned ? groupQueues : null
679
696
  )
680
697
  this.dataRevision += 1
681
698
  })
@@ -981,44 +998,16 @@ class PrinterStore {
981
998
  const emptyCount =
982
999
  emptyTable.length > 0 ? emptyTable.length : toFillEmptyCount
983
1000
 
984
- // 纵向多栏:源商品按对半行落位 + 追加空行;左列优先由 repack 按「数据行+空行」整段完成
1001
+ // 纵向多栏:保留真实行(含分类小计等特殊行),仅追加空行补页;
1002
+ // 每页先左后右、按组落位由随后的 repackVerticalMultiByPages 完成
985
1003
  if (/_vertical$/.test(dataKey)) {
986
1004
  const sourceKey = dataKey.replace(/_vertical$/, '')
987
- const singleKey = sourceKey.replace(/_multi3?/, '')
988
1005
  const colNumber = getMultiNumber(sourceKey)
989
- // 横向源表缺失时先生成(双栏→三栏常见)
990
- if (!this.data._table[sourceKey]?.length) {
991
- this.ensureMultiTableData(sourceKey)
992
- }
993
- const sourceRows =
994
- this.data._table[singleKey] || this.data._table[sourceKey] || []
995
- const sourceColNumber = isMultiTable(singleKey)
996
- ? getMultiNumber(singleKey)
997
- : isMultiTable(sourceKey)
998
- ? colNumber
999
- : 1
1000
- const sourceItems = flattenHorizontalPackedRows(
1001
- sourceRows,
1002
- sourceColNumber
1003
- )
1004
- const baseRowCount = sourceItems.length
1005
- ? Math.max(1, Math.ceil(sourceItems.length / colNumber))
1006
- : 0
1007
- // 钉死总行:left-first repack 后 _isEmptyData 变少,不能用 emptyTable.length 回推
1008
- const emptyCount = resolveVerticalEmptyCount(
1009
- tableData.length,
1010
- baseRowCount,
1011
- emptyTable.length > 0
1012
- ? emptyTable.length
1013
- : this.getFilledTableData(table).length,
1014
- emptyTable.length > 0
1006
+ const sample =
1007
+ table.find(row => row && !isSpecialTableRow(row)) || tableData[0]
1008
+ this.data._table[dataKey] = table.concat(
1009
+ createEmptyRows(emptyCount, sample, colNumber)
1015
1010
  )
1016
- const dataRows = baseRowCount
1017
- ? packVerticalNewspaper(sourceItems, colNumber, baseRowCount, false)
1018
- : []
1019
- const sample = dataRows[0] || sourceRows[0]
1020
- const emptyRows = createEmptyRows(emptyCount, sample, colNumber)
1021
- this.data._table[dataKey] = dataRows.concat(emptyRows)
1022
1011
  return
1023
1012
  }
1024
1013