@veltra/sheet 2.1.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 (202) 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/{vue → components/sheet}/formula-bar.js +1 -1
  28. package/dist/{vue → components/sheet}/formula-suggest-list.js +1 -1
  29. package/dist/{vue → components/sheet}/import-file.js +1 -1
  30. package/dist/{vue → components/sheet}/insert-count-menu-item.js +1 -1
  31. package/dist/{vue → components/sheet}/insert-image.js +1 -1
  32. package/dist/{vue → components/sheet}/popup-helpers.js +1 -1
  33. package/dist/{vue → components/sheet}/popups/border-popup.js +1 -1
  34. package/dist/{vue → components/sheet}/popups/border-preset-glyph.js +1 -1
  35. package/dist/{vue → components/sheet}/popups/export-popup.js +2 -2
  36. package/dist/{vue → components/sheet}/popups/fill-color-popup.js +1 -1
  37. package/dist/{vue → components/sheet}/popups/find-popup.js +1 -1
  38. package/dist/{vue → components/sheet}/popups/font-color-popup.js +1 -1
  39. package/dist/{vue → components/sheet}/popups/font-size-popup.js +1 -1
  40. package/dist/{vue → components/sheet}/popups/import.worker.d.ts +1 -1
  41. package/dist/{vue → components/sheet}/popups/import.worker.js +1 -1
  42. package/dist/{vue → components/sheet}/popups/insert-image-popup.js +1 -1
  43. package/dist/{vue → components/sheet}/sheet-context-menu.js +1 -1
  44. package/dist/{vue → components/sheet}/sheet-tabs.js +1 -1
  45. package/dist/{vue → components/sheet}/sheet-toolbar.js +1 -1
  46. package/dist/{vue → components/sheet}/sheet.js +8 -1
  47. package/dist/{vue → components/sheet}/use-find-replace.js +1 -1
  48. package/dist/{vue → components/sheet}/use-formula-ref-select.js +1 -1
  49. package/dist/{vue → components/sheet}/use-sheet-grid.js +10 -2
  50. package/dist/{vue → components/sheet}/use-sheet-state.js +3 -3
  51. package/dist/{vue → components/sheet}/use-sheet-tabs-bar.js +1 -1
  52. package/dist/{vue → components/sheet}/use-tool-groups.js +2 -2
  53. package/dist/{vue → components/sheet}/use-tool-popup.js +2 -2
  54. package/dist/{vue → components/sheet}/use-toolbar-scroll.js +1 -1
  55. package/dist/index.d.ts +17 -3
  56. package/dist/index.js +13 -3
  57. package/dist/report/binding.d.ts +45 -0
  58. package/dist/report/binding.js +175 -0
  59. package/dist/report/connector.d.ts +66 -0
  60. package/dist/report/connector.js +159 -0
  61. package/dist/report/export-xlsx.d.ts +19 -0
  62. package/dist/report/export-xlsx.js +95 -0
  63. package/dist/report/filter-bar.d.ts +10 -0
  64. package/dist/report/filter-bar.js +31 -0
  65. package/dist/report/index.d.ts +12 -0
  66. package/dist/report/params.d.ts +13 -0
  67. package/dist/report/params.js +61 -0
  68. package/dist/report/render/aggregate.js +157 -0
  69. package/dist/report/render/builder.d.ts +12 -0
  70. package/dist/report/render/builder.js +139 -0
  71. package/dist/report/render/coordinate.d.ts +25 -0
  72. package/dist/report/render/coordinate.js +733 -0
  73. package/dist/report/render/index.d.ts +14 -0
  74. package/dist/report/render/index.js +21 -0
  75. package/dist/report/render/style-resolver.js +50 -0
  76. package/dist/report/render/template-index.d.ts +24 -0
  77. package/dist/report/render/template-index.js +137 -0
  78. package/dist/report/rules/evaluate.d.ts +25 -0
  79. package/dist/report/rules/evaluate.js +82 -0
  80. package/dist/report/rules/index.d.ts +2 -0
  81. package/dist/report/template.d.ts +59 -0
  82. package/dist/report/template.js +80 -0
  83. package/dist/report/types.d.ts +85 -0
  84. package/dist/tools/context.d.ts +1 -1
  85. package/dist/types/index.d.ts +3 -2
  86. package/dist/types/report-designer.d.ts +36 -0
  87. package/dist/types/report-viewer.d.ts +38 -0
  88. package/dist/types/sheet.d.ts +17 -1
  89. package/package.json +3 -5
  90. package/src/components/report/__test__/conditional-rules-helpers.test.ts +87 -0
  91. package/src/components/report/__test__/field-panel-helpers.test.ts +65 -0
  92. package/src/components/report/__test__/float-panel-position.test.ts +45 -0
  93. package/src/components/report/__test__/report-designer.test.ts +388 -0
  94. package/src/components/report/__test__/report-viewer.test.ts +334 -0
  95. package/src/components/report/__test__/topology.test.ts +143 -0
  96. package/src/components/report/__test__/use-report-designer.test.ts +565 -0
  97. package/src/components/report/binding-badge.ts +73 -0
  98. package/src/components/report/designer/cell-coords.ts +94 -0
  99. package/src/components/report/designer/col-widths.ts +33 -0
  100. package/src/components/report/designer/conditional-rules/helpers.ts +186 -0
  101. package/src/components/report/designer/drop-highlight-overlay.vue +74 -0
  102. package/src/components/report/designer/float-panel.vue +387 -0
  103. package/src/components/report/designer/role.ts +32 -0
  104. package/src/components/report/designer/rule-preview.vue +40 -0
  105. package/src/components/report/designer/rule-row.vue +252 -0
  106. package/src/components/report/designer/rules-dialog.vue +126 -0
  107. package/src/components/report/designer/topology-overlay.vue +127 -0
  108. package/src/components/report/designer/topology.ts +124 -0
  109. package/src/components/report/designer/use-grid-overlay.ts +113 -0
  110. package/src/components/report/designer-hub.vue +198 -0
  111. package/src/components/report/field-panel-helpers.ts +44 -0
  112. package/src/components/report/field-panel.vue +136 -0
  113. package/src/components/report/filter-bar.vue +77 -0
  114. package/src/components/report/hub-connection-form.vue +124 -0
  115. package/src/components/report/hub-dataset-editor.vue +369 -0
  116. package/src/components/report/index.ts +2 -0
  117. package/src/components/report/report-designer.vue +356 -0
  118. package/src/components/report/report-viewer.vue +128 -0
  119. package/src/components/report/style-designer.scss +583 -0
  120. package/src/components/report/style-hub.scss +292 -0
  121. package/src/components/report/style-viewer.scss +104 -0
  122. package/src/components/report/style.scss +3 -0
  123. package/src/components/report/style.ts +20 -0
  124. package/src/components/report/use-report-designer.ts +547 -0
  125. package/src/components/report/use-report-viewer.ts +79 -0
  126. package/src/{vue → components/sheet}/__test__/formula-bar.test.ts +37 -18
  127. package/src/{vue → components/sheet}/__test__/insert-image-popup.test.ts +1 -1
  128. package/src/{vue → components/sheet}/__test__/insert-image.test.ts +1 -1
  129. package/src/{vue → components/sheet}/__test__/sheet-component.test.ts +40 -3
  130. package/src/{vue → components/sheet}/__test__/sheet-context-menu.test.ts +1 -1
  131. package/src/{vue → components/sheet}/formula-bar.vue +1 -1
  132. package/src/{vue → components/sheet}/insert-image.ts +1 -1
  133. package/src/{vue → components/sheet}/popup-helpers.ts +1 -1
  134. package/src/{vue → components/sheet}/popups/border-popup.vue +1 -1
  135. package/src/{vue → components/sheet}/popups/export-popup.vue +2 -2
  136. package/src/{vue → components/sheet}/popups/fill-color-popup.vue +1 -1
  137. package/src/{vue → components/sheet}/popups/find-popup.vue +1 -1
  138. package/src/{vue → components/sheet}/popups/font-color-popup.vue +1 -1
  139. package/src/{vue → components/sheet}/popups/font-size-popup.vue +1 -1
  140. package/src/{vue → components/sheet}/popups/insert-image-popup.vue +1 -1
  141. package/src/{vue → components/sheet}/sheet-context-menu.ts +2 -2
  142. package/src/{vue → components/sheet}/sheet-toolbar.vue +2 -2
  143. package/src/{vue → components/sheet}/sheet.vue +4 -3
  144. package/src/{vue → components/sheet}/use-find-replace.ts +1 -1
  145. package/src/{vue → components/sheet}/use-sheet-grid.ts +7 -3
  146. package/src/{vue → components/sheet}/use-sheet-state.ts +3 -3
  147. package/src/{vue → components/sheet}/use-tool-groups.ts +2 -2
  148. package/src/{vue → components/sheet}/use-tool-popup.ts +2 -2
  149. package/src/index.ts +24 -188
  150. package/src/report/__test__/binding.test.ts +218 -0
  151. package/src/report/__test__/connector.test.ts +218 -0
  152. package/src/report/__test__/export-xlsx.test.ts +118 -0
  153. package/src/report/__test__/filter-bar.test.ts +58 -0
  154. package/src/report/__test__/params.test.ts +156 -0
  155. package/src/report/__test__/render.test.ts +728 -0
  156. package/src/report/__test__/rules.test.ts +131 -0
  157. package/src/report/__test__/template.test.ts +355 -0
  158. package/src/report/binding.ts +243 -0
  159. package/src/report/connector.ts +205 -0
  160. package/src/report/export-xlsx.ts +118 -0
  161. package/src/report/filter-bar.ts +36 -0
  162. package/src/report/index.ts +13 -0
  163. package/src/report/params.ts +96 -0
  164. package/src/report/render/__test__/aggregate.test.ts +16 -0
  165. package/src/report/render/__test__/coordinate.test.ts +405 -0
  166. package/src/report/render/aggregate.ts +240 -0
  167. package/src/report/render/builder.ts +211 -0
  168. package/src/report/render/coordinate.ts +1239 -0
  169. package/src/report/render/index.ts +30 -0
  170. package/src/report/render/style-resolver.ts +68 -0
  171. package/src/report/render/template-index.ts +200 -0
  172. package/src/report/rules/evaluate.ts +113 -0
  173. package/src/report/rules/index.ts +7 -0
  174. package/src/report/template.ts +141 -0
  175. package/src/report/types.ts +96 -0
  176. package/src/tools/builtin.ts +2 -2
  177. package/src/types/index.ts +2 -6
  178. package/src/types/report-designer.ts +36 -0
  179. package/src/types/report-viewer.ts +37 -0
  180. package/src/types/sheet.ts +22 -1
  181. /package/dist/{vue → components/sheet}/style.d.ts +0 -0
  182. /package/dist/{vue → components/sheet}/style.js +0 -0
  183. /package/dist/{vue → components/sheet}/style2.css +0 -0
  184. /package/dist/{vue → components/sheet}/use-formula-suggest.js +0 -0
  185. /package/src/{vue → components/sheet}/__test__/import-file.test.ts +0 -0
  186. /package/src/{vue → components/sheet}/__test__/insert-count-menu-item.test.ts +0 -0
  187. /package/src/{vue → components/sheet}/__test__/use-formula-ref-select.test.ts +0 -0
  188. /package/src/{vue → components/sheet}/__test__/use-formula-suggest.test.ts +0 -0
  189. /package/src/{vue → components/sheet}/formula-suggest-list.vue +0 -0
  190. /package/src/{vue → components/sheet}/import-file.ts +0 -0
  191. /package/src/{vue → components/sheet}/index.ts +0 -0
  192. /package/src/{vue → components/sheet}/insert-count-menu-item.vue +0 -0
  193. /package/src/{vue → components/sheet}/parsing.ts +0 -0
  194. /package/src/{vue → components/sheet}/popups/border-preset-glyph.vue +0 -0
  195. /package/src/{vue → components/sheet}/popups/import.worker.ts +0 -0
  196. /package/src/{vue → components/sheet}/sheet-tabs.vue +0 -0
  197. /package/src/{vue → components/sheet}/style.scss +0 -0
  198. /package/src/{vue → components/sheet}/style.ts +0 -0
  199. /package/src/{vue → components/sheet}/use-formula-ref-select.ts +0 -0
  200. /package/src/{vue → components/sheet}/use-formula-suggest.ts +0 -0
  201. /package/src/{vue → components/sheet}/use-sheet-tabs-bar.ts +0 -0
  202. /package/src/{vue → components/sheet}/use-toolbar-scroll.ts +0 -0
@@ -0,0 +1,240 @@
1
+ import type { CellAddress } from '@veltra/sheet-core'
2
+
3
+ import type { DatasetRecords, ReportAggregate, ReportBinding } from '../types'
4
+ import { enumerateExpansionInstances } from './coordinate'
5
+ import type { PhysicalPlacement } from './coordinate'
6
+ import type { TemplateIndex } from './template-index'
7
+
8
+ function filterRows(
9
+ rows: Record<string, unknown>[],
10
+ filter: Record<string, unknown>
11
+ ): Record<string, unknown>[] {
12
+ if (Object.keys(filter).length === 0) return rows
13
+ return rows.filter((row) =>
14
+ Object.entries(filter).every(([field, value]) => row[field] === value)
15
+ )
16
+ }
17
+
18
+ /** 对数值集求聚合;`avg` 空集返回 `undefined`,`sum` 空集仍为 `0` */
19
+ export function aggregateField(
20
+ rows: Record<string, unknown>[],
21
+ field: string,
22
+ aggregate: ReportAggregate
23
+ ): unknown {
24
+ if (aggregate === 'count') return rows.length
25
+ if (aggregate === 'list' || aggregate === 'group') {
26
+ return rows[0]?.[field]
27
+ }
28
+
29
+ const numbers = rows
30
+ .map((row) => row[field])
31
+ .map((value) => (typeof value === 'number' ? value : Number(value)))
32
+ .filter((value) => Number.isFinite(value))
33
+
34
+ if (numbers.length === 0) {
35
+ if (aggregate === 'avg') return undefined
36
+ return 0
37
+ }
38
+
39
+ const sum = numbers.reduce((acc, value) => acc + value, 0)
40
+ if (aggregate === 'avg') return sum / numbers.length
41
+ if (aggregate === 'max') return Math.max(...numbers)
42
+ if (aggregate === 'min') return Math.min(...numbers)
43
+ return sum
44
+ }
45
+
46
+ function isRowExpanding(binding: ReportBinding): boolean {
47
+ return (
48
+ binding.expand === 'down' && (binding.aggregate === 'group' || binding.aggregate === 'list')
49
+ )
50
+ }
51
+
52
+ function isColExpanding(binding: ReportBinding): boolean {
53
+ return (
54
+ binding.expand === 'right' && (binding.aggregate === 'group' || binding.aggregate === 'list')
55
+ )
56
+ }
57
+
58
+ function rowExpandingAncestors(
59
+ binding: ReportBinding,
60
+ index: TemplateIndex
61
+ ): Array<{ addr: CellAddress; binding: ReportBinding }> {
62
+ const chain: Array<{ addr: CellAddress; binding: ReportBinding }> = []
63
+ let current = binding.rowParent
64
+ while (current) {
65
+ const parentBinding = index.bindingAt(current)
66
+ if (parentBinding) chain.unshift({ addr: current, binding: parentBinding })
67
+ current = parentBinding?.rowParent
68
+ }
69
+ return chain.filter((item) => isRowExpanding(item.binding))
70
+ }
71
+
72
+ function colExpandingAncestors(
73
+ binding: ReportBinding,
74
+ index: TemplateIndex
75
+ ): Array<{ addr: CellAddress; binding: ReportBinding }> {
76
+ const chain: Array<{ addr: CellAddress; binding: ReportBinding }> = []
77
+ let current = binding.colParent
78
+ while (current) {
79
+ const parentBinding = index.bindingAt(current)
80
+ if (parentBinding) chain.unshift({ addr: current, binding: parentBinding })
81
+ current = parentBinding?.colParent
82
+ }
83
+ return chain.filter((item) => isColExpanding(item.binding))
84
+ }
85
+
86
+ function mergeFilters(
87
+ rowFilter: Record<string, unknown>,
88
+ colFilter: Record<string, unknown>
89
+ ): Record<string, unknown> {
90
+ return { ...rowFilter, ...colFilter }
91
+ }
92
+
93
+ function resolveAncestorFilters(
94
+ binding: ReportBinding,
95
+ placement: PhysicalPlacement,
96
+ index: TemplateIndex,
97
+ data: DatasetRecords
98
+ ): Record<string, unknown> {
99
+ const rowFilter: Record<string, unknown> = {}
100
+ const colFilter: Record<string, unknown> = {}
101
+ let parentFilter: Record<string, unknown> = {}
102
+
103
+ const rowAncestors = rowExpandingAncestors(binding, index)
104
+ for (let i = 0; i < rowAncestors.length; i++) {
105
+ const ancestor = rowAncestors[i]!
106
+ if (ancestor.binding.dataset !== binding.dataset) continue
107
+ const instances = enumerateExpansionInstances(ancestor.binding, data, parentFilter)
108
+ const pathIndex = placement.rowPath[i]
109
+ if (pathIndex === undefined) continue
110
+ const instance = instances[pathIndex]
111
+ if (!instance) continue
112
+ Object.assign(rowFilter, instance.filter)
113
+ parentFilter = instance.filter
114
+ }
115
+
116
+ parentFilter = {}
117
+ const colAncestors = colExpandingAncestors(binding, index)
118
+ for (let i = 0; i < colAncestors.length; i++) {
119
+ const ancestor = colAncestors[i]!
120
+ if (ancestor.binding.dataset !== binding.dataset) continue
121
+ const instances = enumerateExpansionInstances(ancestor.binding, data, parentFilter)
122
+ const pathIndex = placement.colPath[i]
123
+ if (pathIndex === undefined) continue
124
+ const instance = instances[pathIndex]
125
+ if (!instance) continue
126
+ Object.assign(colFilter, instance.filter)
127
+ parentFilter = instance.filter
128
+ }
129
+
130
+ return mergeFilters(rowFilter, colFilter)
131
+ }
132
+
133
+ function resolveOwnGroupValue(
134
+ binding: ReportBinding,
135
+ placement: PhysicalPlacement,
136
+ index: TemplateIndex,
137
+ data: DatasetRecords
138
+ ): unknown {
139
+ const rowAncestors = rowExpandingAncestors(binding, index)
140
+ const colAncestors = colExpandingAncestors(binding, index)
141
+ let parentFilter: Record<string, unknown> = {}
142
+
143
+ for (let i = 0; i < rowAncestors.length; i++) {
144
+ const ancestor = rowAncestors[i]!
145
+ if (ancestor.binding.dataset !== binding.dataset) continue
146
+ const instances = enumerateExpansionInstances(ancestor.binding, data, parentFilter)
147
+ const pathIndex = placement.rowPath[i]
148
+ if (pathIndex === undefined) continue
149
+ const instance = instances[pathIndex]
150
+ if (instance) parentFilter = instance.filter
151
+ }
152
+
153
+ for (let i = 0; i < colAncestors.length; i++) {
154
+ const ancestor = colAncestors[i]!
155
+ if (ancestor.binding.dataset !== binding.dataset) continue
156
+ const instances = enumerateExpansionInstances(ancestor.binding, data, parentFilter)
157
+ const pathIndex = placement.colPath[i]
158
+ if (pathIndex === undefined) continue
159
+ const instance = instances[pathIndex]
160
+ if (instance) parentFilter = instance.filter
161
+ }
162
+
163
+ const instances = enumerateExpansionInstances(binding, data, parentFilter)
164
+ const pathIndex = isRowExpanding(binding) ? placement.rowPath.at(-1) : placement.colPath.at(-1)
165
+ if (pathIndex === undefined) return undefined
166
+ return instances[pathIndex]?.value
167
+ }
168
+
169
+ /** 按分组字段建索引,避免交叉展开重复 filterRows */
170
+ export class AggregateIndex {
171
+ private readonly datasets = new Map<string, Record<string, unknown>[]>()
172
+ private readonly filterCache = new Map<string, Record<string, unknown>[]>()
173
+
174
+ constructor(data: DatasetRecords) {
175
+ for (const [datasetId, rows] of Object.entries(data)) {
176
+ this.datasets.set(datasetId, rows)
177
+ }
178
+ }
179
+
180
+ rowsFor(dataset: string, filter: Record<string, unknown>): Record<string, unknown>[] {
181
+ const cacheKey = `${dataset}:${JSON.stringify(filter)}`
182
+ const cached = this.filterCache.get(cacheKey)
183
+ if (cached) return cached
184
+ const rows = filterRows(this.datasets.get(dataset) ?? [], filter)
185
+ this.filterCache.set(cacheKey, rows)
186
+ return rows
187
+ }
188
+
189
+ aggregate(
190
+ dataset: string,
191
+ filter: Record<string, unknown>,
192
+ field: string,
193
+ aggregate: ReportAggregate
194
+ ): unknown {
195
+ const rows = this.rowsFor(dataset, filter)
196
+ if (aggregate === 'list') {
197
+ return rows[0]?.[field]
198
+ }
199
+ return aggregateField(rows, field, aggregate)
200
+ }
201
+ }
202
+
203
+ /** 解析单个物理落点的绑定取值 */
204
+ export function resolvePlacementValue(
205
+ placement: PhysicalPlacement,
206
+ index: TemplateIndex,
207
+ data: DatasetRecords,
208
+ aggregateIndex: AggregateIndex
209
+ ): unknown {
210
+ const binding = placement.binding
211
+ if (!binding) return undefined
212
+
213
+ const filter = resolveAncestorFilters(binding, placement, index, data)
214
+ const rows = aggregateIndex.rowsFor(binding.dataset, filter)
215
+
216
+ if (binding.aggregate === 'group') {
217
+ return filter[binding.field] ?? resolveOwnGroupValue(binding, placement, index, data)
218
+ }
219
+
220
+ if (binding.aggregate === 'list') {
221
+ return rows[placement.listIndex]?.[binding.field]
222
+ }
223
+
224
+ return aggregateField(rows, binding.field, binding.aggregate)
225
+ }
226
+
227
+ /** 解析 `list` 明细落点对应的源数据行;非 list 绑定无单条记录上下文 */
228
+ export function resolvePlacementRecord(
229
+ placement: PhysicalPlacement,
230
+ index: TemplateIndex,
231
+ data: DatasetRecords,
232
+ aggregateIndex: AggregateIndex
233
+ ): Record<string, unknown> | undefined {
234
+ const binding = placement.binding
235
+ if (!binding || binding.aggregate !== 'list') return undefined
236
+
237
+ const filter = resolveAncestorFilters(binding, placement, index, data)
238
+ const rows = aggregateIndex.rowsFor(binding.dataset, filter)
239
+ return rows[placement.listIndex]
240
+ }
@@ -0,0 +1,211 @@
1
+ import type {
2
+ CellRange,
3
+ CellSnapshotItem,
4
+ CellType,
5
+ CellValue,
6
+ SheetSnapshot
7
+ } from '@veltra/sheet-core'
8
+ import { cellKey } from '@veltra/sheet-core'
9
+
10
+ import type { ReportColWidthEntry } from '../export-xlsx'
11
+ import type { ConditionalEvalContext } from '../rules'
12
+ import type { DatasetRecords } from '../types'
13
+ import { AggregateIndex, resolvePlacementRecord, resolvePlacementValue } from './aggregate'
14
+ import type { ExpansionLayout, PhysicalPlacement } from './coordinate'
15
+ import { physicalColSpan, physicalRowSpan } from './coordinate'
16
+ import { StyleResolver } from './style-resolver'
17
+ import type { TemplateIndex } from './template-index'
18
+
19
+ function toCellValue(value: unknown): { v: CellValue; t?: CellType } {
20
+ if (value === null || value === undefined) return { v: '' }
21
+ if (typeof value === 'number' && !Number.isNaN(value)) return { v: value, t: 'n' }
22
+ if (typeof value === 'boolean') return { v: value, t: 'b' }
23
+ if (typeof value === 'string') return { v: value, t: 's' }
24
+ if (value instanceof Date) return { v: value.toISOString().slice(0, 10), t: 's' }
25
+ return { v: JSON.stringify(value), t: 's' }
26
+ }
27
+
28
+ function rangeCells(range: CellRange): Array<{ row: number; col: number }> {
29
+ const cells: Array<{ row: number; col: number }> = []
30
+ for (let row = range.start.row; row <= range.end.row; row++) {
31
+ for (let col = range.start.col; col <= range.end.col; col++) {
32
+ cells.push({ row, col })
33
+ }
34
+ }
35
+ return cells
36
+ }
37
+
38
+ function placementSourceValue(
39
+ placement: PhysicalPlacement,
40
+ template: SheetSnapshot,
41
+ index: TemplateIndex,
42
+ data: DatasetRecords,
43
+ aggregateIndex: AggregateIndex
44
+ ): unknown {
45
+ if (placement.binding) {
46
+ return resolvePlacementValue(placement, index, data, aggregateIndex)
47
+ }
48
+ const staticCell = index.cellAt(placement.logical)
49
+ return staticCell?.v
50
+ }
51
+
52
+ function colWidthSourceLogicalCol(placement: PhysicalPlacement): number {
53
+ const binding = placement.binding
54
+ if (binding?.colParent) {
55
+ return binding.colParent.col
56
+ }
57
+ return placement.logical.col
58
+ }
59
+
60
+ /**
61
+ * 将模板列宽映射到填充报表物理列:横向展开列继承列方向父格所在模板列宽。
62
+ */
63
+ export function mapFilledColWidths(
64
+ templateColWidths: ReadonlyArray<ReportColWidthEntry>,
65
+ layout: ExpansionLayout,
66
+ _index: TemplateIndex
67
+ ): ReportColWidthEntry[] {
68
+ if (templateColWidths.length === 0 || layout.colCount === 0) return []
69
+
70
+ const widthByTemplateCol = new Map<number, number>()
71
+ for (const [col, width] of templateColWidths) {
72
+ widthByTemplateCol.set(col, width)
73
+ }
74
+
75
+ const physicalWidths = new Map<number, number>()
76
+ for (const placement of layout.placements) {
77
+ const sourceCol = colWidthSourceLogicalCol(placement)
78
+ const width = widthByTemplateCol.get(sourceCol)
79
+ if (width === undefined) continue
80
+
81
+ const startCol = placement.physical.start.col
82
+ const endCol = placement.physical.end.col
83
+ for (let col = startCol; col <= endCol; col++) {
84
+ physicalWidths.set(col, width)
85
+ }
86
+ }
87
+
88
+ return [...physicalWidths.entries()].sort((a, b) => a[0] - b[0])
89
+ }
90
+
91
+ function placementEvalContext(
92
+ placement: PhysicalPlacement,
93
+ value: unknown,
94
+ template: SheetSnapshot,
95
+ index: TemplateIndex,
96
+ data: DatasetRecords,
97
+ aggregateIndex: AggregateIndex
98
+ ): ConditionalEvalContext {
99
+ const binding = placement.binding
100
+ return {
101
+ cellValue: value,
102
+ bindingField: binding?.field ?? '',
103
+ record: binding ? resolvePlacementRecord(placement, index, data, aggregateIndex) : undefined
104
+ }
105
+ }
106
+
107
+ function collectRowConditionalStyles(
108
+ layout: ExpansionLayout,
109
+ template: SheetSnapshot,
110
+ index: TemplateIndex,
111
+ data: DatasetRecords,
112
+ styles: StyleResolver
113
+ ): void {
114
+ const aggregateIndex = new AggregateIndex(data)
115
+
116
+ for (const placement of layout.placements) {
117
+ const binding = placement.binding
118
+ if (!binding?.conditionalRules?.some((rule) => rule.scope === 'row')) continue
119
+
120
+ const value = placementSourceValue(placement, template, index, data, aggregateIndex)
121
+ const ctx = placementEvalContext(placement, value, template, index, data, aggregateIndex)
122
+
123
+ for (let row = placement.physical.start.row; row <= placement.physical.end.row; row++) {
124
+ styles.collectRowStyle(row, ctx, binding.conditionalRules)
125
+ }
126
+ }
127
+ }
128
+
129
+ /** 由扩展坐标布局组装 Filled Report 快照 */
130
+ export function buildFilledReport(
131
+ template: SheetSnapshot,
132
+ index: TemplateIndex,
133
+ layout: ExpansionLayout,
134
+ data: DatasetRecords
135
+ ): SheetSnapshot {
136
+ const styles = new StyleResolver(template)
137
+ collectRowConditionalStyles(layout, template, index, data, styles)
138
+
139
+ const aggregateIndex = new AggregateIndex(data)
140
+ const cells: CellSnapshotItem[] = []
141
+ const merges: CellRange[] = [...layout.mappedMerges]
142
+ const mergeKeys = new Set(merges.map((m) => cellKey(m.start)))
143
+
144
+ const pushMerge = (range: CellRange): void => {
145
+ const key = cellKey(range.start)
146
+ if (mergeKeys.has(key)) return
147
+ mergeKeys.add(key)
148
+ merges.push(range)
149
+ }
150
+
151
+ const resolveStyleId = (
152
+ placement: PhysicalPlacement,
153
+ templateRow: number,
154
+ templateCol: number,
155
+ value: unknown,
156
+ physicalRow: number
157
+ ): number | undefined => {
158
+ const binding = placement.binding
159
+ const ctx = placementEvalContext(placement, value, template, index, data, aggregateIndex)
160
+ const baseStyleId = binding
161
+ ? styles.resolveCell(templateRow, templateCol, ctx, binding.conditionalRules)
162
+ : styles.resolveStatic(templateRow, templateCol)
163
+ return styles.mergeRowStyle(baseStyleId, physicalRow)
164
+ }
165
+
166
+ for (const placement of layout.placements) {
167
+ const value = placementSourceValue(placement, template, index, data, aggregateIndex)
168
+ const { row: templateRow, col: templateCol } = placement.logical
169
+
170
+ const rowSpan = physicalRowSpan(placement)
171
+ const colSpan = physicalColSpan(placement)
172
+ const shouldMerge = placement.mergeSpan && (rowSpan > 1 || colSpan > 1)
173
+
174
+ if (shouldMerge) {
175
+ const item: CellSnapshotItem = {
176
+ row: placement.physical.start.row,
177
+ col: placement.physical.start.col,
178
+ ...toCellValue(value)
179
+ }
180
+ const styleId = resolveStyleId(
181
+ placement,
182
+ templateRow,
183
+ templateCol,
184
+ value,
185
+ placement.physical.start.row
186
+ )
187
+ if (styleId !== undefined) item.s = styleId
188
+ cells.push(item)
189
+ if (rowSpan > 1 || colSpan > 1) {
190
+ pushMerge(placement.physical)
191
+ }
192
+ continue
193
+ }
194
+
195
+ for (const pos of rangeCells(placement.physical)) {
196
+ const item: CellSnapshotItem = { row: pos.row, col: pos.col, ...toCellValue(value) }
197
+ const styleId = resolveStyleId(placement, templateRow, templateCol, value, pos.row)
198
+ if (styleId !== undefined) item.s = styleId
199
+ cells.push(item)
200
+ }
201
+ }
202
+
203
+ return {
204
+ cells,
205
+ styles: styles.snapshot(),
206
+ merges,
207
+ frozen: template.frozen,
208
+ rows: layout.rowCount,
209
+ cols: Math.max(layout.colCount, template.cols)
210
+ }
211
+ }