@veltra/sheet 2.0.1 → 2.2.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 (339) hide show
  1. package/dist/components/report/binding-badge.js +76 -0
  2. package/dist/components/report/designer/cell-coords.js +46 -0
  3. package/dist/components/report/designer/col-widths.js +15 -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 +385 -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 +269 -0
  10. package/dist/components/report/designer/rules-dialog.js +135 -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/report-designer.js +343 -0
  21. package/dist/components/report/report-viewer.js +111 -0
  22. package/dist/components/report/style.d.ts +17 -0
  23. package/dist/components/report/style.js +18 -0
  24. package/dist/components/report/style2.css +869 -0
  25. package/dist/components/report/use-report-designer.js +360 -0
  26. package/dist/components/report/use-report-viewer.js +50 -0
  27. package/dist/components/sheet/formula-bar.js +393 -0
  28. package/dist/components/sheet/formula-suggest-list.js +66 -0
  29. package/dist/components/sheet/import-file.js +140 -0
  30. package/dist/components/sheet/insert-count-menu-item.js +68 -0
  31. package/dist/components/sheet/insert-image.js +79 -0
  32. package/dist/components/sheet/popup-helpers.js +73 -0
  33. package/dist/components/sheet/popups/border-popup.js +75 -0
  34. package/dist/components/sheet/popups/border-preset-glyph.js +106 -0
  35. package/dist/components/sheet/popups/export-popup.js +51 -0
  36. package/dist/components/sheet/popups/fill-color-popup.js +37 -0
  37. package/dist/components/sheet/popups/find-popup.js +113 -0
  38. package/dist/components/sheet/popups/font-color-popup.js +37 -0
  39. package/dist/components/sheet/popups/font-size-popup.js +63 -0
  40. package/dist/components/sheet/popups/import.worker.d.ts +21 -0
  41. package/dist/components/sheet/popups/import.worker.js +41 -0
  42. package/dist/components/sheet/popups/insert-image-popup.js +38 -0
  43. package/dist/components/sheet/sheet-context-menu.js +200 -0
  44. package/dist/components/sheet/sheet-tabs.js +162 -0
  45. package/dist/components/sheet/sheet-toolbar.js +106 -0
  46. package/dist/components/sheet/sheet.js +198 -0
  47. package/dist/components/sheet/style.d.ts +11 -0
  48. package/dist/components/sheet/style.js +12 -0
  49. package/dist/components/sheet/style2.css +654 -0
  50. package/dist/components/sheet/use-find-replace.js +161 -0
  51. package/dist/components/sheet/use-formula-ref-select.js +37 -0
  52. package/dist/components/sheet/use-formula-suggest.js +126 -0
  53. package/dist/components/sheet/use-sheet-grid.js +188 -0
  54. package/dist/components/sheet/use-sheet-state.js +122 -0
  55. package/dist/components/sheet/use-sheet-tabs-bar.js +90 -0
  56. package/dist/components/sheet/use-tool-groups.js +42 -0
  57. package/dist/components/sheet/use-tool-popup.js +123 -0
  58. package/dist/components/sheet/use-toolbar-scroll.js +72 -0
  59. package/dist/index.d.ts +18 -26
  60. package/dist/index.js +14 -24
  61. package/dist/report/binding.d.ts +45 -0
  62. package/dist/report/binding.js +175 -0
  63. package/dist/report/connector.d.ts +66 -0
  64. package/dist/report/connector.js +159 -0
  65. package/dist/report/export-xlsx.d.ts +19 -0
  66. package/dist/report/export-xlsx.js +95 -0
  67. package/dist/report/filter-bar.d.ts +10 -0
  68. package/dist/report/filter-bar.js +31 -0
  69. package/dist/report/index.d.ts +12 -0
  70. package/dist/report/params.d.ts +13 -0
  71. package/dist/report/params.js +61 -0
  72. package/dist/report/render/aggregate.js +157 -0
  73. package/dist/report/render/builder.d.ts +12 -0
  74. package/dist/report/render/builder.js +139 -0
  75. package/dist/report/render/coordinate.d.ts +25 -0
  76. package/dist/report/render/coordinate.js +733 -0
  77. package/dist/report/render/index.d.ts +14 -0
  78. package/dist/report/render/index.js +21 -0
  79. package/dist/report/render/style-resolver.js +50 -0
  80. package/dist/report/render/template-index.d.ts +24 -0
  81. package/dist/report/render/template-index.js +137 -0
  82. package/dist/report/rules/evaluate.d.ts +25 -0
  83. package/dist/report/rules/evaluate.js +82 -0
  84. package/dist/report/rules/index.d.ts +2 -0
  85. package/dist/report/template.d.ts +59 -0
  86. package/dist/report/template.js +80 -0
  87. package/dist/report/types.d.ts +85 -0
  88. package/dist/tools/builtin.js +279 -8
  89. package/dist/tools/context.d.ts +54 -11
  90. package/dist/tools/context.js +28 -5
  91. package/dist/tools/download.js +68 -0
  92. package/dist/tools/export.worker.d.ts +16 -0
  93. package/dist/tools/export.worker.js +29 -0
  94. package/dist/tools/registry.d.ts +7 -4
  95. package/dist/tools/registry.js +1 -3
  96. package/dist/types/index.d.ts +3 -1
  97. package/dist/types/report-designer.d.ts +36 -0
  98. package/dist/types/report-viewer.d.ts +38 -0
  99. package/dist/types/sheet.d.ts +22 -5
  100. package/package.json +10 -7
  101. package/src/components/report/__test__/conditional-rules-helpers.test.ts +87 -0
  102. package/src/components/report/__test__/field-panel-helpers.test.ts +65 -0
  103. package/src/components/report/__test__/float-panel-position.test.ts +45 -0
  104. package/src/components/report/__test__/report-designer.test.ts +388 -0
  105. package/src/components/report/__test__/report-viewer.test.ts +334 -0
  106. package/src/components/report/__test__/topology.test.ts +143 -0
  107. package/src/components/report/__test__/use-report-designer.test.ts +565 -0
  108. package/src/components/report/binding-badge.ts +73 -0
  109. package/src/components/report/designer/cell-coords.ts +94 -0
  110. package/src/components/report/designer/col-widths.ts +33 -0
  111. package/src/components/report/designer/conditional-rules/helpers.ts +186 -0
  112. package/src/components/report/designer/drop-highlight-overlay.vue +74 -0
  113. package/src/components/report/designer/float-panel.vue +387 -0
  114. package/src/components/report/designer/role.ts +32 -0
  115. package/src/components/report/designer/rule-preview.vue +40 -0
  116. package/src/components/report/designer/rule-row.vue +252 -0
  117. package/src/components/report/designer/rules-dialog.vue +126 -0
  118. package/src/components/report/designer/topology-overlay.vue +127 -0
  119. package/src/components/report/designer/topology.ts +124 -0
  120. package/src/components/report/designer/use-grid-overlay.ts +113 -0
  121. package/src/components/report/designer-hub.vue +198 -0
  122. package/src/components/report/field-panel-helpers.ts +44 -0
  123. package/src/components/report/field-panel.vue +136 -0
  124. package/src/components/report/filter-bar.vue +77 -0
  125. package/src/components/report/hub-connection-form.vue +124 -0
  126. package/src/components/report/hub-dataset-editor.vue +369 -0
  127. package/src/components/report/index.ts +2 -0
  128. package/src/components/report/report-designer.vue +356 -0
  129. package/src/components/report/report-viewer.vue +128 -0
  130. package/src/components/report/style-designer.scss +583 -0
  131. package/src/components/report/style-hub.scss +292 -0
  132. package/src/components/report/style-viewer.scss +104 -0
  133. package/src/components/report/style.scss +3 -0
  134. package/src/components/report/style.ts +20 -0
  135. package/src/components/report/use-report-designer.ts +547 -0
  136. package/src/components/report/use-report-viewer.ts +79 -0
  137. package/src/components/sheet/__test__/formula-bar.test.ts +474 -0
  138. package/src/components/sheet/__test__/import-file.test.ts +183 -0
  139. package/src/components/sheet/__test__/insert-count-menu-item.test.ts +123 -0
  140. package/src/components/sheet/__test__/insert-image-popup.test.ts +89 -0
  141. package/src/components/sheet/__test__/insert-image.test.ts +100 -0
  142. package/src/components/sheet/__test__/sheet-component.test.ts +993 -0
  143. package/src/components/sheet/__test__/sheet-context-menu.test.ts +206 -0
  144. package/src/components/sheet/__test__/use-formula-ref-select.test.ts +35 -0
  145. package/src/components/sheet/__test__/use-formula-suggest.test.ts +83 -0
  146. package/src/components/sheet/formula-bar.vue +473 -0
  147. package/src/components/sheet/formula-suggest-list.vue +64 -0
  148. package/src/components/sheet/import-file.ts +173 -0
  149. package/src/components/sheet/insert-count-menu-item.vue +69 -0
  150. package/src/components/sheet/insert-image.ts +90 -0
  151. package/src/components/sheet/parsing.ts +11 -0
  152. package/src/components/sheet/popup-helpers.ts +50 -0
  153. package/src/components/sheet/popups/border-popup.vue +93 -0
  154. package/src/components/sheet/popups/border-preset-glyph.vue +111 -0
  155. package/src/components/sheet/popups/export-popup.vue +52 -0
  156. package/src/components/sheet/popups/fill-color-popup.vue +39 -0
  157. package/src/components/sheet/popups/find-popup.vue +111 -0
  158. package/src/components/sheet/popups/font-color-popup.vue +39 -0
  159. package/src/components/sheet/popups/font-size-popup.vue +52 -0
  160. package/src/components/sheet/popups/import.worker.ts +66 -0
  161. package/src/components/sheet/popups/insert-image-popup.vue +39 -0
  162. package/src/components/sheet/sheet-context-menu.ts +229 -0
  163. package/src/components/sheet/sheet-tabs.vue +162 -0
  164. package/src/components/sheet/sheet-toolbar.vue +91 -0
  165. package/src/components/sheet/sheet.vue +211 -0
  166. package/src/components/sheet/style.scss +737 -0
  167. package/src/components/sheet/style.ts +13 -0
  168. package/src/components/sheet/use-find-replace.ts +200 -0
  169. package/src/components/sheet/use-formula-ref-select.ts +34 -0
  170. package/src/components/sheet/use-formula-suggest.ts +150 -0
  171. package/src/components/sheet/use-sheet-grid.ts +247 -0
  172. package/src/components/sheet/use-sheet-state.ts +161 -0
  173. package/src/components/sheet/use-sheet-tabs-bar.ts +115 -0
  174. package/src/components/sheet/use-tool-groups.ts +61 -0
  175. package/src/components/sheet/use-tool-popup.ts +150 -0
  176. package/src/components/sheet/use-toolbar-scroll.ts +95 -0
  177. package/src/env.d.ts +11 -0
  178. package/src/index.ts +39 -14
  179. package/src/report/__test__/binding.test.ts +218 -0
  180. package/src/report/__test__/connector.test.ts +218 -0
  181. package/src/report/__test__/export-xlsx.test.ts +118 -0
  182. package/src/report/__test__/filter-bar.test.ts +58 -0
  183. package/src/report/__test__/params.test.ts +156 -0
  184. package/src/report/__test__/render.test.ts +728 -0
  185. package/src/report/__test__/rules.test.ts +131 -0
  186. package/src/report/__test__/template.test.ts +355 -0
  187. package/src/report/binding.ts +243 -0
  188. package/src/report/connector.ts +205 -0
  189. package/src/report/export-xlsx.ts +118 -0
  190. package/src/report/filter-bar.ts +36 -0
  191. package/src/report/index.ts +13 -0
  192. package/src/report/params.ts +96 -0
  193. package/src/report/render/__test__/aggregate.test.ts +16 -0
  194. package/src/report/render/__test__/coordinate.test.ts +405 -0
  195. package/src/report/render/aggregate.ts +240 -0
  196. package/src/report/render/builder.ts +211 -0
  197. package/src/report/render/coordinate.ts +1239 -0
  198. package/src/report/render/index.ts +30 -0
  199. package/src/report/render/style-resolver.ts +68 -0
  200. package/src/report/render/template-index.ts +200 -0
  201. package/src/report/rules/evaluate.ts +113 -0
  202. package/src/report/rules/index.ts +7 -0
  203. package/src/report/template.ts +141 -0
  204. package/src/report/types.ts +96 -0
  205. package/src/tools/__test__/builtin.test.ts +208 -66
  206. package/src/tools/__test__/context.test.ts +62 -2
  207. package/src/tools/builtin.ts +350 -6
  208. package/src/tools/context.ts +89 -11
  209. package/src/tools/download.ts +82 -0
  210. package/src/tools/export.worker.ts +61 -0
  211. package/src/tools/registry.ts +16 -1
  212. package/src/types/index.ts +2 -0
  213. package/src/types/report-designer.ts +36 -0
  214. package/src/types/report-viewer.ts +37 -0
  215. package/src/types/sheet.ts +26 -3
  216. package/dist/core/address.d.ts +0 -60
  217. package/dist/core/address.js +0 -142
  218. package/dist/core/address.js.map +0 -1
  219. package/dist/core/cell-store.d.ts +0 -61
  220. package/dist/core/cell-store.js +0 -135
  221. package/dist/core/cell-store.js.map +0 -1
  222. package/dist/core/command/default-registry.d.ts +0 -10
  223. package/dist/core/command/default-registry.js +0 -18
  224. package/dist/core/command/default-registry.js.map +0 -1
  225. package/dist/core/command/history.d.ts +0 -64
  226. package/dist/core/command/history.js +0 -127
  227. package/dist/core/command/history.js.map +0 -1
  228. package/dist/core/command/merge-cells.d.ts +0 -21
  229. package/dist/core/command/merge-cells.js +0 -74
  230. package/dist/core/command/merge-cells.js.map +0 -1
  231. package/dist/core/command/registry.d.ts +0 -17
  232. package/dist/core/command/registry.js +0 -26
  233. package/dist/core/command/registry.js.map +0 -1
  234. package/dist/core/command/set-cell-formula.d.ts +0 -17
  235. package/dist/core/command/set-cell-formula.js +0 -29
  236. package/dist/core/command/set-cell-formula.js.map +0 -1
  237. package/dist/core/command/set-cell-value.d.ts +0 -22
  238. package/dist/core/command/set-cell-value.js +0 -35
  239. package/dist/core/command/set-cell-value.js.map +0 -1
  240. package/dist/core/command/types.d.ts +0 -68
  241. package/dist/core/events.js +0 -29
  242. package/dist/core/events.js.map +0 -1
  243. package/dist/core/fill.d.ts +0 -34
  244. package/dist/core/fill.js +0 -320
  245. package/dist/core/fill.js.map +0 -1
  246. package/dist/core/formula/ast.d.ts +0 -58
  247. package/dist/core/formula/ast.js +0 -38
  248. package/dist/core/formula/ast.js.map +0 -1
  249. package/dist/core/formula/dependency-graph.d.ts +0 -70
  250. package/dist/core/formula/dependency-graph.js +0 -269
  251. package/dist/core/formula/dependency-graph.js.map +0 -1
  252. package/dist/core/formula/errors.d.ts +0 -25
  253. package/dist/core/formula/errors.js +0 -36
  254. package/dist/core/formula/errors.js.map +0 -1
  255. package/dist/core/formula/evaluator.d.ts +0 -41
  256. package/dist/core/formula/evaluator.js +0 -154
  257. package/dist/core/formula/evaluator.js.map +0 -1
  258. package/dist/core/formula/functions.d.ts +0 -30
  259. package/dist/core/formula/functions.js +0 -209
  260. package/dist/core/formula/functions.js.map +0 -1
  261. package/dist/core/formula/parser.d.ts +0 -7
  262. package/dist/core/formula/parser.js +0 -208
  263. package/dist/core/formula/parser.js.map +0 -1
  264. package/dist/core/formula/tokenizer.d.ts +0 -37
  265. package/dist/core/formula/tokenizer.js +0 -146
  266. package/dist/core/formula/tokenizer.js.map +0 -1
  267. package/dist/core/merge-manager.d.ts +0 -64
  268. package/dist/core/merge-manager.js +0 -111
  269. package/dist/core/merge-manager.js.map +0 -1
  270. package/dist/core/selection.d.ts +0 -31
  271. package/dist/core/selection.js +0 -62
  272. package/dist/core/selection.js.map +0 -1
  273. package/dist/core/sheet.d.ts +0 -116
  274. package/dist/core/sheet.js +0 -224
  275. package/dist/core/sheet.js.map +0 -1
  276. package/dist/core/workbook.d.ts +0 -43
  277. package/dist/core/workbook.js +0 -77
  278. package/dist/core/workbook.js.map +0 -1
  279. package/dist/grid/sheet-grid.d.ts +0 -81
  280. package/dist/grid/sheet-grid.js +0 -331
  281. package/dist/grid/sheet-grid.js.map +0 -1
  282. package/dist/grid/vtable-theme.js +0 -80
  283. package/dist/grid/vtable-theme.js.map +0 -1
  284. package/dist/tools/builtin.js.map +0 -1
  285. package/dist/tools/context.js.map +0 -1
  286. package/dist/tools/registry.js.map +0 -1
  287. package/dist/vue/index.d.ts +0 -2
  288. package/dist/vue/sheet.js +0 -211
  289. package/dist/vue/sheet.js.map +0 -1
  290. package/dist/vue/sheet.vue.d.ts +0 -24
  291. package/dist/vue/style.d.ts +0 -1
  292. package/dist/vue/style.js +0 -2
  293. package/dist/vue/style2.css +0 -93
  294. package/src/core/__test__/address.test.ts +0 -113
  295. package/src/core/__test__/cell-store.test.ts +0 -97
  296. package/src/core/__test__/command.test.ts +0 -377
  297. package/src/core/__test__/fill.test.ts +0 -140
  298. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  299. package/src/core/__test__/formula-engine.test.ts +0 -470
  300. package/src/core/__test__/formula-functions.test.ts +0 -178
  301. package/src/core/__test__/formula-parser.test.ts +0 -160
  302. package/src/core/__test__/history.test.ts +0 -225
  303. package/src/core/__test__/merge-manager.test.ts +0 -106
  304. package/src/core/__test__/sheet.test.ts +0 -142
  305. package/src/core/address.ts +0 -166
  306. package/src/core/cell-store.ts +0 -173
  307. package/src/core/command/default-registry.ts +0 -15
  308. package/src/core/command/history.ts +0 -167
  309. package/src/core/command/index.ts +0 -7
  310. package/src/core/command/merge-cells.ts +0 -74
  311. package/src/core/command/registry.ts +0 -30
  312. package/src/core/command/set-cell-formula.ts +0 -29
  313. package/src/core/command/set-cell-value.ts +0 -38
  314. package/src/core/command/types.ts +0 -73
  315. package/src/core/events.ts +0 -37
  316. package/src/core/fill.ts +0 -314
  317. package/src/core/formula/ast.ts +0 -66
  318. package/src/core/formula/dependency-graph.ts +0 -329
  319. package/src/core/formula/errors.ts +0 -41
  320. package/src/core/formula/evaluator.ts +0 -219
  321. package/src/core/formula/functions.ts +0 -266
  322. package/src/core/formula/index.ts +0 -7
  323. package/src/core/formula/parser.ts +0 -198
  324. package/src/core/formula/tokenizer.ts +0 -160
  325. package/src/core/merge-manager.ts +0 -158
  326. package/src/core/selection.ts +0 -66
  327. package/src/core/sheet.ts +0 -282
  328. package/src/core/workbook.ts +0 -99
  329. package/src/grid/__test__/canvas-mock.ts +0 -49
  330. package/src/grid/__test__/setup.ts +0 -3
  331. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  332. package/src/grid/__test__/sheet-grid.test.ts +0 -275
  333. package/src/grid/sheet-grid.ts +0 -407
  334. package/src/grid/vtable-theme.ts +0 -61
  335. package/src/vue/__test__/sheet-component.test.ts +0 -267
  336. package/src/vue/sheet.vue +0 -235
  337. package/src/vue/style.scss +0 -94
  338. package/src/vue/style.ts +0 -2
  339. /package/src/{vue → components/sheet}/index.ts +0 -0
@@ -0,0 +1,218 @@
1
+ import type { CellAddress } from '@veltra/sheet-core'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import {
5
+ applyReportPreset,
6
+ createReportBinding,
7
+ formatBindingPlaceholder,
8
+ formatCellAddress,
9
+ inferDropPreset,
10
+ inferReportPreset,
11
+ isExpansionBandRow,
12
+ parseCellAddress,
13
+ presetBindingPatch,
14
+ setBindingCatalog
15
+ } from '../binding'
16
+ import type { DatasetCatalogItem, ReportBinding } from '../types'
17
+
18
+ /** 内联 catalog fixture(单一事实源在测试自身,不依赖 playground mock) */
19
+ const ORDERS_DATASET: DatasetCatalogItem = {
20
+ id: 'orders',
21
+ label: '销售明细',
22
+ fields: [
23
+ { name: 'customer', label: '客户', type: 'string' },
24
+ { name: 'orderNo', label: '订单号', type: 'string' },
25
+ { name: 'region', label: '地区', type: 'string' },
26
+ { name: 'amount', label: '金额', type: 'number' },
27
+ { name: 'orderDate', label: '下单日期', type: 'date' }
28
+ ]
29
+ }
30
+
31
+ setBindingCatalog([ORDERS_DATASET])
32
+
33
+ describe('report binding', () => {
34
+ it('createReportBinding 默认 list + 纵向扩展 + 明细预设', () => {
35
+ const binding = createReportBinding(ORDERS_DATASET, 'amount')
36
+ expect(binding).toEqual({
37
+ dataset: 'orders',
38
+ field: 'amount',
39
+ aggregate: 'list',
40
+ expand: 'down',
41
+ preset: 'detail',
42
+ sort: 'none',
43
+ conditionalRules: []
44
+ })
45
+ })
46
+
47
+ it('formatBindingPlaceholder 中文标签(含 list / max / min)', () => {
48
+ expect(formatBindingPlaceholder(createReportBinding(ORDERS_DATASET, 'amount'))).toBe(
49
+ '明细 · 金额'
50
+ )
51
+
52
+ const group = createReportBinding(ORDERS_DATASET, 'customer')
53
+ group.aggregate = 'group'
54
+ expect(formatBindingPlaceholder(group)).toBe('分组 · 客户')
55
+
56
+ const sum = createReportBinding(ORDERS_DATASET, 'amount')
57
+ sum.aggregate = 'sum'
58
+ sum.expand = 'none'
59
+ expect(formatBindingPlaceholder(sum)).toBe('求和 · 金额')
60
+
61
+ const max = createReportBinding(ORDERS_DATASET, 'amount')
62
+ max.aggregate = 'max'
63
+ max.expand = 'none'
64
+ expect(formatBindingPlaceholder(max)).toBe('最大 · 金额')
65
+
66
+ const min = createReportBinding(ORDERS_DATASET, 'amount')
67
+ min.aggregate = 'min'
68
+ min.expand = 'none'
69
+ expect(formatBindingPlaceholder(min)).toBe('最小 · 金额')
70
+ })
71
+
72
+ it('presetBindingPatch / applyReportPreset 映射表', () => {
73
+ const rowParent = { row: 1, col: 0 }
74
+ const colParent = { row: 0, col: 1 }
75
+ const base: ReportBinding = {
76
+ dataset: 'orders',
77
+ field: 'amount',
78
+ expand: 'down',
79
+ aggregate: 'list',
80
+ rowParent,
81
+ colParent
82
+ }
83
+
84
+ expect(presetBindingPatch('groupHeader')).toEqual({
85
+ preset: 'groupHeader',
86
+ expand: 'down',
87
+ aggregate: 'group'
88
+ })
89
+ expect(presetBindingPatch('groupHeader', { transpose: true })).toMatchObject({
90
+ expand: 'right',
91
+ aggregate: 'group'
92
+ })
93
+
94
+ expect(presetBindingPatch('detail')).toEqual({
95
+ preset: 'detail',
96
+ expand: 'down',
97
+ aggregate: 'list'
98
+ })
99
+ expect(presetBindingPatch('detail', { transpose: true })).toMatchObject({
100
+ expand: 'right',
101
+ aggregate: 'list'
102
+ })
103
+
104
+ expect(presetBindingPatch('subtotal')).toEqual({
105
+ preset: 'subtotal',
106
+ expand: 'none',
107
+ aggregate: 'sum'
108
+ })
109
+ expect(applyReportPreset(base, 'subtotal').rowParent).toEqual(rowParent)
110
+
111
+ expect(presetBindingPatch('grandTotal')).toEqual({
112
+ preset: 'grandTotal',
113
+ expand: 'none',
114
+ aggregate: 'sum',
115
+ rowParent: undefined,
116
+ colParent: undefined
117
+ })
118
+ expect(applyReportPreset(base, 'grandTotal').rowParent).toBeUndefined()
119
+ expect(applyReportPreset(base, 'grandTotal').colParent).toBeUndefined()
120
+
121
+ expect(presetBindingPatch('cross')).toEqual({
122
+ preset: 'cross',
123
+ expand: 'none',
124
+ aggregate: 'sum'
125
+ })
126
+ expect(applyReportPreset(base, 'cross').rowParent).toEqual(rowParent)
127
+ expect(applyReportPreset(base, 'cross').colParent).toEqual(colParent)
128
+ })
129
+
130
+ it('inferReportPreset 从字段组合推断预设', () => {
131
+ expect(
132
+ inferReportPreset({
133
+ dataset: 'orders',
134
+ field: 'customer',
135
+ aggregate: 'group',
136
+ expand: 'down'
137
+ })
138
+ ).toBe('groupHeader')
139
+
140
+ expect(
141
+ inferReportPreset({
142
+ dataset: 'orders',
143
+ field: 'orderNo',
144
+ aggregate: 'list',
145
+ expand: 'down',
146
+ rowParent: { row: 1, col: 0 }
147
+ })
148
+ ).toBe('detail')
149
+
150
+ expect(
151
+ inferReportPreset({
152
+ dataset: 'orders',
153
+ field: 'amount',
154
+ aggregate: 'sum',
155
+ expand: 'none',
156
+ rowParent: { row: 1, col: 0 }
157
+ })
158
+ ).toBe('subtotal')
159
+
160
+ expect(
161
+ inferReportPreset({ dataset: 'orders', field: 'amount', aggregate: 'sum', expand: 'none' })
162
+ ).toBe('grandTotal')
163
+
164
+ expect(
165
+ inferReportPreset({
166
+ dataset: 'orders',
167
+ field: 'amount',
168
+ aggregate: 'sum',
169
+ expand: 'none',
170
+ rowParent: { row: 1, col: 0 },
171
+ colParent: { row: 0, col: 1 }
172
+ })
173
+ ).toBe('cross')
174
+
175
+ expect(
176
+ inferReportPreset({
177
+ dataset: 'orders',
178
+ field: 'amount',
179
+ aggregate: 'avg',
180
+ expand: 'none',
181
+ rowParent: { row: 1, col: 0 }
182
+ })
183
+ ).toBeNull()
184
+ })
185
+
186
+ it('inferDropPreset / isExpansionBandRow 落格推断', () => {
187
+ const groupAddr = { row: 1, col: 0 }
188
+ const group = createReportBinding(ORDERS_DATASET, 'customer')
189
+ group.aggregate = 'group'
190
+ group.expand = 'down'
191
+
192
+ const getBindingAt = (addr: CellAddress) => {
193
+ if (addr.row === groupAddr.row && addr.col === groupAddr.col) return group
194
+ return undefined
195
+ }
196
+
197
+ expect(isExpansionBandRow(1, getBindingAt)).toBe(true)
198
+ expect(isExpansionBandRow(0, getBindingAt)).toBe(false)
199
+ expect(inferDropPreset({ row: 2, col: 0 }, 'number', getBindingAt)).toBe('subtotal')
200
+ expect(inferDropPreset({ row: 2, col: 0 }, 'string', getBindingAt)).toBe('detail')
201
+ })
202
+
203
+ it('formatCellAddress / parseCellAddress 支持多字母列互逆', () => {
204
+ const single = { row: 2, col: 1 }
205
+ expect(formatCellAddress(single)).toBe('B3')
206
+ expect(parseCellAddress('B3')).toEqual(single)
207
+
208
+ const wide = { row: 0, col: 26 }
209
+ expect(formatCellAddress(wide)).toBe('AA1')
210
+ expect(parseCellAddress('AA1')).toEqual(wide)
211
+
212
+ const wider = { row: 99, col: 701 }
213
+ expect(formatCellAddress(wider)).toBe('ZZ100')
214
+ expect(parseCellAddress('ZZ100')).toEqual(wider)
215
+
216
+ expect(parseCellAddress('bad')).toBeNull()
217
+ })
218
+ })
@@ -0,0 +1,218 @@
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('query:POST {endpoint}/query,请求体 { connection, sql, values },返回 fields + rows', async () => {
88
+ const calls = stubFetch(async () => ({
89
+ ok: true,
90
+ fields: [{ name: 'customer', type: 'string' }],
91
+ rows: [{ customer: '甲公司' }]
92
+ }))
93
+ const connector = createHttpConnector({ endpoint: ENDPOINT })
94
+
95
+ const result = await connector.query(CONNECTION, SQL, VALUES)
96
+
97
+ expect(result).toEqual({
98
+ ok: true,
99
+ data: {
100
+ fields: [{ name: 'customer', label: 'customer', type: 'string' }],
101
+ rows: [{ customer: '甲公司' }]
102
+ }
103
+ })
104
+ expect(calls[0]!.url).toBe(`${ENDPOINT}/query`)
105
+ expect(calls[0]!.body).toEqual({ connection: CONNECTION, sql: SQL, values: VALUES })
106
+ })
107
+
108
+ it('业务错误分叉:200 + { ok: false, error } 原样透传(三端点一致)', async () => {
109
+ stubFetch(async () => ({
110
+ ok: false,
111
+ error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
112
+ }))
113
+ const connector = createHttpConnector({ endpoint: ENDPOINT })
114
+
115
+ const testResult = await connector.test(CONNECTION)
116
+ expect(testResult).toEqual({
117
+ ok: false,
118
+ error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
119
+ })
120
+
121
+ const describeResult = await connector.describe(CONNECTION, SQL)
122
+ expect(describeResult).toEqual({
123
+ ok: false,
124
+ error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
125
+ })
126
+
127
+ const queryResult = await connector.query(CONNECTION, SQL, VALUES)
128
+ expect(queryResult).toEqual({
129
+ ok: false,
130
+ error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
131
+ })
132
+ })
133
+
134
+ it('传输层错误:非 2xx 折叠为 HTTP_<status> 错误结果', async () => {
135
+ const fetchMock = vi.fn(async () => new Response('Internal Server Error', { status: 500 }))
136
+ vi.stubGlobal('fetch', fetchMock)
137
+ const connector = createHttpConnector({ endpoint: ENDPOINT })
138
+
139
+ const result = await connector.test(CONNECTION)
140
+
141
+ expect(result).toEqual({
142
+ ok: false,
143
+ error: { code: 'HTTP_500', message: '请求失败(HTTP 500)' }
144
+ })
145
+ })
146
+
147
+ it('网络异常:fetch reject 折叠为 NETWORK_ERROR', async () => {
148
+ vi.stubGlobal(
149
+ 'fetch',
150
+ vi.fn(async () => Promise.reject(new Error('fetch failed')))
151
+ )
152
+ const connector = createHttpConnector({ endpoint: ENDPOINT })
153
+
154
+ const result = await connector.test(CONNECTION)
155
+
156
+ expect(result).toEqual({ ok: false, error: { code: 'NETWORK_ERROR', message: 'fetch failed' } })
157
+ })
158
+
159
+ it('响应不可解析:非 JSON / 缺字段折叠为 BAD_RESPONSE', async () => {
160
+ const notJson = vi.fn(async () => new Response('<html>oops</html>', { status: 200 }))
161
+ vi.stubGlobal('fetch', notJson)
162
+ const connector = createHttpConnector({ endpoint: ENDPOINT })
163
+
164
+ const invalidJson = await connector.describe(CONNECTION, SQL)
165
+ expect(invalidJson).toEqual({
166
+ ok: false,
167
+ error: { code: 'BAD_RESPONSE', message: '响应不是合法 JSON' }
168
+ })
169
+
170
+ vi.stubGlobal(
171
+ 'fetch',
172
+ vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 }))
173
+ )
174
+ const missingFields = await connector.describe(CONNECTION, SQL)
175
+ expect(missingFields).toEqual({
176
+ ok: false,
177
+ error: { code: 'BAD_RESPONSE', message: 'describe 响应缺少合法的 fields 数组' }
178
+ })
179
+
180
+ vi.stubGlobal(
181
+ 'fetch',
182
+ vi.fn(async () => new Response(JSON.stringify({ ok: true, fields: [] }), { status: 200 }))
183
+ )
184
+ const missingRows = await connector.query(CONNECTION, SQL, VALUES)
185
+ expect(missingRows).toEqual({
186
+ ok: false,
187
+ error: { code: 'BAD_RESPONSE', message: 'query 响应缺少 rows 数组' }
188
+ })
189
+ })
190
+
191
+ it('ok:false 但 error 形状非法时回退 SERVER_ERROR', async () => {
192
+ stubFetch(async () => ({ ok: false, error: 'oops' }))
193
+ const connector = createHttpConnector({ endpoint: ENDPOINT })
194
+
195
+ const result = await connector.test(CONNECTION)
196
+
197
+ expect(result).toEqual({
198
+ ok: false,
199
+ error: { code: 'SERVER_ERROR', message: '服务端返回未知错误' }
200
+ })
201
+ })
202
+ })
203
+
204
+ describe('resolvePortOnTypeChange', () => {
205
+ it('默认端口随类型切换:MySQL 3306 ↔ PostgreSQL 5432', () => {
206
+ expect(resolvePortOnTypeChange('mysql', 'postgresql', 3306)).toBe(5432)
207
+ expect(resolvePortOnTypeChange('postgresql', 'mysql', 5432)).toBe(3306)
208
+ })
209
+
210
+ it('自定义端口不随类型切换而改变', () => {
211
+ expect(resolvePortOnTypeChange('mysql', 'postgresql', 3307)).toBe(3307)
212
+ expect(resolvePortOnTypeChange('postgresql', 'mysql', 5433)).toBe(5433)
213
+ })
214
+
215
+ it('端口为 0 时切到新类型默认端口', () => {
216
+ expect(resolvePortOnTypeChange('mysql', 'postgresql', 0)).toBe(5432)
217
+ })
218
+ })
@@ -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 buffer = await exportFilledReportXlsx(buildFilledSheet(), [
64
+ [0, 120],
65
+ [1, 140]
66
+ ])
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
+ })