@veltra/sheet 2.4.0 → 2.5.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.
Files changed (147) hide show
  1. package/dist/components/sheet/formula-bar.js +50 -7
  2. package/dist/components/sheet/style2.css +2 -2
  3. package/dist/index.d.ts +2 -19
  4. package/dist/index.js +2 -15
  5. package/dist/types/index.d.ts +1 -3
  6. package/package.json +5 -6
  7. package/src/components/sheet/__test__/formula-bar.test.ts +29 -0
  8. package/src/components/sheet/formula-bar.vue +67 -6
  9. package/src/components/sheet/style.scss +1 -1
  10. package/src/index.ts +1 -27
  11. package/src/types/index.ts +0 -2
  12. package/dist/components/report/binding-badge.js +0 -118
  13. package/dist/components/report/designer/cell-coords.js +0 -46
  14. package/dist/components/report/designer/col-widths.js +0 -29
  15. package/dist/components/report/designer/conditional-rules/helpers.js +0 -139
  16. package/dist/components/report/designer/drill-dialog.js +0 -258
  17. package/dist/components/report/designer/drop-highlight-overlay.js +0 -76
  18. package/dist/components/report/designer/float-panel.js +0 -434
  19. package/dist/components/report/designer/role.js +0 -17
  20. package/dist/components/report/designer/rule-preview.js +0 -33
  21. package/dist/components/report/designer/rule-row.js +0 -253
  22. package/dist/components/report/designer/rules-dialog.js +0 -118
  23. package/dist/components/report/designer/topology-overlay.js +0 -122
  24. package/dist/components/report/designer/topology.js +0 -64
  25. package/dist/components/report/designer/use-grid-overlay.js +0 -75
  26. package/dist/components/report/designer-hub.js +0 -177
  27. package/dist/components/report/field-panel-helpers.js +0 -33
  28. package/dist/components/report/field-panel.js +0 -132
  29. package/dist/components/report/filter-bar.js +0 -74
  30. package/dist/components/report/hub-connection-form.js +0 -145
  31. package/dist/components/report/hub-dataset-editor.js +0 -409
  32. package/dist/components/report/preview-grid-size.js +0 -32
  33. package/dist/components/report/print.js +0 -39
  34. package/dist/components/report/report-designer.js +0 -428
  35. package/dist/components/report/report-viewer.js +0 -253
  36. package/dist/components/report/style.d.ts +0 -17
  37. package/dist/components/report/style.js +0 -18
  38. package/dist/components/report/style2.css +0 -965
  39. package/dist/components/report/use-report-designer.js +0 -387
  40. package/dist/components/report/use-report-viewer.js +0 -140
  41. package/dist/report/binding.d.ts +0 -83
  42. package/dist/report/binding.js +0 -334
  43. package/dist/report/connector.d.ts +0 -66
  44. package/dist/report/connector.js +0 -159
  45. package/dist/report/drill.d.ts +0 -48
  46. package/dist/report/drill.js +0 -78
  47. package/dist/report/export-xlsx.d.ts +0 -13
  48. package/dist/report/export-xlsx.js +0 -13
  49. package/dist/report/filter-bar.d.ts +0 -10
  50. package/dist/report/filter-bar.js +0 -31
  51. package/dist/report/index.d.ts +0 -14
  52. package/dist/report/params.d.ts +0 -13
  53. package/dist/report/params.js +0 -61
  54. package/dist/report/print.d.ts +0 -23
  55. package/dist/report/print.js +0 -197
  56. package/dist/report/render/aggregate.js +0 -193
  57. package/dist/report/render/builder.d.ts +0 -12
  58. package/dist/report/render/builder.js +0 -139
  59. package/dist/report/render/coordinate.d.ts +0 -25
  60. package/dist/report/render/coordinate.js +0 -733
  61. package/dist/report/render/index.d.ts +0 -14
  62. package/dist/report/render/index.js +0 -21
  63. package/dist/report/render/style-resolver.js +0 -50
  64. package/dist/report/render/template-index.d.ts +0 -24
  65. package/dist/report/render/template-index.js +0 -137
  66. package/dist/report/rules/evaluate.d.ts +0 -25
  67. package/dist/report/rules/evaluate.js +0 -82
  68. package/dist/report/rules/index.d.ts +0 -2
  69. package/dist/report/template.d.ts +0 -63
  70. package/dist/report/template.js +0 -83
  71. package/dist/report/types.d.ts +0 -103
  72. package/dist/types/report-designer.d.ts +0 -51
  73. package/dist/types/report-viewer.d.ts +0 -57
  74. package/src/components/report/__test__/binding-badge.test.ts +0 -92
  75. package/src/components/report/__test__/conditional-rules-helpers.test.ts +0 -88
  76. package/src/components/report/__test__/field-panel-helpers.test.ts +0 -65
  77. package/src/components/report/__test__/float-panel-position.test.ts +0 -45
  78. package/src/components/report/__test__/preview-grid-size.test.ts +0 -63
  79. package/src/components/report/__test__/report-designer.test.ts +0 -559
  80. package/src/components/report/__test__/report-drill.test.ts +0 -692
  81. package/src/components/report/__test__/report-viewer.test.ts +0 -400
  82. package/src/components/report/__test__/topology.test.ts +0 -143
  83. package/src/components/report/__test__/use-report-designer.test.ts +0 -804
  84. package/src/components/report/binding-badge.ts +0 -122
  85. package/src/components/report/designer/cell-coords.ts +0 -95
  86. package/src/components/report/designer/col-widths.ts +0 -56
  87. package/src/components/report/designer/conditional-rules/helpers.ts +0 -189
  88. package/src/components/report/designer/drill-dialog.vue +0 -245
  89. package/src/components/report/designer/drop-highlight-overlay.vue +0 -75
  90. package/src/components/report/designer/float-panel.vue +0 -444
  91. package/src/components/report/designer/role.ts +0 -32
  92. package/src/components/report/designer/rule-preview.vue +0 -40
  93. package/src/components/report/designer/rule-row.vue +0 -252
  94. package/src/components/report/designer/rules-dialog.vue +0 -110
  95. package/src/components/report/designer/topology-overlay.vue +0 -128
  96. package/src/components/report/designer/topology.ts +0 -124
  97. package/src/components/report/designer/use-grid-overlay.ts +0 -114
  98. package/src/components/report/designer-hub.vue +0 -198
  99. package/src/components/report/field-panel-helpers.ts +0 -44
  100. package/src/components/report/field-panel.vue +0 -136
  101. package/src/components/report/filter-bar.vue +0 -77
  102. package/src/components/report/hub-connection-form.vue +0 -124
  103. package/src/components/report/hub-dataset-editor.vue +0 -387
  104. package/src/components/report/index.ts +0 -2
  105. package/src/components/report/preview-grid-size.ts +0 -33
  106. package/src/components/report/print.ts +0 -44
  107. package/src/components/report/report-designer.vue +0 -454
  108. package/src/components/report/report-viewer.vue +0 -289
  109. package/src/components/report/style-designer.scss +0 -653
  110. package/src/components/report/style-hub.scss +0 -292
  111. package/src/components/report/style-viewer.scss +0 -144
  112. package/src/components/report/style.scss +0 -3
  113. package/src/components/report/style.ts +0 -20
  114. package/src/components/report/use-report-designer.ts +0 -603
  115. package/src/components/report/use-report-viewer.ts +0 -227
  116. package/src/report/__test__/binding.test.ts +0 -320
  117. package/src/report/__test__/connector.test.ts +0 -239
  118. package/src/report/__test__/drill.test.ts +0 -194
  119. package/src/report/__test__/export-xlsx.test.ts +0 -96
  120. package/src/report/__test__/filter-bar.test.ts +0 -58
  121. package/src/report/__test__/params.test.ts +0 -156
  122. package/src/report/__test__/print.test.ts +0 -170
  123. package/src/report/__test__/render.test.ts +0 -856
  124. package/src/report/__test__/rules.test.ts +0 -131
  125. package/src/report/__test__/template.test.ts +0 -366
  126. package/src/report/binding.ts +0 -474
  127. package/src/report/connector.ts +0 -206
  128. package/src/report/drill.ts +0 -112
  129. package/src/report/export-xlsx.ts +0 -15
  130. package/src/report/filter-bar.ts +0 -36
  131. package/src/report/index.ts +0 -16
  132. package/src/report/params.ts +0 -96
  133. package/src/report/print.ts +0 -234
  134. package/src/report/render/__test__/aggregate.test.ts +0 -16
  135. package/src/report/render/__test__/coordinate.test.ts +0 -405
  136. package/src/report/render/aggregate.ts +0 -289
  137. package/src/report/render/builder.ts +0 -211
  138. package/src/report/render/coordinate.ts +0 -1239
  139. package/src/report/render/index.ts +0 -30
  140. package/src/report/render/style-resolver.ts +0 -68
  141. package/src/report/render/template-index.ts +0 -200
  142. package/src/report/rules/evaluate.ts +0 -113
  143. package/src/report/rules/index.ts +0 -7
  144. package/src/report/template.ts +0 -145
  145. package/src/report/types.ts +0 -116
  146. package/src/types/report-designer.ts +0 -55
  147. package/src/types/report-viewer.ts +0 -54
@@ -1,856 +0,0 @@
1
- import { Sheet } from '@veltra/sheet-core'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { REPORT_META_NAMESPACE, createReportBinding } from '../binding'
5
- import { renderReport, resolveFilledColWidths } from '../render'
6
- import type { DatasetCatalogItem, DatasetRecords } from '../types'
7
-
8
- // ---- 内联 fixtures(单一事实源在测试自身,不依赖 playground mock hub)----
9
-
10
- const ORDERS_DATASET: DatasetCatalogItem = {
11
- id: 'orders',
12
- label: '销售明细',
13
- fields: [
14
- { name: 'customer', label: '客户', type: 'string' },
15
- { name: 'orderNo', label: '订单号', type: 'string' },
16
- { name: 'region', label: '地区', type: 'string' },
17
- { name: 'amount', label: '金额', type: 'number' },
18
- { name: 'orderDate', label: '下单日期', type: 'date' }
19
- ]
20
- }
21
-
22
- const CUSTOMERS_DATASET: DatasetCatalogItem = {
23
- id: 'customers',
24
- label: '客户',
25
- fields: [
26
- { name: 'id', label: '客户ID', type: 'string' },
27
- { name: 'region', label: '地区', type: 'string' }
28
- ]
29
- }
30
-
31
- const SALES_MATRIX_DATASET: DatasetCatalogItem = {
32
- id: 'sales-matrix',
33
- label: '销售矩阵',
34
- fields: [
35
- { name: 'region', label: '地区', type: 'string' },
36
- { name: 'category', label: '品类', type: 'string' },
37
- { name: 'amount', label: '销售额', type: 'number' }
38
- ]
39
- }
40
-
41
- /** 4 客户 × 14 订单(与模板断言严格对应) */
42
- const ORDER_ROWS: Record<string, unknown>[] = [
43
- { customer: '甲公司', region: '华东', orderNo: 'O-1001', amount: 100, orderDate: '2024-01-05' },
44
- { customer: '甲公司', region: '华东', orderNo: 'O-1002', amount: 200, orderDate: '2024-01-12' },
45
- { customer: '甲公司', region: '华东', orderNo: 'O-1003', amount: 150, orderDate: '2024-01-20' },
46
- { customer: '甲公司', region: '华东', orderNo: 'O-1004', amount: 180, orderDate: '2024-01-28' },
47
- { customer: '乙公司', region: '华南', orderNo: 'O-2001', amount: 300, orderDate: '2024-02-01' },
48
- { customer: '乙公司', region: '华南', orderNo: 'O-2002', amount: 250, orderDate: '2024-02-08' },
49
- { customer: '乙公司', region: '华南', orderNo: 'O-2003', amount: 220, orderDate: '2024-02-15' },
50
- { customer: '乙公司', region: '华南', orderNo: 'O-2004', amount: 280, orderDate: '2024-02-22' },
51
- { customer: '丙公司', region: '华北', orderNo: 'O-3001', amount: 400, orderDate: '2024-03-01' },
52
- { customer: '丙公司', region: '华北', orderNo: 'O-3002', amount: 350, orderDate: '2024-03-10' },
53
- { customer: '丙公司', region: '华北', orderNo: 'O-3003', amount: 320, orderDate: '2024-03-18' },
54
- { customer: '丁公司', region: '西部', orderNo: 'O-4001', amount: 500, orderDate: '2024-04-02' },
55
- { customer: '丁公司', region: '西部', orderNo: 'O-4002', amount: 450, orderDate: '2024-04-11' },
56
- { customer: '丁公司', region: '西部', orderNo: 'O-4003', amount: 480, orderDate: '2024-04-20' }
57
- ]
58
-
59
- const CUSTOMER_ROWS: Record<string, unknown>[] = [
60
- { id: 'C-01', name: '甲公司', region: '华东' },
61
- { id: 'C-02', name: '乙公司', region: '华南' },
62
- { id: 'C-03', name: '丙公司', region: '华北' },
63
- { id: 'C-04', name: '丁公司', region: '西部' },
64
- { id: 'C-05', name: '戊公司', region: '华东' },
65
- { id: 'C-06', name: '己公司', region: '华南' },
66
- { id: 'C-07', name: '庚公司', region: '华北' },
67
- { id: 'C-08', name: '辛公司', region: '西部' }
68
- ]
69
-
70
- /** 地区 × 品类矩阵交叉数据(4 地区 × 5 品类 = 20 行) */
71
- const SALES_MATRIX_ROWS: Record<string, unknown>[] = [
72
- { region: '华东', category: '办公设备', qty: 42, amount: 50400, month: '2024-Q1' },
73
- { region: '华东', category: '耗材', qty: 128, amount: 8640, month: '2024-Q1' },
74
- { region: '华东', category: '家具', qty: 18, amount: 46440, month: '2024-Q1' },
75
- { region: '华东', category: '外设', qty: 35, amount: 73465, month: '2024-Q1' },
76
- { region: '华东', category: '网络', qty: 12, amount: 8640, month: '2024-Q1' },
77
- { region: '华南', category: '办公设备', qty: 28, amount: 33600, month: '2024-Q1' },
78
- { region: '华南', category: '耗材', qty: 96, amount: 6480, month: '2024-Q1' },
79
- { region: '华南', category: '家具', qty: 22, amount: 56760, month: '2024-Q1' },
80
- { region: '华南', category: '外设', qty: 30, amount: 62970, month: '2024-Q1' },
81
- { region: '华南', category: '网络', qty: 8, amount: 5760, month: '2024-Q1' },
82
- { region: '华北', category: '办公设备', qty: 20, amount: 24000, month: '2024-Q1' },
83
- { region: '华北', category: '耗材', qty: 72, amount: 4860, month: '2024-Q1' },
84
- { region: '华北', category: '家具', qty: 14, amount: 36120, month: '2024-Q1' },
85
- { region: '华北', category: '外设', qty: 18, amount: 37782, month: '2024-Q1' },
86
- { region: '华北', category: '网络', qty: 10, amount: 7200, month: '2024-Q1' },
87
- { region: '西部', category: '办公设备', qty: 16, amount: 19200, month: '2024-Q1' },
88
- { region: '西部', category: '耗材', qty: 54, amount: 3645, month: '2024-Q1' },
89
- { region: '西部', category: '家具', qty: 12, amount: 30960, month: '2024-Q1' },
90
- { region: '西部', category: '外设', qty: 14, amount: 29386, month: '2024-Q1' },
91
- { region: '西部', category: '网络', qty: 6, amount: 4320, month: '2024-Q1' }
92
- ]
93
-
94
- const MOCK_DATA_RECORDS: DatasetRecords = { orders: ORDER_ROWS, customers: CUSTOMER_ROWS }
95
-
96
- // ---- 内联模板种子 ----
97
-
98
- /** 预置 Report Template:表头(5 列)+ 同一扩展带(分组 + 明细)+ 合计行 */
99
- function seedGroupDetailTemplate(sheet: Sheet): void {
100
- sheet.setCells([
101
- { addr: { row: 0, col: 0 }, data: { v: '客户' } },
102
- { addr: { row: 0, col: 1 }, data: { v: '订单号' } },
103
- { addr: { row: 0, col: 2 }, data: { v: '地区' } },
104
- { addr: { row: 0, col: 3 }, data: { v: '金额' } },
105
- { addr: { row: 0, col: 4 }, data: { v: '下单日期' } },
106
- { addr: { row: 2, col: 1 }, data: { v: '合计' } }
107
- ])
108
-
109
- sheet.setCellStyle(
110
- { start: { row: 0, col: 0 }, end: { row: 0, col: 4 } },
111
- { font: { bold: true }, fill: { color: '#E8EEF7' } }
112
- )
113
- sheet.setCellStyle(
114
- { start: { row: 2, col: 0 }, end: { row: 2, col: 4 } },
115
- { font: { bold: true } }
116
- )
117
-
118
- const groupParent = { row: 1, col: 0 }
119
-
120
- const customerGroup = createReportBinding(ORDERS_DATASET, 'customer')
121
- customerGroup.preset = 'groupHeader'
122
- customerGroup.aggregate = 'group'
123
- customerGroup.expand = 'down'
124
- sheet.setCellMeta(groupParent, REPORT_META_NAMESPACE, customerGroup)
125
-
126
- const orderNo = createReportBinding(ORDERS_DATASET, 'orderNo')
127
- orderNo.rowParent = groupParent
128
- sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, orderNo)
129
-
130
- const region = createReportBinding(ORDERS_DATASET, 'region')
131
- region.rowParent = groupParent
132
- sheet.setCellMeta({ row: 1, col: 2 }, REPORT_META_NAMESPACE, region)
133
-
134
- const amount = createReportBinding(ORDERS_DATASET, 'amount')
135
- amount.rowParent = groupParent
136
- sheet.setCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE, amount)
137
-
138
- const orderDate = createReportBinding(ORDERS_DATASET, 'orderDate')
139
- orderDate.rowParent = groupParent
140
- sheet.setCellMeta({ row: 1, col: 4 }, REPORT_META_NAMESPACE, orderDate)
141
-
142
- const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
143
- subtotal.preset = 'subtotal'
144
- subtotal.aggregate = 'sum'
145
- subtotal.expand = 'none'
146
- subtotal.rowParent = groupParent
147
- sheet.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, subtotal)
148
- }
149
-
150
- /** 预置二维矩阵模板:地区(行)× 品类(列)销售额交叉表 */
151
- function seedMatrixTemplate(sheet: Sheet): void {
152
- sheet.setCells([
153
- { addr: { row: 0, col: 0 }, data: { v: '地区 \\ 品类' } },
154
- { addr: { row: 2, col: 0 }, data: { v: '合计' } }
155
- ])
156
-
157
- sheet.setCellStyle(
158
- { start: { row: 0, col: 0 }, end: { row: 0, col: 0 } },
159
- { font: { bold: true }, fill: { color: '#E8EEF7' } }
160
- )
161
- sheet.setCellStyle(
162
- { start: { row: 2, col: 0 }, end: { row: 2, col: 0 } },
163
- { font: { bold: true } }
164
- )
165
-
166
- const colGroupAddr = { row: 0, col: 1 }
167
- const rowGroupAddr = { row: 1, col: 0 }
168
- const crossAddr = { row: 1, col: 1 }
169
-
170
- const categoryGroup = createReportBinding(SALES_MATRIX_DATASET, 'category')
171
- categoryGroup.preset = 'groupHeader'
172
- categoryGroup.aggregate = 'group'
173
- categoryGroup.expand = 'right'
174
- sheet.setCellMeta(colGroupAddr, REPORT_META_NAMESPACE, categoryGroup)
175
-
176
- const regionGroup = createReportBinding(SALES_MATRIX_DATASET, 'region')
177
- regionGroup.preset = 'groupHeader'
178
- regionGroup.aggregate = 'group'
179
- regionGroup.expand = 'down'
180
- sheet.setCellMeta(rowGroupAddr, REPORT_META_NAMESPACE, regionGroup)
181
-
182
- const cross = createReportBinding(SALES_MATRIX_DATASET, 'amount')
183
- cross.preset = 'cross'
184
- cross.aggregate = 'sum'
185
- cross.expand = 'none'
186
- cross.rowParent = rowGroupAddr
187
- cross.colParent = colGroupAddr
188
- sheet.setCellMeta(crossAddr, REPORT_META_NAMESPACE, cross)
189
-
190
- const colSubtotal = createReportBinding(SALES_MATRIX_DATASET, 'amount')
191
- colSubtotal.preset = 'subtotal'
192
- colSubtotal.aggregate = 'sum'
193
- colSubtotal.expand = 'none'
194
- colSubtotal.colParent = colGroupAddr
195
- sheet.setCellMeta({ row: 2, col: 1 }, REPORT_META_NAMESPACE, colSubtotal)
196
-
197
- const grandTotal = createReportBinding(SALES_MATRIX_DATASET, 'amount')
198
- grandTotal.preset = 'grandTotal'
199
- grandTotal.aggregate = 'sum'
200
- grandTotal.expand = 'none'
201
- sheet.setCellMeta({ row: 2, col: 6 }, REPORT_META_NAMESPACE, grandTotal)
202
- }
203
-
204
- function buildGroupDetailTemplate() {
205
- const sheet = new Sheet()
206
- seedGroupDetailTemplate(sheet)
207
- return sheet.snapshot()
208
- }
209
-
210
- function cellValue(
211
- snapshot: ReturnType<typeof buildGroupDetailTemplate>,
212
- row: number,
213
- col: number
214
- ): unknown {
215
- return snapshot.cells.find((c) => c.row === row && c.col === col)?.v
216
- }
217
-
218
- function hasMerge(
219
- snapshot: ReturnType<typeof buildGroupDetailTemplate>,
220
- start: { row: number; col: number },
221
- end: { row: number; col: number }
222
- ): boolean {
223
- return snapshot.merges.some(
224
- (m) =>
225
- m.start.row === start.row &&
226
- m.start.col === start.col &&
227
- m.end.row === end.row &&
228
- m.end.col === end.col
229
- )
230
- }
231
-
232
- function buildMatrixTemplate() {
233
- const sheet = new Sheet()
234
- seedMatrixTemplate(sheet)
235
- return sheet.snapshot()
236
- }
237
-
238
- describe('renderReport', () => {
239
- it('4 客户 14 订单:行数、分组值、明细 zip、地区列与组小计', () => {
240
- const template = buildGroupDetailTemplate()
241
- const filled = renderReport(template, MOCK_DATA_RECORDS)
242
-
243
- // 表头 1 + 甲(4+1) + 乙(4+1) + 丙(3+1) + 丁(3+1) = 19 行
244
- expect(filled.rows).toBe(19)
245
- expect(filled.meta).toBeUndefined()
246
-
247
- // 表头保留(含地区列)
248
- expect(cellValue(filled, 0, 0)).toBe('客户')
249
- expect(cellValue(filled, 0, 1)).toBe('订单号')
250
- expect(cellValue(filled, 0, 2)).toBe('地区')
251
- expect(cellValue(filled, 0, 3)).toBe('金额')
252
- expect(cellValue(filled, 0, 4)).toBe('下单日期')
253
-
254
- // 甲公司:4 明细 + 小计 630(金额在 col 3)
255
- expect(cellValue(filled, 1, 0)).toBe('甲公司')
256
- expect(cellValue(filled, 1, 1)).toBe('O-1001')
257
- expect(cellValue(filled, 1, 2)).toBe('华东')
258
- expect(cellValue(filled, 1, 3)).toBe(100)
259
- expect(cellValue(filled, 2, 1)).toBe('O-1002')
260
- expect(cellValue(filled, 3, 1)).toBe('O-1003')
261
- expect(cellValue(filled, 4, 1)).toBe('O-1004')
262
- expect(cellValue(filled, 5, 1)).toBe('合计')
263
- expect(cellValue(filled, 5, 3)).toBe(630)
264
-
265
- // 乙公司:4 明细 + 小计 1050
266
- expect(cellValue(filled, 6, 0)).toBe('乙公司')
267
- expect(cellValue(filled, 6, 1)).toBe('O-2001')
268
- expect(cellValue(filled, 6, 2)).toBe('华南')
269
- expect(cellValue(filled, 9, 1)).toBe('O-2004')
270
- expect(cellValue(filled, 10, 1)).toBe('合计')
271
- expect(cellValue(filled, 10, 3)).toBe(1050)
272
-
273
- // 丙公司:3 明细 + 小计 1070
274
- expect(cellValue(filled, 11, 0)).toBe('丙公司')
275
- expect(cellValue(filled, 11, 2)).toBe('华北')
276
- expect(cellValue(filled, 14, 1)).toBe('合计')
277
- expect(cellValue(filled, 14, 3)).toBe(1070)
278
-
279
- // 丁公司:3 明细 + 小计 1430
280
- expect(cellValue(filled, 15, 0)).toBe('丁公司')
281
- expect(cellValue(filled, 15, 2)).toBe('西部')
282
- expect(cellValue(filled, 18, 1)).toBe('合计')
283
- expect(cellValue(filled, 18, 3)).toBe(1430)
284
-
285
- // 分组格纵向 merge 覆盖明细行(不含小计行)
286
- expect(hasMerge(filled, { row: 1, col: 0 }, { row: 4, col: 0 })).toBe(true)
287
- expect(hasMerge(filled, { row: 6, col: 0 }, { row: 9, col: 0 })).toBe(true)
288
- expect(hasMerge(filled, { row: 11, col: 0 }, { row: 13, col: 0 })).toBe(true)
289
- expect(hasMerge(filled, { row: 15, col: 0 }, { row: 17, col: 0 })).toBe(true)
290
- })
291
-
292
- it('分组格被误设为 list 时按明细全量展开(无分组锚点)', () => {
293
- const template = buildGroupDetailTemplate()
294
- const broken = JSON.parse(JSON.stringify(template)) as ReturnType<
295
- typeof buildGroupDetailTemplate
296
- >
297
- const groupMeta = broken.meta!.find((m) => m.row === 1 && m.col === 0)!
298
- ;(groupMeta.payload as { aggregate: string }).aggregate = 'list'
299
-
300
- const filled = renderReport(broken, MOCK_DATA_RECORDS)
301
- // 分组格误设为 list 时按全量明细展开 + 小计
302
- expect(filled.rows).toBe(1 + ORDER_ROWS.length + 1)
303
- expect(cellValue(filled, 1, 1)).toBe('O-1001')
304
- })
305
-
306
- it('Filled Report 不携带 Binding meta', () => {
307
- const template = buildGroupDetailTemplate()
308
- const filled = renderReport(template, MOCK_DATA_RECORDS)
309
- expect(filled.meta?.length ?? 0).toBe(0)
310
- })
311
-
312
- it('模板样式 id 随扩展实例复制', () => {
313
- const sheet = new Sheet()
314
- seedGroupDetailTemplate(sheet)
315
- // 金额列在 col 3
316
- sheet.setCellStyle(
317
- { start: { row: 1, col: 3 }, end: { row: 1, col: 3 } },
318
- { font: { bold: true } }
319
- )
320
- const styleId = sheet.getCellData({ row: 1, col: 3 })?.s
321
- expect(styleId).toBeDefined()
322
-
323
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
324
- const firstAmount = filled.cells.find((c) => c.row === 1 && c.col === 3)
325
- expect(firstAmount?.s).toBe(styleId)
326
- })
327
-
328
- it('模板格对齐样式直通 Filled Report 快照(静态格与绑定扩展格)', () => {
329
- const sheet = new Sheet()
330
- seedGroupDetailTemplate(sheet)
331
- // 静态表头格(resolveStatic 路径)
332
- sheet.setCellStyle(
333
- { start: { row: 0, col: 0 }, end: { row: 0, col: 0 } },
334
- { align: { horizontal: 'right' } }
335
- )
336
- // 绑定明细格(resolveCell 路径,样式随扩展实例复制)
337
- sheet.setCellStyle(
338
- { start: { row: 1, col: 3 }, end: { row: 1, col: 3 } },
339
- { align: { horizontal: 'center' } }
340
- )
341
-
342
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
343
- const header = filled.cells.find((c) => c.row === 0 && c.col === 0)
344
- expect(filled.styles[(header!.s ?? 1) - 1]?.align?.horizontal).toBe('right')
345
-
346
- const firstAmount = filled.cells.find((c) => c.row === 1 && c.col === 3)
347
- expect(firstAmount?.s).toBeDefined()
348
- expect(filled.styles[(firstAmount!.s ?? 1) - 1]?.align?.horizontal).toBe('center')
349
- // 展开带后续实例同样携带对齐
350
- const secondAmount = filled.cells.find((c) => c.row === 2 && c.col === 3)
351
- expect(filled.styles[(secondAmount!.s ?? 1) - 1]?.align?.horizontal).toBe('center')
352
- })
353
-
354
- it('subtotal 支持 avg / count 聚合', () => {
355
- const sheet = new Sheet()
356
- seedGroupDetailTemplate(sheet)
357
- sheet.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, {
358
- dataset: 'orders',
359
- field: 'amount',
360
- preset: 'subtotal',
361
- aggregate: 'avg',
362
- expand: 'none',
363
- rowParent: { row: 1, col: 0 }
364
- })
365
- const avgFilled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
366
- // 甲公司 4 单金额均值 (100+200+150+180)/4 = 157.5
367
- expect(avgFilled.cells.find((c) => c.row === 5 && c.col === 3)?.v).toBe(157.5)
368
-
369
- const sheet2 = new Sheet()
370
- seedGroupDetailTemplate(sheet2)
371
- sheet2.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, {
372
- dataset: 'orders',
373
- field: 'orderNo',
374
- preset: 'subtotal',
375
- aggregate: 'count',
376
- expand: 'none',
377
- rowParent: { row: 1, col: 0 }
378
- })
379
- const countFilled = renderReport(sheet2.snapshot(), MOCK_DATA_RECORDS)
380
- expect(countFilled.cells.find((c) => c.row === 5 && c.col === 3)?.v).toBe(4)
381
- })
382
-
383
- it('grandTotal 行对全量数据汇总', () => {
384
- const sheet = new Sheet()
385
- seedGroupDetailTemplate(sheet)
386
- sheet.setCells([{ addr: { row: 3, col: 1 }, data: { v: '总计' } }])
387
- sheet.setCellMeta({ row: 3, col: 3 }, REPORT_META_NAMESPACE, {
388
- dataset: 'orders',
389
- field: 'amount',
390
- preset: 'grandTotal',
391
- aggregate: 'sum',
392
- expand: 'none'
393
- })
394
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
395
- // 全量金额 4180
396
- expect(filled.cells.find((c) => c.v === 4180 && c.col === 3)?.v).toBe(4180)
397
- })
398
-
399
- it('条件样式规则固化到 Filled Report 样式表', () => {
400
- const sheet = new Sheet()
401
- seedGroupDetailTemplate(sheet)
402
- sheet.setCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE, {
403
- dataset: 'orders',
404
- field: 'amount',
405
- preset: 'detail',
406
- aggregate: 'list',
407
- expand: 'down',
408
- rowParent: { row: 1, col: 0 },
409
- conditionalRules: [{ operator: 'gt', value: 100, style: { fill: { color: '#FFCCCC' } } }]
410
- })
411
-
412
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
413
- const hotCell = filled.cells.find((c) => c.col === 3 && c.v === 150)
414
- expect(hotCell?.s).toBeDefined()
415
- expect(filled.styles[(hotCell!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
416
-
417
- const coldCell = filled.cells.find((c) => c.col === 3 && c.v === 100)
418
- expect(filled.styles[(coldCell!.s ?? 1) - 1]?.fill?.color).not.toBe('#FFCCCC')
419
- })
420
-
421
- it('条件样式按另一字段求值', () => {
422
- const sheet = new Sheet()
423
- seedGroupDetailTemplate(sheet)
424
- sheet.setCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE, {
425
- dataset: 'orders',
426
- field: 'amount',
427
- preset: 'detail',
428
- aggregate: 'list',
429
- expand: 'down',
430
- rowParent: { row: 1, col: 0 },
431
- conditionalRules: [
432
- { operator: 'eq', value: '华东', field: 'region', style: { fill: { color: '#FFCCCC' } } }
433
- ]
434
- })
435
-
436
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
437
- const eastCell = filled.cells.find((c) => c.col === 3 && c.v === 150)
438
- expect(filled.styles[(eastCell!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
439
-
440
- const southCell = filled.cells.find((c) => c.col === 3 && c.v === 300)
441
- expect(filled.styles[(southCell!.s ?? 1) - 1]?.fill?.color).not.toBe('#FFCCCC')
442
- })
443
-
444
- it('scope: row 染满整行含静态格与横向展开列', () => {
445
- const sheet = new Sheet()
446
- seedGroupDetailTemplate(sheet)
447
- sheet.setCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE, {
448
- dataset: 'orders',
449
- field: 'amount',
450
- preset: 'detail',
451
- aggregate: 'list',
452
- expand: 'down',
453
- rowParent: { row: 1, col: 0 },
454
- conditionalRules: [
455
- { operator: 'gt', value: 100, scope: 'row', style: { fill: { color: '#FFCCCC' } } }
456
- ]
457
- })
458
-
459
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
460
- const hotRow = filled.cells.find((c) => c.col === 3 && c.v === 150)!.row
461
- const hotStyle =
462
- filled.styles[(filled.cells.find((c) => c.col === 3 && c.v === 150)!.s ?? 1) - 1]
463
- expect(hotStyle?.fill?.color).toBe('#FFCCCC')
464
-
465
- const staticOrderNo = filled.cells.find(
466
- (c) => c.row === hotRow && c.col === 1 && c.v === 'O-1003'
467
- )
468
- expect(filled.styles[(staticOrderNo!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
469
-
470
- const coldRow = filled.cells.find((c) => c.col === 3 && c.v === 100)!.row
471
- const coldAmount = filled.cells.find((c) => c.row === coldRow && c.col === 3)
472
- expect(filled.styles[(coldAmount!.s ?? 1) - 1]?.fill?.color).not.toBe('#FFCCCC')
473
- })
474
-
475
- it('横向展开明细列同行继承 scope: row 行级样式', () => {
476
- const sheet = new Sheet()
477
- sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '备注' } }])
478
- const amount = createReportBinding(ORDERS_DATASET, 'amount')
479
- amount.aggregate = 'list'
480
- amount.expand = 'right'
481
- amount.conditionalRules = [
482
- { operator: 'gt', value: 100, scope: 'row', style: { fill: { color: '#FFCCCC' } } }
483
- ]
484
- sheet.setCellMeta({ row: 0, col: 1 }, REPORT_META_NAMESPACE, amount)
485
-
486
- const filled = renderReport(sheet.snapshot(), { orders: [ORDER_ROWS[2]!] })
487
- const amountCell = filled.cells.find((c) => c.row === 0 && c.v === 150)
488
- const staticNote = filled.cells.find((c) => c.row === 0 && c.col === 0)
489
- expect(filled.styles[(amountCell!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
490
- expect(filled.styles[(staticNote!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
491
-
492
- const coldFilled = renderReport(sheet.snapshot(), { orders: [ORDER_ROWS[0]!] })
493
- const coldAmount = coldFilled.cells.find((c) => c.row === 0 && c.v === 100)
494
- expect(coldFilled.styles[(coldAmount!.s ?? 1) - 1]?.fill?.color).not.toBe('#FFCCCC')
495
- })
496
-
497
- it('二维矩阵:地区 × 品类销售额交叉扩展', () => {
498
- const filled = renderReport(buildMatrixTemplate(), { 'sales-matrix': SALES_MATRIX_ROWS })
499
-
500
- // 表头 1 + 4 地区 + 1 合计 = 6 行
501
- expect(filled.rows).toBe(6)
502
- expect(cellValue(filled, 0, 0)).toBe('地区 \\ 品类')
503
- expect(cellValue(filled, 0, 1)).toBe('办公设备')
504
- expect(cellValue(filled, 0, 5)).toBe('网络')
505
-
506
- // 华东 × 办公设备 = 50400
507
- expect(cellValue(filled, 1, 0)).toBe('华东')
508
- expect(cellValue(filled, 1, 1)).toBe(50400)
509
-
510
- // 列合计:办公设备四地区之和
511
- const officeTotal = SALES_MATRIX_ROWS.filter((r) => r.category === '办公设备').reduce(
512
- (sum, row) => sum + (row.amount as number),
513
- 0
514
- )
515
- expect(cellValue(filled, 5, 1)).toBe(officeTotal)
516
-
517
- // 总计
518
- const grandTotal = SALES_MATRIX_ROWS.reduce((sum, row) => sum + (row.amount as number), 0)
519
- expect(cellValue(filled, 5, 6)).toBe(grandTotal)
520
- })
521
-
522
- it('同扩展带追加明细列:预览行数与值正确', () => {
523
- const sheet = new Sheet()
524
- seedGroupDetailTemplate(sheet)
525
- sheet.setCells([{ addr: { row: 0, col: 5 }, data: { v: '备注' } }])
526
- const remark = createReportBinding(ORDERS_DATASET, 'orderNo')
527
- remark.rowParent = { row: 1, col: 0 }
528
- sheet.setCellMeta({ row: 1, col: 5 }, REPORT_META_NAMESPACE, remark)
529
-
530
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
531
- expect(filled.rows).toBe(19)
532
- expect(cellValue(filled, 1, 0)).toBe('甲公司')
533
- expect(cellValue(filled, 1, 1)).toBe('O-1001')
534
- expect(cellValue(filled, 1, 5)).toBe('O-1001')
535
- })
536
-
537
- it('独立扩展带:第二数据源 customers 明细正确渲染', () => {
538
- const sheet = new Sheet()
539
- seedGroupDetailTemplate(sheet)
540
- sheet.setCells([{ addr: { row: 0, col: 5 }, data: { v: '客户ID' } }])
541
- const customerId = createReportBinding(CUSTOMERS_DATASET, 'id')
542
- customerId.expand = 'down'
543
- customerId.aggregate = 'list'
544
- sheet.setCellMeta({ row: 4, col: 5 }, REPORT_META_NAMESPACE, customerId)
545
-
546
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
547
- const customers = CUSTOMER_ROWS
548
-
549
- // 订单扩展 19 行(含表头)+ 客户列表
550
- expect(filled.rows).toBe(19 + customers.length)
551
- expect(cellValue(filled, 0, 5)).toBe('客户ID')
552
- expect(cellValue(filled, 19, 5)).toBe(customers[0]!.id)
553
- expect(cellValue(filled, 19 + customers.length - 1, 5)).toBe(
554
- customers[customers.length - 1]!.id
555
- )
556
- })
557
-
558
- it('多行扩展带合并明细输出:分组列与首行明细不丢失', () => {
559
- const sheet = new Sheet()
560
- seedGroupDetailTemplate(sheet)
561
- sheet.clearCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE)
562
- sheet.clearCellMeta({ row: 1, col: 4 }, REPORT_META_NAMESPACE)
563
- sheet.clearCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE)
564
- const snap = sheet.snapshot()
565
- snap.cells = snap.cells.filter((cell) => !(cell.row === 2 && cell.col === 1))
566
- sheet.restore(snap)
567
- sheet.restoreContent(snap)
568
-
569
- const amount = createReportBinding(ORDERS_DATASET, 'amount')
570
- amount.rowParent = { row: 1, col: 0 }
571
- sheet.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, amount)
572
-
573
- const orderDate = createReportBinding(ORDERS_DATASET, 'orderDate')
574
- orderDate.rowParent = { row: 1, col: 0 }
575
- sheet.setCellMeta({ row: 2, col: 4 }, REPORT_META_NAMESPACE, orderDate)
576
-
577
- const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
578
- subtotal.preset = 'subtotal'
579
- subtotal.aggregate = 'sum'
580
- subtotal.expand = 'none'
581
- subtotal.rowParent = { row: 1, col: 0 }
582
- sheet.setCellMeta({ row: 3, col: 3 }, REPORT_META_NAMESPACE, subtotal)
583
- sheet.setCells([{ addr: { row: 3, col: 1 }, data: { v: '合计' } }])
584
-
585
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
586
- expect(filled.rows).toBe(19)
587
- expect(cellValue(filled, 1, 0)).toBe('甲公司')
588
- expect(cellValue(filled, 1, 1)).toBe('O-1001')
589
- expect(cellValue(filled, 1, 3)).toBe(100)
590
- expect(cellValue(filled, 1, 4)).toBe('2024-01-05')
591
- expect(cellValue(filled, 5, 3)).toBe(630)
592
- })
593
-
594
- it('横向展开 right + group:列头向右延展', () => {
595
- const sheet = new Sheet()
596
- sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '品类' } }])
597
- const category = createReportBinding(SALES_MATRIX_DATASET, 'category')
598
- category.aggregate = 'group'
599
- category.expand = 'right'
600
- sheet.setCellMeta({ row: 0, col: 1 }, REPORT_META_NAMESPACE, category)
601
-
602
- const filled = renderReport(sheet.snapshot(), { 'sales-matrix': SALES_MATRIX_ROWS })
603
- expect(cellValue(filled, 0, 1)).toBe('办公设备')
604
- expect(cellValue(filled, 0, 5)).toBe('网络')
605
- })
606
-
607
- it('转置明细 right + list:一条记录一列', () => {
608
- const sheet = new Sheet()
609
- sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '订单号' } }])
610
- const orderNo = createReportBinding(ORDERS_DATASET, 'orderNo')
611
- orderNo.aggregate = 'list'
612
- orderNo.expand = 'right'
613
- sheet.setCellMeta({ row: 0, col: 1 }, REPORT_META_NAMESPACE, orderNo)
614
-
615
- const filled = renderReport(sheet.snapshot(), { orders: ORDER_ROWS.slice(0, 3) })
616
- expect(cellValue(filled, 0, 1)).toBe('O-1001')
617
- expect(cellValue(filled, 0, 2)).toBe('O-1002')
618
- expect(cellValue(filled, 0, 3)).toBe('O-1003')
619
- })
620
-
621
- it('多级列头:年 → 季度列头值正确', () => {
622
- const sheet = new Sheet()
623
- const data = [
624
- { year: '2024', quarter: 'Q1', amount: 10 },
625
- { year: '2024', quarter: 'Q2', amount: 20 },
626
- { year: '2025', quarter: 'Q1', amount: 30 }
627
- ]
628
- const year = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'year')
629
- year.aggregate = 'group'
630
- year.expand = 'right'
631
- sheet.setCellMeta({ row: 0, col: 0 }, REPORT_META_NAMESPACE, year)
632
-
633
- const quarter = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'quarter')
634
- quarter.aggregate = 'group'
635
- quarter.expand = 'right'
636
- quarter.colParent = { row: 0, col: 0 }
637
- sheet.setCellMeta({ row: 1, col: 0 }, REPORT_META_NAMESPACE, quarter)
638
-
639
- const filled = renderReport(sheet.snapshot(), { sales: data })
640
- expect(cellValue(filled, 0, 0)).toBe('2024')
641
- expect(cellValue(filled, 0, 2)).toBe('2025')
642
- expect(cellValue(filled, 1, 0)).toBe('Q1')
643
- expect(cellValue(filled, 1, 1)).toBe('Q2')
644
- expect(cellValue(filled, 1, 2)).toBe('Q1')
645
- })
646
-
647
- it('顶部标题行的交叉表照常展开', () => {
648
- const sheet = new Sheet()
649
- sheet.setCells([
650
- { addr: { row: 0, col: 0 }, data: { v: '销售交叉表' } },
651
- { addr: { row: 2, col: 0 }, data: { v: '合计' } }
652
- ])
653
-
654
- const category = createReportBinding(SALES_MATRIX_DATASET, 'category')
655
- category.aggregate = 'group'
656
- category.expand = 'right'
657
- sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, category)
658
-
659
- const region = createReportBinding(SALES_MATRIX_DATASET, 'region')
660
- region.aggregate = 'group'
661
- region.expand = 'down'
662
- sheet.setCellMeta({ row: 2, col: 0 }, REPORT_META_NAMESPACE, region)
663
-
664
- const cross = createReportBinding(SALES_MATRIX_DATASET, 'amount')
665
- cross.aggregate = 'sum'
666
- cross.expand = 'none'
667
- cross.rowParent = { row: 2, col: 0 }
668
- cross.colParent = { row: 1, col: 1 }
669
- sheet.setCellMeta({ row: 2, col: 1 }, REPORT_META_NAMESPACE, cross)
670
-
671
- const filled = renderReport(sheet.snapshot(), { 'sales-matrix': SALES_MATRIX_ROWS })
672
- expect(cellValue(filled, 0, 0)).toBe('销售交叉表')
673
- expect(cellValue(filled, 1, 1)).toBe('办公设备')
674
- expect(cellValue(filled, 2, 0)).toBe('华东')
675
- expect(cellValue(filled, 2, 1)).toBe(50400)
676
- })
677
-
678
- it('空数据集:展开块为 0,表头仍输出', () => {
679
- const filled = renderReport(buildGroupDetailTemplate(), { orders: [], customers: [] })
680
- expect(filled.rows).toBe(1)
681
- expect(cellValue(filled, 0, 0)).toBe('客户')
682
- })
683
-
684
- it('max / min 聚合', () => {
685
- const sheet = new Sheet()
686
- seedGroupDetailTemplate(sheet)
687
- sheet.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, {
688
- dataset: 'orders',
689
- field: 'amount',
690
- preset: 'subtotal',
691
- aggregate: 'max',
692
- expand: 'none',
693
- rowParent: { row: 1, col: 0 }
694
- })
695
- const maxFilled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
696
- expect(maxFilled.cells.find((c) => c.row === 5 && c.col === 3)?.v).toBe(200)
697
-
698
- const sheet2 = new Sheet()
699
- seedGroupDetailTemplate(sheet2)
700
- sheet2.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, {
701
- dataset: 'orders',
702
- field: 'amount',
703
- preset: 'subtotal',
704
- aggregate: 'min',
705
- expand: 'none',
706
- rowParent: { row: 1, col: 0 }
707
- })
708
- const minFilled = renderReport(sheet2.snapshot(), MOCK_DATA_RECORDS)
709
- expect(minFilled.cells.find((c) => c.row === 5 && c.col === 3)?.v).toBe(100)
710
- })
711
-
712
- it('mergeSpan: false 时扩展格不合并', () => {
713
- const sheet = new Sheet()
714
- const group = createReportBinding(ORDERS_DATASET, 'customer')
715
- group.aggregate = 'group'
716
- group.expand = 'down'
717
- group.mergeSpan = false
718
- sheet.setCellMeta({ row: 1, col: 0 }, REPORT_META_NAMESPACE, group)
719
-
720
- const detail = createReportBinding(ORDERS_DATASET, 'orderNo')
721
- detail.aggregate = 'list'
722
- detail.expand = 'down'
723
- detail.rowParent = { row: 1, col: 0 }
724
- sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, detail)
725
-
726
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
727
- expect(hasMerge(filled, { row: 1, col: 0 }, { row: 4, col: 0 })).toBe(false)
728
- expect(cellValue(filled, 1, 0)).toBe('甲公司')
729
- expect(cellValue(filled, 2, 0)).toBe('甲公司')
730
- })
731
-
732
- it('横向展开列继承列方向父格的模板列宽', () => {
733
- const template = buildMatrixTemplate()
734
- const templateWidths: Array<[number, number]> = [
735
- [0, 80],
736
- [1, 120]
737
- ]
738
- const filledWidths = resolveFilledColWidths(
739
- template,
740
- { 'sales-matrix': SALES_MATRIX_ROWS },
741
- templateWidths
742
- )
743
- expect(filledWidths).toEqual(
744
- expect.arrayContaining([
745
- [0, 80],
746
- [1, 120],
747
- [2, 120],
748
- [3, 120],
749
- [4, 120],
750
- [5, 120]
751
- ])
752
- )
753
- })
754
-
755
- it('跨列行小计按组求和,不是全表合计', () => {
756
- const sheet = new Sheet()
757
- sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '客户' } }])
758
- const groupParent = { row: 1, col: 0 }
759
-
760
- const customerGroup = createReportBinding(ORDERS_DATASET, 'customer')
761
- customerGroup.preset = 'groupHeader'
762
- customerGroup.aggregate = 'group'
763
- customerGroup.expand = 'down'
764
- sheet.setCellMeta(groupParent, REPORT_META_NAMESPACE, customerGroup)
765
-
766
- const orderNo = createReportBinding(ORDERS_DATASET, 'orderNo')
767
- orderNo.rowParent = groupParent
768
- sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, orderNo)
769
-
770
- const amount = createReportBinding(ORDERS_DATASET, 'amount')
771
- amount.rowParent = groupParent
772
- sheet.setCellMeta({ row: 1, col: 2 }, REPORT_META_NAMESPACE, amount)
773
-
774
- const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
775
- subtotal.preset = 'subtotal'
776
- subtotal.aggregate = 'sum'
777
- subtotal.expand = 'none'
778
- subtotal.rowParent = groupParent
779
- sheet.setCellMeta({ row: 2, col: 2 }, REPORT_META_NAMESPACE, subtotal)
780
-
781
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
782
- expect(cellValue(filled, 5, 2)).toBe(630)
783
- expect(cellValue(filled, 10, 2)).toBe(1050)
784
- expect(cellValue(filled, 5, 2)).not.toBe(4180)
785
- })
786
-
787
- it('跨列行小计误挂明细时仍按分组祖先过滤', () => {
788
- const sheet = new Sheet()
789
- sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '客户' } }])
790
- const groupParent = { row: 1, col: 0 }
791
- const listAddr = { row: 1, col: 2 }
792
-
793
- const customerGroup = createReportBinding(ORDERS_DATASET, 'customer')
794
- customerGroup.preset = 'groupHeader'
795
- customerGroup.aggregate = 'group'
796
- customerGroup.expand = 'down'
797
- sheet.setCellMeta(groupParent, REPORT_META_NAMESPACE, customerGroup)
798
-
799
- const amount = createReportBinding(ORDERS_DATASET, 'amount')
800
- amount.rowParent = groupParent
801
- sheet.setCellMeta(listAddr, REPORT_META_NAMESPACE, amount)
802
-
803
- const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
804
- subtotal.preset = 'subtotal'
805
- subtotal.aggregate = 'sum'
806
- subtotal.expand = 'none'
807
- subtotal.rowParent = listAddr
808
- sheet.setCellMeta({ row: 2, col: 2 }, REPORT_META_NAMESPACE, subtotal)
809
-
810
- const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
811
- expect(cellValue(filled, 5, 2)).toBe(630)
812
- expect(cellValue(filled, 10, 2)).toBe(1050)
813
- })
814
-
815
- it('交叉列小计仅 colParent 时按列分组求和,不是全表合计', () => {
816
- const sheet = new Sheet()
817
- const colGroupAddr = { row: 0, col: 1 }
818
- const rowGroupAddr = { row: 1, col: 0 }
819
-
820
- const categoryGroup = createReportBinding(SALES_MATRIX_DATASET, 'category')
821
- categoryGroup.preset = 'groupHeader'
822
- categoryGroup.aggregate = 'group'
823
- categoryGroup.expand = 'right'
824
- sheet.setCellMeta(colGroupAddr, REPORT_META_NAMESPACE, categoryGroup)
825
-
826
- const regionGroup = createReportBinding(SALES_MATRIX_DATASET, 'region')
827
- regionGroup.preset = 'groupHeader'
828
- regionGroup.aggregate = 'group'
829
- regionGroup.expand = 'down'
830
- sheet.setCellMeta(rowGroupAddr, REPORT_META_NAMESPACE, regionGroup)
831
-
832
- const cross = createReportBinding(SALES_MATRIX_DATASET, 'amount')
833
- cross.preset = 'cross'
834
- cross.aggregate = 'sum'
835
- cross.expand = 'none'
836
- cross.rowParent = rowGroupAddr
837
- cross.colParent = colGroupAddr
838
- sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, cross)
839
-
840
- const colSubtotal = createReportBinding(SALES_MATRIX_DATASET, 'amount')
841
- colSubtotal.preset = 'subtotal'
842
- colSubtotal.aggregate = 'sum'
843
- colSubtotal.expand = 'none'
844
- colSubtotal.colParent = colGroupAddr
845
- sheet.setCellMeta({ row: 2, col: 1 }, REPORT_META_NAMESPACE, colSubtotal)
846
-
847
- const filled = renderReport(sheet.snapshot(), { 'sales-matrix': SALES_MATRIX_ROWS })
848
- const officeTotal = SALES_MATRIX_ROWS.filter((row) => row.category === '办公设备').reduce(
849
- (sum, row) => sum + (row.amount as number),
850
- 0
851
- )
852
- const grandTotal = SALES_MATRIX_ROWS.reduce((sum, row) => sum + (row.amount as number), 0)
853
- expect(cellValue(filled, 5, 1)).toBe(officeTotal)
854
- expect(cellValue(filled, 5, 1)).not.toBe(grandTotal)
855
- })
856
- })