原文链接
CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的库点个star,关注一下吧
上一篇【Next.js 项目实战系列】06-身份验证
分配 Issue 给用户
本节代码链接
Select Button
# /app/issues/[id]/AssigneeSelect.tsx
"use client";
import { Select } from "@radix-ui/themes";
const AssigneeSelect = () => {
return (
<Select.Root>
<Select.Trigger placeholder="Assign..." />
<Select.Content>
<Select.Group>
<Select.Label>Suggestions</Select.Label>
<Select.Item value="1">Castamere</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
);
};
export default AssigneeSelect;
效果如下
获取所有用户
本节代码链接
构建 API
# /app/api/users.tsx
import { NextRequest, NextResponse } from "next/server";
import prisma from "@/prisma/client";
export async function GET(reques: NextRequest) {
const users = await prisma.user.findMany({ orderBy: { name: "asc" } });
return NextResponse.json(users);
}
客户端获取数据
# /app/issues/[id]/AssigneeSelect.tsx
"use client";
import { User } from "@prisma/client";
import { Select } from "@radix-ui/themes";
import axios from "axios";
import { useEffect, useState } from "react";
const AssigneeSelect = () => {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
const getUsers = async () => {
const { data } = await axios.get<User[]>("/api/users");
setUsers(data);
};
getUsers();
}, []);
return (
<Select.Root>
<Select.Trigger placeholder="Assign..." />
<Select.Content>
<Select.Group>
<Select.Label>Suggestions</Select.Label>
{users.map((user) => (
<Select.Item value={user.id} key={user.id}>
{user.name}
</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select.Root>
);
};
export default AssigneeSelect;
React-Query
配置 React-Query
本节代码链接
使用如下命令安装 React-Query
npm i @tanstack/react-query
安装好后,在 /app 目录下创建 QueryClientProvider.tsx
# /app/QueryClientProvider.tsx
"use client";
import {
QueryClient,
QueryClientProvider as ReactQueryClientProvider,
} from "@tanstack/react-query";
import { PropsWithChildren } from "react";
const queryClient = new QueryClient();
const QueryClientProvider = ({ children }: PropsWithChildren) => {
return (
<ReactQueryClientProvider client={queryClient}>
{children}
</ReactQueryClientProvider>
);
};
export default QueryClientProvider;
然后在 layout 中将 body 内所有内容用 QueryClientProvider 包起来
# /app/layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<QueryClientProvider>
<AuthProvider>
<Theme appearance="light" accentColor="violet">
<NavBar />
<main className="p-5">
<Container>{children}</Container>
</main>
</Theme>
</AuthProvider>
</QueryClientProvider>
</body>
</html>
);
}
使用 React-Query
本节代码链接
首先,在 "/app/issues/[id]/Assign" 中去掉之前的 useEffect 和 useState,之后参照下面修改
# /app/issues/[id]/AssigneeSelect.tsx
...
+ import { useQuery } from "@tanstack/react-query";
+ import { Skeleton } from "@/app/components";
const AssigneeSelect = () => {
+ const {
+ data: users,
+ error,
+ isLoading,
+ } = useQuery<User[]>({
// 用于缓存的 key,在不同地方调用 useQuery 若 key 一样则不会重复获取
+ queryKey: ["users"],
// 用于获取数据的函数
+ queryFn: () => axios.get<User[]>("/api/users").then((res) => res.data),
// 数据缓存多久
+ staleTime: 60 * 1000,
// 最多重复获取几次
+ retry: 3,
+ });
+ if (error) return null;
+ if (isLoading) return <Skeleton />;
...
};
export default AssigneeSelect;
完整代码(非 git diff 版)
# /app/issues/[id]/AssigneeSelect.tsx
"use client";
import { User } from "@prisma/client";
import { Select } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { Skeleton } from "@/app/components";
const AssigneeSelect = () => {
const {
data: users,
error,
isLoading,
} = useQuery<User[]>({
queryKey: ["users"], // 用于缓存的 key,在不同地方调用 useQuery 若 key 一样则不会重复获取
queryFn: () => axios.get<User[]>("/api/users").then((res) => res.data), // 用于获取数据的函数
staleTime: 60 * 1000, // 数据缓存多久
retry: 3, // 最多重复获取几次
});
if (error) return null;
if (isLoading) return <Skeleton />;
return (
<Select.Root>
<Select.Trigger placeholder="Assign..." />
<Select.Content>
<Select.Group>
<Select.Label>Suggestions</Select.Label>
{users?.map((user) => (
<Select.Item value={user.id} key={user.id}>
{user.name}
</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select.Root>
);
};
export default AssigneeSelect;
Prisma Relation
本节代码链接
我们需要在 Prisma 中的 Issue model 和 User model 创建一个 Relation
# schema.prisma
model Issue {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
description String @db.Text
status Status @default(OPEN)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt()
+ assignedToUserId String? @db.VarChar(255)
+ assignedToUser User? @relation(fields: [assignedToUserId], references: [id])
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
+ assignedIssues Issue[]
}
更新修改 Issue API
本节代码链接
首先,添加一个新的 zod schema,其中 title, description, assignedToUserId 都设置为了 optional
# validationSchema.ts
import { z } from "zod";
export const issueSchema = z.object({
title: z.string().min(1, "Title is required!").max(255),
description: z.string().min(1, "Description is required!").max(65535),
});
export const patchIssueSchema = z.object({
title: z.string().min(1, "Title is required!").max(255).optional(),
description: z.string().min(1, "Description is required!").optional(),
assignedToUserId: z
.string()
.min(1, "AssignedToUserId is required.")
.max(255)
.optional()
.nullable(),
});
然后修改 "/app/api/issues/[id]/route.tsx"
# /app/api/issues/[id]/route.tsx
+ import { patchIssueSchema } from "@/app/validationSchema";
...
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({}, { status: 401 });
const body = await request.json();
// 换成 patchIssueSchema
+ const validation = patchIssueSchema.safeParse(body);
if (!validation.success)
return NextResponse.json(validation.error.format(), { status: 400 });
// 直接将 title, description, assignedToUserId 结构出来
+ const { title, description, assignedToUserId } = body;
// 若 body 中有 assignedToUserId,则判断该用户是否存在
+ if (assignedToUserId) {
+ const user = await prisma.user.findUnique({
+ where: { id: assignedToUserId },
+ });
+ if (!user)
+ return NextResponse.json({ error: "Invalid user" }, { status: 400 });
+ }
const issue = await prisma.issue.findUnique({
where: { id: parseInt(params.id) },
});
if (!issue)
return NextResponse.json({ error: "Invalid Issue" }, { status: 404 });
const updatedIssue = await prisma.issue.update({
where: { id: issue.id },
+ data: {
+ title,
+ description,
+ assignedToUserId,
+ },
});
return NextResponse.json(updatedIssue, { status: 200 });
}
分配 Issue
本节代码链接
# /app/issues/[id]/AssigneeSelect.tsx
...
const AssigneeSelect = ({ issue }: { issue: Issue }) => {
...
return (
<Select.Root
// 设置初始显示值
+ defaultValue={issue.assignedToUserId || ""}
// 当选择时,使用patch (不需要await)
+ onValueChange={(userId) => {
+ axios.patch("/api/issues/" + issue.id, {
+ assignedToUserId: userId === "Unassign" ? null : userId,
+ });
+ }}
>
<Select.Trigger placeholder="Assign..." />
<Select.Content>
<Select.Group>
<Select.Label>Suggestions</Select.Label>
{/* 添加一个 unassign */}
+ <Select.Item value="Unassign">Unassign</Select.Item>
{users?.map((user) => (
<Select.Item value={user.id} key={user.id}>
{user.name}
</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select.Root>
);
};
export default AssigneeSelect;
显示 Toast
本节代码链接
使用如下命令安装
npm i react-hot-toast
我们只需要在该组件任意地方添加 <Toaster />
组件,然后在需要报错的地方调用 toast() 函数即可
# /app/issues/[id]/AssigneeSelect.tsx
+ import toast, { Toaster } from "react-hot-toast";
const AssigneeSelect = ({ issue }: { issue: Issue }) => {
return (
<>
<Select.Root
defaultValue={issue.assignedToUserId || ""}
onValueChange={ (userId) => {
+ axios
+ .patch("/api/issues/" + issue.id, {
+ assignedToUserId: userId === "Unassign" ? null : userId,
+ })
// 调用 toast.error()即可
+ .catch(() => toast.error("Changes could not be saved!"));
}
}
>
...
</Select.Root>
+ <Toaster />
</>
);
};
export default AssigneeSelect;
效果如下
CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的库点个star,关注一下吧
下一篇讲数据处理