@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,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
- })