@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,405 +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 type { DatasetRecords, ReportBinding } from '../../types'
6
- import {
7
- computeExpansionLayout,
8
- enumerateExpansionInstances,
9
- physicalColSpan,
10
- physicalRowSpan,
11
- placementsAt
12
- } from '../coordinate'
13
- import { TemplateStructureError, buildTemplateIndex } from '../template-index'
14
-
15
- // ---- fixtures ----
16
-
17
- const ORDERS: DatasetRecords = {
18
- orders: [
19
- { customer: '甲', region: '华东', orderNo: 'O-1', amount: 100 },
20
- { customer: '甲', region: '华东', orderNo: 'O-2', amount: 200 },
21
- { customer: '乙', region: '华南', orderNo: 'O-3', amount: 300 },
22
- { customer: '丙', region: '华北', orderNo: 'O-4', amount: 400 }
23
- ]
24
- }
25
-
26
- const MATRIX: DatasetRecords = {
27
- sales: [
28
- { region: '华东', category: 'A', amount: 10 },
29
- { region: '华东', category: 'B', amount: 20 },
30
- { region: '华南', category: 'A', amount: 30 },
31
- { region: '华南', category: 'B', amount: 40 }
32
- ]
33
- }
34
-
35
- const MULTI_DS: DatasetRecords = { orders: ORDERS.orders!, customers: [{ id: 'C1' }, { id: 'C2' }] }
36
-
37
- function bind(sheet: Sheet, addr: { row: number; col: number }, binding: ReportBinding): void {
38
- sheet.setCellMeta(addr, REPORT_META_NAMESPACE, binding)
39
- }
40
-
41
- function snapshot(sheet: Sheet) {
42
- return sheet.snapshot()
43
- }
44
-
45
- function layoutOf(sheet: Sheet, data: DatasetRecords = ORDERS) {
46
- const index = buildTemplateIndex(snapshot(sheet))
47
- return computeExpansionLayout(index, data)
48
- }
49
-
50
- function placementRange(
51
- layout: ReturnType<typeof layoutOf>,
52
- row: number,
53
- col: number,
54
- listIndex = 0
55
- ) {
56
- const items = placementsAt(layout, { row, col })
57
- const hit = items[listIndex] ?? items[0]
58
- expect(hit, `逻辑格 R${row}C${col} 无映射`).toBeDefined()
59
- return hit!.physical
60
- }
61
-
62
- // ---- template-index ----
63
-
64
- describe('buildTemplateIndex', () => {
65
- it('父格不存在时抛可读错误', () => {
66
- const sheet = new Sheet()
67
- const binding = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
68
- binding.rowParent = { row: 0, col: 9 }
69
- bind(sheet, { row: 1, col: 0 }, binding)
70
-
71
- expect(() => buildTemplateIndex(snapshot(sheet))).toThrow(TemplateStructureError)
72
- expect(() => buildTemplateIndex(snapshot(sheet))).toThrow(/J1/)
73
- })
74
-
75
- it('父链成环时抛可读错误', () => {
76
- const sheet = new Sheet()
77
- const a = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
78
- const b = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'region')
79
- a.rowParent = { row: 2, col: 0 }
80
- b.rowParent = { row: 1, col: 0 }
81
- bind(sheet, { row: 1, col: 0 }, a)
82
- bind(sheet, { row: 2, col: 0 }, b)
83
-
84
- expect(() => buildTemplateIndex(snapshot(sheet))).toThrow(/父链存在环/)
85
- })
86
- })
87
-
88
- // ---- enumerateExpansionInstances ----
89
-
90
- describe('enumerateExpansionInstances', () => {
91
- const groupBinding: ReportBinding = {
92
- dataset: 'orders',
93
- field: 'customer',
94
- expand: 'down',
95
- aggregate: 'group'
96
- }
97
-
98
- it('group 取去重值数', () => {
99
- const instances = enumerateExpansionInstances(groupBinding, ORDERS, {})
100
- expect(instances).toHaveLength(3)
101
- expect(instances.map((i) => i.value)).toEqual(['甲', '乙', '丙'])
102
- })
103
-
104
- it('list 取记录数', () => {
105
- const listBinding: ReportBinding = {
106
- dataset: 'orders',
107
- field: 'orderNo',
108
- expand: 'down',
109
- aggregate: 'list'
110
- }
111
- expect(enumerateExpansionInstances(listBinding, ORDERS, {})).toHaveLength(4)
112
- })
113
-
114
- it('expand none 恒为 1 个实例', () => {
115
- const noneBinding: ReportBinding = {
116
- dataset: 'orders',
117
- field: 'amount',
118
- expand: 'none',
119
- aggregate: 'sum'
120
- }
121
- expect(enumerateExpansionInstances(noneBinding, ORDERS, {})).toHaveLength(1)
122
- })
123
-
124
- it('空数据集 group 产出 0 实例', () => {
125
- expect(enumerateExpansionInstances(groupBinding, { orders: [] }, {})).toHaveLength(0)
126
- })
127
- })
128
-
129
- // ---- coordinate geometry ----
130
-
131
- describe('computeExpansionLayout', () => {
132
- it('单级纵向:分组头 merge 跨度 = 明细行数,静态表头不被推移', () => {
133
- const sheet = new Sheet()
134
- sheet.setCells([
135
- { addr: { row: 0, col: 0 }, data: { v: '客户' } },
136
- { addr: { row: 0, col: 1 }, data: { v: '订单号' } }
137
- ])
138
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
139
- group.aggregate = 'group'
140
- group.expand = 'down'
141
- bind(sheet, { row: 1, col: 0 }, group)
142
-
143
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
144
- detail.aggregate = 'list'
145
- detail.expand = 'down'
146
- detail.rowParent = { row: 1, col: 0 }
147
- bind(sheet, { row: 1, col: 1 }, detail)
148
-
149
- const layout = layoutOf(sheet)
150
- // 表头 1 + 甲2 + 乙1 + 丙1 = 5 行
151
- expect(layout.rowCount).toBe(5)
152
-
153
- const header = placementRange(layout, 0, 0)
154
- expect(header.start).toEqual({ row: 0, col: 0 })
155
-
156
- const groupA = placementsAt(layout, { row: 1, col: 0 }).find((p) => p.rowPath[0] === 0)!
157
- expect(physicalRowSpan(groupA)).toBe(2)
158
- expect(groupA.physical.start.row).toBe(1)
159
-
160
- const groupB = placementsAt(layout, { row: 1, col: 0 }).find((p) => p.rowPath[0] === 1)!
161
- expect(physicalRowSpan(groupB)).toBe(1)
162
- expect(groupB.physical.start.row).toBe(3)
163
- })
164
-
165
- it('多级纵向嵌套:子分组在父实例内展开', () => {
166
- const sheet = new Sheet()
167
- const data: DatasetRecords = {
168
- orders: [
169
- { region: '华东', customer: '甲', orderNo: '1' },
170
- { region: '华东', customer: '甲', orderNo: '2' },
171
- { region: '华东', customer: '乙', orderNo: '3' },
172
- { region: '华南', customer: '丙', orderNo: '4' }
173
- ]
174
- }
175
-
176
- const region = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'region')
177
- region.aggregate = 'group'
178
- region.expand = 'down'
179
- bind(sheet, { row: 1, col: 0 }, region)
180
-
181
- const customer = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
182
- customer.aggregate = 'group'
183
- customer.expand = 'down'
184
- customer.rowParent = { row: 1, col: 0 }
185
- bind(sheet, { row: 2, col: 0 }, customer)
186
-
187
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
188
- detail.aggregate = 'list'
189
- detail.expand = 'down'
190
- detail.rowParent = { row: 2, col: 0 }
191
- bind(sheet, { row: 2, col: 1 }, detail)
192
-
193
- const layout = layoutOf(sheet, data)
194
- // 华东(甲2+乙1) + 华南(丙1) = 3 组块 × 各自明细 = 4 明细行
195
- expect(layout.rowCount).toBe(4)
196
-
197
- const eastCustomer = placementsAt(layout, { row: 2, col: 0 }).find(
198
- (p) => p.rowPath[0] === 0 && p.rowPath[1] === 0
199
- )!
200
- expect(physicalRowSpan(eastCustomer)).toBe(2)
201
- })
202
-
203
- it('单级横向:列分组向右延展', () => {
204
- const sheet = new Sheet()
205
- sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '品类' } }])
206
-
207
- const category = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'category')
208
- category.aggregate = 'group'
209
- category.expand = 'right'
210
- bind(sheet, { row: 0, col: 1 }, category)
211
-
212
- const layout = layoutOf(sheet, MATRIX)
213
- expect(layout.colCount).toBe(3) // 标题 + A/B 两列
214
- const headers = placementsAt(layout, { row: 0, col: 1 })
215
- expect(headers).toHaveLength(2)
216
- expect(headers[0]!.physical.start.col).toBe(1)
217
- expect(headers[1]!.physical.start.col).toBe(2)
218
- expect(physicalColSpan(headers[0]!)).toBe(1)
219
- })
220
-
221
- it('多级列头:colParent 链逐级向右嵌套', () => {
222
- const sheet = new Sheet()
223
- const data: DatasetRecords = {
224
- sales: [
225
- { year: '2024', quarter: 'Q1', amount: 1 },
226
- { year: '2024', quarter: 'Q2', amount: 2 },
227
- { year: '2025', quarter: 'Q1', amount: 3 }
228
- ]
229
- }
230
-
231
- const year = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'year')
232
- year.aggregate = 'group'
233
- year.expand = 'right'
234
- bind(sheet, { row: 0, col: 0 }, year)
235
-
236
- const quarter = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'quarter')
237
- quarter.aggregate = 'group'
238
- quarter.expand = 'right'
239
- quarter.colParent = { row: 0, col: 0 }
240
- bind(sheet, { row: 1, col: 0 }, quarter)
241
-
242
- const layout = layoutOf(sheet, data)
243
- const yearPlacements = placementsAt(layout, { row: 0, col: 0 })
244
- expect(yearPlacements).toHaveLength(2)
245
- expect(physicalColSpan(yearPlacements[0]!)).toBe(2)
246
- expect(physicalColSpan(yearPlacements[1]!)).toBe(1)
247
-
248
- const quarterPlacements = placementsAt(layout, { row: 1, col: 0 })
249
- expect(quarterPlacements).toHaveLength(3)
250
- })
251
-
252
- it('行列同时展开:交叉格按双父实例落位', () => {
253
- const sheet = new Sheet()
254
- sheet.setCells([
255
- { addr: { row: 0, col: 0 }, data: { v: '标题' } },
256
- { addr: { row: 2, col: 0 }, data: { v: '合计' } }
257
- ])
258
-
259
- const category = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'category')
260
- category.aggregate = 'group'
261
- category.expand = 'right'
262
- bind(sheet, { row: 0, col: 1 }, category)
263
-
264
- const region = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'region')
265
- region.aggregate = 'group'
266
- region.expand = 'down'
267
- bind(sheet, { row: 1, col: 0 }, region)
268
-
269
- const cross = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'amount')
270
- cross.aggregate = 'sum'
271
- cross.expand = 'none'
272
- cross.rowParent = { row: 1, col: 0 }
273
- cross.colParent = { row: 0, col: 1 }
274
- bind(sheet, { row: 1, col: 1 }, cross)
275
-
276
- const layout = layoutOf(sheet, MATRIX)
277
- expect(layout.rowCount).toBe(4) // 表头 + 2 地区 + 合计
278
- expect(layout.colCount).toBe(3) // 角 + 2 品类
279
-
280
- const crossCells = placementsAt(layout, { row: 1, col: 1 })
281
- expect(crossCells).toHaveLength(4)
282
- expect(crossCells.map((p) => p.physical.start)).toEqual(
283
- expect.arrayContaining([
284
- { row: 1, col: 1 },
285
- { row: 1, col: 2 },
286
- { row: 2, col: 1 },
287
- { row: 2, col: 2 }
288
- ])
289
- )
290
- })
291
-
292
- it('mergeSpan: false 时扩展格不合并,逐格占位', () => {
293
- const sheet = new Sheet()
294
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
295
- group.aggregate = 'group'
296
- group.expand = 'down'
297
- group.mergeSpan = false
298
- bind(sheet, { row: 1, col: 0 }, group)
299
-
300
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
301
- detail.aggregate = 'list'
302
- detail.expand = 'down'
303
- detail.rowParent = { row: 1, col: 0 }
304
- bind(sheet, { row: 1, col: 1 }, detail)
305
-
306
- const layout = layoutOf(sheet)
307
- const groupCells = placementsAt(layout, { row: 1, col: 0 })
308
- expect(groupCells.every((p) => !p.mergeSpan)).toBe(true)
309
- expect(groupCells.every((p) => physicalRowSpan(p) === 1)).toBe(true)
310
- expect(groupCells).toHaveLength(4)
311
- })
312
-
313
- it('静态格在扩展带内随明细重复占位', () => {
314
- const sheet = new Sheet()
315
- sheet.setCells([{ addr: { row: 1, col: 2 }, data: { v: '备注' } }])
316
-
317
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
318
- group.aggregate = 'group'
319
- group.expand = 'down'
320
- bind(sheet, { row: 1, col: 0 }, group)
321
-
322
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
323
- detail.aggregate = 'list'
324
- detail.expand = 'down'
325
- detail.rowParent = { row: 1, col: 0 }
326
- bind(sheet, { row: 1, col: 1 }, detail)
327
-
328
- const layout = layoutOf(sheet)
329
- const notes = placementsAt(layout, { row: 1, col: 2 })
330
- expect(notes).toHaveLength(4)
331
- expect(notes.map((p) => p.physical.start.row)).toEqual([0, 1, 2, 3])
332
- })
333
-
334
- it('空展开:扩展格 0 跨度,子树不占位,静态表头仍映射', () => {
335
- const sheet = new Sheet()
336
- sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '客户' } }])
337
-
338
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
339
- group.aggregate = 'group'
340
- group.expand = 'down'
341
- bind(sheet, { row: 1, col: 0 }, group)
342
-
343
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
344
- detail.aggregate = 'list'
345
- detail.expand = 'down'
346
- detail.rowParent = { row: 1, col: 0 }
347
- bind(sheet, { row: 1, col: 1 }, detail)
348
-
349
- const layout = layoutOf(sheet, { orders: [] })
350
- expect(layout.rowCount).toBe(1)
351
- expect(placementsAt(layout, { row: 0, col: 0 })).toHaveLength(1)
352
- expect(placementsAt(layout, { row: 1, col: 0 })).toHaveLength(0)
353
- })
354
-
355
- it('跨数据集:父格过滤不同数据集时不串联', () => {
356
- const sheet = new Sheet()
357
- sheet.setCells([{ addr: { row: 0, col: 5 }, data: { v: '客户ID' } }])
358
-
359
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
360
- group.aggregate = 'group'
361
- group.expand = 'down'
362
- bind(sheet, { row: 1, col: 0 }, group)
363
-
364
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
365
- detail.aggregate = 'list'
366
- detail.expand = 'down'
367
- detail.rowParent = { row: 1, col: 0 }
368
- bind(sheet, { row: 1, col: 1 }, detail)
369
-
370
- const customers = createReportBinding({ id: 'customers', label: 'c', fields: [] }, 'id')
371
- customers.aggregate = 'list'
372
- customers.expand = 'down'
373
- bind(sheet, { row: 4, col: 5 }, customers)
374
-
375
- const layout = layoutOf(sheet, MULTI_DS)
376
- // 订单 4 明细行 + 表头 1 + 客户列表 2 行
377
- expect(layout.rowCount).toBe(4 + 1 + 2)
378
- const customerIds = placementsAt(layout, { row: 4, col: 5 })
379
- expect(customerIds).toHaveLength(2)
380
- expect(customerIds[0]!.physical.start.row).toBe(5)
381
- })
382
-
383
- it('模板合并区域映射到物理表头行', () => {
384
- const sheet = new Sheet()
385
- sheet.setCells([
386
- { addr: { row: 0, col: 0 }, data: { v: '客户' } },
387
- { addr: { row: 0, col: 1 }, data: { v: '订单号' } }
388
- ])
389
- sheet.mergeCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 1 } })
390
-
391
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
392
- group.aggregate = 'group'
393
- group.expand = 'down'
394
- bind(sheet, { row: 1, col: 0 }, group)
395
-
396
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
397
- detail.aggregate = 'list'
398
- detail.expand = 'down'
399
- detail.rowParent = { row: 1, col: 0 }
400
- bind(sheet, { row: 1, col: 1 }, detail)
401
-
402
- const layout = layoutOf(sheet)
403
- expect(layout.mappedMerges).toEqual([{ start: { row: 0, col: 0 }, end: { row: 0, col: 1 } }])
404
- })
405
- })
@@ -1,289 +0,0 @@
1
- import type { CellAddress } from '@veltra/sheet-core'
2
-
3
- import type { DatasetRecords, ReportAggregate, ReportBinding } from '../types'
4
- import { enumerateExpansionInstances } from './coordinate'
5
- import type { PhysicalPlacement } from './coordinate'
6
- import type { TemplateIndex } from './template-index'
7
-
8
- function filterRows(
9
- rows: Record<string, unknown>[],
10
- filter: Record<string, unknown>
11
- ): Record<string, unknown>[] {
12
- if (Object.keys(filter).length === 0) return rows
13
- return rows.filter((row) =>
14
- Object.entries(filter).every(([field, value]) => row[field] === value)
15
- )
16
- }
17
-
18
- /** 对数值集求聚合;`avg` 空集返回 `undefined`,`sum` 空集仍为 `0` */
19
- export function aggregateField(
20
- rows: Record<string, unknown>[],
21
- field: string,
22
- aggregate: ReportAggregate
23
- ): unknown {
24
- if (aggregate === 'count') return rows.length
25
- if (aggregate === 'list' || aggregate === 'group') {
26
- return rows[0]?.[field]
27
- }
28
-
29
- const numbers = rows
30
- .map((row) => row[field])
31
- .map((value) => (typeof value === 'number' ? value : Number(value)))
32
- .filter((value) => Number.isFinite(value))
33
-
34
- if (numbers.length === 0) {
35
- if (aggregate === 'avg') return undefined
36
- return 0
37
- }
38
-
39
- const sum = numbers.reduce((acc, value) => acc + value, 0)
40
- if (aggregate === 'avg') return sum / numbers.length
41
- if (aggregate === 'max') return Math.max(...numbers)
42
- if (aggregate === 'min') return Math.min(...numbers)
43
- return sum
44
- }
45
-
46
- function isRowExpanding(binding: ReportBinding): boolean {
47
- return (
48
- binding.expand === 'down' && (binding.aggregate === 'group' || binding.aggregate === 'list')
49
- )
50
- }
51
-
52
- function isColExpanding(binding: ReportBinding): boolean {
53
- return (
54
- binding.expand === 'right' && (binding.aggregate === 'group' || binding.aggregate === 'list')
55
- )
56
- }
57
-
58
- function isReductionAggregate(aggregate: ReportAggregate): boolean {
59
- return (
60
- aggregate === 'sum' ||
61
- aggregate === 'avg' ||
62
- aggregate === 'count' ||
63
- aggregate === 'max' ||
64
- aggregate === 'min'
65
- )
66
- }
67
-
68
- function rowExpandingAncestors(
69
- binding: ReportBinding,
70
- index: TemplateIndex,
71
- groupsOnly: boolean
72
- ): Array<{ addr: CellAddress; binding: ReportBinding }> {
73
- const chain: Array<{ addr: CellAddress; binding: ReportBinding }> = []
74
- let current = binding.rowParent
75
- while (current) {
76
- const parentBinding = index.bindingAt(current)
77
- if (parentBinding) chain.unshift({ addr: current, binding: parentBinding })
78
- current = parentBinding?.rowParent
79
- }
80
- return chain.filter((item) => {
81
- if (!isRowExpanding(item.binding)) return false
82
- if (groupsOnly && item.binding.aggregate !== 'group') return false
83
- return true
84
- })
85
- }
86
-
87
- function colExpandingAncestors(
88
- binding: ReportBinding,
89
- index: TemplateIndex,
90
- groupsOnly: boolean
91
- ): Array<{ addr: CellAddress; binding: ReportBinding }> {
92
- const chain: Array<{ addr: CellAddress; binding: ReportBinding }> = []
93
- let current = binding.colParent
94
- while (current) {
95
- const parentBinding = index.bindingAt(current)
96
- if (parentBinding) chain.unshift({ addr: current, binding: parentBinding })
97
- current = parentBinding?.colParent
98
- }
99
- return chain.filter((item) => {
100
- if (!isColExpanding(item.binding)) return false
101
- if (groupsOnly && item.binding.aggregate !== 'group') return false
102
- return true
103
- })
104
- }
105
-
106
- function mergeFilters(
107
- rowFilter: Record<string, unknown>,
108
- colFilter: Record<string, unknown>
109
- ): Record<string, unknown> {
110
- return { ...rowFilter, ...colFilter }
111
- }
112
-
113
- function resolveAncestorFilters(
114
- binding: ReportBinding,
115
- placement: PhysicalPlacement,
116
- index: TemplateIndex,
117
- data: DatasetRecords
118
- ): Record<string, unknown> {
119
- const rowFilter: Record<string, unknown> = {}
120
- const colFilter: Record<string, unknown> = {}
121
- let parentFilter: Record<string, unknown> = {}
122
- const groupsOnly = isReductionAggregate(binding.aggregate)
123
-
124
- const rowAncestors = rowExpandingAncestors(binding, index, groupsOnly)
125
- for (let i = 0; i < rowAncestors.length; i++) {
126
- const ancestor = rowAncestors[i]!
127
- if (ancestor.binding.dataset !== binding.dataset) continue
128
- const instances = enumerateExpansionInstances(ancestor.binding, data, parentFilter)
129
- const pathIndex = placement.rowPath[i]
130
- if (pathIndex === undefined) continue
131
- const instance = instances[pathIndex]
132
- if (!instance) continue
133
- Object.assign(rowFilter, instance.filter)
134
- parentFilter = instance.filter
135
- }
136
-
137
- parentFilter = {}
138
- const colAncestors = colExpandingAncestors(binding, index, groupsOnly)
139
- for (let i = 0; i < colAncestors.length; i++) {
140
- const ancestor = colAncestors[i]!
141
- if (ancestor.binding.dataset !== binding.dataset) continue
142
- const instances = enumerateExpansionInstances(ancestor.binding, data, parentFilter)
143
- const pathIndex = placement.colPath[i]
144
- if (pathIndex === undefined) continue
145
- const instance = instances[pathIndex]
146
- if (!instance) continue
147
- Object.assign(colFilter, instance.filter)
148
- parentFilter = instance.filter
149
- }
150
-
151
- return mergeFilters(rowFilter, colFilter)
152
- }
153
-
154
- function resolveOwnGroupValue(
155
- binding: ReportBinding,
156
- placement: PhysicalPlacement,
157
- index: TemplateIndex,
158
- data: DatasetRecords
159
- ): unknown {
160
- const rowAncestors = rowExpandingAncestors(binding, index, false)
161
- const colAncestors = colExpandingAncestors(binding, index, false)
162
- let parentFilter: Record<string, unknown> = {}
163
-
164
- for (let i = 0; i < rowAncestors.length; i++) {
165
- const ancestor = rowAncestors[i]!
166
- if (ancestor.binding.dataset !== binding.dataset) continue
167
- const instances = enumerateExpansionInstances(ancestor.binding, data, parentFilter)
168
- const pathIndex = placement.rowPath[i]
169
- if (pathIndex === undefined) continue
170
- const instance = instances[pathIndex]
171
- if (instance) parentFilter = instance.filter
172
- }
173
-
174
- for (let i = 0; i < colAncestors.length; i++) {
175
- const ancestor = colAncestors[i]!
176
- if (ancestor.binding.dataset !== binding.dataset) continue
177
- const instances = enumerateExpansionInstances(ancestor.binding, data, parentFilter)
178
- const pathIndex = placement.colPath[i]
179
- if (pathIndex === undefined) continue
180
- const instance = instances[pathIndex]
181
- if (instance) parentFilter = instance.filter
182
- }
183
-
184
- const instances = enumerateExpansionInstances(binding, data, parentFilter)
185
- const pathIndex = isRowExpanding(binding) ? placement.rowPath.at(-1) : placement.colPath.at(-1)
186
- if (pathIndex === undefined) return undefined
187
- return instances[pathIndex]?.value
188
- }
189
-
190
- /** 按分组字段建索引,避免交叉展开重复 filterRows */
191
- export class AggregateIndex {
192
- private readonly datasets = new Map<string, Record<string, unknown>[]>()
193
- private readonly filterCache = new Map<string, Record<string, unknown>[]>()
194
-
195
- constructor(data: DatasetRecords) {
196
- for (const [datasetId, rows] of Object.entries(data)) {
197
- this.datasets.set(datasetId, rows)
198
- }
199
- }
200
-
201
- rowsFor(dataset: string, filter: Record<string, unknown>): Record<string, unknown>[] {
202
- const cacheKey = `${dataset}:${JSON.stringify(filter)}`
203
- const cached = this.filterCache.get(cacheKey)
204
- if (cached) return cached
205
- const rows = filterRows(this.datasets.get(dataset) ?? [], filter)
206
- this.filterCache.set(cacheKey, rows)
207
- return rows
208
- }
209
-
210
- aggregate(
211
- dataset: string,
212
- filter: Record<string, unknown>,
213
- field: string,
214
- aggregate: ReportAggregate
215
- ): unknown {
216
- const rows = this.rowsFor(dataset, filter)
217
- if (aggregate === 'list') {
218
- return rows[0]?.[field]
219
- }
220
- return aggregateField(rows, field, aggregate)
221
- }
222
- }
223
-
224
- /** 解析单个物理落点的绑定取值 */
225
- export function resolvePlacementValue(
226
- placement: PhysicalPlacement,
227
- index: TemplateIndex,
228
- data: DatasetRecords,
229
- aggregateIndex: AggregateIndex
230
- ): unknown {
231
- const binding = placement.binding
232
- if (!binding) return undefined
233
-
234
- const filter = resolveAncestorFilters(binding, placement, index, data)
235
- const rows = aggregateIndex.rowsFor(binding.dataset, filter)
236
-
237
- if (binding.aggregate === 'group') {
238
- return filter[binding.field] ?? resolveOwnGroupValue(binding, placement, index, data)
239
- }
240
-
241
- if (binding.aggregate === 'list') {
242
- return rows[placement.listIndex]?.[binding.field]
243
- }
244
-
245
- return aggregateField(rows, binding.field, binding.aggregate)
246
- }
247
-
248
- /** 解析 `list` 明细落点对应的源数据行;非 list 绑定无单条记录上下文 */
249
- export function resolvePlacementRecord(
250
- placement: PhysicalPlacement,
251
- index: TemplateIndex,
252
- data: DatasetRecords,
253
- aggregateIndex: AggregateIndex
254
- ): Record<string, unknown> | undefined {
255
- const binding = placement.binding
256
- if (!binding || binding.aggregate !== 'list') return undefined
257
-
258
- const filter = resolveAncestorFilters(binding, placement, index, data)
259
- const rows = aggregateIndex.rowsFor(binding.dataset, filter)
260
- return rows[placement.listIndex]
261
- }
262
-
263
- /**
264
- * 解析落点的上下文记录(下钻字段映射用):
265
- * `list` 明细取源数据行;分组/汇总格取祖先分组过滤值合成的上下文
266
- * (分组格含自身分组值,如地区分组格 → `{ region: '华东' }`;
267
- * 汇总聚合格如 sum/avg/count/max/min 也将本格聚合计算值并入上下文,如 `{ region: '华东', total: 300 }`)。
268
- */
269
- export function resolvePlacementContext(
270
- placement: PhysicalPlacement,
271
- index: TemplateIndex,
272
- data: DatasetRecords,
273
- aggregateIndex: AggregateIndex
274
- ): Record<string, unknown> | undefined {
275
- const binding = placement.binding
276
- if (!binding) return undefined
277
- if (binding.aggregate === 'list') {
278
- return resolvePlacementRecord(placement, index, data, aggregateIndex)
279
- }
280
-
281
- const filter = resolveAncestorFilters(binding, placement, index, data)
282
- if (binding.aggregate === 'group') {
283
- const value = filter[binding.field] ?? resolveOwnGroupValue(binding, placement, index, data)
284
- return value === undefined ? filter : { ...filter, [binding.field]: value }
285
- }
286
-
287
- const value = resolvePlacementValue(placement, index, data, aggregateIndex)
288
- return value === undefined ? filter : { ...filter, [binding.field]: value }
289
- }