【Next.js 项目实战系列】08-数据处理

原文链接

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的点个star,关注一下吧

上一篇【Next.js 项目实战系列】07-分配 Issue 给用户

数据处理

筛选

添加筛选按钮

本节代码链接

TypeScript 复制代码
# /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;

最终效果如下:

添加筛选参数

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

TypeScript 复制代码
# /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;

处理筛选参数

TypeScript 复制代码
# /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;

最终效果如下

排序

本节代码链接

本节更多的是 TypeScript 技巧

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;

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

TypeScript 复制代码
# /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

使用如下提示词去问 AI 要一些数据(用于模拟)

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.

分页

本节代码链接

  • Pagination.tsx
  • page.tsx
TypeScript 复制代码
# /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;
TypeScript 复制代码
# /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;

重构与优化

本节代码链接

  • issueTable.tsx
  • page.tsx
TypeScript 复制代码
# /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;
TypeScript 复制代码
# /app/issues/page.tsx

import prisma from "@/prisma/client";
import { Status } from "@prisma/client";
import Pagination from "../components/Pagination";
import IssueActions from "./IssueActions";
import IssueTable, { IssueQuery, columnsNames } from "./IssueTable";
import { Flex } from "@radix-ui/themes";

interface Props {
  searchParams: IssueQuery;
}

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

  const orderBy = columnsNames.includes(searchParams.orderBy)
    ? { [searchParams.orderBy]: "asc" }
    : undefined;

  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 (
    <Flex direction="column" gap="4">
      <IssueActions />
      <IssueTable searchParams={searchParams} issues={issues} />
      <Pagination
        pageSize={pageSize}
        currentPage={page}
        itemCount={issueCount}
      />
    </Flex>
  );
};

export const dynamic = "force-dynamic";

export default IssuesPage;

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的点个star,关注一下吧

下一篇讲 Dashboard

下一篇【Next.js 项目实战系列】09-仪表板

相关推荐
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
周三有雨9 小时前
【面试题系列Vue07】Vuex是什么?使用Vuex的好处有哪些?
前端·vue.js·面试·typescript
小王码农记15 小时前
vue中路由缓存
前端·vue.js·缓存·typescript·anti-design-vue
大得36920 小时前
css水平居中+垂直居中
vue.js·react
Star7682 天前
ts泛型的一个小知识
前端·typescript
盛夏绽放2 天前
Vue 3与TypeScript集成指南:构建类型安全的前端应用
前端·vue.js·typescript
Swift社区2 天前
使用 AI 在医疗影像分析中的应用探索
typescript·tensorflow·openai
九月儿3 天前
Vue3 + Vite 项目引入 Typescript
前端·typescript
周三有雨3 天前
Object.prototype.hasOwnProperty.call(item, key) 作用与用途
前端·javascript·vue.js·typescript·原型模式
swipe3 天前
Typescript进阶之类型体操套路四
前端·javascript·typescript