Some checks are pending
Telegram Mini App Shop Builder / Compute version metadata (push) Waiting to run
Telegram Mini App Shop Builder / Run Frontend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run Backend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Waiting to run
Telegram Mini App Shop Builder / Build module. (push) Blocked by required conditions
Telegram Mini App Shop Builder / release (push) Blocked by required conditions
145 lines
5.1 KiB
Vue
145 lines
5.1 KiB
Vue
<template>
|
||
<div
|
||
class="tw:min-h-full tw:w-full tw:flex tw:flex-col tw:items-center tw:p-8 blueprint-bg"
|
||
@click.self="handleBackgroundClick"
|
||
>
|
||
<div v-if="formFields.length === 0" class="tw:absolute tw:top-1/2 tw:left-1/2 tw:-translate-x-1/2 tw:-translate-y-1/2 tw:z-0 tw:text-center tw:text-gray-500 tw:py-12 tw:bg-white/80 tw:backdrop-blur-sm tw:rounded-xl tw:p-8 tw:shadow-sm tw:max-w-md tw:pointer-events-none">
|
||
<i class="fa fa-mouse-pointer tw:text-4xl tw:mb-4 tw:text-blue-500/50"></i>
|
||
<p class="tw:font-medium">Перетащите поля сюда, чтобы начать создавать форму</p>
|
||
</div>
|
||
|
||
<draggable
|
||
v-model="formFields"
|
||
group="fields"
|
||
item-key="id"
|
||
class="tw:w-full tw:max-w-2xl tw:space-y-4 tw:flex-1 tw:min-h-[300px] tw:relative tw:z-10 tw:pb-24"
|
||
ghost-class="ghost-field"
|
||
drag-class="drag-field"
|
||
@change="handleDragChange"
|
||
@click.self="handleBackgroundClick"
|
||
>
|
||
<template #item="{ element: field, index }">
|
||
<div
|
||
class="tw:relative tw:group tw:border-2 tw:rounded-lg tw:p-4 tw:bg-white tw:shadow-sm tw:cursor-pointer tw:transition-all"
|
||
:class="[
|
||
fieldErrors[field.id] ? 'tw:border-red-500 tw:ring-2 tw:ring-red-200' :
|
||
selectedFieldId === field.id ? 'tw:border-blue-500 tw:ring-2 tw:ring-blue-200' : 'tw:border-transparent hover:tw:border-blue-400'
|
||
]"
|
||
@click.stop="selectField(field.id)"
|
||
>
|
||
<!-- Иконка ошибки -->
|
||
<div
|
||
v-if="fieldErrors[field.id]"
|
||
class="tw:absolute tw:-left-3 tw:-top-3 tw:z-20 tw:bg-red-500 tw:text-white tw:rounded-full tw:w-6 tw:h-6 tw:flex tw:items-center tw:justify-center tw:shadow-sm"
|
||
v-tooltip.top="fieldErrors[field.id]"
|
||
>
|
||
<i class="fa fa-exclamation tw:text-xs"></i>
|
||
</div>
|
||
|
||
<!-- Кнопка удаления (справа за пределами блока, видна при выборе) -->
|
||
<div
|
||
class="tw:absolute tw:-right-12 tw:top-1/2 tw:-translate-y-1/2 tw:z-10 tw:transition-opacity tw:duration-200"
|
||
:class="selectedFieldId === field.id ? 'tw:opacity-100' : 'tw:opacity-0 tw:pointer-events-none'"
|
||
>
|
||
<Button
|
||
@click.stop="removeField(field.id)"
|
||
icon="fa fa-trash"
|
||
severity="danger"
|
||
rounded
|
||
size="small"
|
||
v-tooltip.right="'Удалить поле'"
|
||
class="!tw:shadow-md !tw:w-9 !tw:h-9 !tw:p-0 tw:flex tw:items-center tw:justify-center hover:!tw:bg-red-600"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Оверлей для перехвата кликов поверх disabled инпутов -->
|
||
<div class="tw:absolute tw:inset-0 tw:z-[1] tw:bg-transparent"></div>
|
||
|
||
<!-- Предпросмотр поля -->
|
||
<div class="tw:relative tw:z-0">
|
||
<FormKit
|
||
v-if="field.$formkit"
|
||
:key="`${field.id}-${field.prefixIcon}-${field.suffixIcon}`"
|
||
:type="field.$formkit"
|
||
v-bind="getFieldProps(field)"
|
||
:name="field.name || `field_${field.id}`"
|
||
/>
|
||
<div v-else class="tw:text-red-500 tw:text-sm">
|
||
<i class="fa fa-exclamation-triangle tw:mr-2"></i>
|
||
Поле без типа $formkit
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</draggable>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { FormKit } from '@formkit/vue';
|
||
import { Button } from 'primevue';
|
||
import draggable from 'vuedraggable';
|
||
import { useFormFields } from './composables/useFormFields.js';
|
||
import { getFieldProps } from './utils/fieldHelpers.js';
|
||
import { toastBus } from '@/utils/toastHelper';
|
||
|
||
// Используем composable для работы с полями
|
||
const {
|
||
formFields,
|
||
selectedFieldId,
|
||
fieldErrors,
|
||
selectField,
|
||
removeField,
|
||
isFieldNameUnique
|
||
} = useFormFields();
|
||
|
||
function handleDragChange(evt) {
|
||
if (evt.added) {
|
||
const addedField = evt.added.element;
|
||
|
||
// Проверяем уникальность имени
|
||
if (!isFieldNameUnique(addedField.name, addedField.id)) {
|
||
// Удаляем дубликат
|
||
removeField(addedField.id);
|
||
|
||
toastBus.emit('show', {
|
||
severity: 'error',
|
||
summary: 'Ошибка добавления',
|
||
detail: `Поле с именем "${addedField.name}" уже добавлено в форму.`
|
||
});
|
||
return;
|
||
}
|
||
|
||
selectField(addedField.id);
|
||
}
|
||
}
|
||
|
||
function handleBackgroundClick() {
|
||
// Сбрасываем выбор, если есть выбранный элемент
|
||
if (selectedFieldId.value) {
|
||
selectedFieldId.value = null;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.blueprint-bg {
|
||
background-color: #e2e8f0;
|
||
background-image: radial-gradient(#cbd5e1 1px, transparent 1px);
|
||
background-size: 10px 10px;
|
||
}
|
||
|
||
.ghost-field {
|
||
opacity: 0.5;
|
||
background-color: #eff6ff;
|
||
border-color: #93c5fd;
|
||
border-style: dashed;
|
||
}
|
||
|
||
.drag-field {
|
||
opacity: 1;
|
||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||
transform: scale(1.05);
|
||
}
|
||
</style>
|