@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,252 @@
1
+ <template>
2
+ <div :class="cls.b">
3
+ <span :class="cls.e('handle')" title="拖拽排序">≡</span>
4
+
5
+ <div :class="cls.e('main')">
6
+ <div :class="cls.e('meta')">
7
+ <u-select
8
+ size="small"
9
+ :class="cls.e('field-select')"
10
+ :model-value="rule.field"
11
+ :options="fieldOptions"
12
+ placeholder="求值字段"
13
+ @update:model-value="onEvalField"
14
+ />
15
+ <u-select
16
+ size="small"
17
+ :class="cls.e('scope-select')"
18
+ :model-value="rule.scope ?? 'cell'"
19
+ :options="scopeOptions"
20
+ @update:model-value="onScope"
21
+ />
22
+ </div>
23
+
24
+ <div :class="cls.e('condition')">
25
+ <u-select
26
+ size="small"
27
+ :class="cls.e('operator')"
28
+ :model-value="rule.operator"
29
+ :options="operatorOptions"
30
+ @update:model-value="onOperator"
31
+ />
32
+
33
+ <div :class="cls.e('value')">
34
+ <template v-if="rule.operator === 'between'">
35
+ <u-number-input
36
+ v-if="numericField"
37
+ size="small"
38
+ :class="cls.e('between-input')"
39
+ :model-value="betweenStart as number"
40
+ @update:model-value="(v) => patchBetween(0, v ?? 0)"
41
+ />
42
+ <u-input
43
+ v-else
44
+ size="small"
45
+ :class="cls.e('between-input')"
46
+ :model-value="String(betweenStart)"
47
+ @update:model-value="(v) => patchBetween(0, String(v ?? ''))"
48
+ />
49
+ <span :class="cls.e('between-sep')">至</span>
50
+ <u-number-input
51
+ v-if="numericField"
52
+ size="small"
53
+ :class="cls.e('between-input')"
54
+ :model-value="betweenEnd as number"
55
+ @update:model-value="(v) => patchBetween(1, v ?? 0)"
56
+ />
57
+ <u-input
58
+ v-else
59
+ size="small"
60
+ :class="cls.e('between-input')"
61
+ :model-value="String(betweenEnd)"
62
+ @update:model-value="(v) => patchBetween(1, String(v ?? ''))"
63
+ />
64
+ </template>
65
+
66
+ <u-input
67
+ v-else-if="rule.operator === 'contains' || (!numericField && rule.operator === 'eq')"
68
+ size="small"
69
+ :class="cls.e('scalar-input')"
70
+ :model-value="String(rule.value ?? '')"
71
+ placeholder="比较值"
72
+ @update:model-value="(v) => patchValue(String(v ?? ''))"
73
+ />
74
+
75
+ <u-number-input
76
+ v-else
77
+ size="small"
78
+ :class="cls.e('scalar-input')"
79
+ :model-value="scalarNumber"
80
+ @update:model-value="(v) => patchValue(v ?? 0)"
81
+ />
82
+ </div>
83
+
84
+ <u-report-rule-preview :class="cls.e('preview')" :rule="rule" />
85
+ </div>
86
+
87
+ <div :class="cls.e('style')">
88
+ <span :class="cls.e('style-label')">背景</span>
89
+ <u-palette
90
+ size="small"
91
+ :model-value="rule.style.fill?.color ?? ''"
92
+ @update:model-value="patchFillColor"
93
+ />
94
+ <span :class="cls.e('style-label')">字体</span>
95
+ <u-palette
96
+ size="small"
97
+ :model-value="rule.style.font?.color ?? ''"
98
+ @update:model-value="patchFontColor"
99
+ />
100
+ <u-button
101
+ size="small"
102
+ :class="cls.e('toggle')"
103
+ :type="rule.style.font?.bold ? 'primary' : undefined"
104
+ plain
105
+ @click="toggleBold"
106
+ >
107
+ B
108
+ </u-button>
109
+ <u-button
110
+ size="small"
111
+ :class="cls.e('toggle')"
112
+ :type="rule.style.font?.italic ? 'primary' : undefined"
113
+ plain
114
+ @click="toggleItalic"
115
+ >
116
+ I
117
+ </u-button>
118
+ </div>
119
+ </div>
120
+
121
+ <div :class="cls.e('actions')">
122
+ <u-button size="small" text :disabled="index === 0" @click="emit('move-up')">↑</u-button>
123
+ <u-button size="small" text :disabled="index >= total - 1" @click="emit('move-down')">
124
+ ↓
125
+ </u-button>
126
+ <u-button size="small" text type="danger" @click="emit('remove')">删除</u-button>
127
+ </div>
128
+ </div>
129
+ </template>
130
+
131
+ <script lang="ts" setup>
132
+ import { UButton, UInput, UNumberInput, UPalette, USelect } from '@veltra/desktop'
133
+ import { bem } from '@veltra/utils'
134
+ import { computed } from 'vue'
135
+
136
+ import type { ConditionalOperator, ConditionalRule, DatasetField } from '../../../report/types'
137
+ import {
138
+ coerceValueForOperator,
139
+ isNumericFieldType,
140
+ operatorsForFieldType,
141
+ readBetweenValue,
142
+ resolveEvalFieldType,
143
+ RULE_SCOPE_OPTIONS,
144
+ ruleEvalFieldOptions,
145
+ writeBetweenValue
146
+ } from './conditional-rules/helpers'
147
+ import UReportRulePreview from './rule-preview.vue'
148
+
149
+ defineOptions({ name: 'UReportRuleRow' })
150
+
151
+ const props = defineProps<{
152
+ rule: ConditionalRule
153
+ bindingField: string
154
+ datasetFields: readonly DatasetField[]
155
+ index: number
156
+ total: number
157
+ }>()
158
+
159
+ const emit = defineEmits<{
160
+ 'update:rule': [rule: ConditionalRule]
161
+ remove: []
162
+ 'move-up': []
163
+ 'move-down': []
164
+ }>()
165
+
166
+ const cls = bem('report-rule-row')
167
+
168
+ const evalFieldType = computed(() =>
169
+ resolveEvalFieldType(props.rule, props.bindingField, props.datasetFields)
170
+ )
171
+ const operatorOptions = computed(() => operatorsForFieldType(evalFieldType.value))
172
+ const numericField = computed(() => isNumericFieldType(evalFieldType.value))
173
+ const fieldOptions = computed(() => ruleEvalFieldOptions(props.bindingField, props.datasetFields))
174
+ const scopeOptions = computed(() =>
175
+ RULE_SCOPE_OPTIONS.map((option) => ({ value: option.value, label: option.label }))
176
+ )
177
+
178
+ const betweenStart = computed(() => readBetweenValue(props.rule.value, 0, evalFieldType.value))
179
+ const betweenEnd = computed(() => readBetweenValue(props.rule.value, 1, evalFieldType.value))
180
+
181
+ const scalarNumber = computed((): number => {
182
+ const raw = props.rule.value
183
+ if (typeof raw === 'number' && Number.isFinite(raw)) return raw
184
+ const parsed = Number(raw)
185
+ return Number.isFinite(parsed) ? parsed : 0
186
+ })
187
+
188
+ function patchRule(patch: Partial<ConditionalRule>): void {
189
+ emit('update:rule', { ...props.rule, ...patch })
190
+ }
191
+
192
+ function onEvalField(field: string | undefined): void {
193
+ const next: Partial<ConditionalRule> = {}
194
+ if (field === undefined) {
195
+ next.field = undefined
196
+ } else {
197
+ next.field = field
198
+ }
199
+ const nextType = resolveEvalFieldType(
200
+ { ...props.rule, ...next },
201
+ props.bindingField,
202
+ props.datasetFields
203
+ )
204
+ const operator = operatorsForFieldType(nextType)[0]?.value ?? props.rule.operator
205
+ next.operator = operator
206
+ next.value = coerceValueForOperator(operator, nextType, props.rule.value)
207
+ patchRule(next)
208
+ }
209
+
210
+ function onScope(scope: 'cell' | 'row'): void {
211
+ patchRule({ scope: scope === 'cell' ? undefined : scope })
212
+ }
213
+
214
+ function onOperator(operator: ConditionalOperator): void {
215
+ patchRule({
216
+ operator,
217
+ value: coerceValueForOperator(operator, evalFieldType.value, props.rule.value)
218
+ })
219
+ }
220
+
221
+ function patchValue(value: unknown): void {
222
+ patchRule({ value })
223
+ }
224
+
225
+ function patchBetween(index: 0 | 1, next: number | string): void {
226
+ patchRule({ value: writeBetweenValue(props.rule.value, index, next, evalFieldType.value) })
227
+ }
228
+
229
+ function patchFillColor(color: string): void {
230
+ patchRule({ style: { ...props.rule.style, fill: color ? { color } : undefined } })
231
+ }
232
+
233
+ function patchFontColor(color: string): void {
234
+ patchRule({
235
+ style: { ...props.rule.style, font: { ...props.rule.style.font, color: color || undefined } }
236
+ })
237
+ }
238
+
239
+ function toggleBold(): void {
240
+ const bold = !props.rule.style.font?.bold
241
+ patchRule({
242
+ style: { ...props.rule.style, font: { ...props.rule.style.font, bold: bold || undefined } }
243
+ })
244
+ }
245
+
246
+ function toggleItalic(): void {
247
+ const italic = !props.rule.style.font?.italic
248
+ patchRule({
249
+ style: { ...props.rule.style, font: { ...props.rule.style.font, italic: italic || undefined } }
250
+ })
251
+ }
252
+ </script>
@@ -0,0 +1,126 @@
1
+ <template>
2
+ <u-dialog
3
+ v-model="visible"
4
+ title="条件样式"
5
+ style="width: min(960px, 92vw); max-height: 85vh"
6
+ @close="onClose"
7
+ >
8
+ <div :class="cls.b">
9
+ <p :class="cls.e('hint')">
10
+ 按列表顺序依次求值并合并样式。可指定求值字段(默认同绑定格字段);作用范围为「整行」时染满物理输出行(含横向展开列与静态格)。交叉表下整行高亮会覆盖同行所有列,明细行报表更合适。
11
+ </p>
12
+
13
+ <ul v-if="draftItems.length" ref="parentRef" :class="cls.e('list')">
14
+ <li v-for="(item, index) in draftItems" :key="item.id" :class="cls.e('item')">
15
+ <u-report-rule-row
16
+ :rule="item.rule"
17
+ :binding-field="bindingField"
18
+ :dataset-fields="datasetFields"
19
+ :index="index"
20
+ :total="draftItems.length"
21
+ @update:rule="item.rule = $event"
22
+ @remove="removeRule(index)"
23
+ @move-up="moveRule(index, -1)"
24
+ @move-down="moveRule(index, 1)"
25
+ />
26
+ </li>
27
+ </ul>
28
+ <p v-else :class="cls.e('empty')">暂无规则,点击下方按钮添加。</p>
29
+
30
+ <u-button size="small" plain @click="addRule">添加规则</u-button>
31
+ </div>
32
+
33
+ <template #footer>
34
+ <u-button size="small" @click="visible = false">取消</u-button>
35
+ <u-button size="small" type="primary" @click="confirm">确定</u-button>
36
+ </template>
37
+ </u-dialog>
38
+ </template>
39
+
40
+ <script lang="ts" setup>
41
+ import { animations, useDnD } from '@veltra/compositions'
42
+ import { UButton, UDialog } from '@veltra/desktop'
43
+ import { bem } from '@veltra/utils'
44
+ import { computed, ref, watch } from 'vue'
45
+
46
+ import type { ConditionalRule, DatasetField } from '../../../report/types'
47
+ import {
48
+ cloneRulesFromDraft,
49
+ createDraftItem,
50
+ initDraftFromRules,
51
+ type DraftRuleItem
52
+ } from './conditional-rules/helpers'
53
+ import UReportRuleRow from './rule-row.vue'
54
+
55
+ defineOptions({ name: 'UReportRulesDialog' })
56
+
57
+ const props = withDefaults(
58
+ defineProps<{
59
+ modelValue: boolean
60
+ rules: ConditionalRule[]
61
+ bindingField: string
62
+ datasetFields?: readonly DatasetField[]
63
+ /** @deprecated 使用 datasetFields + resolveEvalFieldType */
64
+ fieldType?: DatasetField['type']
65
+ }>(),
66
+ { datasetFields: () => [], fieldType: 'number' }
67
+ )
68
+
69
+ const emit = defineEmits<{
70
+ 'update:modelValue': [value: boolean]
71
+ save: [rules: ConditionalRule[]]
72
+ }>()
73
+
74
+ const cls = bem('report-rules-dialog')
75
+
76
+ const draftItems = ref<DraftRuleItem[]>([])
77
+
78
+ const { parentRef } = useDnD<DraftRuleItem>({
79
+ values: draftItems,
80
+ dragHandle: '.u-report-rule-row__handle',
81
+ plugins: [animations()]
82
+ })
83
+
84
+ const visible = computed({
85
+ get: () => props.modelValue,
86
+ set: (value: boolean) => emit('update:modelValue', value)
87
+ })
88
+
89
+ watch(
90
+ () => props.modelValue,
91
+ (open) => {
92
+ if (!open) return
93
+ draftItems.value = initDraftFromRules(props.rules, props.fieldType, props.bindingField)
94
+ }
95
+ )
96
+
97
+ function addRule(): void {
98
+ draftItems.value = [
99
+ ...draftItems.value,
100
+ createDraftItem(undefined, props.fieldType, props.bindingField)
101
+ ]
102
+ }
103
+
104
+ function removeRule(index: number): void {
105
+ draftItems.value = draftItems.value.filter((_, i) => i !== index)
106
+ }
107
+
108
+ function moveRule(index: number, delta: -1 | 1): void {
109
+ const target = index + delta
110
+ if (target < 0 || target >= draftItems.value.length) return
111
+ const next = [...draftItems.value]
112
+ const [item] = next.splice(index, 1)
113
+ if (!item) return
114
+ next.splice(target, 0, item)
115
+ draftItems.value = next
116
+ }
117
+
118
+ function confirm(): void {
119
+ emit('save', cloneRulesFromDraft(draftItems.value))
120
+ visible.value = false
121
+ }
122
+
123
+ function onClose(): void {
124
+ visible.value = false
125
+ }
126
+ </script>
@@ -0,0 +1,127 @@
1
+ <template>
2
+ <svg v-if="visible" :class="cls.b" :width="svgWidth" :height="svgHeight" aria-hidden="true">
3
+ <defs>
4
+ <marker
5
+ :id="rowArrowMarkerId"
6
+ markerWidth="8"
7
+ markerHeight="8"
8
+ refX="6"
9
+ refY="4"
10
+ orient="auto"
11
+ markerUnits="strokeWidth"
12
+ >
13
+ <path d="M0,0 L8,4 L0,8 Z" fill="var(--u-color-primary, #2563eb)" />
14
+ </marker>
15
+ <marker
16
+ :id="colArrowMarkerId"
17
+ markerWidth="8"
18
+ markerHeight="8"
19
+ refX="6"
20
+ refY="4"
21
+ orient="auto"
22
+ markerUnits="strokeWidth"
23
+ >
24
+ <path d="M0,0 L8,4 L0,8 Z" fill="var(--u-color-warning, #d97706)" />
25
+ </marker>
26
+ </defs>
27
+ <path
28
+ v-for="(item, index) in arcPaths"
29
+ :key="index"
30
+ :class="[cls.e('arc'), cls.em('arc', item.direction)]"
31
+ :d="item.path"
32
+ :marker-end="`url(#${item.direction === 'row' ? rowArrowMarkerId : colArrowMarkerId})`"
33
+ />
34
+ </svg>
35
+ </template>
36
+
37
+ <script lang="ts" setup>
38
+ import type { CellAddress, SheetGrid } from '@veltra/sheet-core'
39
+ import { bem } from '@veltra/utils'
40
+ import { computed, ref, toRef } from 'vue'
41
+
42
+ import type { ReportBinding } from '../../../report/types'
43
+ import {
44
+ buildTopologyArcPath,
45
+ collectTopologyLinks,
46
+ type TopologyBindingEntry,
47
+ type TopologyLinkDirection
48
+ } from './topology'
49
+ import { readCellOverlayRect, readGridOverlaySize, useGridOverlaySync } from './use-grid-overlay'
50
+
51
+ defineOptions({ name: 'UReportTopologyOverlay' })
52
+
53
+ const props = defineProps<{
54
+ cell: CellAddress | null
55
+ binding: ReportBinding | null
56
+ entries: TopologyBindingEntry[]
57
+ metaTick: number
58
+ hostEl: HTMLElement | null
59
+ getGrid: () => SheetGrid | undefined
60
+ getBindingAt: (addr: CellAddress) => ReportBinding | undefined
61
+ }>()
62
+
63
+ const cls = bem('report-topology')
64
+
65
+ const instanceId = Math.random().toString(36).slice(2, 8)
66
+ const rowArrowMarkerId = `u-report-topology-row-arrow-${instanceId}`
67
+ const colArrowMarkerId = `u-report-topology-col-arrow-${instanceId}`
68
+
69
+ const hostEl = toRef(props, 'hostEl')
70
+
71
+ const svgWidth = ref(0)
72
+ const svgHeight = ref(0)
73
+ const arcPaths = ref<Array<{ path: string; direction: TopologyLinkDirection }>>([])
74
+ const inView = ref(false)
75
+
76
+ const visible = computed(
77
+ () => !!props.binding && !!props.cell && inView.value && arcPaths.value.length > 0
78
+ )
79
+
80
+ function updateOverlay(): void {
81
+ const cell = props.cell
82
+ const binding = props.binding
83
+ const host = props.hostEl
84
+ if (!cell || !binding || !host) {
85
+ inView.value = false
86
+ arcPaths.value = []
87
+ return
88
+ }
89
+
90
+ const size = readGridOverlaySize(host)
91
+ if (!size) {
92
+ inView.value = false
93
+ arcPaths.value = []
94
+ return
95
+ }
96
+
97
+ svgWidth.value = size.width
98
+ svgHeight.value = size.height
99
+
100
+ const links = collectTopologyLinks(cell, binding, props.entries, props.getBindingAt)
101
+ const paths: Array<{ path: string; direction: TopologyLinkDirection }> = []
102
+
103
+ for (const link of links) {
104
+ const fromRect = readCellOverlayRect(link.from, host, props.getGrid)
105
+ const toRect = readCellOverlayRect(link.to, host, props.getGrid)
106
+ if (!fromRect || !toRect) continue
107
+ paths.push({
108
+ direction: link.direction,
109
+ path: buildTopologyArcPath(
110
+ { x: fromRect.centerX, y: fromRect.centerY },
111
+ { x: toRect.centerX, y: toRect.centerY }
112
+ )
113
+ })
114
+ }
115
+
116
+ arcPaths.value = paths
117
+ inView.value = paths.length > 0
118
+ }
119
+
120
+ useGridOverlaySync({
121
+ hostEl,
122
+ getGrid: () => props.getGrid(),
123
+ watchSources: () =>
124
+ [props.cell?.row, props.cell?.col, props.binding, props.metaTick, props.hostEl] as const,
125
+ update: updateOverlay
126
+ })
127
+ </script>
@@ -0,0 +1,124 @@
1
+ import type { CellAddress } from '@veltra/sheet-core'
2
+
3
+ import type { ReportBinding } from '../../../report/types'
4
+
5
+ export type TopologyLinkDirection = 'row' | 'col'
6
+
7
+ export interface TopologyLink {
8
+ /** 子格(依赖方) */
9
+ from: CellAddress
10
+ /** 父绑定格 */
11
+ to: CellAddress
12
+ /** 连线方向:行方向父格 / 列方向父格 */
13
+ direction: TopologyLinkDirection
14
+ }
15
+
16
+ export interface TopologyBindingEntry {
17
+ addr: CellAddress
18
+ binding: ReportBinding
19
+ }
20
+
21
+ function linkKey(from: CellAddress, to: CellAddress, direction: TopologyLinkDirection): string {
22
+ return `${direction}:${from.row},${from.col}->${to.row},${to.col}`
23
+ }
24
+
25
+ function sameAddress(a: CellAddress, b: CellAddress): boolean {
26
+ return a.row === b.row && a.col === b.col
27
+ }
28
+
29
+ function pushLink(
30
+ links: TopologyLink[],
31
+ seen: Set<string>,
32
+ from: CellAddress,
33
+ to: CellAddress,
34
+ direction: TopologyLinkDirection
35
+ ): void {
36
+ const key = linkKey(from, to, direction)
37
+ if (seen.has(key)) return
38
+ links.push({ from, to, direction })
39
+ seen.add(key)
40
+ }
41
+
42
+ /** 查找将指定格作为行方向父格的绑定格 */
43
+ export function findCellsWithRowParent(
44
+ parent: CellAddress,
45
+ entries: readonly TopologyBindingEntry[]
46
+ ): CellAddress[] {
47
+ const children: CellAddress[] = []
48
+ for (const { addr, binding } of entries) {
49
+ if (binding.rowParent && sameAddress(binding.rowParent, parent)) {
50
+ children.push(addr)
51
+ }
52
+ }
53
+ return children
54
+ }
55
+
56
+ /** 查找将指定格作为列方向父格的绑定格 */
57
+ export function findCellsWithColParent(
58
+ parent: CellAddress,
59
+ entries: readonly TopologyBindingEntry[]
60
+ ): CellAddress[] {
61
+ const children: CellAddress[] = []
62
+ for (const { addr, binding } of entries) {
63
+ if (binding.colParent && sameAddress(binding.colParent, parent)) {
64
+ children.push(addr)
65
+ }
66
+ }
67
+ return children
68
+ }
69
+
70
+ /** 收集选中格相关的拓扑连线:沿 rowParent / colParent 链上行、下行到直接子绑定 */
71
+ export function collectTopologyLinks(
72
+ cell: CellAddress,
73
+ binding: ReportBinding,
74
+ entries: readonly TopologyBindingEntry[],
75
+ getBindingAt: (addr: CellAddress) => ReportBinding | undefined
76
+ ): TopologyLink[] {
77
+ const links: TopologyLink[] = []
78
+ const seen = new Set<string>()
79
+
80
+ let rowCursor = cell
81
+ let rowBinding: ReportBinding | undefined = binding
82
+ while (rowBinding?.rowParent) {
83
+ const parent = rowBinding.rowParent
84
+ pushLink(links, seen, rowCursor, parent, 'row')
85
+ rowCursor = parent
86
+ rowBinding = getBindingAt(parent)
87
+ }
88
+
89
+ let colCursor = cell
90
+ let colBinding: ReportBinding | undefined = binding
91
+ while (colBinding?.colParent) {
92
+ const parent = colBinding.colParent
93
+ pushLink(links, seen, colCursor, parent, 'col')
94
+ colCursor = parent
95
+ colBinding = getBindingAt(parent)
96
+ }
97
+
98
+ for (const child of findCellsWithRowParent(cell, entries)) {
99
+ pushLink(links, seen, child, cell, 'row')
100
+ }
101
+
102
+ for (const child of findCellsWithColParent(cell, entries)) {
103
+ pushLink(links, seen, child, cell, 'col')
104
+ }
105
+
106
+ return links
107
+ }
108
+
109
+ /** @deprecated 05 将删除;暂保留别名 */
110
+ export const findCellsWithLeftParent = findCellsWithRowParent
111
+
112
+ /** 子格 → 父格的二次贝塞尔弧线路径 */
113
+ export function buildTopologyArcPath(
114
+ from: { x: number; y: number },
115
+ to: { x: number; y: number }
116
+ ): string {
117
+ const dx = to.x - from.x
118
+ const dy = to.y - from.y
119
+ const distance = Math.hypot(dx, dy) || 1
120
+ const lift = Math.min(48, Math.max(16, distance * 0.35))
121
+ const cx = (from.x + to.x) / 2
122
+ const cy = (from.y + to.y) / 2 - lift * Math.sign(dy || -1)
123
+ return `M ${from.x} ${from.y} Q ${cx} ${cy} ${to.x} ${to.y}`
124
+ }