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

原文链接

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

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

数据处理

筛选

添加筛选按钮

本节代码链接

# /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

使用如下提示词去问 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
# /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/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
# /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;
# /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;

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

下一篇讲 Dashboard

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

### Next.js 实战项目案例或教程 以下是关于 Next.js 的一些实际应用案例和教程资源: #### 1. **Next.js 演示商店项目** 该项目是一个基于 Next.js 和 Moltin API 构建的电子商务前端展示店[^1]。它提供了完整的源码以及详细的文档说明,适合开发者学习如何构建一个现代化的电商网站。 项目地址:[https://gitcode.com/gh_mirrors/ne/nextjs-demo-store](https://gitcode.com/gh_mirrors/ne/nextjs-demo-store) 此项目的核心功能包括商品列表显示、购物车管理、订单提交等。通过阅读其代码结构可以了解 Next.js 中 `getStaticProps` 和 `getServerSideProps` 的实际应用场景。 --- #### 2. **精简项目入门指南** 对于初学者来说,理解 Next.js 的目录结构调整非常重要。按照官方推荐的最佳实践,项目的入口文件通常位于 `src/pages/_app.js` 文件中[^2]。这使得开发者能够更灵活地自定义全局布局和状态管理逻辑。 如果希望快速上手并熟悉 Next.js 的基本架构设计,则可以从简单的博客系统或者个人主页类的小型项目入手练习。 --- #### 3. **数据处理与交互优化** 在复杂的应用场景下,掌握高效的数据获取方式至关重要。例如,在文章提到的一个系列教程里介绍了有关筛选条件配置及其 UI 控件实现方法的内容[^3]。这些技巧可以帮助提升用户体验的同时也增强了系统的可维护性。 此外还涉及到了 GraphQL 查询语言集成到 React 应用中的过程分析;这对于那些计划采用 Headless CMS 解决方案的人来说尤为有价值。 --- ```javascript // 示例代码片段展示了如何利用 getServerSideProps 来动态加载页面所需的数据 export async function getServerSideProps(context) { const res = await fetch(`https://api.example.com/items`); const items = await res.json(); return { props: { items }, // 将解析后的 JSON 对象作为组件属性传递下去 }; } ``` 上述例子简单示范了服务器端渲染技术是如何工作的——即每次请求都会重新执行这段函数从而返回最新版本的信息给客户端呈现出来。 --- #### 总结 综上所述,无论是想深入研究框架本身特性还是寻求灵感来开发属于自己的 web app ,都可以从上面列举出来的几个方向找到合适的切入点 。 不仅限于理论知识的学习,更重要的是动手去做具体的实例操作才能真正提高技术水平!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值