342 lines
10 KiB
Vue
Executable File
342 lines
10 KiB
Vue
Executable File
<template>
|
|
<div :class="gridColumnClass">
|
|
<!-- Input Menu -->
|
|
<UInputMenu
|
|
v-if="selectSchema.selectType === 'input'"
|
|
:model-value="resolvedModelValue"
|
|
:items="normalizedItems"
|
|
:multiple="selectSchema.multiple"
|
|
:searchable="selectSchema.searchable"
|
|
:creatable="selectSchema.creatable"
|
|
:placeholder="selectSchema.placeholder"
|
|
:option-attribute="selectSchema.optionAttribute"
|
|
:value-attribute="selectSchema.valueAttribute"
|
|
:by="selectSchema.compareBy"
|
|
v-bind="selectSchema.additionalProps"
|
|
@update:model-value="onChange"
|
|
@update:open="emitEvent('open', $event)"
|
|
@update:search-term="handleSearchTerm"
|
|
@create="emitEvent('create', $event)"
|
|
:class="[gridColumnClass, 'cursor-pointer w-full']"
|
|
size="xl"
|
|
dir="rtl"
|
|
color="primary"
|
|
highlight
|
|
:ui="{ trailing: 'hidden' }"
|
|
>
|
|
<!-- Forward Slots -->
|
|
<template
|
|
v-for="(_, slotName) in $slots"
|
|
:key="slotName"
|
|
v-slot:[slotName]="slotProps"
|
|
>
|
|
<slot :name="slotName" v-bind="slotProps" />
|
|
</template>
|
|
|
|
<template #empty>
|
|
<slot name="empty">دادهای پیدا نشد</slot>
|
|
</template>
|
|
|
|
<template #loading>
|
|
<slot name="loading">در حال بارگذاری...</slot>
|
|
</template>
|
|
</UInputMenu>
|
|
|
|
<!-- Select Menu -->
|
|
<USelectMenu
|
|
v-if="selectSchema.selectType === 'select'"
|
|
:model-value="resolvedModelValue"
|
|
:items="normalizedItems"
|
|
:multiple="selectSchema.multiple"
|
|
:searchable="selectSchema.searchable"
|
|
:creatable="selectSchema.creatable"
|
|
:loading="isLoading"
|
|
:placeholder="selectSchema.placeholder"
|
|
:option-attribute="selectSchema.optionAttribute"
|
|
:value-attribute="selectSchema.valueAttribute"
|
|
:by="selectSchema.compareBy"
|
|
v-bind="selectSchema.additionalProps"
|
|
@update:model-value="onChange"
|
|
@update:open="emitEvent('open', $event)"
|
|
@update:search-term="handleSearchTerm"
|
|
@create="emitEvent('create', $event)"
|
|
:class="[gridColumnClass, 'cursor-pointer']"
|
|
:ui="{ wrapper: 'w-full', base: 'w-full' }"
|
|
dir="rtl"
|
|
color="primary"
|
|
size="xl"
|
|
highlight
|
|
>
|
|
<!-- Forward Slots -->
|
|
<template
|
|
v-for="(_, slotName) in $slots"
|
|
:key="slotName"
|
|
v-slot:[slotName]="slotProps"
|
|
>
|
|
<slot :name="slotName" v-bind="slotProps" />
|
|
</template>
|
|
|
|
<template #empty>
|
|
<slot name="empty">دادهای پیدا نشد</slot>
|
|
</template>
|
|
|
|
<template #loading>
|
|
<slot name="loading">در حال بارگذاری...</slot>
|
|
</template>
|
|
</USelectMenu>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from "vue";
|
|
import { debounce } from "lodash-es";
|
|
|
|
const props = defineProps({
|
|
dropdownSchema: { type: Object, required: true },
|
|
});
|
|
|
|
const emit = defineEmits(["dropdownSelectEvents"]);
|
|
const { $http: httpService } = useNuxtApp();
|
|
|
|
const isLoading = ref(false);
|
|
const apiItems = ref([]);
|
|
|
|
/* ---------------- SCHEMA BASE ---------------- */
|
|
const selectSchema = computed(() => {
|
|
const i = props.dropdownSchema || {};
|
|
|
|
return {
|
|
fieldId: i.key || i.name || i.id || null,
|
|
modelValue: i.modelValue ?? i.value ?? null,
|
|
selectType: i.selectType ?? "select",
|
|
items: Array.isArray(i.items)
|
|
? i.items
|
|
: Array.isArray(i.options)
|
|
? i.options
|
|
: [],
|
|
optionAttribute: i.optionAttribute ?? "label",
|
|
valueAttribute: i.valueAttribute ?? "value",
|
|
multiple:
|
|
i.multiple === true || i.multi_select === 1 || i.multi_select === true,
|
|
searchable: i.searchable ?? true,
|
|
creatable: i.creatable ?? i.allowCreate ?? false,
|
|
loading: i.loading ?? false,
|
|
placeholder: i.placeholder || "لطفاً انتخاب کنید...",
|
|
compareBy: i.compareBy,
|
|
additionalProps: i.additionalProps || i.extraProps || {},
|
|
columnSpan: resolveColSpan(i),
|
|
mode: i.mode || "default",
|
|
apiConfig: i.apiConfig || {},
|
|
};
|
|
});
|
|
|
|
/* ---------------- GRID ---------------- */
|
|
function resolveColSpan(input) {
|
|
let span = 12;
|
|
if (input.colSpan) span = input.colSpan;
|
|
if (input.colClass?.includes("col-")) {
|
|
const m = input.colClass.match(/col-(\d+)/);
|
|
if (m) span = Number(m[1]);
|
|
}
|
|
return Math.min(12, Math.max(1, span));
|
|
}
|
|
|
|
const gridColumnClass = computed(
|
|
() => `col-span-${selectSchema.value.columnSpan}`
|
|
);
|
|
|
|
/* ---------------- NORMALIZE ITEMS ---------------- */
|
|
const normalizedItems = computed(() => {
|
|
const { optionAttribute, valueAttribute, mode } = selectSchema.value;
|
|
const sourceItems =
|
|
mode === "api" ? apiItems.value : selectSchema.value.items;
|
|
|
|
return sourceItems.map((item) => {
|
|
if (typeof item !== "object") {
|
|
return {
|
|
[optionAttribute]: String(item),
|
|
[valueAttribute]: item,
|
|
raw: item,
|
|
};
|
|
}
|
|
return { ...item, raw: item };
|
|
});
|
|
});
|
|
|
|
/* ---------------- RESOLVED MODEL VALUE ---------------- */
|
|
const resolvedModelValue = computed(() => {
|
|
const { modelValue, multiple } = selectSchema.value;
|
|
|
|
if (multiple && Array.isArray(modelValue)) {
|
|
return modelValue.map(resolveSingleValue).filter(Boolean);
|
|
}
|
|
return resolveSingleValue(modelValue);
|
|
});
|
|
|
|
function resolveSingleValue(val) {
|
|
if (val === null || val === undefined) return null;
|
|
if (typeof val === "object") return val;
|
|
|
|
let item = normalizedItems.value.find(
|
|
(i) => i?.[selectSchema.value.valueAttribute] === val
|
|
);
|
|
|
|
// اگر پیدا نشد و mode API است → آیتم موقت اضافه کن
|
|
if (!item && selectSchema.value.mode === "api") {
|
|
item = {
|
|
[selectSchema.value.optionAttribute]: val,
|
|
[selectSchema.value.valueAttribute]: val,
|
|
raw: val,
|
|
__temp: true,
|
|
};
|
|
apiItems.value.push(item);
|
|
}
|
|
|
|
return item || null;
|
|
}
|
|
|
|
/* ---------------- EMIT HANDLERS ---------------- */
|
|
function emitEvent(action, payload) {
|
|
emit("dropdownSelectEvents", {
|
|
action,
|
|
payload,
|
|
fieldId: selectSchema.value.fieldId,
|
|
timestamp: Date.now(),
|
|
});
|
|
}
|
|
|
|
function onChange(val) {
|
|
// اضافه کردن به apiItems اگر وجود ندارد
|
|
if (selectSchema.value.mode === "api") {
|
|
const values = Array.isArray(val) ? val : [val];
|
|
values.forEach((v) => {
|
|
const valueKey =
|
|
typeof v === "object" ? v[selectSchema.value.valueAttribute] : v;
|
|
if (
|
|
!apiItems.value.find(
|
|
(i) => i[selectSchema.value.valueAttribute] === valueKey
|
|
)
|
|
) {
|
|
apiItems.value.push(
|
|
typeof v === "object"
|
|
? v
|
|
: {
|
|
[selectSchema.value.optionAttribute]: valueKey,
|
|
[selectSchema.value.valueAttribute]: valueKey,
|
|
raw: v,
|
|
}
|
|
);
|
|
}
|
|
});
|
|
}
|
|
props.dropdownSchema.modelValue = val;
|
|
emitEvent("change", val);
|
|
}
|
|
watch(
|
|
() => normalizedItems.value.length,
|
|
(len) => {
|
|
if (!len) return;
|
|
|
|
const { multiple, modelValue, selectType } = selectSchema.value;
|
|
if (selectType !== "select") return;
|
|
|
|
if (
|
|
(multiple && Array.isArray(modelValue) && modelValue.length) ||
|
|
(!multiple && modelValue != null)
|
|
)
|
|
return;
|
|
|
|
onChange(multiple ? [normalizedItems.value[0]] : normalizedItems.value[0]);
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
/* ---------------- SEARCH WITH DEBOUNCE ---------------- */
|
|
const debouncedFetch = debounce(
|
|
async (searchText, url, crud = "GET", payload = "") => {
|
|
if (!searchText) {
|
|
apiItems.value = [];
|
|
isLoading.value = false;
|
|
return;
|
|
}
|
|
|
|
const { searchField = "name" } = selectSchema.value.apiConfig;
|
|
url = url.replace("{{filter}}", searchText);
|
|
payload = payload?.replace("{{filter}}", searchText);
|
|
|
|
isLoading.value = true;
|
|
|
|
if (crud === "GET") {
|
|
httpService
|
|
.getRequest(url)
|
|
.then((res) => {
|
|
const mapped = res
|
|
.filter((item) =>
|
|
String(item?.[searchField] || "")
|
|
.toLowerCase()
|
|
.includes(searchText.toLowerCase())
|
|
)
|
|
.map((item) => ({
|
|
[selectSchema.value.optionAttribute]: item[searchField],
|
|
[selectSchema.value.valueAttribute]: item.id ?? item[searchField],
|
|
raw: item,
|
|
}));
|
|
|
|
// جایگزینی آیتم موقت با آیتم واقعی
|
|
apiItems.value = apiItems.value.map((existing) =>
|
|
existing.__temp
|
|
? mapped.find(
|
|
(m) =>
|
|
m[selectSchema.value.valueAttribute] ===
|
|
existing[selectSchema.value.valueAttribute]
|
|
) || existing
|
|
: existing
|
|
);
|
|
|
|
// اضافه کردن آیتمهای جدید
|
|
mapped.forEach((m) => {
|
|
if (
|
|
!apiItems.value.find(
|
|
(i) =>
|
|
i[selectSchema.value.valueAttribute] ===
|
|
m[selectSchema.value.valueAttribute]
|
|
)
|
|
) {
|
|
apiItems.value.push(m);
|
|
}
|
|
});
|
|
})
|
|
.catch((error) =>
|
|
console.error("Remote search API error:", crud, error)
|
|
)
|
|
.finally(() => (isLoading.value = false));
|
|
} else if (crud === "POST") {
|
|
let payload_obj = payload ? JSON.parse(payload) : {};
|
|
httpService
|
|
.postRequest(url, payload_obj)
|
|
.then((res) => {
|
|
apiItems.value = res.data;
|
|
})
|
|
.catch((error) =>
|
|
console.error("Remote search API error:", crud, error)
|
|
)
|
|
.finally(() => (isLoading.value = false));
|
|
}
|
|
},
|
|
300
|
|
);
|
|
|
|
function handleSearchTerm(searchText) {
|
|
emitEvent("search", searchText);
|
|
|
|
const { mode, apiConfig } = selectSchema.value;
|
|
if (mode === "api" && apiConfig?.url) {
|
|
debouncedFetch(
|
|
searchText,
|
|
apiConfig.url,
|
|
apiConfig.crud,
|
|
apiConfig.payload
|
|
);
|
|
}
|
|
}
|
|
</script>
|