@veltra/desktop 1.7.18 → 1.8.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 (82) hide show
  1. package/dist/components/batch-edit/batch-edit-form.js +40 -26
  2. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  3. package/dist/components/batch-edit/batch-edit-list.js +17 -5
  4. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  5. package/dist/components/batch-edit/di.js.map +1 -1
  6. package/dist/components/batch-edit/use-handlers.js +3 -3
  7. package/dist/components/batch-edit/use-handlers.js.map +1 -1
  8. package/dist/components/batch-edit/use-shortcut-key.js +4 -2
  9. package/dist/components/batch-edit/use-shortcut-key.js.map +1 -1
  10. package/dist/components/card/style2.css +0 -2
  11. package/dist/components/dialog/style2.css +1 -1
  12. package/dist/components/dropdown/dropdown.js +5 -3
  13. package/dist/components/dropdown/dropdown.js.map +1 -1
  14. package/dist/components/expression-editor/components/path-preview.js +3 -6
  15. package/dist/components/expression-editor/components/path-preview.js.map +1 -1
  16. package/dist/components/expression-editor/components/variable-picker.js +49 -12
  17. package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
  18. package/dist/components/expression-editor/core/chip.js +1 -15
  19. package/dist/components/expression-editor/core/chip.js.map +1 -1
  20. package/dist/components/expression-editor/core/editor.js +20 -18
  21. package/dist/components/expression-editor/core/editor.js.map +1 -1
  22. package/dist/components/expression-editor/expression-editor.js +53 -22
  23. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  24. package/dist/components/expression-editor/style2.css +135 -96
  25. package/dist/components/form/form.js.map +1 -1
  26. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +0 -1
  27. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  28. package/dist/components/form/style2.css +8 -3
  29. package/dist/components/index.d.ts +26 -26
  30. package/dist/components/index.js +52 -52
  31. package/dist/components/multi-select/multi-select.js +16 -12
  32. package/dist/components/multi-select/multi-select.js.map +1 -1
  33. package/dist/components/multi-select/style2.css +7 -0
  34. package/dist/components/multi-tree-select/multi-tree-select.js +82 -56
  35. package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
  36. package/dist/components/multi-tree-select/style2.css +7 -0
  37. package/dist/components/select/select.js +7 -3
  38. package/dist/components/select/select.js.map +1 -1
  39. package/dist/components/select/style2.css +7 -0
  40. package/dist/components/select/use-options.js +14 -3
  41. package/dist/components/select/use-options.js.map +1 -1
  42. package/dist/components/tree-select/style2.css +7 -0
  43. package/dist/components/tree-select/tree-select.js +49 -17
  44. package/dist/components/tree-select/tree-select.js.map +1 -1
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +2 -2
  47. package/dist/types/dropdown.d.ts +10 -0
  48. package/dist/types/multi-tree-select.d.ts +7 -1
  49. package/dist/types/tree-select.d.ts +7 -1
  50. package/package.json +1 -1
  51. package/src/components/batch-edit/__test__/batch-edit.test.ts +119 -7
  52. package/src/components/batch-edit/batch-edit-form.vue +21 -2
  53. package/src/components/batch-edit/batch-edit-list.vue +18 -1
  54. package/src/components/batch-edit/di.ts +1 -1
  55. package/src/components/batch-edit/use-handlers.ts +6 -5
  56. package/src/components/batch-edit/use-shortcut-key.ts +5 -2
  57. package/src/components/card/style.scss +0 -2
  58. package/src/components/dialog/style.scss +2 -1
  59. package/src/components/dropdown/dropdown.vue +9 -4
  60. package/src/components/expression-editor/__test__/chip.test.ts +3 -14
  61. package/src/components/expression-editor/components/path-preview.vue +1 -2
  62. package/src/components/expression-editor/components/variable-picker.vue +54 -13
  63. package/src/components/expression-editor/core/chip.ts +3 -29
  64. package/src/components/expression-editor/core/editor.ts +28 -21
  65. package/src/components/expression-editor/expression-editor.vue +47 -21
  66. package/src/components/expression-editor/style.scss +167 -111
  67. package/src/components/form/form.vue +0 -1
  68. package/src/components/form/style.scss +11 -4
  69. package/src/components/multi-select/multi-select.vue +11 -2
  70. package/src/components/multi-select/style.scss +6 -0
  71. package/src/components/multi-tree-select/multi-tree-select.vue +58 -9
  72. package/src/components/multi-tree-select/style.scss +6 -0
  73. package/src/components/select/__test__/select.test.ts +78 -0
  74. package/src/components/select/select.vue +13 -3
  75. package/src/components/select/style.scss +6 -0
  76. package/src/components/select/use-options.ts +25 -4
  77. package/src/components/tree-select/__test__/tree-select.test.ts +116 -0
  78. package/src/components/tree-select/style.scss +6 -0
  79. package/src/components/tree-select/tree-select.vue +63 -13
  80. package/src/types/dropdown.ts +10 -0
  81. package/src/types/multi-tree-select.ts +12 -1
  82. package/src/types/tree-select.ts +12 -1
@@ -5,48 +5,71 @@
5
5
  $root-name: expression-editor;
6
6
 
7
7
  @include m.b($root-name) {
8
+ // 根节点只做布局(提示行在视觉盒之外),视觉盒由 __shell 承担
8
9
  position: relative;
9
- background-color: fn.use-var(bg-color, top);
10
- border-width: fn.use-var(border, width);
11
- border-style: fn.use-var(border, style);
12
- border-color: fn.use-var(border, muted-color);
13
- border-radius: fn.use-var(radius, default);
14
10
  width: 100%;
15
11
 
16
- @include m.is-not(disabled, readonly) {
17
- &:hover {
18
- border-color: fn.use-var(color, primary);
19
- }
12
+ @include m.e(shell) {
13
+ background-color: fn.use-var(bg-color, top);
14
+ border-width: fn.use-var(border, width);
15
+ border-style: fn.use-var(border, style);
16
+ border-color: fn.use-var(border, muted-color);
17
+ border-radius: fn.use-var(radius, default);
18
+ transition:
19
+ border-color fn.use-var(transition, fast) fn.use-var(transition, ease),
20
+ box-shadow fn.use-var(transition, fast) fn.use-var(transition, ease);
21
+
22
+ @include m.is-not(disabled, readonly) {
23
+ &:hover {
24
+ border-color: fn.use-var(color, primary);
25
+ }
20
26
 
21
- &:focus-within {
22
- border-color: fn.use-var(color, primary);
23
- outline: none;
27
+ &:focus-within {
28
+ border-color: fn.use-var(color, primary);
29
+ box-shadow: fn.use-var(focus-ring);
30
+ outline: none;
31
+ }
24
32
  }
25
- }
26
33
 
27
- @include m.is('disabled') {
28
- background-color: fn.use-var(color, disabled);
29
- cursor: not-allowed;
30
- border-color: fn.use-var(border, muted-color);
34
+ @include m.is('disabled') {
35
+ background-color: fn.use-var(color, disabled);
36
+ cursor: not-allowed;
37
+ border-color: fn.use-var(border, muted-color);
31
38
 
32
- &:hover {
39
+ &:hover {
40
+ border-color: fn.use-var(border, muted-color);
41
+ }
42
+ }
43
+
44
+ @include m.is('readonly') {
45
+ background-color: fn.use-var(bg-color, middle);
46
+ cursor: default;
33
47
  border-color: fn.use-var(border, muted-color);
48
+ user-select: text;
49
+
50
+ &:hover {
51
+ border-color: fn.use-var(border, muted-color);
52
+ }
34
53
  }
35
- }
36
54
 
37
- @include m.is('readonly') {
38
- background-color: fn.use-var(bg-color, middle);
39
- cursor: default;
40
- border-color: fn.use-var(border, muted-color);
41
- user-select: text;
55
+ @include m.size using ($size) {
56
+ min-height: fn.use-var(form-component-height, $size);
57
+ padding: fn.use-var(gap, $size);
42
58
 
43
- &:hover {
44
- border-color: fn.use-var(border, muted-color);
59
+ @include m.bem($root-name, placeholder) {
60
+ font-size: fn.use-var(font-size-main, $size);
61
+ }
62
+
63
+ @include m.bem($root-name, container) {
64
+ font-size: fn.use-var(font-size-main, $size);
65
+ }
45
66
  }
46
67
  }
47
68
 
48
69
  @include m.e(placeholder) {
49
70
  position: absolute;
71
+ top: 0;
72
+ left: 0;
50
73
  color: fn.use-var(text-color, placeholder);
51
74
  pointer-events: none;
52
75
  user-select: none;
@@ -54,6 +77,43 @@ $root-name: expression-editor;
54
77
  font-weight: 400;
55
78
  }
56
79
 
80
+ // 提示 + 输入区的定位层:placeholder 相对它对齐,hint 出现时不至于错位
81
+ @include m.e(body) {
82
+ position: relative;
83
+ }
84
+
85
+ // 编辑器上方的 `@` 使用提示(空内容时显示),位于边框盒之外
86
+ @include m.e(hint) {
87
+ font-size: fn.use-var(font-size-main, small);
88
+ color: fn.use-var(text-color, placeholder);
89
+ user-select: none;
90
+ margin-bottom: 4px;
91
+ }
92
+
93
+ // mention 面板定位锚点:跟随 `@` 字符,零尺寸不参与绘制
94
+ @include m.e(mention-anchor) {
95
+ position: absolute;
96
+ width: 0;
97
+ height: 0;
98
+ pointer-events: none;
99
+ }
100
+
101
+ // 键位提示(编辑器 hint 与面板 footer 共用)
102
+ @include m.e(hint, picker-hint) {
103
+ kbd {
104
+ display: inline-block;
105
+ min-width: 18px;
106
+ padding: 1px 4px;
107
+ border: fn.use-var(border);
108
+ border-radius: fn.use-var(radius, small);
109
+ font-size: 11px;
110
+ line-height: 1.4;
111
+ text-align: center;
112
+ color: fn.use-var(text-color, second);
113
+ background-color: fn.use-var(bg-color, top);
114
+ }
115
+ }
116
+
57
117
  @include m.e(container) {
58
118
  position: relative;
59
119
  outline: none;
@@ -105,12 +165,6 @@ $root-name: expression-editor;
105
165
  fn.use-var(color, primary, light-7)
106
166
  );
107
167
  box-shadow: 0 1px 4px fn.use-var(shadow, color);
108
-
109
- #{fn.bem($root-name, chip-close)} {
110
- opacity: 1;
111
- max-width: 14px;
112
- margin-left: 2px;
113
- }
114
168
  }
115
169
 
116
170
  &.is-focused {
@@ -122,94 +176,93 @@ $root-name: expression-editor;
122
176
  pointer-events: none;
123
177
  }
124
178
 
125
- @include m.e(chip-close) {
126
- display: inline-flex;
127
- align-items: center;
128
- justify-content: center;
129
- width: 14px;
130
- max-width: 0;
131
- margin-left: 0;
132
- overflow: hidden;
133
- font-size: 14px;
134
- line-height: 1;
135
- color: fn.use-var(color, primary);
136
- opacity: 0;
137
- transition:
138
- opacity 0.12s ease,
139
- max-width 0.12s ease,
140
- margin-left 0.12s ease;
141
- cursor: pointer;
142
-
143
- &:hover {
144
- transform: scale(1.1);
145
- }
146
- }
147
-
148
179
  // ─── 变量选择面板 ──────────────────────────────────────────
180
+ // 面板以 tip 为浮层载体,但按库内标准下拉面板形态渲染:
181
+ // 负 margin 抵消 tip 内容自带的 gap 内边距,由 picker 自绘边框卡片
149
182
  @include m.e(picker) {
150
- min-width: 220px;
183
+ margin: calc(#{fn.use-var(gap, default)} * -1);
184
+ min-width: 240px;
185
+ display: flex;
186
+ flex-direction: column;
187
+ overflow: hidden;
151
188
  background-color: fn.use-var(bg-color, top);
189
+ border: fn.use-var(border);
190
+ border-radius: fn.use-var(radius, default);
191
+ backdrop-filter: fn.use-var(bg-filter);
192
+ -webkit-backdrop-filter: fn.use-var(bg-filter);
152
193
  }
153
194
 
154
195
  @include m.e(picker-body) {
196
+ flex: 1 1 auto;
197
+ min-height: 0;
155
198
  display: flex;
156
199
  align-items: stretch;
157
- min-height: 200px;
158
200
  }
159
201
 
160
202
  @include m.e(panel) {
161
203
  flex: 1 1 auto;
162
204
  min-width: 0;
163
205
  max-height: 320px;
206
+ display: flex;
207
+ flex-direction: column;
164
208
  }
165
209
 
166
210
  @include m.e(panel-list) {
167
- height: 100%;
168
- min-height: 200px;
211
+ flex: 1 1 auto;
212
+ min-height: 0;
169
213
  }
170
214
 
171
215
  @include m.e(panel-content) {
172
- padding: 4px 0;
216
+ padding: 4px;
173
217
  }
174
218
 
175
219
  @include m.e(item) {
176
- padding: 6px 12px;
177
- cursor: pointer;
178
- user-select: none;
179
220
  display: flex;
180
221
  align-items: center;
181
222
  justify-content: space-between;
182
- gap: 8px;
183
- transition:
184
- background-color 0.12s ease,
185
- color 0.12s ease;
186
- color: fn.use-var(text-color, main);
187
- font-size: 13px;
188
- line-height: 1.4;
223
+ gap: fn.use-var(gap, default);
224
+ height: fn.use-var(form-component-height, default);
225
+ padding: 0 fn.use-var(gap, default);
189
226
  border-radius: fn.use-var(radius, small);
190
- margin: 1px 4px;
227
+ font-size: fn.use-var(font-size-main, default);
228
+ color: fn.use-var(text-color, main);
229
+ cursor: pointer;
230
+ user-select: none;
231
+ transition:
232
+ background-color 0.15s ease,
233
+ color 0.15s ease;
191
234
 
192
235
  @include m.is(active) {
193
- background-color: fn.use-var(color, primary);
194
- color: fn.use-var(text-color, white);
195
-
196
- @include m.bem($root-name, item-expand) {
197
- color: fn.use-var(text-color, white);
198
- }
236
+ background-color: fn.use-var(bg-color, hover);
237
+ color: fn.use-var(text-color, title);
199
238
  }
200
239
  }
201
240
 
202
241
  @include m.e(item-label) {
203
242
  flex: 1;
204
- overflow: hidden;
205
- text-overflow: ellipsis;
206
- white-space: nowrap;
243
+ min-width: 0;
244
+ @include m.ellipsis;
207
245
  }
208
246
 
209
247
  @include m.e(item-expand) {
210
248
  flex-shrink: 0;
211
249
  color: fn.use-var(text-color, placeholder);
212
250
  font-size: 12px;
251
+ // 扩大点击热区:u-icon 是固定 1em 盒(全局 border-box),padding 会把 svg 挤成 0 宽,
252
+ // 这里改回 content-box 让字形保持原尺寸,再用负 margin 抵消布局占位
253
+ box-sizing: content-box;
254
+ padding: 6px;
255
+ margin: -6px;
256
+ border-radius: fn.use-var(radius, small);
257
+ cursor: pointer;
258
+ transition:
259
+ color 0.15s ease,
260
+ background-color 0.15s ease;
261
+
262
+ &:hover {
263
+ color: fn.use-var(color, primary);
264
+ background-color: fn.use-var(bg-color, hover);
265
+ }
213
266
  }
214
267
 
215
268
  @include m.e(empty) {
@@ -218,30 +271,51 @@ $root-name: expression-editor;
218
271
  text-align: center;
219
272
  }
220
273
 
274
+ // 面板底部键盘操作提示
275
+ @include m.e(picker-hint) {
276
+ display: flex;
277
+ align-items: center;
278
+ flex-wrap: wrap;
279
+ gap: 4px 12px;
280
+ padding: 6px 10px;
281
+ border-top: fn.use-var(border);
282
+ font-size: fn.use-var(font-size-main, small);
283
+ color: fn.use-var(text-color, placeholder);
284
+ user-select: none;
285
+
286
+ span {
287
+ display: inline-flex;
288
+ align-items: center;
289
+ gap: 3px;
290
+ }
291
+ }
292
+
221
293
  // ─── 面包屑 ───────────────────────────────────────────────
222
294
  @include m.e(breadcrumbs) {
223
295
  display: flex;
224
296
  align-items: center;
225
297
  flex-wrap: wrap;
226
- padding: 6px 10px;
298
+ gap: 2px;
299
+ padding: 6px 8px;
300
+ background-color: fn.use-var(bg-color, middle);
227
301
  border-bottom: fn.use-var(border);
228
- font-size: 12px;
302
+ font-size: fn.use-var(font-size-main, small);
229
303
  color: fn.use-var(text-color, second);
230
304
  }
231
305
 
232
306
  @include m.e(breadcrumb-item) {
233
307
  display: inline-flex;
234
308
  align-items: center;
235
- gap: 4px;
309
+ gap: 2px;
236
310
  }
237
311
 
238
312
  @include m.e(breadcrumb-link) {
313
+ padding: 3px 6px;
314
+ border-radius: fn.use-var(radius, small);
239
315
  cursor: pointer;
240
316
  transition:
241
- color 0.12s ease,
242
- background-color 0.12s ease;
243
- padding: 2px 6px;
244
- border-radius: fn.use-var(radius, small);
317
+ color 0.15s ease,
318
+ background-color 0.15s ease;
245
319
 
246
320
  &:hover {
247
321
  color: fn.use-var(color, primary);
@@ -255,7 +329,7 @@ $root-name: expression-editor;
255
329
  }
256
330
 
257
331
  @include m.e(breadcrumb-separator) {
258
- font-size: 10px;
332
+ font-size: 12px;
259
333
  color: fn.use-var(text-color, placeholder);
260
334
  }
261
335
 
@@ -265,7 +339,7 @@ $root-name: expression-editor;
265
339
  border-left: fn.use-var(border);
266
340
  padding: 8px 12px;
267
341
  background-color: fn.use-var(bg-color, middle);
268
- font-size: 12px;
342
+ font-size: fn.use-var(font-size-main, small);
269
343
  }
270
344
 
271
345
  @include m.e(path-preview-title) {
@@ -286,6 +360,8 @@ $root-name: expression-editor;
286
360
  gap: 4px;
287
361
  line-height: 1.6;
288
362
  color: fn.use-var(text-color, main);
363
+ // 层级缩进由模板传入的 --depth 驱动
364
+ padding-left: calc(var(--depth, 0) * 12px);
289
365
 
290
366
  @include m.is(active) {
291
367
  color: fn.use-var(color, primary);
@@ -293,28 +369,8 @@ $root-name: expression-editor;
293
369
  }
294
370
  }
295
371
 
296
- @include m.e(path-tree-branch) {
297
- color: fn.use-var(text-color, placeholder);
298
- }
299
-
300
372
  @include m.e(path-tree-label) {
301
- overflow: hidden;
302
- text-overflow: ellipsis;
303
- white-space: nowrap;
304
- }
305
-
306
- @include m.size using ($size) {
307
- min-height: fn.use-var(form-component-height, $size);
308
- padding: fn.use-var(gap, $size);
309
-
310
- @include m.bem($root-name, placeholder) {
311
- top: fn.use-var(gap, $size);
312
- left: fn.use-var(gap, $size);
313
- font-size: fn.use-var(font-size-main, $size);
314
- }
315
-
316
- @include m.bem($root-name, container) {
317
- font-size: fn.use-var(font-size-main, $size);
318
- }
373
+ min-width: 0;
374
+ @include m.ellipsis;
319
375
  }
320
376
  }
@@ -3,7 +3,6 @@
3
3
  tag="form"
4
4
  ref="grid"
5
5
  @submit.prevent
6
- gap="0 12px"
7
6
  :cols="cols || breakpointCols"
8
7
  :class="[cls.b, bem.is('readonly', readonly)]"
9
8
  >
@@ -3,15 +3,22 @@
3
3
  @use 'pkg:@veltra/styles/vars';
4
4
 
5
5
  @include m.b(form) {
6
+ // gap 走 CSS 而非 u-grid 内联样式,只读态才能覆盖行距
7
+ gap: 0 12px;
8
+
9
+ // 只读态:label 退为次级文本,值收进浅色圆角条;行距拉开让各项独立成块,避免粘连
6
10
  @include m.is(readonly) {
7
- gap: 6px;
11
+ gap: 10px 12px;
8
12
 
9
13
  @include m.bem(form-item, label) {
10
- background-color: fn.use-var(bg-color, top); // #fbfcfe;
14
+ color: fn.use-var(text-color, second);
11
15
  }
16
+
12
17
  @include m.bem(form-item, content) {
13
- background-color: fn.use-var(bg-color, middle); // #f5f8fb;
14
- padding: 0 8px;
18
+ padding: 4px 12px;
19
+ color: fn.use-var(text-color, main);
20
+ background-color: fn.use-var(bg-color, middle);
21
+ border-radius: fn.use-var(radius, default);
15
22
  }
16
23
  }
17
24
 
@@ -80,11 +80,14 @@
80
80
  虚拟化启用时内容容器 height 由 useVirtualizer 命令式写入;
81
81
  模板不再绑定 totalSize,避免尺寸变化引起重渲染。
82
82
  -->
83
+ <div v-if="loading" :class="cls.e('loading')">
84
+ <ULoading type="dual-ring" />
85
+ </div>
83
86
  <u-scroll
84
87
  tag="ul"
85
88
  :class="[cls.e('options')]"
86
89
  ref="scrollRef"
87
- v-if="options.length"
90
+ v-else-if="options.length"
88
91
  :content-class="[cls.e('options-wrap'), bem.is('virtual', virtualEnabled)]"
89
92
  >
90
93
  <template v-if="virtualEnabled">
@@ -166,6 +169,7 @@ import { UCheckbox } from '../checkbox'
166
169
  import { UDropdown } from '../dropdown'
167
170
  import { UEmpty } from '../empty'
168
171
  import { UIcon } from '../icon'
172
+ import { ULoading } from '../loading'
169
173
  import { UScroll } from '../scroll'
170
174
  import { useOptions } from '../select/use-options'
171
175
  import { UTag } from '../tag'
@@ -204,7 +208,12 @@ const scrollRef = shallowRef<ScrollExposed>()
204
208
 
205
209
  const hovered = shallowRef(false)
206
210
 
207
- const { options: rawOptions, queryString, allOptions: rawAllOptions } = useOptions({ props })
211
+ const {
212
+ options: rawOptions,
213
+ queryString,
214
+ loading,
215
+ allOptions: rawAllOptions
216
+ } = useOptions({ props })
208
217
 
209
218
  const createdOptions = shallowRef<Record<string, any>[]>([])
210
219
 
@@ -170,4 +170,10 @@ $root-name: multi-select;
170
170
  text-align: center;
171
171
  padding: fn.use-var(gap, default) 0;
172
172
  }
173
+
174
+ @include m.e(loading) {
175
+ // 与选项内容区同高,加载期间保持面板形态,避免塌缩成贴着输入框的小条
176
+ @include m.flex($justify: center, $align: center);
177
+ min-height: 240px;
178
+ }
173
179
  }
@@ -71,15 +71,19 @@
71
71
  {{ allExpanded ? '收起全部' : '展开全部' }}
72
72
  </u-button>
73
73
  </div>
74
+ <div v-if="remoteLoading" :class="cls.e('loading')">
75
+ <ULoading type="dual-ring" />
76
+ </div>
74
77
  <!-- 菜单列表 -->
75
78
  <u-tree
79
+ v-else
76
80
  v-bind="treeProps"
77
81
  v-model:checked="model"
78
82
  @update:checked="handleCheck"
79
83
  ref="treeRef"
80
84
  :class="cls.e('content-tree')"
81
85
  checkable
82
- :data="data"
86
+ :data="treeData"
83
87
  :slots="slots"
84
88
  scroll-to-view
85
89
  ></u-tree>
@@ -100,7 +104,7 @@
100
104
  </template>
101
105
 
102
106
  <script lang="ts" setup>
103
- import { dfs, o } from '@cat-kit/core'
107
+ import { debounce, dfs, o } from '@cat-kit/core'
104
108
  import { useFormFallbackProps } from '@veltra/compositions'
105
109
  import { ArrowDown, Close } from '@veltra/icons/normal'
106
110
  import { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'
@@ -113,6 +117,7 @@ import { UButton } from '../button'
113
117
  import { UCheckbox } from '../checkbox'
114
118
  import { UDropdown } from '../dropdown'
115
119
  import { UIcon } from '../icon'
120
+ import { ULoading } from '../loading'
116
121
  import { UTag } from '../tag'
117
122
  import { UTree } from '../tree'
118
123
  import type { TreeSlotsScope } from '../tree/di'
@@ -148,6 +153,7 @@ const treeProps = computed(() => {
148
153
  'label',
149
154
  'readonly',
150
155
  'modelValue',
156
+ 'data',
151
157
  'contentClass',
152
158
  'contentStyle',
153
159
  'minWidth',
@@ -159,11 +165,56 @@ const emit = defineEmits<MultiTreeSelectEmits>()
159
165
 
160
166
  const slots = defineSlots<{ default?: (props: TreeSlotsScope) => any }>()
161
167
 
162
- /**过滤 */
168
+ /** 是否可搜索:显式声明或数据源为远程函数 */
169
+ const filterable = computed(() => props.filterable || typeof props.data === 'function')
170
+
171
+ /** 过滤 */
163
172
  const qs = shallowRef('')
164
- watch(qs, (qs) => {
165
- treeRef.value?.filter(qs)
166
- })
173
+
174
+ const treeRef = shallowRef<TreeExposed>()
175
+
176
+ /** 远程搜索返回的树数据 */
177
+ const remoteData = shallowRef<Record<string, any>[]>()
178
+
179
+ /** 面板渲染的树数据:data 为函数时来自远程搜索,否则为静态数据 */
180
+ const treeData = computed(() => (typeof props.data === 'function' ? remoteData.value : props.data))
181
+
182
+ const remoteLoading = shallowRef(false)
183
+ /** 远程请求序号;响应返回时序号不一致说明已有更新的请求,结果作废 */
184
+ let remoteSeq = 0
185
+
186
+ const debouncedRemoteSearch = debounce(
187
+ async (
188
+ query: string,
189
+ dataFn: (qs: string) => Promise<Record<string, any>[]> | Record<string, any>[]
190
+ ) => {
191
+ const seq = ++remoteSeq
192
+ remoteLoading.value = true
193
+ try {
194
+ const data = await dataFn(query)
195
+ // 响应到达时查询词可能已变化或已有更新的请求,过期结果直接丢弃
196
+ if (seq !== remoteSeq || query !== qs.value) return
197
+ remoteData.value = data ?? []
198
+ // 搜索结果展示完整命中路径,与本地过滤自动展开祖先的行为对齐
199
+ if (query) nextTick(() => treeRef.value?.expandAll())
200
+ } finally {
201
+ if (seq === remoteSeq) remoteLoading.value = false
202
+ }
203
+ },
204
+ 200
205
+ )
206
+
207
+ watch(
208
+ [qs, () => props.data],
209
+ ([query, data]) => {
210
+ if (typeof data === 'function') {
211
+ debouncedRemoteSearch(query, data)
212
+ return
213
+ }
214
+ treeRef.value?.filter(query)
215
+ },
216
+ { immediate: true }
217
+ )
167
218
 
168
219
  const model = defineModel<(string | number)[]>({ default: () => [] })
169
220
 
@@ -179,8 +230,6 @@ const showClear = computed(() => {
179
230
  return props.clearable && model.value?.length && hovered.value && !disabled.value
180
231
  })
181
232
 
182
- const treeRef = shallowRef<TreeExposed>()
183
-
184
233
  const dropdownRef = shallowRef<DropdownExposed>()
185
234
 
186
235
  let changedByEvent = false
@@ -237,7 +286,7 @@ const handleClear = () => {
237
286
  const keyDicts = shallowRef(new Map<string | number, Record<string, any>>())
238
287
 
239
288
  watch(
240
- () => props.data,
289
+ treeData,
241
290
  (data) => {
242
291
  if (!data?.length) {
243
292
  keyDicts.value = new Map()
@@ -126,4 +126,10 @@ $root-name: multi-tree-select;
126
126
  @include m.e(content-tree) {
127
127
  max-height: 240px;
128
128
  }
129
+
130
+ @include m.e(loading) {
131
+ // 与树内容区同高,加载期间保持面板形态,避免塌缩成贴着输入框的小条
132
+ @include m.flex($justify: center, $align: center);
133
+ min-height: 240px;
134
+ }
129
135
  }