2026-09-12 01:33:07 +03:30
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed } from 'vue';
|
|
|
|
|
import UiButton from './UiButton.vue';
|
|
|
|
|
import UiIcon from '@/components/UiIcon.vue';
|
2026-10-05 20:00:21 +03:30
|
|
|
import { toFaDigits } from '@/lib/jalali';
|
2026-09-12 01:33:07 +03:30
|
|
|
|
|
|
|
|
const props = withDefaults(
|
|
|
|
|
defineProps<{
|
|
|
|
|
page: number;
|
|
|
|
|
totalPages?: number;
|
|
|
|
|
totalItems?: number;
|
|
|
|
|
pageSize?: number;
|
2026-10-05 20:00:21 +03:30
|
|
|
/** با مقداردهی، انتخاب «تعداد در صفحه» نمایش داده میشود */
|
|
|
|
|
pageSizeOptions?: number[];
|
2026-09-12 01:33:07 +03:30
|
|
|
}>(),
|
|
|
|
|
{
|
|
|
|
|
page: 1,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
'update:page': [page: number];
|
2026-10-05 20:00:21 +03:30
|
|
|
'update:pageSize': [size: number];
|
2026-09-12 01:33:07 +03:30
|
|
|
change: [page: number];
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const effectiveTotalPages = computed(() => {
|
|
|
|
|
if (props.totalPages != null) return Math.max(1, props.totalPages);
|
|
|
|
|
if (props.totalItems != null && props.pageSize != null && props.pageSize > 0) {
|
|
|
|
|
return Math.max(1, Math.ceil(props.totalItems / props.pageSize));
|
|
|
|
|
}
|
|
|
|
|
return 1;
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-05 20:00:21 +03:30
|
|
|
const showSizePicker = computed(() => Boolean(props.pageSizeOptions?.length && props.pageSize));
|
|
|
|
|
|
2026-09-12 01:33:07 +03:30
|
|
|
function goTo(p: number) {
|
|
|
|
|
if (p < 1 || p > effectiveTotalPages.value || p === props.page) return;
|
|
|
|
|
emit('update:page', p);
|
|
|
|
|
emit('change', p);
|
|
|
|
|
}
|
2026-10-05 20:00:21 +03:30
|
|
|
|
|
|
|
|
function onSizeChange(e: Event) {
|
|
|
|
|
const size = Number((e.target as HTMLSelectElement).value);
|
|
|
|
|
if (!size || size === props.pageSize) return;
|
|
|
|
|
emit('update:pageSize', size);
|
|
|
|
|
if (props.page !== 1) {
|
|
|
|
|
emit('update:page', 1);
|
|
|
|
|
emit('change', 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-12 01:33:07 +03:30
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-05 20:00:21 +03:30
|
|
|
<nav
|
|
|
|
|
v-if="effectiveTotalPages > 1 || totalItems !== undefined || showSizePicker"
|
|
|
|
|
class="ui-pagination"
|
|
|
|
|
aria-label="صفحهبندی"
|
|
|
|
|
>
|
2026-09-12 01:33:07 +03:30
|
|
|
<div class="pagination-info">
|
|
|
|
|
<span v-if="totalItems !== undefined">
|
2026-10-05 20:00:21 +03:30
|
|
|
مجموع: <strong>{{ toFaDigits(totalItems.toLocaleString('en-US')) }}</strong> مورد
|
2026-09-12 01:33:07 +03:30
|
|
|
</span>
|
|
|
|
|
<span v-if="effectiveTotalPages > 1" class="page-count">
|
2026-10-05 20:00:21 +03:30
|
|
|
(صفحه {{ toFaDigits(page) }} از {{ toFaDigits(effectiveTotalPages) }})
|
2026-09-12 01:33:07 +03:30
|
|
|
</span>
|
2026-10-05 20:00:21 +03:30
|
|
|
<label v-if="showSizePicker" class="size-picker">
|
|
|
|
|
<span>نمایش</span>
|
|
|
|
|
<select :value="pageSize" @change="onSizeChange">
|
|
|
|
|
<option v-for="n in pageSizeOptions" :key="n" :value="n">{{ toFaDigits(n) }}</option>
|
|
|
|
|
</select>
|
|
|
|
|
<span>در هر صفحه</span>
|
|
|
|
|
</label>
|
2026-09-12 01:33:07 +03:30
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="effectiveTotalPages > 1" class="pagination-controls">
|
|
|
|
|
<UiButton
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
:disabled="page <= 1"
|
|
|
|
|
title="صفحه اول"
|
|
|
|
|
@click="goTo(1)"
|
|
|
|
|
>
|
|
|
|
|
<UiIcon name="chevrons-right" :size="14" />
|
|
|
|
|
</UiButton>
|
|
|
|
|
|
|
|
|
|
<UiButton
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
:disabled="page <= 1"
|
|
|
|
|
title="صفحه قبل"
|
|
|
|
|
@click="goTo(page - 1)"
|
|
|
|
|
>
|
|
|
|
|
<UiIcon name="chevron-right" :size="14" />
|
|
|
|
|
قبلی
|
|
|
|
|
</UiButton>
|
|
|
|
|
|
|
|
|
|
<span class="current-indicator">
|
2026-10-05 20:00:21 +03:30
|
|
|
{{ toFaDigits(page) }} / {{ toFaDigits(effectiveTotalPages) }}
|
2026-09-12 01:33:07 +03:30
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<UiButton
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
:disabled="page >= effectiveTotalPages"
|
|
|
|
|
title="صفحه بعد"
|
|
|
|
|
@click="goTo(page + 1)"
|
|
|
|
|
>
|
|
|
|
|
بعدی
|
|
|
|
|
<UiIcon name="chevron-left" :size="14" />
|
|
|
|
|
</UiButton>
|
|
|
|
|
|
|
|
|
|
<UiButton
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
:disabled="page >= effectiveTotalPages"
|
|
|
|
|
title="صفحه آخر"
|
|
|
|
|
@click="goTo(effectiveTotalPages)"
|
|
|
|
|
>
|
|
|
|
|
<UiIcon name="chevrons-left" :size="14" />
|
|
|
|
|
</UiButton>
|
|
|
|
|
</div>
|
|
|
|
|
</nav>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.ui-pagination {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: var(--sp-3);
|
|
|
|
|
padding-top: var(--sp-4);
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pagination-info {
|
|
|
|
|
font-size: var(--fs-sm);
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-10-05 20:00:21 +03:30
|
|
|
flex-wrap: wrap;
|
2026-09-12 01:33:07 +03:30
|
|
|
gap: var(--sp-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.page-count {
|
|
|
|
|
color: var(--muted-light);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 20:00:21 +03:30
|
|
|
.size-picker {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
margin-inline-start: var(--sp-2);
|
|
|
|
|
font-size: var(--fs-xs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-picker select {
|
|
|
|
|
height: 30px;
|
|
|
|
|
min-width: 3.6rem;
|
|
|
|
|
padding: 0 0.45rem;
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
font: inherit;
|
|
|
|
|
font-size: var(--fs-sm);
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-12 01:33:07 +03:30
|
|
|
.pagination-controls {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--sp-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.current-indicator {
|
|
|
|
|
padding: 0 var(--sp-2);
|
|
|
|
|
font-size: var(--fs-xs);
|
|
|
|
|
font-weight: var(--fw-bold);
|
|
|
|
|
color: var(--ink-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
.ui-pagination {
|
|
|
|
|
flex-direction: column;
|
2026-11-12 15:31:58 +03:30
|
|
|
align-items: stretch;
|
|
|
|
|
gap: 0.65rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pagination-info {
|
|
|
|
|
width: 100%;
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
font-size: 0.78rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.page-count {
|
|
|
|
|
flex: 0 1 auto;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-picker {
|
|
|
|
|
margin-inline-start: 0;
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-picker span:last-child {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-picker select {
|
|
|
|
|
height: 28px;
|
|
|
|
|
min-width: 3.25rem;
|
|
|
|
|
padding: 0 0.3rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pagination-controls {
|
|
|
|
|
width: 100%;
|
|
|
|
|
justify-content: center;
|
2026-09-12 01:33:07 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|