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