@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.
- package/dist/components/batch-edit/batch-edit-form.js +40 -26
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-list.js +17 -5
- package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
- package/dist/components/batch-edit/di.js.map +1 -1
- package/dist/components/batch-edit/use-handlers.js +3 -3
- package/dist/components/batch-edit/use-handlers.js.map +1 -1
- package/dist/components/batch-edit/use-shortcut-key.js +4 -2
- package/dist/components/batch-edit/use-shortcut-key.js.map +1 -1
- package/dist/components/card/style2.css +0 -2
- package/dist/components/dialog/style2.css +1 -1
- package/dist/components/dropdown/dropdown.js +5 -3
- package/dist/components/dropdown/dropdown.js.map +1 -1
- package/dist/components/expression-editor/components/path-preview.js +3 -6
- package/dist/components/expression-editor/components/path-preview.js.map +1 -1
- package/dist/components/expression-editor/components/variable-picker.js +49 -12
- package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
- package/dist/components/expression-editor/core/chip.js +1 -15
- package/dist/components/expression-editor/core/chip.js.map +1 -1
- package/dist/components/expression-editor/core/editor.js +20 -18
- package/dist/components/expression-editor/core/editor.js.map +1 -1
- package/dist/components/expression-editor/expression-editor.js +53 -22
- package/dist/components/expression-editor/expression-editor.js.map +1 -1
- package/dist/components/expression-editor/style2.css +135 -96
- package/dist/components/form/form.js.map +1 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +0 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/style2.css +8 -3
- package/dist/components/index.d.ts +26 -26
- package/dist/components/index.js +52 -52
- package/dist/components/multi-select/multi-select.js +16 -12
- package/dist/components/multi-select/multi-select.js.map +1 -1
- package/dist/components/multi-select/style2.css +7 -0
- package/dist/components/multi-tree-select/multi-tree-select.js +82 -56
- package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
- package/dist/components/multi-tree-select/style2.css +7 -0
- package/dist/components/select/select.js +7 -3
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/select/style2.css +7 -0
- package/dist/components/select/use-options.js +14 -3
- package/dist/components/select/use-options.js.map +1 -1
- package/dist/components/tree-select/style2.css +7 -0
- package/dist/components/tree-select/tree-select.js +49 -17
- package/dist/components/tree-select/tree-select.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/types/dropdown.d.ts +10 -0
- package/dist/types/multi-tree-select.d.ts +7 -1
- package/dist/types/tree-select.d.ts +7 -1
- package/package.json +1 -1
- package/src/components/batch-edit/__test__/batch-edit.test.ts +119 -7
- package/src/components/batch-edit/batch-edit-form.vue +21 -2
- package/src/components/batch-edit/batch-edit-list.vue +18 -1
- package/src/components/batch-edit/di.ts +1 -1
- package/src/components/batch-edit/use-handlers.ts +6 -5
- package/src/components/batch-edit/use-shortcut-key.ts +5 -2
- package/src/components/card/style.scss +0 -2
- package/src/components/dialog/style.scss +2 -1
- package/src/components/dropdown/dropdown.vue +9 -4
- package/src/components/expression-editor/__test__/chip.test.ts +3 -14
- package/src/components/expression-editor/components/path-preview.vue +1 -2
- package/src/components/expression-editor/components/variable-picker.vue +54 -13
- package/src/components/expression-editor/core/chip.ts +3 -29
- package/src/components/expression-editor/core/editor.ts +28 -21
- package/src/components/expression-editor/expression-editor.vue +47 -21
- package/src/components/expression-editor/style.scss +167 -111
- package/src/components/form/form.vue +0 -1
- package/src/components/form/style.scss +11 -4
- package/src/components/multi-select/multi-select.vue +11 -2
- package/src/components/multi-select/style.scss +6 -0
- package/src/components/multi-tree-select/multi-tree-select.vue +58 -9
- package/src/components/multi-tree-select/style.scss +6 -0
- package/src/components/select/__test__/select.test.ts +78 -0
- package/src/components/select/select.vue +13 -3
- package/src/components/select/style.scss +6 -0
- package/src/components/select/use-options.ts +25 -4
- package/src/components/tree-select/__test__/tree-select.test.ts +116 -0
- package/src/components/tree-select/style.scss +6 -0
- package/src/components/tree-select/tree-select.vue +63 -13
- package/src/types/dropdown.ts +10 -0
- package/src/types/multi-tree-select.ts +12 -1
- 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 {
|
|
198
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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 {
|
|
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
|
-
|
|
131
|
-
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 &&
|
|
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
|
-
[
|
|
278
|
+
[treeData, model],
|
|
229
279
|
([data, modelVal]) => {
|
|
230
280
|
if (isUserActive()) return
|
|
231
281
|
if (!data?.length || modelVal === undefined || modelVal === null || modelVal === '') {
|
package/src/types/dropdown.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|
package/src/types/tree-select.ts
CHANGED
|
@@ -5,9 +5,20 @@ import type { TreeProps } from './tree'
|
|
|
5
5
|
|
|
6
6
|
/** 树形选择器组件属性 */
|
|
7
7
|
export interface TreeSelectProps
|
|
8
|
-
extends
|
|
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
|
/**
|