blog / src /components /common /ClientPagination.astro
cacode's picture
Upload 434 files
96dd062 verified
---
import { Icon } from "astro-icon/components";
import I18nKey from "@/i18n/i18nKey";
import { i18n } from "@/i18n/translation";
interface Props {
totalItems: number;
itemsPerPage: number;
currentPage: number;
sectionId: string;
}
const { totalItems, itemsPerPage, currentPage, sectionId } = Astro.props;
const totalPages = Math.ceil(totalItems / itemsPerPage);
// 生成智能分页页码数组
function generatePageNumbers(current: number, total: number) {
const delta = 2; // 当前页左右显示的页码数量
const range: number[] = [];
const rangeWithDots: (number | string)[] = [];
// 如果总页数小于等于7,显示所有页码
if (total <= 7) {
for (let i = 1; i <= total; i++) {
range.push(i);
}
return range;
}
// 计算显示范围
const left = Math.max(2, current - delta);
const right = Math.min(total - 1, current + delta);
// 始终显示第一页
rangeWithDots.push(1);
// 如果左边界大于2,添加省略号
if (left > 2) {
rangeWithDots.push("...");
}
// 添加中间页码
for (let i = left; i <= right; i++) {
rangeWithDots.push(i);
}
// 如果右边界小于最后一页-1,添加省略号
if (right < total - 1) {
rangeWithDots.push("...");
}
// 始终显示最后一页(如果总页数大于1)
if (total > 1) {
rangeWithDots.push(total);
}
return rangeWithDots;
}
const pageNumbers = generatePageNumbers(currentPage, totalPages);
---
{totalPages > 1 && (
<div class="responsive-pagination flex justify-center items-center mt-8" data-pagination-section={sectionId}>
<!-- 移动端简化版分页 -->
<div class="mobile-pagination items-center gap-3">
<button
type="button"
class="btn-card overflow-hidden rounded-lg text-(--primary) w-11 h-11 disabled:opacity-50 disabled:cursor-not-allowed"
data-page="prev"
data-section={sectionId}
disabled={currentPage === 1}
aria-label={i18n(I18nKey.bangumiPrevPage)}
>
<Icon name="material-symbols:chevron-left-rounded" class="text-[1.75rem]" />
</button>
<!-- 移动端页码信息 -->
<div class="bg-(--card-bg) flex items-center rounded-lg px-4 h-11 gap-1.5">
<span class="mobile-current-page text-base font-bold text-(--primary)">{currentPage}</span>
<span class="text-sm text-neutral-500 dark:text-neutral-500">/</span>
<span class="mobile-total-pages text-base font-bold text-neutral-700 dark:text-neutral-300">{totalPages}</span>
</div>
<button
type="button"
class="btn-card overflow-hidden rounded-lg text-(--primary) w-11 h-11 disabled:opacity-50 disabled:cursor-not-allowed"
data-page="next"
data-section={sectionId}
disabled={currentPage === totalPages}
aria-label={i18n(I18nKey.bangumiNextPage)}
>
<Icon name="material-symbols:chevron-right-rounded" class="text-[1.75rem]" />
</button>
</div>
<!-- 桌面端完整版分页 -->
<div class="desktop-pagination items-center gap-3">
<button
type="button"
class="btn-card overflow-hidden rounded-lg text-(--primary) w-11 h-11 disabled:opacity-50 disabled:cursor-not-allowed"
data-page="prev"
data-section={sectionId}
disabled={currentPage === 1}
aria-label={i18n(I18nKey.bangumiPrevPage)}
>
<Icon name="material-symbols:chevron-left-rounded" class="text-[1.75rem]" />
</button>
<div class="bg-(--card-bg) flex flex-row rounded-lg items-center text-neutral-700 dark:text-neutral-300 font-bold" data-page-numbers={sectionId}>
{pageNumbers.map((pageItem) => (
pageItem === '...' ? (
<Icon name="material-symbols:more-horiz" class="mx-1" />
) : (
<button
type="button"
class:list={[
"rounded-lg overflow-hidden w-11 h-11 flex items-center justify-center font-bold",
{
"bg-(--primary) text-white dark:text-black/70": pageItem === currentPage,
"btn-card active:scale-[0.85]": pageItem !== currentPage
}
]}
data-page={pageItem}
data-section={sectionId}
aria-label={`${pageItem}`}
aria-current={pageItem === currentPage ? "page" : undefined}
>
{pageItem}
</button>
)
))}
</div>
<button
type="button"
class="btn-card overflow-hidden rounded-lg text-(--primary) w-11 h-11 disabled:opacity-50 disabled:cursor-not-allowed"
data-page="next"
data-section={sectionId}
disabled={currentPage === totalPages}
aria-label={i18n(I18nKey.bangumiNextPage)}
>
<Icon name="material-symbols:chevron-right-rounded" class="text-[1.75rem]" />
</button>
</div>
</div>
)}
<script is:inline define:vars={{ itemsPerPage, sectionId }}>
function initPagination() {
let currentPage = 1;
const pagination = document.querySelector(`[data-pagination-section="${sectionId}"]`);
if (!pagination) return;
// 更新移动端页码显示
function updateMobileDisplay() {
const mobileCurrentPage = pagination.querySelector('.mobile-current-page');
if (mobileCurrentPage) {
mobileCurrentPage.textContent = currentPage.toString();
}
}
const pageButtons = pagination.querySelectorAll('[data-page]');
pageButtons.forEach(button => {
button.addEventListener('click', function() {
const page = this.dataset.page;
if (page === 'prev') {
currentPage = Math.max(1, currentPage - 1);
} else if (page === 'next') {
const items = document.querySelectorAll(`[data-item-section="${sectionId}"]:not(.hidden)`);
const totalPages = Math.ceil(items.length / itemsPerPage);
currentPage = Math.min(totalPages, currentPage + 1);
} else {
currentPage = parseInt(page);
}
updatePage();
updateMobileDisplay();
});
});
// Listen for filter updates
if (pagination) {
pagination.addEventListener('updatePagination', function(_event) {
currentPage = 1;
updatePage();
updateMobileDisplay();
});
}
function updatePage() {
const items = document.querySelectorAll(`[data-item-section="${sectionId}"]:not(.hidden)`);
const totalPages = Math.ceil(items.length / itemsPerPage);
// Hide all items first
for (const item of items) {
item.style.display = 'none';
}
// Show items for current page
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
for (let i = startIndex; i < endIndex && i < items.length; i++) {
items[i].style.display = 'block';
}
// Update pagination buttons
updatePaginationButtons(totalPages);
}
// 生成智能分页页码数组的JavaScript版本
function generatePageNumbers(current, total) {
const delta = 2; // 当前页左右显示的页码数量
const rangeWithDots = [];
// 如果总页数小于等于7,显示所有页码
if (total <= 7) {
for (let i = 1; i <= total; i++) {
rangeWithDots.push(i);
}
return rangeWithDots;
}
// 计算显示范围
const left = Math.max(2, current - delta);
const right = Math.min(total - 1, current + delta);
// 始终显示第一页
rangeWithDots.push(1);
// 如果左边界大于2,添加省略号
if (left > 2) {
rangeWithDots.push('...');
}
// 添加中间页码
for (let i = left; i <= right; i++) {
rangeWithDots.push(i);
}
// 如果右边界小于最后一页-1,添加省略号
if (right < total - 1) {
rangeWithDots.push('...');
}
// 始终显示最后一页(如果总页数大于1)
if (total > 1) {
rangeWithDots.push(total);
}
return rangeWithDots;
}
function updatePaginationButtons(totalPages) {
// 更新所有的上一页和下一页按钮(移动端和桌面端)
const prevButtons = pagination.querySelectorAll('[data-page="prev"]');
const nextButtons = pagination.querySelectorAll('[data-page="next"]');
const pageNumbersContainer = pagination.querySelector(`[data-page-numbers="${sectionId}"]`);
prevButtons.forEach(btn => {
btn.disabled = currentPage === 1;
});
nextButtons.forEach(btn => {
btn.disabled = currentPage === totalPages;
});
if (pageNumbersContainer) {
pageNumbersContainer.innerHTML = '';
const pageNumbers = generatePageNumbers(currentPage, totalPages);
pageNumbers.forEach(pageItem => {
if (pageItem === '...') {
// 创建省略号图标
const iconContainer = document.createElement('span');
iconContainer.innerHTML = '<svg class="mx-1" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>';
pageNumbersContainer.appendChild(iconContainer.firstChild);
} else {
const button = document.createElement('button');
button.className = pageItem === currentPage
? 'rounded-lg overflow-hidden w-11 h-11 flex items-center justify-center font-bold bg-(--primary) text-white dark:text-black/70'
: 'rounded-lg overflow-hidden w-11 h-11 flex items-center justify-center font-bold btn-card active:scale-[0.85]';
button.dataset.page = pageItem.toString();
button.dataset.section = sectionId;
button.textContent = pageItem.toString();
button.addEventListener('click', function() {
currentPage = pageItem;
updatePage();
updateMobileDisplay();
});
pageNumbersContainer.appendChild(button);
}
});
}
}
// Initial page setup
updatePage();
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPagination);
} else {
initPagination();
}
</script>
<style>
.responsive-pagination {
/* 确保在所有设备上都能正确显示 */
max-width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 移动端样式 (< 768px) */
.mobile-pagination {
display: flex;
padding: 0 1rem;
}
.desktop-pagination {
display: none;
}
/* 桌面端样式 (>= 768px) */
@media (min-width: 768px) {
.mobile-pagination {
display: none;
}
.desktop-pagination {
display: flex;
}
}
/* 小屏手机优化 */
@media (max-width: 640px) {
.mobile-pagination {
padding: 0 0.5rem;
}
.mobile-pagination button {
padding: 0.25rem;
}
}
/* 超小屏优化 */
@media (max-width: 480px) {
.mobile-pagination {
padding: 0 0.25rem;
}
.mobile-pagination .space-x-1 > * + * {
margin-left: 0.125rem;
}
}
/* 平滑过渡效果 */
.responsive-pagination button {
transition: all 0.2s ease-in-out;
}
/* 高对比度模式支持 */
@media (prefers-contrast: high) {
.responsive-pagination button {
border: 1px solid currentColor;
}
}
/* 减少动画偏好支持 */
@media (prefers-reduced-motion: reduce) {
.responsive-pagination button {
transition: none;
}
}
/* 触摸设备优化 */
@media (hover: none) and (pointer: coarse) {
.responsive-pagination button {
min-height: 44px; /* iOS建议的最小触摸目标 */
min-width: 44px;
}
/* 移动端触摸优化 */
.mobile-pagination button {
min-height: 40px;
min-width: 40px;
}
}
/* 横屏手机优化 */
@media (max-width: 768px) and (orientation: landscape) {
.mobile-pagination {
padding: 0 0.5rem;
}
.mobile-pagination button {
padding: 0.25rem;
}
}
</style>