Skip to main content

数据处理

筛选

添加筛选按钮

本节代码链接

/app/issues/IssueStatusFilter.tsx
"use client";
import { Status } from "@prisma/client";
import { Select } from "@radix-ui/themes";

// 将所有可选值存储到外置 const 里
const statuses: { label: string; value?: Status }[] = [
{ label: "All" },
{ label: "Open", value: "OPEN" },
{ label: "In Progress", value: "IN_PROGRESS" },
{ label: "Closed", value: "CLOSED" },
];

const IssueStatusFilter = () => {
return (
<Select.Root defaultValue=" ">
<Select.Trigger placeholder="Filter by status..." />
<Select.Content>
{statuses.map((status) => (
<Select.Item key={status.value} value={status.value || " "}>
{status.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
);
};
export default IssueStatusFilter;

最终效果如下:

Filtering Button

添加筛选参数

在刚刚的 IssueStatusFilter 中,为 Selet 添加 OnValueChage,使得在选项改变时,跳转至添加参数的页面 /issues?status=

/app/issues/IssueStatusFilter.tsx
  ...
+ import { useRouter } from "next/navigation";
...
const IssueStatusFilter = () => {
+ const router = useRouter();
+ const setStatusFilter = (status: string) => {
+ if (status === "All") {
+ router.push("/issues");
+ return;
+ }
+ const query = status ? `?status=${status}` : "";
+ router.push("/issues" + query);
+ };

return (
+ <Select.Root defaultValue="All" onValueChange={setStatusFilter}>
...
</Select.Root>
);
};
export default IssueStatusFilter;

处理筛选参数

/app/issues/page.tsx
  ...
+ interface Props {
+ searchParams: { status: Status };
+ }

+ const IssuesPage = async ({ searchParams }: Props) => {
// 判断 status 是否合法,若合法则加入到筛选项,若不合法则换成 undefined
+ const status = Object.values(Status).includes(searchParams.status)
+ ? searchParams.status
+ : undefined;

// prisma 获取数据时直接添加参数
+ const issues = await prisma.issue.findMany({
+ where: {
+ status,
+ },
+ });
...
};
export default IssuesPage;

最终效果如下

Filtering

排序

本节代码链接

本节更多的是 TypeScript 技巧

/app/issues/page.tsx
  ...

interface Props {
- searchParams: { status: Status };
+ searchParams: { status: Status; orderBy: keyof Issue };
}
// 设置 className 为可选,对于一些固定值可以使用 keyof
+ const columns: { label: string; value: keyof Issue; className?: string }[] = [
+ { label: "Issue", value: "title" },
+ { label: "Status", value: "status", className: "hidden md:table-cell" },
+ { label: "Created", value: "createdAt", className: "hidden md:table-cell" },
+ ];

const IssuesPage = async ({ searchParams }: Props) => {
const status = Object.values(Status).includes(searchParams.status)
? searchParams.status
: undefined;

// 判断是否在其中可以使用 .includes()
// 如果是判断一个对象数组中的某一个键,可以像下面这样,先map成一个数组,再 .includes()
+ const orderBy = columns
+ .map((column) => column.value)
+ .includes(searchParams.orderBy)
+ ? { [searchParams.orderBy]: "asc" }
+ : undefined;

const issues = await prisma.issue.findMany({
where: {
status,
},
// got-add-next-line
+ orderBy,
});

return (
<div>
<IssueActions />
<Table.Root variant="surface">
<Table.Header>
<Table.Row>
+ {columns.map((column) => (
+ <Table.ColumnHeaderCell
+ key={column.label}
+ className={column.className}
+ >
+ <NextLink
+ href={{
{/* 使用 ... 展开数组*/}
+ query: { ...searchParams, orderBy: column.value },
+ }}
+ >
+ {column.label}
+ {column.value === searchParams.orderBy && (
+ <ArrowUpIcon className="inline" />
+ )}
+ </NextLink>
+ </Table.ColumnHeaderCell>
+ ))}
</Table.Row>
</Table.Header>
...
</Table.Root>
</div>
);
};

export const dynamic = "force-dynamic";

export default IssuesPage;

这里更多的是讲,多个参数时的处理方式

/app/issues/IssueStatusFilter.tsx
  ...
+ import { useRouter, useSearchParams } from "next/navigation";


const IssueStatusFilter = () => {
const router = useRouter();
// 获取搜索参数
+ const searchParams = useSearchParams();

const setStatusFilter = (status: string) => {
// 创建一个空的 searchParams
+ const params = new URLSearchParams();
// 获取其他现有的 searchParams
+ if (searchParams.get("orderBy"))
+ params.append("orderBy", searchParams.get("orderBy")!);
// 善用三元表达式
+ if (status) params.append("status", status === "All" ? "All" : status);
+ const query = params.size ? "?" + params.toString() : "";

router.push("/issues" + query);
};

return (
<Select.Root
{/* 别忘了设置初始值 */}
+ defaultValue={searchParams.get("status") || "All"}
onValueChange={setStatusFilter}
>
<Select.Trigger placeholder="Filter by status..." />
<Select.Content>
{statuses.map((status) => (
<Select.Item
key={status.value || "All"}
value={status.value || "All"}
>
{status.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
);
};
export default IssueStatusFilter;

Dummy Data

Given the following prisma model, generate 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 with Mark down synatx. Provide different values for the createdAt and updatedAt columns.

分页

本节代码链接

/app/issues/page.tsx
  ...
+ import Pagination from "../components/Pagination";

interface Props {
- searchParams: { status: Status; orderBy: keyof Issue };
+ searchParams: { status: Status; orderBy: keyof Issue; page: string };
}

...

const IssuesPage = async ({ searchParams }: Props) => {
...
+ const page = parseInt(searchParams.page) || 1;
+ const pageSize = 10;
+ const where = { status };

const issues = await prisma.issue.findMany({
+ where,
orderBy,
+ skip: (page - 1) * pageSize,
+ take: pageSize,
});
+ const issueCount = await prisma.issue.count({ where });

return (
<div>
<IssueActions />
<Table.Root variant="surface">
...
</Table.Root>
+ <Pagination
+ pageSize={pageSize}
+ currentPage={page}
+ itemCount={issueCount}
+ />
</div>
);
};

export const dynamic = "force-dynamic";

export default IssuesPage;

重构与优化

本节代码链接

/app/issues/issueTable.tsx
import { ArrowUpIcon } from "@radix-ui/react-icons";
import { Table } from "@radix-ui/themes";
import NextLink from "next/link";
import { IssueStatusBadge, Link } from "@/app/components";
import { Issue, Status } from "@prisma/client";

interface Props {
searchParams: IssueQuery;
issues: Issue[];
}

// 将 IssueQuery 定义为 interface
export interface IssueQuery {
status: Status;
orderBy: keyof Issue;
page: string;
}

const IssueTable = ({ searchParams, issues }: Props) => {
return (
<Table.Root variant="surface">
<Table.Header>
<Table.Row>
{columns.map((column) => (
<Table.ColumnHeaderCell
key={column.label}
className={column.className}
>
<NextLink
href={{
query: { ...searchParams, orderBy: column.value },
}}
>
{column.label}
{column.value === searchParams.orderBy && (
<ArrowUpIcon className="inline" />
)}
</NextLink>
</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.createdAt.toDateString()}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
};

// 将 columns 定义在这里
const columns: { label: string; value: keyof Issue; className?: string }[] = [
{ label: "Issue", value: "title" },
{ label: "Status", value: "status", className: "hidden md:table-cell" },
{ label: "Created", value: "createdAt", className: "hidden md:table-cell" },
];

// 只把需要的内容导出
export const columnsNames = columns.map((column) => column.value);

export default IssueTable;
Buy me a coffee ☕:
This article is licensed under CC 4.0 BY-SA