@veltra/sheet 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist/components/report/binding-badge.js +100 -0
  2. package/dist/components/report/designer/cell-coords.js +46 -0
  3. package/dist/components/report/designer/col-widths.js +29 -0
  4. package/dist/components/report/designer/conditional-rules/helpers.js +139 -0
  5. package/dist/components/report/designer/drop-highlight-overlay.js +76 -0
  6. package/dist/components/report/designer/float-panel.js +422 -0
  7. package/dist/components/report/designer/role.js +17 -0
  8. package/dist/components/report/designer/rule-preview.js +33 -0
  9. package/dist/components/report/designer/rule-row.js +253 -0
  10. package/dist/components/report/designer/rules-dialog.js +118 -0
  11. package/dist/components/report/designer/topology-overlay.js +122 -0
  12. package/dist/components/report/designer/topology.js +64 -0
  13. package/dist/components/report/designer/use-grid-overlay.js +75 -0
  14. package/dist/components/report/designer-hub.js +177 -0
  15. package/dist/components/report/field-panel-helpers.js +33 -0
  16. package/dist/components/report/field-panel.js +132 -0
  17. package/dist/components/report/filter-bar.js +74 -0
  18. package/dist/components/report/hub-connection-form.js +145 -0
  19. package/dist/components/report/hub-dataset-editor.js +394 -0
  20. package/dist/components/report/preview-grid-size.js +32 -0
  21. package/dist/components/report/report-designer.js +393 -0
  22. package/dist/components/report/report-viewer.js +124 -0
  23. package/dist/components/report/style.d.ts +17 -0
  24. package/dist/components/report/style.js +18 -0
  25. package/dist/components/report/style2.css +874 -0
  26. package/dist/components/report/use-report-designer.js +382 -0
  27. package/dist/components/report/use-report-viewer.js +55 -0
  28. package/dist/{vue → components/sheet}/formula-bar.js +1 -1
  29. package/dist/{vue → components/sheet}/formula-suggest-list.js +1 -1
  30. package/dist/{vue → components/sheet}/import-file.js +1 -1
  31. package/dist/{vue → components/sheet}/insert-count-menu-item.js +1 -1
  32. package/dist/{vue → components/sheet}/insert-image.js +1 -1
  33. package/dist/{vue → components/sheet}/popup-helpers.js +1 -1
  34. package/dist/{vue → components/sheet}/popups/border-popup.js +31 -3
  35. package/dist/{vue → components/sheet}/popups/border-preset-glyph.js +1 -1
  36. package/dist/{vue → components/sheet}/popups/export-popup.js +2 -2
  37. package/dist/{vue → components/sheet}/popups/fill-color-popup.js +3 -3
  38. package/dist/{vue → components/sheet}/popups/find-popup.js +1 -1
  39. package/dist/{vue → components/sheet}/popups/font-color-popup.js +3 -3
  40. package/dist/{vue → components/sheet}/popups/font-size-popup.js +3 -3
  41. package/dist/{vue → components/sheet}/popups/import.worker.d.ts +1 -1
  42. package/dist/{vue → components/sheet}/popups/import.worker.js +1 -1
  43. package/dist/{vue → components/sheet}/popups/insert-image-popup.js +1 -1
  44. package/dist/{vue → components/sheet}/sheet-context-menu.js +7 -4
  45. package/dist/{vue → components/sheet}/sheet-tabs.js +1 -1
  46. package/dist/{vue → components/sheet}/sheet-toolbar.js +1 -1
  47. package/dist/{vue → components/sheet}/sheet.js +16 -1
  48. package/dist/{vue → components/sheet}/use-find-replace.js +1 -1
  49. package/dist/{vue → components/sheet}/use-formula-ref-select.js +1 -1
  50. package/dist/{vue → components/sheet}/use-sheet-grid.js +22 -4
  51. package/dist/{vue → components/sheet}/use-sheet-state.js +6 -5
  52. package/dist/{vue → components/sheet}/use-sheet-tabs-bar.js +1 -1
  53. package/dist/{vue → components/sheet}/use-tool-groups.js +2 -2
  54. package/dist/{vue → components/sheet}/use-tool-popup.js +2 -2
  55. package/dist/{vue → components/sheet}/use-toolbar-scroll.js +1 -1
  56. package/dist/index.d.ts +17 -3
  57. package/dist/index.js +13 -3
  58. package/dist/report/binding.d.ts +83 -0
  59. package/dist/report/binding.js +332 -0
  60. package/dist/report/connector.d.ts +66 -0
  61. package/dist/report/connector.js +159 -0
  62. package/dist/report/export-xlsx.d.ts +17 -0
  63. package/dist/report/export-xlsx.js +92 -0
  64. package/dist/report/filter-bar.d.ts +10 -0
  65. package/dist/report/filter-bar.js +31 -0
  66. package/dist/report/index.d.ts +12 -0
  67. package/dist/report/params.d.ts +13 -0
  68. package/dist/report/params.js +61 -0
  69. package/dist/report/render/aggregate.js +169 -0
  70. package/dist/report/render/builder.d.ts +12 -0
  71. package/dist/report/render/builder.js +139 -0
  72. package/dist/report/render/coordinate.d.ts +25 -0
  73. package/dist/report/render/coordinate.js +733 -0
  74. package/dist/report/render/index.d.ts +14 -0
  75. package/dist/report/render/index.js +21 -0
  76. package/dist/report/render/style-resolver.js +50 -0
  77. package/dist/report/render/template-index.d.ts +24 -0
  78. package/dist/report/render/template-index.js +137 -0
  79. package/dist/report/rules/evaluate.d.ts +25 -0
  80. package/dist/report/rules/evaluate.js +82 -0
  81. package/dist/report/rules/index.d.ts +2 -0
  82. package/dist/report/template.d.ts +63 -0
  83. package/dist/report/template.js +83 -0
  84. package/dist/report/types.d.ts +85 -0
  85. package/dist/tools/apply-style.js +29 -0
  86. package/dist/tools/builtin.js +17 -17
  87. package/dist/tools/context.d.ts +39 -6
  88. package/dist/tools/context.js +38 -1
  89. package/dist/types/index.d.ts +3 -2
  90. package/dist/types/report-designer.d.ts +38 -0
  91. package/dist/types/report-viewer.d.ts +38 -0
  92. package/dist/types/sheet.d.ts +21 -1
  93. package/package.json +6 -8
  94. package/src/components/report/__test__/conditional-rules-helpers.test.ts +88 -0
  95. package/src/components/report/__test__/field-panel-helpers.test.ts +65 -0
  96. package/src/components/report/__test__/float-panel-position.test.ts +45 -0
  97. package/src/components/report/__test__/preview-grid-size.test.ts +63 -0
  98. package/src/components/report/__test__/report-designer.test.ts +464 -0
  99. package/src/components/report/__test__/report-viewer.test.ts +357 -0
  100. package/src/components/report/__test__/topology.test.ts +143 -0
  101. package/src/components/report/__test__/use-report-designer.test.ts +697 -0
  102. package/src/components/report/binding-badge.ts +92 -0
  103. package/src/components/report/designer/cell-coords.ts +94 -0
  104. package/src/components/report/designer/col-widths.ts +56 -0
  105. package/src/components/report/designer/conditional-rules/helpers.ts +189 -0
  106. package/src/components/report/designer/drop-highlight-overlay.vue +74 -0
  107. package/src/components/report/designer/float-panel.vue +431 -0
  108. package/src/components/report/designer/role.ts +32 -0
  109. package/src/components/report/designer/rule-preview.vue +40 -0
  110. package/src/components/report/designer/rule-row.vue +252 -0
  111. package/src/components/report/designer/rules-dialog.vue +110 -0
  112. package/src/components/report/designer/topology-overlay.vue +127 -0
  113. package/src/components/report/designer/topology.ts +124 -0
  114. package/src/components/report/designer/use-grid-overlay.ts +113 -0
  115. package/src/components/report/designer-hub.vue +198 -0
  116. package/src/components/report/field-panel-helpers.ts +44 -0
  117. package/src/components/report/field-panel.vue +136 -0
  118. package/src/components/report/filter-bar.vue +77 -0
  119. package/src/components/report/hub-connection-form.vue +124 -0
  120. package/src/components/report/hub-dataset-editor.vue +369 -0
  121. package/src/components/report/index.ts +2 -0
  122. package/src/components/report/preview-grid-size.ts +33 -0
  123. package/src/components/report/report-designer.vue +420 -0
  124. package/src/components/report/report-viewer.vue +136 -0
  125. package/src/components/report/style-designer.scss +587 -0
  126. package/src/components/report/style-hub.scss +292 -0
  127. package/src/components/report/style-viewer.scss +104 -0
  128. package/src/components/report/style.scss +3 -0
  129. package/src/components/report/style.ts +20 -0
  130. package/src/components/report/use-report-designer.ts +596 -0
  131. package/src/components/report/use-report-viewer.ts +88 -0
  132. package/src/{vue → components/sheet}/__test__/formula-bar.test.ts +37 -18
  133. package/src/{vue → components/sheet}/__test__/insert-image-popup.test.ts +1 -1
  134. package/src/{vue → components/sheet}/__test__/insert-image.test.ts +1 -1
  135. package/src/{vue → components/sheet}/__test__/sheet-component.test.ts +77 -3
  136. package/src/{vue → components/sheet}/__test__/sheet-context-menu.test.ts +4 -3
  137. package/src/{vue → components/sheet}/formula-bar.vue +1 -1
  138. package/src/{vue → components/sheet}/insert-image.ts +1 -1
  139. package/src/{vue → components/sheet}/popup-helpers.ts +1 -1
  140. package/src/{vue → components/sheet}/popups/border-popup.vue +32 -4
  141. package/src/{vue → components/sheet}/popups/export-popup.vue +2 -2
  142. package/src/{vue → components/sheet}/popups/fill-color-popup.vue +3 -3
  143. package/src/{vue → components/sheet}/popups/find-popup.vue +1 -1
  144. package/src/{vue → components/sheet}/popups/font-color-popup.vue +3 -3
  145. package/src/{vue → components/sheet}/popups/font-size-popup.vue +3 -3
  146. package/src/{vue → components/sheet}/popups/insert-image-popup.vue +1 -1
  147. package/src/{vue → components/sheet}/sheet-context-menu.ts +10 -4
  148. package/src/{vue → components/sheet}/sheet-toolbar.vue +2 -2
  149. package/src/{vue → components/sheet}/sheet.vue +6 -3
  150. package/src/{vue → components/sheet}/use-find-replace.ts +1 -1
  151. package/src/{vue → components/sheet}/use-sheet-grid.ts +23 -5
  152. package/src/{vue → components/sheet}/use-sheet-state.ts +11 -6
  153. package/src/{vue → components/sheet}/use-tool-groups.ts +2 -2
  154. package/src/{vue → components/sheet}/use-tool-popup.ts +2 -2
  155. package/src/index.ts +24 -188
  156. package/src/report/__test__/binding.test.ts +320 -0
  157. package/src/report/__test__/connector.test.ts +239 -0
  158. package/src/report/__test__/export-xlsx.test.ts +118 -0
  159. package/src/report/__test__/filter-bar.test.ts +58 -0
  160. package/src/report/__test__/params.test.ts +156 -0
  161. package/src/report/__test__/render.test.ts +830 -0
  162. package/src/report/__test__/rules.test.ts +131 -0
  163. package/src/report/__test__/template.test.ts +366 -0
  164. package/src/report/binding.ts +474 -0
  165. package/src/report/connector.ts +206 -0
  166. package/src/report/export-xlsx.ts +113 -0
  167. package/src/report/filter-bar.ts +36 -0
  168. package/src/report/index.ts +13 -0
  169. package/src/report/params.ts +96 -0
  170. package/src/report/render/__test__/aggregate.test.ts +16 -0
  171. package/src/report/render/__test__/coordinate.test.ts +405 -0
  172. package/src/report/render/aggregate.ts +261 -0
  173. package/src/report/render/builder.ts +211 -0
  174. package/src/report/render/coordinate.ts +1239 -0
  175. package/src/report/render/index.ts +30 -0
  176. package/src/report/render/style-resolver.ts +68 -0
  177. package/src/report/render/template-index.ts +200 -0
  178. package/src/report/rules/evaluate.ts +113 -0
  179. package/src/report/rules/index.ts +7 -0
  180. package/src/report/template.ts +145 -0
  181. package/src/report/types.ts +96 -0
  182. package/src/tools/__test__/apply-style.test.ts +129 -0
  183. package/src/tools/apply-style.ts +41 -0
  184. package/src/tools/builtin.ts +20 -20
  185. package/src/tools/context.ts +67 -4
  186. package/src/tools/index.ts +1 -0
  187. package/src/types/index.ts +2 -6
  188. package/src/types/report-designer.ts +38 -0
  189. package/src/types/report-viewer.ts +37 -0
  190. package/src/types/sheet.ts +26 -1
  191. /package/dist/{vue → components/sheet}/style.d.ts +0 -0
  192. /package/dist/{vue → components/sheet}/style.js +0 -0
  193. /package/dist/{vue → components/sheet}/style2.css +0 -0
  194. /package/dist/{vue → components/sheet}/use-formula-suggest.js +0 -0
  195. /package/src/{vue → components/sheet}/__test__/import-file.test.ts +0 -0
  196. /package/src/{vue → components/sheet}/__test__/insert-count-menu-item.test.ts +0 -0
  197. /package/src/{vue → components/sheet}/__test__/use-formula-ref-select.test.ts +0 -0
  198. /package/src/{vue → components/sheet}/__test__/use-formula-suggest.test.ts +0 -0
  199. /package/src/{vue → components/sheet}/formula-suggest-list.vue +0 -0
  200. /package/src/{vue → components/sheet}/import-file.ts +0 -0
  201. /package/src/{vue → components/sheet}/index.ts +0 -0
  202. /package/src/{vue → components/sheet}/insert-count-menu-item.vue +0 -0
  203. /package/src/{vue → components/sheet}/parsing.ts +0 -0
  204. /package/src/{vue → components/sheet}/popups/border-preset-glyph.vue +0 -0
  205. /package/src/{vue → components/sheet}/popups/import.worker.ts +0 -0
  206. /package/src/{vue → components/sheet}/sheet-tabs.vue +0 -0
  207. /package/src/{vue → components/sheet}/style.scss +0 -0
  208. /package/src/{vue → components/sheet}/style.ts +0 -0
  209. /package/src/{vue → components/sheet}/use-formula-ref-select.ts +0 -0
  210. /package/src/{vue → components/sheet}/use-formula-suggest.ts +0 -0
  211. /package/src/{vue → components/sheet}/use-sheet-tabs-bar.ts +0 -0
  212. /package/src/{vue → components/sheet}/use-toolbar-scroll.ts +0 -0
@@ -0,0 +1,596 @@
1
+ import type { CellAddress } from '@veltra/sheet-core'
2
+ import type { ResolveCellRenderer, ResolveCellStyleHook } from '@veltra/sheet-core'
3
+ import { Workbook } from '@veltra/sheet-core'
4
+ import { computed, onScopeDispose, ref, watch, type ComputedRef, type Ref } from 'vue'
5
+
6
+ import {
7
+ REPORT_META_NAMESPACE,
8
+ applyReportPreset,
9
+ createReportBinding,
10
+ formatCellAddress,
11
+ inferDropPreset,
12
+ inferParentsForPreset,
13
+ isColGroupBinding,
14
+ isHorizontalExpandBinding,
15
+ isRowGroupBinding,
16
+ isVerticalExpandBinding,
17
+ listColParentCandidates,
18
+ listRowParentCandidates,
19
+ prefersGroupParent,
20
+ presetStillApplies
21
+ } from '../../report/binding'
22
+ import type { DataConnection, QueryResult, Result } from '../../report/connector'
23
+ import { buildParamDefs } from '../../report/params'
24
+ import {
25
+ assertCompatibleTemplateVersion,
26
+ createReportTemplate,
27
+ getTemplateDatasets,
28
+ resolveParamDefaults,
29
+ type ReportDatasetDef,
30
+ type ReportTemplate
31
+ } from '../../report/template'
32
+ import type { DatasetCatalogItem, DatasetField, ReportBinding } from '../../report/types'
33
+ import type { ReportDesignerProps } from '../../types'
34
+ import { createBindingBadgeRenderer, createBindingBadgeStyleResolver } from './binding-badge'
35
+ import type { TopologyBindingEntry } from './designer/topology'
36
+
37
+ /**
38
+ * 设计器内部数据集:以 `connectionId` 引用连接(连接列表经 `v-model:connections`
39
+ * 注入,是单一事实源);`getTemplate()` 吐出模板时解析为内嵌连接对象(`ReportDatasetDef`)。
40
+ */
41
+ export interface DesignerDataset {
42
+ id: string
43
+ label: string
44
+ connectionId: string
45
+ sql: string
46
+ /** 参数元数据覆盖(label / 类型 / 默认值 / 选项) */
47
+ paramOverrides?: ReportDatasetDef['paramOverrides']
48
+ /** 字段中文名覆盖(name → label) */
49
+ fieldOverrides?: ReportDatasetDef['fieldOverrides']
50
+ /** describe 解析的字段缓存(字段面板 catalog 的数据源;fieldOverrides 在 catalog 层应用) */
51
+ fields?: DatasetField[]
52
+ }
53
+
54
+ /** 数据中枢 drawer 的消费面(`UseReportDesignerReturn` 的子集) */
55
+ export interface DatasetHubController {
56
+ connections: Ref<DataConnection[]>
57
+ datasets: Ref<DesignerDataset[]>
58
+ addConnection: (connection: DataConnection) => void
59
+ updateConnection: (connection: DataConnection) => void
60
+ /** 删除连接并级联删除其数据集 */
61
+ removeConnection: (connectionId: string) => void
62
+ addDataset: (connectionId: string) => DesignerDataset
63
+ updateDataset: (datasetId: string, patch: Partial<Omit<DesignerDataset, 'id'>>) => void
64
+ removeDataset: (datasetId: string) => void
65
+ /** 真实测试连接(契约无状态,新建草稿同样可测) */
66
+ testConnection: (connection: DataConnection) => Promise<Result<void>>
67
+ /** 经连接器 describe 解析字段并写入数据集字段缓存 */
68
+ describeDataset: (datasetId: string) => Promise<Result<DatasetField[]>>
69
+ /** 按参数默认值经连接器取数(数据集记录预览) */
70
+ previewDataset: (datasetId: string) => Promise<Result<QueryResult>>
71
+ }
72
+
73
+ export type ParentPickMode = 'row' | 'col'
74
+
75
+ export interface ParentPickState {
76
+ mode: ParentPickMode
77
+ source: CellAddress
78
+ }
79
+
80
+ export interface UseReportDesignerReturn extends DatasetHubController {
81
+ workbook: ComputedRef<Workbook>
82
+ /** 字段面板目录(数据集 + 应用 fieldOverrides 后的字段 label) */
83
+ catalog: ComputedRef<DatasetCatalogItem[]>
84
+ /** 已绑定字段键集合:`${datasetId}:${fieldName}` */
85
+ boundKeys: ComputedRef<Set<string>>
86
+ /** 当前选区地址(跟随模型 selection-change) */
87
+ activeCell: Ref<CellAddress | null>
88
+ /** 当前选区 A1 标签 */
89
+ selectionLabel: ComputedRef<string>
90
+ /** 当前选区绑定(跟随 meta-change) */
91
+ activeBinding: ComputedRef<ReportBinding | undefined>
92
+ /** 当前选区绑定字段的数据类型(条件规则对话框控件映射用;缺省 number) */
93
+ activeFieldType: ComputedRef<DatasetField['type']>
94
+ /** 当前绑定所属数据集的字段列表(条件规则求值字段选择) */
95
+ activeDatasetFields: ComputedRef<DatasetField[]>
96
+ /** 当前选区绑定的行方向父格 A1 标签(无父格显示 —) */
97
+ resolvedRowParentLabel: ComputedRef<string>
98
+ /** 当前选区绑定的列方向父格 A1 标签(无父格显示 —) */
99
+ resolvedColParentLabel: ComputedRef<string>
100
+ /** 父格点选态(进入后在网格上点目标格写入父格) */
101
+ parentPick: Ref<ParentPickState | null>
102
+ /** 行方向父格下拉候选(同数据集纵向扩展绑定格) */
103
+ rowParentCandidates: ComputedRef<CellAddress[]>
104
+ /** 列方向父格下拉候选(同数据集横向扩展绑定格) */
105
+ colParentCandidates: ComputedRef<CellAddress[]>
106
+ /** 全部绑定条目(拓扑连线数据源;跟随 meta-change) */
107
+ bindingEntries: ComputedRef<TopologyBindingEntry[]>
108
+ /** meta 变更计数(网格覆层组件的同步信号) */
109
+ metaTick: Ref<number>
110
+ getBindingAt: (addr: CellAddress) => ReportBinding | undefined
111
+ /** 字段 label 解析(catalog O(1) 查找;Action Pill 摘要与徽章共用) */
112
+ resolveFieldLabel: (datasetId: string, fieldName: string) => string
113
+ /** 拖拽/点击落格写 Cell Meta 绑定(角色推导与 playground 设计器一致) */
114
+ bindField: (datasetId: string, fieldName: string, addr?: CellAddress) => void
115
+ /** 就地修补当前选区绑定(Action Pill:预设 / 展开方向 / 父格 / 聚合 / 排序 / 条件规则) */
116
+ patchActiveBinding: (patch: Partial<ReportBinding>) => void
117
+ /** 进入父格点选态 */
118
+ startParentPick: (mode: ParentPickMode) => void
119
+ /** 取消父格点选态 */
120
+ cancelParentPick: () => void
121
+ /** 父格点选:将目标格写入当前编辑格的 rowParent / colParent */
122
+ pickParentAt: (target: CellAddress) => void
123
+ /** 清除行 / 列方向父格 */
124
+ clearParent: (mode: ParentPickMode) => void
125
+ /** 清除当前选区绑定 */
126
+ removeActiveBinding: () => void
127
+ /** 绑定格角色底色样式 hook(与徽章 renderer 配套) */
128
+ resolveCellStyle: ResolveCellStyleHook
129
+ /** 绑定格角色徽章渲染 hook(ADR-0004) */
130
+ resolveCellRenderer: ResolveCellRenderer
131
+ /** 取回含 meta 绑定与内嵌数据集定义的 Report Template */
132
+ getTemplate: () => ReportTemplate
133
+ }
134
+
135
+ export interface UseReportDesignerOptions {
136
+ props: ReportDesignerProps
137
+ /** `v-model:connections` 可写代理(get 读 props,set 触发 `update:connections`) */
138
+ connections: Ref<DataConnection[]>
139
+ }
140
+
141
+ /**
142
+ * UReportDesigner 的 headless 编排:连接 / 数据集状态、绑定写 Cell Meta、
143
+ * 角色徽章渲染 hook、`getTemplate()` 模板吐出。不持有 DOM / 网格引用。
144
+ */
145
+ export function useReportDesigner(options: UseReportDesignerOptions): UseReportDesignerReturn {
146
+ const { props, connections } = options
147
+
148
+ /** 设计态工作簿:宿主可注入(USheet / UReportViewer 先例),缺省内部自建 */
149
+ const internalWorkbook = new Workbook()
150
+ const workbook = computed(() => props.workbook ?? internalWorkbook)
151
+
152
+ const datasets = ref<DesignerDataset[]>([])
153
+
154
+ /** meta 变更计数:boundKeys 等派生状态的响应式触发(绑定经命令系统写入后由模型发 meta-change) */
155
+ const metaTick = ref(0)
156
+ const parentPick = ref<ParentPickState | null>(null)
157
+ const offMeta = workbook.value.activeSheet.on('meta-change', () => {
158
+ metaTick.value++
159
+ })
160
+ onScopeDispose(offMeta)
161
+
162
+ /** 稳定选区地址引用(值未变时不替换对象,避免触发下游 watcher) */
163
+ const activeCell = ref<CellAddress | null>(null)
164
+ function syncActiveCell(): void {
165
+ const next = workbook.value.activeSheet.getSelection().activeCell ?? null
166
+ const prev = activeCell.value
167
+ if (!next) {
168
+ activeCell.value = null
169
+ return
170
+ }
171
+ if (prev && prev.row === next.row && prev.col === next.col) return
172
+ activeCell.value = { row: next.row, col: next.col }
173
+ }
174
+ const offSelection = workbook.value.activeSheet.on('selection-change', syncActiveCell)
175
+ onScopeDispose(offSelection)
176
+ syncActiveCell()
177
+
178
+ const selectionLabel = computed(() =>
179
+ activeCell.value ? formatCellAddress(activeCell.value) : '—'
180
+ )
181
+
182
+ const catalog = computed((): DatasetCatalogItem[] =>
183
+ datasets.value.map((dataset) => ({
184
+ id: dataset.id,
185
+ label: dataset.label,
186
+ fields: (dataset.fields ?? []).map((field) => ({
187
+ ...field,
188
+ label: dataset.fieldOverrides?.[field.name]?.label ?? field.label
189
+ }))
190
+ }))
191
+ )
192
+
193
+ /** 字段 label 的 O(1) 查找表(徽章 hook 性能契约) */
194
+ const fieldLabelMap = computed(() => {
195
+ const map = new Map<string, string>()
196
+ for (const dataset of catalog.value) {
197
+ for (const field of dataset.fields) map.set(`${dataset.id}:${field.name}`, field.label)
198
+ }
199
+ return map
200
+ })
201
+
202
+ function resolveFieldLabel(datasetId: string, fieldName: string): string {
203
+ return fieldLabelMap.value.get(`${datasetId}:${fieldName}`) ?? fieldName
204
+ }
205
+
206
+ const boundKeys = computed(() => {
207
+ metaTick.value
208
+ const keys = new Set<string>()
209
+ for (const [, namespace, payload] of workbook.value.activeSheet.entriesCellMeta()) {
210
+ if (namespace !== REPORT_META_NAMESPACE) continue
211
+ const binding = payload as ReportBinding
212
+ keys.add(`${binding.dataset}:${binding.field}`)
213
+ }
214
+ return keys
215
+ })
216
+
217
+ function getBindingAt(addr: CellAddress): ReportBinding | undefined {
218
+ return workbook.value.activeSheet.getCellMeta<ReportBinding>(addr, REPORT_META_NAMESPACE)
219
+ }
220
+
221
+ function bindField(datasetId: string, fieldName: string, addr?: CellAddress): void {
222
+ const dataset = catalog.value.find((item) => item.id === datasetId)
223
+ if (!dataset) return
224
+
225
+ const field = dataset.fields.find((item) => item.name === fieldName)
226
+ const target = addr ?? workbook.value.activeSheet.getSelection().activeCell
227
+ if (!target) return
228
+
229
+ const preset = inferDropPreset(target, field?.type ?? 'string', getBindingAt)
230
+ let binding = createReportBinding(dataset, fieldName)
231
+ binding = applyReportPreset(binding, preset)
232
+
233
+ const parents = inferParentsForPreset(target, preset, getBindingAt)
234
+ if (parents.rowParent) binding.rowParent = parents.rowParent
235
+ if (parents.colParent) binding.colParent = parents.colParent
236
+
237
+ workbook.value.activeSheet.setCellMeta(target, REPORT_META_NAMESPACE, binding)
238
+ }
239
+
240
+ // ---- Action Pill / 拓扑连线(playground 设计器平移,行为不变) ----
241
+
242
+ const activeBinding = computed((): ReportBinding | undefined => {
243
+ metaTick.value
244
+ const cell = activeCell.value
245
+ if (!cell) return undefined
246
+ return getBindingAt(cell)
247
+ })
248
+
249
+ const activeFieldType = computed((): DatasetField['type'] => {
250
+ const binding = activeBinding.value
251
+ if (!binding) return 'number'
252
+ const dataset = catalog.value.find((item) => item.id === binding.dataset)
253
+ const field = dataset?.fields.find((item) => item.name === binding.field)
254
+ return field?.type ?? 'number'
255
+ })
256
+
257
+ const activeDatasetFields = computed((): DatasetField[] => {
258
+ const binding = activeBinding.value
259
+ if (!binding) return []
260
+ return catalog.value.find((item) => item.id === binding.dataset)?.fields ?? []
261
+ })
262
+
263
+ const resolvedRowParentLabel = computed(() => {
264
+ metaTick.value
265
+ const binding = activeBinding.value
266
+ if (!binding?.rowParent) return '—'
267
+ return formatCellAddress(binding.rowParent)
268
+ })
269
+
270
+ const resolvedColParentLabel = computed(() => {
271
+ metaTick.value
272
+ const binding = activeBinding.value
273
+ if (!binding?.colParent) return '—'
274
+ return formatCellAddress(binding.colParent)
275
+ })
276
+
277
+ const rowParentCandidates = computed((): CellAddress[] => {
278
+ metaTick.value
279
+ const binding = activeBinding.value
280
+ if (!binding) return []
281
+ return listRowParentCandidates(binding, bindingEntries.value, {
282
+ preferGroup: prefersGroupParent(binding)
283
+ })
284
+ })
285
+
286
+ const colParentCandidates = computed((): CellAddress[] => {
287
+ metaTick.value
288
+ const binding = activeBinding.value
289
+ if (!binding) return []
290
+ return listColParentCandidates(binding, bindingEntries.value, {
291
+ preferGroup: prefersGroupParent(binding)
292
+ })
293
+ })
294
+
295
+ const bindingEntries = computed((): TopologyBindingEntry[] => {
296
+ metaTick.value
297
+ const entries: TopologyBindingEntry[] = []
298
+ for (const [addr, namespace, payload] of workbook.value.activeSheet.entriesCellMeta()) {
299
+ if (namespace !== REPORT_META_NAMESPACE) continue
300
+ entries.push({ addr, binding: payload as ReportBinding })
301
+ }
302
+ return entries
303
+ })
304
+
305
+ function parentNeedsCorrection(addr: CellAddress | undefined): boolean {
306
+ if (!addr) return true
307
+ const parent = getBindingAt(addr)
308
+ return !parent || parent.aggregate === 'list'
309
+ }
310
+
311
+ function patchActiveBinding(patch: Partial<ReportBinding>): void {
312
+ const cell = activeCell.value
313
+ const binding = activeBinding.value
314
+ if (!cell || !binding) return
315
+
316
+ const next: ReportBinding = { ...binding, ...patch }
317
+ if ('rowParent' in patch && patch.rowParent === undefined) delete next.rowParent
318
+ if ('colParent' in patch && patch.colParent === undefined) delete next.colParent
319
+ if (
320
+ ('aggregate' in patch || 'expand' in patch) &&
321
+ !('preset' in patch) &&
322
+ !presetStillApplies(next)
323
+ ) {
324
+ delete next.preset
325
+ }
326
+
327
+ if (patch.preset === 'subtotal') {
328
+ const inferred = inferParentsForPreset(cell, 'subtotal', getBindingAt)
329
+ if (parentNeedsCorrection(next.rowParent)) {
330
+ if (inferred.rowParent) next.rowParent = inferred.rowParent
331
+ else delete next.rowParent
332
+ }
333
+ if (parentNeedsCorrection(next.colParent)) {
334
+ if (inferred.colParent) next.colParent = inferred.colParent
335
+ else delete next.colParent
336
+ }
337
+ }
338
+
339
+ workbook.value.activeSheet.setCellMeta(cell, REPORT_META_NAMESPACE, next)
340
+ }
341
+
342
+ function startParentPick(mode: ParentPickMode): void {
343
+ const cell = activeCell.value
344
+ if (!cell || !activeBinding.value) return
345
+ parentPick.value = { mode, source: { row: cell.row, col: cell.col } }
346
+ }
347
+
348
+ function cancelParentPick(): void {
349
+ parentPick.value = null
350
+ }
351
+
352
+ function isValidParentTarget(
353
+ mode: ParentPickMode,
354
+ source: ReportBinding,
355
+ target: ReportBinding | undefined
356
+ ): boolean {
357
+ if (!target) return false
358
+ if (target.dataset !== source.dataset) return false
359
+ const groupOnly = prefersGroupParent(source)
360
+ if (mode === 'row') {
361
+ return groupOnly ? isRowGroupBinding(target) : isVerticalExpandBinding(target)
362
+ }
363
+ return groupOnly ? isColGroupBinding(target) : isHorizontalExpandBinding(target)
364
+ }
365
+
366
+ function pickParentAt(target: CellAddress): void {
367
+ const pick = parentPick.value
368
+ if (!pick) return
369
+ if (target.row === pick.source.row && target.col === pick.source.col) return
370
+
371
+ const sourceBinding = getBindingAt(pick.source)
372
+ if (!sourceBinding) {
373
+ parentPick.value = null
374
+ return
375
+ }
376
+
377
+ const targetBinding = getBindingAt(target)
378
+ if (!isValidParentTarget(pick.mode, sourceBinding, targetBinding)) {
379
+ workbook.value.activeSheet.selectCell(pick.source)
380
+ return
381
+ }
382
+
383
+ const patch = pick.mode === 'row' ? { rowParent: { ...target } } : { colParent: { ...target } }
384
+ const next: ReportBinding = { ...sourceBinding, ...patch }
385
+ workbook.value.activeSheet.setCellMeta(pick.source, REPORT_META_NAMESPACE, next)
386
+ parentPick.value = null
387
+ workbook.value.activeSheet.selectCell(pick.source)
388
+ }
389
+
390
+ function clearParent(mode: ParentPickMode): void {
391
+ if (mode === 'row') patchActiveBinding({ rowParent: undefined })
392
+ else patchActiveBinding({ colParent: undefined })
393
+ }
394
+
395
+ function removeActiveBinding(): void {
396
+ const cell = activeCell.value
397
+ if (!cell) return
398
+ workbook.value.activeSheet.clearCellMeta(cell, REPORT_META_NAMESPACE)
399
+ }
400
+
401
+ // ---- 数据中枢:连接 / 数据集 CRUD 与连接器调用 ----
402
+
403
+ function addConnection(connection: DataConnection): void {
404
+ connections.value = [...connections.value, connection]
405
+ }
406
+
407
+ function updateConnection(connection: DataConnection): void {
408
+ connections.value = connections.value.map((item) =>
409
+ item.id === connection.id ? connection : item
410
+ )
411
+ }
412
+
413
+ function removeConnection(connectionId: string): void {
414
+ connections.value = connections.value.filter((item) => item.id !== connectionId)
415
+ // 级联删除其数据集(playground 设计器平移)
416
+ datasets.value = datasets.value.filter((dataset) => dataset.connectionId !== connectionId)
417
+ }
418
+
419
+ let datasetSeq = 0
420
+ function addDataset(connectionId: string): DesignerDataset {
421
+ const dataset: DesignerDataset = {
422
+ id: `dataset-${Date.now()}-${datasetSeq++}`,
423
+ label: '新数据集',
424
+ connectionId,
425
+ sql: ''
426
+ }
427
+ datasets.value = [...datasets.value, dataset]
428
+ return dataset
429
+ }
430
+
431
+ function updateDataset(datasetId: string, patch: Partial<Omit<DesignerDataset, 'id'>>): void {
432
+ datasets.value = datasets.value.map((item) =>
433
+ item.id === datasetId ? { ...item, ...patch } : item
434
+ )
435
+ }
436
+
437
+ function removeDataset(datasetId: string): void {
438
+ datasets.value = datasets.value.filter((item) => item.id !== datasetId)
439
+ }
440
+
441
+ function resolveDatasetConnection(dataset: DesignerDataset): DataConnection | undefined {
442
+ return connections.value.find((item) => item.id === dataset.connectionId)
443
+ }
444
+
445
+ function datasetNotFound(): Result<never> {
446
+ return { ok: false, error: { code: 'DATASET_NOT_FOUND', message: '数据集不存在' } }
447
+ }
448
+
449
+ function connectionNotFound(): Result<never> {
450
+ return { ok: false, error: { code: 'CONNECTION_NOT_FOUND', message: '数据集所属连接不存在' } }
451
+ }
452
+
453
+ function testConnection(connection: DataConnection): Promise<Result<void>> {
454
+ return props.connector.test(connection)
455
+ }
456
+
457
+ async function describeDataset(datasetId: string): Promise<Result<DatasetField[]>> {
458
+ const dataset = datasets.value.find((item) => item.id === datasetId)
459
+ if (!dataset) return datasetNotFound()
460
+ const connection = resolveDatasetConnection(dataset)
461
+ if (!connection) return connectionNotFound()
462
+
463
+ const sql = dataset.sql
464
+ const result = await props.connector.describe(connection, sql)
465
+ // 并发守卫:describe 返回时 SQL 已再变更则丢弃过期结果
466
+ if (result.ok) {
467
+ const current = datasets.value.find((item) => item.id === datasetId)
468
+ if (current && current.sql === sql) updateDataset(datasetId, { fields: result.data })
469
+ }
470
+ return result
471
+ }
472
+
473
+ async function previewDataset(datasetId: string): Promise<Result<QueryResult>> {
474
+ const dataset = datasets.value.find((item) => item.id === datasetId)
475
+ if (!dataset) return datasetNotFound()
476
+ const connection = resolveDatasetConnection(dataset)
477
+ if (!connection) return connectionNotFound()
478
+
479
+ const values = resolveParamDefaults(buildParamDefs(dataset.sql, dataset.paramOverrides))
480
+ return props.connector.query(connection, dataset.sql, values)
481
+ }
482
+
483
+ // ---- 渲染与模板 ----
484
+
485
+ const resolveCellStyle = createBindingBadgeStyleResolver(getBindingAt)
486
+ const resolveCellRenderer = createBindingBadgeRenderer(getBindingAt, resolveFieldLabel)
487
+
488
+ function getTemplate(): ReportTemplate {
489
+ const resolved: ReportDatasetDef[] = datasets.value.flatMap((dataset) => {
490
+ const connection = resolveDatasetConnection(dataset)
491
+ if (!connection) return []
492
+ return [
493
+ {
494
+ id: dataset.id,
495
+ label: dataset.label,
496
+ connection: { ...connection },
497
+ sql: dataset.sql,
498
+ ...(dataset.paramOverrides ? { paramOverrides: dataset.paramOverrides } : {}),
499
+ ...(dataset.fieldOverrides ? { fieldOverrides: dataset.fieldOverrides } : {})
500
+ }
501
+ ]
502
+ })
503
+ // `Sheet.snapshot()` 不产生 datasets 字段(restore 往返会丢失),吐出时附加内嵌数据集定义
504
+ return createReportTemplate(workbook.value.activeSheet.snapshot(), resolved)
505
+ }
506
+
507
+ /**
508
+ * 载入既有 Report Template 继续设计(`template` prop):
509
+ * 快照恢复网格绑定(restore + restoreContent 与查看器同路径);
510
+ * 内嵌数据集定义还原为设计态(connectionId 引用),内嵌连接按 id 合并进
511
+ * v-model 连接列表(仅缺省追加,宿主列表是单一事实源);
512
+ * describe 恢复字段缓存(字段面板数据源),业务错误忽略(字段留空,可在数据中枢重试)。
513
+ */
514
+ function loadTemplate(template: ReportTemplate): void {
515
+ assertCompatibleTemplateVersion(template)
516
+ const sheet = workbook.value.activeSheet
517
+ sheet.restore(template)
518
+ sheet.restoreContent(template)
519
+ sheet.history.clear()
520
+ syncActiveCell()
521
+
522
+ const defs = getTemplateDatasets(template)
523
+ datasets.value = defs.map((def) => ({
524
+ id: def.id,
525
+ label: def.label,
526
+ connectionId: def.connection.id,
527
+ sql: def.sql,
528
+ ...(def.paramOverrides ? { paramOverrides: def.paramOverrides } : {}),
529
+ ...(def.fieldOverrides ? { fieldOverrides: def.fieldOverrides } : {})
530
+ }))
531
+
532
+ const missing = defs
533
+ .map((def) => def.connection)
534
+ .filter((connection) => !connections.value.some((item) => item.id === connection.id))
535
+ if (missing.length > 0) {
536
+ connections.value = [
537
+ ...connections.value,
538
+ ...missing.map((connection) => ({ ...connection }))
539
+ ]
540
+ }
541
+
542
+ for (const dataset of datasets.value) {
543
+ if (dataset.sql.trim()) void describeDataset(dataset.id)
544
+ }
545
+ }
546
+
547
+ // 模板更换:重新载入(仅在提供了 template 时;缺省为空白设计态)
548
+ watch(
549
+ () => props.template,
550
+ (template) => {
551
+ if (template) loadTemplate(template)
552
+ },
553
+ { immediate: true }
554
+ )
555
+
556
+ return {
557
+ workbook,
558
+ connections,
559
+ datasets,
560
+ catalog,
561
+ boundKeys,
562
+ activeCell,
563
+ selectionLabel,
564
+ activeBinding,
565
+ activeFieldType,
566
+ activeDatasetFields,
567
+ resolvedRowParentLabel,
568
+ resolvedColParentLabel,
569
+ parentPick,
570
+ rowParentCandidates,
571
+ colParentCandidates,
572
+ bindingEntries,
573
+ metaTick,
574
+ getBindingAt,
575
+ resolveFieldLabel,
576
+ bindField,
577
+ patchActiveBinding,
578
+ startParentPick,
579
+ cancelParentPick,
580
+ pickParentAt,
581
+ clearParent,
582
+ removeActiveBinding,
583
+ resolveCellStyle,
584
+ resolveCellRenderer,
585
+ getTemplate,
586
+ addConnection,
587
+ updateConnection,
588
+ removeConnection,
589
+ addDataset,
590
+ updateDataset,
591
+ removeDataset,
592
+ testConnection,
593
+ describeDataset,
594
+ previewDataset
595
+ }
596
+ }
@@ -0,0 +1,88 @@
1
+ import type { SheetSnapshot } from '@veltra/sheet-core'
2
+ import { computed, ref, shallowRef, watch, type ComputedRef, type Ref, type ShallowRef } from 'vue'
3
+
4
+ import type { ConnectorError } from '../../report/connector'
5
+ import type { ReportColWidthEntry } from '../../report/export-xlsx'
6
+ import { renderReport, resolveFilledColWidths } from '../../report/render'
7
+ import {
8
+ fetchTemplateRecords,
9
+ resolveParamDefaults,
10
+ resolveTemplateParams
11
+ } from '../../report/template'
12
+ import type { ParamValues, QueryParamDef } from '../../report/types'
13
+ import type { ReportViewerProps } from '../../types'
14
+
15
+ export interface UseReportViewerReturn {
16
+ /** Filter Bar 参数(模板实际绑定的数据集的查询参数并集) */
17
+ params: ComputedRef<QueryParamDef[]>
18
+ /** 当前运行时参数值(初始为参数默认值) */
19
+ values: Ref<ParamValues>
20
+ /** 取数中 */
21
+ loading: Ref<boolean>
22
+ /** 最近一次取数的业务错误(可读提示;成功或重新取数时清空) */
23
+ error: ShallowRef<ConnectorError | null>
24
+ /** 最近一次成功展开渲染的 Filled Report 快照(组件壳据此替换网格内容) */
25
+ filledSnapshot: ShallowRef<SheetSnapshot | null>
26
+ /** 展开后物理列宽(由模板 colWidths 映射;无模板列宽时为 undefined) */
27
+ filledColWidths: ShallowRef<ReadonlyArray<ReportColWidthEntry> | undefined>
28
+ /** 重新取数并展开渲染 */
29
+ refresh: () => Promise<void>
30
+ /** Filter Bar 改值:写入新参数值并重新取数 */
31
+ setValues: (next: ParamValues) => void
32
+ }
33
+
34
+ /**
35
+ * UReportViewer 的 headless 编排:参数提取 → 取数 → renderReport 展开。
36
+ * 不持有 DOM / 网格引用;填充快照经 `filledSnapshot` 交给组件壳应用。
37
+ */
38
+ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn {
39
+ const loading = ref(false)
40
+ const error = shallowRef<ConnectorError | null>(null)
41
+ const filledSnapshot = shallowRef<SheetSnapshot | null>(null)
42
+ const filledColWidths = shallowRef<ReadonlyArray<ReportColWidthEntry> | undefined>(undefined)
43
+
44
+ const params = computed(() => resolveTemplateParams(props.template))
45
+ // 初始值由下方 immediate watch 播种(参数默认值)
46
+ const values = shallowRef<ParamValues>({})
47
+
48
+ // 并发守卫:只应用最后一次取数结果(Filter Bar 连续改值时前序结果作废)
49
+ let fetchSeq = 0
50
+
51
+ async function refresh(): Promise<void> {
52
+ const current = ++fetchSeq
53
+ const template = props.template
54
+ loading.value = true
55
+ error.value = null
56
+ const result = await fetchTemplateRecords(props.connector, template, values.value)
57
+ if (current !== fetchSeq) return
58
+ loading.value = false
59
+ if (!result.ok) {
60
+ error.value = result.error
61
+ filledColWidths.value = undefined
62
+ return
63
+ }
64
+ filledSnapshot.value = renderReport(template, result.data)
65
+ filledColWidths.value = template.colWidths?.length
66
+ ? resolveFilledColWidths(template, result.data, template.colWidths)
67
+ : undefined
68
+ }
69
+
70
+ function setValues(next: ParamValues): void {
71
+ values.value = next
72
+ void refresh()
73
+ }
74
+
75
+ // 模板更换:清空上一模板的填充结果,参数值重置为新模板默认值并重新取数
76
+ watch(
77
+ () => props.template,
78
+ () => {
79
+ filledSnapshot.value = null
80
+ filledColWidths.value = undefined
81
+ values.value = resolveParamDefaults(params.value)
82
+ void refresh()
83
+ },
84
+ { immediate: true }
85
+ )
86
+
87
+ return { params, values, loading, error, filledSnapshot, filledColWidths, refresh, setValues }
88
+ }