plugin-ui-for-kzt 0.0.28 → 0.0.30
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/BaseCheckbox/BaseCheckbox.vue.d.ts +2 -2
- package/dist/components/BaseDropdown/BaseDropdown.vue.d.ts +1 -1
- package/dist/components/BaseInput/BaseInput.vue.d.ts +3 -3
- package/dist/components/BaseInputCalendar/BaseInputCalendar.vue.d.ts +4 -4
- package/dist/components/BaseInputCurrency/BaseInputCurrency.vue.d.ts +3 -3
- package/dist/components/BaseInputEmail/BaseInputEmail.vue.d.ts +3 -3
- package/dist/components/BaseInputPhone/BaseInputPhone.vue.d.ts +3 -3
- package/dist/components/BaseRadio/BaseRadio.vue.d.ts +2 -2
- package/dist/components/BaseSelect/BaseSelect.vue.d.ts +16 -3
- package/dist/components/BaseTable/BaseTable.vue.d.ts +14 -1
- package/dist/components/BaseTextarea/BaseTextarea.vue.d.ts +3 -3
- package/dist/components/BaseToggle/BaseToggle.vue.d.ts +2 -2
- package/dist/components/BaseTooltip/BaseTooltip.vue.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/sprite.svg +1 -1
- package/example/App.vue +271 -7
- package/package.json +1 -1
- package/src/assets/icons/search.svg +4 -0
- package/src/components/BaseButton/BaseButton.vue +20 -20
- package/src/components/BaseSelect/BaseSelect.vue +164 -10
- package/src/components/BaseSelect/README.md +110 -1
- package/src/components/BaseTable/BaseTable.vue +278 -26
- package/src/components/BaseTable/README.md +96 -1
- package/src/components/BaseTooltip/BaseTooltip.vue +9 -5
- package/src/components/BaseUpload/BaseUpload.vue +97 -25
- package/src/types/input.d.ts +1 -0
- package/src/types/table.d.ts +6 -0
- package/src/vue-virtual-scroller.d.ts +4 -0
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
:disabled="actualDisabled"
|
|
9
9
|
>
|
|
10
10
|
<template #top>
|
|
11
|
+
<!-- Старый header для дефолтного поведения -->
|
|
11
12
|
<div
|
|
13
|
+
v-if="!searchable"
|
|
12
14
|
:data-error="Boolean(error)"
|
|
13
15
|
:data-disabled="disabled"
|
|
14
16
|
class="base-select__header"
|
|
@@ -21,12 +23,9 @@
|
|
|
21
23
|
<slot name="headerIcon" />
|
|
22
24
|
</div>
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
>
|
|
28
|
-
{{ actualOption.name }}
|
|
29
|
-
</div>
|
|
26
|
+
<div v-if="actualOption" class="base-select__header_value">
|
|
27
|
+
{{ actualOption.name }}
|
|
28
|
+
</div>
|
|
30
29
|
|
|
31
30
|
<div
|
|
32
31
|
v-else
|
|
@@ -48,15 +47,45 @@
|
|
|
48
47
|
</div>
|
|
49
48
|
</div>
|
|
50
49
|
</div>
|
|
50
|
+
|
|
51
|
+
<BaseInput
|
|
52
|
+
v-else
|
|
53
|
+
:id="id"
|
|
54
|
+
:model-value="displayValue || ''"
|
|
55
|
+
:placeholder="placeholder"
|
|
56
|
+
:disabled="disabled"
|
|
57
|
+
:readonly="readonly"
|
|
58
|
+
:size="size"
|
|
59
|
+
:error="Boolean(error)"
|
|
60
|
+
class="base-select__input"
|
|
61
|
+
@input="handleInputChange"
|
|
62
|
+
>
|
|
63
|
+
<template #left-icon>
|
|
64
|
+
<base-icon
|
|
65
|
+
name="search"
|
|
66
|
+
:size="size"
|
|
67
|
+
/>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<template #right-icon>
|
|
71
|
+
<base-icon
|
|
72
|
+
v-if="!readonly"
|
|
73
|
+
:name="actualOption ? 'close' : 'arrow-down'"
|
|
74
|
+
:size="size"
|
|
75
|
+
class="base-select__toggle-icon"
|
|
76
|
+
@click="handleIconClick"
|
|
77
|
+
/>
|
|
78
|
+
</template>
|
|
79
|
+
</BaseInput>
|
|
51
80
|
</template>
|
|
52
81
|
|
|
53
82
|
<template #dropdown>
|
|
54
83
|
<div
|
|
55
|
-
v-if="(
|
|
84
|
+
v-if="(filteredOptions ?? []).length"
|
|
56
85
|
class="base-select__dropdown"
|
|
57
|
-
>
|
|
86
|
+
>
|
|
58
87
|
<dynamic-scroller
|
|
59
|
-
:items="
|
|
88
|
+
:items="filteredOptions as ICoreSelectBaseProps[]"
|
|
60
89
|
:min-item-size="36"
|
|
61
90
|
key-field="id"
|
|
62
91
|
class="base-select__list"
|
|
@@ -86,6 +115,9 @@
|
|
|
86
115
|
</template>
|
|
87
116
|
</dynamic-scroller>
|
|
88
117
|
</div>
|
|
118
|
+
<div class="base-select__empty">
|
|
119
|
+
<slot name="empty" />
|
|
120
|
+
</div>
|
|
89
121
|
</template>
|
|
90
122
|
</base-dropdown>
|
|
91
123
|
</div>
|
|
@@ -99,6 +131,7 @@ import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
|
|
|
99
131
|
import type { ICoreSelectProps, TSelectValue, ICoreSelectBaseProps, ICoreSelectOption, ISelectSlotProps } from '../../types/input';
|
|
100
132
|
import BaseDropdown from '../BaseDropdown/BaseDropdown.vue';
|
|
101
133
|
import BaseIcon from '../BaseIcon/BaseIcon.vue';
|
|
134
|
+
import BaseInput from '../BaseInput/BaseInput.vue';
|
|
102
135
|
import BaseOpenedListItem from '../BaseOpenedListItem/BaseOpenedListItem.vue';
|
|
103
136
|
import { useKitSize } from '../../composables/kit/size';
|
|
104
137
|
import { useKitState } from '../../composables/kit/state';
|
|
@@ -109,18 +142,31 @@ const props = withDefaults(defineProps<ICoreSelectProps & {
|
|
|
109
142
|
}>(), {
|
|
110
143
|
options: () => [],
|
|
111
144
|
size: 'medium',
|
|
145
|
+
searchable: false,
|
|
112
146
|
});
|
|
113
147
|
|
|
114
148
|
const emit = defineEmits<{
|
|
115
149
|
(e: 'update:modelValue', value: TSelectValue): void
|
|
116
150
|
(e: 'change', value: TSelectValue): void
|
|
151
|
+
(e: 'error'): void
|
|
152
|
+
(e: 'search', query: string): void
|
|
153
|
+
(e: 'open'): void
|
|
154
|
+
(e: 'clear'): void
|
|
117
155
|
}>();
|
|
118
156
|
|
|
119
157
|
const actualValue = ref<TSelectValue>(props.modelValue ?? '');
|
|
158
|
+
const searchQuery = ref<string>('');
|
|
120
159
|
const actualOption = computed(() =>
|
|
121
160
|
props.options?.find((item: ICoreSelectOption) => item?.id === actualValue.value) || null
|
|
122
161
|
);
|
|
123
162
|
|
|
163
|
+
const displayValue = computed(() => {
|
|
164
|
+
if (actualOption.value) {
|
|
165
|
+
return actualOption.value.name;
|
|
166
|
+
}
|
|
167
|
+
return searchQuery.value;
|
|
168
|
+
});
|
|
169
|
+
|
|
124
170
|
watch(() => props.modelValue, (val) => {
|
|
125
171
|
actualValue.value = val ?? '';
|
|
126
172
|
}, { immediate: true });
|
|
@@ -129,10 +175,72 @@ function handleInput(value: TSelectValue) {
|
|
|
129
175
|
actualValue.value = value;
|
|
130
176
|
emit('update:modelValue', value);
|
|
131
177
|
emit('change', value);
|
|
178
|
+
dropdownVisible.value = false;
|
|
179
|
+
searchQuery.value = '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleInputChange(event: Event) {
|
|
183
|
+
const target = event.target as HTMLInputElement;
|
|
184
|
+
const inputValue = target.value;
|
|
185
|
+
searchQuery.value = inputValue;
|
|
186
|
+
|
|
187
|
+
emit('search', inputValue);
|
|
188
|
+
|
|
189
|
+
if (inputValue) {
|
|
190
|
+
const exactMatch = props.options?.find((option: ICoreSelectOption) =>
|
|
191
|
+
option.name.toLowerCase() === inputValue.toLowerCase()
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (exactMatch) {
|
|
195
|
+
handleInput(exactMatch.id);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!dropdownVisible.value) {
|
|
200
|
+
dropdownVisible.value = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function handleIconClick(event: Event) {
|
|
206
|
+
event.stopPropagation();
|
|
207
|
+
if (actualOption.value) {
|
|
208
|
+
handleInput('');
|
|
209
|
+
emit('clear');
|
|
210
|
+
} else {
|
|
211
|
+
dropdownVisible.value = !dropdownVisible.value;
|
|
212
|
+
}
|
|
132
213
|
}
|
|
133
214
|
|
|
134
215
|
const actualDisabled = computed(() => props.disabled || !props.options?.length);
|
|
135
216
|
const dropdownVisible = ref(false);
|
|
217
|
+
|
|
218
|
+
const filteredOptions = computed(() => {
|
|
219
|
+
if (!props.searchable || !searchQuery.value.trim()) {
|
|
220
|
+
return props.options || [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const query = searchQuery.value.toLowerCase().trim();
|
|
224
|
+
return (props.options || []).filter((option: ICoreSelectOption) =>
|
|
225
|
+
option.name.toLowerCase().includes(query)
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
watch(() => props.error, (error) => {
|
|
230
|
+
console.log('ERROR', error);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
watch(dropdownVisible, (isVisible) => {
|
|
234
|
+
if (isVisible) {
|
|
235
|
+
emit('open');
|
|
236
|
+
}
|
|
237
|
+
if (!isVisible) {
|
|
238
|
+
if (searchQuery.value && !actualOption.value) {
|
|
239
|
+
emit('error');
|
|
240
|
+
}
|
|
241
|
+
searchQuery.value = '';
|
|
242
|
+
}
|
|
243
|
+
});
|
|
136
244
|
const { sizeClassList } = useKitSize(props);
|
|
137
245
|
const { stateClassList } = useKitState(props);
|
|
138
246
|
const { styleClassList } = useKitStyle(props);
|
|
@@ -162,6 +270,7 @@ defineSlots<{
|
|
|
162
270
|
iconItem(props: { item: ICoreSelectBaseProps }): any;
|
|
163
271
|
header(props: { value: ICoreSelectProps['options'] }): any;
|
|
164
272
|
headerIcon(): any;
|
|
273
|
+
empty(): any;
|
|
165
274
|
}>();
|
|
166
275
|
</script>
|
|
167
276
|
|
|
@@ -185,6 +294,10 @@ defineSlots<{
|
|
|
185
294
|
border: 1px solid var(--primary-black-300);
|
|
186
295
|
outline: 4px solid var(--effects-primary-focus);
|
|
187
296
|
}
|
|
297
|
+
|
|
298
|
+
&__toggle-icon {
|
|
299
|
+
transform: rotate(180deg);
|
|
300
|
+
}
|
|
188
301
|
}
|
|
189
302
|
|
|
190
303
|
.dropdown__dropdown {
|
|
@@ -202,6 +315,10 @@ defineSlots<{
|
|
|
202
315
|
#{$select}__header {
|
|
203
316
|
border: 1px solid var(--error-red-light-01);
|
|
204
317
|
}
|
|
318
|
+
|
|
319
|
+
#{$select}__toggle-icon {
|
|
320
|
+
color: var(--error-red);
|
|
321
|
+
}
|
|
205
322
|
}
|
|
206
323
|
|
|
207
324
|
&__wrapper {
|
|
@@ -225,6 +342,8 @@ defineSlots<{
|
|
|
225
342
|
|
|
226
343
|
&__header_value {
|
|
227
344
|
color: var(--primary-text-primary);
|
|
345
|
+
|
|
346
|
+
@include text-clamp(1);
|
|
228
347
|
}
|
|
229
348
|
|
|
230
349
|
&__placeholder {
|
|
@@ -247,6 +366,25 @@ defineSlots<{
|
|
|
247
366
|
transform: translate3d(0, -50%, 0);
|
|
248
367
|
}
|
|
249
368
|
|
|
369
|
+
&__input {
|
|
370
|
+
cursor: pointer;
|
|
371
|
+
|
|
372
|
+
@include text-clamp(1);
|
|
373
|
+
|
|
374
|
+
&.--is-readonly {
|
|
375
|
+
cursor: default;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
&__toggle-icon {
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
transition: transform var(--transition);
|
|
382
|
+
|
|
383
|
+
&:hover {
|
|
384
|
+
opacity: 0.7;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
250
388
|
&__dropdown {
|
|
251
389
|
width: 100%;
|
|
252
390
|
height: 100%;
|
|
@@ -266,7 +404,7 @@ defineSlots<{
|
|
|
266
404
|
font: var(--typography-text-m-regular);
|
|
267
405
|
}
|
|
268
406
|
|
|
269
|
-
&__dropdown {
|
|
407
|
+
&__dropdown, &__input {
|
|
270
408
|
border-radius: var(--corner-radius-s);
|
|
271
409
|
}
|
|
272
410
|
|
|
@@ -289,6 +427,10 @@ defineSlots<{
|
|
|
289
427
|
font: var(--typography-text-m-regular);
|
|
290
428
|
}
|
|
291
429
|
|
|
430
|
+
&__input {
|
|
431
|
+
border-radius: var(--corner-radius-m);
|
|
432
|
+
}
|
|
433
|
+
|
|
292
434
|
&__header {
|
|
293
435
|
height: 48px;
|
|
294
436
|
padding: var(--spacing-m) var(--spacing-2l);
|
|
@@ -307,6 +449,10 @@ defineSlots<{
|
|
|
307
449
|
font: var(--typography-text-l-regular);
|
|
308
450
|
}
|
|
309
451
|
|
|
452
|
+
&__input {
|
|
453
|
+
border-radius: var(--corner-radius-l);
|
|
454
|
+
}
|
|
455
|
+
|
|
310
456
|
&__header {
|
|
311
457
|
height: 56px;
|
|
312
458
|
padding: var(--spacing-m) var(--spacing-l);
|
|
@@ -328,6 +474,10 @@ defineSlots<{
|
|
|
328
474
|
&__arrow {
|
|
329
475
|
color: var(--ui-colors-input-icon-disabled);
|
|
330
476
|
}
|
|
477
|
+
|
|
478
|
+
&__toggle-icon {
|
|
479
|
+
color: var(--ui-colors-input-icon-disabled);
|
|
480
|
+
}
|
|
331
481
|
}
|
|
332
482
|
}
|
|
333
483
|
|
|
@@ -336,6 +486,10 @@ defineSlots<{
|
|
|
336
486
|
&__header {
|
|
337
487
|
pointer-events: none;
|
|
338
488
|
}
|
|
489
|
+
|
|
490
|
+
&__input {
|
|
491
|
+
pointer-events: none;
|
|
492
|
+
}
|
|
339
493
|
}
|
|
340
494
|
}
|
|
341
495
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
### ✅
|
|
7
|
+
### ✅ Примеры использования
|
|
8
8
|
|
|
9
|
+
#### Базовый пример
|
|
9
10
|
```vue
|
|
10
11
|
<base-select
|
|
11
12
|
v-model="selected"
|
|
@@ -21,6 +22,99 @@
|
|
|
21
22
|
/>
|
|
22
23
|
```
|
|
23
24
|
|
|
25
|
+
#### Server-side поиск (рекомендуемый подход)
|
|
26
|
+
```vue
|
|
27
|
+
<template>
|
|
28
|
+
<base-select
|
|
29
|
+
id="employee-select"
|
|
30
|
+
v-model="selectedEmployee"
|
|
31
|
+
:options="employeeOptions"
|
|
32
|
+
searchable
|
|
33
|
+
placeholder="Найти сотрудника..."
|
|
34
|
+
label="Выберите сотрудника"
|
|
35
|
+
:error="employeeError"
|
|
36
|
+
@search="handleEmployeeSearch"
|
|
37
|
+
@open="handleEmployeeOpen"
|
|
38
|
+
@clear="handleEmployeeClear"
|
|
39
|
+
@error="handleSearchError"
|
|
40
|
+
/>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup>
|
|
44
|
+
import { ref } from 'vue'
|
|
45
|
+
import axios from 'axios'
|
|
46
|
+
|
|
47
|
+
const selectedEmployee = ref('')
|
|
48
|
+
const employeeOptions = ref([])
|
|
49
|
+
const employeeError = ref('')
|
|
50
|
+
const searchTimeout = ref(null)
|
|
51
|
+
|
|
52
|
+
// Дебаунс поиска для оптимизации запросов
|
|
53
|
+
const debounceSearch = (callback, delay) => {
|
|
54
|
+
if (searchTimeout.value) {
|
|
55
|
+
clearTimeout(searchTimeout.value)
|
|
56
|
+
}
|
|
57
|
+
searchTimeout.value = setTimeout(callback, delay)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Загрузка сотрудников с сервера
|
|
61
|
+
const fetchEmployees = async (query = '') => {
|
|
62
|
+
try {
|
|
63
|
+
const params = {
|
|
64
|
+
limit: 100,
|
|
65
|
+
skip: 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (query?.trim()) {
|
|
69
|
+
params.name = query.trim()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const response = await axios.get('/api/employees', { params })
|
|
73
|
+
const employees = response.data.content || []
|
|
74
|
+
|
|
75
|
+
employeeOptions.value = employees.map(item => ({
|
|
76
|
+
id: item.employeeId,
|
|
77
|
+
name: `${item.lastName} ${item.firstName} ${item.middleName}`.trim(),
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
employeeError.value = ''
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Failed to fetch employees:', error)
|
|
83
|
+
employeeOptions.value = []
|
|
84
|
+
employeeError.value = 'Ошибка загрузки данных'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Обработчик поиска с дебаунсом
|
|
89
|
+
const handleEmployeeSearch = (query) => {
|
|
90
|
+
debounceSearch(() => {
|
|
91
|
+
if (query.length >= 2 || query.length === 0) {
|
|
92
|
+
fetchEmployees(query)
|
|
93
|
+
}
|
|
94
|
+
}, 700) // Дебаунс 700мс
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Загрузка при открытии если данных нет
|
|
98
|
+
const handleEmployeeOpen = () => {
|
|
99
|
+
if (employeeOptions.value.length === 0) {
|
|
100
|
+
fetchEmployees()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Очистка выбора
|
|
105
|
+
const handleEmployeeClear = () => {
|
|
106
|
+
selectedEmployee.value = ''
|
|
107
|
+
employeeError.value = ''
|
|
108
|
+
fetchEmployees() // Загружаем исходный список
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Обработка ошибок поиска
|
|
112
|
+
const handleSearchError = () => {
|
|
113
|
+
employeeError.value = 'Элемент не найден'
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
```
|
|
117
|
+
|
|
24
118
|
---
|
|
25
119
|
|
|
26
120
|
### ⚙️ Пропсы
|
|
@@ -55,6 +149,9 @@
|
|
|
55
149
|
- `size?: 'small' | 'medium' | 'large'`
|
|
56
150
|
Управляет размерами шрифта, паддингов и иконок.
|
|
57
151
|
|
|
152
|
+
- `searchable?: boolean`
|
|
153
|
+
**Включает режим поиска. При `true` заменяет обычный заголовок на поле ввода для поиска. Идеально подходит для server-side поиска больших наборов данных.
|
|
154
|
+
|
|
58
155
|
- `multiple?: boolean`
|
|
59
156
|
Поддержка множественного выбора (не реализовано в текущем компоненте, зарезервировано).
|
|
60
157
|
|
|
@@ -71,6 +168,18 @@
|
|
|
71
168
|
- `change`
|
|
72
169
|
Также эмитится при выборе. Полезно для побочных эффектов.
|
|
73
170
|
|
|
171
|
+
- `search`
|
|
172
|
+
Эмитится при вводе в поисковое поле (только при `searchable: true`). Возвращает строку поискового запроса. Используется для server-side поиска.
|
|
173
|
+
|
|
174
|
+
- `open`
|
|
175
|
+
Эмитится при открытии выпадающего меню. Полезно для первоначальной загрузки данных при server-side поиске.
|
|
176
|
+
|
|
177
|
+
- `clear`
|
|
178
|
+
Эмитится при очистке выбранного значения через иконку крестика (только при `searchable: true`).
|
|
179
|
+
|
|
180
|
+
- `error`
|
|
181
|
+
Эмитится когда пользователь вводит текст, но не выбирает ни одну опцию из списка. Полезно для валидации поиска.
|
|
182
|
+
|
|
74
183
|
---
|
|
75
184
|
|
|
76
185
|
### 🧩 Слоты
|