Files
megatest/src/components/FormBuilder/FieldConfigModal.vue
T
2026-03-23 00:47:20 +03:30

448 lines
13 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
<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>