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
377 lines
15 KiB
Vue
377 lines
15 KiB
Vue
<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>
|