@veltra/sheet 2.4.0 → 2.5.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.
Files changed (142) hide show
  1. package/dist/index.d.ts +2 -19
  2. package/dist/index.js +2 -15
  3. package/dist/types/index.d.ts +1 -3
  4. package/package.json +2 -3
  5. package/src/index.ts +1 -27
  6. package/src/types/index.ts +0 -2
  7. package/dist/components/report/binding-badge.js +0 -118
  8. package/dist/components/report/designer/cell-coords.js +0 -46
  9. package/dist/components/report/designer/col-widths.js +0 -29
  10. package/dist/components/report/designer/conditional-rules/helpers.js +0 -139
  11. package/dist/components/report/designer/drill-dialog.js +0 -258
  12. package/dist/components/report/designer/drop-highlight-overlay.js +0 -76
  13. package/dist/components/report/designer/float-panel.js +0 -434
  14. package/dist/components/report/designer/role.js +0 -17
  15. package/dist/components/report/designer/rule-preview.js +0 -33
  16. package/dist/components/report/designer/rule-row.js +0 -253
  17. package/dist/components/report/designer/rules-dialog.js +0 -118
  18. package/dist/components/report/designer/topology-overlay.js +0 -122
  19. package/dist/components/report/designer/topology.js +0 -64
  20. package/dist/components/report/designer/use-grid-overlay.js +0 -75
  21. package/dist/components/report/designer-hub.js +0 -177
  22. package/dist/components/report/field-panel-helpers.js +0 -33
  23. package/dist/components/report/field-panel.js +0 -132
  24. package/dist/components/report/filter-bar.js +0 -74
  25. package/dist/components/report/hub-connection-form.js +0 -145
  26. package/dist/components/report/hub-dataset-editor.js +0 -409
  27. package/dist/components/report/preview-grid-size.js +0 -32
  28. package/dist/components/report/print.js +0 -39
  29. package/dist/components/report/report-designer.js +0 -428
  30. package/dist/components/report/report-viewer.js +0 -253
  31. package/dist/components/report/style.d.ts +0 -17
  32. package/dist/components/report/style.js +0 -18
  33. package/dist/components/report/style2.css +0 -965
  34. package/dist/components/report/use-report-designer.js +0 -387
  35. package/dist/components/report/use-report-viewer.js +0 -140
  36. package/dist/report/binding.d.ts +0 -83
  37. package/dist/report/binding.js +0 -334
  38. package/dist/report/connector.d.ts +0 -66
  39. package/dist/report/connector.js +0 -159
  40. package/dist/report/drill.d.ts +0 -48
  41. package/dist/report/drill.js +0 -78
  42. package/dist/report/export-xlsx.d.ts +0 -13
  43. package/dist/report/export-xlsx.js +0 -13
  44. package/dist/report/filter-bar.d.ts +0 -10
  45. package/dist/report/filter-bar.js +0 -31
  46. package/dist/report/index.d.ts +0 -14
  47. package/dist/report/params.d.ts +0 -13
  48. package/dist/report/params.js +0 -61
  49. package/dist/report/print.d.ts +0 -23
  50. package/dist/report/print.js +0 -197
  51. package/dist/report/render/aggregate.js +0 -193
  52. package/dist/report/render/builder.d.ts +0 -12
  53. package/dist/report/render/builder.js +0 -139
  54. package/dist/report/render/coordinate.d.ts +0 -25
  55. package/dist/report/render/coordinate.js +0 -733
  56. package/dist/report/render/index.d.ts +0 -14
  57. package/dist/report/render/index.js +0 -21
  58. package/dist/report/render/style-resolver.js +0 -50
  59. package/dist/report/render/template-index.d.ts +0 -24
  60. package/dist/report/render/template-index.js +0 -137
  61. package/dist/report/rules/evaluate.d.ts +0 -25
  62. package/dist/report/rules/evaluate.js +0 -82
  63. package/dist/report/rules/index.d.ts +0 -2
  64. package/dist/report/template.d.ts +0 -63
  65. package/dist/report/template.js +0 -83
  66. package/dist/report/types.d.ts +0 -103
  67. package/dist/types/report-designer.d.ts +0 -51
  68. package/dist/types/report-viewer.d.ts +0 -57
  69. package/src/components/report/__test__/binding-badge.test.ts +0 -92
  70. package/src/components/report/__test__/conditional-rules-helpers.test.ts +0 -88
  71. package/src/components/report/__test__/field-panel-helpers.test.ts +0 -65
  72. package/src/components/report/__test__/float-panel-position.test.ts +0 -45
  73. package/src/components/report/__test__/preview-grid-size.test.ts +0 -63
  74. package/src/components/report/__test__/report-designer.test.ts +0 -559
  75. package/src/components/report/__test__/report-drill.test.ts +0 -692
  76. package/src/components/report/__test__/report-viewer.test.ts +0 -400
  77. package/src/components/report/__test__/topology.test.ts +0 -143
  78. package/src/components/report/__test__/use-report-designer.test.ts +0 -804
  79. package/src/components/report/binding-badge.ts +0 -122
  80. package/src/components/report/designer/cell-coords.ts +0 -95
  81. package/src/components/report/designer/col-widths.ts +0 -56
  82. package/src/components/report/designer/conditional-rules/helpers.ts +0 -189
  83. package/src/components/report/designer/drill-dialog.vue +0 -245
  84. package/src/components/report/designer/drop-highlight-overlay.vue +0 -75
  85. package/src/components/report/designer/float-panel.vue +0 -444
  86. package/src/components/report/designer/role.ts +0 -32
  87. package/src/components/report/designer/rule-preview.vue +0 -40
  88. package/src/components/report/designer/rule-row.vue +0 -252
  89. package/src/components/report/designer/rules-dialog.vue +0 -110
  90. package/src/components/report/designer/topology-overlay.vue +0 -128
  91. package/src/components/report/designer/topology.ts +0 -124
  92. package/src/components/report/designer/use-grid-overlay.ts +0 -114
  93. package/src/components/report/designer-hub.vue +0 -198
  94. package/src/components/report/field-panel-helpers.ts +0 -44
  95. package/src/components/report/field-panel.vue +0 -136
  96. package/src/components/report/filter-bar.vue +0 -77
  97. package/src/components/report/hub-connection-form.vue +0 -124
  98. package/src/components/report/hub-dataset-editor.vue +0 -387
  99. package/src/components/report/index.ts +0 -2
  100. package/src/components/report/preview-grid-size.ts +0 -33
  101. package/src/components/report/print.ts +0 -44
  102. package/src/components/report/report-designer.vue +0 -454
  103. package/src/components/report/report-viewer.vue +0 -289
  104. package/src/components/report/style-designer.scss +0 -653
  105. package/src/components/report/style-hub.scss +0 -292
  106. package/src/components/report/style-viewer.scss +0 -144
  107. package/src/components/report/style.scss +0 -3
  108. package/src/components/report/style.ts +0 -20
  109. package/src/components/report/use-report-designer.ts +0 -603
  110. package/src/components/report/use-report-viewer.ts +0 -227
  111. package/src/report/__test__/binding.test.ts +0 -320
  112. package/src/report/__test__/connector.test.ts +0 -239
  113. package/src/report/__test__/drill.test.ts +0 -194
  114. package/src/report/__test__/export-xlsx.test.ts +0 -96
  115. package/src/report/__test__/filter-bar.test.ts +0 -58
  116. package/src/report/__test__/params.test.ts +0 -156
  117. package/src/report/__test__/print.test.ts +0 -170
  118. package/src/report/__test__/render.test.ts +0 -856
  119. package/src/report/__test__/rules.test.ts +0 -131
  120. package/src/report/__test__/template.test.ts +0 -366
  121. package/src/report/binding.ts +0 -474
  122. package/src/report/connector.ts +0 -206
  123. package/src/report/drill.ts +0 -112
  124. package/src/report/export-xlsx.ts +0 -15
  125. package/src/report/filter-bar.ts +0 -36
  126. package/src/report/index.ts +0 -16
  127. package/src/report/params.ts +0 -96
  128. package/src/report/print.ts +0 -234
  129. package/src/report/render/__test__/aggregate.test.ts +0 -16
  130. package/src/report/render/__test__/coordinate.test.ts +0 -405
  131. package/src/report/render/aggregate.ts +0 -289
  132. package/src/report/render/builder.ts +0 -211
  133. package/src/report/render/coordinate.ts +0 -1239
  134. package/src/report/render/index.ts +0 -30
  135. package/src/report/render/style-resolver.ts +0 -68
  136. package/src/report/render/template-index.ts +0 -200
  137. package/src/report/rules/evaluate.ts +0 -113
  138. package/src/report/rules/index.ts +0 -7
  139. package/src/report/template.ts +0 -145
  140. package/src/report/types.ts +0 -116
  141. package/src/types/report-designer.ts +0 -55
  142. 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
- })