191 lines
5.3 KiB
Vue
Executable File
191 lines
5.3 KiB
Vue
Executable File
<template>
|
||
<div class="my-select">
|
||
<USelect
|
||
v-model="selectedValue"
|
||
:items="formattedItems"
|
||
:placeholder="selectSchema.placeholder || 'انتخاب کنید'"
|
||
:multiple="selectSchema.multiple || false"
|
||
:disabled="selectSchema.disabled || loading"
|
||
:loading="loading"
|
||
@update:modelValue="onModelUpdate"
|
||
:class="[gridColumnClass, 'cursor-pointer']"
|
||
:style="selectWidthStyle"
|
||
:ui="{ wrapper: 'w-full', base: 'w-full' }"
|
||
dir="rtl"
|
||
color="primary"
|
||
size="xl"
|
||
highlight
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, watch } from "vue";
|
||
import { useCachedRequest } from "@/composables/useCachedRequest";
|
||
|
||
const props = defineProps({
|
||
selectSchema: { type: Object, default: () => ({}) },
|
||
gridColumnClass: { type: String, default: "" },
|
||
});
|
||
|
||
const emit = defineEmits(["my-select-action"]);
|
||
const emitAction = (action, payload) => {
|
||
emit("my-select-action", { action, payload });
|
||
};
|
||
|
||
const selectedValue = ref(null);
|
||
const rawItems = ref([]);
|
||
const loading = ref(false);
|
||
const lastEmittedValue = ref(null);
|
||
|
||
const { fetchRequest } = useCachedRequest();
|
||
const mode = computed(() => props.selectSchema.mode ?? "local");
|
||
|
||
const formattedItems = computed(() => {
|
||
const labelKey = props.selectSchema.labelKey || "name";
|
||
const valueKey = props.selectSchema.valueKey || "id";
|
||
|
||
return rawItems.value.map((item) => {
|
||
if (typeof item === "string") return { label: item, value: item };
|
||
return {
|
||
label:
|
||
item[labelKey] ?? item.name ?? item.title ?? item.label ?? "بدون عنوان",
|
||
value: item[valueKey] ?? item.id ?? item._id ?? item.value,
|
||
};
|
||
});
|
||
});
|
||
const selectWidthStyle = computed(() => {
|
||
return props.selectSchema.width ? { width: props.selectSchema.width } : {};
|
||
});
|
||
|
||
// مقایسه امن برای جلوگیری از emit تکراری
|
||
const isSameValue = (a, b) => {
|
||
if (Array.isArray(a) && Array.isArray(b)) {
|
||
if (a.length !== b.length) return false;
|
||
return a.every((v, i) => v === b[i]);
|
||
}
|
||
return a === b;
|
||
};
|
||
|
||
const emitSelected = (val) => {
|
||
if (isSameValue(val, lastEmittedValue.value)) return;
|
||
|
||
lastEmittedValue.value = Array.isArray(val) ? [...val] : val;
|
||
|
||
const selectedItem = formattedItems.value.find((i) =>
|
||
props.selectSchema.multiple ? val?.includes(i.value) : i.value === val,
|
||
);
|
||
|
||
emitAction("selected", selectedItem);
|
||
};
|
||
|
||
const syncDefaultSelection = () => {
|
||
if (props.selectSchema.value != null) return;
|
||
if (!formattedItems.value.length) return;
|
||
|
||
const isEmpty =
|
||
selectedValue.value == null ||
|
||
(Array.isArray(selectedValue.value) && !selectedValue.value.length);
|
||
|
||
if (isEmpty) {
|
||
const firstItem = formattedItems.value[0];
|
||
const newVal = props.selectSchema.multiple
|
||
? [firstItem.value]
|
||
: firstItem.value;
|
||
|
||
selectedValue.value = newVal;
|
||
emitSelected(newVal);
|
||
}
|
||
};
|
||
|
||
const onModelUpdate = (val) => {
|
||
emitSelected(val);
|
||
};
|
||
|
||
const fetchItems = async () => {
|
||
if (mode.value !== "api") {
|
||
rawItems.value =
|
||
props.selectSchema.options ?? props.selectSchema.items ?? [];
|
||
syncDefaultSelection();
|
||
return;
|
||
}
|
||
|
||
const apiConfig = props.selectSchema.apiConfig || {};
|
||
if (!apiConfig.url) return;
|
||
|
||
loading.value = true;
|
||
try {
|
||
const data = await fetchRequest(apiConfig);
|
||
rawItems.value = Array.isArray(data) ? data : (data?.data ?? []);
|
||
syncDefaultSelection();
|
||
emitAction("fetch:success", { items: rawItems.value });
|
||
} catch (error) {
|
||
emitAction("fetch:error", { error: error?.message ?? error });
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
watch(
|
||
() => [props.selectSchema.items, props.selectSchema.options],
|
||
() => {
|
||
if (mode.value !== "api") {
|
||
rawItems.value =
|
||
props.selectSchema.options ?? props.selectSchema.items ?? [];
|
||
syncDefaultSelection();
|
||
}
|
||
},
|
||
{ immediate: true, deep: true },
|
||
);
|
||
|
||
watch(
|
||
() => props.selectSchema.value,
|
||
(val) => {
|
||
selectedValue.value = val ?? null;
|
||
emitSelected(val);
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
onMounted(fetchItems);
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.my-select {
|
||
button[role="combobox"] {
|
||
direction: rtl; /* راستچین کردن متن و placeholder */
|
||
text-align: right; /* ترازبندی متن داخل button */
|
||
}
|
||
|
||
/* 2. placeholder */
|
||
button[role="combobox"] [data-slot="placeholder"] {
|
||
text-align: right;
|
||
direction: rtl;
|
||
}
|
||
|
||
/* 3. آیکون پایین (chevron) */
|
||
button[role="combobox"] [data-slot="trailing"] {
|
||
right: auto; /* اگر نیاز باشد موقعیت icon را اصلاح کنید */
|
||
left: 0; /* icon به سمت چپ میرود */
|
||
}
|
||
|
||
/* 4. منوی بازشونده (اگر teleport شده باشد) */
|
||
/* کلاس واقعی منو را از inspector پیدا کنید و به جای .u-select__menu بنویسید */
|
||
.u-select__menu {
|
||
direction: rtl !important;
|
||
text-align: right !important;
|
||
}
|
||
|
||
/* 5. آیتمهای منو */
|
||
.u-select__menu [data-slot="item"] {
|
||
text-align: right;
|
||
direction: rtl;
|
||
display: flex;
|
||
flex-direction: row-reverse; /* اگر icon ها در آیتم هستند آنها را برعکس میکند */
|
||
}
|
||
}
|
||
.i-lucide\:check {
|
||
display: none;
|
||
}
|
||
</style>
|