448 lines
13 KiB
Vue
448 lines
13 KiB
Vue
<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> |