@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,239 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest'
2
-
3
- import { createHttpConnector, resolvePortOnTypeChange } from '../connector'
4
- import type { DataConnection } from '../connector'
5
- import type { ParamValues } from '../types'
6
-
7
- const CONNECTION: DataConnection = {
8
- id: 'demo',
9
- label: '演示业务库',
10
- type: 'mysql',
11
- host: '127.0.0.1',
12
- port: 3306,
13
- database: 'demo_business',
14
- username: 'demo',
15
- password: 'demo'
16
- }
17
-
18
- const SQL = 'SELECT customer, amount FROM orders WHERE region = ${region}'
19
- const VALUES: ParamValues = { region: '华东' }
20
-
21
- const ENDPOINT = 'https://api.example.com/report'
22
-
23
- interface FetchCall {
24
- url: string
25
- method: string
26
- headers: Headers
27
- body: unknown
28
- }
29
-
30
- function stubFetch(respond: (url: string, init: RequestInit) => Promise<unknown>): FetchCall[] {
31
- const calls: FetchCall[] = []
32
- const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
33
- const url = String(input)
34
- const method = init?.method ?? 'GET'
35
- const headers = new Headers(init?.headers)
36
- const body = typeof init?.body === 'string' ? (JSON.parse(init.body) as unknown) : undefined
37
- calls.push({ url, method, headers, body })
38
- const payload = await respond(url, init ?? {})
39
- return new Response(JSON.stringify(payload), {
40
- status: 200,
41
- headers: { 'Content-Type': 'application/json' }
42
- })
43
- })
44
- vi.stubGlobal('fetch', fetchMock)
45
- return calls
46
- }
47
-
48
- afterEach(() => {
49
- vi.unstubAllGlobals()
50
- })
51
-
52
- describe('createHttpConnector', () => {
53
- it('test:POST {endpoint}/test,请求体为 { connection },成功返回 ok 且无 data', async () => {
54
- const calls = stubFetch(async () => ({ ok: true }))
55
- const connector = createHttpConnector({ endpoint: ENDPOINT })
56
-
57
- const result = await connector.test(CONNECTION)
58
-
59
- expect(result).toEqual({ ok: true, data: undefined })
60
- expect(calls).toHaveLength(1)
61
- expect(calls[0]!.url).toBe(`${ENDPOINT}/test`)
62
- expect(calls[0]!.method).toBe('POST')
63
- expect(calls[0]!.headers.get('content-type')).toBe('application/json')
64
- expect(calls[0]!.body).toEqual({ connection: CONNECTION })
65
- })
66
-
67
- it('describe:POST {endpoint}/describe,请求体 { connection, sql },fields 归一化为 DatasetField', async () => {
68
- const calls = stubFetch(async () => ({
69
- ok: true,
70
- fields: [{ name: 'customer' }, { name: 'amount', type: 'number' }]
71
- }))
72
- const connector = createHttpConnector({ endpoint: ENDPOINT })
73
-
74
- const result = await connector.describe(CONNECTION, SQL)
75
-
76
- expect(result).toEqual({
77
- ok: true,
78
- data: [
79
- { name: 'customer', label: 'customer', type: 'string' },
80
- { name: 'amount', label: 'amount', type: 'number' }
81
- ]
82
- })
83
- expect(calls[0]!.url).toBe(`${ENDPOINT}/describe`)
84
- expect(calls[0]!.body).toEqual({ connection: CONNECTION, sql: SQL })
85
- })
86
-
87
- it('describe:服务端返回 label 时保留中文显示名', async () => {
88
- stubFetch(async () => ({
89
- ok: true,
90
- fields: [
91
- { name: 'customer', label: '客户名称', type: 'string' },
92
- { name: 'amount', label: '金额', type: 'number' }
93
- ]
94
- }))
95
- const connector = createHttpConnector({ endpoint: ENDPOINT })
96
-
97
- const result = await connector.describe(CONNECTION, SQL)
98
-
99
- expect(result).toEqual({
100
- ok: true,
101
- data: [
102
- { name: 'customer', label: '客户名称', type: 'string' },
103
- { name: 'amount', label: '金额', type: 'number' }
104
- ]
105
- })
106
- })
107
-
108
- it('query:POST {endpoint}/query,请求体 { connection, sql, values },返回 fields + rows', async () => {
109
- const calls = stubFetch(async () => ({
110
- ok: true,
111
- fields: [{ name: 'customer', type: 'string' }],
112
- rows: [{ customer: '甲公司' }]
113
- }))
114
- const connector = createHttpConnector({ endpoint: ENDPOINT })
115
-
116
- const result = await connector.query(CONNECTION, SQL, VALUES)
117
-
118
- expect(result).toEqual({
119
- ok: true,
120
- data: {
121
- fields: [{ name: 'customer', label: 'customer', type: 'string' }],
122
- rows: [{ customer: '甲公司' }]
123
- }
124
- })
125
- expect(calls[0]!.url).toBe(`${ENDPOINT}/query`)
126
- expect(calls[0]!.body).toEqual({ connection: CONNECTION, sql: SQL, values: VALUES })
127
- })
128
-
129
- it('业务错误分叉:200 + { ok: false, error } 原样透传(三端点一致)', async () => {
130
- stubFetch(async () => ({
131
- ok: false,
132
- error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
133
- }))
134
- const connector = createHttpConnector({ endpoint: ENDPOINT })
135
-
136
- const testResult = await connector.test(CONNECTION)
137
- expect(testResult).toEqual({
138
- ok: false,
139
- error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
140
- })
141
-
142
- const describeResult = await connector.describe(CONNECTION, SQL)
143
- expect(describeResult).toEqual({
144
- ok: false,
145
- error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
146
- })
147
-
148
- const queryResult = await connector.query(CONNECTION, SQL, VALUES)
149
- expect(queryResult).toEqual({
150
- ok: false,
151
- error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
152
- })
153
- })
154
-
155
- it('传输层错误:非 2xx 折叠为 HTTP_<status> 错误结果', async () => {
156
- const fetchMock = vi.fn(async () => new Response('Internal Server Error', { status: 500 }))
157
- vi.stubGlobal('fetch', fetchMock)
158
- const connector = createHttpConnector({ endpoint: ENDPOINT })
159
-
160
- const result = await connector.test(CONNECTION)
161
-
162
- expect(result).toEqual({
163
- ok: false,
164
- error: { code: 'HTTP_500', message: '请求失败(HTTP 500)' }
165
- })
166
- })
167
-
168
- it('网络异常:fetch reject 折叠为 NETWORK_ERROR', async () => {
169
- vi.stubGlobal(
170
- 'fetch',
171
- vi.fn(async () => Promise.reject(new Error('fetch failed')))
172
- )
173
- const connector = createHttpConnector({ endpoint: ENDPOINT })
174
-
175
- const result = await connector.test(CONNECTION)
176
-
177
- expect(result).toEqual({ ok: false, error: { code: 'NETWORK_ERROR', message: 'fetch failed' } })
178
- })
179
-
180
- it('响应不可解析:非 JSON / 缺字段折叠为 BAD_RESPONSE', async () => {
181
- const notJson = vi.fn(async () => new Response('<html>oops</html>', { status: 200 }))
182
- vi.stubGlobal('fetch', notJson)
183
- const connector = createHttpConnector({ endpoint: ENDPOINT })
184
-
185
- const invalidJson = await connector.describe(CONNECTION, SQL)
186
- expect(invalidJson).toEqual({
187
- ok: false,
188
- error: { code: 'BAD_RESPONSE', message: '响应不是合法 JSON' }
189
- })
190
-
191
- vi.stubGlobal(
192
- 'fetch',
193
- vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 }))
194
- )
195
- const missingFields = await connector.describe(CONNECTION, SQL)
196
- expect(missingFields).toEqual({
197
- ok: false,
198
- error: { code: 'BAD_RESPONSE', message: 'describe 响应缺少合法的 fields 数组' }
199
- })
200
-
201
- vi.stubGlobal(
202
- 'fetch',
203
- vi.fn(async () => new Response(JSON.stringify({ ok: true, fields: [] }), { status: 200 }))
204
- )
205
- const missingRows = await connector.query(CONNECTION, SQL, VALUES)
206
- expect(missingRows).toEqual({
207
- ok: false,
208
- error: { code: 'BAD_RESPONSE', message: 'query 响应缺少 rows 数组' }
209
- })
210
- })
211
-
212
- it('ok:false 但 error 形状非法时回退 SERVER_ERROR', async () => {
213
- stubFetch(async () => ({ ok: false, error: 'oops' }))
214
- const connector = createHttpConnector({ endpoint: ENDPOINT })
215
-
216
- const result = await connector.test(CONNECTION)
217
-
218
- expect(result).toEqual({
219
- ok: false,
220
- error: { code: 'SERVER_ERROR', message: '服务端返回未知错误' }
221
- })
222
- })
223
- })
224
-
225
- describe('resolvePortOnTypeChange', () => {
226
- it('默认端口随类型切换:MySQL 3306 ↔ PostgreSQL 5432', () => {
227
- expect(resolvePortOnTypeChange('mysql', 'postgresql', 3306)).toBe(5432)
228
- expect(resolvePortOnTypeChange('postgresql', 'mysql', 5432)).toBe(3306)
229
- })
230
-
231
- it('自定义端口不随类型切换而改变', () => {
232
- expect(resolvePortOnTypeChange('mysql', 'postgresql', 3307)).toBe(3307)
233
- expect(resolvePortOnTypeChange('postgresql', 'mysql', 5433)).toBe(5433)
234
- })
235
-
236
- it('端口为 0 时切到新类型默认端口', () => {
237
- expect(resolvePortOnTypeChange('mysql', 'postgresql', 0)).toBe(5432)
238
- })
239
- })
@@ -1,194 +0,0 @@
1
- import { Sheet, cellKey } from '@veltra/sheet-core'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { REPORT_META_NAMESPACE } from '../binding'
5
- import {
6
- buildDrillHitMap,
7
- createDrillStack,
8
- currentDrillLayer,
9
- popDrillLayer,
10
- pushDrillLayer,
11
- resolveDrillParams
12
- } from '../drill'
13
- import { createReportTemplate, type ReportTemplate } from '../template'
14
- import type { ReportBinding, ReportDrillConfig } from '../types'
15
-
16
- // ---- 内联 fixtures ----
17
-
18
- /** 最小 Report Template:下钻栈只关心引用与参数,不消费快照内容 */
19
- function makeTemplate(rows: number): ReportTemplate {
20
- return {
21
- version: 1,
22
- cells: [],
23
- styles: [],
24
- merges: [],
25
- frozen: { rows: 0, cols: 0 },
26
- rows,
27
- cols: 0
28
- }
29
- }
30
-
31
- const TPL_A = makeTemplate(10)
32
- const TPL_B = makeTemplate(20)
33
- const TPL_C = makeTemplate(30)
34
-
35
- const DRILL: ReportDrillConfig = {
36
- target: 'tpl-detail',
37
- mapping: { region: 'p_region', amount: 'p_amount' },
38
- openMode: 'switch'
39
- }
40
-
41
- describe('resolveDrillParams', () => {
42
- it('按映射从记录取值为详情报参数', () => {
43
- const values = resolveDrillParams(DRILL, { region: '华东', amount: 100 })
44
- expect(values).toEqual({ p_region: '华东', p_amount: 100 })
45
- })
46
-
47
- it('记录缺字段时跳过对应参数,null 值保留', () => {
48
- const values = resolveDrillParams(DRILL, { region: '华东', amount: null })
49
- expect(values).toEqual({ p_region: '华东', p_amount: null })
50
- expect(
51
- 'p_missing' in resolveDrillParams({ ...DRILL, mapping: { missing: 'p_missing' } }, {})
52
- ).toBe(false)
53
- })
54
-
55
- it('空映射返回空参数', () => {
56
- expect(resolveDrillParams({ ...DRILL, mapping: {} }, { region: '华东' })).toEqual({})
57
- })
58
- })
59
-
60
- describe('下钻栈', () => {
61
- it('根层即当前层', () => {
62
- const stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
63
- expect(stack).toHaveLength(1)
64
- expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a0' } })
65
- })
66
-
67
- it('push 后当前层为新层,前层参数定格为离开时的值', () => {
68
- const stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
69
- const next = pushDrillLayer(stack, { template: TPL_B, params: { p: 'b1' } }, { p: 'a1' })
70
- expect(next).toHaveLength(2)
71
- expect(currentDrillLayer(next)).toEqual({ template: TPL_B, params: { p: 'b1' } })
72
- expect(next[0]).toEqual({ template: TPL_A, params: { p: 'a1' } })
73
- // 不可变更新:原栈不受影响
74
- expect(stack).toHaveLength(1)
75
- })
76
-
77
- it('push 缺省 currentParams 时保留前层原参数', () => {
78
- const stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
79
- const next = pushDrillLayer(stack, { template: TPL_B, params: {} })
80
- expect(next[0]).toEqual({ template: TPL_A, params: { p: 'a0' } })
81
- })
82
-
83
- it('pop 逐级回退并保留各层当时参数', () => {
84
- let stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
85
- stack = pushDrillLayer(stack, { template: TPL_B, params: { p: 'b1' } }, { p: 'a1' })
86
- stack = pushDrillLayer(stack, { template: TPL_C, params: { p: 'c1' } }, { p: 'b2' })
87
-
88
- stack = popDrillLayer(stack)
89
- expect(currentDrillLayer(stack)).toEqual({ template: TPL_B, params: { p: 'b2' } })
90
- stack = popDrillLayer(stack)
91
- expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a1' } })
92
- })
93
-
94
- it('仅剩根层时 pop 原样返回', () => {
95
- const stack = createDrillStack({ template: TPL_A, params: {} })
96
- expect(popDrillLayer(stack)).toBe(stack)
97
- })
98
-
99
- it('指向自身照常压栈,靠 pop 逐级退出', () => {
100
- let stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
101
- stack = pushDrillLayer(stack, { template: TPL_A, params: { p: 'a1' } })
102
- stack = pushDrillLayer(stack, { template: TPL_A, params: { p: 'a2' } })
103
- expect(stack).toHaveLength(3)
104
-
105
- stack = popDrillLayer(stack)
106
- expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a1' } })
107
- stack = popDrillLayer(stack)
108
- expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a0' } })
109
- })
110
-
111
- it('成环照常压栈,回退沿压栈路径逐级退出', () => {
112
- let stack = createDrillStack({ template: TPL_A, params: { p: 'root' } })
113
- stack = pushDrillLayer(stack, { template: TPL_B, params: { p: 'b1' } })
114
- stack = pushDrillLayer(stack, { template: TPL_A, params: { p: 'a-from-b' } })
115
- stack = pushDrillLayer(stack, { template: TPL_B, params: { p: 'b2' } })
116
- expect(stack.map((layer) => layer.template)).toEqual([TPL_A, TPL_B, TPL_A, TPL_B])
117
-
118
- stack = popDrillLayer(stack)
119
- expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a-from-b' } })
120
- stack = popDrillLayer(stack)
121
- expect(currentDrillLayer(stack)).toEqual({ template: TPL_B, params: { p: 'b1' } })
122
- stack = popDrillLayer(stack)
123
- expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'root' } })
124
- })
125
- })
126
-
127
- describe('buildDrillHitMap 与上下文解析', () => {
128
- it('分组格与汇总聚合格均生成正确命中上下文(含汇总格自身聚合值)', () => {
129
- const sheet = new Sheet()
130
- sheet.setCellMeta({ row: 0, col: 0 }, REPORT_META_NAMESPACE, {
131
- dataset: 'ds',
132
- field: 'region',
133
- aggregate: 'group',
134
- expand: 'down',
135
- drill: { target: 't2', mapping: { region: 'p_region' }, openMode: 'switch' }
136
- })
137
- sheet.setCellMeta({ row: 0, col: 1 }, REPORT_META_NAMESPACE, {
138
- dataset: 'ds',
139
- field: 'amount',
140
- aggregate: 'sum',
141
- expand: 'none',
142
- rowParent: { row: 0, col: 0 },
143
- drill: {
144
- target: 't2',
145
- mapping: { region: 'p_region', amount: 'p_amount' },
146
- openMode: 'dialog'
147
- }
148
- })
149
-
150
- const template = sheet.snapshot()
151
- const data = {
152
- ds: [
153
- { region: '华东', amount: 100 },
154
- { region: '华东', amount: 200 },
155
- { region: '华南', amount: 50 }
156
- ]
157
- }
158
-
159
- const hitMap = buildDrillHitMap(template, data)
160
- // 华东分组格 (0,0)
161
- const hitGroup0 = hitMap.get(cellKey({ row: 0, col: 0 }))
162
- expect(hitGroup0?.record).toEqual({ region: '华东' })
163
-
164
- // 华东汇总格 (0,1) 拥有祖先分组 region 与自身的求和计算值 300
165
- const hitSum0 = hitMap.get(cellKey({ row: 0, col: 1 }))
166
- expect(hitSum0?.record).toEqual({ region: '华东', amount: 300 })
167
- expect(resolveDrillParams(hitSum0!.config, hitSum0!.record)).toEqual({
168
- p_region: '华东',
169
- p_amount: 300
170
- })
171
-
172
- // 华南汇总格 (1,1)
173
- const hitSum1 = hitMap.get(cellKey({ row: 1, col: 1 }))
174
- expect(hitSum1?.record).toEqual({ region: '华南', amount: 50 })
175
- expect(resolveDrillParams(hitSum1!.config, hitSum1!.record)).toEqual({
176
- p_region: '华南',
177
- p_amount: 50
178
- })
179
- })
180
- })
181
-
182
- describe('旧绑定兼容', () => {
183
- it('无 drill 字段的旧绑定序列化往返后不出现 drill 键', () => {
184
- const binding: ReportBinding = {
185
- dataset: 'orders',
186
- field: 'region',
187
- expand: 'down',
188
- aggregate: 'list'
189
- }
190
- const restored = JSON.parse(JSON.stringify(binding)) as ReportBinding
191
- expect('drill' in restored).toBe(false)
192
- expect(restored.drill).toBeUndefined()
193
- })
194
- })
@@ -1,96 +0,0 @@
1
- import { Sheet } from '@veltra/sheet-core'
2
- import { importXlsx } from '@veltra/sheet-core/core/io/import'
3
- import { describe, expect, it } from 'vitest'
4
-
5
- import { REPORT_META_NAMESPACE, createReportBinding } from '../binding'
6
- import { exportFilledReportXlsx } from '../export-xlsx'
7
- import { renderReport } from '../render'
8
- import type { DatasetCatalogItem } from '../types'
9
-
10
- // ---- 内联 fixtures ----
11
-
12
- const ORDERS_DATASET: DatasetCatalogItem = {
13
- id: 'orders',
14
- label: '销售明细',
15
- fields: [
16
- { name: 'customer', label: '客户', type: 'string' },
17
- { name: 'amount', label: '金额', type: 'number' }
18
- ]
19
- }
20
-
21
- const ORDER_ROWS: Record<string, unknown>[] = [
22
- { customer: '甲公司', amount: 100 },
23
- { customer: '乙公司', amount: 400 }
24
- ]
25
-
26
- /** 金额 > 300 标红(条件样式打平进快照后随导出保真) */
27
- const RED_RULE = { operator: 'gt' as const, value: 300, style: { font: { color: '#DC2626' } } }
28
-
29
- /** 分组(客户)+ 明细(金额,带条件规则)模板 */
30
- function buildFilledSheet(): Sheet {
31
- const sheet = new Sheet()
32
- sheet.setCells([
33
- { addr: { row: 0, col: 0 }, data: { v: '客户' } },
34
- { addr: { row: 0, col: 1 }, data: { v: '金额' } }
35
- ])
36
- sheet.setCellStyle(
37
- { start: { row: 0, col: 0 }, end: { row: 0, col: 1 } },
38
- { font: { bold: true }, fill: { color: '#E8EEF7' } }
39
- )
40
-
41
- const groupAddr = { row: 1, col: 0 }
42
- const group = createReportBinding(ORDERS_DATASET, 'customer')
43
- group.preset = 'groupHeader'
44
- group.aggregate = 'group'
45
- group.expand = 'down'
46
- sheet.setCellMeta(groupAddr, REPORT_META_NAMESPACE, group)
47
-
48
- const amount = createReportBinding(ORDERS_DATASET, 'amount')
49
- amount.rowParent = groupAddr
50
- amount.conditionalRules = [RED_RULE]
51
- sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, amount)
52
-
53
- const filled = renderReport(sheet.snapshot(), { orders: ORDER_ROWS })
54
- const filledSheet = new Sheet()
55
- filledSheet.restore(filled)
56
- filledSheet.restoreContent(filled)
57
- return filledSheet
58
- }
59
-
60
- describe('exportFilledReportXlsx', () => {
61
- it('导出保真 XLSX:值 / 表头样式 / 条件样式打平后随快照导出', async () => {
62
- const filled = buildFilledSheet()
63
- filled.setColWidth(0, 120)
64
- filled.setColWidth(1, 140)
65
- const buffer = await exportFilledReportXlsx(filled)
66
-
67
- // 合法 ZIP(xlsx)字节
68
- expect(buffer[0]).toBe(0x50)
69
- expect(buffer[1]).toBe(0x4b)
70
-
71
- const workbook = await importXlsx(buffer)
72
- const sheet = workbook.getSheets()[0]!
73
-
74
- // 展开值保真(分组头 + 明细值)
75
- const values: unknown[] = []
76
- for (const [, data] of sheet.store.entries()) values.push(data.v)
77
- expect(values).toContain('甲公司')
78
- expect(values).toContain('乙公司')
79
- expect(values).toContain(400)
80
-
81
- // 条件样式打平:amount=400(>300)的明细格带红字
82
- let redFound = false
83
- for (const [, data] of sheet.store.entries()) {
84
- if (data.v !== 400 || data.s == null) continue
85
- const style = sheet.stylePool.get(data.s)
86
- if (style?.font?.color === '#DC2626') redFound = true
87
- }
88
- expect(redFound).toBe(true)
89
-
90
- // 表头静态样式保真(加粗 + 底色)
91
- const header = sheet.getCellData({ row: 0, col: 0 })
92
- const headerStyle = header?.s != null ? sheet.stylePool.get(header.s) : undefined
93
- expect(headerStyle?.font?.bold).toBe(true)
94
- expect(headerStyle?.fill?.color).toBe('#E8EEF7')
95
- })
96
- })
@@ -1,58 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { parseDateRangeValue, patchParamValues, resolveNumberParamValue } from '../filter-bar'
4
-
5
- describe('parseDateRangeValue', () => {
6
- it('解析数组形式的日期范围', () => {
7
- expect(parseDateRangeValue(['2024-01-01', '2024-12-31'])).toEqual(['2024-01-01', '2024-12-31'])
8
- })
9
-
10
- it('解析 from/to 对象形式', () => {
11
- expect(parseDateRangeValue({ from: '2024-03-01', to: '2024-03-31' })).toEqual([
12
- '2024-03-01',
13
- '2024-03-31'
14
- ])
15
- })
16
-
17
- it('解析 start/end 对象形式', () => {
18
- expect(parseDateRangeValue({ start: '2024-06-01', end: '2024-06-30' })).toEqual([
19
- '2024-06-01',
20
- '2024-06-30'
21
- ])
22
- })
23
-
24
- it('空值回退为空字符串元组', () => {
25
- expect(parseDateRangeValue(undefined)).toEqual(['', ''])
26
- expect(parseDateRangeValue(null)).toEqual(['', ''])
27
- expect(parseDateRangeValue('')).toEqual(['', ''])
28
- })
29
- })
30
-
31
- describe('resolveNumberParamValue', () => {
32
- it('保留有限数字', () => {
33
- expect(resolveNumberParamValue(42)).toBe(42)
34
- expect(resolveNumberParamValue('12.5')).toBe(12.5)
35
- })
36
-
37
- it('空值与非法值返回 undefined', () => {
38
- expect(resolveNumberParamValue('')).toBeUndefined()
39
- expect(resolveNumberParamValue(null)).toBeUndefined()
40
- expect(resolveNumberParamValue(undefined)).toBeUndefined()
41
- expect(resolveNumberParamValue('abc')).toBeUndefined()
42
- })
43
- })
44
-
45
- describe('patchParamValues', () => {
46
- it('合并单参数变更且保留其余值', () => {
47
- const next = patchParamValues({ region: '华东', qty: 10 }, 'region', '华南')
48
- expect(next).toEqual({ region: '华南', qty: 10 })
49
- })
50
-
51
- it('支持 date-range 元组写入', () => {
52
- const next = patchParamValues({ dateRange: ['2024-01-01', '2024-01-31'] }, 'dateRange', [
53
- '2024-02-01',
54
- '2024-02-28'
55
- ])
56
- expect(next.dateRange).toEqual(['2024-02-01', '2024-02-28'])
57
- })
58
- })