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

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 }}&nbsp;</span>
<span>-</span>
<span>&nbsp;{{ recordRange.end }}</span>
<span>&nbsp;از&nbsp;</span>
</div>
<span>{{ totalRecords }}</span>
<span>&nbsp;رکورد</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>