@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,728 @@
1
+ import { Sheet } from '@veltra/sheet-core'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { REPORT_META_NAMESPACE, createReportBinding } from '../binding'
5
+ import { renderReport, resolveFilledColWidths } from '../render'
6
+ import type { DatasetCatalogItem, DatasetRecords } from '../types'
7
+
8
+ // ---- 内联 fixtures(单一事实源在测试自身,不依赖 playground mock hub)----
9
+
10
+ const ORDERS_DATASET: DatasetCatalogItem = {
11
+ id: 'orders',
12
+ label: '销售明细',
13
+ fields: [
14
+ { name: 'customer', label: '客户', type: 'string' },
15
+ { name: 'orderNo', label: '订单号', type: 'string' },
16
+ { name: 'region', label: '地区', type: 'string' },
17
+ { name: 'amount', label: '金额', type: 'number' },
18
+ { name: 'orderDate', label: '下单日期', type: 'date' }
19
+ ]
20
+ }
21
+
22
+ const CUSTOMERS_DATASET: DatasetCatalogItem = {
23
+ id: 'customers',
24
+ label: '客户',
25
+ fields: [
26
+ { name: 'id', label: '客户ID', type: 'string' },
27
+ { name: 'region', label: '地区', type: 'string' }
28
+ ]
29
+ }
30
+
31
+ const SALES_MATRIX_DATASET: DatasetCatalogItem = {
32
+ id: 'sales-matrix',
33
+ label: '销售矩阵',
34
+ fields: [
35
+ { name: 'region', label: '地区', type: 'string' },
36
+ { name: 'category', label: '品类', type: 'string' },
37
+ { name: 'amount', label: '销售额', type: 'number' }
38
+ ]
39
+ }
40
+
41
+ /** 4 客户 × 14 订单(与模板断言严格对应) */
42
+ const ORDER_ROWS: Record<string, unknown>[] = [
43
+ { customer: '甲公司', region: '华东', orderNo: 'O-1001', amount: 100, orderDate: '2024-01-05' },
44
+ { customer: '甲公司', region: '华东', orderNo: 'O-1002', amount: 200, orderDate: '2024-01-12' },
45
+ { customer: '甲公司', region: '华东', orderNo: 'O-1003', amount: 150, orderDate: '2024-01-20' },
46
+ { customer: '甲公司', region: '华东', orderNo: 'O-1004', amount: 180, orderDate: '2024-01-28' },
47
+ { customer: '乙公司', region: '华南', orderNo: 'O-2001', amount: 300, orderDate: '2024-02-01' },
48
+ { customer: '乙公司', region: '华南', orderNo: 'O-2002', amount: 250, orderDate: '2024-02-08' },
49
+ { customer: '乙公司', region: '华南', orderNo: 'O-2003', amount: 220, orderDate: '2024-02-15' },
50
+ { customer: '乙公司', region: '华南', orderNo: 'O-2004', amount: 280, orderDate: '2024-02-22' },
51
+ { customer: '丙公司', region: '华北', orderNo: 'O-3001', amount: 400, orderDate: '2024-03-01' },
52
+ { customer: '丙公司', region: '华北', orderNo: 'O-3002', amount: 350, orderDate: '2024-03-10' },
53
+ { customer: '丙公司', region: '华北', orderNo: 'O-3003', amount: 320, orderDate: '2024-03-18' },
54
+ { customer: '丁公司', region: '西部', orderNo: 'O-4001', amount: 500, orderDate: '2024-04-02' },
55
+ { customer: '丁公司', region: '西部', orderNo: 'O-4002', amount: 450, orderDate: '2024-04-11' },
56
+ { customer: '丁公司', region: '西部', orderNo: 'O-4003', amount: 480, orderDate: '2024-04-20' }
57
+ ]
58
+
59
+ const CUSTOMER_ROWS: Record<string, unknown>[] = [
60
+ { id: 'C-01', name: '甲公司', region: '华东' },
61
+ { id: 'C-02', name: '乙公司', region: '华南' },
62
+ { id: 'C-03', name: '丙公司', region: '华北' },
63
+ { id: 'C-04', name: '丁公司', region: '西部' },
64
+ { id: 'C-05', name: '戊公司', region: '华东' },
65
+ { id: 'C-06', name: '己公司', region: '华南' },
66
+ { id: 'C-07', name: '庚公司', region: '华北' },
67
+ { id: 'C-08', name: '辛公司', region: '西部' }
68
+ ]
69
+
70
+ /** 地区 × 品类矩阵交叉数据(4 地区 × 5 品类 = 20 行) */
71
+ const SALES_MATRIX_ROWS: Record<string, unknown>[] = [
72
+ { region: '华东', category: '办公设备', qty: 42, amount: 50400, month: '2024-Q1' },
73
+ { region: '华东', category: '耗材', qty: 128, amount: 8640, month: '2024-Q1' },
74
+ { region: '华东', category: '家具', qty: 18, amount: 46440, month: '2024-Q1' },
75
+ { region: '华东', category: '外设', qty: 35, amount: 73465, month: '2024-Q1' },
76
+ { region: '华东', category: '网络', qty: 12, amount: 8640, month: '2024-Q1' },
77
+ { region: '华南', category: '办公设备', qty: 28, amount: 33600, month: '2024-Q1' },
78
+ { region: '华南', category: '耗材', qty: 96, amount: 6480, month: '2024-Q1' },
79
+ { region: '华南', category: '家具', qty: 22, amount: 56760, month: '2024-Q1' },
80
+ { region: '华南', category: '外设', qty: 30, amount: 62970, month: '2024-Q1' },
81
+ { region: '华南', category: '网络', qty: 8, amount: 5760, month: '2024-Q1' },
82
+ { region: '华北', category: '办公设备', qty: 20, amount: 24000, month: '2024-Q1' },
83
+ { region: '华北', category: '耗材', qty: 72, amount: 4860, month: '2024-Q1' },
84
+ { region: '华北', category: '家具', qty: 14, amount: 36120, month: '2024-Q1' },
85
+ { region: '华北', category: '外设', qty: 18, amount: 37782, month: '2024-Q1' },
86
+ { region: '华北', category: '网络', qty: 10, amount: 7200, month: '2024-Q1' },
87
+ { region: '西部', category: '办公设备', qty: 16, amount: 19200, month: '2024-Q1' },
88
+ { region: '西部', category: '耗材', qty: 54, amount: 3645, month: '2024-Q1' },
89
+ { region: '西部', category: '家具', qty: 12, amount: 30960, month: '2024-Q1' },
90
+ { region: '西部', category: '外设', qty: 14, amount: 29386, month: '2024-Q1' },
91
+ { region: '西部', category: '网络', qty: 6, amount: 4320, month: '2024-Q1' }
92
+ ]
93
+
94
+ const MOCK_DATA_RECORDS: DatasetRecords = { orders: ORDER_ROWS, customers: CUSTOMER_ROWS }
95
+
96
+ // ---- 内联模板种子 ----
97
+
98
+ /** 预置 Report Template:表头(5 列)+ 同一扩展带(分组 + 明细)+ 合计行 */
99
+ function seedGroupDetailTemplate(sheet: Sheet): void {
100
+ sheet.setCells([
101
+ { addr: { row: 0, col: 0 }, data: { v: '客户' } },
102
+ { addr: { row: 0, col: 1 }, data: { v: '订单号' } },
103
+ { addr: { row: 0, col: 2 }, data: { v: '地区' } },
104
+ { addr: { row: 0, col: 3 }, data: { v: '金额' } },
105
+ { addr: { row: 0, col: 4 }, data: { v: '下单日期' } },
106
+ { addr: { row: 2, col: 1 }, data: { v: '合计' } }
107
+ ])
108
+
109
+ sheet.setCellStyle(
110
+ { start: { row: 0, col: 0 }, end: { row: 0, col: 4 } },
111
+ { font: { bold: true }, fill: { color: '#E8EEF7' } }
112
+ )
113
+ sheet.setCellStyle(
114
+ { start: { row: 2, col: 0 }, end: { row: 2, col: 4 } },
115
+ { font: { bold: true } }
116
+ )
117
+
118
+ const groupParent = { row: 1, col: 0 }
119
+
120
+ const customerGroup = createReportBinding(ORDERS_DATASET, 'customer')
121
+ customerGroup.preset = 'groupHeader'
122
+ customerGroup.aggregate = 'group'
123
+ customerGroup.expand = 'down'
124
+ sheet.setCellMeta(groupParent, REPORT_META_NAMESPACE, customerGroup)
125
+
126
+ const orderNo = createReportBinding(ORDERS_DATASET, 'orderNo')
127
+ orderNo.rowParent = groupParent
128
+ sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, orderNo)
129
+
130
+ const region = createReportBinding(ORDERS_DATASET, 'region')
131
+ region.rowParent = groupParent
132
+ sheet.setCellMeta({ row: 1, col: 2 }, REPORT_META_NAMESPACE, region)
133
+
134
+ const amount = createReportBinding(ORDERS_DATASET, 'amount')
135
+ amount.rowParent = groupParent
136
+ sheet.setCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE, amount)
137
+
138
+ const orderDate = createReportBinding(ORDERS_DATASET, 'orderDate')
139
+ orderDate.rowParent = groupParent
140
+ sheet.setCellMeta({ row: 1, col: 4 }, REPORT_META_NAMESPACE, orderDate)
141
+
142
+ const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
143
+ subtotal.preset = 'subtotal'
144
+ subtotal.aggregate = 'sum'
145
+ subtotal.expand = 'none'
146
+ subtotal.rowParent = groupParent
147
+ sheet.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, subtotal)
148
+ }
149
+
150
+ /** 预置二维矩阵模板:地区(行)× 品类(列)销售额交叉表 */
151
+ function seedMatrixTemplate(sheet: Sheet): void {
152
+ sheet.setCells([
153
+ { addr: { row: 0, col: 0 }, data: { v: '地区 \\ 品类' } },
154
+ { addr: { row: 2, col: 0 }, data: { v: '合计' } }
155
+ ])
156
+
157
+ sheet.setCellStyle(
158
+ { start: { row: 0, col: 0 }, end: { row: 0, col: 0 } },
159
+ { font: { bold: true }, fill: { color: '#E8EEF7' } }
160
+ )
161
+ sheet.setCellStyle(
162
+ { start: { row: 2, col: 0 }, end: { row: 2, col: 0 } },
163
+ { font: { bold: true } }
164
+ )
165
+
166
+ const colGroupAddr = { row: 0, col: 1 }
167
+ const rowGroupAddr = { row: 1, col: 0 }
168
+ const crossAddr = { row: 1, col: 1 }
169
+
170
+ const categoryGroup = createReportBinding(SALES_MATRIX_DATASET, 'category')
171
+ categoryGroup.preset = 'groupHeader'
172
+ categoryGroup.aggregate = 'group'
173
+ categoryGroup.expand = 'right'
174
+ sheet.setCellMeta(colGroupAddr, REPORT_META_NAMESPACE, categoryGroup)
175
+
176
+ const regionGroup = createReportBinding(SALES_MATRIX_DATASET, 'region')
177
+ regionGroup.preset = 'groupHeader'
178
+ regionGroup.aggregate = 'group'
179
+ regionGroup.expand = 'down'
180
+ sheet.setCellMeta(rowGroupAddr, REPORT_META_NAMESPACE, regionGroup)
181
+
182
+ const cross = createReportBinding(SALES_MATRIX_DATASET, 'amount')
183
+ cross.preset = 'cross'
184
+ cross.aggregate = 'sum'
185
+ cross.expand = 'none'
186
+ cross.rowParent = rowGroupAddr
187
+ cross.colParent = colGroupAddr
188
+ sheet.setCellMeta(crossAddr, REPORT_META_NAMESPACE, cross)
189
+
190
+ const colSubtotal = createReportBinding(SALES_MATRIX_DATASET, 'amount')
191
+ colSubtotal.preset = 'subtotal'
192
+ colSubtotal.aggregate = 'sum'
193
+ colSubtotal.expand = 'none'
194
+ colSubtotal.colParent = colGroupAddr
195
+ sheet.setCellMeta({ row: 2, col: 1 }, REPORT_META_NAMESPACE, colSubtotal)
196
+
197
+ const grandTotal = createReportBinding(SALES_MATRIX_DATASET, 'amount')
198
+ grandTotal.preset = 'grandTotal'
199
+ grandTotal.aggregate = 'sum'
200
+ grandTotal.expand = 'none'
201
+ sheet.setCellMeta({ row: 2, col: 6 }, REPORT_META_NAMESPACE, grandTotal)
202
+ }
203
+
204
+ function buildGroupDetailTemplate() {
205
+ const sheet = new Sheet()
206
+ seedGroupDetailTemplate(sheet)
207
+ return sheet.snapshot()
208
+ }
209
+
210
+ function cellValue(
211
+ snapshot: ReturnType<typeof buildGroupDetailTemplate>,
212
+ row: number,
213
+ col: number
214
+ ): unknown {
215
+ return snapshot.cells.find((c) => c.row === row && c.col === col)?.v
216
+ }
217
+
218
+ function hasMerge(
219
+ snapshot: ReturnType<typeof buildGroupDetailTemplate>,
220
+ start: { row: number; col: number },
221
+ end: { row: number; col: number }
222
+ ): boolean {
223
+ return snapshot.merges.some(
224
+ (m) =>
225
+ m.start.row === start.row &&
226
+ m.start.col === start.col &&
227
+ m.end.row === end.row &&
228
+ m.end.col === end.col
229
+ )
230
+ }
231
+
232
+ function buildMatrixTemplate() {
233
+ const sheet = new Sheet()
234
+ seedMatrixTemplate(sheet)
235
+ return sheet.snapshot()
236
+ }
237
+
238
+ describe('renderReport', () => {
239
+ it('4 客户 14 订单:行数、分组值、明细 zip、地区列与组小计', () => {
240
+ const template = buildGroupDetailTemplate()
241
+ const filled = renderReport(template, MOCK_DATA_RECORDS)
242
+
243
+ // 表头 1 + 甲(4+1) + 乙(4+1) + 丙(3+1) + 丁(3+1) = 19 行
244
+ expect(filled.rows).toBe(19)
245
+ expect(filled.meta).toBeUndefined()
246
+
247
+ // 表头保留(含地区列)
248
+ expect(cellValue(filled, 0, 0)).toBe('客户')
249
+ expect(cellValue(filled, 0, 1)).toBe('订单号')
250
+ expect(cellValue(filled, 0, 2)).toBe('地区')
251
+ expect(cellValue(filled, 0, 3)).toBe('金额')
252
+ expect(cellValue(filled, 0, 4)).toBe('下单日期')
253
+
254
+ // 甲公司:4 明细 + 小计 630(金额在 col 3)
255
+ expect(cellValue(filled, 1, 0)).toBe('甲公司')
256
+ expect(cellValue(filled, 1, 1)).toBe('O-1001')
257
+ expect(cellValue(filled, 1, 2)).toBe('华东')
258
+ expect(cellValue(filled, 1, 3)).toBe(100)
259
+ expect(cellValue(filled, 2, 1)).toBe('O-1002')
260
+ expect(cellValue(filled, 3, 1)).toBe('O-1003')
261
+ expect(cellValue(filled, 4, 1)).toBe('O-1004')
262
+ expect(cellValue(filled, 5, 1)).toBe('合计')
263
+ expect(cellValue(filled, 5, 3)).toBe(630)
264
+
265
+ // 乙公司:4 明细 + 小计 1050
266
+ expect(cellValue(filled, 6, 0)).toBe('乙公司')
267
+ expect(cellValue(filled, 6, 1)).toBe('O-2001')
268
+ expect(cellValue(filled, 6, 2)).toBe('华南')
269
+ expect(cellValue(filled, 9, 1)).toBe('O-2004')
270
+ expect(cellValue(filled, 10, 1)).toBe('合计')
271
+ expect(cellValue(filled, 10, 3)).toBe(1050)
272
+
273
+ // 丙公司:3 明细 + 小计 1070
274
+ expect(cellValue(filled, 11, 0)).toBe('丙公司')
275
+ expect(cellValue(filled, 11, 2)).toBe('华北')
276
+ expect(cellValue(filled, 14, 1)).toBe('合计')
277
+ expect(cellValue(filled, 14, 3)).toBe(1070)
278
+
279
+ // 丁公司:3 明细 + 小计 1430
280
+ expect(cellValue(filled, 15, 0)).toBe('丁公司')
281
+ expect(cellValue(filled, 15, 2)).toBe('西部')
282
+ expect(cellValue(filled, 18, 1)).toBe('合计')
283
+ expect(cellValue(filled, 18, 3)).toBe(1430)
284
+
285
+ // 分组格纵向 merge 覆盖明细行(不含小计行)
286
+ expect(hasMerge(filled, { row: 1, col: 0 }, { row: 4, col: 0 })).toBe(true)
287
+ expect(hasMerge(filled, { row: 6, col: 0 }, { row: 9, col: 0 })).toBe(true)
288
+ expect(hasMerge(filled, { row: 11, col: 0 }, { row: 13, col: 0 })).toBe(true)
289
+ expect(hasMerge(filled, { row: 15, col: 0 }, { row: 17, col: 0 })).toBe(true)
290
+ })
291
+
292
+ it('分组格被误设为 list 时按明细全量展开(无分组锚点)', () => {
293
+ const template = buildGroupDetailTemplate()
294
+ const broken = JSON.parse(JSON.stringify(template)) as ReturnType<
295
+ typeof buildGroupDetailTemplate
296
+ >
297
+ const groupMeta = broken.meta!.find((m) => m.row === 1 && m.col === 0)!
298
+ ;(groupMeta.payload as { aggregate: string }).aggregate = 'list'
299
+
300
+ const filled = renderReport(broken, MOCK_DATA_RECORDS)
301
+ // 分组格误设为 list 时按全量明细展开 + 小计
302
+ expect(filled.rows).toBe(1 + ORDER_ROWS.length + 1)
303
+ expect(cellValue(filled, 1, 1)).toBe('O-1001')
304
+ })
305
+
306
+ it('Filled Report 不携带 Binding meta', () => {
307
+ const template = buildGroupDetailTemplate()
308
+ const filled = renderReport(template, MOCK_DATA_RECORDS)
309
+ expect(filled.meta?.length ?? 0).toBe(0)
310
+ })
311
+
312
+ it('模板样式 id 随扩展实例复制', () => {
313
+ const sheet = new Sheet()
314
+ seedGroupDetailTemplate(sheet)
315
+ // 金额列在 col 3
316
+ sheet.setCellStyle(
317
+ { start: { row: 1, col: 3 }, end: { row: 1, col: 3 } },
318
+ { font: { bold: true } }
319
+ )
320
+ const styleId = sheet.getCellData({ row: 1, col: 3 })?.s
321
+ expect(styleId).toBeDefined()
322
+
323
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
324
+ const firstAmount = filled.cells.find((c) => c.row === 1 && c.col === 3)
325
+ expect(firstAmount?.s).toBe(styleId)
326
+ })
327
+
328
+ it('subtotal 支持 avg / count 聚合', () => {
329
+ const sheet = new Sheet()
330
+ seedGroupDetailTemplate(sheet)
331
+ sheet.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, {
332
+ dataset: 'orders',
333
+ field: 'amount',
334
+ preset: 'subtotal',
335
+ aggregate: 'avg',
336
+ expand: 'none',
337
+ rowParent: { row: 1, col: 0 }
338
+ })
339
+ const avgFilled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
340
+ // 甲公司 4 单金额均值 (100+200+150+180)/4 = 157.5
341
+ expect(avgFilled.cells.find((c) => c.row === 5 && c.col === 3)?.v).toBe(157.5)
342
+
343
+ const sheet2 = new Sheet()
344
+ seedGroupDetailTemplate(sheet2)
345
+ sheet2.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, {
346
+ dataset: 'orders',
347
+ field: 'orderNo',
348
+ preset: 'subtotal',
349
+ aggregate: 'count',
350
+ expand: 'none',
351
+ rowParent: { row: 1, col: 0 }
352
+ })
353
+ const countFilled = renderReport(sheet2.snapshot(), MOCK_DATA_RECORDS)
354
+ expect(countFilled.cells.find((c) => c.row === 5 && c.col === 3)?.v).toBe(4)
355
+ })
356
+
357
+ it('grandTotal 行对全量数据汇总', () => {
358
+ const sheet = new Sheet()
359
+ seedGroupDetailTemplate(sheet)
360
+ sheet.setCells([{ addr: { row: 3, col: 1 }, data: { v: '总计' } }])
361
+ sheet.setCellMeta({ row: 3, col: 3 }, REPORT_META_NAMESPACE, {
362
+ dataset: 'orders',
363
+ field: 'amount',
364
+ preset: 'grandTotal',
365
+ aggregate: 'sum',
366
+ expand: 'none'
367
+ })
368
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
369
+ // 全量金额 4180
370
+ expect(filled.cells.find((c) => c.v === 4180 && c.col === 3)?.v).toBe(4180)
371
+ })
372
+
373
+ it('条件样式规则固化到 Filled Report 样式表', () => {
374
+ const sheet = new Sheet()
375
+ seedGroupDetailTemplate(sheet)
376
+ sheet.setCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE, {
377
+ dataset: 'orders',
378
+ field: 'amount',
379
+ preset: 'detail',
380
+ aggregate: 'list',
381
+ expand: 'down',
382
+ rowParent: { row: 1, col: 0 },
383
+ conditionalRules: [{ operator: 'gt', value: 100, style: { fill: { color: '#FFCCCC' } } }]
384
+ })
385
+
386
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
387
+ const hotCell = filled.cells.find((c) => c.col === 3 && c.v === 150)
388
+ expect(hotCell?.s).toBeDefined()
389
+ expect(filled.styles[(hotCell!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
390
+
391
+ const coldCell = filled.cells.find((c) => c.col === 3 && c.v === 100)
392
+ expect(filled.styles[(coldCell!.s ?? 1) - 1]?.fill?.color).not.toBe('#FFCCCC')
393
+ })
394
+
395
+ it('条件样式按另一字段求值', () => {
396
+ const sheet = new Sheet()
397
+ seedGroupDetailTemplate(sheet)
398
+ sheet.setCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE, {
399
+ dataset: 'orders',
400
+ field: 'amount',
401
+ preset: 'detail',
402
+ aggregate: 'list',
403
+ expand: 'down',
404
+ rowParent: { row: 1, col: 0 },
405
+ conditionalRules: [
406
+ { operator: 'eq', value: '华东', field: 'region', style: { fill: { color: '#FFCCCC' } } }
407
+ ]
408
+ })
409
+
410
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
411
+ const eastCell = filled.cells.find((c) => c.col === 3 && c.v === 150)
412
+ expect(filled.styles[(eastCell!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
413
+
414
+ const southCell = filled.cells.find((c) => c.col === 3 && c.v === 300)
415
+ expect(filled.styles[(southCell!.s ?? 1) - 1]?.fill?.color).not.toBe('#FFCCCC')
416
+ })
417
+
418
+ it('scope: row 染满整行含静态格与横向展开列', () => {
419
+ const sheet = new Sheet()
420
+ seedGroupDetailTemplate(sheet)
421
+ sheet.setCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE, {
422
+ dataset: 'orders',
423
+ field: 'amount',
424
+ preset: 'detail',
425
+ aggregate: 'list',
426
+ expand: 'down',
427
+ rowParent: { row: 1, col: 0 },
428
+ conditionalRules: [
429
+ { operator: 'gt', value: 100, scope: 'row', style: { fill: { color: '#FFCCCC' } } }
430
+ ]
431
+ })
432
+
433
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
434
+ const hotRow = filled.cells.find((c) => c.col === 3 && c.v === 150)!.row
435
+ const hotStyle =
436
+ filled.styles[(filled.cells.find((c) => c.col === 3 && c.v === 150)!.s ?? 1) - 1]
437
+ expect(hotStyle?.fill?.color).toBe('#FFCCCC')
438
+
439
+ const staticOrderNo = filled.cells.find(
440
+ (c) => c.row === hotRow && c.col === 1 && c.v === 'O-1003'
441
+ )
442
+ expect(filled.styles[(staticOrderNo!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
443
+
444
+ const coldRow = filled.cells.find((c) => c.col === 3 && c.v === 100)!.row
445
+ const coldAmount = filled.cells.find((c) => c.row === coldRow && c.col === 3)
446
+ expect(filled.styles[(coldAmount!.s ?? 1) - 1]?.fill?.color).not.toBe('#FFCCCC')
447
+ })
448
+
449
+ it('横向展开明细列同行继承 scope: row 行级样式', () => {
450
+ const sheet = new Sheet()
451
+ sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '备注' } }])
452
+ const amount = createReportBinding(ORDERS_DATASET, 'amount')
453
+ amount.aggregate = 'list'
454
+ amount.expand = 'right'
455
+ amount.conditionalRules = [
456
+ { operator: 'gt', value: 100, scope: 'row', style: { fill: { color: '#FFCCCC' } } }
457
+ ]
458
+ sheet.setCellMeta({ row: 0, col: 1 }, REPORT_META_NAMESPACE, amount)
459
+
460
+ const filled = renderReport(sheet.snapshot(), { orders: [ORDER_ROWS[2]!] })
461
+ const amountCell = filled.cells.find((c) => c.row === 0 && c.v === 150)
462
+ const staticNote = filled.cells.find((c) => c.row === 0 && c.col === 0)
463
+ expect(filled.styles[(amountCell!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
464
+ expect(filled.styles[(staticNote!.s ?? 1) - 1]?.fill?.color).toBe('#FFCCCC')
465
+
466
+ const coldFilled = renderReport(sheet.snapshot(), { orders: [ORDER_ROWS[0]!] })
467
+ const coldAmount = coldFilled.cells.find((c) => c.row === 0 && c.v === 100)
468
+ expect(coldFilled.styles[(coldAmount!.s ?? 1) - 1]?.fill?.color).not.toBe('#FFCCCC')
469
+ })
470
+
471
+ it('二维矩阵:地区 × 品类销售额交叉扩展', () => {
472
+ const filled = renderReport(buildMatrixTemplate(), { 'sales-matrix': SALES_MATRIX_ROWS })
473
+
474
+ // 表头 1 + 4 地区 + 1 合计 = 6 行
475
+ expect(filled.rows).toBe(6)
476
+ expect(cellValue(filled, 0, 0)).toBe('地区 \\ 品类')
477
+ expect(cellValue(filled, 0, 1)).toBe('办公设备')
478
+ expect(cellValue(filled, 0, 5)).toBe('网络')
479
+
480
+ // 华东 × 办公设备 = 50400
481
+ expect(cellValue(filled, 1, 0)).toBe('华东')
482
+ expect(cellValue(filled, 1, 1)).toBe(50400)
483
+
484
+ // 列合计:办公设备四地区之和
485
+ const officeTotal = SALES_MATRIX_ROWS.filter((r) => r.category === '办公设备').reduce(
486
+ (sum, row) => sum + (row.amount as number),
487
+ 0
488
+ )
489
+ expect(cellValue(filled, 5, 1)).toBe(officeTotal)
490
+
491
+ // 总计
492
+ const grandTotal = SALES_MATRIX_ROWS.reduce((sum, row) => sum + (row.amount as number), 0)
493
+ expect(cellValue(filled, 5, 6)).toBe(grandTotal)
494
+ })
495
+
496
+ it('同扩展带追加明细列:预览行数与值正确', () => {
497
+ const sheet = new Sheet()
498
+ seedGroupDetailTemplate(sheet)
499
+ sheet.setCells([{ addr: { row: 0, col: 5 }, data: { v: '备注' } }])
500
+ const remark = createReportBinding(ORDERS_DATASET, 'orderNo')
501
+ remark.rowParent = { row: 1, col: 0 }
502
+ sheet.setCellMeta({ row: 1, col: 5 }, REPORT_META_NAMESPACE, remark)
503
+
504
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
505
+ expect(filled.rows).toBe(19)
506
+ expect(cellValue(filled, 1, 0)).toBe('甲公司')
507
+ expect(cellValue(filled, 1, 1)).toBe('O-1001')
508
+ expect(cellValue(filled, 1, 5)).toBe('O-1001')
509
+ })
510
+
511
+ it('独立扩展带:第二数据源 customers 明细正确渲染', () => {
512
+ const sheet = new Sheet()
513
+ seedGroupDetailTemplate(sheet)
514
+ sheet.setCells([{ addr: { row: 0, col: 5 }, data: { v: '客户ID' } }])
515
+ const customerId = createReportBinding(CUSTOMERS_DATASET, 'id')
516
+ customerId.expand = 'down'
517
+ customerId.aggregate = 'list'
518
+ sheet.setCellMeta({ row: 4, col: 5 }, REPORT_META_NAMESPACE, customerId)
519
+
520
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
521
+ const customers = CUSTOMER_ROWS
522
+
523
+ // 订单扩展 19 行(含表头)+ 客户列表
524
+ expect(filled.rows).toBe(19 + customers.length)
525
+ expect(cellValue(filled, 0, 5)).toBe('客户ID')
526
+ expect(cellValue(filled, 19, 5)).toBe(customers[0]!.id)
527
+ expect(cellValue(filled, 19 + customers.length - 1, 5)).toBe(
528
+ customers[customers.length - 1]!.id
529
+ )
530
+ })
531
+
532
+ it('多行扩展带合并明细输出:分组列与首行明细不丢失', () => {
533
+ const sheet = new Sheet()
534
+ seedGroupDetailTemplate(sheet)
535
+ sheet.clearCellMeta({ row: 1, col: 3 }, REPORT_META_NAMESPACE)
536
+ sheet.clearCellMeta({ row: 1, col: 4 }, REPORT_META_NAMESPACE)
537
+ sheet.clearCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE)
538
+ const snap = sheet.snapshot()
539
+ snap.cells = snap.cells.filter((cell) => !(cell.row === 2 && cell.col === 1))
540
+ sheet.restore(snap)
541
+ sheet.restoreContent(snap)
542
+
543
+ const amount = createReportBinding(ORDERS_DATASET, 'amount')
544
+ amount.rowParent = { row: 1, col: 0 }
545
+ sheet.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, amount)
546
+
547
+ const orderDate = createReportBinding(ORDERS_DATASET, 'orderDate')
548
+ orderDate.rowParent = { row: 1, col: 0 }
549
+ sheet.setCellMeta({ row: 2, col: 4 }, REPORT_META_NAMESPACE, orderDate)
550
+
551
+ const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
552
+ subtotal.preset = 'subtotal'
553
+ subtotal.aggregate = 'sum'
554
+ subtotal.expand = 'none'
555
+ subtotal.rowParent = { row: 1, col: 0 }
556
+ sheet.setCellMeta({ row: 3, col: 3 }, REPORT_META_NAMESPACE, subtotal)
557
+ sheet.setCells([{ addr: { row: 3, col: 1 }, data: { v: '合计' } }])
558
+
559
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
560
+ expect(filled.rows).toBe(19)
561
+ expect(cellValue(filled, 1, 0)).toBe('甲公司')
562
+ expect(cellValue(filled, 1, 1)).toBe('O-1001')
563
+ expect(cellValue(filled, 1, 3)).toBe(100)
564
+ expect(cellValue(filled, 1, 4)).toBe('2024-01-05')
565
+ expect(cellValue(filled, 5, 3)).toBe(630)
566
+ })
567
+
568
+ it('横向展开 right + group:列头向右延展', () => {
569
+ const sheet = new Sheet()
570
+ sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '品类' } }])
571
+ const category = createReportBinding(SALES_MATRIX_DATASET, 'category')
572
+ category.aggregate = 'group'
573
+ category.expand = 'right'
574
+ sheet.setCellMeta({ row: 0, col: 1 }, REPORT_META_NAMESPACE, category)
575
+
576
+ const filled = renderReport(sheet.snapshot(), { 'sales-matrix': SALES_MATRIX_ROWS })
577
+ expect(cellValue(filled, 0, 1)).toBe('办公设备')
578
+ expect(cellValue(filled, 0, 5)).toBe('网络')
579
+ })
580
+
581
+ it('转置明细 right + list:一条记录一列', () => {
582
+ const sheet = new Sheet()
583
+ sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '订单号' } }])
584
+ const orderNo = createReportBinding(ORDERS_DATASET, 'orderNo')
585
+ orderNo.aggregate = 'list'
586
+ orderNo.expand = 'right'
587
+ sheet.setCellMeta({ row: 0, col: 1 }, REPORT_META_NAMESPACE, orderNo)
588
+
589
+ const filled = renderReport(sheet.snapshot(), { orders: ORDER_ROWS.slice(0, 3) })
590
+ expect(cellValue(filled, 0, 1)).toBe('O-1001')
591
+ expect(cellValue(filled, 0, 2)).toBe('O-1002')
592
+ expect(cellValue(filled, 0, 3)).toBe('O-1003')
593
+ })
594
+
595
+ it('多级列头:年 → 季度列头值正确', () => {
596
+ const sheet = new Sheet()
597
+ const data = [
598
+ { year: '2024', quarter: 'Q1', amount: 10 },
599
+ { year: '2024', quarter: 'Q2', amount: 20 },
600
+ { year: '2025', quarter: 'Q1', amount: 30 }
601
+ ]
602
+ const year = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'year')
603
+ year.aggregate = 'group'
604
+ year.expand = 'right'
605
+ sheet.setCellMeta({ row: 0, col: 0 }, REPORT_META_NAMESPACE, year)
606
+
607
+ const quarter = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'quarter')
608
+ quarter.aggregate = 'group'
609
+ quarter.expand = 'right'
610
+ quarter.colParent = { row: 0, col: 0 }
611
+ sheet.setCellMeta({ row: 1, col: 0 }, REPORT_META_NAMESPACE, quarter)
612
+
613
+ const filled = renderReport(sheet.snapshot(), { sales: data })
614
+ expect(cellValue(filled, 0, 0)).toBe('2024')
615
+ expect(cellValue(filled, 0, 2)).toBe('2025')
616
+ expect(cellValue(filled, 1, 0)).toBe('Q1')
617
+ expect(cellValue(filled, 1, 1)).toBe('Q2')
618
+ expect(cellValue(filled, 1, 2)).toBe('Q1')
619
+ })
620
+
621
+ it('顶部标题行的交叉表照常展开', () => {
622
+ const sheet = new Sheet()
623
+ sheet.setCells([
624
+ { addr: { row: 0, col: 0 }, data: { v: '销售交叉表' } },
625
+ { addr: { row: 2, col: 0 }, data: { v: '合计' } }
626
+ ])
627
+
628
+ const category = createReportBinding(SALES_MATRIX_DATASET, 'category')
629
+ category.aggregate = 'group'
630
+ category.expand = 'right'
631
+ sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, category)
632
+
633
+ const region = createReportBinding(SALES_MATRIX_DATASET, 'region')
634
+ region.aggregate = 'group'
635
+ region.expand = 'down'
636
+ sheet.setCellMeta({ row: 2, col: 0 }, REPORT_META_NAMESPACE, region)
637
+
638
+ const cross = createReportBinding(SALES_MATRIX_DATASET, 'amount')
639
+ cross.aggregate = 'sum'
640
+ cross.expand = 'none'
641
+ cross.rowParent = { row: 2, col: 0 }
642
+ cross.colParent = { row: 1, col: 1 }
643
+ sheet.setCellMeta({ row: 2, col: 1 }, REPORT_META_NAMESPACE, cross)
644
+
645
+ const filled = renderReport(sheet.snapshot(), { 'sales-matrix': SALES_MATRIX_ROWS })
646
+ expect(cellValue(filled, 0, 0)).toBe('销售交叉表')
647
+ expect(cellValue(filled, 1, 1)).toBe('办公设备')
648
+ expect(cellValue(filled, 2, 0)).toBe('华东')
649
+ expect(cellValue(filled, 2, 1)).toBe(50400)
650
+ })
651
+
652
+ it('空数据集:展开块为 0,表头仍输出', () => {
653
+ const filled = renderReport(buildGroupDetailTemplate(), { orders: [], customers: [] })
654
+ expect(filled.rows).toBe(1)
655
+ expect(cellValue(filled, 0, 0)).toBe('客户')
656
+ })
657
+
658
+ it('max / min 聚合', () => {
659
+ const sheet = new Sheet()
660
+ seedGroupDetailTemplate(sheet)
661
+ sheet.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, {
662
+ dataset: 'orders',
663
+ field: 'amount',
664
+ preset: 'subtotal',
665
+ aggregate: 'max',
666
+ expand: 'none',
667
+ rowParent: { row: 1, col: 0 }
668
+ })
669
+ const maxFilled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
670
+ expect(maxFilled.cells.find((c) => c.row === 5 && c.col === 3)?.v).toBe(200)
671
+
672
+ const sheet2 = new Sheet()
673
+ seedGroupDetailTemplate(sheet2)
674
+ sheet2.setCellMeta({ row: 2, col: 3 }, REPORT_META_NAMESPACE, {
675
+ dataset: 'orders',
676
+ field: 'amount',
677
+ preset: 'subtotal',
678
+ aggregate: 'min',
679
+ expand: 'none',
680
+ rowParent: { row: 1, col: 0 }
681
+ })
682
+ const minFilled = renderReport(sheet2.snapshot(), MOCK_DATA_RECORDS)
683
+ expect(minFilled.cells.find((c) => c.row === 5 && c.col === 3)?.v).toBe(100)
684
+ })
685
+
686
+ it('mergeSpan: false 时扩展格不合并', () => {
687
+ const sheet = new Sheet()
688
+ const group = createReportBinding(ORDERS_DATASET, 'customer')
689
+ group.aggregate = 'group'
690
+ group.expand = 'down'
691
+ group.mergeSpan = false
692
+ sheet.setCellMeta({ row: 1, col: 0 }, REPORT_META_NAMESPACE, group)
693
+
694
+ const detail = createReportBinding(ORDERS_DATASET, 'orderNo')
695
+ detail.aggregate = 'list'
696
+ detail.expand = 'down'
697
+ detail.rowParent = { row: 1, col: 0 }
698
+ sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, detail)
699
+
700
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
701
+ expect(hasMerge(filled, { row: 1, col: 0 }, { row: 4, col: 0 })).toBe(false)
702
+ expect(cellValue(filled, 1, 0)).toBe('甲公司')
703
+ expect(cellValue(filled, 2, 0)).toBe('甲公司')
704
+ })
705
+
706
+ it('横向展开列继承列方向父格的模板列宽', () => {
707
+ const template = buildMatrixTemplate()
708
+ const templateWidths: Array<[number, number]> = [
709
+ [0, 80],
710
+ [1, 120]
711
+ ]
712
+ const filledWidths = resolveFilledColWidths(
713
+ template,
714
+ { 'sales-matrix': SALES_MATRIX_ROWS },
715
+ templateWidths
716
+ )
717
+ expect(filledWidths).toEqual(
718
+ expect.arrayContaining([
719
+ [0, 80],
720
+ [1, 120],
721
+ [2, 120],
722
+ [3, 120],
723
+ [4, 120],
724
+ [5, 120]
725
+ ])
726
+ )
727
+ })
728
+ })