Files
megatest/src/components/FormBuilder/FieldConfigModal.vue
T

448 lines
13 KiB
Vue
Raw Normal View History

2026-03-23 00:47:20 +03:30
<template>
<a-modal
:open="open"
:title="modalTitle"
:ok-text="mode === 'create' ? 'ثبت المان' : 'ذخیره تغییرات'"
cancel-text="انصراف"
width="720px"
centered
destroy-on-close
@ok="handleSubmit"
@cancel="handleCancel"
>
<div v-if="localDraft" class="space-y-5">
<!-- اطلاعات عمومی -->
<section class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<h3 class="text-sm font-semibold text-slate-800 mb-4">تنظیمات عمومی</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<label class="block text-xs font-medium text-slate-600 mb-2">
عنوان فیلد
</label>
<a-input
v-model:value="localDraft.label"
size="large"
placeholder="مثلاً نام کاربر"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-2">
عرض
</label>
<a-input-number
v-model:value="localDraft.width"
size="large"
class="!w-full"
:min="120"
:max="900"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-2">
ارتفاع
</label>
<a-input-number
v-model:value="localDraft.height"
size="large"
class="!w-full"
:min="48"
:max="300"
/>
</div>
</div>
</section>
<!-- INPUT -->
<section
v-if="localDraft.type === 'input'"
class="rounded-xl border border-slate-200 bg-white p-4"
>
<h3 class="text-sm font-semibold text-slate-800 mb-4">تنظیمات Input</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-slate-600 mb-2">
نوع ورودی
</label>
<a-select
v-model:value="localDraft.inputType"
size="large"
:options="inputTypeOptions"
@change="handleInputTypeChange"
/>
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 text-sm text-slate-700">
<a-checkbox v-model:checked="localDraft.required" />
<span>فیلد اجباری باشد</span>
</label>
</div>
<template v-if="localDraft.inputType === 'text'">
<div>
<label class="block text-xs font-medium text-slate-600 mb-2">
حداقل طول متن
</label>
<a-input-number
v-model:value="localDraft.minLength"
size="large"
class="!w-full"
:min="0"
:max="1000"
placeholder="مثلاً 3"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-2">
حداکثر طول متن
</label>
<a-input-number
v-model:value="localDraft.maxLength"
size="large"
class="!w-full"
:min="0"
:max="1000"
placeholder="مثلاً 50"
/>
</div>
</template>
<template v-if="localDraft.inputType === 'number'">
<div>
<label class="block text-xs font-medium text-slate-600 mb-2">
حداقل مقدار
</label>
<a-input-number
v-model:value="localDraft.min"
size="large"
class="!w-full"
placeholder="مثلاً 0"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-2">
حداکثر مقدار
</label>
<a-input-number
v-model:value="localDraft.max"
size="large"
class="!w-full"
placeholder="مثلاً 100"
/>
</div>
<div class="md:col-span-2">
<label class="block text-xs font-medium text-slate-600 mb-2">
Step
</label>
<a-input-number
v-model:value="localDraft.step"
size="large"
class="!w-full"
:min="1"
placeholder="مثلاً 1"
/>
</div>
</template>
</div>
</section>
<!-- SELECT -->
<section
v-else-if="localDraft.type === 'select'"
class="rounded-xl border border-slate-200 bg-white p-4"
>
<h3 class="text-sm font-semibold text-slate-800 mb-4">تنظیمات Select</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="flex items-end">
<label class="flex items-center gap-2 text-sm text-slate-700">
<a-checkbox v-model:checked="localDraft.required" />
<span>فیلد اجباری باشد</span>
</label>
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 text-sm text-slate-700">
<a-checkbox v-model:checked="localDraft.multiple" />
<span>انتخاب چندتایی فعال باشد</span>
</label>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between gap-3">
<label class="block text-xs font-medium text-slate-600">
گزینه‌ها
</label>
<a-button size="small" type="dashed" @click="addOption">
افزودن گزینه
</a-button>
</div>
<div
v-for="(option, index) in localDraft.options"
:key="`option-${index}`"
class="flex items-center gap-2"
>
<a-input
v-model:value="localDraft.options[index]"
size="large"
:placeholder="`گزینه ${index + 1}`"
/>
<a-button
danger
@click="removeOption(index)"
:disabled="(localDraft.options?.length || 0) <= 1"
>
حذف
</a-button>
</div>
</div>
</section>
<!-- SLIDER -->
<section
v-else-if="localDraft.type === 'slider'"
class="rounded-xl border border-slate-200 bg-white p-4"
>
<h3 class="text-sm font-semibold text-slate-800 mb-4">تنظیمات Slider</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-end md:col-span-2">
<label class="flex items-center gap-2 text-sm text-slate-700">
<a-checkbox v-model:checked="localDraft.required" />
<span>فیلد اجباری باشد</span>
</label>
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-2">
حداقل مقدار
</label>
<a-input-number
v-model:value="localDraft.min"
size="large"
class="!w-full"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-2">
حداکثر مقدار
</label>
<a-input-number
v-model:value="localDraft.max"
size="large"
class="!w-full"
/>
</div>
<div class="md:col-span-2">
<label class="block text-xs font-medium text-slate-600 mb-2">
Step
</label>
<a-input-number
v-model:value="localDraft.step"
size="large"
class="!w-full"
:min="1"
/>
</div>
</div>
</section>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { message } from 'ant-design-vue'
import { cloneField, sanitizeField } from '@/handler/formBuilder.handler'
import {FormFieldConfig} from "@/models/form";
const props = defineProps<{
open: boolean
mode: 'create' | 'edit'
field: FormFieldConfig | null
}>()
const emit = defineEmits<{
(e: 'submit', field: FormFieldConfig): void
(e: 'cancel'): void
(e: 'update:open', value: boolean): void
}>()
const localDraft = ref<FormFieldConfig | null>(null)
const inputTypeOptions = [
{ label: 'متنی', value: 'text' },
{ label: 'عددی', value: 'number' },
{ label: 'رنگ', value: 'color' },
{ label: 'چک‌باکس', value: 'checkbox' },
{ label: 'رادیوباتن', value: 'radio' },
]
const modalTitle = computed(() => {
if (!localDraft.value) return 'تنظیمات المان'
const action = props.mode === 'create' ? 'افزودن' : 'ویرایش'
if (localDraft.value.type === 'input') return `${action} Input`
if (localDraft.value.type === 'select') return `${action} Select`
return `${action} Slider`
})
watch(
() => props.field,
(value) => {
localDraft.value = value ? cloneField(value) : null
if (localDraft.value?.type === 'select' && !localDraft.value.options?.length) {
localDraft.value.options = ['گزینه ۱']
}
},
{ immediate: true }
)
function handleInputTypeChange() {
if (!localDraft.value || localDraft.value.type !== 'input') return
if (localDraft.value.inputType !== 'text') {
localDraft.value.minLength = undefined
localDraft.value.maxLength = undefined
}
if (localDraft.value.inputType !== 'number') {
localDraft.value.min = undefined
localDraft.value.max = undefined
localDraft.value.step = undefined
}
if (localDraft.value.inputType === 'number' && typeof localDraft.value.step !== 'number') {
localDraft.value.step = 1
}
}
function addOption() {
if (!localDraft.value || localDraft.value.type !== 'select') return
if (!localDraft.value.options) {
localDraft.value.options = []
}
localDraft.value.options.push(`گزینه ${localDraft.value.options.length + 1}`)
}
function removeOption(index: number) {
if (!localDraft.value || localDraft.value.type !== 'select') return
if (!localDraft.value.options) return
if (localDraft.value.options.length <= 1) return
localDraft.value.options.splice(index, 1)
}
function validateDraft(field: FormFieldConfig) {
if (!field.label?.trim()) {
message.error('عنوان فیلد را وارد کنید')
return false
}
if (!field.width || field.width < 120) {
message.error('عرض فیلد باید حداقل 120 باشد')
return false
}
if (!field.height || field.height < 48) {
message.error('ارتفاع فیلد باید حداقل 48 باشد')
return false
}
if (field.type === 'input') {
if (!field.inputType) {
message.error('نوع input را انتخاب کنید')
return false
}
if (
field.inputType === 'text' &&
typeof field.minLength === 'number' &&
typeof field.maxLength === 'number' &&
field.minLength > field.maxLength
) {
message.error('حداقل طول متن نمی‌تواند از حداکثر بیشتر باشد')
return false
}
if (
field.inputType === 'number' &&
typeof field.min === 'number' &&
typeof field.max === 'number' &&
field.min > field.max
) {
message.error('حداقل مقدار نمی‌تواند از حداکثر بیشتر باشد')
return false
}
if (
field.inputType === 'number' &&
typeof field.step === 'number' &&
field.step <= 0
) {
message.error('step باید بزرگ‌تر از صفر باشد')
return false
}
}
if (field.type === 'select') {
const validOptions = (field.options || []).map((item) => item.trim()).filter(Boolean)
if (!validOptions.length) {
message.error('حداقل یک گزینه برای select وارد کنید')
return false
}
}
if (field.type === 'slider') {
if (
typeof field.min === 'number' &&
typeof field.max === 'number' &&
field.min >= field.max
) {
message.error('مقدار max باید از min بزرگ‌تر باشد')
return false
}
if (typeof field.step === 'number' && field.step <= 0) {
message.error('step باید بزرگ‌تر از صفر باشد')
return false
}
}
return true
}
function handleSubmit() {
if (!localDraft.value) return
const cleaned = sanitizeField(cloneField(localDraft.value))
if (!validateDraft(cleaned)) return
emit('submit', cleaned)
emit('update:open', false)
}
function handleCancel() {
emit('cancel')
emit('update:open', false)
}
</script>