271 lines
7.8 KiB
Vue
Executable File
271 lines
7.8 KiB
Vue
Executable File
<!-- components/myPagination.vue -->
|
|
<template>
|
|
<div
|
|
class="jahat-pagination flex items-center flex-wrap border-t border-gray-200 p-2 rounded-none mt-2"
|
|
:class="pagination.alignment"
|
|
>
|
|
<!-- نمایش محدوده رکوردها و انتخاب تعداد سطر -->
|
|
<div v-if="showTotalRecords" class="flex items-center mb-0 mr-3 text-sm">
|
|
<div class="hidden md:block">
|
|
<span>{{ recordRange.start }} </span>
|
|
<span>-</span>
|
|
<span> {{ recordRange.end }}</span>
|
|
<span> از </span>
|
|
</div>
|
|
<span>{{ totalRecords }}</span>
|
|
<span> رکورد</span>
|
|
|
|
<label for="pagination-limit" class="ml-2 mr-4 mb-0 whitespace-nowrap">
|
|
<span class="hidden md:block"> تعداد سطرها </span>
|
|
<span class="md:hidden"> سطرها </span>
|
|
</label>
|
|
|
|
<select
|
|
:id="limitSelectId"
|
|
v-model.number="internalLimit"
|
|
@change="onLimitChange"
|
|
class="min-w-[3.5em] py-1 px-2 border dark:bg-dark-primary-800 border-gray-300 rounded-md bg-white text-sm"
|
|
>
|
|
<option v-for="opt in limitOptions" :key="opt" :value="opt">
|
|
{{ opt }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- ورودی عددی + UPagination -->
|
|
<div v-if="showPageSelection" class="flex items-center gap-2">
|
|
<!-- ورودی عددی (همیشه نمایش داده شود) -->
|
|
<div v-if="isMobile" class="flex gap-1 justify-center w-full">
|
|
<UButton
|
|
:disabled="internalPage <= 1"
|
|
size="sm"
|
|
color="gray"
|
|
@click="onPrevClick"
|
|
aria-label="صفحه قبلی"
|
|
>
|
|
<UIcon name="i-lucide-chevron-right" size="xl" class=""></UIcon>
|
|
</UButton>
|
|
<input
|
|
type="number"
|
|
dir="ltr"
|
|
v-model.number="internalPage"
|
|
class="py-1 px-2 border border-gray-300 rounded-md font-bold text-sm w-16 text-center no-spinner"
|
|
:id="pageInputId"
|
|
placeholder="00"
|
|
:min="1"
|
|
:max="totalPages"
|
|
@keyup="debouncedPageChange"
|
|
@keydown="clearDebounce"
|
|
aria-label="وارد کردن شماره صفحه"
|
|
/>
|
|
|
|
<!-- نمایش فقط دکمههای قبلی/بعدی در موبایل -->
|
|
|
|
<UButton
|
|
:disabled="internalPage >= totalPages"
|
|
size="sm"
|
|
color="gray"
|
|
@click="onNextClick"
|
|
aria-label="صفحه بعدی"
|
|
>
|
|
<UIcon name="i-lucide-chevron-left" size="xl" class=""></UIcon>
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- نمایش UPagination کامل در دسکتاپ -->
|
|
<UPagination
|
|
v-else
|
|
:page="internalPage"
|
|
:total="totalRecords"
|
|
:page-count="totalPages"
|
|
size="xl"
|
|
:max="3"
|
|
:sibling-count="1"
|
|
:show-edges="true"
|
|
@update:page="onPageClick"
|
|
:prev-button="{ label: 'قبلی', color: 'gray' }"
|
|
:next-button="{ label: 'بعدی', trailing: true, color: 'gray' }"
|
|
:active-button="{ variant: 'outline' }"
|
|
:inactive-button="{ color: 'gray' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { useMediaQuery } from "@vueuse/core";
|
|
import { ref, computed, watch } from "vue";
|
|
import { useDebounceFn } from "@vueuse/core";
|
|
import { useRoute, useRouter } from "#imports";
|
|
|
|
const props = defineProps({
|
|
/**
|
|
* اطلاعات پیجینیشن ورودی
|
|
* مثال: { pages: 1000, total: 10000, page: 1, offset: 0, limit: 10 }
|
|
*/
|
|
paginationInfo: {
|
|
type: Object,
|
|
required: true,
|
|
validator(value) {
|
|
return (
|
|
typeof value.total === "number" &&
|
|
typeof value.page === "number" &&
|
|
typeof value.limit === "number"
|
|
);
|
|
},
|
|
},
|
|
showTotalRecords: { type: Boolean, default: true },
|
|
showPageSelection: { type: Boolean, default: true },
|
|
limitOptions: {
|
|
type: Array,
|
|
default: () => [5, 10, 15, 20, 25, 50, 75, 100],
|
|
},
|
|
limitSelectId: { type: String, default: "pagination-limit" },
|
|
pageInputId: { type: String, default: "pagination-page" },
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
"update:paginationInfo",
|
|
"pageChanged",
|
|
"limitChanged",
|
|
]);
|
|
|
|
// تشخیص موبایل (مثلاً عرض کمتر از 768px)
|
|
const isMobile = useMediaQuery("(max-width: 767px)");
|
|
const onPrevClick = () => {
|
|
const newPage = Math.max(1, internalPage.value - 1);
|
|
if (newPage !== internalPage.value) {
|
|
internalPage.value = newPage;
|
|
onPageClick(newPage);
|
|
}
|
|
};
|
|
|
|
const onNextClick = () => {
|
|
const newPage = Math.min(totalPages.value, internalPage.value + 1);
|
|
if (newPage !== internalPage.value) {
|
|
internalPage.value = newPage;
|
|
onPageClick(newPage);
|
|
}
|
|
};
|
|
// استخراج route و router برای بهروزرسانی URL
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
// --- State داخلی ---
|
|
const internalPage = ref(props.paginationInfo.page);
|
|
const internalLimit = ref(props.paginationInfo.limit);
|
|
|
|
// --- محاسبات کمکی ---
|
|
const totalPages = computed(
|
|
() =>
|
|
props.paginationInfo.pages ||
|
|
Math.ceil(props.paginationInfo.total / internalLimit.value) ||
|
|
1
|
|
);
|
|
|
|
const totalRecords = computed(() => props.paginationInfo.total);
|
|
|
|
const recordRange = computed(() => {
|
|
const start = (internalPage.value - 1) * internalLimit.value + 1;
|
|
const end = Math.min(
|
|
internalPage.value * internalLimit.value,
|
|
totalRecords.value
|
|
);
|
|
return { start, end };
|
|
});
|
|
|
|
const pagination = {
|
|
alignment: "justify-between",
|
|
};
|
|
|
|
// --- Debounce برای ورودی عددی ---
|
|
const debouncedEmit = useDebounceFn(() => {
|
|
const page = Math.max(1, Math.min(internalPage.value, totalPages.value));
|
|
internalPage.value = page; // normalize
|
|
emitPageChange(page, internalLimit.value);
|
|
}, 500);
|
|
|
|
const debouncedPageChange = () => {
|
|
debouncedEmit();
|
|
};
|
|
|
|
const clearDebounce = () => {
|
|
debouncedEmit.clear();
|
|
};
|
|
|
|
// --- هندلرهای اصلی ---
|
|
const onPageClick = (page) => {
|
|
internalPage.value = page;
|
|
emitPageChange(page, internalLimit.value);
|
|
// بهروزرسانی URL
|
|
router.replace({
|
|
query: { ...route.query, page: page.toString() },
|
|
});
|
|
};
|
|
|
|
const onLimitChange = () => {
|
|
internalPage.value = 1; // reset to first page
|
|
emitLimitChange(1, internalLimit.value);
|
|
|
|
// بهروزرسانی URL
|
|
router.replace({
|
|
query: { ...route.query, page: "1", limit: internalLimit.value.toString() },
|
|
});
|
|
};
|
|
|
|
// --- ارسال event و sync با والد ---
|
|
const emitPageChange = (page, limit) => {
|
|
const newPagination = {
|
|
page,
|
|
limit,
|
|
total: props.paginationInfo.total,
|
|
pages: totalPages.value,
|
|
offset: (page - 1) * limit,
|
|
};
|
|
emit("update:paginationInfo", newPagination);
|
|
emit("pageChanged", { pageNumber: page, limit });
|
|
};
|
|
|
|
const emitLimitChange = (page, limit) => {
|
|
const newPagination = {
|
|
page,
|
|
limit,
|
|
total: props.paginationInfo.total,
|
|
pages: Math.ceil(props.paginationInfo.total / limit),
|
|
offset: 0,
|
|
};
|
|
emit("update:paginationInfo", newPagination);
|
|
emit("limitChanged", { pageNumber: page, limit });
|
|
};
|
|
|
|
// --- همگامسازی با prop خارجی (در صورت تغییر از خارج) ---
|
|
watch(
|
|
() => props.paginationInfo,
|
|
(newVal) => {
|
|
internalPage.value = newVal.page;
|
|
internalLimit.value = newVal.limit;
|
|
},
|
|
{ deep: true }
|
|
);
|
|
</script>
|
|
|
|
<style scoped>
|
|
.jahat-pagination {
|
|
direction: rtl;
|
|
}
|
|
/* حذف اسپینر در این کامپوننت */
|
|
.no-spinner {
|
|
-moz-appearance: textfield; /* فایرفاکس */
|
|
}
|
|
|
|
.no-spinner::-webkit-inner-spin-button,
|
|
.no-spinner::-webkit-outer-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
|
|
.no-spinner {
|
|
appearance: textfield;
|
|
}
|
|
</style>
|