complete Complete User Request Submission section RAD-1
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
class: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white border border-gray-200 rounded-2xl p-4" :class="class">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
maximizable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large', 'xlarge'].includes(value)
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
dismissableMask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'hide', 'show', 'fullscreen-change'])
|
||||
|
||||
const isVisible = ref(props.visible)
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
watch(() => props.visible, (newValue) => {
|
||||
isVisible.value = newValue
|
||||
})
|
||||
|
||||
watch(isVisible, (newValue) => {
|
||||
emit('update:visible', newValue)
|
||||
if (newValue) {
|
||||
emit('show')
|
||||
nextTick(() => {
|
||||
checkFullscreenState()
|
||||
})
|
||||
} else {
|
||||
emit('hide')
|
||||
}
|
||||
})
|
||||
|
||||
const checkFullscreenState = () => {
|
||||
const dialog = document.querySelector('.p-dialog:not(.p-dialog-hidden)')
|
||||
if (dialog) {
|
||||
const rect = dialog.getBoundingClientRect()
|
||||
const isMaximized = rect.width >= window.innerWidth * 0.95 && rect.height >= window.innerHeight * 0.95
|
||||
|
||||
if (isMaximized !== isFullscreen.value) {
|
||||
isFullscreen.value = isMaximized
|
||||
emit('fullscreen-change', isMaximized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
if (isVisible.value) {
|
||||
checkFullscreenState()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
const hide = () => {
|
||||
isVisible.value = false
|
||||
}
|
||||
|
||||
const show = () => {
|
||||
isVisible.value = true
|
||||
}
|
||||
|
||||
const handleMaximize = () => {
|
||||
isFullscreen.value = true
|
||||
emit('fullscreen-change', true)
|
||||
}
|
||||
|
||||
const handleUnmaximize = () => {
|
||||
isFullscreen.value = false
|
||||
emit('fullscreen-change', false)
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
small: 'max-w-md',
|
||||
medium: 'max-w-lg',
|
||||
large: 'max-w-2xl',
|
||||
xlarge: 'max-w-3xl'
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="isVisible"
|
||||
:modal="modal"
|
||||
:closable="closable"
|
||||
:dismissableMask="dismissableMask"
|
||||
:maximizable="maximizable"
|
||||
@hide="hide"
|
||||
@show="show"
|
||||
@maximize="handleMaximize"
|
||||
@unmaximize="handleUnmaximize"
|
||||
:class="[!maximizable ? sizeClasses[size] : '', props.class]"
|
||||
:pt="{
|
||||
root: 'backdrop-blur-sm',
|
||||
header: 'border-b border-gray-200',
|
||||
content: 'p-0',
|
||||
footer: 'border-t border-gray-200',
|
||||
}"
|
||||
>
|
||||
<template #header v-if="title">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ title }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="p-6">
|
||||
<slot :isFullscreen="isFullscreen" />
|
||||
</div>
|
||||
|
||||
<template #footer v-if="$slots.footer">
|
||||
<div class="flex justify-end gap-3">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Array, Object],
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
optionValue: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
optionLabel: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'انتخاب کنید'
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchUrl: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
searchParam: {
|
||||
type: String,
|
||||
default: 'search'
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: '!text-xs !h-full'
|
||||
},
|
||||
pt: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
option: '!text-sm',
|
||||
placeholder: '!text-sm',
|
||||
label: '!pt-3'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'filter', 'change', 'update:options'])
|
||||
|
||||
const isLoading = ref(false)
|
||||
const searchTimeout = ref(null)
|
||||
|
||||
const handleFilter = (event) => {
|
||||
if (searchTimeout.value) {
|
||||
clearTimeout(searchTimeout.value)
|
||||
}
|
||||
|
||||
const searchValue = event.value ? event.value.trim() : ''
|
||||
|
||||
// If search is empty, make request immediately (no debounce)
|
||||
if (!searchValue) {
|
||||
isLoading.value = true
|
||||
|
||||
if (props.searchUrl) {
|
||||
axios.get(props.searchUrl, {
|
||||
params: {}
|
||||
}).then(response => {
|
||||
if (response.data.success) {
|
||||
emit('update:options', response.data.data)
|
||||
}
|
||||
isLoading.value = false
|
||||
}).catch(error => {
|
||||
console.error('Search error:', error)
|
||||
isLoading.value = false
|
||||
})
|
||||
} else {
|
||||
emit('filter', event)
|
||||
isLoading.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If search has value, use debounce
|
||||
isLoading.value = true
|
||||
|
||||
searchTimeout.value = setTimeout(() => {
|
||||
if (props.searchUrl) {
|
||||
const params = {}
|
||||
params[props.searchParam] = searchValue
|
||||
|
||||
axios.get(props.searchUrl, {
|
||||
params: params
|
||||
}).then(response => {
|
||||
if (response.data.success) {
|
||||
const newOptions = response.data.data
|
||||
const currentValue = props.modelValue
|
||||
const currentOption = props.options.find(opt => {
|
||||
const value = typeof opt === 'object' ? opt[props.optionValue] : opt
|
||||
return value == currentValue
|
||||
})
|
||||
|
||||
let finalOptions = newOptions
|
||||
if (currentOption && !newOptions.find(opt => {
|
||||
const value = typeof opt === 'object' ? opt[props.optionValue] : opt
|
||||
return value == currentValue
|
||||
})) {
|
||||
finalOptions = [currentOption, ...newOptions]
|
||||
}
|
||||
|
||||
emit('update:options', finalOptions)
|
||||
}
|
||||
isLoading.value = false
|
||||
}).catch(error => {
|
||||
console.error('Search error:', error)
|
||||
isLoading.value = false
|
||||
})
|
||||
} else {
|
||||
emit('filter', event)
|
||||
isLoading.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleChange = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
return () => {
|
||||
if (searchTimeout.value) {
|
||||
clearTimeout(searchTimeout.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select
|
||||
:class="class"
|
||||
:placeholder="placeholder"
|
||||
:modelValue="modelValue"
|
||||
:options="options"
|
||||
:optionValue="optionValue"
|
||||
:optionLabel="optionLabel"
|
||||
:multiple="multiple"
|
||||
:filter="filter"
|
||||
:loading="loading || isLoading"
|
||||
:pt="pt"
|
||||
@update:modelValue="handleChange"
|
||||
@filter="handleFilter"
|
||||
/>
|
||||
</template>
|
||||
Reference in New Issue
Block a user