Files
interview-demo-code/frontend/admin/src/components/FormBuilder/FormCanvas.vue
Nikita Kiselev 3cc82e45f0
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
Squashed commit message
2026-03-11 23:02:54 +03:00

145 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>