@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,113 @@
1
+ import type { CellAddress, SheetGrid } from '@veltra/sheet-core'
2
+ import { onBeforeUnmount, watch, type Ref } from 'vue'
3
+
4
+ import {
5
+ getCellOverlayRect,
6
+ resolveGridOverlayLayout,
7
+ type CellOverlayRect,
8
+ type GridOverlayLayout
9
+ } from './cell-coords'
10
+
11
+ type GridGetter = () => SheetGrid | undefined
12
+
13
+ /** 网格滚动时重算 overlay 坐标;避免在 props 不稳定时重复绑定监听 */
14
+ export function useGridOverlaySync(options: {
15
+ hostEl: Ref<HTMLElement | null>
16
+ getGrid: GridGetter
17
+ watchSources: () => readonly unknown[]
18
+ update: () => void
19
+ }): void {
20
+ let offScroll: (() => void) | undefined
21
+ let rafId = 0
22
+
23
+ function scheduleUpdate(): void {
24
+ cancelAnimationFrame(rafId)
25
+ rafId = requestAnimationFrame(options.update)
26
+ }
27
+
28
+ function bindScroll(): void {
29
+ offScroll?.()
30
+ offScroll = undefined
31
+ const grid = options.getGrid()
32
+ if (!grid) return
33
+
34
+ const table = grid.getTable()
35
+ const onScroll = (): void => scheduleUpdate()
36
+ const scrollEvent = 'scroll'
37
+ table.on(scrollEvent, onScroll)
38
+ offScroll = () => table.off(scrollEvent, onScroll)
39
+ }
40
+
41
+ watch(
42
+ options.watchSources,
43
+ () => {
44
+ bindScroll()
45
+ scheduleUpdate()
46
+ },
47
+ { immediate: true, flush: 'post' }
48
+ )
49
+
50
+ onBeforeUnmount(() => {
51
+ cancelAnimationFrame(rafId)
52
+ offScroll?.()
53
+ })
54
+ }
55
+
56
+ export function readCellOverlayRect(
57
+ cell: CellAddress,
58
+ host: HTMLElement,
59
+ getGrid: GridGetter
60
+ ): ReturnType<typeof getCellOverlayRect> {
61
+ const grid = getGrid()
62
+ if (!grid) return null
63
+ const resolved = resolveGridOverlayLayout(host)
64
+ if (!resolved) return null
65
+ return getCellOverlayRect(grid, cell, resolved.layout)
66
+ }
67
+
68
+ export function readGridOverlaySize(host: HTMLElement): { width: number; height: number } | null {
69
+ const resolved = resolveGridOverlayLayout(host)
70
+ if (!resolved) return null
71
+ const { layout } = resolved
72
+ return { width: layout.viewW + layout.offsetX, height: layout.viewH + layout.offsetY }
73
+ }
74
+
75
+ export interface BindingFloatPanelPosition {
76
+ placement: 'above' | 'below'
77
+ left: number
78
+ top: number
79
+ }
80
+
81
+ /** 悬浮编辑卡锚点:空间按网格画布(非宿主顶)计算,避免顶行格误判为上方可放 */
82
+ export function resolveBindingFloatPanelPosition(
83
+ rect: CellOverlayRect,
84
+ layout: GridOverlayLayout,
85
+ panelWidth: number,
86
+ panelHeight: number,
87
+ hostWidth: number,
88
+ options?: { gap?: number; edgePad?: number }
89
+ ): BindingFloatPanelPosition {
90
+ const gap = options?.gap ?? 8
91
+ const edgePad = options?.edgePad ?? 10
92
+ const gridTop = layout.offsetY
93
+ const gridBottom = layout.offsetY + layout.viewH
94
+
95
+ let left = rect.centerX
96
+ const halfW = panelWidth / 2
97
+ left = Math.max(edgePad + halfW, Math.min(hostWidth - edgePad - halfW, left))
98
+
99
+ const spaceAbove = rect.top - gridTop
100
+ const spaceBelow = gridBottom - rect.bottom
101
+ const need = panelHeight + gap
102
+
103
+ let placement: 'above' | 'below' =
104
+ spaceAbove >= need || spaceAbove >= spaceBelow ? 'above' : 'below'
105
+
106
+ if (placement === 'above' && rect.top - gap - panelHeight < gridTop + edgePad) {
107
+ if (spaceBelow >= need || spaceBelow > spaceAbove) placement = 'below'
108
+ }
109
+
110
+ const top = placement === 'above' ? rect.top - gap : rect.bottom + gap
111
+
112
+ return { placement, left: Math.round(left), top: Math.round(top) }
113
+ }
@@ -0,0 +1,198 @@
1
+ <template>
2
+ <div :class="cls.b">
3
+ <header :class="cls.e('head')">
4
+ <h3 :class="cls.e('title')">数据中枢</h3>
5
+ <u-button size="small" type="primary" @click="emit('close')">完成</u-button>
6
+ </header>
7
+
8
+ <div :class="cls.e('body')">
9
+ <aside :class="cls.e('sidebar')">
10
+ <div :class="cls.e('sidebar-head')">
11
+ <span :class="cls.e('sidebar-title')">连接与数据集</span>
12
+ <u-button size="small" plain @click="startNewConnection">新建连接</u-button>
13
+ </div>
14
+
15
+ <div :class="cls.e('tree')">
16
+ <section v-for="conn in connections" :key="conn.id" :class="cls.e('conn-group')">
17
+ <div
18
+ :class="[
19
+ cls.e('conn'),
20
+ bem.is('active', activeConnectionId === conn.id && !activeDatasetId)
21
+ ]"
22
+ @click="selectConnection(conn.id)"
23
+ >
24
+ <span :class="cls.e('conn-label')">{{ conn.label }}</span>
25
+ <u-tag size="small" type="info">{{ conn.type }}</u-tag>
26
+ </div>
27
+
28
+ <ul :class="cls.e('dataset-list')">
29
+ <li
30
+ v-for="item in datasetsByConnection(conn.id)"
31
+ :key="item.id"
32
+ :class="[cls.e('dataset-item'), bem.is('active', activeDatasetId === item.id)]"
33
+ @click="selectDataset(item.id)"
34
+ >
35
+ <span>{{ item.label }}</span>
36
+ <span :class="cls.e('dataset-id')">{{ item.id }}</span>
37
+ </li>
38
+ </ul>
39
+
40
+ <u-button size="small" text :class="cls.e('add-dataset')" @click="addDataset(conn.id)">
41
+ + 新建数据集
42
+ </u-button>
43
+ </section>
44
+
45
+ <p v-if="connections.length === 0" :class="cls.e('sidebar-empty')">
46
+ 尚无连接。点「新建连接」配置 MySQL / PostgreSQL 数据连接。
47
+ </p>
48
+ </div>
49
+ </aside>
50
+
51
+ <div :class="cls.e('main')">
52
+ <u-report-hub-dataset-editor
53
+ v-if="activeDatasetId"
54
+ :hub="hub"
55
+ :dataset-id="activeDatasetId"
56
+ @remove="removeActiveDataset"
57
+ />
58
+ <u-report-hub-connection-form
59
+ v-else-if="connectionDraft"
60
+ :connection="connectionDraft"
61
+ :is-new="creatingConnection"
62
+ :test="hub.testConnection"
63
+ @save="saveConnection"
64
+ @remove="removeActiveConnection"
65
+ />
66
+ <div v-else :class="cls.e('empty')">
67
+ <p>选择左侧连接或数据集开始配置;数据集字段解析后即可在字段面板拖拽绑定。</p>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </template>
73
+
74
+ <script lang="ts" setup>
75
+ import { UButton, UTag } from '@veltra/desktop'
76
+ import { bem } from '@veltra/utils'
77
+ import { computed, ref, watch } from 'vue'
78
+
79
+ import type { DataConnection } from '../../report/connector'
80
+ import { DEFAULT_CONNECTION_PORTS } from '../../report/connector'
81
+ import UReportHubConnectionForm from './hub-connection-form.vue'
82
+ import UReportHubDatasetEditor from './hub-dataset-editor.vue'
83
+ import type { DatasetHubController } from './use-report-designer'
84
+
85
+ defineOptions({ name: 'UReportDatasetHub' })
86
+
87
+ const props = defineProps<{ hub: DatasetHubController }>()
88
+
89
+ const emit = defineEmits<{ close: [] }>()
90
+
91
+ const cls = bem('report-hub')
92
+
93
+ const connections = computed(() => props.hub.connections.value)
94
+ const datasets = computed(() => props.hub.datasets.value)
95
+
96
+ function datasetsByConnection(connectionId: string) {
97
+ return datasets.value.filter((item) => item.connectionId === connectionId)
98
+ }
99
+
100
+ const activeConnectionId = ref<string>()
101
+ const activeDatasetId = ref<string>()
102
+ const creatingConnection = ref(false)
103
+ const connectionDraft = ref<DataConnection>()
104
+
105
+ // 打开时默认选中首个连接 / 数据集
106
+ watch(
107
+ connections,
108
+ () => {
109
+ if (!activeConnectionId.value && connections.value[0]) {
110
+ activeConnectionId.value = connections.value[0]!.id
111
+ syncConnectionDraft()
112
+ }
113
+ },
114
+ { immediate: true }
115
+ )
116
+
117
+ function syncConnectionDraft(): void {
118
+ if (activeDatasetId.value) {
119
+ connectionDraft.value = undefined
120
+ return
121
+ }
122
+ if (creatingConnection.value) return
123
+ const id = activeConnectionId.value
124
+ const conn = id ? connections.value.find((item) => item.id === id) : undefined
125
+ connectionDraft.value = conn ? { ...conn } : undefined
126
+ }
127
+
128
+ function selectConnection(connectionId: string): void {
129
+ activeConnectionId.value = connectionId
130
+ activeDatasetId.value = undefined
131
+ creatingConnection.value = false
132
+ syncConnectionDraft()
133
+ }
134
+
135
+ function selectDataset(datasetId: string): void {
136
+ activeDatasetId.value = datasetId
137
+ const dataset = datasets.value.find((item) => item.id === datasetId)
138
+ if (dataset) activeConnectionId.value = dataset.connectionId
139
+ connectionDraft.value = undefined
140
+ creatingConnection.value = false
141
+ }
142
+
143
+ let connectionSeq = 0
144
+ function startNewConnection(): void {
145
+ creatingConnection.value = true
146
+ activeDatasetId.value = undefined
147
+ connectionDraft.value = {
148
+ id: `conn-${Date.now()}-${connectionSeq++}`,
149
+ label: '新连接',
150
+ type: 'mysql',
151
+ host: '127.0.0.1',
152
+ port: DEFAULT_CONNECTION_PORTS.mysql,
153
+ database: '',
154
+ username: '',
155
+ password: ''
156
+ }
157
+ }
158
+
159
+ function saveConnection(conn: DataConnection): void {
160
+ if (creatingConnection.value) {
161
+ props.hub.addConnection(conn)
162
+ creatingConnection.value = false
163
+ } else {
164
+ props.hub.updateConnection(conn)
165
+ }
166
+ activeConnectionId.value = conn.id
167
+ syncConnectionDraft()
168
+ }
169
+
170
+ function removeActiveConnection(): void {
171
+ const id = activeConnectionId.value
172
+ if (!id) return
173
+ // removeConnection 级联删除其数据集
174
+ props.hub.removeConnection(id)
175
+ activeDatasetId.value = undefined
176
+ activeConnectionId.value = connections.value[0]?.id
177
+ creatingConnection.value = false
178
+ syncConnectionDraft()
179
+ }
180
+
181
+ function addDataset(connectionId: string): void {
182
+ const dataset = props.hub.addDataset(connectionId)
183
+ activeConnectionId.value = connectionId
184
+ activeDatasetId.value = dataset.id
185
+ creatingConnection.value = false
186
+ connectionDraft.value = undefined
187
+ }
188
+
189
+ function removeActiveDataset(): void {
190
+ const id = activeDatasetId.value
191
+ if (!id) return
192
+ props.hub.removeDataset(id)
193
+ activeDatasetId.value = datasets.value.find(
194
+ (item) => item.connectionId === activeConnectionId.value
195
+ )?.id
196
+ syncConnectionDraft()
197
+ }
198
+ </script>
@@ -0,0 +1,44 @@
1
+ import type { DatasetCatalogItem, DatasetField } from '../../report/types'
2
+
3
+ /** 字段拖拽的 dataTransfer MIME(字段面板 dragstart / 网格 drop 共用) */
4
+ export const FIELD_DRAG_MIME = 'application/x-sheet-report-field'
5
+
6
+ /** 字段拖拽负载:`${datasetId}:${fieldName}` */
7
+ export function formatFieldDragPayload(datasetId: string, fieldName: string): string {
8
+ return `${datasetId}:${fieldName}`
9
+ }
10
+
11
+ /** 解析字段拖拽负载;非法输入返回 null */
12
+ export function parseFieldDragPayload(
13
+ raw: string
14
+ ): { datasetId: string; fieldName: string } | null {
15
+ const sep = raw.indexOf(':')
16
+ if (sep <= 0) return null
17
+ const fieldName = raw.slice(sep + 1)
18
+ if (!fieldName) return null
19
+ return { datasetId: raw.slice(0, sep), fieldName }
20
+ }
21
+
22
+ export function fieldTypeGlyph(type: DatasetField['type']): string {
23
+ if (type === 'number') return '#'
24
+ if (type === 'date') return '日'
25
+ return '文'
26
+ }
27
+
28
+ export function filterDatasetsByQuery(
29
+ datasets: DatasetCatalogItem[],
30
+ query: string
31
+ ): DatasetCatalogItem[] {
32
+ const keyword = query.trim().toLowerCase()
33
+ if (!keyword) return datasets
34
+
35
+ return datasets
36
+ .map((dataset) => ({
37
+ ...dataset,
38
+ fields: dataset.fields.filter(
39
+ (field) =>
40
+ field.label.toLowerCase().includes(keyword) || field.name.toLowerCase().includes(keyword)
41
+ )
42
+ }))
43
+ .filter((dataset) => dataset.fields.length > 0)
44
+ }
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <aside :class="cls.b">
3
+ <h3 :class="cls.e('title')">数据集字段</h3>
4
+ <p :class="cls.e('meta')">
5
+ 当前选区:
6
+ <u-tag size="small" type="primary">{{ selectionLabel }}</u-tag>
7
+ </p>
8
+
9
+ <u-input
10
+ v-model="searchQuery"
11
+ :class="cls.e('search')"
12
+ size="small"
13
+ clearable
14
+ placeholder="搜索字段…"
15
+ />
16
+
17
+ <p :class="cls.e('hint')">点击或拖拽字段到网格;中文名为主,英文键为绑定标识。</p>
18
+
19
+ <section v-for="dataset in visibleDatasets" :key="dataset.id" :class="cls.e('dataset')">
20
+ <u-button
21
+ text
22
+ size="small"
23
+ :class="cls.e('dataset-toggle')"
24
+ :aria-expanded="isDatasetExpanded(dataset.id)"
25
+ @click="toggleDataset(dataset.id)"
26
+ >
27
+ <span
28
+ :class="[cls.e('dataset-chevron'), bem.is('expanded', isDatasetExpanded(dataset.id))]"
29
+ aria-hidden="true"
30
+ >
31
+ ▸
32
+ </span>
33
+ <span :class="cls.e('dataset-name')">{{ dataset.label }}</span>
34
+ <span :class="cls.e('dataset-count')">{{ dataset.fields.length }}</span>
35
+ </u-button>
36
+
37
+ <div v-show="isDatasetExpanded(dataset.id)" :class="cls.e('fields')">
38
+ <div
39
+ v-for="field in dataset.fields"
40
+ :key="field.name"
41
+ role="button"
42
+ tabindex="0"
43
+ :class="[cls.e('pill'), bem.is('bound', isBound(dataset.id, field.name))]"
44
+ draggable="true"
45
+ :title="`${field.label}(${field.name})`"
46
+ @click="emit('bind', dataset.id, field.name)"
47
+ @keydown.enter.prevent="emit('bind', dataset.id, field.name)"
48
+ @keydown.space.prevent="emit('bind', dataset.id, field.name)"
49
+ @dragstart="onDragStart($event, dataset.id, field.name)"
50
+ >
51
+ <span :class="cls.e('pill-icon')" :data-type="field.type">{{
52
+ fieldTypeGlyph(field.type)
53
+ }}</span>
54
+ <span :class="cls.e('pill-label')">{{ field.label }}</span>
55
+ <span v-if="isBound(dataset.id, field.name)" :class="cls.e('pill-bound')" title="已绑定"
56
+ >✓</span
57
+ >
58
+ </div>
59
+ </div>
60
+ </section>
61
+
62
+ <p v-if="datasets.length === 0" :class="cls.e('empty')">
63
+ 尚无可用数据集,请点「数据中枢」创建连接与 SQL 数据集。
64
+ </p>
65
+ <p v-else-if="visibleDatasets.length === 0" :class="cls.e('empty')">没有匹配的字段。</p>
66
+ </aside>
67
+ </template>
68
+
69
+ <script lang="ts" setup>
70
+ import { UButton, UInput, UTag } from '@veltra/desktop'
71
+ import { bem } from '@veltra/utils'
72
+ import { computed, ref } from 'vue'
73
+
74
+ import type { DatasetCatalogItem } from '../../report/types'
75
+ import {
76
+ FIELD_DRAG_MIME,
77
+ fieldTypeGlyph,
78
+ filterDatasetsByQuery,
79
+ formatFieldDragPayload
80
+ } from './field-panel-helpers'
81
+
82
+ defineOptions({ name: 'UReportFieldPanel' })
83
+
84
+ const props = defineProps<{
85
+ /** 本报表已选用的数据集(含可编辑中文 label) */
86
+ datasets: DatasetCatalogItem[]
87
+ selectionLabel: string
88
+ /** 已绑定字段键:`${datasetId}:${fieldName}` */
89
+ boundKeys: Set<string>
90
+ }>()
91
+
92
+ const emit = defineEmits<{ bind: [datasetId: string, fieldName: string] }>()
93
+
94
+ const cls = bem('report-field-panel')
95
+
96
+ const searchQuery = ref('')
97
+ const collapsedDatasetIds = ref<Set<string>>(new Set())
98
+
99
+ const visibleDatasets = computed(() => filterDatasetsByQuery(props.datasets, searchQuery.value))
100
+
101
+ /** 折叠集合;搜索时视为全部展开(返回空集) */
102
+ const effectiveCollapsed = computed(() => {
103
+ const keyword = searchQuery.value.trim()
104
+ if (!keyword) return collapsedDatasetIds.value
105
+ return new Set<string>()
106
+ })
107
+
108
+ function isDatasetExpanded(datasetId: string): boolean {
109
+ return !effectiveCollapsed.value.has(datasetId)
110
+ }
111
+
112
+ function isBound(datasetId: string, fieldName: string): boolean {
113
+ return props.boundKeys.has(`${datasetId}:${fieldName}`)
114
+ }
115
+
116
+ function toggleDataset(datasetId: string): void {
117
+ const next = new Set(collapsedDatasetIds.value)
118
+ if (next.has(datasetId)) next.delete(datasetId)
119
+ else next.add(datasetId)
120
+ collapsedDatasetIds.value = next
121
+ }
122
+
123
+ function onDragStart(event: DragEvent, datasetId: string, fieldName: string): void {
124
+ event.dataTransfer?.setData(FIELD_DRAG_MIME, formatFieldDragPayload(datasetId, fieldName))
125
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'copy'
126
+
127
+ const target = event.currentTarget
128
+ if (event.dataTransfer && target instanceof HTMLElement) {
129
+ event.dataTransfer.setDragImage(
130
+ target,
131
+ Math.round(target.offsetWidth / 2),
132
+ Math.round(target.offsetHeight / 2)
133
+ )
134
+ }
135
+ }
136
+ </script>
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <section v-if="queryParams.length" :class="cls.b">
3
+ <span :class="cls.e('label')">筛选参数</span>
4
+ <div :class="cls.e('fields')">
5
+ <div v-for="param in queryParams" :key="param.id" :class="cls.e('field')">
6
+ <span :class="cls.e('field-label')">{{ param.label }}</span>
7
+ <u-date-picker
8
+ v-if="param.type === 'date'"
9
+ :model-value="String(values[param.id] ?? '')"
10
+ size="small"
11
+ @update:model-value="(v) => emitChange(param.id, v)"
12
+ />
13
+ <u-date-range-picker
14
+ v-else-if="param.type === 'date-range'"
15
+ :model-value="dateRangeModel(param.id)"
16
+ size="small"
17
+ @update:model-value="(v) => emitDateRangeChange(param.id, v)"
18
+ />
19
+ <u-select
20
+ v-else-if="param.type === 'select'"
21
+ :model-value="String(values[param.id] ?? '')"
22
+ size="small"
23
+ :options="param.options ?? []"
24
+ @update:model-value="(v) => emitChange(param.id, v)"
25
+ />
26
+ <u-number-input
27
+ v-else-if="param.type === 'number'"
28
+ :model-value="numberModel(param.id)"
29
+ size="small"
30
+ @update:model-value="(v) => emitChange(param.id, v)"
31
+ />
32
+ <u-input
33
+ v-else
34
+ :model-value="String(values[param.id] ?? '')"
35
+ size="small"
36
+ @update:model-value="(v) => emitChange(param.id, v)"
37
+ />
38
+ </div>
39
+ </div>
40
+ </section>
41
+ </template>
42
+
43
+ <script lang="ts" setup>
44
+ import { UDatePicker, UDateRangePicker, UInput, UNumberInput, USelect } from '@veltra/desktop'
45
+ import { bem } from '@veltra/utils'
46
+
47
+ import {
48
+ parseDateRangeValue,
49
+ patchParamValues,
50
+ resolveNumberParamValue
51
+ } from '../../report/filter-bar'
52
+ import type { ParamValues, QueryParamDef } from '../../report/types'
53
+
54
+ defineOptions({ name: 'UReportFilterBar' })
55
+
56
+ const props = defineProps<{ queryParams: QueryParamDef[]; values: ParamValues }>()
57
+
58
+ const emit = defineEmits<{ 'update:values': [values: ParamValues] }>()
59
+
60
+ const cls = bem('report-filter-bar')
61
+
62
+ function dateRangeModel(paramId: string): [string, string] {
63
+ return parseDateRangeValue(props.values[paramId])
64
+ }
65
+
66
+ function numberModel(paramId: string): number | undefined {
67
+ return resolveNumberParamValue(props.values[paramId])
68
+ }
69
+
70
+ function emitChange(id: string, value: unknown): void {
71
+ emit('update:values', patchParamValues(props.values, id, value))
72
+ }
73
+
74
+ function emitDateRangeChange(id: string, value?: [string, string]): void {
75
+ emit('update:values', patchParamValues(props.values, id, value ?? ['', '']))
76
+ }
77
+ </script>
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <section :class="cls.b">
3
+ <h4 :class="cls.e('title')">{{ isNew ? '新建连接' : '编辑连接' }}</h4>
4
+ <div :class="cls.e('grid')">
5
+ <div :class="cls.e('field')">
6
+ <span :class="cls.e('label')">显示名</span>
7
+ <u-input v-model="draft.label" size="small" placeholder="连接名称" />
8
+ </div>
9
+ <div :class="cls.e('field')">
10
+ <span :class="cls.e('label')">类型</span>
11
+ <u-select v-model="draft.type" size="small" :options="typeOptions" />
12
+ </div>
13
+ <div :class="cls.e('field')">
14
+ <span :class="cls.e('label')">主机</span>
15
+ <u-input v-model="draft.host" size="small" placeholder="127.0.0.1" />
16
+ </div>
17
+ <div :class="cls.e('field')">
18
+ <span :class="cls.e('label')">端口</span>
19
+ <u-input v-model="portText" size="small" type="number" :placeholder="String(defaultPort)" />
20
+ </div>
21
+ <div :class="cls.e('field')">
22
+ <span :class="cls.e('label')">数据库</span>
23
+ <u-input v-model="draft.database" size="small" placeholder="数据库名" />
24
+ </div>
25
+ <div :class="cls.e('field')">
26
+ <span :class="cls.e('label')">用户名</span>
27
+ <u-input v-model="draft.username" size="small" />
28
+ </div>
29
+ <div :class="cls.e('field')">
30
+ <span :class="cls.e('label')">密码</span>
31
+ <u-input v-model="draft.password" size="small" type="password" show-password />
32
+ </div>
33
+ </div>
34
+
35
+ <div v-if="testMessage" :class="[cls.e('test'), bem.is('ok', testOk)]" role="status">
36
+ {{ testMessage }}
37
+ </div>
38
+
39
+ <div :class="cls.e('actions')">
40
+ <u-button size="small" :loading="testing" @click="onTest">测试连接</u-button>
41
+ <u-button v-if="!isNew" size="small" type="danger" plain @click="emit('remove')">
42
+ 删除连接
43
+ </u-button>
44
+ <u-button size="small" type="primary" @click="onSave">保存</u-button>
45
+ </div>
46
+ </section>
47
+ </template>
48
+
49
+ <script lang="ts" setup>
50
+ import { UButton, UInput, USelect } from '@veltra/desktop'
51
+ import { bem } from '@veltra/utils'
52
+ import { computed, reactive, ref, watch } from 'vue'
53
+
54
+ import type { ConnectionType, DataConnection, Result } from '../../report/connector'
55
+ import { DEFAULT_CONNECTION_PORTS, resolvePortOnTypeChange } from '../../report/connector'
56
+
57
+ defineOptions({ name: 'UReportHubConnectionForm' })
58
+
59
+ const props = defineProps<{
60
+ connection: DataConnection
61
+ isNew?: boolean
62
+ /** 真实测试连接(经 DataConnector;契约无状态,新建草稿同样可测) */
63
+ test: (connection: DataConnection) => Promise<Result<void>>
64
+ }>()
65
+
66
+ const emit = defineEmits<{ save: [connection: DataConnection]; remove: [] }>()
67
+
68
+ const cls = bem('report-hub-connection-form')
69
+
70
+ /** 连接类型收敛为 MySQL / PostgreSQL(ADR-0003 决策 3) */
71
+ const typeOptions: { label: string; value: ConnectionType }[] = [
72
+ { label: 'MySQL', value: 'mysql' },
73
+ { label: 'PostgreSQL', value: 'postgresql' }
74
+ ]
75
+
76
+ const draft = reactive<DataConnection>({ ...props.connection })
77
+
78
+ const defaultPort = computed(() => DEFAULT_CONNECTION_PORTS[draft.type])
79
+
80
+ const portText = computed({
81
+ get: () => String(draft.port),
82
+ set: (value: string) => {
83
+ const n = Number(value)
84
+ draft.port = Number.isFinite(n) ? n : 0
85
+ }
86
+ })
87
+
88
+ const testing = ref(false)
89
+ const testMessage = ref('')
90
+ const testOk = ref(false)
91
+
92
+ watch(
93
+ () => props.connection,
94
+ (conn) => {
95
+ testMessage.value = ''
96
+ testOk.value = false
97
+ Object.assign(draft, conn)
98
+ },
99
+ { immediate: true }
100
+ )
101
+
102
+ watch(
103
+ () => draft.type,
104
+ (type, prevType) => {
105
+ if (!prevType) return
106
+ draft.port = resolvePortOnTypeChange(prevType, type, draft.port)
107
+ }
108
+ )
109
+
110
+ async function onTest(): Promise<void> {
111
+ testing.value = true
112
+ testMessage.value = ''
113
+ const result = await props.test({ ...draft })
114
+ testing.value = false
115
+ testOk.value = result.ok
116
+ testMessage.value = result.ok
117
+ ? `连接成功:${draft.label}(${draft.host}:${draft.port}/${draft.database})`
118
+ : result.error.message
119
+ }
120
+
121
+ function onSave(): void {
122
+ emit('save', { ...draft })
123
+ }
124
+ </script>