Files
inkreach-official-website/apps/website/app/components/product/ProductPagination.vue
T

250 lines
5.1 KiB
Vue

<script setup lang="ts">
const props = defineProps<{
total: number;
page: number;
pageSize: number;
}>();
const emit = defineEmits<{
'update:page': [page: number];
'update:pageSize': [size: number];
}>();
const totalPages = computed(() => {
if (props.total <= 0) return 1;
return Math.max(1, Math.ceil(props.total / props.pageSize));
});
const pageNumbers = computed<(number | string)[]>(() => {
const total = totalPages.value;
const current = props.page;
if (total <= 7) {
return Array.from({ length: total }, (_, i) => i + 1);
}
const pages: (number | string)[] = [1];
const start = Math.max(2, current - 1);
const end = Math.min(total - 1, current + 1);
if (start > 2) pages.push('...');
for (let i = start; i <= end; i++) pages.push(i);
if (end < total - 1) pages.push('...');
pages.push(total);
return pages;
});
const canPrev = computed(() => props.page > 1);
const canNext = computed(() => props.page < totalPages.value);
const gotoInput = ref('');
function goPrev(): void {
if (canPrev.value) emit('update:page', props.page - 1);
}
function goNext(): void {
if (canNext.value) emit('update:page', props.page + 1);
}
function goToPage(page: number): void {
if (page < 1 || page > totalPages.value || page === props.page) return;
emit('update:page', page);
}
function onPageSizeChange(event: Event): void {
const target = event.target as HTMLSelectElement;
const size = Number(target.value);
if (!Number.isFinite(size) || size <= 0) return;
emit('update:pageSize', size);
}
function onGoto(): void {
const value = Number(gotoInput.value);
if (!Number.isFinite(value)) return;
const clamped = Math.min(totalPages.value, Math.max(1, Math.floor(value)));
gotoInput.value = '';
goToPage(clamped);
}
function onGotoKeydown(event: KeyboardEvent): void {
if (event.key === 'Enter') {
event.preventDefault();
onGoto();
}
}
</script>
<template>
<div class="pagination">
<div class="total-text">
总计 <span>{{ total }}</span> 个产品
</div>
<div class="page-buttons">
<button
type="button"
class="nav-btn"
:disabled="!canPrev"
@click="goPrev"
>
<i class="fa-solid fa-chevron-left"></i>
</button>
<button
v-for="(item, idx) in pageNumbers"
:key="`page-${idx}-${item}`"
type="button"
class="page-btn"
:class="{
current: item === page,
dots: item === '...',
}"
:disabled="item === '...'"
@click="typeof item === 'number' && goToPage(item)"
>
{{ item }}
</button>
<button
type="button"
class="nav-btn"
:disabled="!canNext"
@click="goNext"
>
<i class="fa-solid fa-chevron-right"></i>
</button>
</div>
<div class="right-controls">
<select :value="pageSize" class="size-select" @change="onPageSizeChange">
<option :value="12">12 / </option>
<option :value="20">20 / </option>
<option :value="40">40 / </option>
</select>
<span class="goto-label">导航至</span>
<input
v-model="gotoInput"
type="number"
min="1"
:max="totalPages"
class="goto-input"
@keydown="onGotoKeydown"
/>
<button type="button" class="goto-btn" @click="onGoto">Go</button>
</div>
</div>
</template>
<style scoped>
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
padding: 16px 24px;
font-size: 14px;
color: #777;
}
.total-text span {
font-weight: 600;
color: #222;
}
.page-buttons {
display: flex;
align-items: center;
gap: 4px;
}
.nav-btn,
.page-btn {
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
border-radius: 6px;
border: 1px solid #eee;
background: #fff;
color: #555;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.nav-btn:hover:not(:disabled),
.page-btn:hover:not(.dots):not(.current) {
border-color: #ff6a00;
color: #ff6a00;
}
.nav-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.page-btn.current {
background: #ff6a00;
border-color: #ff6a00;
color: #fff;
}
.page-btn.dots {
border-color: transparent;
cursor: default;
}
.right-controls {
display: flex;
align-items: center;
gap: 8px;
}
.size-select {
height: 32px;
padding: 0 8px;
border: 1px solid #eee;
border-radius: 6px;
background: #fff;
font-size: 14px;
color: #555;
outline: none;
}
.goto-label {
color: #999;
}
.goto-input {
width: 48px;
height: 32px;
padding: 0 8px;
border: 1px solid #eee;
border-radius: 6px;
background: #fff;
font-size: 14px;
color: #222;
outline: none;
text-align: center;
}
.goto-input:focus {
border-color: #ff6a00;
}
.goto-btn {
height: 32px;
padding: 0 12px;
border: 1px solid #ff6a00;
border-radius: 6px;
background: transparent;
color: #ff6a00;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.goto-btn:hover {
background: #ff6a00;
color: #fff;
}
</style>