Next.js项目实践(八)– 过滤、排序、分页

本文实现问题列表的过滤、排序以及分页。


构建过滤器组件

app/issues添加IssueStatusFilter.tsx,创建IssueStatusFilter组件,这是一个用状态来筛选列表的组件。

import { Status } from "@prisma/client";
import { Select } from "@radix-ui/themes";
​
const statuses: { label: string; value: Status | "ALL" }[] = [
  { label: "All", value: "ALL" },
  { label: "Open", value: "OPEN" },
  { label: "In Progress", value: "IN_PROGRESS" },
  { label: "Closed", value: "CLOSED" },
];
​
const IssueStatusFilter = () => {
  return (
    <Select.Root>
      <Select.Trigger placeholder="Filter by status..."></Select.Trigger>
      <Select.Content>
        {statuses.map((status) => (
          <Select.Item key={status.value} value={status.value!}>
            {status.label}
          </Select.Item>
        ))}
      </Select.Content>
    </Select.Root>
  );
};
​
export default IssueStatusFilter;
​

在IssueActions里调用IssueStatusFilter。并将之前的Box改成Flex, justify=’between’,使得IssueStatusFilter在左侧,原”New Issue”按钮在右侧。

查看页面。

提交Git,命名:Build the filter component.


实现过滤器

修改IssueStatusFilter组件,添加Select组件的onValueChange事件处理,当先中某个status时,就跳转到 /issues?status=<选中的状态>。

在IssuesPage,获取status查询字符串,并进行相应的过滤。如果不在Status枚举范围内,则为undefined,在Prisma中,where某个属性赋于undefined不作过滤。

查看过滤效果。

提交Git,命名:Filter issues.


使列表可排序

当点击列表某一列标题时,列表就会进行排序。跟过滤一样,可以将点击列作为searchParams传递。当前列表有3列,我们不希望每一列进行设置,可以统一设置列表数据,再进行map操作。在定义value类型时,用了keyof Issue ,表示只有Issue模型的属性才是合法的。

给每列的标题加上Link, 其中href不用类似{`/issues?orderBy=${volumn.value}`}这样的写法,因为这样在点击标题排序时会将原来的status查询参数覆盖掉,我们使用了提供对象及属性query的写法。

最后,为了使排序可视化,在排序列使用<ArrowUpIcon>组件标示排序箭头,这里我们只实现升序,未实现降序,如果要实现的也是很简单的。

查看页面。点击标题时不会覆盖status查询参数。

提交Git,命名:Make columns sortable.


列表排序

Prisma的排序语法类似 orderBy:{title:’asc’},但我们不能硬编码title等属性,需要动态获取,通过将searchParams参数加上方括号达到目的。另外还需要判断查询参数orderBy的值是合规的,比如必须是status、title或createAt。

修改IssuesPage如下。

提交Git,命名:Sort issues.


修复Filter错误

目前应用有一个Bug,就是选择状态进行过滤时,会将orderBy的查询参数删除。我们需要在过滤时保存原来的orderBy参数。

回到IssueStatusFilter组件修改,为了获取查询参数,需要用到useSearchParams的Hook,再通过URLSearchParams简化获取查询参数的键值对,也可方便返回查询字符串。

刷新网页后,url的status与组件筛选显示不一致,需要设置Select的defaultValue。

提交Git,命名:Fix filtering bugs.


生成虚拟数据

生成一些虚拟数据用于测试分页功能。这里使用chatGPT来生成,输入以下要求:

Given the following Prisma model, generater SQL statement to insert 20 records in the issues table. Use real-world titles and descriptions for issues. Status can be OPEN, IN_PROGRESS or CLOSED. description should be a paragraph long. Provide defferent values for createdAt and updatedAt columns.
model Issue {

  id               Int      @id @default(autoincrement())
  title            String   @db.VarChar(255)
  description      String   @db.Text
  status           Status   @default(OPEN)
  createAt         DateTime @default(now())
  updateAt         DateTime @updatedAt
}

输入的要求越详细,输出的结果越理想,ChatGPT就生成了插入20条记录的SQL语句。复制下来到DataGrip执行就生成了20条记录。

如果您无法访问chatGTP也是没关系,只需随意插入20条或更多记录即可。

形成的页面记录。


构建分页组件

分页组件是通用的,我们在app/components下新建Pagination.tsx文件。Pagination组件接受3个参数:条目总数itemCount, 当前页currentPage, 每页显示条数pageSize。

计算出总页数,在总页数小于等于1时,直接返回不显示Pagination组件。接着显示组件,用Flex组件包裹使其水平放置,当当前页面为1时,前一页及首页按钮禁用,当当前页为最后一页时,后一页及末页按钮禁用。

根据实际显示效果,做一些样式化。

import {
  ChevronLeftIcon,
  ChevronRightIcon,
  DoubleArrowLeftIcon,
  DoubleArrowRightIcon,
} from "@radix-ui/react-icons";
import { Button, Flex, Text } from "@radix-ui/themes";
​
interface Props {
  itemCount: number;
  currentPage: number;
  pageSize: number;
}
​
const Pagination = ({ itemCount, currentPage, pageSize }: Props) => {
  const pageCount = Math.ceil(itemCount / pageSize);
  if (pageCount <= 1) return;
  return (
    <Flex align="center" gap="2">
      <Text size="2">
        Page {currentPage} of {pageCount}
      </Text>
      <Button color="gray" variant="soft" disabled={currentPage === 1}>
        <DoubleArrowLeftIcon />
      </Button>
      <Button color="gray" variant="soft" disabled={currentPage === 1}>
        <ChevronLeftIcon />
      </Button>
      <Button color="gray" variant="soft" disabled={currentPage === pageCount}>
        <ChevronRightIcon />
      </Button>
      <Button color="gray" variant="soft" disabled={currentPage === pageCount}>
        <DoubleArrowRightIcon />
      </Button>
    </Flex>
  );
};
​
export default Pagination;
​

提交Git,命名:Build the layout of the pagination component.


实施分页

实施分页,我们需要将要显示的页码作为查询参数传递,类似前面的filter和orderBy。修改Pagination组件,首先使用useSearchParams获取原有的查询参数,因为不能清除之前的查询参数;接着根据不同的按钮,设置要显示的页面,并作为查询参数传递。

提交Git,命名:Implement pagination.


将问题列表分页

修改IssuesPage,Props添加page,然后定义pageSize,并获取当前筛选的问题总数目issueCount, 由于获取问题列表清单及总数目的where对象是一样,我们提取出来免得重复。prisma获取某一页的数据使用skip和take属性。

最后,在表格底部添加 Pagination组件,并提供必需的属性。

查看页面,并测试翻页正常。

提交Git,命名:Paginate issues.


提取 IssueTable 组件

目前,IssuesPage违反了单一职责的原则,该页面的职责是关注布局,但已包含了Issue列表表格的细节,我们将表格细节提取出来,形成IssueTable。IssueTable接受searchParams和issues参数,由于searchParams在IssuesPage中也会用到,我们提取出IssueQuery结构并export供IssuesPage调用。同样的,我们export出 columnNames供IssuesPage调用。

import { Issue, Status } from "@prisma/client";
import { ArrowUpIcon } from "@radix-ui/react-icons";
import { Table } from "@radix-ui/themes";
import Link from "next/link";
import { IssueStatusBadge } from "../components";
​
export interface IssueQuery {
  status: Status;
  orderBy: keyof Issue;
  page: string;
}
​
interface Props {
  searchParams: IssueQuery;
  issues: Issue[];
}
​
const IssueTable = ({ searchParams, issues }: Props) => {
  return (
    <Table.Root variant="surface">
      <Table.Header>
        <Table.Row>
          {columns.map((column) => (
            <Table.ColumnHeaderCell
              key={column.value}
              className={column.className}
            >
              <Link
                href={{ query: { ...searchParams, orderBy: column.value } }}
              >
                {column.label}
              </Link>
              {column.value === searchParams.orderBy && (
                <ArrowUpIcon className="inline" />
              )}
            </Table.ColumnHeaderCell>
          ))}
        </Table.Row>
      </Table.Header>
​
      <Table.Body>
        {issues.map((issue) => (
          <Table.Row key={issue.id}>
            <Table.Cell>
              <Link href={`/issues/${issue.id}`}>{issue.title}</Link>
              <div className="block md:hidden">
                {<IssueStatusBadge status={issue.status} />}
              </div>
            </Table.Cell>
            <Table.Cell className="hidden md:table-cell">
              {<IssueStatusBadge status={issue.status} />}
            </Table.Cell>
            <Table.Cell className="hidden md:table-cell">
              {issue.createAt.toLocaleString()}
            </Table.Cell>
          </Table.Row>
        ))}
      </Table.Body>
    </Table.Root>
  );
};
​
const columns: { label: string; value: keyof Issue; className?: string }[] = [
  { label: "Issue", value: "title" },
  { label: "Status", value: "status", className: "hidden md:table-cell" },
  { label: "Create", value: "createAt", className: "hidden md:table-cell" },
];
​
export const columnNames = columns.map((column) => column.value);
​
export default IssueTable;
​

然后在IssuesPage调用IssueTable,IssuePage就变得清晰简单了。

提交Git,命名:Refactor:Extract IssueTable.


小结

本文实现了问题列表的过滤、排序以及分页。

下一篇实现仪表板功能。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注