Files
interview-demo-code/frontend/admin/src/components/FormBuilder/FormCanvas.vue
Nikita Kiselev 6a59dcc0c9 feat: add FormKit framework support and update dependencies
- Add `telecart_forms` table migration and default checkout form seeder
- Implement `FormsHandler` to fetch form schemas
- Update `OrderCreateService` to handle custom fields in order comments
- Add `update` method to QueryBuilder and Grammar
- Add `Arr::except` helper
- Update composer dependencies (Carbon, Symfony, PHPUnit, etc.)
- Improve `MigratorService` error handling
- Add unit tests for new functionality
2025-11-23 16:05:46 +03:00

140 lines
4.9 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
:key="`${field.id}-${field.prefixIcon}-${field.suffixIcon}`"
:type="field.$formkit"
v-bind="getFieldProps(field)"
:name="field.name || `field_${field.id}`"
/>
</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>