conflict-nuxt-4/app/components/auto-import/DropdownSelect.vue
2026-02-12 11:24:27 +03:30

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>