Files
interview-demo-code/frontend/admin/src/components/FormBuilder/FieldSettings.vue
Nikita Kiselev 3abcb18f0c
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
Squashed commit message
2026-03-11 22:55:28 +03:00

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