@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,243 @@
1
+ import type { CellAddress } from '@veltra/sheet-core'
2
+ import { formatAddress, parseAddress } from '@veltra/sheet-core'
3
+
4
+ import type {
5
+ DatasetCatalogItem,
6
+ ReportAggregate,
7
+ ReportBinding,
8
+ ReportExpand,
9
+ ReportPreset
10
+ } from './types'
11
+
12
+ /** 字段 label 解析用的 catalog(由宿主注入) */
13
+ let bindingCatalog: DatasetCatalogItem[] = []
14
+
15
+ export function setBindingCatalog(catalog: DatasetCatalogItem[]): void {
16
+ bindingCatalog = catalog
17
+ }
18
+
19
+ export const REPORT_META_NAMESPACE = 'report'
20
+
21
+ const AGGREGATE_PLACEHOLDER_TAG: Record<ReportAggregate, string> = {
22
+ list: '明细',
23
+ group: '分组',
24
+ sum: '求和',
25
+ avg: '平均',
26
+ count: '计数',
27
+ max: '最大',
28
+ min: '最小'
29
+ }
30
+
31
+ /** 预设 → 绑定字段补丁(父格除 grandTotal 外均保留推断值) */
32
+ export function presetBindingPatch(
33
+ preset: ReportPreset,
34
+ options?: { transpose?: boolean }
35
+ ): Partial<ReportBinding> {
36
+ const expand: ReportExpand = options?.transpose ? 'right' : 'down'
37
+ switch (preset) {
38
+ case 'groupHeader':
39
+ return { preset, expand, aggregate: 'group' }
40
+ case 'detail':
41
+ return { preset, expand, aggregate: 'list' }
42
+ case 'subtotal':
43
+ return { preset, expand: 'none', aggregate: 'sum' }
44
+ case 'grandTotal':
45
+ return {
46
+ preset,
47
+ expand: 'none',
48
+ aggregate: 'sum',
49
+ rowParent: undefined,
50
+ colParent: undefined
51
+ }
52
+ case 'cross':
53
+ return { preset, expand: 'none', aggregate: 'sum' }
54
+ }
55
+ }
56
+
57
+ /** 将预设写入绑定(grandTotal 清空双父格;其余保留既有父格) */
58
+ export function applyReportPreset(
59
+ binding: ReportBinding,
60
+ preset: ReportPreset,
61
+ options?: { transpose?: boolean }
62
+ ): ReportBinding {
63
+ const patch = presetBindingPatch(preset, options)
64
+ const next: ReportBinding = { ...binding, ...patch }
65
+ if (preset === 'grandTotal') {
66
+ delete next.rowParent
67
+ delete next.colParent
68
+ }
69
+ return next
70
+ }
71
+
72
+ export function isVerticalExpandBinding(binding: ReportBinding): boolean {
73
+ return (
74
+ binding.expand === 'down' && (binding.aggregate === 'group' || binding.aggregate === 'list')
75
+ )
76
+ }
77
+
78
+ export function isHorizontalExpandBinding(binding: ReportBinding): boolean {
79
+ return binding.expand === 'right' && binding.aggregate === 'group'
80
+ }
81
+
82
+ /** 指定模板行是否含纵向扩展绑定(展开带行) */
83
+ export function isExpansionBandRow(
84
+ row: number,
85
+ getBindingAt: (addr: CellAddress) => ReportBinding | undefined,
86
+ scanCols = 64
87
+ ): boolean {
88
+ for (let col = 0; col < scanCols; col++) {
89
+ const binding = getBindingAt({ row, col })
90
+ if (binding && isVerticalExpandBinding(binding)) return true
91
+ }
92
+ return false
93
+ }
94
+
95
+ /** 落格时推断预设:默认明细;数值字段且位于展开带正下方相邻行 → 小计 */
96
+ export function inferDropPreset(
97
+ addr: CellAddress,
98
+ fieldType: DatasetCatalogItem['fields'][number]['type'],
99
+ getBindingAt: (addr: CellAddress) => ReportBinding | undefined
100
+ ): ReportPreset {
101
+ if (fieldType === 'number' && addr.row > 0 && isExpansionBandRow(addr.row - 1, getBindingAt)) {
102
+ return 'subtotal'
103
+ }
104
+ return 'detail'
105
+ }
106
+
107
+ /** 同数据集纵向扩展绑定格(行方向父格下拉候选) */
108
+ export function listRowParentCandidates(
109
+ binding: ReportBinding,
110
+ entries: ReadonlyArray<{ addr: CellAddress; binding: ReportBinding }>
111
+ ): CellAddress[] {
112
+ const candidates: CellAddress[] = []
113
+ for (const entry of entries) {
114
+ if (entry.binding.dataset !== binding.dataset) continue
115
+ if (!isVerticalExpandBinding(entry.binding)) continue
116
+ candidates.push(entry.addr)
117
+ }
118
+ return candidates
119
+ }
120
+
121
+ /** 同数据集横向扩展绑定格(列方向父格下拉候选) */
122
+ export function listColParentCandidates(
123
+ binding: ReportBinding,
124
+ entries: ReadonlyArray<{ addr: CellAddress; binding: ReportBinding }>
125
+ ): CellAddress[] {
126
+ const candidates: CellAddress[] = []
127
+ for (const entry of entries) {
128
+ if (entry.binding.dataset !== binding.dataset) continue
129
+ if (!isHorizontalExpandBinding(entry.binding)) continue
130
+ candidates.push(entry.addr)
131
+ }
132
+ return candidates
133
+ }
134
+
135
+ /** 同列向上找最近的纵向扩展绑定,作为 rowParent 候选 */
136
+ export function inferRowParentCandidate(
137
+ addr: CellAddress,
138
+ getBindingAt: (addr: CellAddress) => ReportBinding | undefined
139
+ ): CellAddress | null {
140
+ for (let row = addr.row - 1; row >= 0; row--) {
141
+ const binding = getBindingAt({ row, col: addr.col })
142
+ if (binding && isVerticalExpandBinding(binding)) {
143
+ return { row, col: addr.col }
144
+ }
145
+ }
146
+ return null
147
+ }
148
+
149
+ /** 同行向左找最近的横向扩展绑定,作为 colParent 候选 */
150
+ export function inferColParentCandidate(
151
+ addr: CellAddress,
152
+ getBindingAt: (addr: CellAddress) => ReportBinding | undefined
153
+ ): CellAddress | null {
154
+ for (let col = addr.col - 1; col >= 0; col--) {
155
+ const binding = getBindingAt({ row: addr.row, col })
156
+ if (binding && isHorizontalExpandBinding(binding)) {
157
+ return { row: addr.row, col }
158
+ }
159
+ }
160
+ return null
161
+ }
162
+
163
+ /** 从绑定字段推断预设;不匹配任何预设时返回 null(自定义) */
164
+ export function inferReportPreset(binding: ReportBinding): ReportPreset | null {
165
+ if (binding.preset) return binding.preset
166
+
167
+ if (
168
+ binding.expand === 'none' &&
169
+ binding.aggregate === 'sum' &&
170
+ binding.rowParent &&
171
+ binding.colParent
172
+ ) {
173
+ return 'cross'
174
+ }
175
+ if (
176
+ binding.expand === 'none' &&
177
+ binding.aggregate === 'sum' &&
178
+ !binding.rowParent &&
179
+ !binding.colParent
180
+ ) {
181
+ return 'grandTotal'
182
+ }
183
+ if (
184
+ binding.expand === 'none' &&
185
+ binding.aggregate === 'sum' &&
186
+ binding.rowParent &&
187
+ !binding.colParent
188
+ ) {
189
+ return 'subtotal'
190
+ }
191
+ if (binding.aggregate === 'group' && (binding.expand === 'down' || binding.expand === 'right')) {
192
+ return 'groupHeader'
193
+ }
194
+ if (binding.aggregate === 'list' && (binding.expand === 'down' || binding.expand === 'right')) {
195
+ return 'detail'
196
+ }
197
+ return null
198
+ }
199
+
200
+ /** 默认明细 + 纵向扩展 */
201
+ export function createReportBinding(dataset: DatasetCatalogItem, fieldName: string): ReportBinding {
202
+ return {
203
+ dataset: dataset.id,
204
+ field: fieldName,
205
+ aggregate: 'list',
206
+ expand: 'down',
207
+ preset: 'detail',
208
+ sort: 'none',
209
+ conditionalRules: []
210
+ }
211
+ }
212
+
213
+ /** 设计地址 → A1 标签(支持多字母列) */
214
+ export function formatCellAddress(addr: CellAddress): string {
215
+ return formatAddress(addr)
216
+ }
217
+
218
+ /** A1 标签 → 设计地址;非法输入返回 null */
219
+ export function parseCellAddress(label: string): CellAddress | null {
220
+ return parseAddress(label)
221
+ }
222
+
223
+ /** 解析字段中文标签(找不到时回退字段名);可传入报表配置中的 label */
224
+ function resolveFieldLabel(
225
+ datasetId: string,
226
+ fieldName: string,
227
+ resolveLabel?: (datasetId: string, fieldName: string) => string
228
+ ): string {
229
+ const override = resolveLabel?.(datasetId, fieldName)
230
+ if (override) return override
231
+ const dataset = bindingCatalog.find((d) => d.id === datasetId)
232
+ const field = dataset?.fields.find((f) => f.name === fieldName)
233
+ return field?.label ?? fieldName
234
+ }
235
+
236
+ /** Binding Placeholder 中文可读文案(如「分组 · 客户」) */
237
+ export function formatBindingPlaceholder(
238
+ binding: ReportBinding,
239
+ resolveLabel?: (datasetId: string, fieldName: string) => string
240
+ ): string {
241
+ const tag = AGGREGATE_PLACEHOLDER_TAG[binding.aggregate]
242
+ return `${tag} · ${resolveFieldLabel(binding.dataset, binding.field, resolveLabel)}`
243
+ }
@@ -0,0 +1,205 @@
1
+ import type { DatasetField, ParamValues } from './types'
2
+
3
+ /** 数据连接类型(ADR-0003 决策 3:收敛为 MySQL / PostgreSQL) */
4
+ export type ConnectionType = 'mysql' | 'postgresql'
5
+
6
+ /** 各连接类型的默认端口 */
7
+ export const DEFAULT_CONNECTION_PORTS: Record<ConnectionType, number> = {
8
+ mysql: 3306,
9
+ postgresql: 5432
10
+ }
11
+
12
+ /**
13
+ * 切换连接类型时解析端口:若当前端口仍是上一类型的默认值(或 0),则切到新类型默认端口;
14
+ * 用户已自定义的端口保持不变。
15
+ */
16
+ export function resolvePortOnTypeChange(
17
+ prevType: ConnectionType,
18
+ nextType: ConnectionType,
19
+ currentPort: number
20
+ ): number {
21
+ if (prevType === nextType) return currentPort
22
+ if (currentPort === DEFAULT_CONNECTION_PORTS[prevType] || currentPort === 0) {
23
+ return DEFAULT_CONNECTION_PORTS[nextType]
24
+ }
25
+ return currentPort
26
+ }
27
+
28
+ /**
29
+ * 数据连接(纯序列化对象,仅驻留内存)。
30
+ * 凭据持久化与安全存储由下游负责(ADR-0003 决策 4)。
31
+ */
32
+ export interface DataConnection {
33
+ id: string
34
+ label: string
35
+ type: ConnectionType
36
+ host: string
37
+ port: number
38
+ database: string
39
+ username: string
40
+ password: string
41
+ }
42
+
43
+ /** 业务错误(`200 + { ok: false, error }`)或客户端传输层错误 */
44
+ export interface ConnectorError {
45
+ code: string
46
+ message: string
47
+ }
48
+
49
+ /** 统一结果:成功携带 data;失败携带 error(连接器不抛异常,调用方只处理一条错误路径) */
50
+ export type Result<T> = { ok: true; data: T } | { ok: false; error: ConnectorError }
51
+
52
+ /** query 成功结果:字段 schema + 行记录(契约 `{ fields, rows }` 归一化后) */
53
+ export interface QueryResult {
54
+ fields: DatasetField[]
55
+ rows: Record<string, unknown>[]
56
+ }
57
+
58
+ /**
59
+ * 数据连接器(词汇表:Data Connector):test / describe / query 三方法镜像 HTTP 契约三端点。
60
+ * 真实数据库访问由下游后端以任意语言实现(BYO)。
61
+ */
62
+ export interface DataConnector {
63
+ test(connection: DataConnection): Promise<Result<void>>
64
+ describe(connection: DataConnection, sql: string): Promise<Result<DatasetField[]>>
65
+ query(connection: DataConnection, sql: string, values: ParamValues): Promise<Result<QueryResult>>
66
+ }
67
+
68
+ export interface CreateHttpConnectorOptions {
69
+ /** 契约端点前缀,如 `https://api.example.com/report`;三端点 = `{endpoint}/test|describe|query`(无版本段) */
70
+ endpoint: string
71
+ }
72
+
73
+ // ---- HTTP 契约响应形状(ADR-0003 决策 3)----
74
+ // 传输层错误用 HTTP 状态码;业务错误一律 `200 + { ok: false, error }`。
75
+
76
+ /** 契约字段 schema:`{ name, type? }`(type 缺省视为 string) */
77
+ interface ContractField {
78
+ name: string
79
+ type?: DatasetField['type']
80
+ }
81
+
82
+ interface ContractErrorBody {
83
+ ok: false
84
+ error: ConnectorError
85
+ }
86
+
87
+ type TestResponseBody = { ok: true } | ContractErrorBody
88
+
89
+ type DescribeResponseBody = { ok: true; fields: ContractField[] } | ContractErrorBody
90
+
91
+ type QueryResponseBody =
92
+ | { ok: true; fields: ContractField[]; rows: Record<string, unknown>[] }
93
+ | ContractErrorBody
94
+
95
+ /** 客户端错误:网络不可达 / 非 2xx / 响应不可解析(业务错误由服务端 200 透传) */
96
+ function badResponse(message: string): Result<never> {
97
+ return { ok: false, error: { code: 'BAD_RESPONSE', message } }
98
+ }
99
+
100
+ function isRecord(value: unknown): value is Record<string, unknown> {
101
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
102
+ }
103
+
104
+ function toConnectorError(value: unknown): ConnectorError {
105
+ if (isRecord(value)) {
106
+ const { code, message } = value
107
+ if (typeof code === 'string' && typeof message === 'string') {
108
+ return { code, message }
109
+ }
110
+ }
111
+ return { code: 'SERVER_ERROR', message: '服务端返回未知错误' }
112
+ }
113
+
114
+ /** 契约字段 → 内核 DatasetField(label 缺省回退 name,type 缺省 string) */
115
+ function normalizeField(field: ContractField): DatasetField {
116
+ return { name: field.name, label: field.name, type: field.type ?? 'string' }
117
+ }
118
+
119
+ /** 校验并归一化契约 fields;形状不合法返回 null */
120
+ function normalizeFields(fields: unknown): DatasetField[] | null {
121
+ if (!Array.isArray(fields)) return null
122
+ const normalized: DatasetField[] = []
123
+ for (const field of fields) {
124
+ if (!isRecord(field) || typeof field.name !== 'string') return null
125
+ normalized.push(normalizeField(field as unknown as ContractField))
126
+ }
127
+ return normalized
128
+ }
129
+
130
+ /**
131
+ * 创建 HTTP 数据连接器:三方法对应 `POST {endpoint}/test|describe|query`。
132
+ * - 请求体 JSON 序列化,`Content-Type: application/json`;
133
+ * - 网络异常与非 2xx(传输层错误)折叠为 `{ ok: false, error }`,不抛异常;
134
+ * - 业务错误(连接失败 / SQL 报错)由服务端以 `200 + { ok: false, error: { code, message } }` 返回并原样透传;
135
+ * - `describe` / `query` 的 fields 归一化为内核 `DatasetField`(label 回退 name,type 缺省 string)。
136
+ */
137
+ export function createHttpConnector({ endpoint }: CreateHttpConnectorOptions): DataConnector {
138
+ async function postJson<TBody>(path: string, body: unknown): Promise<Result<TBody>> {
139
+ let response: Response
140
+ try {
141
+ response = await fetch(`${endpoint}${path}`, {
142
+ method: 'POST',
143
+ headers: { 'Content-Type': 'application/json' },
144
+ body: JSON.stringify(body)
145
+ })
146
+ } catch (error) {
147
+ return {
148
+ ok: false,
149
+ error: {
150
+ code: 'NETWORK_ERROR',
151
+ message: error instanceof Error ? error.message : String(error)
152
+ }
153
+ }
154
+ }
155
+
156
+ if (!response.ok) {
157
+ return {
158
+ ok: false,
159
+ error: { code: `HTTP_${response.status}`, message: `请求失败(HTTP ${response.status})` }
160
+ }
161
+ }
162
+
163
+ let payload: unknown
164
+ try {
165
+ payload = await response.json()
166
+ } catch {
167
+ return badResponse('响应不是合法 JSON')
168
+ }
169
+
170
+ if (!isRecord(payload)) return badResponse('响应不是 JSON 对象')
171
+ return { ok: true, data: payload as TBody }
172
+ }
173
+
174
+ return {
175
+ async test(connection) {
176
+ const result = await postJson<TestResponseBody>('/test', { connection })
177
+ if (!result.ok) return result
178
+ if (result.data.ok === true) return { ok: true, data: undefined }
179
+ return { ok: false, error: toConnectorError(result.data.error) }
180
+ },
181
+
182
+ async describe(connection, sql) {
183
+ const result = await postJson<DescribeResponseBody>('/describe', { connection, sql })
184
+ if (!result.ok) return result
185
+ if (result.data.ok !== true) {
186
+ return { ok: false, error: toConnectorError(result.data.error) }
187
+ }
188
+ const fields = normalizeFields(result.data.fields)
189
+ if (!fields) return badResponse('describe 响应缺少合法的 fields 数组')
190
+ return { ok: true, data: fields }
191
+ },
192
+
193
+ async query(connection, sql, values) {
194
+ const result = await postJson<QueryResponseBody>('/query', { connection, sql, values })
195
+ if (!result.ok) return result
196
+ if (result.data.ok !== true) {
197
+ return { ok: false, error: toConnectorError(result.data.error) }
198
+ }
199
+ const fields = normalizeFields(result.data.fields)
200
+ if (!fields) return badResponse('query 响应缺少合法的 fields 数组')
201
+ if (!Array.isArray(result.data.rows)) return badResponse('query 响应缺少 rows 数组')
202
+ return { ok: true, data: { fields, rows: result.data.rows } }
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,118 @@
1
+ import type { Sheet } from '@veltra/sheet-core'
2
+ import { rangeToHucre, styleToHucre } from '@veltra/sheet-core/core/io/export'
3
+ import type { Cell, CellValue, ColumnDef, RowDef, WriteSheet } from 'hucre'
4
+ import { writeXlsx } from 'hucre/xlsx'
5
+
6
+ /** 列宽条目:模型列索引 → 像素宽(sheet-core 列宽未进 SheetSnapshot,导出时由调用方显式传入) */
7
+ export type ReportColWidthEntry = readonly [number, number]
8
+
9
+ /** 像素 → Excel points */
10
+ function pxToPt(px: number): number {
11
+ return px * 0.75
12
+ }
13
+
14
+ /** VTable 像素列宽 → Excel 字符宽度(近似换算) */
15
+ export function pxToExcelColWidth(px: number): number {
16
+ return Math.max(1, Math.round((px - 5) / 7))
17
+ }
18
+
19
+ /** 从列宽条目构建 hucre ColumnDef */
20
+ export function buildColumnDefs(colWidths: ReadonlyArray<ReportColWidthEntry>): ColumnDef[] {
21
+ if (colWidths.length === 0) return []
22
+ const maxCol = Math.max(...colWidths.map(([col]) => col))
23
+ const columns: ColumnDef[] = Array.from({ length: maxCol + 1 }, () => ({}))
24
+ for (const [col, width] of colWidths) {
25
+ columns[col] = { width: pxToExcelColWidth(width) }
26
+ }
27
+ return columns
28
+ }
29
+
30
+ /**
31
+ * 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽)。
32
+ * 条件样式颜色已在 renderReport 展开阶段打平进 StylePool(ADR-0001 决策 2)。
33
+ *
34
+ * 与 `exportWorkbookXlsx`(sheet-core)的差异:列宽未进 SheetSnapshot,
35
+ * 只能经 VTable 运行时读取,故这里接收显式列宽并写入 columns 定义。
36
+ */
37
+ export async function exportFilledReportXlsx(
38
+ sheet: Sheet,
39
+ colWidths: ReadonlyArray<ReportColWidthEntry>
40
+ ): Promise<Uint8Array> {
41
+ let maxRow = -1
42
+ let maxCol = -1
43
+ for (const [addr] of sheet.store.entries()) {
44
+ if (addr.row > maxRow) maxRow = addr.row
45
+ if (addr.col > maxCol) maxCol = addr.col
46
+ }
47
+ for (const [row] of sheet.getRowHeights()) {
48
+ if (row > maxRow) maxRow = row
49
+ }
50
+ for (const [col] of colWidths) {
51
+ if (col > maxCol) maxCol = col
52
+ }
53
+
54
+ const rows =
55
+ maxRow < 0
56
+ ? []
57
+ : Array.from({ length: maxRow + 1 }, () =>
58
+ Array.from({ length: maxCol + 1 }, () => null as CellValue)
59
+ )
60
+ const cells = new Map<string, Partial<Cell>>()
61
+
62
+ for (const [addr, data] of sheet.store.entries()) {
63
+ const row = rows[addr.row]!
64
+ const key = `${addr.row},${addr.col}`
65
+ const cell: Partial<Cell> = {}
66
+ if (data.s != null) {
67
+ const style = sheet.stylePool.get(data.s)
68
+ if (style) cell.style = styleToHucre(style)
69
+ }
70
+ if (data.f != null && data.f !== '') {
71
+ cell.formula = data.f
72
+ if (data.v != null) cell.formulaResult = data.v as CellValue
73
+ row[addr.col] = (data.v as CellValue) ?? null
74
+ cells.set(key, cell)
75
+ continue
76
+ }
77
+ if (data.t === 'e') {
78
+ cell.value = data.v as string
79
+ cell.type = 'error'
80
+ row[addr.col] = null
81
+ cells.set(key, cell)
82
+ continue
83
+ }
84
+ if (data.t === 'd') {
85
+ cell.value = data.v as number
86
+ cell.type = 'number'
87
+ cell.style = { ...cell.style, numFmt: 'yyyy-mm-dd' }
88
+ row[addr.col] = null
89
+ cells.set(key, cell)
90
+ continue
91
+ }
92
+ row[addr.col] = (data.v as CellValue) ?? null
93
+ if (Object.keys(cell).length > 0) cells.set(key, cell)
94
+ }
95
+
96
+ const writeSheet: WriteSheet = {
97
+ name: sheet.name || '报表',
98
+ rows,
99
+ ...(cells.size > 0 ? { cells } : {}),
100
+ ...(sheet.merges.size > 0
101
+ ? { merges: sheet.merges.getMerges().map((range) => rangeToHucre(range)) }
102
+ : {}),
103
+ ...(colWidths.length > 0 ? { columns: buildColumnDefs(colWidths) } : {})
104
+ }
105
+
106
+ const frozen = sheet.frozen
107
+ if (frozen.rows > 0 || frozen.cols > 0) {
108
+ writeSheet.freezePane = { rows: frozen.rows, columns: frozen.cols }
109
+ }
110
+
111
+ const rowDefs = new Map<number, RowDef>()
112
+ for (const [row, height] of sheet.getRowHeights()) {
113
+ rowDefs.set(row, { height: pxToPt(height) })
114
+ }
115
+ if (rowDefs.size > 0) writeSheet.rowDefs = rowDefs
116
+
117
+ return writeXlsx({ sheets: [writeSheet], activeSheet: 0 })
118
+ }
@@ -0,0 +1,36 @@
1
+ import type { ParamValues } from './types'
2
+
3
+ // Filter Bar 控件值规范化(自 playground filter-bar-helpers 平移,行为不变)
4
+
5
+ function toParamString(value: unknown): string {
6
+ if (value == null || value === '') return ''
7
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
8
+ return String(value)
9
+ }
10
+ return ''
11
+ }
12
+
13
+ /** 将运行时 date-range 参数值规范为 [from, to] 字符串元组 */
14
+ export function parseDateRangeValue(raw: unknown): [string, string] {
15
+ if (Array.isArray(raw)) {
16
+ return [toParamString(raw[0]), toParamString(raw[1])]
17
+ }
18
+ if (raw && typeof raw === 'object') {
19
+ const obj = raw as Record<string, unknown>
20
+ return [toParamString(obj.from ?? obj.start), toParamString(obj.to ?? obj.end)]
21
+ }
22
+ return ['', '']
23
+ }
24
+
25
+ /** 将运行时 number 参数值规范为 UNumberInput 可接受的 number | undefined */
26
+ export function resolveNumberParamValue(raw: unknown): number | undefined {
27
+ if (raw === '' || raw == null) return undefined
28
+ if (typeof raw === 'number' && Number.isFinite(raw)) return raw
29
+ const parsed = Number(raw)
30
+ return Number.isFinite(parsed) ? parsed : undefined
31
+ }
32
+
33
+ /** 合并单参数变更,保持其余参数值不变 */
34
+ export function patchParamValues(values: ParamValues, id: string, value: unknown): ParamValues {
35
+ return { ...values, [id]: value }
36
+ }
@@ -0,0 +1,13 @@
1
+ // 报表纯 TS 内核(ADR-0003 决策 1):渲染引擎 / 绑定 / 条件规则 / 查询参数 / 数据连接器 /
2
+ // Report Template(自包含模板与取数)/ Filter Bar 值规范化。
3
+ // 框架无关、无 DOM 依赖;组件壳(UReportDesigner / UReportViewer)落 components/report/。
4
+
5
+ export * from './types'
6
+ export * from './binding'
7
+ export * from './rules'
8
+ export * from './params'
9
+ export * from './render'
10
+ export * from './connector'
11
+ export * from './template'
12
+ export * from './filter-bar'
13
+ export * from './export-xlsx'