Squashed commit message
Some checks failed
Telegram Mini App Shop Builder / Compute version metadata (push) Has been cancelled
Telegram Mini App Shop Builder / Run Frontend tests (push) Has been cancelled
Telegram Mini App Shop Builder / Run Backend tests (push) Has been cancelled
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Has been cancelled
Telegram Mini App Shop Builder / Build module. (push) Has been cancelled
Telegram Mini App Shop Builder / release (push) Has been cancelled
Some checks failed
Telegram Mini App Shop Builder / Compute version metadata (push) Has been cancelled
Telegram Mini App Shop Builder / Run Frontend tests (push) Has been cancelled
Telegram Mini App Shop Builder / Run Backend tests (push) Has been cancelled
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Has been cancelled
Telegram Mini App Shop Builder / Build module. (push) Has been cancelled
Telegram Mini App Shop Builder / release (push) Has been cancelled
This commit is contained in:
376
frontend/admin/src/components/FormBuilder/FieldSettings.vue
Normal file
376
frontend/admin/src/components/FormBuilder/FieldSettings.vue
Normal file
@@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!selectedField" class="tw:text-gray-400 tw:text-center tw:py-8">
|
||||
<i class="fa fa-mouse-pointer tw:text-2xl tw:mb-2"></i>
|
||||
<p>Выберите поле для настройки</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="tw:space-y-4">
|
||||
<!-- Тип поля (только для чтения) -->
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Тип поля</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs tw:mt-0.5"
|
||||
v-tooltip.top="'Тип элемента формы (например, текст, число, выбор). Нельзя изменить после создания.'"
|
||||
></i>
|
||||
</div>
|
||||
<InputText
|
||||
:value="selectedField.$formkit"
|
||||
disabled
|
||||
class="tw:w-full tw:bg-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Название поля -->
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Название (name)</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs tw:mt-0.5"
|
||||
v-tooltip.top="'Уникальный ключ поля в JSON-объекте данных. Используется при отправке формы. Должен быть на английском.'"
|
||||
></i>
|
||||
</div>
|
||||
<InputText
|
||||
:modelValue="selectedField.name"
|
||||
@update:modelValue="onNameChange"
|
||||
class="tw:w-full"
|
||||
:class="{ 'p-invalid': nameError }"
|
||||
placeholder="field_name"
|
||||
:disabled="selectedField.locked"
|
||||
/>
|
||||
<small v-if="nameError" class="p-error tw:text-red-500 tw:text-xs tw:mt-1 tw:block">{{ nameError }}</small>
|
||||
</div>
|
||||
|
||||
<!-- Метка -->
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Метка (label)</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs tw:mt-0.5"
|
||||
v-tooltip.top="'Подпись, которая отображается над полем ввода для пользователя.'"
|
||||
></i>
|
||||
</div>
|
||||
<InputText
|
||||
:modelValue="selectedField.label"
|
||||
@update:modelValue="updateField(selectedField.id, { label: $event })"
|
||||
class="tw:w-full"
|
||||
placeholder="Название поля"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Help Text -->
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Подсказка (help)</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs tw:mt-0.5"
|
||||
v-tooltip.top="'Дополнительный поясняющий текст, который отображается мелким шрифтом под полем.'"
|
||||
></i>
|
||||
</div>
|
||||
<InputText
|
||||
:modelValue="selectedField.help"
|
||||
@update:modelValue="updateField(selectedField.id, { help: $event })"
|
||||
class="tw:w-full"
|
||||
placeholder="Текст подсказки"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Иконки -->
|
||||
<div class="tw:grid tw:grid-cols-2 tw:gap-2">
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2 tw:mb-1">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Иконка слева</label>
|
||||
</div>
|
||||
<IconPicker
|
||||
:modelValue="selectedField.prefixIcon"
|
||||
@update:modelValue="updateField(selectedField.id, { prefixIcon: $event })"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2 tw:mb-1">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Иконка справа</label>
|
||||
</div>
|
||||
<IconPicker
|
||||
:modelValue="selectedField.suffixIcon"
|
||||
@update:modelValue="updateField(selectedField.id, { suffixIcon: $event })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder (для текстовых полей) -->
|
||||
<div v-if="hasPlaceholder">
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Текст-заполнитель (placeholder)</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs tw:mt-0.5"
|
||||
v-tooltip.top="'Текст-подсказка внутри поля, который исчезает при начале ввода.'"
|
||||
></i>
|
||||
</div>
|
||||
<InputText
|
||||
:modelValue="selectedField.placeholder"
|
||||
@update:modelValue="updateField(selectedField.id, { placeholder: $event })"
|
||||
class="tw:w-full"
|
||||
placeholder="Например: Введите ваше имя"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Настройки для Range/Number -->
|
||||
<div v-if="isRangeOrNumber">
|
||||
<div class="tw:grid tw:grid-cols-2 tw:gap-2">
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Минимум</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs tw:mt-0.5"
|
||||
v-tooltip.top="'Минимально допустимое значение.'"
|
||||
></i>
|
||||
</div>
|
||||
<InputNumber
|
||||
:modelValue="Number(selectedField.min)"
|
||||
@update:modelValue="updateField(selectedField.id, { min: $event })"
|
||||
class="tw:w-full"
|
||||
inputClass="tw:w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Максимум</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs tw:mt-0.5"
|
||||
v-tooltip.top="'Максимально допустимое значение.'"
|
||||
></i>
|
||||
</div>
|
||||
<InputNumber
|
||||
:modelValue="Number(selectedField.max)"
|
||||
@update:modelValue="updateField(selectedField.id, { max: $event })"
|
||||
class="tw:w-full"
|
||||
inputClass="tw:w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="tw:col-span-2">
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Шаг (step)</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs tw:mt-0.5"
|
||||
v-tooltip.top="'Шаг изменения значения (например, 1 или 0.5).'"
|
||||
></i>
|
||||
</div>
|
||||
<InputNumber
|
||||
:modelValue="Number(selectedField.step)"
|
||||
@update:modelValue="updateField(selectedField.id, { step: $event })"
|
||||
class="tw:w-full"
|
||||
inputClass="tw:w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Настройки для Color -->
|
||||
<div v-if="selectedField.$formkit === 'color'">
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Значение по умолчанию</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs"
|
||||
v-tooltip.top="'Цвет, выбранный по умолчанию.'"
|
||||
></i>
|
||||
</div>
|
||||
<div class="tw:flex tw:gap-2 tw:items-baseline">
|
||||
<div class="tw:relative tw:w-10 tw:h-10 tw:rounded tw:overflow-hidden tw:border tw:border-gray-300">
|
||||
<input
|
||||
type="color"
|
||||
:value="selectedField.value || '#000000'"
|
||||
@input="updateField(selectedField.id, { value: $event.target.value })"
|
||||
class="tw:absolute tw:-top-2 tw:-left-2 tw:w-16 tw:h-16 tw:cursor-pointer tw:p-0 tw:border-0"
|
||||
/>
|
||||
</div>
|
||||
<InputText
|
||||
:modelValue="selectedField.value"
|
||||
@update:modelValue="updateField(selectedField.id, { value: $event })"
|
||||
class="tw:flex-1"
|
||||
placeholder="#000000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Опции (для select и radio) -->
|
||||
<div v-if="hasOptions">
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Опции</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs tw:mt-0.5"
|
||||
v-tooltip.top="'Список вариантов для выбора. Текст - то, что видит пользователь. Значение - то, что отправляется на сервер.'"
|
||||
></i>
|
||||
</div>
|
||||
<div class="tw:space-y-2">
|
||||
<div
|
||||
v-for="(option, index) in selectedField.options"
|
||||
:key="index"
|
||||
class="tw:flex tw:gap-2 tw:items-center"
|
||||
>
|
||||
<InputText
|
||||
:modelValue="option.label"
|
||||
@update:modelValue="updateFieldOption(selectedField.id, index, 'label', $event)"
|
||||
placeholder="Текст"
|
||||
class="tw:flex-1 tw:w-full"
|
||||
/>
|
||||
<InputText
|
||||
:modelValue="option.value"
|
||||
@update:modelValue="updateFieldOption(selectedField.id, index, 'value', $event)"
|
||||
placeholder="Значение"
|
||||
class="tw:flex-1 tw:w-full"
|
||||
/>
|
||||
<Button
|
||||
icon="fa fa-trash"
|
||||
severity="danger"
|
||||
size="small"
|
||||
text
|
||||
rounded
|
||||
@click="removeFieldOption(selectedField.id, index)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
label="Добавить опцию"
|
||||
icon="fa fa-plus"
|
||||
size="small"
|
||||
class="tw:w-full"
|
||||
@click="addFieldOption(selectedField.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Валидация -->
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Валидация</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs"
|
||||
v-tooltip.top="'Правила проверки данных (FormKit Validation). Разделяются вертикальной чертой |. Например: required|email|length:5,10'"
|
||||
></i>
|
||||
</div>
|
||||
<InputText
|
||||
:modelValue="selectedField.validation"
|
||||
@update:modelValue="updateField(selectedField.id, { validation: $event })"
|
||||
class="tw:w-full"
|
||||
placeholder="required|email|length:5,10"
|
||||
/>
|
||||
<p class="tw:text-xs tw:text-gray-500 tw:mt-1">
|
||||
Примеры: required, email, length:5,10, number, url. <a href="https://formkit.com/essentials/validation" target="_blank">Документация <i class="fa fa-external-link"></i></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Label валидации -->
|
||||
<div>
|
||||
<div class="tw:flex tw:items-baseline tw:gap-2">
|
||||
<label class="tw:block tw:text-sm tw:font-medium">Имя поля для ошибок</label>
|
||||
<i
|
||||
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help tw:text-xs"
|
||||
v-tooltip.top="'Название поля, которое будет подставляться в текст ошибки валидации вместо системного имени.'"
|
||||
></i>
|
||||
</div>
|
||||
<InputText
|
||||
:modelValue="selectedField.validationLabel"
|
||||
@update:modelValue="updateField(selectedField.id, { validationLabel: $event })"
|
||||
class="tw:w-full"
|
||||
placeholder="Например: Пароль"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Button, InputText, InputNumber, useConfirm } from 'primevue';
|
||||
import { useFormFields } from './composables/useFormFields.js';
|
||||
import { supportsPlaceholder, supportsOptions } from './utils/fieldHelpers.js';
|
||||
import IconPicker from '@/components/FormBuilder/IconPicker.vue';
|
||||
|
||||
const {
|
||||
formFields,
|
||||
selectedFieldId,
|
||||
removeField,
|
||||
updateField,
|
||||
addFieldOption,
|
||||
removeFieldOption,
|
||||
updateFieldOption,
|
||||
isFieldNameUnique,
|
||||
setFieldError // Импортируем метод для установки ошибок
|
||||
} = useFormFields();
|
||||
|
||||
const confirm = useConfirm();
|
||||
const nameError = ref(null);
|
||||
|
||||
const selectedField = computed(() => {
|
||||
if (!selectedFieldId || !selectedFieldId.value || !formFields || !formFields.value) {
|
||||
return null;
|
||||
}
|
||||
return formFields.value.find(f => f.id === selectedFieldId.value);
|
||||
});
|
||||
|
||||
const hasPlaceholder = computed(() => {
|
||||
if (!selectedField.value) return false;
|
||||
return supportsPlaceholder(selectedField.value.$formkit);
|
||||
});
|
||||
|
||||
const hasOptions = computed(() => {
|
||||
if (!selectedField.value) return false;
|
||||
return supportsOptions(selectedField.value.$formkit);
|
||||
});
|
||||
|
||||
const isRangeOrNumber = computed(() => {
|
||||
if (!selectedField.value) return false;
|
||||
return ['range', 'number'].includes(selectedField.value.$formkit);
|
||||
});
|
||||
|
||||
// Сбрасываем ошибку при смене поля
|
||||
watch(selectedFieldId, () => {
|
||||
nameError.value = null;
|
||||
// Ошибки в глобальном состоянии сбрасываются только при исправлении
|
||||
});
|
||||
|
||||
function onNameChange(newName) {
|
||||
if (!selectedField.value) return;
|
||||
|
||||
// Убираем пробелы и спецсимволы, кроме _ и букв/цифр
|
||||
// Хотя FormKit позволяет многое, лучше придерживаться стандартных правил переменных
|
||||
const sanitized = newName.trim(); // .replace(/[^a-zA-Z0-9_]/g, '');
|
||||
// Не будем жестко фильтровать, но проверим уникальность
|
||||
|
||||
if (!sanitized) {
|
||||
nameError.value = 'Имя поля не может быть пустым';
|
||||
setFieldError(selectedField.value.id, nameError.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sanitized !== selectedField.value.name && !isFieldNameUnique(sanitized, selectedField.value.id)) {
|
||||
nameError.value = 'Поле с таким именем уже существует';
|
||||
setFieldError(selectedField.value.id, nameError.value);
|
||||
return;
|
||||
}
|
||||
|
||||
nameError.value = null;
|
||||
setFieldError(selectedField.value.id, null);
|
||||
updateField(selectedField.value.id, { name: sanitized });
|
||||
}
|
||||
|
||||
function removeSelectedField(event) {
|
||||
if (!selectedField.value) return;
|
||||
|
||||
confirm.require({
|
||||
target: event.currentTarget,
|
||||
message: `Вы уверены, что хотите удалить поле "${selectedField.value.label || selectedField.value.name}"?`,
|
||||
icon: 'fa fa-exclamation-triangle',
|
||||
acceptLabel: 'Да, удалить',
|
||||
rejectLabel: 'Нет',
|
||||
acceptClass: 'p-button-danger p-button-sm',
|
||||
rejectClass: 'p-button-secondary p-button-sm p-button-text',
|
||||
accept: () => {
|
||||
removeField(selectedField.value.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user