@veltra/desktop 1.7.19 → 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 (71) 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/use-handlers.js +3 -3
  4. package/dist/components/batch-edit/use-handlers.js.map +1 -1
  5. package/dist/components/batch-edit/use-shortcut-key.js +4 -2
  6. package/dist/components/batch-edit/use-shortcut-key.js.map +1 -1
  7. package/dist/components/card/style2.css +0 -2
  8. package/dist/components/dropdown/dropdown.js +5 -3
  9. package/dist/components/dropdown/dropdown.js.map +1 -1
  10. package/dist/components/expression-editor/components/path-preview.js +3 -6
  11. package/dist/components/expression-editor/components/path-preview.js.map +1 -1
  12. package/dist/components/expression-editor/components/variable-picker.js +49 -12
  13. package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
  14. package/dist/components/expression-editor/core/editor.js +20 -0
  15. package/dist/components/expression-editor/core/editor.js.map +1 -1
  16. package/dist/components/expression-editor/expression-editor.js +53 -18
  17. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  18. package/dist/components/expression-editor/style2.css +135 -73
  19. package/dist/components/form/form.js.map +1 -1
  20. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +0 -1
  21. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  22. package/dist/components/form/style2.css +8 -3
  23. package/dist/components/index.d.ts +26 -26
  24. package/dist/components/index.js +52 -52
  25. package/dist/components/multi-select/multi-select.js +16 -12
  26. package/dist/components/multi-select/multi-select.js.map +1 -1
  27. package/dist/components/multi-select/style2.css +7 -0
  28. package/dist/components/multi-tree-select/multi-tree-select.js +82 -56
  29. package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
  30. package/dist/components/multi-tree-select/style2.css +7 -0
  31. package/dist/components/select/select.js +7 -3
  32. package/dist/components/select/select.js.map +1 -1
  33. package/dist/components/select/style2.css +7 -0
  34. package/dist/components/select/use-options.js +14 -3
  35. package/dist/components/select/use-options.js.map +1 -1
  36. package/dist/components/tree-select/style2.css +7 -0
  37. package/dist/components/tree-select/tree-select.js +49 -17
  38. package/dist/components/tree-select/tree-select.js.map +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +2 -2
  41. package/dist/types/dropdown.d.ts +10 -0
  42. package/dist/types/multi-tree-select.d.ts +7 -1
  43. package/dist/types/tree-select.d.ts +7 -1
  44. package/package.json +1 -1
  45. package/src/components/batch-edit/__test__/batch-edit.test.ts +112 -1
  46. package/src/components/batch-edit/batch-edit-form.vue +21 -2
  47. package/src/components/batch-edit/use-handlers.ts +6 -5
  48. package/src/components/batch-edit/use-shortcut-key.ts +5 -2
  49. package/src/components/card/style.scss +0 -2
  50. package/src/components/dropdown/dropdown.vue +9 -4
  51. package/src/components/expression-editor/components/path-preview.vue +1 -2
  52. package/src/components/expression-editor/components/variable-picker.vue +54 -13
  53. package/src/components/expression-editor/core/editor.ts +28 -0
  54. package/src/components/expression-editor/expression-editor.vue +47 -17
  55. package/src/components/expression-editor/style.scss +167 -82
  56. package/src/components/form/form.vue +0 -1
  57. package/src/components/form/style.scss +11 -4
  58. package/src/components/multi-select/multi-select.vue +11 -2
  59. package/src/components/multi-select/style.scss +6 -0
  60. package/src/components/multi-tree-select/multi-tree-select.vue +58 -9
  61. package/src/components/multi-tree-select/style.scss +6 -0
  62. package/src/components/select/__test__/select.test.ts +78 -0
  63. package/src/components/select/select.vue +13 -3
  64. package/src/components/select/style.scss +6 -0
  65. package/src/components/select/use-options.ts +25 -4
  66. package/src/components/tree-select/__test__/tree-select.test.ts +116 -0
  67. package/src/components/tree-select/style.scss +6 -0
  68. package/src/components/tree-select/tree-select.vue +63 -13
  69. package/src/types/dropdown.ts +10 -0
  70. package/src/types/multi-tree-select.ts +12 -1
  71. package/src/types/tree-select.ts +12 -1
@@ -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
  }
@@ -218,4 +218,82 @@ describe('USelect', () => {
218
218
  unmount()
219
219
  }
220
220
  })
221
+
222
+ it('loads options from a remote function with loading state', async () => {
223
+ let resolveRemote!: (options: Record<string, any>[]) => void
224
+ const calls: string[] = []
225
+ const { host, unmount } = mountSelect({
226
+ options: (qs: string) => {
227
+ calls.push(qs)
228
+ return new Promise((resolve) => {
229
+ resolveRemote = resolve
230
+ })
231
+ }
232
+ })
233
+
234
+ try {
235
+ // 挂载即以空串调用一次
236
+ expect(calls).toEqual([''])
237
+
238
+ await openDropdown(host)
239
+ expect(document.body.querySelector('.u-select__loading')).toBeTruthy()
240
+ expect(queryOptions()).toHaveLength(0)
241
+
242
+ resolveRemote([{ label: '远程A', value: 'a' }])
243
+ await sleep(50)
244
+ expect(document.body.querySelector('.u-select__loading')).toBeFalsy()
245
+ expect(queryOptions().map((el) => el.textContent)).toEqual(['远程A'])
246
+ } finally {
247
+ unmount()
248
+ }
249
+ })
250
+
251
+ it('queries the remote function on input', async () => {
252
+ const calls: string[] = []
253
+ const { host, unmount } = mountSelect({
254
+ options: (qs: string) => {
255
+ calls.push(qs)
256
+ return Promise.resolve(
257
+ qs ? [{ label: '上海', value: 'shanghai' }] : [{ label: '北京', value: 'beijing' }]
258
+ )
259
+ }
260
+ })
261
+
262
+ try {
263
+ await openDropdown(host)
264
+ await typeQuery(host, '上海')
265
+ expect(calls).toEqual(['', '上海'])
266
+ expect(queryOptions().map((el) => el.textContent)).toEqual(['上海'])
267
+ } finally {
268
+ unmount()
269
+ }
270
+ })
271
+
272
+ it('discards stale remote responses when a newer query resolves first', async () => {
273
+ const pending: ((options: Record<string, any>[]) => void)[] = []
274
+ const { host, unmount } = mountSelect({
275
+ options: (qs: string) =>
276
+ new Promise((resolve) => {
277
+ pending.push(resolve)
278
+ })
279
+ })
280
+
281
+ try {
282
+ await openDropdown(host)
283
+ await typeQuery(host, '北')
284
+ expect(pending).toHaveLength(2)
285
+
286
+ // 新查询先返回
287
+ pending[1]!([{ label: '北京', value: 'beijing' }])
288
+ await sleep(20)
289
+ expect(queryOptions().map((el) => el.textContent)).toEqual(['北京'])
290
+
291
+ // 慢的旧响应(初始空串查询)后返回,不能覆盖新结果
292
+ pending[0]!([{ label: '旧数据', value: 'old' }])
293
+ await sleep(20)
294
+ expect(queryOptions().map((el) => el.textContent)).toEqual(['北京'])
295
+ } finally {
296
+ unmount()
297
+ }
298
+ })
221
299
  })
@@ -58,8 +58,11 @@
58
58
  单选列表。虚拟化启用时,内容容器 height 由 useVirtualizer 命令式写入;
59
59
  此处 content-style 仅承担 grid 相关样式,避免 height 变化触发模板重渲染。
60
60
  -->
61
+ <div v-if="loading" :class="cls.e('loading')">
62
+ <ULoading type="dual-ring" />
63
+ </div>
61
64
  <u-scroll
62
- v-if="options.length"
65
+ v-else-if="options.length"
63
66
  tag="ul"
64
67
  :class="cls.e('options')"
65
68
  ref="scrollRef"
@@ -137,6 +140,7 @@ import { UDropdown } from '../dropdown'
137
140
  import { UEmpty } from '../empty'
138
141
  import { UIcon } from '../icon'
139
142
  import { UInput } from '../input'
143
+ import { ULoading } from '../loading'
140
144
  import { UScroll } from '../scroll'
141
145
  import { useKeyboard } from './use-keyboard'
142
146
  import { useOptions } from './use-options'
@@ -194,8 +198,14 @@ const filterable = computed(() => {
194
198
  return props.filterable || typeof props.options === 'function'
195
199
  })
196
200
 
197
- const { queryString, options, allOptions, temOptionsToCreatedOptions, clearCreatedOptions } =
198
- useOptions({ props })
201
+ const {
202
+ queryString,
203
+ loading,
204
+ options,
205
+ allOptions,
206
+ temOptionsToCreatedOptions,
207
+ clearCreatedOptions
208
+ } = useOptions({ props })
199
209
 
200
210
  const dropdownVisible = shallowRef(false)
201
211
 
@@ -116,4 +116,10 @@ $root-name: select;
116
116
  text-align: center;
117
117
  padding: fn.use-var(gap, small) 0;
118
118
  }
119
+
120
+ @include m.e(loading) {
121
+ // 与选项内容区同高,加载期间保持面板形态,避免塌缩成贴着输入框的小条
122
+ @include m.flex($justify: center, $align: center);
123
+ min-height: 240px;
124
+ }
119
125
  }
@@ -11,6 +11,8 @@ interface Options {
11
11
  interface UseOptionsReturned {
12
12
  /** 查询字符串 */
13
13
  queryString: ShallowRef<string>
14
+ /** 远程搜索加载中 */
15
+ loading: ShallowRef<boolean>
14
16
  /** 选项 */
15
17
  options: ComputedRef<Record<string, any>[]>
16
18
  /** 所有选项 */
@@ -32,6 +34,10 @@ export function useOptions(o: Options): UseOptionsReturned {
32
34
  /** 筛选 */
33
35
  const queryString = shallowRef('')
34
36
 
37
+ const loading = shallowRef(false)
38
+ /** 远程请求序号;响应返回时序号不一致说明已有更新的请求,结果作废 */
39
+ let remoteSeq = 0
40
+
35
41
  const remoteOptions = shallowRef<Record<string, any>[]>([])
36
42
  const filteredOptions = shallowRef<Record<string, any>[]>([])
37
43
  // 临时选项
@@ -139,9 +145,17 @@ export function useOptions(o: Options): UseOptionsReturned {
139
145
  )
140
146
 
141
147
  const debouncedRemoteFilter = debounce(async (qs: string, propsOptions: Function) => {
142
- const options = await propsOptions(queryString.value)
143
- remoteOptions.value = options
144
- setTempOption(queryString.value, options)
148
+ const seq = ++remoteSeq
149
+ loading.value = true
150
+ try {
151
+ const options = await propsOptions(qs)
152
+ // 响应到达时查询词可能已变化或已有更新的请求,过期结果直接丢弃
153
+ if (seq !== remoteSeq || qs !== queryString.value) return
154
+ remoteOptions.value = options ?? []
155
+ setTempOption(qs, remoteOptions.value)
156
+ } finally {
157
+ if (seq === remoteSeq) loading.value = false
158
+ }
145
159
  }, 200)
146
160
 
147
161
  watch(
@@ -162,5 +176,12 @@ export function useOptions(o: Options): UseOptionsReturned {
162
176
  { immediate: true }
163
177
  )
164
178
 
165
- return { queryString, options, allOptions, temOptionsToCreatedOptions, clearCreatedOptions }
179
+ return {
180
+ queryString,
181
+ loading,
182
+ options,
183
+ allOptions,
184
+ temOptionsToCreatedOptions,
185
+ clearCreatedOptions
186
+ }
166
187
  }
@@ -65,6 +65,38 @@ async function openDropdown(host: HTMLElement) {
65
65
  if (queryNodeContents().length > 0) return
66
66
  }
67
67
  }
68
+
69
+ /** 打开面板并等待进入加载态(远程数据未返回前树不渲染) */
70
+ async function openPanelWithLoading(host: HTMLElement) {
71
+ const input = host.querySelector('input')!
72
+ input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
73
+ for (let attempt = 0; attempt < 20; attempt++) {
74
+ await nextTick()
75
+ await sleep(25)
76
+ if (document.body.querySelector('.u-tree-select__loading') || queryNodeContents().length > 0) {
77
+ return
78
+ }
79
+ }
80
+ }
81
+
82
+ async function typeQuery(host: HTMLElement, text: string) {
83
+ const input = host.querySelector('input')!
84
+ input.focus()
85
+ // 清空查询串(查询态下 model 绑定查询串)
86
+ input.value = ''
87
+ input.dispatchEvent(
88
+ new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteContentBackward' })
89
+ )
90
+ await nextTick()
91
+
92
+ input.value = text
93
+ input.dispatchEvent(
94
+ new InputEvent('input', { bubbles: true, data: text, inputType: 'insertText' })
95
+ )
96
+ await nextTick()
97
+ // 远程搜索防抖 200ms
98
+ await sleep(250)
99
+ }
68
100
  function queryNodeContents() {
69
101
  return [...document.body.querySelectorAll<HTMLElement>('.u-tree__node-content')]
70
102
  }
@@ -130,4 +162,88 @@ describe('UTreeSelect', () => {
130
162
  unmount()
131
163
  }
132
164
  })
165
+
166
+ it('loads default tree remotely on mount with loading state', async () => {
167
+ let resolveRemote!: (data: Record<string, any>[]) => void
168
+ const calls: string[] = []
169
+ const { host, unmount } = mountTreeSelect({
170
+ data: (qs: string) => {
171
+ calls.push(qs)
172
+ return new Promise((resolve) => {
173
+ resolveRemote = resolve
174
+ })
175
+ }
176
+ })
177
+
178
+ try {
179
+ // 挂载即以空串调用一次
180
+ expect(calls).toEqual([''])
181
+
182
+ await openPanelWithLoading(host)
183
+ expect(document.body.querySelector('.u-tree-select__loading')).toBeTruthy()
184
+ expect(queryNodeContents()).toHaveLength(0)
185
+
186
+ resolveRemote(treeData)
187
+ await sleep(50)
188
+ expect(document.body.querySelector('.u-tree-select__loading')).toBeFalsy()
189
+ expect(queryNodeContents().length).toBeGreaterThan(0)
190
+ } finally {
191
+ unmount()
192
+ }
193
+ })
194
+
195
+ it('queries the remote function on input and expands the result', async () => {
196
+ const calls: string[] = []
197
+ const { host, unmount } = mountTreeSelect({
198
+ data: (qs: string) => {
199
+ calls.push(qs)
200
+ return Promise.resolve(qs ? [treeData[0]!] : treeData)
201
+ }
202
+ })
203
+
204
+ try {
205
+ // 初始空串加载完成
206
+ await sleep(50)
207
+ expect(calls).toEqual([''])
208
+
209
+ await openDropdown(host)
210
+ await typeQuery(host, '朝阳区')
211
+ expect(calls).toEqual(['', '朝阳区'])
212
+
213
+ const contents = queryNodeContents()
214
+ expect(contents.some((el) => el.textContent?.includes('朝阳区(最新)'))).toBe(true)
215
+ } finally {
216
+ unmount()
217
+ }
218
+ })
219
+
220
+ it('discards stale remote responses when a newer query resolves first', async () => {
221
+ const pending: ((data: Record<string, any>[]) => void)[] = []
222
+ const { host, unmount } = mountTreeSelect({
223
+ data: (qs: string) =>
224
+ new Promise((resolve) => {
225
+ pending.push(resolve)
226
+ })
227
+ })
228
+
229
+ try {
230
+ await openPanelWithLoading(host)
231
+ expect(document.body.querySelector('.u-tree-select__loading')).toBeTruthy()
232
+
233
+ await typeQuery(host, '上海')
234
+ expect(pending).toHaveLength(2)
235
+
236
+ // 新查询先返回
237
+ pending[1]!([treeData[1]!])
238
+ await sleep(20)
239
+ expect(queryNodeContents().some((el) => el.textContent?.includes('上海'))).toBe(true)
240
+
241
+ // 慢的旧响应(初始空串查询)后返回,不能覆盖新结果
242
+ pending[0]!(treeData)
243
+ await sleep(20)
244
+ expect(queryNodeContents().some((el) => el.textContent?.includes('北京'))).toBe(false)
245
+ } finally {
246
+ unmount()
247
+ }
248
+ })
133
249
  })
@@ -45,6 +45,12 @@ $root-name: tree-select;
45
45
  max-height: 240px;
46
46
  }
47
47
 
48
+ @include m.e(loading) {
49
+ // 与树内容区同高,加载期间保持面板形态,避免塌缩成贴着输入框的小条
50
+ @include m.flex($justify: center, $align: center);
51
+ min-height: 240px;
52
+ }
53
+
48
54
  @include m.e(panel) {
49
55
  @include m.size using ($size) {
50
56
  @include m.bem($root-name, content-tree) {