fix: switch between code and visual for custom forms
This commit is contained in:
@@ -33,44 +33,80 @@
|
|||||||
|
|
||||||
<!-- Режим визуального конструктора -->
|
<!-- Режим визуального конструктора -->
|
||||||
<template v-if="activeMode === 'visual'">
|
<template v-if="activeMode === 'visual'">
|
||||||
<!-- Панель доступных полей -->
|
<!-- Если форма кастомная, показываем предупреждение вместо редактора -->
|
||||||
<div class="tw:w-64 tw:flex-shrink-0 tw:h-full tw:overflow-hidden">
|
<div v-if="isCustom" class="tw:flex-1 tw:flex tw:items-center tw:justify-center">
|
||||||
<FieldsPanel class="tw:h-full" />
|
<div class="tw:max-w-2xl tw:p-8 tw:bg-yellow-50 tw:border-2 tw:border-yellow-200 tw:rounded-lg">
|
||||||
</div>
|
<div class="tw:flex tw:items-start tw:gap-4">
|
||||||
|
<i class="fa fa-exclamation-triangle tw:text-3xl tw:text-yellow-600"></i>
|
||||||
<!-- Основная зона конструктора -->
|
<div>
|
||||||
<div class="tw:flex-1 tw:flex tw:gap-4 tw:overflow-hidden">
|
<h3 class="tw:text-lg tw:font-bold tw:text-yellow-800 tw:mb-2">
|
||||||
<!-- Зона формы -->
|
Форма является кастомной
|
||||||
<div class="tw:flex-1 tw:flex tw:flex-col tw:border tw:border-gray-200 tw:rounded-lg tw:overflow-hidden tw:bg-white tw:relative">
|
</h3>
|
||||||
<!-- Кнопка очистки (абсолютно позиционирована) -->
|
<p class="tw:text-yellow-700 tw:mb-4">
|
||||||
<div class="tw:absolute tw:top-4 tw:right-4 tw:z-20">
|
Эта форма была создана или изменена вручную в редакторе кода и не может быть отображена в визуальном редакторе.
|
||||||
<Button
|
</p>
|
||||||
label="Очистить"
|
<p class="tw:text-yellow-700 tw:mb-4">
|
||||||
icon="fa fa-trash"
|
Для работы с этой формой используйте режим "Код". Если вы хотите создать новую форму в визуальном редакторе, необходимо сбросить текущую форму.
|
||||||
severity="danger"
|
</p>
|
||||||
size="small"
|
<Button
|
||||||
text
|
label="Перейти в режим кода"
|
||||||
v-tooltip.left="'Удалить все поля и очистить форму'"
|
icon="fa fa-code"
|
||||||
@click="clearForm"
|
@click="activeMode = 'code'"
|
||||||
/>
|
class="tw:mr-2"
|
||||||
</div>
|
/>
|
||||||
|
<Button
|
||||||
<!-- Контент (FormCanvas) занимает все оставшееся пространство -->
|
label="Сбросить и создать новую"
|
||||||
<div class="tw:flex-1 tw:overflow-y-auto tw:relative">
|
icon="fa fa-trash"
|
||||||
<FormCanvas class="tw:min-h-full" />
|
severity="danger"
|
||||||
|
outlined
|
||||||
|
@click="showResetConfirmation"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Панель настроек (справа) -->
|
|
||||||
<div class="tw:w-80 tw:flex-shrink-0 tw:h-full tw:overflow-y-auto">
|
|
||||||
<Panel class="tw:min-h-full">
|
|
||||||
<template #header>
|
|
||||||
<span>Настройки поля</span>
|
|
||||||
</template>
|
|
||||||
<FieldSettings />
|
|
||||||
</Panel>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Обычный визуальный редактор для некстомных форм -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- Панель доступных полей -->
|
||||||
|
<div class="tw:w-64 tw:flex-shrink-0 tw:h-full tw:overflow-hidden">
|
||||||
|
<FieldsPanel class="tw:h-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Основная зона конструктора -->
|
||||||
|
<div class="tw:flex-1 tw:flex tw:gap-4 tw:overflow-hidden">
|
||||||
|
<!-- Зона формы -->
|
||||||
|
<div class="tw:flex-1 tw:flex tw:flex-col tw:border tw:border-gray-200 tw:rounded-lg tw:overflow-hidden tw:bg-white tw:relative">
|
||||||
|
<!-- Кнопка очистки (абсолютно позиционирована) -->
|
||||||
|
<div class="tw:absolute tw:top-4 tw:right-4 tw:z-20">
|
||||||
|
<Button
|
||||||
|
label="Очистить"
|
||||||
|
icon="fa fa-trash"
|
||||||
|
severity="danger"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
v-tooltip.left="'Удалить все поля и очистить форму'"
|
||||||
|
@click="clearForm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Контент (FormCanvas) занимает все оставшееся пространство -->
|
||||||
|
<div class="tw:flex-1 tw:overflow-y-auto tw:relative">
|
||||||
|
<FormCanvas class="tw:min-h-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель настроек (справа) -->
|
||||||
|
<div class="tw:w-80 tw:flex-shrink-0 tw:h-full tw:overflow-y-auto">
|
||||||
|
<Panel class="tw:min-h-full">
|
||||||
|
<template #header>
|
||||||
|
<span>Настройки поля</span>
|
||||||
|
</template>
|
||||||
|
<FieldSettings />
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Режим редактирования кода -->
|
<!-- Режим редактирования кода -->
|
||||||
@@ -87,7 +123,12 @@
|
|||||||
<template v-if="activeMode === 'preview'">
|
<template v-if="activeMode === 'preview'">
|
||||||
<div class="tw:flex-1 tw:flex tw:justify-center tw:overflow-auto">
|
<div class="tw:flex-1 tw:flex tw:justify-center tw:overflow-auto">
|
||||||
<div class="tw:w-full">
|
<div class="tw:w-full">
|
||||||
|
<div v-if="jsonError" class="tw:p-4 tw:bg-red-50 tw:border tw:border-red-200 tw:rounded tw:text-red-700">
|
||||||
|
<i class="fa fa-exclamation-circle tw:mr-2"></i>
|
||||||
|
Ошибка в схеме: {{ jsonError }}
|
||||||
|
</div>
|
||||||
<FormRenderer
|
<FormRenderer
|
||||||
|
v-else
|
||||||
:schema="formSchema"
|
:schema="formSchema"
|
||||||
submit-label="Отправить форму"
|
submit-label="Отправить форму"
|
||||||
@submit="handleFormSubmit"
|
@submit="handleFormSubmit"
|
||||||
@@ -101,7 +142,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, provide, computed, onMounted } from 'vue';
|
import { ref, provide, computed, onMounted, watch } from 'vue';
|
||||||
import { Button, Panel, SelectButton, useConfirm, ConfirmPopup, ConfirmDialog } from 'primevue';
|
import { Button, Panel, SelectButton, useConfirm, ConfirmPopup, ConfirmDialog } from 'primevue';
|
||||||
import FieldsPanel from '@/components/FormBuilder/FieldsPanel.vue';
|
import FieldsPanel from '@/components/FormBuilder/FieldsPanel.vue';
|
||||||
import FormCanvas from '@/components/FormBuilder/FormCanvas.vue';
|
import FormCanvas from '@/components/FormBuilder/FormCanvas.vue';
|
||||||
@@ -109,6 +150,8 @@ import FieldSettings from '@/components/FormBuilder/FieldSettings.vue';
|
|||||||
import FormRenderer from '@/components/FormBuilder/FormRenderer.vue';
|
import FormRenderer from '@/components/FormBuilder/FormRenderer.vue';
|
||||||
import CodeEditor from '@/components/FormBuilder/CodeEditor.vue';
|
import CodeEditor from '@/components/FormBuilder/CodeEditor.vue';
|
||||||
import { toastBus } from '@/utils/toastHelper';
|
import { toastBus } from '@/utils/toastHelper';
|
||||||
|
import { saveRevision } from './utils/revisionManager.js';
|
||||||
|
import { createEmptySchema } from './utils/schemaParser.js';
|
||||||
|
|
||||||
const formFields = defineModel({
|
const formFields = defineModel({
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -120,13 +163,19 @@ const isCustom = defineModel('isCustom', {
|
|||||||
default: false
|
default: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Локальные состояния (не сохраняются в БД)
|
||||||
|
const dirtyFromCode = ref(false);
|
||||||
|
const lastSyncedSchema = ref(null);
|
||||||
|
|
||||||
const activeMode = ref('visual');
|
const activeMode = ref('visual');
|
||||||
const jsonCode = ref('');
|
const jsonCode = ref('');
|
||||||
const originalJsonCode = ref(''); // Для отслеживания изменений в режиме кода
|
|
||||||
const jsonError = ref(null);
|
const jsonError = ref(null);
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const selectButtonKey = ref(0);
|
const selectButtonKey = ref(0);
|
||||||
|
|
||||||
|
// Алиас формы для сохранения ревизий (можно передавать через props, пока используем 'checkout')
|
||||||
|
const formAlias = 'checkout';
|
||||||
|
|
||||||
const modes = [
|
const modes = [
|
||||||
{ label: 'Визуальный', value: 'visual', icon: 'fa fa-th-large' },
|
{ label: 'Визуальный', value: 'visual', icon: 'fa fa-th-large' },
|
||||||
{ label: 'Код', value: 'code', icon: 'fa fa-code' },
|
{ label: 'Код', value: 'code', icon: 'fa fa-code' },
|
||||||
@@ -149,14 +198,49 @@ const formSchema = computed(() => {
|
|||||||
return formFields.value || [];
|
return formFields.value || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Инициализация режима при монтировании
|
// Инициализация при монтировании
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
initializeForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Инициализация формы при загрузке из БД
|
||||||
|
function initializeForm() {
|
||||||
|
const schema = formFields.value || [];
|
||||||
|
|
||||||
|
// Устанавливаем lastSyncedSchema равным загруженной схеме
|
||||||
|
lastSyncedSchema.value = JSON.parse(JSON.stringify(schema));
|
||||||
|
|
||||||
|
// dirtyFromCode всегда false при загрузке
|
||||||
|
dirtyFromCode.value = false;
|
||||||
|
|
||||||
|
// Определяем начальный режим на основе isCustom
|
||||||
if (isCustom.value) {
|
if (isCustom.value) {
|
||||||
activeMode.value = 'code';
|
activeMode.value = 'code';
|
||||||
jsonCode.value = JSON.stringify(formFields.value, null, 2);
|
jsonCode.value = JSON.stringify(schema, null, 2);
|
||||||
originalJsonCode.value = jsonCode.value;
|
|
||||||
} else {
|
} else {
|
||||||
activeMode.value = 'visual';
|
activeMode.value = 'visual';
|
||||||
|
jsonCode.value = JSON.stringify(schema, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отслеживание изменений в визуальном редакторе
|
||||||
|
watch(formFields, (newSchema) => {
|
||||||
|
// Если мы в визуальном режиме и форма не кастомная, обновляем lastSyncedSchema
|
||||||
|
if (activeMode.value === 'visual' && !dirtyFromCode.value && !isCustom.value) {
|
||||||
|
lastSyncedSchema.value = JSON.parse(JSON.stringify(newSchema));
|
||||||
|
// Обновляем jsonCode для синхронизации
|
||||||
|
jsonCode.value = JSON.stringify(newSchema, null, 2);
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// Отслеживание изменений isCustom для переинициализации
|
||||||
|
watch(isCustom, () => {
|
||||||
|
// При изменении isCustom извне (например, при загрузке из БД) переинициализируем
|
||||||
|
if (activeMode.value === 'code' && !isCustom.value) {
|
||||||
|
// Если isCustom стал false, но мы в режиме кода, это значит форма была сброшена
|
||||||
|
// Синхронизируем состояния
|
||||||
|
lastSyncedSchema.value = JSON.parse(JSON.stringify(formFields.value));
|
||||||
|
dirtyFromCode.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,106 +272,169 @@ function handleModeChange(newMode) {
|
|||||||
// Пытаемся распарсить JSON перед уходом
|
// Пытаемся распарсить JSON перед уходом
|
||||||
if (jsonError.value) {
|
if (jsonError.value) {
|
||||||
toastBus.emit('show', { severity: 'error', summary: 'Ошибка JSON', detail: 'Исправьте ошибки в JSON перед переключением режима' });
|
toastBus.emit('show', { severity: 'error', summary: 'Ошибка JSON', detail: 'Исправьте ошибки в JSON перед переключением режима' });
|
||||||
// Не обновляем activeMode, остаемся в code
|
|
||||||
cancelModeSwitch();
|
cancelModeSwitch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(jsonCode.value);
|
const parsed = JSON.parse(jsonCode.value);
|
||||||
|
|
||||||
if (hasDuplicateNames(parsed)) {
|
if (hasDuplicateNames(parsed)) {
|
||||||
toastBus.emit('show', { severity: 'error', summary: 'Ошибка валидации', detail: 'В схеме есть поля с одинаковыми именами (name). Исправьте их перед переключением.' });
|
toastBus.emit('show', { severity: 'error', summary: 'Ошибка валидации', detail: 'В схеме есть поля с одинаковыми именами (name). Исправьте их перед переключением.' });
|
||||||
cancelModeSwitch();
|
cancelModeSwitch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, были ли изменения в коде
|
// Если переключаемся в визуальный режим
|
||||||
let hasChanges = false;
|
if (newMode === 'visual') {
|
||||||
try {
|
// Проверяем, нужно ли показывать предупреждение
|
||||||
// Сравниваем распарсенные объекты, чтобы избежать ложных срабатываний из-за форматирования
|
const needsWarning = isCustom.value || dirtyFromCode.value;
|
||||||
const originalObj = JSON.parse(originalJsonCode.value);
|
|
||||||
hasChanges = JSON.stringify(parsed) !== JSON.stringify(originalObj);
|
|
||||||
} catch (e) {
|
|
||||||
hasChanges = true; // Если не смогли распарсить оригинал, считаем что изменения были (или ошибка)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если переключаемся в визуальный режим
|
if (needsWarning) {
|
||||||
if (newMode === 'visual') {
|
// Сохраняем ревизию перед деструктивной операцией
|
||||||
if (hasChanges) {
|
saveRevision(formAlias, formFields.value, 'reset_to_visual');
|
||||||
// Если были изменения, показываем предупреждение
|
|
||||||
confirm.require({
|
|
||||||
group: 'modeSwitch',
|
|
||||||
header: 'Предупреждение',
|
|
||||||
message: 'Вы внесли изменения в код вручную.\n\nПосле ручного изменения визуальный редактор может работать некорректно или не отображать все настройки.\n\nВы можете остаться в режиме кода или очистить форму и начать заново в визуальном редакторе.',
|
|
||||||
icon: 'fa fa-exclamation-triangle',
|
|
||||||
acceptLabel: 'Очистить и перейти',
|
|
||||||
rejectLabel: 'Остаться в коде',
|
|
||||||
acceptClass: 'p-button-danger',
|
|
||||||
accept: () => {
|
|
||||||
formFields.value = [];
|
|
||||||
selectedFieldId.value = null;
|
|
||||||
isCustom.value = false;
|
|
||||||
jsonCode.value = '[]';
|
|
||||||
|
|
||||||
// Успешный переход
|
// Показываем предупреждение
|
||||||
activeMode.value = 'visual';
|
confirm.require({
|
||||||
},
|
group: 'modeSwitch',
|
||||||
reject: () => {
|
header: 'Предупреждение',
|
||||||
// Остаемся в коде
|
message: isCustom.value
|
||||||
// activeMode.value НЕ обновляем, он остался 'code'
|
? 'Загруженная форма является кастомной и может содержать неподдерживаемые элементы.\n\nОткрытие визуального редактора приведет к полному сбросу формы. Все нестандартные настройки будут потеряны.'
|
||||||
cancelModeSwitch();
|
: 'Форма была изменена вручную в редакторе кода.\n\nВизуальный редактор не поддерживает все конструкции, и для его открытия потребуется полностью сбросить форму и создать новую. Все нестандартные настройки будут потеряны.',
|
||||||
}
|
icon: 'fa fa-exclamation-triangle',
|
||||||
});
|
acceptLabel: 'Сбросить и открыть визуальный редактор',
|
||||||
|
rejectLabel: 'Отменить',
|
||||||
|
acceptClass: 'p-button-danger',
|
||||||
|
accept: () => {
|
||||||
|
resetFormToVisual();
|
||||||
|
activeMode.value = 'visual';
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
cancelModeSwitch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Не обновляем activeMode сразу, ждем решения пользователя
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// Если изменений не было, просто переключаемся и сбрасываем флаг кастомного режима
|
|
||||||
isCustom.value = false;
|
|
||||||
formFields.value = parsed;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Переход в Preview - разрешаем обновление модели из кода
|
|
||||||
console.log('Updating formFields for Preview:', parsed);
|
|
||||||
formFields.value = parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Ошибка парсинга при переключении:', e);
|
|
||||||
toastBus.emit('show', { severity: 'error', summary: 'Ошибка', detail: 'Некорректный JSON' });
|
|
||||||
cancelModeSwitch();
|
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
// Если isCustom=false и dirtyFromCode=false, просто переключаемся
|
||||||
|
// Обновляем схему из кода
|
||||||
|
formFields.value = parsed;
|
||||||
|
lastSyncedSchema.value = JSON.parse(JSON.stringify(parsed));
|
||||||
|
dirtyFromCode.value = false;
|
||||||
|
}
|
||||||
|
} else if (newMode === 'preview') {
|
||||||
|
// Переход в Preview - обновляем модель из кода
|
||||||
|
formFields.value = parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка парсинга при переключении:', e);
|
||||||
|
toastBus.emit('show', { severity: 'error', summary: 'Ошибка', detail: 'Некорректный JSON' });
|
||||||
|
cancelModeSwitch();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если переключаемся В режим кода
|
// Если переключаемся В режим кода
|
||||||
if (newMode === 'code') {
|
if (newMode === 'code') {
|
||||||
isCustom.value = true;
|
// Обновляем jsonCode из текущей схемы
|
||||||
jsonCode.value = JSON.stringify(formFields.value, null, 2);
|
jsonCode.value = JSON.stringify(formFields.value, null, 2);
|
||||||
originalJsonCode.value = jsonCode.value; // Сохраняем исходное состояние
|
// Обновляем lastSyncedSchema, если мы пришли из визуального редактора
|
||||||
|
if (activeMode.value === 'visual') {
|
||||||
|
lastSyncedSchema.value = JSON.parse(JSON.stringify(formFields.value));
|
||||||
|
dirtyFromCode.value = false;
|
||||||
|
}
|
||||||
|
// isCustom не меняем автоматически при переходе в режим кода
|
||||||
|
// Он установится в true только когда пользователь реально изменит код (через handleJsonInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если переключаемся В визуальный режим из preview
|
||||||
|
if (newMode === 'visual' && activeMode.value === 'preview') {
|
||||||
|
// Никаких проверок, просто переключаемся
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем режим для успешных переходов
|
// Обновляем режим для успешных переходов
|
||||||
activeMode.value = newMode;
|
activeMode.value = newMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сбрасывает форму для визуального редактора
|
||||||
|
*/
|
||||||
|
function resetFormToVisual() {
|
||||||
|
// Сохраняем ревизию (уже сохранена выше, но на всякий случай)
|
||||||
|
saveRevision(formAlias, formFields.value, 'reset_to_visual');
|
||||||
|
|
||||||
|
// Создаем пустую схему
|
||||||
|
const emptySchema = createEmptySchema();
|
||||||
|
|
||||||
|
// Обновляем все состояния
|
||||||
|
formFields.value = emptySchema;
|
||||||
|
selectedFieldId.value = null;
|
||||||
|
isCustom.value = false;
|
||||||
|
jsonCode.value = JSON.stringify(emptySchema, null, 2);
|
||||||
|
lastSyncedSchema.value = JSON.parse(JSON.stringify(emptySchema));
|
||||||
|
dirtyFromCode.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показывает подтверждение сброса формы
|
||||||
|
*/
|
||||||
|
function showResetConfirmation() {
|
||||||
|
// Сохраняем ревизию перед деструктивной операцией
|
||||||
|
saveRevision(formAlias, formFields.value, 'reset_to_visual');
|
||||||
|
|
||||||
|
confirm.require({
|
||||||
|
group: 'modeSwitch',
|
||||||
|
header: 'Подтверждение сброса',
|
||||||
|
message: 'Вы уверены, что хотите сбросить кастомную форму и создать новую в визуальном редакторе?\n\nВсе текущие настройки будут потеряны.',
|
||||||
|
icon: 'fa fa-exclamation-triangle',
|
||||||
|
acceptLabel: 'Сбросить и создать новую',
|
||||||
|
rejectLabel: 'Отменить',
|
||||||
|
acceptClass: 'p-button-danger',
|
||||||
|
accept: () => {
|
||||||
|
resetFormToVisual();
|
||||||
|
activeMode.value = 'visual';
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
// Отменяем действие
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleJsonInput() {
|
function handleJsonInput() {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(jsonCode.value);
|
const parsed = JSON.parse(jsonCode.value);
|
||||||
|
|
||||||
if (hasDuplicateNames(parsed)) {
|
if (hasDuplicateNames(parsed)) {
|
||||||
jsonError.value = 'Ошибка: найдены поля с одинаковыми именами (name)';
|
jsonError.value = 'Ошибка: найдены поля с одинаковыми именами (name)';
|
||||||
} else {
|
} else {
|
||||||
jsonError.value = null;
|
jsonError.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем модель сразу, чтобы изменения не терялись
|
// Обновляем модель сразу, чтобы изменения не терялись
|
||||||
console.log('handleJsonInput: Updating formFields', parsed);
|
|
||||||
formFields.value = parsed;
|
formFields.value = parsed;
|
||||||
|
|
||||||
|
// Проверяем, изменилась ли схема относительно lastSyncedSchema
|
||||||
|
if (lastSyncedSchema.value !== null) {
|
||||||
|
const currentSchemaStr = JSON.stringify(parsed);
|
||||||
|
const lastSyncedStr = JSON.stringify(lastSyncedSchema.value);
|
||||||
|
const hasChanges = currentSchemaStr !== lastSyncedStr;
|
||||||
|
dirtyFromCode.value = hasChanges;
|
||||||
|
|
||||||
|
// Если есть изменения и форма еще не кастомная, устанавливаем isCustom=true
|
||||||
|
if (hasChanges && !isCustom.value) {
|
||||||
|
isCustom.value = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Если lastSyncedSchema еще не установлен, считаем что изменения есть
|
||||||
|
dirtyFromCode.value = true;
|
||||||
|
if (!isCustom.value) {
|
||||||
|
isCustom.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
jsonError.value = e.message;
|
jsonError.value = e.message;
|
||||||
|
// При ошибке парсинга не обновляем dirtyFromCode и isCustom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,9 +449,18 @@ function clearForm(event) {
|
|||||||
acceptClass: 'p-button-danger p-button-sm',
|
acceptClass: 'p-button-danger p-button-sm',
|
||||||
rejectClass: 'p-button-secondary p-button-sm p-button-text',
|
rejectClass: 'p-button-secondary p-button-sm p-button-text',
|
||||||
accept: () => {
|
accept: () => {
|
||||||
formFields.value = [];
|
// Сохраняем ревизию перед очисткой
|
||||||
|
saveRevision(formAlias, formFields.value, 'clear_form');
|
||||||
|
|
||||||
|
const emptySchema = createEmptySchema();
|
||||||
|
formFields.value = emptySchema;
|
||||||
selectedFieldId.value = null;
|
selectedFieldId.value = null;
|
||||||
jsonCode.value = '[]';
|
jsonCode.value = JSON.stringify(emptySchema, null, 2);
|
||||||
|
lastSyncedSchema.value = JSON.parse(JSON.stringify(emptySchema));
|
||||||
|
dirtyFromCode.value = false;
|
||||||
|
// После очистки в визуальном редакторе форма не кастомная
|
||||||
|
isCustom.value = false;
|
||||||
|
|
||||||
toastBus.emit('show', { severity: 'success', summary: 'Успешно', detail: 'Форма очищена' });
|
toastBus.emit('show', { severity: 'success', summary: 'Успешно', detail: 'Форма очищена' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,11 +58,16 @@
|
|||||||
<!-- Предпросмотр поля -->
|
<!-- Предпросмотр поля -->
|
||||||
<div class="tw:relative tw:z-0">
|
<div class="tw:relative tw:z-0">
|
||||||
<FormKit
|
<FormKit
|
||||||
|
v-if="field.$formkit"
|
||||||
:key="`${field.id}-${field.prefixIcon}-${field.suffixIcon}`"
|
:key="`${field.id}-${field.prefixIcon}-${field.suffixIcon}`"
|
||||||
:type="field.$formkit"
|
:type="field.$formkit"
|
||||||
v-bind="getFieldProps(field)"
|
v-bind="getFieldProps(field)"
|
||||||
:name="field.name || `field_${field.id}`"
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Утилита для управления ревизиями схемы формы
|
||||||
|
* Сохраняет предыдущие версии перед деструктивными операциями
|
||||||
|
*/
|
||||||
|
|
||||||
|
const REVISION_STORAGE_KEY = 'formBuilder_revisions';
|
||||||
|
const MAX_REVISIONS = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет ревизию схемы перед деструктивной операцией
|
||||||
|
* @param {string} formAlias - Алиас формы (например, 'checkout')
|
||||||
|
* @param {Array} schema - Схема формы для сохранения
|
||||||
|
* @param {string} reason - Причина сохранения (например, 'reset_to_visual')
|
||||||
|
*/
|
||||||
|
export function saveRevision(formAlias, schema, reason = 'unknown') {
|
||||||
|
try {
|
||||||
|
const revisions = getRevisions(formAlias);
|
||||||
|
const revision = {
|
||||||
|
id: `rev_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
schema: JSON.parse(JSON.stringify(schema)), // Deep clone
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
|
||||||
|
revisions.unshift(revision);
|
||||||
|
|
||||||
|
// Ограничиваем количество ревизий
|
||||||
|
if (revisions.length > MAX_REVISIONS) {
|
||||||
|
revisions.splice(MAX_REVISIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = getAllRevisions();
|
||||||
|
storage[formAlias] = revisions;
|
||||||
|
localStorage.setItem(REVISION_STORAGE_KEY, JSON.stringify(storage));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка сохранения ревизии:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает все ревизии для формы
|
||||||
|
* @param {string} formAlias - Алиас формы
|
||||||
|
* @returns {Array} Массив ревизий
|
||||||
|
*/
|
||||||
|
export function getRevisions(formAlias) {
|
||||||
|
try {
|
||||||
|
const storage = getAllRevisions();
|
||||||
|
return storage[formAlias] || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения ревизий:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает все ревизии для всех форм
|
||||||
|
* @returns {Object} Объект с ревизиями по алиасам форм
|
||||||
|
*/
|
||||||
|
function getAllRevisions() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(REVISION_STORAGE_KEY);
|
||||||
|
return stored ? JSON.parse(stored) : {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка чтения ревизий из localStorage:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Восстанавливает схему из ревизии
|
||||||
|
* @param {string} formAlias - Алиас формы
|
||||||
|
* @param {string} revisionId - ID ревизии
|
||||||
|
* @returns {Array|null} Схема формы или null, если ревизия не найдена
|
||||||
|
*/
|
||||||
|
export function restoreRevision(formAlias, revisionId) {
|
||||||
|
try {
|
||||||
|
const revisions = getRevisions(formAlias);
|
||||||
|
const revision = revisions.find(r => r.id === revisionId);
|
||||||
|
if (revision) {
|
||||||
|
return JSON.parse(JSON.stringify(revision.schema)); // Deep clone
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка восстановления ревизии:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаляет ревизию
|
||||||
|
* @param {string} formAlias - Алиас формы
|
||||||
|
* @param {string} revisionId - ID ревизии
|
||||||
|
*/
|
||||||
|
export function deleteRevision(formAlias, revisionId) {
|
||||||
|
try {
|
||||||
|
const revisions = getRevisions(formAlias);
|
||||||
|
const filtered = revisions.filter(r => r.id !== revisionId);
|
||||||
|
const storage = getAllRevisions();
|
||||||
|
storage[formAlias] = filtered;
|
||||||
|
localStorage.setItem(REVISION_STORAGE_KEY, JSON.stringify(storage));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка удаления ревизии:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает все ревизии для формы
|
||||||
|
* @param {string} formAlias - Алиас формы
|
||||||
|
*/
|
||||||
|
export function clearRevisions(formAlias) {
|
||||||
|
try {
|
||||||
|
const storage = getAllRevisions();
|
||||||
|
delete storage[formAlias];
|
||||||
|
localStorage.setItem(REVISION_STORAGE_KEY, JSON.stringify(storage));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка очистки ревизий:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Утилита для работы со схемами форм
|
||||||
|
* Упрощенная логика: если isCustom=true, схема несовместима с визуальным редактором
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет совместимость схемы с визуальным редактором
|
||||||
|
* @param {boolean} isCustom - Флаг кастомной формы
|
||||||
|
* @returns {boolean} true если схема совместима, false если нет
|
||||||
|
*/
|
||||||
|
export function isSchemaCompatible(isCustom) {
|
||||||
|
// Если форма кастомная, она несовместима с визуальным редактором
|
||||||
|
return !isCustom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает пустую схему для визуального редактора
|
||||||
|
* @returns {Array} Пустая схема
|
||||||
|
*/
|
||||||
|
export function createEmptySchema() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,12 @@ class SettingsHandler
|
|||||||
|
|
||||||
if ($forms) {
|
if ($forms) {
|
||||||
foreach ($forms as $form) {
|
foreach ($forms as $form) {
|
||||||
$schema = json_decode($form['schema'], true, 512, JSON_THROW_ON_ERROR);
|
try {
|
||||||
|
$schema = json_decode($form['schema'] ?? '[]', true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
} catch (\JsonException $exception) {
|
||||||
|
$schema = [];
|
||||||
|
}
|
||||||
|
|
||||||
$data['forms'][$form['alias']] = [
|
$data['forms'][$form['alias']] = [
|
||||||
'alias' => $form['alias'],
|
'alias' => $form['alias'],
|
||||||
'friendly_name' => $form['friendly_name'],
|
'friendly_name' => $form['friendly_name'],
|
||||||
|
|||||||
Reference in New Issue
Block a user