plugin-ui-for-kzt 0.0.17 → 0.0.18
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/BaseBadge/BaseBadge.vue.d.ts +49 -0
- package/dist/components/BaseBadge/BaseBadgeGroup.vue.d.ts +30 -0
- package/dist/components/BaseButton/BaseButton.vue.d.ts +1 -1
- package/dist/components/BaseCalendar/BaseCalendar.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox/BaseCheckbox.vue.d.ts +1 -1
- package/dist/components/BaseChips/BaseChips.vue.d.ts +1 -1
- package/dist/components/BaseDropdown/BaseDropdown.vue.d.ts +1 -1
- package/dist/components/BaseInput/BaseInput.vue.d.ts +2 -2
- package/dist/components/BaseInputCalendar/BaseInputCalendar.vue.d.ts +3 -3
- package/dist/components/BaseInputCurrency/BaseInputCurrency.vue.d.ts +2 -2
- package/dist/components/BaseInputEmail/BaseInputEmail.vue.d.ts +2 -2
- package/dist/components/BaseInputPhone/BaseInputPhone.vue.d.ts +2 -2
- package/dist/components/BaseModal/BaseModal.vue.d.ts +4 -0
- package/dist/components/BaseOpenedListItem/BaseOpenedListItem.vue.d.ts +1 -1
- package/dist/components/BasePagination/BasePagination.vue.d.ts +1 -1
- package/dist/components/BaseRadio/BaseRadio.vue.d.ts +1 -1
- package/dist/components/BaseSegmentedButtons/BaseSegmentedButtons.vue.d.ts +2 -2
- package/dist/components/BaseSelect/BaseSelect.vue.d.ts +2 -2
- package/dist/components/BaseTag/BaseTag.vue.d.ts +61 -0
- package/dist/components/BaseTextarea/BaseTextarea.vue.d.ts +1 -1
- package/dist/components/BaseToggle/BaseToggle.vue.d.ts +1 -1
- package/dist/components/BaseTooltip/BaseTooltip.vue.d.ts +1 -1
- package/dist/components/BaseUpload/BaseUpload.vue.d.ts +31 -0
- package/dist/components/{Modal/Modal.vue.d.ts → BaseUpload/ImageModal.vue.d.ts} +2 -10
- package/dist/components/Toaster/Toaster.vue.d.ts +1 -1
- package/dist/composables/useModal.d.ts +7 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1 -1
- package/dist/plugins/modalPlugin.d.ts +0 -13
- package/dist/sprite.svg +1 -1
- package/dist/store/modal.d.ts +154 -9
- package/example/App.vue +123 -311
- package/example/MyCustomModal.vue +37 -0
- package/package.json +1 -1
- package/src/assets/icons/add.svg +4 -0
- package/src/assets/icons/arrow-up.svg +4 -0
- package/src/assets/icons/close-circle.svg +5 -0
- package/src/assets/icons/close.svg +4 -0
- package/src/assets/icons/document-text.svg +4 -0
- package/src/assets/icons/export.svg +5 -0
- package/src/assets/icons/gallery.svg +5 -0
- package/src/assets/icons/notification-icon.svg +7 -0
- package/src/assets/icons/search-zoom-in.svg +6 -0
- package/src/assets/icons/trash.svg +7 -0
- package/src/assets/icons/upload.svg +5 -0
- package/src/components/BaseBadge/BaseBadge.vue +188 -0
- package/src/components/BaseBadge/BaseBadgeGroup.vue +120 -0
- package/src/components/BaseBadge/README.md +127 -0
- package/src/components/BaseBreadCrumbs/BaseBreadCrumbs.vue +2 -1
- package/src/components/BaseButton/BaseButton.vue +0 -112
- package/src/components/BaseInput/BaseInput.vue +4 -3
- package/src/components/BaseModal/BaseModal.vue +189 -0
- package/src/components/BaseTag/BaseTag.vue +245 -0
- package/src/components/BaseTag/README.md +125 -0
- package/src/components/BaseTextarea/BaseTextarea.vue +7 -14
- package/src/components/BaseUpload/BaseUpload.vue +392 -0
- package/src/components/BaseUpload/ImageModal.vue +25 -0
- package/src/composables/useModal.ts +14 -0
- package/src/index.ts +32 -19
- package/src/plugins/modalPlugin.ts +92 -76
- package/src/store/modal.ts +39 -16
- package/src/styles/root.scss +1 -0
- package/src/types/badge.d.ts +19 -0
- package/src/types/modal.d.ts +8 -0
- package/src/types/tag.d.ts +14 -0
- package/src/types/uploadedFile.d.ts +13 -0
- package/src/types/utils.d.ts +1 -1
- package/dist/types/index.d.ts +0 -5
- package/src/components/Modal/Modal.vue +0 -149
- package/src/components/Modal/README.md +0 -47
- package/src/types/index.ts +0 -7
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Компонент BaseTag
|
|
2
|
+
|
|
3
|
+
Компонент `BaseTag` — это переиспользуемый Vue 3 компонент для отображения тегов с настраиваемым текстом, размером, цветом и поведением. Поддерживает закрываемые теги, теги с функцией добавления и поле ввода для создания новых тегов. Компонент следует методологии БЭМ для именования CSS-классов и стилизован с использованием SCSS.
|
|
4
|
+
|
|
5
|
+
## Возможности
|
|
6
|
+
|
|
7
|
+
- **Настраиваемый размер**: Поддерживает четыре варианта размеров: `extra-small`, `small`, `medium`, `large`.
|
|
8
|
+
- **Варианты цветов**: Предлагает цветовые схемы `primary` и `secondary` с эффектами наведения.
|
|
9
|
+
- **Закрываемые теги**: Опционально включает иконку закрытия для вызова события `close`.
|
|
10
|
+
- **Функция добавления тега**: Поддерживает иконку добавления для создания новых тегов.
|
|
11
|
+
- **Поле ввода**: Отображает поле ввода для создания новых тегов, если включен `isHasAddTag`.
|
|
12
|
+
- **Поддержка TypeScript**: Полностью типизирован с использованием TypeScript.
|
|
13
|
+
- **Стилизация по БЭМ**: Использует методологию БЭМ для консистентности и удобства поддержки CSS.
|
|
14
|
+
- **Композаблы**: Использует `useKitSize` и `useKitColor` для модульного управления размером и цветом.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Использование
|
|
18
|
+
|
|
19
|
+
Компонент `BaseTag` можно использовать в разных конфигурациях в зависимости от желаемой функциональности. Ниже приведены примеры типичных случаев использования.
|
|
20
|
+
|
|
21
|
+
### Простой тег
|
|
22
|
+
|
|
23
|
+
```vue
|
|
24
|
+
<template>
|
|
25
|
+
<base-tag text="Пример тега" />
|
|
26
|
+
</template>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Отображает тег с текстом "Пример тега" в размере `medium` и цвете `primary`.
|
|
30
|
+
|
|
31
|
+
### Закрываемый тег
|
|
32
|
+
|
|
33
|
+
```vue
|
|
34
|
+
<template>
|
|
35
|
+
<base-tag text="Закрываемый тег" closable @close="handleClose" />
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
const handleClose = () => {
|
|
40
|
+
console.log('Тег закрыт');
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Тег с иконкой закрытия, которая вызывает событие `close` при клике.
|
|
46
|
+
|
|
47
|
+
### Тег с полем ввода
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<template>
|
|
51
|
+
<base-tag
|
|
52
|
+
:isHasAddTag="true"
|
|
53
|
+
v-model:inputText="tagInput"
|
|
54
|
+
@updateInput="updateInput"
|
|
55
|
+
@addTag="createTag"
|
|
56
|
+
/>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup lang="ts">
|
|
60
|
+
import { ref } from 'vue';
|
|
61
|
+
|
|
62
|
+
const tagInput = ref('');
|
|
63
|
+
const updateInput = (value: string) => {
|
|
64
|
+
tagInput.value = value;
|
|
65
|
+
};
|
|
66
|
+
const createTag = () => {
|
|
67
|
+
console.log('Новый тег:', tagInput.value);
|
|
68
|
+
};
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Тег с полем ввода для создания новых тегов. Поле ввода отображается, если `isHasAddTag` равно `true`.
|
|
73
|
+
|
|
74
|
+
### Тег с иконкой добавления
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<template>
|
|
78
|
+
<base-tag text="Добавить тег" addingTag @addTag="handleAddTag" />
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup lang="ts">
|
|
82
|
+
const handleAddTag = () => {
|
|
83
|
+
console.log('Инициировано добавление тега');
|
|
84
|
+
};
|
|
85
|
+
</script>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Тег с иконкой добавления, которая вызывает событие `addTag` при клике.
|
|
89
|
+
|
|
90
|
+
## Пропсы
|
|
91
|
+
|
|
92
|
+
| Проп | Тип | По умолчанию | Описание |
|
|
93
|
+
|--------------|----------|--------------|--------------------------------------------------------------------------|
|
|
94
|
+
| `text` | `string` | `''` | Текст, отображаемый в теге. |
|
|
95
|
+
| `size` | `string` | `'medium'` | Размер тега: `extra-small`, `small`, `medium`, `large`. |
|
|
96
|
+
| `color` | `string` | `'primary'` | Цветовая схема: `primary`, `secondary`. |
|
|
97
|
+
| `closable` | `boolean`| `false` | Включает иконку закрытия для удаления тега. |
|
|
98
|
+
| `isHasAddTag`| `boolean`| `false` | Включает поле ввода для создания нового тега. |
|
|
99
|
+
| `addingTag` | `boolean`| `false` | Включает иконку добавления тега. |
|
|
100
|
+
| `inputText` | `string` | `''` | Начальное значение поля ввода (используется с `v-model:inputText`). |
|
|
101
|
+
|
|
102
|
+
## События
|
|
103
|
+
|
|
104
|
+
| Событие | Параметры | Описание |
|
|
105
|
+
|----------------|----------------------|-------------------------------------------------------|
|
|
106
|
+
| `close` | - | Вызывается при клике на иконку закрытия. |
|
|
107
|
+
| `addTag` | - | Вызывается при клике на иконку добавления тега. |
|
|
108
|
+
| `updateInput` | `value: string` | Вызывается при изменении текста в поле ввода. |
|
|
109
|
+
|
|
110
|
+
## Стилизация
|
|
111
|
+
|
|
112
|
+
Модификаторы:
|
|
113
|
+
- `--primary-color`, `--secondary-color` — для цветовых схем.
|
|
114
|
+
- `--extra-small-size`, `--small-size`, `--medium-size`, `--large-size` — для размеров.
|
|
115
|
+
- `--adding-tag` — для стилизации тега с иконкой добавления.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## Зависимости
|
|
119
|
+
|
|
120
|
+
- **`BaseIcon`**: Компонент для отображения иконок (используется для иконок закрытия и добавления).
|
|
121
|
+
- **`useKitSize`**: Композабл для управления размерами.
|
|
122
|
+
- **`useKitColor`**: Композабл для управления цветами.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
@@ -65,7 +65,8 @@ function handleInput(event: Event) {
|
|
|
65
65
|
@import '../../styles/root';
|
|
66
66
|
|
|
67
67
|
.base-textarea {
|
|
68
|
-
width:
|
|
68
|
+
width: 100%;
|
|
69
|
+
height: 100%;
|
|
69
70
|
$textarea: &;
|
|
70
71
|
|
|
71
72
|
textarea {
|
|
@@ -80,11 +81,13 @@ function handleInput(event: Event) {
|
|
|
80
81
|
&__wrapper {
|
|
81
82
|
display: flex;
|
|
82
83
|
position: relative;
|
|
84
|
+
width: 100%;
|
|
85
|
+
height: 100%;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
&__field {
|
|
89
|
+
width: 100%;
|
|
86
90
|
min-width: 320px;
|
|
87
|
-
max-width: calc(-16px + 50vw);
|
|
88
91
|
color: var(--primary-text-primary);
|
|
89
92
|
background: var(--bg-light);
|
|
90
93
|
border: 1px solid var(--primary-black-300);
|
|
@@ -128,6 +131,8 @@ function handleInput(event: Event) {
|
|
|
128
131
|
flex-direction: column;
|
|
129
132
|
gap: 6px;
|
|
130
133
|
align-items: center;
|
|
134
|
+
width: 100%;
|
|
135
|
+
height: 100%;
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
&__label-text {
|
|
@@ -152,10 +157,6 @@ function handleInput(event: Event) {
|
|
|
152
157
|
|
|
153
158
|
&.--small-size {
|
|
154
159
|
#{$textarea} {
|
|
155
|
-
&__wrapper {
|
|
156
|
-
height: 40px;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
160
|
&__field {
|
|
160
161
|
padding: var(--spacing-s) calc(var(--spacing-2l) + 35px) var(--spacing-s) var(--spacing-2l);
|
|
161
162
|
font: var(--typography-text-m-regular);
|
|
@@ -172,10 +173,6 @@ function handleInput(event: Event) {
|
|
|
172
173
|
|
|
173
174
|
&.--medium-size {
|
|
174
175
|
#{$textarea} {
|
|
175
|
-
&__wrapper {
|
|
176
|
-
height: 48px;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
176
|
&__field {
|
|
180
177
|
padding: var(--spacing-m) calc(var(--spacing-2l) + 35px) var(--spacing-m) var(--spacing-2l);
|
|
181
178
|
font: var(--typography-text-m-regular);
|
|
@@ -192,10 +189,6 @@ function handleInput(event: Event) {
|
|
|
192
189
|
|
|
193
190
|
&.--large-size {
|
|
194
191
|
#{$textarea} {
|
|
195
|
-
&__wrapper {
|
|
196
|
-
height: 56px;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
192
|
&__field {
|
|
200
193
|
padding: var(--spacing-2l) calc(var(--spacing-l) + 35px) var(--spacing-2l) var(--spacing-l);
|
|
201
194
|
font: var(--typography-text-l-regular);
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="base-file-upload">
|
|
3
|
+
<input
|
|
4
|
+
type="file"
|
|
5
|
+
ref="fileInput"
|
|
6
|
+
:multiple="multiple"
|
|
7
|
+
@change="handleFileUpload"
|
|
8
|
+
class="base-file-upload__input"
|
|
9
|
+
:accept="acceptedFormats.join(',')"
|
|
10
|
+
/>
|
|
11
|
+
<div class="base-file-upload__wrapper" @click="triggerFileInput">
|
|
12
|
+
<base-icon
|
|
13
|
+
name="upload"
|
|
14
|
+
class="base-file-upload__icon-upload"
|
|
15
|
+
size="large"
|
|
16
|
+
/>
|
|
17
|
+
<p class="base-file-upload__title">
|
|
18
|
+
<slot name="title">Перетащите или кликните для загрузки</slot>
|
|
19
|
+
</p>
|
|
20
|
+
<p class="base-file-upload__description">
|
|
21
|
+
<slot name="description">Допустимый формат файлов: pdf, docx, jpeg до 25 MB</slot>
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div v-if="uploadedFiles.length > 0" class="base-file-upload__files">
|
|
26
|
+
<!-- Медиафайлы (изображения) в ряд -->
|
|
27
|
+
<div v-if="mediaFiles.length > 0" class="base-file-upload__media-row">
|
|
28
|
+
<div
|
|
29
|
+
v-for="(file, index) in mediaFiles"
|
|
30
|
+
:key="index"
|
|
31
|
+
class="base-file-upload__media-item"
|
|
32
|
+
>
|
|
33
|
+
<img :src="getImagePreview(file.file)" alt="Preview" class="base-file-upload__media-preview" />
|
|
34
|
+
<base-button
|
|
35
|
+
class="base-file-upload__zoom"
|
|
36
|
+
size="custom"
|
|
37
|
+
color="custom"
|
|
38
|
+
@click="openImageModal(file.file)"
|
|
39
|
+
>
|
|
40
|
+
<base-icon name="search-zoom-in" size="custom" class="base-file-upload__zoom-icon" />
|
|
41
|
+
</base-button>
|
|
42
|
+
<base-button
|
|
43
|
+
class="base-file-upload__remove"
|
|
44
|
+
size="custom"
|
|
45
|
+
color="custom"
|
|
46
|
+
@click="removeFile(index, true)"
|
|
47
|
+
>
|
|
48
|
+
<base-icon name="trash" size="custom" class="base-file-upload__zoom-icon" />
|
|
49
|
+
</base-button>
|
|
50
|
+
</div>
|
|
51
|
+
<!-- Заглушка для загрузки дополнительных медиафайлов -->
|
|
52
|
+
<div
|
|
53
|
+
v-if="mediaFiles.length > 0"
|
|
54
|
+
class="base-file-upload__media-item base-file-upload__media-placeholder"
|
|
55
|
+
@click="triggerFileInput"
|
|
56
|
+
>
|
|
57
|
+
<base-icon name="gallery" size="medium" class="base-file-upload__placeholder-icon" />
|
|
58
|
+
<p class="base-file-upload__placeholder-text"><span>Загрузить</span> <br /> фото</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Остальные файлы в колонках -->
|
|
63
|
+
<div v-if="otherFiles.length > 0" class="base-file-upload__file-list">
|
|
64
|
+
<div
|
|
65
|
+
v-for="(file, index) in otherFiles"
|
|
66
|
+
:key="index"
|
|
67
|
+
class="base-file-upload__file-item"
|
|
68
|
+
>
|
|
69
|
+
<div class="base-file-upload__file-item-wrapper">
|
|
70
|
+
<base-icon name="document-text" size="medium" />
|
|
71
|
+
<span>{{ file.name }} ({{ formatFileSize(file.size) }})</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="base-file-upload__file-item-wrapper">
|
|
74
|
+
<base-button
|
|
75
|
+
class="base-file-upload__file-item-button"
|
|
76
|
+
size="custom"
|
|
77
|
+
color="custom"
|
|
78
|
+
@click="downloadFile(file.file, index, false)"
|
|
79
|
+
>
|
|
80
|
+
<base-icon
|
|
81
|
+
name="export"
|
|
82
|
+
size="medium"
|
|
83
|
+
class="base-file-upload__file-item-icon"
|
|
84
|
+
/>
|
|
85
|
+
</base-button>
|
|
86
|
+
<base-button
|
|
87
|
+
class="base-file-upload__file-item-button base-file-upload__file-item-button-trash"
|
|
88
|
+
size="custom"
|
|
89
|
+
color="custom"
|
|
90
|
+
@click="removeFile(index, false)"
|
|
91
|
+
>
|
|
92
|
+
<base-icon
|
|
93
|
+
name="trash"
|
|
94
|
+
size="medium"
|
|
95
|
+
class="base-file-upload__file-item-icon"
|
|
96
|
+
/>
|
|
97
|
+
</base-button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<p v-if="errorMessage" class="base-file-upload__error">{{ errorMessage }}</p>
|
|
104
|
+
</div>
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<script setup lang="ts">
|
|
108
|
+
import { ref, computed, watch } from 'vue';
|
|
109
|
+
import BaseButton from '../BaseButton/BaseButton.vue';
|
|
110
|
+
import BaseIcon from '../BaseIcon/BaseIcon.vue';
|
|
111
|
+
import { useModal } from '../../composables/useModal';
|
|
112
|
+
import type { UploadedFile, IpropsUpload } from '../../types/uploadedFile.d';
|
|
113
|
+
import ImageModal from './ImageModal.vue';
|
|
114
|
+
|
|
115
|
+
const props = withDefaults(defineProps<IpropsUpload>(), {
|
|
116
|
+
multiple: true,
|
|
117
|
+
maxFileSize: 25 * 1024 * 1024, // 25 MB в байтах
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const emit = defineEmits(['update:files']);
|
|
121
|
+
|
|
122
|
+
const fileInput = ref<HTMLInputElement | null>(null);
|
|
123
|
+
const uploadedFiles = ref<UploadedFile[]>([]);
|
|
124
|
+
const isUploading = ref(false);
|
|
125
|
+
const errorMessage = ref<string | null>(null);
|
|
126
|
+
const modal = useModal();
|
|
127
|
+
|
|
128
|
+
const mediaExtensions = ['.jpg', '.jpeg', '.png'];
|
|
129
|
+
const mediaFiles = computed(() =>
|
|
130
|
+
uploadedFiles.value.filter((file) =>
|
|
131
|
+
mediaExtensions.includes(`.${file.name.split('.').pop()?.toLowerCase() || ''}`)
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
const otherFiles = computed(() =>
|
|
135
|
+
uploadedFiles.value.filter((file) =>
|
|
136
|
+
!mediaExtensions.includes(`.${file.name.split('.').pop()?.toLowerCase() || ''}`)
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const triggerFileInput = () => {
|
|
141
|
+
fileInput.value?.click();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const handleFileUpload = (event: Event) => {
|
|
145
|
+
const target = event.target as HTMLInputElement;
|
|
146
|
+
const files = target.files;
|
|
147
|
+
|
|
148
|
+
if (files) {
|
|
149
|
+
isUploading.value = true;
|
|
150
|
+
errorMessage.value = null;
|
|
151
|
+
|
|
152
|
+
Array.from(files).forEach((file) => {
|
|
153
|
+
const fileExtension = `.${file.name.split('.').pop()?.toLowerCase() || ''}`;
|
|
154
|
+
if (!props.acceptedFormats.includes(fileExtension)) {
|
|
155
|
+
errorMessage.value = `Недопустимый формат файла: ${file.name}. Допустимы: ${props.acceptedFormats.join(', ')}.`;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (file.size > props.maxFileSize) {
|
|
160
|
+
errorMessage.value = `Файл ${file.name} превышает максимальный размер ${formatFileSize(props.maxFileSize)}.`;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
uploadedFiles.value.push({ name: file.name, size: file.size, file });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
isUploading.value = false;
|
|
168
|
+
target.value = '';
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const removeFile = (index: number, isMedia: boolean) => {
|
|
173
|
+
const files = isMedia ? mediaFiles.value : otherFiles.value;
|
|
174
|
+
const globalIndex = uploadedFiles.value.indexOf(files[index]);
|
|
175
|
+
if (globalIndex !== -1) {
|
|
176
|
+
uploadedFiles.value.splice(globalIndex, 1);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const downloadFile = (file: File, index: number, isMedia: boolean) => {
|
|
181
|
+
const url = URL.createObjectURL(file);
|
|
182
|
+
const link = document.createElement('a');
|
|
183
|
+
link.href = url;
|
|
184
|
+
link.download = file.name;
|
|
185
|
+
document.body.appendChild(link);
|
|
186
|
+
link.click();
|
|
187
|
+
document.body.removeChild(link);
|
|
188
|
+
URL.revokeObjectURL(url);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const openImageModal = (file: File) => {
|
|
192
|
+
const imageUrl = getImagePreview(file);
|
|
193
|
+
modal.open('image-modal', { closable: true, imageUrl }, ImageModal);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const formatFileSize = (bytes: number) => {
|
|
197
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
198
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
199
|
+
return `${bytes} B`;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const getImagePreview = (file: File) => {
|
|
203
|
+
return URL.createObjectURL(file);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
watch(uploadedFiles, (newFiles) => {
|
|
207
|
+
emit('update:files', newFiles);
|
|
208
|
+
}, { deep: true });
|
|
209
|
+
</script>
|
|
210
|
+
|
|
211
|
+
<style lang="scss" scoped>
|
|
212
|
+
@import '../../styles/variables';
|
|
213
|
+
@import '../../styles/root';
|
|
214
|
+
|
|
215
|
+
.base-file-upload {
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
gap: var(--spacing-l);
|
|
219
|
+
|
|
220
|
+
&__wrapper {
|
|
221
|
+
display: flex;
|
|
222
|
+
flex-direction: column;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: center;
|
|
225
|
+
gap: var(--spacing-xs);
|
|
226
|
+
border-radius: var(--corner-radius-m);
|
|
227
|
+
border: 1px solid var(--primary-black-300);
|
|
228
|
+
background: var(--bg-light);
|
|
229
|
+
padding: 32px 120px;
|
|
230
|
+
transition: all var(--transition);
|
|
231
|
+
|
|
232
|
+
p {
|
|
233
|
+
margin: 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@include hover {
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
background: var(--primary-black-200);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
&__title {
|
|
243
|
+
color: var(--primary-text-primary);
|
|
244
|
+
font: var(--typography-text-m-medium);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
&__description {
|
|
248
|
+
color: var(--primary-text-tertiary);
|
|
249
|
+
font: var(--typography-text-s-regular);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
&__input {
|
|
253
|
+
display: none;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
&__media-row {
|
|
257
|
+
display: flex;
|
|
258
|
+
gap: 1rem;
|
|
259
|
+
flex-wrap: wrap;
|
|
260
|
+
margin-bottom: 1rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
&__media-item {
|
|
264
|
+
position: relative;
|
|
265
|
+
display: inline-block;
|
|
266
|
+
width: 104px;
|
|
267
|
+
height: 104px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
&__media-preview {
|
|
271
|
+
width: 100%;
|
|
272
|
+
height: 100%;
|
|
273
|
+
object-fit: cover;
|
|
274
|
+
border-radius: var(--corner-radius-xs);
|
|
275
|
+
border: 1px solid var(--primary-black-300);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
&__remove,
|
|
279
|
+
&__zoom {
|
|
280
|
+
position: absolute;
|
|
281
|
+
bottom: 10px;
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
background: var(--bg-light);
|
|
284
|
+
border-radius: var(--corner-radius-2xs);
|
|
285
|
+
padding: var(--spacing-xs);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
&__remove {
|
|
289
|
+
right: 5px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
&__zoom {
|
|
293
|
+
left: 5px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
&__zoom-icon {
|
|
297
|
+
width: 20px;
|
|
298
|
+
height: 20px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
&__file-list {
|
|
302
|
+
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
&__file-item {
|
|
307
|
+
display: flex;
|
|
308
|
+
justify-content: space-between;
|
|
309
|
+
align-items: center;
|
|
310
|
+
padding: 8px 0;
|
|
311
|
+
border-bottom: 1px solid var(--primary-black-200);
|
|
312
|
+
|
|
313
|
+
@include hover {
|
|
314
|
+
.base-file-upload__file-item-wrapper {
|
|
315
|
+
color: var(--primary-blue);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.base-file-upload__file-item-button {
|
|
319
|
+
background: var(--primary-blue-100);
|
|
320
|
+
}
|
|
321
|
+
.base-file-upload__file-item-button-trash {
|
|
322
|
+
background: var(--error-red-light-05);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@include pressed {
|
|
327
|
+
.base-file-upload__file-item-wrapper {
|
|
328
|
+
color: var(--primary-blue-deep);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.base-file-upload__file-item-button {
|
|
332
|
+
background: var(--primary-blue-200);
|
|
333
|
+
}
|
|
334
|
+
.base-file-upload__file-item-button-trash {
|
|
335
|
+
background: var(--error-red-light-04);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
&__file-item-wrapper {
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 8px;
|
|
344
|
+
color: var(--primary-text-primary);
|
|
345
|
+
font: var(--typography-text-m-regular);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
&__file-item-button {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
justify-content: center;
|
|
352
|
+
background: transparent;
|
|
353
|
+
border-radius: var(--corner-radius-2xs);
|
|
354
|
+
padding: var(--spacing-xs);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
&__error {
|
|
358
|
+
color: var(--primary-red-500);
|
|
359
|
+
margin-top: 0.5rem;
|
|
360
|
+
font-size: var(--typography-text-s-regular);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
&__media-placeholder {
|
|
364
|
+
display: flex;
|
|
365
|
+
flex-direction: column;
|
|
366
|
+
align-items: center;
|
|
367
|
+
justify-content: center;
|
|
368
|
+
gap: var(--spacing-s);
|
|
369
|
+
border: 1px dashed var(--primary-black-300);
|
|
370
|
+
border-radius: var(--corner-radius-xs);
|
|
371
|
+
cursor: pointer;
|
|
372
|
+
background: var(--bg-light);
|
|
373
|
+
transition: all var(--transition);
|
|
374
|
+
|
|
375
|
+
&:hover {
|
|
376
|
+
background: var(--primary-black-200);
|
|
377
|
+
border-color: var(--primary-blue-500);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
&__placeholder-text {
|
|
382
|
+
color: var(--primary-text-tertiary);
|
|
383
|
+
font: var(--typography-text-s-regular);
|
|
384
|
+
margin: 0;
|
|
385
|
+
text-align: center;
|
|
386
|
+
|
|
387
|
+
span {
|
|
388
|
+
color: var(--primary-blue);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="image-modal">
|
|
3
|
+
<img :src="modalProps.imageUrl" alt="Enlarged Image" class="image-modal__image" />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
defineProps<{
|
|
9
|
+
modalProps: { closable?: boolean; imageUrl?: string };
|
|
10
|
+
}>();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<style lang="scss" scoped>
|
|
14
|
+
@import '../../styles/variables';
|
|
15
|
+
@import '../../styles/root';
|
|
16
|
+
|
|
17
|
+
.image-modal {
|
|
18
|
+
&__image {
|
|
19
|
+
max-width: 90vw;
|
|
20
|
+
max-height: 80vh;
|
|
21
|
+
object-fit: contain;
|
|
22
|
+
border-radius: var(--corner-radius-m);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { inject } from 'vue';
|
|
2
|
+
import type { ICoreModalProps, ModalComponent } from '../types/modal';
|
|
3
|
+
interface ModalApi {
|
|
4
|
+
open(id: string, props?: ICoreModalProps, component?: ModalComponent): void;
|
|
5
|
+
close(id: string): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function useModal() {
|
|
9
|
+
const modal = inject<ModalApi>('$modal');
|
|
10
|
+
if (!modal) {
|
|
11
|
+
throw new Error('Modal plugin is not installed');
|
|
12
|
+
}
|
|
13
|
+
return modal;
|
|
14
|
+
}
|