You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 13 Beta(appDir)环境下如何实现分页功能?

在Next.js 13 App Router中实现产品列表分页(含页码高亮)

核心思路

App Router模式下不再支持getStaticProps/getServerSideProps,改用Server Component获取初始分页数据,搭配Client Component处理路由跳转与交互,同时依赖API返回分页元信息(当前页、总页数、是否有下一页)来实现完整分页逻辑。


步骤1:Server Component获取分页数据(app/products/page.tsx)

默认情况下page.tsx是Server Component,可直接在组件内异步请求API,通过searchParams获取当前页码:

import Pagination from './Pagination';

// 定义类型,根据你的API返回结构调整
type Product = {
  id: number;
  name: string;
  price: number;
};

type PaginationMeta = {
  currentPage: number;
  totalPages: number;
  hasNextPage: boolean;
};

type ApiResponse = {
  products: Product[];
  pagination: PaginationMeta;
};

export default async function ProductsPage({
  searchParams,
}: {
  searchParams: { page?: string };
}) {
  // 解析当前页码,默认显示第1页
  const currentPage = parseInt(searchParams.page) || 1;
  
  // 请求分页数据,可选添加缓存策略
  const res = await fetch(
    `http://127.0.0.1:8000/store/products/?page=${currentPage}`,
    { next: { revalidate: 60 } } // 60秒后重新验证缓存
  );

  if (!res.ok) throw new Error('获取产品列表失败');
  const { products, pagination } = await res.json() as ApiResponse;

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">产品列表</h1>
      {/* 产品列表展示 */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
        {products.map(product => (
          <div key={product.id} className="border p-4 rounded-lg">
            <h3 className="font-semibold">{product.name}</h3>
            <p className="text-gray-600">${product.price.toFixed(2)}</p>
          </div>
        ))}
      </div>
      {/* 分页组件 */}
      <Pagination
        currentPage={pagination.currentPage}
        totalPages={pagination.totalPages}
        hasNextPage={pagination.hasNextPage}
      />
    </div>
  );
}

步骤2:Client Component实现分页交互(app/products/Pagination.tsx)

添加'use client'指令转为客户端组件,处理路由跳转、页码高亮和下一页逻辑:

'use client';

import { useRouter, useSearchParams } from 'next/navigation';

type PaginationProps = {
  currentPage: number;
  totalPages: number;
  hasNextPage: boolean;
};

export default function Pagination({
  currentPage,
  totalPages,
  hasNextPage,
}: PaginationProps) {
  const router = useRouter();
  const searchParams = useSearchParams();

  // 生成优化后的页码列表(避免显示过多页码)
  const generatePageNumbers = () => {
    const pages: (number | string)[] = [];
    const start = Math.max(1, currentPage - 2);
    const end = Math.min(totalPages, currentPage + 2);

    // 添加首页和省略号(如果需要)
    if (start > 1) pages.push(1);
    if (start > 2) pages.push('...');

    // 添加当前页前后的页码
    for (let i = start; i <= end; i++) pages.push(i);

    // 添加末页和省略号(如果需要)
    if (end < totalPages - 1) pages.push('...');
    if (end < totalPages) pages.push(totalPages);

    return pages;
  };

  // 切换页码逻辑
  const handlePageChange = (page: number) => {
    const newParams = new URLSearchParams(searchParams);
    newParams.set('page', page.toString());
    router.push(`/products?${newParams.toString()}`);
  };

  const pageNumbers = generatePageNumbers();

  return (
    <div className="flex items-center justify-center gap-2">
      {/* 上一页按钮 */}
      <button
        onClick={() => handlePageChange(currentPage - 1)}
        disabled={currentPage === 1}
        className="px-4 py-2 border rounded disabled:opacity-50 disabled:cursor-not-allowed"
      >
        上一页
      </button>

      {/* 页码列表(含高亮) */}
      {pageNumbers.map((page, idx) => (
        <span
          key={idx}
          className={`px-3 py-2 rounded cursor-pointer ${
            typeof page === 'number' && page === currentPage
              ? 'bg-blue-500 text-white'
              : 'border hover:bg-gray-100'
          }`}
          onClick={() => typeof page === 'number' && handlePageChange(page)}
        >
          {page}
        </span>
      ))}

      {/* 下一页按钮 */}
      <button
        onClick={() => handlePageChange(currentPage + 1)}
        disabled={!hasNextPage}
        className="px-4 py-2 border rounded disabled:opacity-50 disabled:cursor-not-allowed"
      >
        下一页
      </button>
    </div>
  );
}

关键说明

  1. API要求:确保你的API返回分页元信息(当前页、总页数、是否有下一页),如果API只返回总条数,可通过totalPages = Math.ceil(totalItems / itemsPerPage)计算总页数。
  2. 路由跳转:通过useRouter和useSearchParams修改URL的page参数,实现无刷新分页。
  3. 页码高亮:通过判断当前页与点击页码是否匹配,给对应元素添加背景色高亮。
  4. 缓存优化:fetch时添加next.revalidate可设置缓存过期时间,减少重复请求。
  5. Loading状态:可在app/products目录下创建loading.tsx,添加全局加载提示。

内容的提问来源于stack exchange,提问作者Sabbir Hasan Munna

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 22:20:44