@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,804 +0,0 @@
1
- import { Workbook } from '@veltra/sheet-core'
2
- import { describe, expect, it } from 'vitest'
3
- import { effectScope, ref, type EffectScope, type Ref } from 'vue'
4
-
5
- import { REPORT_META_NAMESPACE } from '../../../report/binding'
6
- import type { DataConnection, DataConnector } from '../../../report/connector'
7
- import { IncompatibleTemplateVersionError, type ReportTemplate } from '../../../report/template'
8
- import type {
9
- DatasetField,
10
- ParamValues,
11
- ReportBinding,
12
- ReportDrillConfig
13
- } from '../../../report/types'
14
- import { roleBindingDefaults } from '../designer/role'
15
- import { useReportDesigner, type UseReportDesignerReturn } from '../use-report-designer'
16
-
17
- // ---- 内联 fixtures:stub connector(实现 DataConnector 接口的内存测试夹具)----
18
-
19
- const MYSQL: DataConnection = {
20
- id: 'c1',
21
- label: 'stub MySQL',
22
- type: 'mysql',
23
- host: '127.0.0.1',
24
- port: 3306,
25
- database: 'demo',
26
- username: 'root',
27
- password: ''
28
- }
29
-
30
- const PG: DataConnection = {
31
- id: 'c2',
32
- label: 'stub PG',
33
- type: 'postgresql',
34
- host: '127.0.0.1',
35
- port: 5432,
36
- database: 'demo',
37
- username: 'postgres',
38
- password: ''
39
- }
40
-
41
- const ORDER_FIELDS: DatasetField[] = [
42
- { name: 'customer', label: '客户', type: 'string' },
43
- { name: 'orderNo', label: '订单号', type: 'string' },
44
- { name: 'amount', label: '金额', type: 'number' }
45
- ]
46
-
47
- interface StubCalls {
48
- test: DataConnection[]
49
- describe: Array<{ connection: DataConnection; sql: string }>
50
- query: Array<{ connection: DataConnection; sql: string; values: ParamValues }>
51
- }
52
-
53
- function createStubConnector() {
54
- const calls: StubCalls = { test: [], describe: [], query: [] }
55
- const connector: DataConnector = {
56
- test: (connection) => {
57
- calls.test.push(connection)
58
- return Promise.resolve({ ok: true, data: undefined })
59
- },
60
- describe: (connection, sql) => {
61
- calls.describe.push({ connection, sql })
62
- if (sql.includes('BAD')) {
63
- return Promise.resolve({
64
- ok: false,
65
- error: { code: 'SQL_ERROR', message: 'syntax error near BAD' }
66
- })
67
- }
68
- return Promise.resolve({ ok: true, data: ORDER_FIELDS })
69
- },
70
- query: (connection, sql, values) => {
71
- calls.query.push({ connection, sql, values })
72
- return Promise.resolve({
73
- ok: true,
74
- data: { fields: ORDER_FIELDS, rows: [{ customer: '甲公司', amount: 100 }] }
75
- })
76
- }
77
- }
78
- return { connector, calls }
79
- }
80
-
81
- interface DesignerFixture {
82
- scope: EffectScope
83
- designer: UseReportDesignerReturn
84
- connections: Ref<DataConnection[]>
85
- workbook: Workbook
86
- calls: StubCalls
87
- }
88
-
89
- function createDesigner(
90
- initial: DataConnection[] = [MYSQL],
91
- options?: { template?: ReportTemplate }
92
- ): DesignerFixture {
93
- const { connector, calls } = createStubConnector()
94
- const connections = ref<DataConnection[]>(initial)
95
- const workbook = new Workbook()
96
- const scope = effectScope()
97
- const designer = scope.run(() =>
98
- useReportDesigner({ props: { connector, workbook, template: options?.template }, connections })
99
- )!
100
- return { scope, designer, connections, workbook, calls }
101
- }
102
-
103
- function bindGroupHeader(
104
- designer: UseReportDesignerReturn,
105
- workbook: Workbook,
106
- datasetId: string,
107
- addr: { row: number; col: number },
108
- field = 'customer'
109
- ): void {
110
- designer.bindField(datasetId, field, addr)
111
- workbook.activeSheet.selectCell(addr)
112
- designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
113
- }
114
-
115
- describe('useReportDesigner:连接 / 数据集 CRUD 与连接器调用', () => {
116
- it('连接 CRUD 全走 v-model 代理;删除连接级联删除其数据集', () => {
117
- const { designer, connections } = createDesigner()
118
-
119
- designer.addConnection(PG)
120
- expect(connections.value).toEqual([MYSQL, PG])
121
-
122
- designer.updateConnection({ ...PG, label: 'renamed' })
123
- expect(connections.value[1]).toEqual({ ...PG, label: 'renamed' })
124
-
125
- const dataset = designer.addDataset('c2')
126
- designer.removeConnection('c2')
127
- expect(connections.value).toEqual([MYSQL])
128
- expect(designer.datasets.value.find((item) => item.id === dataset.id)).toBeUndefined()
129
- })
130
-
131
- it('测试连接透传 connector.test(新建草稿同样可测)', async () => {
132
- const { designer, calls } = createDesigner()
133
- const draft: DataConnection = { ...MYSQL, id: 'draft', host: '10.0.0.1' }
134
- const result = await designer.testConnection(draft)
135
- expect(result).toEqual({ ok: true, data: undefined })
136
- expect(calls.test).toEqual([draft])
137
- })
138
-
139
- it('describeDataset 经连接器解析字段并写入数据集字段缓存;fieldOverrides 在 catalog 层应用', async () => {
140
- const { designer } = createDesigner()
141
- const dataset = designer.addDataset('c1')
142
- designer.updateDataset(dataset.id, {
143
- sql: 'SELECT customer, amount FROM orders',
144
- fieldOverrides: { customer: { label: '客户名称' } }
145
- })
146
-
147
- const result = await designer.describeDataset(dataset.id)
148
- expect(result).toEqual({ ok: true, data: ORDER_FIELDS })
149
- expect(designer.datasets.value[0]!.fields).toEqual(ORDER_FIELDS)
150
-
151
- const catalog = designer.catalog.value
152
- expect(catalog).toHaveLength(1)
153
- expect(catalog[0]!.fields.map((field) => field.label)).toEqual(['客户名称', '订单号', '金额'])
154
- })
155
-
156
- it('describeDataset 业务错误整体透传且不写字段缓存', async () => {
157
- const { designer } = createDesigner()
158
- const dataset = designer.addDataset('c1')
159
- designer.updateDataset(dataset.id, { sql: 'SELECT BAD' })
160
-
161
- const result = await designer.describeDataset(dataset.id)
162
- expect(result).toEqual({
163
- ok: false,
164
- error: { code: 'SQL_ERROR', message: 'syntax error near BAD' }
165
- })
166
- expect(designer.datasets.value[0]!.fields).toBeUndefined()
167
- })
168
-
169
- it('previewDataset 按参数默认值(含元数据覆盖)经连接器取数', async () => {
170
- const { designer, calls } = createDesigner()
171
- const dataset = designer.addDataset('c1')
172
- designer.updateDataset(dataset.id, {
173
- sql: 'SELECT customer FROM orders WHERE customer LIKE ${keyword} AND amount >= ${minAmount}',
174
- paramOverrides: { minAmount: { defaultValue: 50 } }
175
- })
176
-
177
- await designer.previewDataset(dataset.id)
178
- expect(calls.query).toHaveLength(1)
179
- expect(calls.query[0]!.connection).toEqual(MYSQL)
180
- expect(calls.query[0]!.values).toEqual({ keyword: '', minAmount: 50 })
181
- })
182
- })
183
-
184
- describe('useReportDesigner:拖拽落格写 Cell Meta', () => {
185
- async function seedDataset(designer: UseReportDesignerReturn, connectionId = 'c1') {
186
- const dataset = designer.addDataset(connectionId)
187
- designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
188
- await designer.describeDataset(dataset.id)
189
- return dataset
190
- }
191
-
192
- it('落格写入 createReportBinding 默认绑定(detail/list/down)', async () => {
193
- const { designer, workbook } = createDesigner()
194
- const dataset = await seedDataset(designer)
195
-
196
- designer.bindField(dataset.id, 'customer', { row: 0, col: 0 })
197
-
198
- const binding = workbook.activeSheet.getCellMeta<ReportBinding>(
199
- { row: 0, col: 0 },
200
- REPORT_META_NAMESPACE
201
- )
202
- expect(binding).toEqual({
203
- dataset: dataset.id,
204
- field: 'customer',
205
- aggregate: 'list',
206
- expand: 'down',
207
- preset: 'detail',
208
- sort: 'none',
209
- conditionalRules: []
210
- })
211
- expect(designer.boundKeys.value.has(`${dataset.id}:customer`)).toBe(true)
212
- })
213
-
214
- it('同列向上推断 rowParent 候选', async () => {
215
- const { designer, workbook } = createDesigner()
216
- const dataset = await seedDataset(designer)
217
-
218
- designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
219
- designer.bindField(dataset.id, 'orderNo', { row: 2, col: 0 })
220
-
221
- const detail = workbook.activeSheet.getCellMeta<ReportBinding>(
222
- { row: 2, col: 0 },
223
- REPORT_META_NAMESPACE
224
- )
225
- expect(detail).toMatchObject({
226
- dataset: dataset.id,
227
- field: 'orderNo',
228
- preset: 'detail',
229
- aggregate: 'list',
230
- expand: 'down',
231
- rowParent: { row: 1, col: 0 }
232
- })
233
- })
234
-
235
- it('跨数据集拖拽保留字段自己的 dataset,不继承分组数据集', async () => {
236
- const { designer, workbook } = createDesigner([MYSQL, PG])
237
- const orders = await seedDataset(designer, 'c1')
238
- const inventory = await seedDataset(designer, 'c2')
239
-
240
- designer.bindField(orders.id, 'customer', { row: 1, col: 0 })
241
- designer.bindField(inventory.id, 'orderNo', { row: 2, col: 0 })
242
-
243
- const detail = workbook.activeSheet.getCellMeta<ReportBinding>(
244
- { row: 2, col: 0 },
245
- REPORT_META_NAMESPACE
246
- )
247
- expect(detail).toMatchObject({
248
- dataset: inventory.id,
249
- field: 'orderNo',
250
- rowParent: { row: 1, col: 0 }
251
- })
252
- })
253
-
254
- it('数值字段落至展开带下方相邻行时推断为小计预设', async () => {
255
- const { designer, workbook } = createDesigner()
256
- const dataset = await seedDataset(designer)
257
-
258
- bindGroupHeader(designer, workbook, dataset.id, { row: 1, col: 0 })
259
- designer.bindField(dataset.id, 'amount', { row: 2, col: 0 })
260
-
261
- const subtotal = workbook.activeSheet.getCellMeta<ReportBinding>(
262
- { row: 2, col: 0 },
263
- REPORT_META_NAMESPACE
264
- )
265
- expect(subtotal).toMatchObject({
266
- preset: 'subtotal',
267
- aggregate: 'sum',
268
- expand: 'none',
269
- rowParent: { row: 1, col: 0 }
270
- })
271
- })
272
-
273
- it('同行明细落格把 rowParent 推到左侧分组头', async () => {
274
- const { designer, workbook } = createDesigner()
275
- const dataset = await seedDataset(designer)
276
-
277
- bindGroupHeader(designer, workbook, dataset.id, { row: 1, col: 0 })
278
- designer.bindField(dataset.id, 'orderNo', { row: 1, col: 2 })
279
-
280
- const detail = workbook.activeSheet.getCellMeta<ReportBinding>(
281
- { row: 1, col: 2 },
282
- REPORT_META_NAMESPACE
283
- )
284
- expect(detail).toMatchObject({
285
- field: 'orderNo',
286
- preset: 'detail',
287
- rowParent: { row: 1, col: 0 }
288
- })
289
- })
290
-
291
- it('下方跨列小计落格推到行分组头而不是同列 list', async () => {
292
- const { designer, workbook } = createDesigner()
293
- const dataset = await seedDataset(designer)
294
-
295
- bindGroupHeader(designer, workbook, dataset.id, { row: 1, col: 0 })
296
- designer.bindField(dataset.id, 'orderNo', { row: 1, col: 2 })
297
- designer.bindField(dataset.id, 'amount', { row: 2, col: 2 })
298
-
299
- const subtotal = workbook.activeSheet.getCellMeta<ReportBinding>(
300
- { row: 2, col: 2 },
301
- REPORT_META_NAMESPACE
302
- )
303
- expect(subtotal).toMatchObject({
304
- preset: 'subtotal',
305
- aggregate: 'sum',
306
- expand: 'none',
307
- rowParent: { row: 1, col: 0 }
308
- })
309
- expect(subtotal?.colParent).toBeUndefined()
310
- })
311
-
312
- it('右侧列小计落格推到列分组头', async () => {
313
- const { designer, workbook } = createDesigner()
314
- const dataset = await seedDataset(designer)
315
-
316
- designer.bindField(dataset.id, 'customer', { row: 0, col: 1 })
317
- workbook.activeSheet.selectCell({ row: 0, col: 1 })
318
- designer.patchActiveBinding({ preset: 'groupHeader', aggregate: 'group', expand: 'right' })
319
- designer.bindField(dataset.id, 'amount', { row: 0, col: 2 })
320
-
321
- const subtotal = workbook.activeSheet.getCellMeta<ReportBinding>(
322
- { row: 0, col: 2 },
323
- REPORT_META_NAMESPACE
324
- )
325
- expect(subtotal).toMatchObject({
326
- preset: 'subtotal',
327
- aggregate: 'sum',
328
- expand: 'none',
329
- colParent: { row: 0, col: 1 }
330
- })
331
- })
332
-
333
- it('未给落点地址时回退到当前选区', async () => {
334
- const { designer, workbook } = createDesigner()
335
- const dataset = await seedDataset(designer)
336
- workbook.activeSheet.selectCell({ row: 3, col: 2 })
337
-
338
- designer.bindField(dataset.id, 'amount')
339
- expect(workbook.activeSheet.getCellMeta({ row: 3, col: 2 }, REPORT_META_NAMESPACE)).toBeTruthy()
340
- })
341
- })
342
-
343
- describe('useReportDesigner:角色徽章渲染 hook(ADR-0004 首个消费者)', () => {
344
- it('绑定格返回徽章布局(renderDefault: false),未绑定格回落默认渲染', async () => {
345
- const { designer, workbook } = createDesigner()
346
- const dataset = designer.addDataset('c1')
347
- designer.updateDataset(dataset.id, { sql: 'SELECT customer FROM orders' })
348
- await designer.describeDataset(dataset.id)
349
- designer.bindField(dataset.id, 'customer', { row: 0, col: 0 })
350
-
351
- expect(designer.resolveCellRenderer({ row: 0, col: 1 }, undefined)).toBeUndefined()
352
-
353
- const layout = designer.resolveCellRenderer({ row: 0, col: 0 }, undefined)
354
- expect(layout).toBeTruthy()
355
- expect(layout!.renderDefault).toBe(false)
356
- expect(layout!.rootContainer).toBeTruthy()
357
- // 根容器不得挂 percentCalc 宽高(VTable 会与 cellSize Math.max 出 NaN)
358
- expect(layout!.rootContainer.attribute.width).toBeUndefined()
359
- expect(layout!.rootContainer.attribute.height).toBeUndefined()
360
- expect(layout!.rootContainer.attribute.fill).toBeUndefined()
361
-
362
- const style = designer.resolveCellStyle({ row: 0, col: 0 }, undefined)
363
- expect(style?.fill).toEqual({ color: '#effaf3' })
364
-
365
- // 模型与快照无 renderer 残留
366
- expect(workbook.activeSheet.getCellData({ row: 0, col: 0 })).toBeUndefined()
367
- })
368
- })
369
-
370
- describe('useReportDesigner:getTemplate', () => {
371
- it('返回含 meta 绑定与内嵌数据集定义的 Report Template', async () => {
372
- const { designer } = createDesigner([MYSQL, PG])
373
- const orders = designer.addDataset('c1')
374
- designer.updateDataset(orders.id, {
375
- label: '销售明细',
376
- sql: 'SELECT customer FROM orders WHERE customer LIKE ${keyword}',
377
- paramOverrides: { keyword: { label: '客户关键词' } },
378
- fieldOverrides: { customer: { label: '客户名称' } }
379
- })
380
- await designer.describeDataset(orders.id)
381
- designer.bindField(orders.id, 'customer', { row: 1, col: 0 })
382
-
383
- const template = designer.getTemplate()
384
-
385
- expect(template.version).toBe(1)
386
-
387
- // meta 绑定随快照吐出
388
- const meta = template.meta ?? []
389
- expect(meta).toHaveLength(1)
390
- expect(meta[0]).toMatchObject({ row: 1, col: 0, namespace: 'report' })
391
- expect((meta[0]!.payload as ReportBinding).field).toBe('customer')
392
-
393
- // 数据集内嵌连接对象(无 connectionId / fields 缓存泄漏)
394
- expect(template.datasets).toHaveLength(1)
395
- const dataset = template.datasets![0]!
396
- expect(dataset).toEqual({
397
- id: orders.id,
398
- label: '销售明细',
399
- connection: MYSQL,
400
- sql: 'SELECT customer FROM orders WHERE customer LIKE ${keyword}',
401
- paramOverrides: { keyword: { label: '客户关键词' } },
402
- fieldOverrides: { customer: { label: '客户名称' } }
403
- })
404
- expect(dataset.connection).not.toBe(MYSQL)
405
- })
406
-
407
- it('无匹配连接的数据集不吐出(凭据不悬空)', () => {
408
- const { designer } = createDesigner([MYSQL])
409
- const orphan = designer.addDataset('c-missing')
410
- designer.updateDataset(orphan.id, { sql: 'SELECT 1' })
411
-
412
- expect(designer.getTemplate().datasets).toEqual([])
413
- })
414
- })
415
-
416
- describe('useReportDesigner:Action Pill 就地编辑(角色 / 聚合 / 条件规则 / 清除绑定)', () => {
417
- async function seedAndBind(
418
- designer: UseReportDesignerReturn,
419
- addr: { row: number; col: number }
420
- ) {
421
- const dataset = designer.addDataset('c1')
422
- designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
423
- await designer.describeDataset(dataset.id)
424
- designer.bindField(dataset.id, 'amount', addr)
425
- return dataset
426
- }
427
-
428
- it('预设切换写入 presetBindingDefaults 组合值;同族聚合切换保留预设', async () => {
429
- const { designer, workbook } = createDesigner()
430
- await seedAndBind(designer, { row: 0, col: 0 })
431
- workbook.activeSheet.selectCell({ row: 0, col: 0 })
432
-
433
- designer.patchActiveBinding(roleBindingDefaults('subtotal'))
434
- let binding = designer.activeBinding.value
435
- expect(binding).toMatchObject({ preset: 'subtotal', aggregate: 'sum', expand: 'none' })
436
-
437
- designer.patchActiveBinding({ aggregate: 'avg', expand: 'none' })
438
- binding = designer.activeBinding.value
439
- expect(binding).toMatchObject({ preset: 'subtotal', aggregate: 'avg', expand: 'none' })
440
- })
441
-
442
- it('聚合/展开偏离预设族时清除 preset', async () => {
443
- const { designer, workbook } = createDesigner()
444
- await seedAndBind(designer, { row: 0, col: 0 })
445
- workbook.activeSheet.selectCell({ row: 0, col: 0 })
446
-
447
- designer.patchActiveBinding(roleBindingDefaults('subtotal'))
448
- designer.patchActiveBinding({ aggregate: 'list', expand: 'down' })
449
- expect(designer.activeBinding.value?.preset).toBeUndefined()
450
- })
451
-
452
- it('条件规则与排序经 patch 写入绑定;拓扑条目跟随 meta 变更', async () => {
453
- const { designer, workbook } = createDesigner()
454
- const dataset = await seedAndBind(designer, { row: 0, col: 0 })
455
- workbook.activeSheet.selectCell({ row: 0, col: 0 })
456
-
457
- designer.patchActiveBinding({
458
- sort: 'desc',
459
- conditionalRules: [{ operator: 'gt', value: 100, style: { font: { color: '#DC2626' } } }]
460
- })
461
-
462
- const binding = workbook.activeSheet.getCellMeta<ReportBinding>(
463
- { row: 0, col: 0 },
464
- REPORT_META_NAMESPACE
465
- )
466
- expect(binding?.sort).toBe('desc')
467
- expect(binding?.conditionalRules).toHaveLength(1)
468
-
469
- const entries = designer.bindingEntries.value
470
- expect(entries).toHaveLength(1)
471
- expect(entries[0]).toMatchObject({ addr: { row: 0, col: 0 }, binding: { dataset: dataset.id } })
472
- })
473
-
474
- it('点选式父格编辑写入模板存储的 rowParent', async () => {
475
- const { designer, workbook } = createDesigner()
476
- const dataset = designer.addDataset('c1')
477
- designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
478
- await designer.describeDataset(dataset.id)
479
-
480
- designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
481
- designer.bindField(dataset.id, 'amount', { row: 2, col: 1 })
482
-
483
- workbook.activeSheet.selectCell({ row: 1, col: 0 })
484
- designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
485
- workbook.activeSheet.selectCell({ row: 2, col: 1 })
486
- designer.startParentPick('row')
487
- designer.pickParentAt({ row: 1, col: 0 })
488
-
489
- const binding = workbook.activeSheet.getCellMeta<ReportBinding>(
490
- { row: 2, col: 1 },
491
- REPORT_META_NAMESPACE
492
- )
493
- expect(binding?.rowParent).toEqual({ row: 1, col: 0 })
494
- expect(designer.parentPick.value).toBeNull()
495
- })
496
-
497
- it('切换小计行时纠正指向 list 的父格', async () => {
498
- const { designer, workbook } = createDesigner()
499
- const dataset = designer.addDataset('c1')
500
- designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
501
- await designer.describeDataset(dataset.id)
502
-
503
- designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
504
- workbook.activeSheet.selectCell({ row: 1, col: 0 })
505
- designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
506
- designer.bindField(dataset.id, 'orderNo', { row: 1, col: 2 })
507
- designer.bindField(dataset.id, 'amount', { row: 2, col: 2 })
508
-
509
- workbook.activeSheet.selectCell({ row: 2, col: 2 })
510
- designer.patchActiveBinding({ rowParent: { row: 1, col: 2 } })
511
- expect(designer.activeBinding.value?.rowParent).toEqual({ row: 1, col: 2 })
512
-
513
- designer.patchActiveBinding(roleBindingDefaults('subtotal'))
514
- expect(designer.activeBinding.value).toMatchObject({
515
- preset: 'subtotal',
516
- rowParent: { row: 1, col: 0 }
517
- })
518
- })
519
-
520
- it('非法点选父格时选区弹回源格且保持拾取态', async () => {
521
- const { designer, workbook } = createDesigner()
522
- const dataset = designer.addDataset('c1')
523
- designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
524
- await designer.describeDataset(dataset.id)
525
-
526
- designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
527
- workbook.activeSheet.selectCell({ row: 1, col: 0 })
528
- designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
529
- designer.bindField(dataset.id, 'amount', { row: 2, col: 2 })
530
-
531
- workbook.activeSheet.selectCell({ row: 2, col: 2 })
532
- designer.startParentPick('row')
533
- designer.pickParentAt({ row: 0, col: 3 })
534
-
535
- expect(designer.parentPick.value).toEqual({ mode: 'row', source: { row: 2, col: 2 } })
536
- expect(workbook.activeSheet.getSelection().activeCell).toEqual({ row: 2, col: 2 })
537
- expect(
538
- workbook.activeSheet.getCellMeta<ReportBinding>({ row: 2, col: 2 }, REPORT_META_NAMESPACE)
539
- ?.rowParent
540
- ).toEqual({ row: 1, col: 0 })
541
- })
542
-
543
- it('清除绑定移除 Cell Meta;行 / 列方向父格标签读取绑定存储值', async () => {
544
- const { designer, workbook } = createDesigner()
545
- const dataset = designer.addDataset('c1')
546
- designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
547
- await designer.describeDataset(dataset.id)
548
-
549
- // 同列上方分组头 + 下方小计(rowParent 推断到分组头)
550
- designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
551
- workbook.activeSheet.selectCell({ row: 1, col: 0 })
552
- designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
553
- designer.bindField(dataset.id, 'amount', { row: 2, col: 0 })
554
-
555
- workbook.activeSheet.selectCell({ row: 2, col: 0 })
556
- expect(designer.resolvedRowParentLabel.value).toBe('A2')
557
-
558
- designer.removeActiveBinding()
559
- expect(
560
- workbook.activeSheet.getCellMeta({ row: 2, col: 0 }, REPORT_META_NAMESPACE)
561
- ).toBeUndefined()
562
- expect(designer.activeBinding.value).toBeUndefined()
563
- expect(designer.boundKeys.value.has(`${dataset.id}:amount`)).toBe(false)
564
- })
565
- })
566
-
567
- describe('useReportDesigner:下钻配置读写与序列化', () => {
568
- const DRILL: ReportDrillConfig = {
569
- target: 'tpl-order-detail',
570
- mapping: { customer: 'p_customer', amount: 'p_minAmount' },
571
- openMode: 'dialog'
572
- }
573
-
574
- async function seedAndBind(
575
- designer: UseReportDesignerReturn,
576
- workbook: Workbook,
577
- addr: { row: number; col: number }
578
- ) {
579
- const dataset = designer.addDataset('c1')
580
- designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
581
- await designer.describeDataset(dataset.id)
582
- designer.bindField(dataset.id, 'customer', addr)
583
- workbook.activeSheet.selectCell(addr)
584
- return dataset
585
- }
586
-
587
- it('drill 经 patchActiveBinding 写入,随 getTemplate() 序列化 round-trip', async () => {
588
- const { designer, workbook } = createDesigner()
589
- await seedAndBind(designer, workbook, { row: 0, col: 0 })
590
-
591
- designer.patchActiveBinding({ drill: DRILL })
592
- expect(designer.activeBinding.value?.drill).toEqual(DRILL)
593
-
594
- const template = designer.getTemplate()
595
- expect(template.version).toBe(1)
596
- const meta = template.meta ?? []
597
- expect(meta).toHaveLength(1)
598
- expect((meta[0]!.payload as ReportBinding).drill).toEqual(DRILL)
599
-
600
- // JSON 序列化往返后下钻配置完整(模板可序列化流转)
601
- const restored = JSON.parse(JSON.stringify(template)) as ReportTemplate
602
- expect((restored.meta![0]!.payload as ReportBinding).drill).toEqual(DRILL)
603
- })
604
-
605
- it('patchActiveBinding({ drill: undefined }) 移除下钻配置(meta 不留 drill 键)', async () => {
606
- const { designer, workbook } = createDesigner()
607
- await seedAndBind(designer, workbook, { row: 0, col: 0 })
608
-
609
- designer.patchActiveBinding({ drill: DRILL })
610
- designer.patchActiveBinding({ drill: undefined })
611
-
612
- const binding = workbook.activeSheet.getCellMeta<ReportBinding>(
613
- { row: 0, col: 0 },
614
- REPORT_META_NAMESPACE
615
- )
616
- expect(binding).toBeTruthy()
617
- expect('drill' in binding!).toBe(false)
618
- expect(designer.getTemplate().meta![0]!.payload).not.toHaveProperty('drill')
619
- })
620
-
621
- it('载入含下钻配置的模板后还原,getTemplate 往返保留', async () => {
622
- const binding: ReportBinding = {
623
- dataset: 'ds-template',
624
- field: 'customer',
625
- preset: 'groupHeader',
626
- aggregate: 'group',
627
- expand: 'down',
628
- drill: DRILL
629
- }
630
- const template: ReportTemplate = {
631
- version: 1,
632
- cells: [],
633
- styles: [],
634
- merges: [],
635
- frozen: { rows: 0, cols: 0 },
636
- rows: 24,
637
- cols: 10,
638
- meta: [{ row: 1, col: 0, namespace: REPORT_META_NAMESPACE, payload: binding }],
639
- datasets: [
640
- {
641
- id: 'ds-template',
642
- label: '模板数据集',
643
- connection: { ...PG, id: 'conn-template' },
644
- sql: 'SELECT customer, amount FROM orders'
645
- }
646
- ]
647
- }
648
-
649
- const { designer, workbook } = createDesigner([MYSQL], { template })
650
-
651
- // 下钻配置随绑定 meta 还原
652
- const loaded = workbook.activeSheet.getCellMeta<ReportBinding>(
653
- { row: 1, col: 0 },
654
- REPORT_META_NAMESPACE
655
- )
656
- expect(loaded?.drill).toEqual(DRILL)
657
-
658
- // 选中该格:activeBinding 暴露 drill(Action Pill 下钻入口回填数据源)
659
- workbook.activeSheet.selectCell({ row: 1, col: 0 })
660
- expect(designer.activeBinding.value?.drill).toEqual(DRILL)
661
-
662
- // 往返吐出保留
663
- const roundTrip = designer.getTemplate()
664
- expect(roundTrip.version).toBe(1)
665
- expect((roundTrip.meta![0]!.payload as ReportBinding).drill).toEqual(DRILL)
666
- })
667
- })
668
-
669
- describe('useReportDesigner:template 版本校验', () => {
670
- it('载入缺少 version 的模板时抛可读错误', () => {
671
- const { connector } = createStubConnector()
672
- const connections = ref<DataConnection[]>([MYSQL])
673
- expect(() =>
674
- effectScope().run(() =>
675
- useReportDesigner({
676
- props: {
677
- connector,
678
- template: {
679
- cells: [],
680
- styles: [],
681
- merges: [],
682
- frozen: { rows: 0, cols: 0 },
683
- rows: 24,
684
- cols: 10
685
- }
686
- },
687
- connections
688
- })
689
- )
690
- ).toThrow(IncompatibleTemplateVersionError)
691
- })
692
-
693
- it('载入高于当前支持 version 的模板时抛可读错误', () => {
694
- const { connector } = createStubConnector()
695
- const connections = ref<DataConnection[]>([MYSQL])
696
- expect(() =>
697
- effectScope().run(() =>
698
- useReportDesigner({
699
- props: {
700
- connector,
701
- template: {
702
- version: 99,
703
- cells: [],
704
- styles: [],
705
- merges: [],
706
- frozen: { rows: 0, cols: 0 },
707
- rows: 24,
708
- cols: 10
709
- }
710
- },
711
- connections
712
- })
713
- )
714
- ).toThrow(/高于当前支持/)
715
- })
716
- })
717
-
718
- describe('useReportDesigner:template prop 载入既有模板', () => {
719
- function buildTemplate(): ReportTemplate {
720
- const group = createReportBindingFixture()
721
- return {
722
- version: 1,
723
- cells: [{ row: 0, col: 0, v: '客户' }],
724
- styles: [],
725
- merges: [],
726
- frozen: { rows: 0, cols: 0 },
727
- rows: 24,
728
- cols: 10,
729
- meta: [{ row: 1, col: 0, namespace: REPORT_META_NAMESPACE, payload: group }],
730
- datasets: [
731
- {
732
- id: 'ds-template',
733
- label: '模板数据集',
734
- connection: { ...PG, id: 'conn-template' },
735
- sql: 'SELECT customer, amount FROM orders',
736
- paramOverrides: { keyword: { label: '客户关键词' } },
737
- fieldOverrides: { customer: { label: '客户名称' } }
738
- }
739
- ]
740
- }
741
- }
742
-
743
- function createReportBindingFixture(): ReportBinding {
744
- return {
745
- dataset: 'ds-template',
746
- field: 'customer',
747
- preset: 'groupHeader',
748
- aggregate: 'group',
749
- expand: 'down',
750
- sort: 'none',
751
- conditionalRules: [{ operator: 'eq', value: '甲公司', style: { fill: { color: '#FFCCCC' } } }]
752
- }
753
- }
754
-
755
- it('恢复网格绑定与设计态数据集;内嵌连接按 id 合并进 v-model 列表;describe 恢复字段缓存', async () => {
756
- const { designer, connections, workbook, calls } = createDesigner([MYSQL], {
757
- template: buildTemplate()
758
- })
759
-
760
- // 绑定随快照恢复(含条件规则)
761
- const binding = workbook.activeSheet.getCellMeta<ReportBinding>(
762
- { row: 1, col: 0 },
763
- REPORT_META_NAMESPACE
764
- )
765
- expect(binding).toEqual(createReportBindingFixture())
766
-
767
- // 数据集还原为设计态(connectionId 引用;fields 缓存经 describe 恢复)
768
- expect(designer.datasets.value).toHaveLength(1)
769
- expect(designer.datasets.value[0]).toMatchObject({
770
- id: 'ds-template',
771
- label: '模板数据集',
772
- connectionId: 'conn-template',
773
- sql: 'SELECT customer, amount FROM orders'
774
- })
775
- await Promise.resolve()
776
- expect(calls.describe).toHaveLength(1)
777
- expect(calls.describe[0]!.connection.id).toBe('conn-template')
778
-
779
- // 内嵌连接按 id 合并(已有连接不重复追加)
780
- expect(connections.value.map((item) => item.id)).toEqual(['c1', 'conn-template'])
781
-
782
- // catalog 应用 fieldOverrides(字段面板与徽章 label 数据源)
783
- const catalog = designer.catalog.value
784
- expect(catalog[0]!.fields.map((field) => field.label)).toEqual(['客户名称', '订单号', '金额'])
785
- })
786
-
787
- it('载入后 getTemplate 往返保持绑定与数据集定义;宿主已有同 id 连接时不覆盖', async () => {
788
- const template = buildTemplate()
789
- const { designer } = createDesigner(
790
- [MYSQL, { ...PG, id: 'conn-template', label: '宿主连接' }],
791
- { template }
792
- )
793
-
794
- // 宿主同 id 连接优先(单一事实源),模板内嵌连接不覆盖
795
- const roundTrip = designer.getTemplate()
796
- expect(roundTrip.datasets).toHaveLength(1)
797
- expect(roundTrip.datasets![0]!.connection.label).toBe('宿主连接')
798
-
799
- // 绑定完整往返
800
- const meta = roundTrip.meta ?? []
801
- expect(meta).toHaveLength(1)
802
- expect(meta[0]!.payload).toEqual(createReportBindingFixture())
803
- })
804
- })