@veltra/desktop 1.3.5 → 1.3.7
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/action/action-group.vue.d.ts +2 -2
- package/dist/components/action/action.vue.d.ts +1 -1
- package/dist/components/auto-complete/auto-complete.vue.d.ts +2 -2
- package/dist/components/cascade/cascade.js +8 -2
- package/dist/components/cascade/cascade.js.map +1 -1
- package/dist/components/cascade/cascade.vue.d.ts +5 -5
- package/dist/components/cascade/style2.css +4 -0
- package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
- package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
- package/dist/components/code-editor/code-editor.vue.d.ts +2 -2
- package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
- package/dist/components/date-picker/date-picker.vue.d.ts +2 -2
- package/dist/components/date-range-picker/date-range-picker.js +5 -2
- package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
- package/dist/components/date-range-picker/date-range-picker.vue.d.ts +2 -2
- package/dist/components/date-range-picker/style2.css +4 -0
- package/dist/components/form-item/form-item.js.map +1 -1
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form-item/style2.css +2 -1
- package/dist/components/grid-input/style2.css +3 -0
- package/dist/components/input/input.vue.d.ts +4 -4
- package/dist/components/multi-select/multi-select.js +67 -40
- package/dist/components/multi-select/multi-select.js.map +1 -1
- package/dist/components/multi-select/multi-select.vue.d.ts +7 -7
- package/dist/components/multi-select/style2.css +23 -10
- package/dist/components/multi-tree-select/multi-tree-select.js +52 -35
- package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
- package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +6 -6
- package/dist/components/multi-tree-select/style2.css +26 -16
- package/dist/components/number-input/number-input.vue.d.ts +4 -4
- package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
- package/dist/components/password-input/password-input.vue.d.ts +1 -1
- package/dist/components/progress-nodes/progress-nodes.vue.d.ts +1 -1
- package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
- package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
- package/dist/components/select/select.js +159 -95
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/select/select.vue.d.ts +2 -2
- package/dist/components/select/style2.css +4 -17
- package/dist/components/select/use-options.js +67 -26
- package/dist/components/select/use-options.js.map +1 -1
- package/dist/components/steps/steps.vue.d.ts +1 -1
- package/dist/components/tabs/tabs-horizontal.vue.d.ts +2 -2
- package/dist/components/tabs/tabs-vertical.vue.d.ts +2 -2
- package/dist/components/tabs/tabs.vue.d.ts +2 -2
- package/dist/components/textarea/style2.css +1 -0
- package/dist/components/textarea/textarea.vue.d.ts +4 -4
- package/dist/components/tree/tree.vue.d.ts +1 -1
- package/dist/components/tree-select/style2.css +6 -15
- package/dist/components/tree-select/tree-select.js +95 -41
- package/dist/components/tree-select/tree-select.js.map +1 -1
- package/dist/components/tree-select/tree-select.vue.d.ts +6 -6
- package/package.json +6 -6
- package/src/components/cascade/cascade.vue +15 -2
- package/src/components/cascade/style.scss +5 -0
- package/src/components/date-range-picker/date-range-picker.vue +9 -1
- package/src/components/date-range-picker/style.scss +5 -0
- package/src/components/form-item/form-item.vue +2 -2
- package/src/components/form-item/style.scss +2 -1
- package/src/components/grid-input/style.scss +1 -0
- package/src/components/multi-select/multi-select.vue +50 -19
- package/src/components/multi-select/style.scss +27 -4
- package/src/components/multi-tree-select/multi-tree-select.vue +34 -14
- package/src/components/multi-tree-select/style.scss +28 -9
- package/src/components/select/__test__/select.test.ts +110 -0
- package/src/components/select/select.vue +130 -46
- package/src/components/select/style.scss +5 -14
- package/src/components/select/use-options.ts +83 -39
- package/src/components/textarea/style.scss +1 -0
- package/src/components/tree-select/style.scss +4 -8
- package/src/components/tree-select/tree-select.vue +90 -17
|
@@ -17,24 +17,24 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
17
17
|
"update:modelValue": (value: string | number | undefined) => any;
|
|
18
18
|
} & {
|
|
19
19
|
"update:modelValue": (value?: string | number | undefined) => any;
|
|
20
|
-
clear: () => any;
|
|
21
20
|
change: (selectedData?: Record<string, any> | undefined) => any;
|
|
21
|
+
clear: () => any;
|
|
22
22
|
"update:text": (text?: string | undefined) => any;
|
|
23
23
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
24
24
|
"onUpdate:modelValue"?: ((value?: string | number | undefined) => any) | undefined;
|
|
25
|
-
onClear?: (() => any) | undefined;
|
|
26
25
|
onChange?: ((selectedData?: Record<string, any> | undefined) => any) | undefined;
|
|
26
|
+
onClear?: (() => any) | undefined;
|
|
27
27
|
"onUpdate:text"?: ((text?: string | undefined) => any) | undefined;
|
|
28
28
|
}>, {
|
|
29
|
+
disabled: boolean;
|
|
30
|
+
readonly: boolean;
|
|
31
|
+
minWidth: string;
|
|
32
|
+
expandAll: boolean;
|
|
29
33
|
labelKey: string;
|
|
30
34
|
valueKey: string;
|
|
31
|
-
expandAll: boolean;
|
|
32
35
|
placeholder: string;
|
|
33
36
|
clearable: boolean;
|
|
34
37
|
filterable: boolean;
|
|
35
|
-
minWidth: string;
|
|
36
|
-
disabled: boolean;
|
|
37
|
-
readonly: boolean;
|
|
38
38
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
39
39
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
40
40
|
declare const _default: typeof __VLS_export;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veltra/desktop",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist",
|
|
6
6
|
"src"
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"@cat-kit/core": ">=1.1.8",
|
|
58
58
|
"@cat-kit/excel": ">=1.0.0",
|
|
59
59
|
"@cat-kit/fe": "^1.1.8",
|
|
60
|
-
"@veltra/compositions": "1.3.
|
|
61
|
-
"@veltra/directives": "1.3.
|
|
62
|
-
"@veltra/icons": "1.3.
|
|
63
|
-
"@veltra/styles": "1.3.
|
|
64
|
-
"@veltra/utils": "1.3.
|
|
60
|
+
"@veltra/compositions": "1.3.7",
|
|
61
|
+
"@veltra/directives": "1.3.7",
|
|
62
|
+
"@veltra/icons": "1.3.7",
|
|
63
|
+
"@veltra/styles": "1.3.7",
|
|
64
|
+
"@veltra/utils": "1.3.7",
|
|
65
65
|
"vue": ">=3.5.40"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
<u-dropdown
|
|
3
3
|
v-if="!readonly"
|
|
4
4
|
v-bind="$attrs"
|
|
5
|
-
:class="[
|
|
5
|
+
:class="[
|
|
6
|
+
cls.b,
|
|
7
|
+
bem.is('disabled', disabled),
|
|
8
|
+
bem.is('multiple', multiple),
|
|
9
|
+
cls.m(size),
|
|
10
|
+
bem.is('focus', multiple && dropdownVisible)
|
|
11
|
+
]"
|
|
6
12
|
:content-class="[cls.e('panel'), cls.em('panel', size)]"
|
|
7
13
|
trigger="click"
|
|
8
14
|
ref="dropdownRef"
|
|
@@ -10,7 +16,7 @@
|
|
|
10
16
|
width="auto"
|
|
11
17
|
@mouseenter="hovered = true"
|
|
12
18
|
@mouseleave="hovered = false"
|
|
13
|
-
@update:visible="
|
|
19
|
+
@update:visible="handleDropdownVisible"
|
|
14
20
|
>
|
|
15
21
|
<template #trigger>
|
|
16
22
|
<!-- 单选展示 -->
|
|
@@ -233,6 +239,13 @@ function handleClear() {
|
|
|
233
239
|
// 过滤
|
|
234
240
|
const qs = shallowRef<string>('')
|
|
235
241
|
|
|
242
|
+
const dropdownVisible = shallowRef(false)
|
|
243
|
+
|
|
244
|
+
const handleDropdownVisible = (visible: boolean) => {
|
|
245
|
+
dropdownVisible.value = visible
|
|
246
|
+
if (!visible) qs.value = ''
|
|
247
|
+
}
|
|
248
|
+
|
|
236
249
|
watch([qs, forest], ([qs, forest]) => {
|
|
237
250
|
const { filterable } = props
|
|
238
251
|
if (!filterable || !qs) {
|
|
@@ -23,6 +23,11 @@ $menu-name: $root-name + '-panel-item';
|
|
|
23
23
|
&:hover {
|
|
24
24
|
border-color: fn.use-var(color, primary);
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
@include m.is(focus) {
|
|
28
|
+
border-color: fn.use-var(color, primary);
|
|
29
|
+
box-shadow: fn.use-var(focus-ring);
|
|
30
|
+
}
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
@include m.size using ($size) {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
:disabled="disabled"
|
|
11
11
|
@mouseenter.native="hovered = true"
|
|
12
12
|
@mouseleave.native="hovered = false"
|
|
13
|
+
@update:visible="dropdownVisible = $event"
|
|
13
14
|
>
|
|
14
15
|
<template #trigger>
|
|
15
16
|
<input
|
|
@@ -87,8 +88,15 @@ const emit = defineEmits<DateRangePickerEmits>()
|
|
|
87
88
|
|
|
88
89
|
const cls = bem('date-range-picker')
|
|
89
90
|
|
|
91
|
+
const dropdownVisible = shallowRef(false)
|
|
92
|
+
|
|
90
93
|
const className = computed(() => {
|
|
91
|
-
return [
|
|
94
|
+
return [
|
|
95
|
+
cls.b,
|
|
96
|
+
cls.m(size.value),
|
|
97
|
+
bem.is('disabled', disabled.value),
|
|
98
|
+
bem.is('focus', dropdownVisible.value)
|
|
99
|
+
]
|
|
92
100
|
})
|
|
93
101
|
|
|
94
102
|
const { formProps } = injectFormContext()
|
|
@@ -32,6 +32,11 @@ $root-name: date-range-picker;
|
|
|
32
32
|
border-color: fn.use-var(color, primary);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
@include m.is(focus) {
|
|
36
|
+
border-color: fn.use-var(color, primary);
|
|
37
|
+
box-shadow: fn.use-var(focus-ring);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
@include m.e(input) {
|
|
36
41
|
outline: none;
|
|
37
42
|
border: none;
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
>
|
|
8
8
|
<u-tip v-if="tips" :content="tips" :class="cls.e('tips')" :show-delay="500">
|
|
9
9
|
<span :class="cls.e('tip-label')">
|
|
10
|
-
<slot name="label">{{ label }}
|
|
10
|
+
<slot name="label">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>
|
|
11
11
|
</span>
|
|
12
12
|
</u-tip>
|
|
13
13
|
<template v-else>
|
|
14
|
-
<slot name="label">{{ label }}
|
|
14
|
+
<slot name="label">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>
|
|
15
15
|
</template>
|
|
16
16
|
</label>
|
|
17
17
|
|
|
@@ -28,7 +28,7 @@ $root-name: form-item;
|
|
|
28
28
|
color: fn.use-var(text-color, main);
|
|
29
29
|
text-align: right;
|
|
30
30
|
line-height: 1.3;
|
|
31
|
-
font-weight:
|
|
31
|
+
font-weight: 400;
|
|
32
32
|
|
|
33
33
|
@include m.flex($justify: flex-end);
|
|
34
34
|
|
|
@@ -99,6 +99,7 @@ $root-name: form-item;
|
|
|
99
99
|
padding-right: 0;
|
|
100
100
|
padding-bottom: 4px;
|
|
101
101
|
text-align: left;
|
|
102
|
+
color: fn.use-var(text-color, second);
|
|
102
103
|
justify-content: flex-start;
|
|
103
104
|
}
|
|
104
105
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
v-bind="$attrs"
|
|
5
5
|
trigger="click"
|
|
6
6
|
ref="dropdownRef"
|
|
7
|
-
:class="[cls.b, cls.m(size), bem.is('disabled', disabled)]"
|
|
7
|
+
:class="[cls.b, cls.m(size), bem.is('disabled', disabled), bem.is('focus', dropdownVisible)]"
|
|
8
8
|
:content-class="[cls.e('panel'), cls.em('panel', size), contentClass]"
|
|
9
9
|
:content-style="contentStyle"
|
|
10
10
|
:min-width="minWidth"
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
>
|
|
17
17
|
<!-- 触发 -->
|
|
18
18
|
<template #trigger>
|
|
19
|
-
<span :class="cls.e('placeholder')" v-
|
|
19
|
+
<span :class="cls.e('placeholder')" v-if="!filterable && !model?.length">
|
|
20
20
|
{{ placeholder }}
|
|
21
21
|
</span>
|
|
22
22
|
|
|
23
|
-
<div v-if="model?.length" :class="cls.e('tags')">
|
|
23
|
+
<div v-if="model?.length || filterable" :class="cls.e('tags')">
|
|
24
24
|
<u-tag
|
|
25
25
|
v-for="option of tags"
|
|
26
26
|
:key="option[valueKey]"
|
|
@@ -31,6 +31,19 @@
|
|
|
31
31
|
{{ option[labelKey] }}
|
|
32
32
|
</u-tag>
|
|
33
33
|
<u-tag v-if="restTag" size="small"> {{ restTag }}+ </u-tag>
|
|
34
|
+
|
|
35
|
+
<!-- 内嵌查询输入框:承载过滤与回车创建 -->
|
|
36
|
+
<input
|
|
37
|
+
v-if="filterable"
|
|
38
|
+
ref="inputRef"
|
|
39
|
+
:class="cls.e('input')"
|
|
40
|
+
v-model="queryString"
|
|
41
|
+
:placeholder="model?.length ? '' : placeholder"
|
|
42
|
+
:disabled="disabled"
|
|
43
|
+
@click.stop="handleInputClick"
|
|
44
|
+
@focus="handleInputFocus"
|
|
45
|
+
@keydown.enter.prevent="handleCreateByEnter"
|
|
46
|
+
/>
|
|
34
47
|
</div>
|
|
35
48
|
|
|
36
49
|
<transition name="zoom-in" mode="out-in">
|
|
@@ -62,19 +75,6 @@
|
|
|
62
75
|
<span> 已选 {{ model?.length }}/{{ max ?? options?.length ?? 0 }} </span>
|
|
63
76
|
</div>
|
|
64
77
|
|
|
65
|
-
<!-- 过滤器 -->
|
|
66
|
-
<div v-if="filterable" :class="cls.e('content-filter')">
|
|
67
|
-
<u-input
|
|
68
|
-
placeholder="输入关键字进行过滤"
|
|
69
|
-
v-model="queryString"
|
|
70
|
-
@keydown.enter.prevent="handleCreateByEnter"
|
|
71
|
-
>
|
|
72
|
-
<template #suffix>
|
|
73
|
-
<u-icon><Search /></u-icon>
|
|
74
|
-
</template>
|
|
75
|
-
</u-input>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
78
|
<!-- 多选列表 -->
|
|
79
79
|
<!--
|
|
80
80
|
虚拟化启用时内容容器 height 由 useVirtualizer 命令式写入;
|
|
@@ -143,10 +143,18 @@
|
|
|
143
143
|
<script lang="ts" setup>
|
|
144
144
|
import { o as chainObj } from '@cat-kit/core'
|
|
145
145
|
import { useFormFallbackProps, useVirtualizer } from '@veltra/compositions'
|
|
146
|
-
import { ArrowDown, Close
|
|
146
|
+
import { ArrowDown, Close } from '@veltra/icons/normal'
|
|
147
147
|
import { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'
|
|
148
148
|
import { injectFormContext } from '@veltra/utils'
|
|
149
|
-
import {
|
|
149
|
+
import {
|
|
150
|
+
computed,
|
|
151
|
+
nextTick,
|
|
152
|
+
shallowRef,
|
|
153
|
+
shallowReactive,
|
|
154
|
+
watch,
|
|
155
|
+
provide,
|
|
156
|
+
useTemplateRef
|
|
157
|
+
} from 'vue'
|
|
150
158
|
|
|
151
159
|
import type {
|
|
152
160
|
MultiSelectEmits,
|
|
@@ -158,7 +166,6 @@ import { UCheckbox } from '../checkbox'
|
|
|
158
166
|
import { UDropdown } from '../dropdown'
|
|
159
167
|
import { UEmpty } from '../empty'
|
|
160
168
|
import { UIcon } from '../icon'
|
|
161
|
-
import { UInput } from '../input'
|
|
162
169
|
import { UScroll } from '../scroll'
|
|
163
170
|
import { useOptions } from '../select/use-options'
|
|
164
171
|
import { UTag } from '../tag'
|
|
@@ -319,12 +326,35 @@ const restTag = computed(() => {
|
|
|
319
326
|
return (model.value?.length ?? 0) - tags.value.length
|
|
320
327
|
})
|
|
321
328
|
|
|
329
|
+
const dropdownVisible = shallowRef(false)
|
|
330
|
+
|
|
331
|
+
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
|
|
332
|
+
|
|
322
333
|
const handleDropdownVisible = (visible: boolean) => {
|
|
334
|
+
dropdownVisible.value = visible
|
|
323
335
|
if (!visible) {
|
|
324
336
|
queryString.value = ''
|
|
325
337
|
}
|
|
326
338
|
}
|
|
327
339
|
|
|
340
|
+
/**
|
|
341
|
+
* 拦截输入区域的点击(@click.stop):阻止 dropdown 的 trigger 开合切换,
|
|
342
|
+
* 保持面板展开以不中断输入。
|
|
343
|
+
*/
|
|
344
|
+
const handleInputClick = () => {
|
|
345
|
+
if (!dropdownVisible.value) dropdownRef.value?.open()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const handleInputFocus = () => {
|
|
349
|
+
if (!dropdownVisible.value) dropdownRef.value?.open()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** 勾选后把焦点还给输入框,便于连续输入过滤 */
|
|
353
|
+
const refocusInput = () => {
|
|
354
|
+
if (!filterable.value) return
|
|
355
|
+
nextTick(() => inputRef.value?.focus())
|
|
356
|
+
}
|
|
357
|
+
|
|
328
358
|
const handleCheck = (option: Record<string, any>, checked: boolean) => {
|
|
329
359
|
if (checked) {
|
|
330
360
|
if (option.__isTemp && props.creatable) {
|
|
@@ -352,6 +382,7 @@ const handleCheck = (option: Record<string, any>, checked: boolean) => {
|
|
|
352
382
|
|
|
353
383
|
emitChange()
|
|
354
384
|
dropdownRef.value?.updateDropdown()
|
|
385
|
+
refocusInput()
|
|
355
386
|
}
|
|
356
387
|
|
|
357
388
|
/** 全选 */
|
|
@@ -36,13 +36,18 @@ $root-name: multi-select;
|
|
|
36
36
|
border-color: fn.use-var(color, primary);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
@include m.is(focus) {
|
|
40
|
+
border-color: fn.use-var(color, primary);
|
|
41
|
+
box-shadow: fn.use-var(focus-ring);
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
@include m.e(ripple) {
|
|
40
45
|
background-color: fn.use-var(color, primary, light-7);
|
|
41
46
|
opacity: 0.5;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
@include m.e(options) {
|
|
45
|
-
max-height:
|
|
50
|
+
max-height: 240px;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
@include m.e(options-wrap) {
|
|
@@ -79,6 +84,27 @@ $root-name: multi-select;
|
|
|
79
84
|
@include m.flex($justify: flex-start, $align: center, $wrap: wrap);
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
@include m.e(input) {
|
|
88
|
+
flex: 1;
|
|
89
|
+
min-width: 60px;
|
|
90
|
+
padding: 0;
|
|
91
|
+
margin: 0;
|
|
92
|
+
border: none;
|
|
93
|
+
outline: none;
|
|
94
|
+
background: transparent;
|
|
95
|
+
font: inherit;
|
|
96
|
+
color: fn.use-var(text-color, main);
|
|
97
|
+
cursor: text;
|
|
98
|
+
|
|
99
|
+
&::placeholder {
|
|
100
|
+
color: fn.use-var(text-color, placeholder);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
&:disabled {
|
|
104
|
+
cursor: not-allowed;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
82
108
|
@include m.e(readonly-tags) {
|
|
83
109
|
padding-left: 0;
|
|
84
110
|
padding-right: 0;
|
|
@@ -104,9 +130,6 @@ $root-name: multi-select;
|
|
|
104
130
|
|
|
105
131
|
@include m.e(panel) {
|
|
106
132
|
@include m.size using ($size) {
|
|
107
|
-
@include m.bem($root-name, content-filter) {
|
|
108
|
-
padding: calc(fn.use-var(gap, $size) / 2) fn.use-var(gap, $size);
|
|
109
|
-
}
|
|
110
133
|
@include m.bem($root-name, content-header) {
|
|
111
134
|
padding: calc(fn.use-var(gap, $size) / 2) calc(fn.use-var(gap, $size) * 2);
|
|
112
135
|
border-bottom: fn.use-var(border);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<u-dropdown
|
|
3
3
|
v-if="!readonly"
|
|
4
4
|
v-bind="$attrs"
|
|
5
|
-
:class="[cls.b, cls.m(size), bem.is('disabled', disabled)]"
|
|
5
|
+
:class="[cls.b, cls.m(size), bem.is('disabled', disabled), bem.is('focus', dropdownVisible)]"
|
|
6
6
|
trigger="click"
|
|
7
7
|
:content-class="[cls.e('panel'), cls.em('panel', size), contentClass]"
|
|
8
8
|
:content-style="contentStyle"
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
>
|
|
17
17
|
<template #trigger>
|
|
18
18
|
<!-- 默认展示 -->
|
|
19
|
-
<span :class="cls.e('placeholder')" v-
|
|
19
|
+
<span :class="cls.e('placeholder')" v-if="!filterable && !tags.length">
|
|
20
20
|
{{ placeholder }}
|
|
21
21
|
</span>
|
|
22
22
|
|
|
23
23
|
<!-- 选择的数据项 -->
|
|
24
|
-
<div v-if="tags.length" :class="cls.e('tags')">
|
|
24
|
+
<div v-if="tags.length || filterable" :class="cls.e('tags')">
|
|
25
25
|
<u-tag
|
|
26
26
|
v-for="(tag, index) in visibleTags"
|
|
27
27
|
:key="tag[valueKey]"
|
|
@@ -31,6 +31,18 @@
|
|
|
31
31
|
{{ tag[labelKey] }}
|
|
32
32
|
</u-tag>
|
|
33
33
|
<u-tag v-if="hiddenCount > 0">+{{ hiddenCount }}</u-tag>
|
|
34
|
+
|
|
35
|
+
<!-- 内嵌查询输入框:承载过滤 -->
|
|
36
|
+
<input
|
|
37
|
+
v-if="filterable"
|
|
38
|
+
ref="inputRef"
|
|
39
|
+
:class="cls.e('input')"
|
|
40
|
+
v-model="qs"
|
|
41
|
+
:placeholder="tags.length ? '' : placeholder"
|
|
42
|
+
:disabled="disabled"
|
|
43
|
+
@click.stop="handleInputClick"
|
|
44
|
+
@focus="handleInputFocus"
|
|
45
|
+
/>
|
|
34
46
|
</div>
|
|
35
47
|
<!-- 清空 icon -->
|
|
36
48
|
<transition name="zoom-in" mode="out-in">
|
|
@@ -59,14 +71,6 @@
|
|
|
59
71
|
{{ allExpanded ? '收起全部' : '展开全部' }}
|
|
60
72
|
</u-button>
|
|
61
73
|
</div>
|
|
62
|
-
<!-- 过滤器 -->
|
|
63
|
-
<div v-if="filterable" :class="[cls.e('content-filter'), cls.m(size)]">
|
|
64
|
-
<u-input placeholder="输入关键字进行过滤" v-model="qs">
|
|
65
|
-
<template #suffix>
|
|
66
|
-
<u-icon><Search /></u-icon>
|
|
67
|
-
</template>
|
|
68
|
-
</u-input>
|
|
69
|
-
</div>
|
|
70
74
|
<!-- 菜单列表 -->
|
|
71
75
|
<u-tree
|
|
72
76
|
v-bind="treeProps"
|
|
@@ -98,10 +102,10 @@
|
|
|
98
102
|
<script lang="ts" setup>
|
|
99
103
|
import { dfs, o } from '@cat-kit/core'
|
|
100
104
|
import { useFormFallbackProps } from '@veltra/compositions'
|
|
101
|
-
import { ArrowDown, Close
|
|
105
|
+
import { ArrowDown, Close } from '@veltra/icons/normal'
|
|
102
106
|
import { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'
|
|
103
107
|
import { injectFormContext } from '@veltra/utils'
|
|
104
|
-
import { computed, nextTick, shallowRef, watch } from 'vue'
|
|
108
|
+
import { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'
|
|
105
109
|
|
|
106
110
|
import type { MultiTreeSelectProps, MultiTreeSelectEmits, TreeExposed } from '../../types'
|
|
107
111
|
import type { DropdownExposed } from '../../types'
|
|
@@ -109,7 +113,6 @@ import { UButton } from '../button'
|
|
|
109
113
|
import { UCheckbox } from '../checkbox'
|
|
110
114
|
import { UDropdown } from '../dropdown'
|
|
111
115
|
import { UIcon } from '../icon'
|
|
112
|
-
import { UInput } from '../input'
|
|
113
116
|
import { UTag } from '../tag'
|
|
114
117
|
import { UTree } from '../tree'
|
|
115
118
|
import type { TreeSlotsScope } from '../tree/di'
|
|
@@ -306,9 +309,26 @@ const hiddenCount = computed(() => {
|
|
|
306
309
|
return 0
|
|
307
310
|
})
|
|
308
311
|
|
|
312
|
+
const dropdownVisible = shallowRef(false)
|
|
313
|
+
|
|
314
|
+
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
|
|
315
|
+
|
|
309
316
|
const handleDropdownVisible = (visible: boolean) => {
|
|
317
|
+
dropdownVisible.value = visible
|
|
310
318
|
if (!visible) {
|
|
311
319
|
qs.value = ''
|
|
312
320
|
}
|
|
313
321
|
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 拦截输入区域的点击(@click.stop):阻止 dropdown 的 trigger 开合切换,
|
|
325
|
+
* 保持面板展开以不中断输入。
|
|
326
|
+
*/
|
|
327
|
+
const handleInputClick = () => {
|
|
328
|
+
if (!dropdownVisible.value) dropdownRef.value?.open()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const handleInputFocus = () => {
|
|
332
|
+
if (!dropdownVisible.value) dropdownRef.value?.open()
|
|
333
|
+
}
|
|
314
334
|
</script>
|
|
@@ -22,6 +22,11 @@ $root-name: multi-tree-select;
|
|
|
22
22
|
border-color: fn.use-var(color, primary);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
@include m.is(focus) {
|
|
26
|
+
border-color: fn.use-var(color, primary);
|
|
27
|
+
box-shadow: fn.use-var(focus-ring);
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
@include m.size using ($size) {
|
|
26
31
|
min-height: fn.use-var(form-component-height, $size);
|
|
27
32
|
border-radius: fn.use-var(radius, $size);
|
|
@@ -58,16 +63,33 @@ $root-name: multi-tree-select;
|
|
|
58
63
|
color: fn.use-var(text-color, placeholder);
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
@include m.e(filter) {
|
|
62
|
-
margin-bottom: 4px;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
66
|
@include m.e(tags) {
|
|
66
67
|
flex-grow: 1;
|
|
67
68
|
overflow: hidden;
|
|
68
69
|
@include m.flex($justify: flex-start, $align: center, $wrap: wrap);
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
@include m.e(input) {
|
|
73
|
+
flex: 1;
|
|
74
|
+
min-width: 60px;
|
|
75
|
+
padding: 0;
|
|
76
|
+
margin: 0;
|
|
77
|
+
border: none;
|
|
78
|
+
outline: none;
|
|
79
|
+
background: transparent;
|
|
80
|
+
font: inherit;
|
|
81
|
+
color: fn.use-var(text-color, main);
|
|
82
|
+
cursor: text;
|
|
83
|
+
|
|
84
|
+
&::placeholder {
|
|
85
|
+
color: fn.use-var(text-color, placeholder);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&:disabled {
|
|
89
|
+
cursor: not-allowed;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
@include m.e(readonly-tags) {
|
|
72
94
|
padding-left: 0;
|
|
73
95
|
padding-right: 0;
|
|
@@ -90,21 +112,18 @@ $root-name: multi-tree-select;
|
|
|
90
112
|
|
|
91
113
|
@include m.e(panel) {
|
|
92
114
|
@include m.size using ($size) {
|
|
93
|
-
@include m.bem($root-name, content-filter) {
|
|
94
|
-
padding: calc(fn.use-var(gap, $size) / 2) fn.use-var(gap, $size);
|
|
95
|
-
}
|
|
96
115
|
@include m.bem($root-name, content-header) {
|
|
97
116
|
padding: calc(fn.use-var(gap, $size) / 2) fn.use-var(gap, $size);
|
|
98
117
|
border-bottom: fn.use-var(border);
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
@include m.bem($root-name, content-tree) {
|
|
102
|
-
padding:
|
|
121
|
+
padding: fn.use-var(gap, $size);
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
@include m.e(content-tree) {
|
|
108
|
-
max-height:
|
|
127
|
+
max-height: 240px;
|
|
109
128
|
}
|
|
110
129
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createApp, h, nextTick, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import USelect from '../select.vue'
|
|
5
|
+
|
|
6
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
7
|
+
|
|
8
|
+
function mountSelect(props: Record<string, unknown>) {
|
|
9
|
+
const host = document.createElement('div')
|
|
10
|
+
document.body.appendChild(host)
|
|
11
|
+
|
|
12
|
+
const model = ref(props.modelValue)
|
|
13
|
+
const app = createApp({
|
|
14
|
+
render() {
|
|
15
|
+
return h(USelect, {
|
|
16
|
+
...props,
|
|
17
|
+
modelValue: model.value,
|
|
18
|
+
'onUpdate:modelValue': (value: unknown) => {
|
|
19
|
+
model.value = value
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
app.mount(host)
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
host,
|
|
29
|
+
model,
|
|
30
|
+
unmount() {
|
|
31
|
+
app.unmount()
|
|
32
|
+
host.remove()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function queryOptions() {
|
|
38
|
+
return [...document.body.querySelectorAll<HTMLElement>('.u-select__option')]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function openDropdown(host: HTMLElement) {
|
|
42
|
+
const input = host.querySelector('input')!
|
|
43
|
+
// 与组件 capture 拦截一致:点击原生 input 打开面板并进入查询态
|
|
44
|
+
input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
45
|
+
await nextTick()
|
|
46
|
+
await nextTick()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function typeQuery(host: HTMLElement, text: string) {
|
|
50
|
+
const input = host.querySelector('input')!
|
|
51
|
+
input.focus()
|
|
52
|
+
// 清空查询串(查询态下 model 绑定 queryString)
|
|
53
|
+
input.value = ''
|
|
54
|
+
input.dispatchEvent(
|
|
55
|
+
new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteContentBackward' })
|
|
56
|
+
)
|
|
57
|
+
await nextTick()
|
|
58
|
+
|
|
59
|
+
input.value = text
|
|
60
|
+
input.dispatchEvent(
|
|
61
|
+
new InputEvent('input', { bubbles: true, data: text, inputType: 'insertText' })
|
|
62
|
+
)
|
|
63
|
+
await nextTick()
|
|
64
|
+
// 本地过滤防抖 200ms
|
|
65
|
+
await sleep(250)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function clickOption(option: HTMLElement) {
|
|
69
|
+
option.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
70
|
+
await nextTick()
|
|
71
|
+
await sleep(50)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('USelect', () => {
|
|
75
|
+
it('highlights the latest created option after consecutive creations', async () => {
|
|
76
|
+
const { host, model, unmount } = mountSelect({
|
|
77
|
+
filterable: true,
|
|
78
|
+
creatable: true,
|
|
79
|
+
options: [
|
|
80
|
+
{ label: '选项0', value: '0' },
|
|
81
|
+
{ label: '选项1', value: '1' }
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// 创建并选择第一个选项
|
|
87
|
+
await openDropdown(host)
|
|
88
|
+
await typeQuery(host, '新A')
|
|
89
|
+
expect(queryOptions().some((el) => el.textContent === '新A')).toBe(true)
|
|
90
|
+
await clickOption(queryOptions().find((el) => el.textContent === '新A')!)
|
|
91
|
+
expect(model.value).toBe('新A')
|
|
92
|
+
|
|
93
|
+
// 创建并选择第二个选项
|
|
94
|
+
await openDropdown(host)
|
|
95
|
+
await typeQuery(host, '新B')
|
|
96
|
+
expect(queryOptions().some((el) => el.textContent === '新B')).toBe(true)
|
|
97
|
+
await clickOption(queryOptions().find((el) => el.textContent === '新B')!)
|
|
98
|
+
expect(model.value).toBe('新B')
|
|
99
|
+
expect(host.querySelector('input')!.value).toBe('新B')
|
|
100
|
+
|
|
101
|
+
// 重新展开,高亮应落在后创建的“新B”上,而非停留在“新A”
|
|
102
|
+
await openDropdown(host)
|
|
103
|
+
const selected = queryOptions().filter((el) => el.classList.contains('is-selected'))
|
|
104
|
+
expect(selected).toHaveLength(1)
|
|
105
|
+
expect(selected[0]!.textContent).toBe('新B')
|
|
106
|
+
} finally {
|
|
107
|
+
unmount()
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
})
|