@veltra/sheet 2.1.1 → 2.3.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 (212) hide show
  1. package/dist/components/report/binding-badge.js +100 -0
  2. package/dist/components/report/designer/cell-coords.js +46 -0
  3. package/dist/components/report/designer/col-widths.js +29 -0
  4. package/dist/components/report/designer/conditional-rules/helpers.js +139 -0
  5. package/dist/components/report/designer/drop-highlight-overlay.js +76 -0
  6. package/dist/components/report/designer/float-panel.js +422 -0
  7. package/dist/components/report/designer/role.js +17 -0
  8. package/dist/components/report/designer/rule-preview.js +33 -0
  9. package/dist/components/report/designer/rule-row.js +253 -0
  10. package/dist/components/report/designer/rules-dialog.js +118 -0
  11. package/dist/components/report/designer/topology-overlay.js +122 -0
  12. package/dist/components/report/designer/topology.js +64 -0
  13. package/dist/components/report/designer/use-grid-overlay.js +75 -0
  14. package/dist/components/report/designer-hub.js +177 -0
  15. package/dist/components/report/field-panel-helpers.js +33 -0
  16. package/dist/components/report/field-panel.js +132 -0
  17. package/dist/components/report/filter-bar.js +74 -0
  18. package/dist/components/report/hub-connection-form.js +145 -0
  19. package/dist/components/report/hub-dataset-editor.js +394 -0
  20. package/dist/components/report/preview-grid-size.js +32 -0
  21. package/dist/components/report/report-designer.js +393 -0
  22. package/dist/components/report/report-viewer.js +124 -0
  23. package/dist/components/report/style.d.ts +17 -0
  24. package/dist/components/report/style.js +18 -0
  25. package/dist/components/report/style2.css +874 -0
  26. package/dist/components/report/use-report-designer.js +382 -0
  27. package/dist/components/report/use-report-viewer.js +55 -0
  28. package/dist/{vue → components/sheet}/formula-bar.js +1 -1
  29. package/dist/{vue → components/sheet}/formula-suggest-list.js +1 -1
  30. package/dist/{vue → components/sheet}/import-file.js +1 -1
  31. package/dist/{vue → components/sheet}/insert-count-menu-item.js +1 -1
  32. package/dist/{vue → components/sheet}/insert-image.js +1 -1
  33. package/dist/{vue → components/sheet}/popup-helpers.js +1 -1
  34. package/dist/{vue → components/sheet}/popups/border-popup.js +31 -3
  35. package/dist/{vue → components/sheet}/popups/border-preset-glyph.js +1 -1
  36. package/dist/{vue → components/sheet}/popups/export-popup.js +2 -2
  37. package/dist/{vue → components/sheet}/popups/fill-color-popup.js +3 -3
  38. package/dist/{vue → components/sheet}/popups/find-popup.js +1 -1
  39. package/dist/{vue → components/sheet}/popups/font-color-popup.js +3 -3
  40. package/dist/{vue → components/sheet}/popups/font-size-popup.js +3 -3
  41. package/dist/{vue → components/sheet}/popups/import.worker.d.ts +1 -1
  42. package/dist/{vue → components/sheet}/popups/import.worker.js +1 -1
  43. package/dist/{vue → components/sheet}/popups/insert-image-popup.js +1 -1
  44. package/dist/{vue → components/sheet}/sheet-context-menu.js +7 -4
  45. package/dist/{vue → components/sheet}/sheet-tabs.js +1 -1
  46. package/dist/{vue → components/sheet}/sheet-toolbar.js +1 -1
  47. package/dist/{vue → components/sheet}/sheet.js +16 -1
  48. package/dist/{vue → components/sheet}/use-find-replace.js +1 -1
  49. package/dist/{vue → components/sheet}/use-formula-ref-select.js +1 -1
  50. package/dist/{vue → components/sheet}/use-sheet-grid.js +22 -4
  51. package/dist/{vue → components/sheet}/use-sheet-state.js +6 -5
  52. package/dist/{vue → components/sheet}/use-sheet-tabs-bar.js +1 -1
  53. package/dist/{vue → components/sheet}/use-tool-groups.js +2 -2
  54. package/dist/{vue → components/sheet}/use-tool-popup.js +2 -2
  55. package/dist/{vue → components/sheet}/use-toolbar-scroll.js +1 -1
  56. package/dist/index.d.ts +17 -3
  57. package/dist/index.js +13 -3
  58. package/dist/report/binding.d.ts +83 -0
  59. package/dist/report/binding.js +332 -0
  60. package/dist/report/connector.d.ts +66 -0
  61. package/dist/report/connector.js +159 -0
  62. package/dist/report/export-xlsx.d.ts +17 -0
  63. package/dist/report/export-xlsx.js +92 -0
  64. package/dist/report/filter-bar.d.ts +10 -0
  65. package/dist/report/filter-bar.js +31 -0
  66. package/dist/report/index.d.ts +12 -0
  67. package/dist/report/params.d.ts +13 -0
  68. package/dist/report/params.js +61 -0
  69. package/dist/report/render/aggregate.js +169 -0
  70. package/dist/report/render/builder.d.ts +12 -0
  71. package/dist/report/render/builder.js +139 -0
  72. package/dist/report/render/coordinate.d.ts +25 -0
  73. package/dist/report/render/coordinate.js +733 -0
  74. package/dist/report/render/index.d.ts +14 -0
  75. package/dist/report/render/index.js +21 -0
  76. package/dist/report/render/style-resolver.js +50 -0
  77. package/dist/report/render/template-index.d.ts +24 -0
  78. package/dist/report/render/template-index.js +137 -0
  79. package/dist/report/rules/evaluate.d.ts +25 -0
  80. package/dist/report/rules/evaluate.js +82 -0
  81. package/dist/report/rules/index.d.ts +2 -0
  82. package/dist/report/template.d.ts +63 -0
  83. package/dist/report/template.js +83 -0
  84. package/dist/report/types.d.ts +85 -0
  85. package/dist/tools/apply-style.js +29 -0
  86. package/dist/tools/builtin.js +17 -17
  87. package/dist/tools/context.d.ts +39 -6
  88. package/dist/tools/context.js +38 -1
  89. package/dist/types/index.d.ts +3 -2
  90. package/dist/types/report-designer.d.ts +38 -0
  91. package/dist/types/report-viewer.d.ts +38 -0
  92. package/dist/types/sheet.d.ts +21 -1
  93. package/package.json +6 -8
  94. package/src/components/report/__test__/conditional-rules-helpers.test.ts +88 -0
  95. package/src/components/report/__test__/field-panel-helpers.test.ts +65 -0
  96. package/src/components/report/__test__/float-panel-position.test.ts +45 -0
  97. package/src/components/report/__test__/preview-grid-size.test.ts +63 -0
  98. package/src/components/report/__test__/report-designer.test.ts +464 -0
  99. package/src/components/report/__test__/report-viewer.test.ts +357 -0
  100. package/src/components/report/__test__/topology.test.ts +143 -0
  101. package/src/components/report/__test__/use-report-designer.test.ts +697 -0
  102. package/src/components/report/binding-badge.ts +92 -0
  103. package/src/components/report/designer/cell-coords.ts +94 -0
  104. package/src/components/report/designer/col-widths.ts +56 -0
  105. package/src/components/report/designer/conditional-rules/helpers.ts +189 -0
  106. package/src/components/report/designer/drop-highlight-overlay.vue +74 -0
  107. package/src/components/report/designer/float-panel.vue +431 -0
  108. package/src/components/report/designer/role.ts +32 -0
  109. package/src/components/report/designer/rule-preview.vue +40 -0
  110. package/src/components/report/designer/rule-row.vue +252 -0
  111. package/src/components/report/designer/rules-dialog.vue +110 -0
  112. package/src/components/report/designer/topology-overlay.vue +127 -0
  113. package/src/components/report/designer/topology.ts +124 -0
  114. package/src/components/report/designer/use-grid-overlay.ts +113 -0
  115. package/src/components/report/designer-hub.vue +198 -0
  116. package/src/components/report/field-panel-helpers.ts +44 -0
  117. package/src/components/report/field-panel.vue +136 -0
  118. package/src/components/report/filter-bar.vue +77 -0
  119. package/src/components/report/hub-connection-form.vue +124 -0
  120. package/src/components/report/hub-dataset-editor.vue +369 -0
  121. package/src/components/report/index.ts +2 -0
  122. package/src/components/report/preview-grid-size.ts +33 -0
  123. package/src/components/report/report-designer.vue +420 -0
  124. package/src/components/report/report-viewer.vue +136 -0
  125. package/src/components/report/style-designer.scss +587 -0
  126. package/src/components/report/style-hub.scss +292 -0
  127. package/src/components/report/style-viewer.scss +104 -0
  128. package/src/components/report/style.scss +3 -0
  129. package/src/components/report/style.ts +20 -0
  130. package/src/components/report/use-report-designer.ts +596 -0
  131. package/src/components/report/use-report-viewer.ts +88 -0
  132. package/src/{vue → components/sheet}/__test__/formula-bar.test.ts +37 -18
  133. package/src/{vue → components/sheet}/__test__/insert-image-popup.test.ts +1 -1
  134. package/src/{vue → components/sheet}/__test__/insert-image.test.ts +1 -1
  135. package/src/{vue → components/sheet}/__test__/sheet-component.test.ts +77 -3
  136. package/src/{vue → components/sheet}/__test__/sheet-context-menu.test.ts +4 -3
  137. package/src/{vue → components/sheet}/formula-bar.vue +1 -1
  138. package/src/{vue → components/sheet}/insert-image.ts +1 -1
  139. package/src/{vue → components/sheet}/popup-helpers.ts +1 -1
  140. package/src/{vue → components/sheet}/popups/border-popup.vue +32 -4
  141. package/src/{vue → components/sheet}/popups/export-popup.vue +2 -2
  142. package/src/{vue → components/sheet}/popups/fill-color-popup.vue +3 -3
  143. package/src/{vue → components/sheet}/popups/find-popup.vue +1 -1
  144. package/src/{vue → components/sheet}/popups/font-color-popup.vue +3 -3
  145. package/src/{vue → components/sheet}/popups/font-size-popup.vue +3 -3
  146. package/src/{vue → components/sheet}/popups/insert-image-popup.vue +1 -1
  147. package/src/{vue → components/sheet}/sheet-context-menu.ts +10 -4
  148. package/src/{vue → components/sheet}/sheet-toolbar.vue +2 -2
  149. package/src/{vue → components/sheet}/sheet.vue +6 -3
  150. package/src/{vue → components/sheet}/use-find-replace.ts +1 -1
  151. package/src/{vue → components/sheet}/use-sheet-grid.ts +23 -5
  152. package/src/{vue → components/sheet}/use-sheet-state.ts +11 -6
  153. package/src/{vue → components/sheet}/use-tool-groups.ts +2 -2
  154. package/src/{vue → components/sheet}/use-tool-popup.ts +2 -2
  155. package/src/index.ts +24 -188
  156. package/src/report/__test__/binding.test.ts +320 -0
  157. package/src/report/__test__/connector.test.ts +239 -0
  158. package/src/report/__test__/export-xlsx.test.ts +118 -0
  159. package/src/report/__test__/filter-bar.test.ts +58 -0
  160. package/src/report/__test__/params.test.ts +156 -0
  161. package/src/report/__test__/render.test.ts +830 -0
  162. package/src/report/__test__/rules.test.ts +131 -0
  163. package/src/report/__test__/template.test.ts +366 -0
  164. package/src/report/binding.ts +474 -0
  165. package/src/report/connector.ts +206 -0
  166. package/src/report/export-xlsx.ts +113 -0
  167. package/src/report/filter-bar.ts +36 -0
  168. package/src/report/index.ts +13 -0
  169. package/src/report/params.ts +96 -0
  170. package/src/report/render/__test__/aggregate.test.ts +16 -0
  171. package/src/report/render/__test__/coordinate.test.ts +405 -0
  172. package/src/report/render/aggregate.ts +261 -0
  173. package/src/report/render/builder.ts +211 -0
  174. package/src/report/render/coordinate.ts +1239 -0
  175. package/src/report/render/index.ts +30 -0
  176. package/src/report/render/style-resolver.ts +68 -0
  177. package/src/report/render/template-index.ts +200 -0
  178. package/src/report/rules/evaluate.ts +113 -0
  179. package/src/report/rules/index.ts +7 -0
  180. package/src/report/template.ts +145 -0
  181. package/src/report/types.ts +96 -0
  182. package/src/tools/__test__/apply-style.test.ts +129 -0
  183. package/src/tools/apply-style.ts +41 -0
  184. package/src/tools/builtin.ts +20 -20
  185. package/src/tools/context.ts +67 -4
  186. package/src/tools/index.ts +1 -0
  187. package/src/types/index.ts +2 -6
  188. package/src/types/report-designer.ts +38 -0
  189. package/src/types/report-viewer.ts +37 -0
  190. package/src/types/sheet.ts +26 -1
  191. /package/dist/{vue → components/sheet}/style.d.ts +0 -0
  192. /package/dist/{vue → components/sheet}/style.js +0 -0
  193. /package/dist/{vue → components/sheet}/style2.css +0 -0
  194. /package/dist/{vue → components/sheet}/use-formula-suggest.js +0 -0
  195. /package/src/{vue → components/sheet}/__test__/import-file.test.ts +0 -0
  196. /package/src/{vue → components/sheet}/__test__/insert-count-menu-item.test.ts +0 -0
  197. /package/src/{vue → components/sheet}/__test__/use-formula-ref-select.test.ts +0 -0
  198. /package/src/{vue → components/sheet}/__test__/use-formula-suggest.test.ts +0 -0
  199. /package/src/{vue → components/sheet}/formula-suggest-list.vue +0 -0
  200. /package/src/{vue → components/sheet}/import-file.ts +0 -0
  201. /package/src/{vue → components/sheet}/index.ts +0 -0
  202. /package/src/{vue → components/sheet}/insert-count-menu-item.vue +0 -0
  203. /package/src/{vue → components/sheet}/parsing.ts +0 -0
  204. /package/src/{vue → components/sheet}/popups/border-preset-glyph.vue +0 -0
  205. /package/src/{vue → components/sheet}/popups/import.worker.ts +0 -0
  206. /package/src/{vue → components/sheet}/sheet-tabs.vue +0 -0
  207. /package/src/{vue → components/sheet}/style.scss +0 -0
  208. /package/src/{vue → components/sheet}/style.ts +0 -0
  209. /package/src/{vue → components/sheet}/use-formula-ref-select.ts +0 -0
  210. /package/src/{vue → components/sheet}/use-formula-suggest.ts +0 -0
  211. /package/src/{vue → components/sheet}/use-sheet-tabs-bar.ts +0 -0
  212. /package/src/{vue → components/sheet}/use-toolbar-scroll.ts +0 -0
@@ -0,0 +1,239 @@
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
+ })
@@ -0,0 +1,118 @@
1
+ import { Sheet } from '@veltra/sheet-core'
2
+ import { buildWorkbookFromHucre } from '@veltra/sheet-core/core/io/import'
3
+ import { readXlsx } from 'hucre/xlsx'
4
+ import { describe, expect, it } from 'vitest'
5
+
6
+ import { REPORT_META_NAMESPACE, createReportBinding } from '../binding'
7
+ import { buildColumnDefs, exportFilledReportXlsx, pxToExcelColWidth } from '../export-xlsx'
8
+ import { renderReport } from '../render'
9
+ import type { DatasetCatalogItem } from '../types'
10
+
11
+ // ---- 内联 fixtures ----
12
+
13
+ const ORDERS_DATASET: DatasetCatalogItem = {
14
+ id: 'orders',
15
+ label: '销售明细',
16
+ fields: [
17
+ { name: 'customer', label: '客户', type: 'string' },
18
+ { name: 'amount', label: '金额', type: 'number' }
19
+ ]
20
+ }
21
+
22
+ const ORDER_ROWS: Record<string, unknown>[] = [
23
+ { customer: '甲公司', amount: 100 },
24
+ { customer: '乙公司', amount: 400 }
25
+ ]
26
+
27
+ /** 金额 > 300 标红(条件样式打平进快照后随导出保真) */
28
+ const RED_RULE = { operator: 'gt' as const, value: 300, style: { font: { color: '#DC2626' } } }
29
+
30
+ /** 分组(客户)+ 明细(金额,带条件规则)模板 */
31
+ function buildFilledSheet(): Sheet {
32
+ const sheet = new Sheet()
33
+ sheet.setCells([
34
+ { addr: { row: 0, col: 0 }, data: { v: '客户' } },
35
+ { addr: { row: 0, col: 1 }, data: { v: '金额' } }
36
+ ])
37
+ sheet.setCellStyle(
38
+ { start: { row: 0, col: 0 }, end: { row: 0, col: 1 } },
39
+ { font: { bold: true }, fill: { color: '#E8EEF7' } }
40
+ )
41
+
42
+ const groupAddr = { row: 1, col: 0 }
43
+ const group = createReportBinding(ORDERS_DATASET, 'customer')
44
+ group.preset = 'groupHeader'
45
+ group.aggregate = 'group'
46
+ group.expand = 'down'
47
+ sheet.setCellMeta(groupAddr, REPORT_META_NAMESPACE, group)
48
+
49
+ const amount = createReportBinding(ORDERS_DATASET, 'amount')
50
+ amount.rowParent = groupAddr
51
+ amount.conditionalRules = [RED_RULE]
52
+ sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, amount)
53
+
54
+ const filled = renderReport(sheet.snapshot(), { orders: ORDER_ROWS })
55
+ const filledSheet = new Sheet()
56
+ filledSheet.restore(filled)
57
+ filledSheet.restoreContent(filled)
58
+ return filledSheet
59
+ }
60
+
61
+ describe('exportFilledReportXlsx', () => {
62
+ it('导出保真 XLSX:值 / 表头样式 / 条件样式打平后随快照导出', async () => {
63
+ const filled = buildFilledSheet()
64
+ filled.setColWidth(0, 120)
65
+ filled.setColWidth(1, 140)
66
+ const buffer = await exportFilledReportXlsx(filled)
67
+
68
+ // 合法 ZIP(xlsx)字节
69
+ expect(buffer[0]).toBe(0x50)
70
+ expect(buffer[1]).toBe(0x4b)
71
+
72
+ const hucreWb = await readXlsx(buffer, { readStyles: true })
73
+ const workbook = buildWorkbookFromHucre(hucreWb)
74
+ const sheet = workbook.getSheets()[0]!
75
+
76
+ // 展开值保真(分组头 + 明细值)
77
+ const values: unknown[] = []
78
+ for (const [, data] of sheet.store.entries()) values.push(data.v)
79
+ expect(values).toContain('甲公司')
80
+ expect(values).toContain('乙公司')
81
+ expect(values).toContain(400)
82
+
83
+ // 条件样式打平:amount=400(>300)的明细格带红字
84
+ let redFound = false
85
+ for (const [, data] of sheet.store.entries()) {
86
+ if (data.v !== 400 || data.s == null) continue
87
+ const style = sheet.stylePool.get(data.s)
88
+ if (style?.font?.color === '#DC2626') redFound = true
89
+ }
90
+ expect(redFound).toBe(true)
91
+
92
+ // 表头静态样式保真(加粗 + 底色)
93
+ const header = sheet.getCellData({ row: 0, col: 0 })
94
+ const headerStyle = header?.s != null ? sheet.stylePool.get(header.s) : undefined
95
+ expect(headerStyle?.font?.bold).toBe(true)
96
+ expect(headerStyle?.fill?.color).toBe('#E8EEF7')
97
+ })
98
+ })
99
+
100
+ describe('buildColumnDefs / pxToExcelColWidth', () => {
101
+ it('像素列宽换算为 Excel 字符宽度并写入对应列', () => {
102
+ expect(pxToExcelColWidth(120)).toBe(16)
103
+ expect(pxToExcelColWidth(0)).toBe(1)
104
+
105
+ const columns = buildColumnDefs([
106
+ [0, 120],
107
+ [2, 145]
108
+ ])
109
+ expect(columns).toHaveLength(3)
110
+ expect(columns[0]).toEqual({ width: 16 })
111
+ expect(columns[1]).toEqual({})
112
+ expect(columns[2]).toEqual({ width: 20 })
113
+ })
114
+
115
+ it('空列宽不产生 columns 定义', () => {
116
+ expect(buildColumnDefs([])).toEqual([])
117
+ })
118
+ })
@@ -0,0 +1,58 @@
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
+ })
@@ -0,0 +1,156 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { buildParamDefs, extractParamIds, resolveBoundDatasetParams } from '../params'
4
+ import type { QueryParamDef } from '../types'
5
+
6
+ describe('report params', () => {
7
+ describe('extractParamIds', () => {
8
+ it('从 SQL 提取 ${param} 占位符并去重', () => {
9
+ expect(
10
+ extractParamIds(
11
+ 'SELECT * FROM orders WHERE order_date >= ${dateFrom} AND order_date <= ${dateTo}'
12
+ )
13
+ ).toEqual(['dateFrom', 'dateTo'])
14
+
15
+ expect(
16
+ extractParamIds("SELECT * FROM orders WHERE region = ${region} AND ${region} != ''")
17
+ ).toEqual(['region'])
18
+ expect(extractParamIds('SELECT * FROM orders')).toEqual([])
19
+ })
20
+
21
+ it('忽略空白占位符与未闭合占位符', () => {
22
+ expect(extractParamIds('WHERE a = ${ } AND b = ${ x }')).toEqual(['x'])
23
+ expect(extractParamIds('WHERE a = ${unclosed')).toEqual([])
24
+ })
25
+ })
26
+
27
+ describe('buildParamDefs', () => {
28
+ it('等值比较推断 number 类型(旧行为;编辑器通常覆盖为 select/text)', () => {
29
+ expect(buildParamDefs('SELECT customer FROM orders WHERE region = ${region}')).toEqual([
30
+ { id: 'region', label: 'region', type: 'number', defaultValue: 0 }
31
+ ])
32
+ })
33
+
34
+ it('BETWEEN 单参数推断 date-range 类型', () => {
35
+ const params = buildParamDefs(
36
+ 'SELECT customer FROM orders WHERE order_date BETWEEN ${dateRange}'
37
+ )
38
+ expect(params).toHaveLength(1)
39
+ expect(params[0]?.id).toBe('dateRange')
40
+ expect(params[0]?.type).toBe('date-range')
41
+ expect(params[0]?.defaultValue).toEqual(['', ''])
42
+ })
43
+
44
+ it('BETWEEN 双参数:首参推断 date,次参无 AND 后续回落 text(旧行为)', () => {
45
+ const sql = 'SELECT customer FROM orders WHERE order_date BETWEEN ${dateFrom} AND ${dateTo}'
46
+ const params = buildParamDefs(sql)
47
+ const types = Object.fromEntries(params.map((p) => [p.id, p.type]))
48
+ expect(types).toEqual({ dateFrom: 'date', dateTo: 'text' })
49
+ })
50
+
51
+ it('LIKE 推断 text、数值比较推断 number', () => {
52
+ expect(
53
+ buildParamDefs('SELECT customer FROM orders WHERE customer LIKE ${keyword}')[0]?.type
54
+ ).toBe('text')
55
+ expect(
56
+ buildParamDefs('SELECT customer FROM orders WHERE amount > ${minAmount}')[0]?.type
57
+ ).toBe('number')
58
+ })
59
+
60
+ it('元数据覆盖 label / 类型 / 默认值 / 选项', () => {
61
+ const params = buildParamDefs('SELECT customer FROM orders WHERE region = ${region}', {
62
+ region: {
63
+ label: '地区',
64
+ type: 'select',
65
+ defaultValue: '',
66
+ options: [
67
+ { label: '华东', value: '华东' },
68
+ { label: '华南', value: '华南' }
69
+ ]
70
+ }
71
+ })
72
+ expect(params[0]).toEqual({
73
+ id: 'region',
74
+ label: '地区',
75
+ type: 'select',
76
+ defaultValue: '',
77
+ options: [
78
+ { label: '华东', value: '华东' },
79
+ { label: '华南', value: '华南' }
80
+ ]
81
+ })
82
+ })
83
+ })
84
+
85
+ describe('resolveBoundDatasetParams', () => {
86
+ const DATE_FROM: QueryParamDef = {
87
+ id: 'dateFrom',
88
+ label: '开始日期',
89
+ type: 'date',
90
+ defaultValue: '2024-01-01'
91
+ }
92
+ const DATE_TO: QueryParamDef = {
93
+ id: 'dateTo',
94
+ label: '结束日期',
95
+ type: 'date',
96
+ defaultValue: '2024-12-31'
97
+ }
98
+ const REGION: QueryParamDef = {
99
+ id: 'region',
100
+ label: '地区',
101
+ type: 'select',
102
+ defaultValue: '',
103
+ options: [{ label: '华东', value: '华东' }]
104
+ }
105
+ const ALERT_THRESHOLD: QueryParamDef = {
106
+ id: 'alertThreshold',
107
+ label: '库存预警阈值',
108
+ type: 'number',
109
+ defaultValue: 80
110
+ }
111
+
112
+ const datasetParams: Record<string, QueryParamDef[]> = {
113
+ orders: [DATE_FROM, DATE_TO, REGION],
114
+ customers: [REGION],
115
+ 'sales-matrix': [REGION],
116
+ 'inventory-alerts': [ALERT_THRESHOLD]
117
+ }
118
+
119
+ it('按绑定数据集合并查询参数并去重', () => {
120
+ const merged = resolveBoundDatasetParams(
121
+ [],
122
+ ['orders', 'customers'],
123
+ (id) => datasetParams[id] ?? []
124
+ )
125
+ expect(merged.map((item) => item.id).sort()).toEqual(['dateFrom', 'dateTo', 'region'])
126
+
127
+ const matrix = resolveBoundDatasetParams(
128
+ [],
129
+ ['sales-matrix'],
130
+ (id) => datasetParams[id] ?? []
131
+ )
132
+ expect(matrix.map((item) => item.id)).toEqual(['region'])
133
+
134
+ const inventory = resolveBoundDatasetParams(
135
+ [],
136
+ ['inventory-alerts'],
137
+ (id) => datasetParams[id] ?? []
138
+ )
139
+ expect(inventory.map((item) => item.id)).toEqual(['alertThreshold'])
140
+ })
141
+
142
+ it('无绑定数据集时回退全局参数', () => {
143
+ const fallback = resolveBoundDatasetParams([REGION], [], () => [])
144
+ expect(fallback.map((item) => item.id)).toEqual(['region'])
145
+ })
146
+
147
+ it('同名参数先见为准', () => {
148
+ const overridden: QueryParamDef = { ...REGION, label: '覆盖地区' }
149
+ const merged = resolveBoundDatasetParams([], ['orders', 'customers'], (id) =>
150
+ id === 'orders' ? [REGION] : [overridden]
151
+ )
152
+ expect(merged).toHaveLength(1)
153
+ expect(merged[0]?.label).toBe('地区')
154
+ })
155
+ })
156
+ })