plugin-ui-for-kzt 0.0.62 → 0.0.63
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/.claude/settings.local.json +7 -0
- package/dist/components/BaseBadge/BaseBadge.vue.d.ts +2 -2
- package/dist/components/BaseButton/BaseButton.vue.d.ts +3 -7
- package/dist/components/BaseCalendar/BaseCalendar.vue.d.ts +3 -3
- package/dist/components/BaseCheckbox/BaseCheckbox.vue.d.ts +3 -7
- package/dist/components/BaseDropdown/BaseDropdown.vue.d.ts +2 -6
- package/dist/components/BaseField/BaseField.vue.d.ts +3 -3
- package/dist/components/BaseInput/BaseInput.vue.d.ts +5 -5
- package/dist/components/BaseInputCalendar/BaseInputCalendar.vue.d.ts +6 -6
- package/dist/components/BaseInputCurrency/BaseInputCurrency.vue.d.ts +6 -6
- package/dist/components/BaseInputEmail/BaseInputEmail.vue.d.ts +4 -4
- package/dist/components/BaseInputPhone/BaseInputPhone.vue.d.ts +4 -4
- package/dist/components/BaseInputWithSelector/BaseInputWithSelector.vue.d.ts +1 -1
- package/dist/components/BaseOpenedListItem/BaseOpenedListItem.vue.d.ts +2 -6
- package/dist/components/BasePagination/BasePagination.vue.d.ts +1 -1
- package/dist/components/BaseRadio/BaseRadio.vue.d.ts +3 -7
- package/dist/components/BaseSegmentedButtons/BaseSegmentedButtons.vue.d.ts +2 -6
- package/dist/components/BaseSelect/BaseSelect.vue.d.ts +4 -12
- package/dist/components/BaseSiteInput/BaseSiteInput.vue.d.ts +1 -1
- package/dist/components/BaseTag/BaseTag.vue.d.ts +1 -1
- package/dist/components/BaseTextarea/BaseTextarea.vue.d.ts +4 -4
- package/dist/components/BaseToast/BaseToast.vue.d.ts +1 -1
- package/dist/components/BaseToggle/BaseToggle.vue.d.ts +3 -7
- package/dist/components/BaseUpload/BaseUpload.vue.d.ts +6 -4
- package/dist/index.js +1 -1
- package/dist/sprite.svg +1 -1
- package/dist/store/modal.d.ts +13 -13
- package/dist/utils/viewPort.d.ts +5 -0
- package/example/App.vue +1 -0
- package/package.json +1 -1
- package/src/assets/icons/upload-2.svg +7 -0
- package/src/assets/icons/upload.svg +3 -3
- package/src/components/BaseInputPhone/README.md +78 -0
- package/src/components/BaseModal/README.md +88 -0
- package/src/components/BaseSiteInput/README.md +76 -0
- package/src/components/BaseSwiper/README.md +88 -0
- package/src/components/BaseTabs/README.md +110 -0
- package/src/components/BaseUpload/BaseUpload.vue +132 -18
- package/src/components/BaseUpload/README.md +126 -0
- package/src/types/uploadedFile.d.ts +10 -2
- package/src/utils/viewPort.ts +50 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# BaseSiteInput
|
|
2
|
+
|
|
3
|
+
Компонент для ввода URL-адреса сайта. Автоматически добавляет префикс `https://`, убирает его из отображаемого значения и валидирует формат URL в реальном времени.
|
|
4
|
+
|
|
5
|
+
## Использование
|
|
6
|
+
|
|
7
|
+
```vue
|
|
8
|
+
<template>
|
|
9
|
+
<BaseSiteInput
|
|
10
|
+
v-model="siteUrl"
|
|
11
|
+
placeholder="example.com"
|
|
12
|
+
size="medium"
|
|
13
|
+
/>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { ref } from 'vue';
|
|
18
|
+
const siteUrl = ref('');
|
|
19
|
+
</script>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Пропсы
|
|
23
|
+
|
|
24
|
+
| Пропс | Тип | По умолчанию | Описание |
|
|
25
|
+
|------------------|-----------|:----------------------:|-------------------------------------------------------|
|
|
26
|
+
| `modelValue` | `string` | `''` | Значение поля (v-model). Хранится с префиксом `https://` |
|
|
27
|
+
| `size` | `string` | `'medium'` | Размер: `'small'`, `'medium'`, `'large'` |
|
|
28
|
+
| `focusable` | `boolean` | `true` | Управляет фокусируемостью поля |
|
|
29
|
+
| `id` | `string` | — | HTML-атрибут `id` для поля |
|
|
30
|
+
| `error` | `boolean` | — | Внешнее состояние ошибки |
|
|
31
|
+
| `validationText` | `string` | `'Некорректный URL'` | Текст ошибки при невалидном URL |
|
|
32
|
+
|
|
33
|
+
## События
|
|
34
|
+
|
|
35
|
+
| Событие | Payload | Описание |
|
|
36
|
+
|---------------------|----------|-------------------------------------------------|
|
|
37
|
+
| `update:modelValue` | `string` | Всегда эмитирует значение с префиксом `https://` |
|
|
38
|
+
|
|
39
|
+
## Поведение
|
|
40
|
+
|
|
41
|
+
- Префикс `https://` **всегда хранится** в `modelValue`, но **не отображается** в поле ввода — пользователь видит только домен.
|
|
42
|
+
- При вводе значения `https://` автоматически добавляется, если его нет.
|
|
43
|
+
- Валидация происходит в реальном времени через `watch`. Проверяется формат домена: `example.com`, `sub.example.org/path` и т.д.
|
|
44
|
+
- Если введён некорректный URL, показывается ошибка (из `validationText` или дефолтная).
|
|
45
|
+
- Внешний пропс `error` объединяется с внутренней ошибкой валидации: если хотя бы одно из них — показывается состояние ошибки.
|
|
46
|
+
|
|
47
|
+
## Примеры
|
|
48
|
+
|
|
49
|
+
### Получение значения
|
|
50
|
+
|
|
51
|
+
```vue
|
|
52
|
+
<BaseSiteInput v-model="url" placeholder="example.com" />
|
|
53
|
+
<!-- После ввода "example.com" значение url === 'https://example.com' -->
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Кастомный текст ошибки
|
|
57
|
+
|
|
58
|
+
```vue
|
|
59
|
+
<BaseSiteInput
|
|
60
|
+
v-model="url"
|
|
61
|
+
validation-text="Введите корректный адрес сайта"
|
|
62
|
+
placeholder="example.com"
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Разные размеры
|
|
67
|
+
|
|
68
|
+
```vue
|
|
69
|
+
<BaseSiteInput v-model="url" size="small" placeholder="example.com" />
|
|
70
|
+
<BaseSiteInput v-model="url" size="medium" placeholder="example.com" />
|
|
71
|
+
<BaseSiteInput v-model="url" size="large" placeholder="example.com" />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Зависимости
|
|
75
|
+
|
|
76
|
+
- `BaseInput`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# BaseSwiper
|
|
2
|
+
|
|
3
|
+
Компонент карусели изображений с кнопками навигации, пагинацией и поддержкой автопрокрутки.
|
|
4
|
+
|
|
5
|
+
## Использование
|
|
6
|
+
|
|
7
|
+
```vue
|
|
8
|
+
<template>
|
|
9
|
+
<BaseSwiper
|
|
10
|
+
:images="images"
|
|
11
|
+
:autoplay="true"
|
|
12
|
+
:autoplay-interval="4000"
|
|
13
|
+
/>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
const images = [
|
|
18
|
+
'/img/slide1.jpg',
|
|
19
|
+
'/img/slide2.jpg',
|
|
20
|
+
'/img/slide3.jpg',
|
|
21
|
+
];
|
|
22
|
+
</script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Пропсы
|
|
26
|
+
|
|
27
|
+
| Пропс | Тип | По умолчанию | Описание |
|
|
28
|
+
|----------------------|-----------------------|:-------------------------------------:|-------------------------------------------------|
|
|
29
|
+
| `images` | `string[]` | `[]` | Массив URL изображений |
|
|
30
|
+
| `currentSlide` | `number` | `0` | Индекс текущего слайда (для внешнего управления)|
|
|
31
|
+
| `autoplay` | `boolean` | `false` | Включить автопрокрутку |
|
|
32
|
+
| `autoplayInterval` | `number` | `3000` | Интервал автопрокрутки в миллисекундах |
|
|
33
|
+
| `buttonSize` | `'medium' \| 'large'` | `'medium'` | Размер кнопок навигации |
|
|
34
|
+
| `paginationSettings` | `PaginationSettings` | `{ type: 'dots', color: 'primary' }` | Настройки пагинации |
|
|
35
|
+
|
|
36
|
+
### Тип `PaginationSettings`
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface PaginationSettings {
|
|
40
|
+
type?: PaginationType; // тип пагинации (например, 'dots')
|
|
41
|
+
color?: PaginationColor; // цвет: 'primary' | 'secondary'
|
|
42
|
+
size?: ICoreSize;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## События
|
|
47
|
+
|
|
48
|
+
| Событие | Payload | Описание |
|
|
49
|
+
|-----------------------|----------|---------------------------------------------|
|
|
50
|
+
| `update:currentSlide` | `number` | Вызывается при смене слайда, передаёт индекс |
|
|
51
|
+
|
|
52
|
+
## Поведение
|
|
53
|
+
|
|
54
|
+
- Кнопка «назад» недоступна (`disabled`) на первом слайде, кнопка «вперёд» — на последнем.
|
|
55
|
+
- При автопрокрутке слайды переключаются **циклически** (после последнего возвращается к первому).
|
|
56
|
+
- Автопрокрутка запускается при монтировании (`onMounted`) и останавливается при размонтировании (`onUnmounted`).
|
|
57
|
+
- Пагинация синхронизирована с текущим слайдом через `watch` в обе стороны — можно управлять слайдом как через кнопки, так и через пагинацию.
|
|
58
|
+
- Максимальная ширина контейнера: `640px`.
|
|
59
|
+
|
|
60
|
+
## Примеры
|
|
61
|
+
|
|
62
|
+
### С кастомной пагинацией и автопрокруткой
|
|
63
|
+
|
|
64
|
+
```vue
|
|
65
|
+
<BaseSwiper
|
|
66
|
+
:images="images"
|
|
67
|
+
:autoplay="true"
|
|
68
|
+
:autoplay-interval="5000"
|
|
69
|
+
:pagination-settings="{ type: 'dots', color: 'secondary' }"
|
|
70
|
+
button-size="large"
|
|
71
|
+
@update:current-slide="onSlideChange"
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Внешнее управление слайдом
|
|
76
|
+
|
|
77
|
+
```vue
|
|
78
|
+
<BaseSwiper
|
|
79
|
+
:images="images"
|
|
80
|
+
:current-slide="currentSlide"
|
|
81
|
+
@update:current-slide="currentSlide = $event"
|
|
82
|
+
/>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Зависимости
|
|
86
|
+
|
|
87
|
+
- `BaseIcon` (иконки `arrow-left-circle`, `arrow-right-circle`)
|
|
88
|
+
- `BasePagination`
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# BaseTabs
|
|
2
|
+
|
|
3
|
+
Компонент вкладок (табов) с поддержкой иконок, состояния `disabled` и нескольких размеров.
|
|
4
|
+
|
|
5
|
+
## Использование
|
|
6
|
+
|
|
7
|
+
```vue
|
|
8
|
+
<template>
|
|
9
|
+
<BaseTabs v-model="activeTab" :tabs="tabs">
|
|
10
|
+
<div v-if="activeTab === 0">Контент первой вкладки</div>
|
|
11
|
+
<div v-if="activeTab === 1">Контент второй вкладки</div>
|
|
12
|
+
<div v-if="activeTab === 2">Контент третьей вкладки</div>
|
|
13
|
+
</BaseTabs>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { ref } from 'vue';
|
|
18
|
+
|
|
19
|
+
const activeTab = ref(0);
|
|
20
|
+
|
|
21
|
+
const tabs = [
|
|
22
|
+
{ id: 0, title: 'Обзор' },
|
|
23
|
+
{ id: 1, title: 'Настройки' },
|
|
24
|
+
{ id: 2, title: 'История' },
|
|
25
|
+
];
|
|
26
|
+
</script>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Пропсы
|
|
30
|
+
|
|
31
|
+
| Пропс | Тип | По умолчанию | Описание |
|
|
32
|
+
|--------------|----------|:------------:|-------------------------------------------------------------------|
|
|
33
|
+
| `tabs` | `ITab[]` | — | Массив объектов вкладок |
|
|
34
|
+
| `modelValue` | `number` | `0` | ID активной вкладки (v-model) |
|
|
35
|
+
| `size` | `string` | `'medium'` | Размер: `'extra-small'`, `'small'`, `'medium'` |
|
|
36
|
+
|
|
37
|
+
### Тип `ITab`
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
interface ITab {
|
|
41
|
+
id: number;
|
|
42
|
+
title: string;
|
|
43
|
+
icon?: Component; // Vue-компонент иконки
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## События
|
|
49
|
+
|
|
50
|
+
| Событие | Payload | Описание |
|
|
51
|
+
|---------------------|----------|---------------------------------------|
|
|
52
|
+
| `update:modelValue` | `number` | Вызывается при выборе вкладки, передаёт `id` |
|
|
53
|
+
|
|
54
|
+
## Слоты
|
|
55
|
+
|
|
56
|
+
| Слот | Описание |
|
|
57
|
+
|-----------|--------------------------------------------|
|
|
58
|
+
| `default` | Контент вкладок (управляется снаружи через `v-if`) |
|
|
59
|
+
|
|
60
|
+
## Размеры
|
|
61
|
+
|
|
62
|
+
| Значение | Высота таба | Шрифт |
|
|
63
|
+
|---------------|-------------|--------------------------------|
|
|
64
|
+
| `extra-small` | 38px | `typography-text-s-medium` |
|
|
65
|
+
| `small` | 48px | `typography-text-m-medium` |
|
|
66
|
+
| `medium` | 56px | `typography-text-l-medium` |
|
|
67
|
+
|
|
68
|
+
## Поведение
|
|
69
|
+
|
|
70
|
+
- Клик по вкладке с `disabled: true` игнорируется.
|
|
71
|
+
- Активная вкладка выделяется синей нижней границей.
|
|
72
|
+
- Внешнее изменение `modelValue` через `watch` синхронизируется с внутренним состоянием.
|
|
73
|
+
- Горизонтальная линия-разделитель рисуется через псевдоэлемент `::before` на заголовке, активный таб перекрывает её через `z-index`.
|
|
74
|
+
|
|
75
|
+
## Примеры
|
|
76
|
+
|
|
77
|
+
### С иконками
|
|
78
|
+
|
|
79
|
+
```vue
|
|
80
|
+
<script setup lang="ts">
|
|
81
|
+
import IconHome from '@/icons/IconHome.vue';
|
|
82
|
+
import IconSettings from '@/icons/IconSettings.vue';
|
|
83
|
+
|
|
84
|
+
const tabs = [
|
|
85
|
+
{ id: 0, title: 'Главная', icon: IconHome },
|
|
86
|
+
{ id: 1, title: 'Настройки', icon: IconSettings },
|
|
87
|
+
];
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<BaseTabs v-model="activeTab" :tabs="tabs" size="small">
|
|
92
|
+
<div v-if="activeTab === 0">Главная</div>
|
|
93
|
+
<div v-if="activeTab === 1">Настройки</div>
|
|
94
|
+
</BaseTabs>
|
|
95
|
+
</template>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### С заблокированной вкладкой
|
|
99
|
+
|
|
100
|
+
```vue
|
|
101
|
+
const tabs = [
|
|
102
|
+
{ id: 0, title: 'Активная' },
|
|
103
|
+
{ id: 1, title: 'Заблокированная', disabled: true },
|
|
104
|
+
{ id: 2, title: 'Ещё одна' },
|
|
105
|
+
];
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Зависимости
|
|
109
|
+
|
|
110
|
+
- `BaseButton`
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
:accept="acceptedFormats.join(',')"
|
|
10
10
|
/>
|
|
11
11
|
<div
|
|
12
|
+
v-if="!isMobile || !multiple || uploadedFiles.length === 0"
|
|
12
13
|
class="base-file-upload__wrapper"
|
|
13
14
|
:class="{ '--is-drag-over': isDragOver }"
|
|
14
15
|
@click="triggerFileInput"
|
|
@@ -20,16 +21,32 @@
|
|
|
20
21
|
class="base-file-upload__block"
|
|
21
22
|
>
|
|
22
23
|
<base-icon
|
|
24
|
+
v-if="!isMobile"
|
|
23
25
|
name="upload"
|
|
24
26
|
class="base-file-upload__icon-upload"
|
|
25
27
|
size="large"
|
|
26
28
|
/>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
<base-icon
|
|
30
|
+
v-else
|
|
31
|
+
name="upload-2"
|
|
32
|
+
class="base-file-upload__icon-upload"
|
|
33
|
+
size="medium"
|
|
34
|
+
/>
|
|
35
|
+
<div
|
|
36
|
+
class="base-file-upload__content"
|
|
37
|
+
>
|
|
38
|
+
<p class="base-file-upload__title">
|
|
39
|
+
<slot name="title">{{ title }}</slot>
|
|
40
|
+
<base-icon
|
|
41
|
+
v-if="isMobile"
|
|
42
|
+
name="upload"
|
|
43
|
+
size="medium"
|
|
44
|
+
/>
|
|
45
|
+
</p>
|
|
46
|
+
<p class="base-file-upload__description">
|
|
47
|
+
<slot name="description">{{ subtitle }}</slot>
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
33
50
|
</div>
|
|
34
51
|
<div v-else>
|
|
35
52
|
<div
|
|
@@ -47,7 +64,7 @@
|
|
|
47
64
|
class="base-file-upload__file-item-button"
|
|
48
65
|
size="custom"
|
|
49
66
|
color="custom"
|
|
50
|
-
@click="downloadFile(file
|
|
67
|
+
@click="downloadFile(file)"
|
|
51
68
|
>
|
|
52
69
|
<base-icon
|
|
53
70
|
name="export"
|
|
@@ -109,12 +126,13 @@
|
|
|
109
126
|
@drop="handleDrop"
|
|
110
127
|
>
|
|
111
128
|
<base-icon name="gallery" size="medium" class="base-file-upload__placeholder-icon"/>
|
|
112
|
-
<p class="base-file-upload__placeholder-text"><span
|
|
129
|
+
<p class="base-file-upload__placeholder-text"><span>{{ texts?.upload }}</span> <br/> {{ texts?.photo }}</p>
|
|
113
130
|
</div>
|
|
114
131
|
</div>
|
|
115
132
|
|
|
116
133
|
<!-- Остальные файлы в колонках -->
|
|
117
134
|
<div v-if="otherFiles.length > 0 && multiple" class="base-file-upload__file-list">
|
|
135
|
+
<div class="base-file-upload__list-title"> {{ texts?.documents }}</div>
|
|
118
136
|
<div
|
|
119
137
|
v-for="(file, index) in otherFiles"
|
|
120
138
|
:key="index"
|
|
@@ -129,7 +147,7 @@
|
|
|
129
147
|
class="base-file-upload__file-item-button"
|
|
130
148
|
size="custom"
|
|
131
149
|
color="custom"
|
|
132
|
-
@click="downloadFile(file
|
|
150
|
+
@click="downloadFile(file)"
|
|
133
151
|
>
|
|
134
152
|
<base-icon
|
|
135
153
|
name="export"
|
|
@@ -151,6 +169,16 @@
|
|
|
151
169
|
</base-button>
|
|
152
170
|
</div>
|
|
153
171
|
</div>
|
|
172
|
+
<div v-if="isMobile && props.multiple"
|
|
173
|
+
class="base-file-upload__more"
|
|
174
|
+
@click="triggerFileInput">
|
|
175
|
+
{{ texts?.loadMore }}
|
|
176
|
+
<base-icon
|
|
177
|
+
name="upload"
|
|
178
|
+
class="base-file-upload__icon-upload"
|
|
179
|
+
size="medium"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
154
182
|
</div>
|
|
155
183
|
</div>
|
|
156
184
|
|
|
@@ -166,6 +194,7 @@ import { useModal } from '../../composables/useModal';
|
|
|
166
194
|
import type { UploadedFile, IpropsUpload } from '../../types/uploadedFile.d';
|
|
167
195
|
import ImageModal from './ImageModal.vue';
|
|
168
196
|
import CropModal from './CropModal.vue';
|
|
197
|
+
import { isMobile } from "../../utils/viewPort";
|
|
169
198
|
|
|
170
199
|
const props = withDefaults( defineProps<IpropsUpload>(), {
|
|
171
200
|
multiple: true,
|
|
@@ -182,7 +211,7 @@ const errorMessage = ref<string | null>( null );
|
|
|
182
211
|
const isDragOver = ref( false );
|
|
183
212
|
const modal = useModal();
|
|
184
213
|
|
|
185
|
-
const mediaExtensions = ['.jpg', '.jpeg', '.png'
|
|
214
|
+
const mediaExtensions = ['.jpg', '.jpeg', '.png'];
|
|
186
215
|
const mediaFiles = computed( () =>
|
|
187
216
|
uploadedFiles.value.filter( ( file ) =>
|
|
188
217
|
mediaExtensions.includes( `.${file.name.split( '.' ).pop()?.toLowerCase() || ''}` )
|
|
@@ -232,18 +261,27 @@ const processFiles = ( files: File[] ) => {
|
|
|
232
261
|
uploadedFiles.value = [];
|
|
233
262
|
}
|
|
234
263
|
|
|
264
|
+
const errors: string[] = [];
|
|
265
|
+
const normalizedFormats = props.acceptedFormats.map( ( f: string ) => f.toLowerCase() );
|
|
266
|
+
|
|
235
267
|
filesToProcess.forEach( ( file ) => {
|
|
236
268
|
const fileExtension = `.${file.name.split( '.' ).pop()?.toLowerCase() || ''}`;
|
|
237
|
-
|
|
238
|
-
|
|
269
|
+
|
|
270
|
+
if ( !normalizedFormats.includes( fileExtension ) ) {
|
|
271
|
+
errors.push( `Недопустимый формат файла: ${file.name}. Допустимый: ${props.acceptedFormats.join( ', ' )}.` );
|
|
239
272
|
return;
|
|
240
273
|
}
|
|
241
274
|
|
|
242
275
|
if ( file.size > props.maxFileSize ) {
|
|
243
|
-
|
|
276
|
+
errors.push( `Файл ${file.name} превышает максимальный размер ${formatFileSize( props.maxFileSize )}.` );
|
|
244
277
|
return;
|
|
245
278
|
}
|
|
246
279
|
|
|
280
|
+
const isDuplicate = uploadedFiles.value.some(
|
|
281
|
+
( f: UploadedFile ) => f.name === file.name && f.size === file.size
|
|
282
|
+
);
|
|
283
|
+
if ( isDuplicate ) return;
|
|
284
|
+
|
|
247
285
|
if ( props.enableCrop && isImageFile( file ) ) {
|
|
248
286
|
openCropModal( file );
|
|
249
287
|
} else {
|
|
@@ -252,6 +290,10 @@ const processFiles = ( files: File[] ) => {
|
|
|
252
290
|
}
|
|
253
291
|
} );
|
|
254
292
|
|
|
293
|
+
if ( errors.length > 0 ) {
|
|
294
|
+
errorMessage.value = errors.join( ' ' );
|
|
295
|
+
}
|
|
296
|
+
|
|
255
297
|
isUploading.value = false;
|
|
256
298
|
};
|
|
257
299
|
|
|
@@ -306,7 +348,7 @@ const removeFile = ( index: number, isMedia: boolean ) => {
|
|
|
306
348
|
}
|
|
307
349
|
};
|
|
308
350
|
|
|
309
|
-
const downloadFile = ( file: { file: File, name: string }
|
|
351
|
+
const downloadFile = ( file: { file: File, name: string } ) => {
|
|
310
352
|
if ( file?.file?.type ) {
|
|
311
353
|
const url = URL.createObjectURL( file.file );
|
|
312
354
|
const link = document.createElement( 'a' );
|
|
@@ -387,12 +429,18 @@ watch( () => props.files,
|
|
|
387
429
|
color: var(--primary-blue);
|
|
388
430
|
}
|
|
389
431
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@media #{$mobile-max-767} {
|
|
435
|
+
padding: 12px;
|
|
436
|
+
align-items: start;
|
|
393
437
|
}
|
|
394
438
|
}
|
|
395
439
|
|
|
440
|
+
&__icon-upload {
|
|
441
|
+
color: var(--primary-blue);
|
|
442
|
+
}
|
|
443
|
+
|
|
396
444
|
&__block {
|
|
397
445
|
margin: 0 auto;
|
|
398
446
|
text-align: center;
|
|
@@ -400,16 +448,45 @@ watch( () => props.files,
|
|
|
400
448
|
svg {
|
|
401
449
|
margin: 0 auto;
|
|
402
450
|
}
|
|
451
|
+
|
|
452
|
+
@media #{$mobile-max-767} {
|
|
453
|
+
display: flex;
|
|
454
|
+
gap: 12px;
|
|
455
|
+
margin: 0;
|
|
456
|
+
svg {
|
|
457
|
+
margin: 0;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
&__content {
|
|
463
|
+
@media #{$mobile-max-767} {
|
|
464
|
+
text-align: start;
|
|
465
|
+
}
|
|
403
466
|
}
|
|
404
467
|
|
|
405
468
|
&__title {
|
|
406
|
-
color: var(--primary-
|
|
469
|
+
color: var(--primary-blue);
|
|
407
470
|
font: var(--typography-text-m-medium);
|
|
471
|
+
|
|
472
|
+
@media #{$mobile-max-767} {
|
|
473
|
+
font: var(--typography-text-s-bold);
|
|
474
|
+
display: flex;
|
|
475
|
+
color: var(--primary-blue);
|
|
476
|
+
gap: 2px;
|
|
477
|
+
svg {
|
|
478
|
+
margin: 0;
|
|
479
|
+
color: var(--primary-blue);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
408
482
|
}
|
|
409
483
|
|
|
410
484
|
&__description {
|
|
411
485
|
color: var(--primary-text-tertiary);
|
|
412
486
|
font: var(--typography-text-s-regular);
|
|
487
|
+
@media #{$mobile-max-767} {
|
|
488
|
+
font: var(--typography-text-xs-bold);
|
|
489
|
+
}
|
|
413
490
|
}
|
|
414
491
|
|
|
415
492
|
&__input {
|
|
@@ -466,6 +543,23 @@ watch( () => props.files,
|
|
|
466
543
|
flex-direction: column;
|
|
467
544
|
}
|
|
468
545
|
|
|
546
|
+
&__list-title {
|
|
547
|
+
font: var(--typography-text-s-medium);
|
|
548
|
+
margin: 12px 0;
|
|
549
|
+
color: var(--primary-black);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
&__more {
|
|
554
|
+
display: flex;
|
|
555
|
+
justify-content: end;
|
|
556
|
+
align-items: center;
|
|
557
|
+
gap: 8px;
|
|
558
|
+
padding: 14px 4px;
|
|
559
|
+
cursor: pointer;
|
|
560
|
+
color: var(--primary-blue);
|
|
561
|
+
}
|
|
562
|
+
|
|
469
563
|
&__file-item {
|
|
470
564
|
display: flex;
|
|
471
565
|
justify-content: space-between;
|
|
@@ -473,10 +567,15 @@ watch( () => props.files,
|
|
|
473
567
|
padding: 8px 12px;
|
|
474
568
|
border-bottom: 1px solid var(--primary-black-200);
|
|
475
569
|
|
|
570
|
+
|
|
476
571
|
&:last-child {
|
|
477
572
|
border-bottom: none;
|
|
478
573
|
}
|
|
479
574
|
|
|
575
|
+
@media #{$mobile-max-767} {
|
|
576
|
+
padding: 8px 0;
|
|
577
|
+
}
|
|
578
|
+
|
|
480
579
|
@include hover {
|
|
481
580
|
.base-file-upload__file-item-wrapper {
|
|
482
581
|
color: var(--primary-blue);
|
|
@@ -510,6 +609,12 @@ watch( () => props.files,
|
|
|
510
609
|
gap: 8px;
|
|
511
610
|
color: var(--primary-text-primary);
|
|
512
611
|
font: var(--typography-text-m-regular);
|
|
612
|
+
|
|
613
|
+
@media #{$mobile-max-767} {
|
|
614
|
+
span {
|
|
615
|
+
font: var(--typography-text-xs-regular);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
513
618
|
}
|
|
514
619
|
|
|
515
620
|
&__file-item-button {
|
|
@@ -568,5 +673,14 @@ watch( () => props.files,
|
|
|
568
673
|
color: var(--primary-blue);
|
|
569
674
|
}
|
|
570
675
|
}
|
|
676
|
+
|
|
677
|
+
&__files {
|
|
678
|
+
@media #{$mobile-max-767} {
|
|
679
|
+
padding: 0 8px;
|
|
680
|
+
border-radius: var(--corner-radius-m);
|
|
681
|
+
background: var(--bg-light);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
571
685
|
}
|
|
572
686
|
</style>
|