| ---
|
| 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)[] = [];
|
|
|
|
|
| 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);
|
|
|
|
|
| if (left > 2) {
|
| rangeWithDots.push("...");
|
| }
|
|
|
|
|
| for (let i = left; i <= right; i++) {
|
| rangeWithDots.push(i);
|
| }
|
|
|
|
|
| if (right < total - 1) {
|
| rangeWithDots.push("...");
|
| }
|
|
|
|
|
| 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();
|
| });
|
| });
|
|
|
|
|
| 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);
|
|
|
|
|
| for (const item of items) {
|
| item.style.display = 'none';
|
| }
|
|
|
|
|
| const startIndex = (currentPage - 1) * itemsPerPage;
|
| const endIndex = startIndex + itemsPerPage;
|
| for (let i = startIndex; i < endIndex && i < items.length; i++) {
|
| items[i].style.display = 'block';
|
| }
|
|
|
|
|
| updatePaginationButtons(totalPages);
|
| }
|
|
|
|
|
| function generatePageNumbers(current, total) {
|
| const delta = 2;
|
| const rangeWithDots = [];
|
|
|
|
|
| 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);
|
|
|
|
|
| if (left > 2) {
|
| rangeWithDots.push('...');
|
| }
|
|
|
|
|
| for (let i = left; i <= right; i++) {
|
| rangeWithDots.push(i);
|
| }
|
|
|
|
|
| if (right < total - 1) {
|
| rangeWithDots.push('...');
|
| }
|
|
|
|
|
| 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);
|
| }
|
| });
|
| }
|
| }
|
|
|
|
|
| updatePage();
|
| }
|
|
|
|
|
| if (document.readyState === 'loading') {
|
| document.addEventListener('DOMContentLoaded', initPagination);
|
| } else {
|
| initPagination();
|
| }
|
| </script>
|
|
|
| <style>
|
| .responsive-pagination {
|
|
|
| max-width: 100%;
|
| overflow-x: auto;
|
| -webkit-overflow-scrolling: touch;
|
| }
|
|
|
|
|
| .mobile-pagination {
|
| display: flex;
|
| padding: 0 1rem;
|
| }
|
|
|
| .desktop-pagination {
|
| display: none;
|
| }
|
|
|
|
|
| @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;
|
| 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> |