@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
@@ -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) {
@@ -49,8 +49,13 @@
49
49
  <template #content>
50
50
  <!-- 菜单列表 -->
51
51
 
52
+ <div v-if="remoteLoading" :class="cls.e('loading')">
53
+ <ULoading type="dual-ring" />
54
+ </div>
52
55
  <u-tree
56
+ v-else
53
57
  v-bind="treeProps"
58
+ :data="treeData"
54
59
  :selected="model"
55
60
  @update:selected="handleSelect"
56
61
  ref="treeRef"
@@ -68,7 +73,7 @@
68
73
  </template>
69
74
 
70
75
  <script lang="ts" setup>
71
- import { dfs, o } from '@cat-kit/core'
76
+ import { debounce, dfs, o } from '@cat-kit/core'
72
77
  import { useFormFallbackProps, useUserAction } from '@veltra/compositions'
73
78
  import { ArrowDown, Close } from '@veltra/icons/normal'
74
79
  import { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'
@@ -79,6 +84,7 @@ import type { TreeSelectProps, TreeSelectEmits, TreeExposed, InputExposed } from
79
84
  import { UDropdown } from '../dropdown'
80
85
  import { UIcon } from '../icon'
81
86
  import { UInput } from '../input'
87
+ import { ULoading } from '../loading'
82
88
  import { UTree } from '../tree'
83
89
  import type { TreeSlotsScope } from '../tree/di'
84
90
 
@@ -113,6 +119,7 @@ const treeProps = computed(() => {
113
119
  'disabled',
114
120
  'label',
115
121
  'readonly',
122
+ 'data',
116
123
  'contentClass',
117
124
  'contentStyle',
118
125
  'minWidth',
@@ -125,11 +132,56 @@ const cls = bem('tree-select')
125
132
  const labelKey = computed(() => fieldKey(props.labelKey, 'label'))
126
133
  const valueKey = computed(() => fieldKey(props.valueKey, 'value'))
127
134
 
128
- /**过滤 */
135
+ /** 是否可搜索:显式声明或数据源为远程函数 */
136
+ const filterable = computed(() => props.filterable || typeof props.data === 'function')
137
+
138
+ /** 过滤 */
129
139
  const qs = shallowRef('')
130
- watch(qs, (qs) => {
131
- treeRef.value?.filter(qs)
132
- })
140
+
141
+ const treeRef = shallowRef<TreeExposed>()
142
+
143
+ /** 远程搜索返回的树数据 */
144
+ const remoteData = shallowRef<Record<string, any>[]>()
145
+
146
+ /** 面板渲染的树数据:data 为函数时来自远程搜索,否则为静态数据 */
147
+ const treeData = computed(() => (typeof props.data === 'function' ? remoteData.value : props.data))
148
+
149
+ const remoteLoading = shallowRef(false)
150
+ /** 远程请求序号;响应返回时序号不一致说明已有更新的请求,结果作废 */
151
+ let remoteSeq = 0
152
+
153
+ const debouncedRemoteSearch = debounce(
154
+ async (
155
+ query: string,
156
+ dataFn: (qs: string) => Promise<Record<string, any>[]> | Record<string, any>[]
157
+ ) => {
158
+ const seq = ++remoteSeq
159
+ remoteLoading.value = true
160
+ try {
161
+ const data = await dataFn(query)
162
+ // 响应到达时查询词可能已变化或已有更新的请求,过期结果直接丢弃
163
+ if (seq !== remoteSeq || query !== qs.value) return
164
+ remoteData.value = data ?? []
165
+ // 搜索结果展示完整命中路径,与本地过滤自动展开祖先的行为对齐
166
+ if (query) nextTick(() => treeRef.value?.expandAll())
167
+ } finally {
168
+ if (seq === remoteSeq) remoteLoading.value = false
169
+ }
170
+ },
171
+ 200
172
+ )
173
+
174
+ watch(
175
+ [qs, () => props.data],
176
+ ([query, data]) => {
177
+ if (typeof data === 'function') {
178
+ debouncedRemoteSearch(query, data)
179
+ return
180
+ }
181
+ treeRef.value?.filter(query)
182
+ },
183
+ { immediate: true }
184
+ )
133
185
 
134
186
  const model = defineModel<string | number>()
135
187
 
@@ -156,21 +208,21 @@ function setLabel(next?: string) {
156
208
 
157
209
  /** 触发输入框的值:查询态下承载查询串,否则展示选中标签 */
158
210
  const inputValue = computed(() => {
159
- if (props.filterable && querying.value) return qs.value
211
+ if (filterable.value && querying.value) return qs.value
160
212
  return model.value || model.value === 0 ? label.value : undefined
161
213
  })
162
214
 
163
215
  /** 查询态下已选标签降级为占位提示 */
164
216
  const inputPlaceholder = computed(() => {
165
217
  const display = model.value || model.value === 0 ? label.value : undefined
166
- if (props.filterable && querying.value && display) {
218
+ if (filterable.value && querying.value && display) {
167
219
  return display
168
220
  }
169
221
  return props.placeholder
170
222
  })
171
223
 
172
224
  function handleQueryInput(value: string) {
173
- if (!props.filterable) return
225
+ if (!filterable.value) return
174
226
  qs.value = value
175
227
  }
176
228
 
@@ -188,7 +240,7 @@ const showClear = computed(() => {
188
240
  * 非输入区域(箭头、留白)放行,维持原开合行为。
189
241
  */
190
242
  function handleTriggerClickCapture(e: MouseEvent) {
191
- if (!props.filterable || disabled.value) return
243
+ if (!filterable.value || disabled.value) return
192
244
  if (!(e.target instanceof HTMLInputElement)) return
193
245
 
194
246
  e.stopPropagation()
@@ -196,7 +248,7 @@ function handleTriggerClickCapture(e: MouseEvent) {
196
248
  }
197
249
 
198
250
  watch(dropdownVisible, (visible) => {
199
- if (visible && props.filterable) {
251
+ if (visible && filterable.value) {
200
252
  // 面板展开后进入查询态并聚焦输入框,可以立即输入查询
201
253
  querying.value = true
202
254
  nextTick(() => inputRef.value?.el?.focus())
@@ -209,8 +261,6 @@ const { formProps } = injectFormContext()
209
261
 
210
262
  const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])
211
263
 
212
- const treeRef = shallowRef<TreeExposed>()
213
-
214
264
  const dropdownRef = shallowRef<InstanceType<typeof UDropdown>>()
215
265
 
216
266
  const { userAction, isUserActive } = useUserAction()
@@ -225,7 +275,7 @@ const handleClear = userAction(() => {
225
275
 
226
276
  /** 外部回显:按 data 查找 label(O(n));用户动作期内跳过 */
227
277
  watch(
228
- [() => props.data, model],
278
+ [treeData, model],
229
279
  ([data, modelVal]) => {
230
280
  if (isUserActive()) return
231
281
  if (!data?.length || modelVal === undefined || modelVal === null || modelVal === '') {
@@ -8,6 +8,16 @@ export interface DropdownProps {
8
8
  * @default 'hover'
9
9
  */
10
10
  trigger?: 'hover' | 'click' | 'custom'
11
+ /**
12
+ * 弹出方向;空间不足时仍会自动翻转
13
+ * @default 'bottom'
14
+ */
15
+ direction?: 'top' | 'bottom' | 'left' | 'right'
16
+ /**
17
+ * 弹出对齐方式
18
+ * @default 'start'
19
+ */
20
+ alignment?: 'center' | 'start' | 'end'
11
21
  /**
12
22
  * 宽度
13
23
  * @default - 跟随触发宽度
@@ -4,9 +4,20 @@ import type { CSSProperties } from 'vue'
4
4
  import type { TreeProps } from './tree'
5
5
  /** 树形多选组件组件属性 */
6
6
  export interface MultiTreeSelectProps
7
- extends FormComponentProps, Omit<TreeProps, 'selected' | 'checked' | 'selectable' | 'checkable'> {
7
+ extends
8
+ FormComponentProps,
9
+ Omit<TreeProps, 'selected' | 'checked' | 'selectable' | 'checkable' | 'data'> {
8
10
  modelValue?: (string | number)[]
9
11
 
12
+ /**
13
+ * 树数据
14
+ * @description 如果传入一个函数,那么filterable会被强制启用;
15
+ * 函数按查询词返回匹配的树,初始以空串调用一次(加载默认树)
16
+ */
17
+ data?:
18
+ | Record<string, any>[]
19
+ | ((qs: string) => Promise<Record<string, any>[]> | Record<string, any>[])
20
+
10
21
  /**自定义占位文字 */
11
22
  placeholder?: string
12
23
  /**
@@ -5,9 +5,20 @@ import type { TreeProps } from './tree'
5
5
 
6
6
  /** 树形选择器组件属性 */
7
7
  export interface TreeSelectProps
8
- extends FormComponentProps, Omit<TreeProps, 'selected' | 'checked' | 'selectable' | 'checkable'> {
8
+ extends
9
+ FormComponentProps,
10
+ Omit<TreeProps, 'selected' | 'checked' | 'selectable' | 'checkable' | 'data'> {
9
11
  modelValue?: string | number
10
12
 
13
+ /**
14
+ * 树数据
15
+ * @description 如果传入一个函数,那么filterable会被强制启用;
16
+ * 函数按查询词返回匹配的树,初始以空串调用一次(加载默认树)
17
+ */
18
+ data?:
19
+ | Record<string, any>[]
20
+ | ((qs: string) => Promise<Record<string, any>[]> | Record<string, any>[])
21
+
11
22
  /** 自定义占位文字 */
12
23
  placeholder?: string
13
24
  /**