【Next.js 项目实战系列】02-创建 Issue

原文链接

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

上一篇【Next.js 项目实战系列】01-创建项目

创建 Issue

配置 MySQL 与 Prisma​

在数据库中可以找到相关内容,这里不再赘述

添加 model​

本节代码链接

# schema.prismamodel Issue {id Int @id @default(autoincrement())title String @db.VarChar(255)description String @db.Textstatus Status @default(OPEN)createdAt DateTime @default(now())updatedAt DateTime @updatedAt()
}enum Status {OPENIN_PROGRESSCLOSED
}

使用以下指令同步到数据库

npx prisma format
npx prisma migrate dev

编写 API​

本节代码链接

这里使用 zod 来验证表单,具体内容可参考使用 zod 验证表单

# /app/api/issues/route.tsimport { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import prisma from "@/prisma/client";const createIssueSchema = z.object({title: z.string().min(1).max(255),description: z.string().min(1),
});export async function POST(request: NextRequest) {const body = await request.json();const validation = createIssueSchema.safeParse(body);if (!validation.success)return NextResponse.json(validation.error.errors, { status: 400 });const newIssue = await prisma.issue.create({data: { title: body.title, description: body.description },});return NextResponse.json(newIssue, { status: 201 });
}

Radix-UI​

本节代码链接

radix-ui 也是一个类 DaisyUI 的组件库,使用如下指令安装

npm install @radix-ui/themes

安装好后,进行如下初始配置,将主 layout 中的所有内容用 <Theme > 标签包起来

# /app/layout.tsximport type { Metadata } from "next";
+ import "@radix-ui/themes/styles.css";import { Inter } from "next/font/google";
+ import { Theme } from "@radix-ui/themes";import "./globals.css";import NavBar from "./NavBar";const inter = Inter({ subsets: ["latin"] });export const metadata: Metadata = {title: "Create Next App",description: "Generated by create next app",};export default function RootLayout({children,}: Readonly<{children: React.ReactNode;}>) {return (<html lang="en"><body className={inter.className}>
+         <Theme><NavBar /><main>{children}</main>
+         </Theme></body></html>);}

创建新 Issue 页面​

本节代码链接

# /app/issues/new/page.tsx"use client";
import { Button, TextArea, TextField } from "@radix-ui/themes";const NewIssuePage = () => {return (<div className="max-w-xl space-y-3"><TextField.Root><TextField.Input placeholder="Title" /></TextField.Root><TextArea placeholder="Description" /><Button>Submit New</Button></div>);
};
export default NewIssuePage;

显示效果如下 

New Issue Page

Radix-UI 定义 UI 样式​

本节代码链接

在 layout.tsx 中添加 <Themepanel >

# /app/layout.tsx+ import { Theme, ThemePanel } from "@radix-ui/themes";...return (<html lang="en"><body className={inter.className}><Theme><NavBar /><main className="p-5">{children}</main>
+           <ThemePanel /></Theme></body></html>);...

效果如下

Theme Panel

调整好自己想要的样式之后点击 Copy Theme,将 copy 到的 <Theme > 标签替换掉原来的即可

  #  /app/layout.tsx...return (<html lang="en"><body className={inter.className}>{/*添加到这里即可*/}<Theme appearance="light" accentColor="violet"><NavBar /><main className="p-5">{children}</main></Theme></body></html>);...

设置字体​

在 Radix-UI 中设置字体需要以下步骤,可以参考 radix-ui-font

首先在 layout.tsx 中修改

# /app/layout.tsximport { Theme } from "@radix-ui/themes";import "@radix-ui/themes/styles.css";import type { Metadata } from "next";import { Inter } from "next/font/google";import NavBar from "./NavBar";import "./globals.css";
- const inter = Inter({ subsets: ["latin"] });
+ const inter = Inter({
+   subsets: ['latin'],
+   variable: '--font-inter',
+ });export const metadata: Metadata = {title: "Create Next App",description: "Generated by create next app",};export default function RootLayout({children,}: Readonly<{children: React.ReactNode;}>) {return (<html lang="en">
-       <body className={inter.className}>
+       <body className={inter.variable}><Theme appearance="light" accentColor="violet"><NavBar /><main className="p-5">{children}</main></Theme></body></html>);}

然后添加 /app/theme-config.css 并添加以下内容

/app/theme-config.css

.radix-themes {--default-font-family: var(--font-inter);
}

最后在 layout.tsx 中 import 进来即可

···
import "./theme-config.css";
···

MarkDown Editor​

本节代码链接

react-simlemde-editor 是一款集成式 MarkDown 编辑器,使用如下命令安装

npm install --save react-simplemde-editor easymde

效果如下:

Simple MarkDown Editor

提交表单​

本节代码链接

我们使用 react-hook-form 和 axios 进行表单提交

npm i react-hook-form
npm i axios
# /app/issues/new/page.tsx"use client";import { Button, TextField } from "@radix-ui/themes";import { useRouter } from "next/navigation";// import
+ import axios from "axios";
+ import "easymde/dist/easymde.min.css";
+ import { Controller, useForm } from "react-hook-form";
+ import SimpleMDE from "react-simplemde-editor";// 使用 interface 表明 form 中有哪些内容
+ interface IssueForm {
+   title: string;
+   description: string;
+ }const NewIssuePage = () => {// 使用 React Hook Form
+   const { register, control, handleSubmit } = useForm<IssueForm>();// 使用 router 进行页面跳转
+   const router = useRouter();return ({/* 将最外层 div 换为 form */}
+     <form className="max-w-xl space-y-3"
+       onSubmit={handleSubmit(async (data) => {{/* 使用 axios 进行 post */}
+         await axios.post("/api/issues", data);
+         router.push("/issues");
+       })}><TextField.Root>{/* 将该组件注册为 form 中的 title 字段 */}
+         <TextField.Input placeholder="Title" {...register("title")} /></TextField.Root>{/* 由于 simpleMDE 不能直接像上面的 Input 一样传入参数,我们这里使用 React Hook Form 中的 Controller */}
-       <SimpleMDE placeholder="Description" />
+       <Controller
+         name="description"
+         control={control}
+         render={({ field }) => (
+           <SimpleMDE placeholder="Description" {...field} />
+         )}
+       /><Button>Submit New</Button>
+     </form>);};export default NewIssuePage;

完整代码(非 git diff 版)

# /app/issues/new/page.tsx"use client";
import { Button, TextField } from "@radix-ui/themes";
import axios from "axios";
import "easymde/dist/easymde.min.css";
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
import SimpleMDE from "react-simplemde-editor";interface IssueForm {title: string;description: string;
}const NewIssuePage = () => {const { register, control, handleSubmit } = useForm<IssueForm>();const router = useRouter();return (<formclassName="max-w-xl space-y-3"onSubmit={handleSubmit(async (data) => {await axios.post("/api/issues", data);router.push("/issues");})}><TextField.Root><TextField.Input placeholder="Title" {...register("title")} /></TextField.Root><Controllername="description"control={control}render={({ field }) => (<SimpleMDE placeholder="Description" {...field} />)}/><Button>Submit New</Button></form>);
};
export default NewIssuePage;

效果如下:

submit form

Handle Error​

本节代码链接

表单验证​

之前说到,我们使用 zod 进行表单验证,可以在使用 zod 时,自定义报错内容

# /app/api/issues/new/route.tsx...const createIssueSchema = z.object({// 在定义时,可以加第二个参数,表示如果未满足该项时的报错
+   title: z.string().min(1, "Title is required!").max(255),
+   description: z.string().min(1, "Description is required!"),});export async function POST(request: NextRequest) {...if (!validation.success)// 改为调用 validation.error.format()
-     return NextResponse.json(validation.error.errors, { status: 400 });
+     return NextResponse.json(validation.error.format(), { status: 400 });...}

报错显示​

接下来实现一个这样的 Error Callout

Error Callout

在 /app/issues/new/page.tsx 中修改。把 axios 的相关内容放到一个 try-catch block 里

# /app/issues/new/page.tsx"use client";...const NewIssuePage = () => {...// 添加 useState 变量
+   const [error, setError] = useState("");return (...{/*若报错则显示一个 CallOut*/}
+       {error && (
+         <Callout.Root color="red" className="mb-5">
+           <Callout.Text>{error}</Callout.Text>
+         </Callout.Root>
+       )}<formclassName="space-y-3"onSubmit={handleSubmit(async (data) => {// 报错时设置 error
+           try {
+             await axios.post("/api/issues", data);
+             router.push("/issues");
+           } catch (error) {
+             setError("An unexpected Error occured!");
+           }})}>...};export default NewIssuePage;

用户端验证​

本节代码链接

Zod schema​

我们在用户端验证时,也需要用到刚刚 zod 中编辑的 schema,为此我们应该将其移动到一个单独的文件中。在 VS Code 中 可以方便的进行重构,将 createIssueSchema 移动到一个新文件中,并自动更新引用

首先右键想要重构的变量,点击 重构

Refactor 1

然后选择 move to a new file

Refactor 2

使用 Zod Schema 推断 interface​

将刚刚移出的 schema 移动到 /app 目录下,重命名为 validationSchema.ts

之前在 new page 中,我们定义了一个 interface,用于定义表单,但其实与我们在 zod 中定义的内容是重复的,如果我们之后还需要增删内容,需要在两边修改,较为麻烦。我们可以直接使用刚刚的 zod schema 来定义 interface ,如下所示

# /app/issues/new/page.tsx+  import { createIssueSchema } from "@/app/validationSchema";
+  import { z } from "zod";
- interface IssueForm {
-   title: string;
-   description: string;
- }
+  type IssueForm = z.infer<typeof createIssueSchema>;

使用 hookform 集成 zod 验证表单​

安装 hookform/resolvers,用于将 React Hook Form 插件使用表单验证插件(比如 zod)

npm i @hookform/resolvers
# /app/issues/new/page.tsx"use client";...// import
+ import { Button, Callout, Text, TextField } from "@radix-ui/themes";
+ import { zodResolver } from "@hookform/resolvers/zod";type IssueForm = z.infer<typeof createIssueSchema>;const NewIssuePage = () => {const {register,control,handleSubmit,// errors 则为验证结果
+     formState: { errors },} = useForm<IssueForm>({// 将 zodResoler 传入,以验证表单
+     resolver: zodResolver(createIssueSchema),});...return (<div className="max-w-xl">...<TextField.Root><TextField.Input placeholder="Title" {...register("title")} /></TextField.Root>{/* 根据验证结果来显示提示,此处为 title 字段的信息 */}
+       {errors.title && (
+         <Text color="red" as="p">
+           {errors.title.message}
+         </Text>
+       )}<Controllername="description"control={control}render={({ field }) => (<SimpleMDE placeholder="Description" {...field} />)}/>{/* 根据验证结果来显示提示,此处为 description 字段的信息 */}
+       {errors.description && (
+         <Text color="red" as="p">
+           {errors.description.message}
+         </Text>
+       )}...</div>);};export default NewIssuePage;

最终效果如下:

Client Side Validation

将 ErrorMessage 封装​

# /app/components/ErrorMessage.tsximport { Text } from "@radix-ui/themes";
import { PropsWithChildren } from "react";const ErrorMessage = ({ children }: PropsWithChildren) => {if (!children) return null;return (<Text color="red" as="p">{children}</Text>);
};
export default ErrorMessage;

 然后我们可以在 new Page 中直接调用

# /app/issues/new/page.tsx"use client";...// import
+ import ErrorMessage from "@/app/components/ErrorMessage";return (<div className="max-w-xl">...{/* 根据验证结果来显示提示,此处为 title 字段的信息 */}
-       {errors.title && (
-         <Text color="red" as="p">
-           {errors.title.message}
-         </Text>
-       )}
+       <ErrorMessage>{errors.title?.message}</ErrorMessage>...
-       {errors.description && (
-         <Text color="red" as="p">
-           {errors.description.message}
-         </Text>
-       )}
+        <ErrorMessage>{errors.description?.message}</ErrorMessage>...</div>);};export default NewIssuePage;

Button 优化技巧​

本节代码链接

首先我们可以添加一个 Spinner 给 Button。其次,我们可以给 Button 添加一个 disabled 属性,使得其只能被点击一次,避免多次提交表单

Spinner 代码

# /app/issues/new/page.tsx+ import Spinner from "@/app/components/Spinner";const NewIssuePage = () => {
+   const [isSubmitting, setSubmitting] = useState(false);return (<div className="max-w-xl">...<formclassName="space-y-3"onSubmit={handleSubmit(async (data) => {try {
+             setSubmitting(true);await axios.post("/api/issues", data);router.push("/issues");} catch (error) {
+             setSubmitting(false);setError("An unexpected Error occured!");}})}>...
+         <Button disabled={isSubmitting}>
+           Submit New Issue {isSubmitting && <Spinner />}
+         </Button></form></div>);};

最终版本​

本节代码链接

/app/issues/new/page.tsx"use client";
import { Button, Callout, Text, TextField } from "@radix-ui/themes";
import axios from "axios";
import "easymde/dist/easymde.min.css";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import SimpleMDE from "react-simplemde-editor";
import { zodResolver } from "@hookform/resolvers/zod";
import { createIssueSchema } from "@/app/validationSchema";
import { z } from "zod";
import ErrorMessage from "@/app/components/ErrorMessage";type IssueForm = z.infer<typeof createIssueSchema>;const NewIssuePage = () => {const {register,control,handleSubmit,formState: { errors },} = useForm<IssueForm>({resolver: zodResolver(createIssueSchema),});const router = useRouter();const [error, setError] = useState("");return (<div className="max-w-xl">{error && (<Callout.Root color="red" className="mb-5"><Callout.Text>{error}</Callout.Text></Callout.Root>)}<formclassName="space-y-3"onSubmit={handleSubmit(async (data) => {try {await axios.post("/api/issues", data);router.push("/issues");} catch (error) {setError("An unexpected Error occured!");}})}><TextField.Root><TextField.Input placeholder="Title" {...register("title")} /></TextField.Root><ErrorMessage>{errors.title?.message}</ErrorMessage><Controllername="description"control={control}render={({ field }) => (<SimpleMDE placeholder="Description" {...field} />)}/><ErrorMessage>{errors.description?.message}</ErrorMessage><Button>Submit New</Button></form></div>);
};
export default NewIssuePage;

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

下一篇讲查看 Issue

下一篇【Next.js 项目实战系列】03-查看 Issue

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/452840.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Greenhills学习总结

学习背景&#xff1a;近期参与xx项目过程中&#xff0c;遇到较多的关于代码集成编译的知识盲区&#xff0c;因此需要进行相关知识的学习和扫盲。 参考资料&#xff1a;GreenHills2017.7编译手册:本手册是GreenHills 2017.7.14版编译器的软件使用手册。该手册详细介绍了GreenHi…

数学中的直觉、联想和抽象漫谈

数学中的直觉、联想和抽象漫谈 直觉、联想和抽象不是孤立存在的&#xff0c;而是相互交织、共同作用的。构成了我们认知理解世界的不可或缺的三种能力。我们应该重视并培养这些思维能力&#xff0c;以更好地适应不断变化的世界。 在数学的世界里&#xff0c;直觉、联想和抽象是…

【每日一题】24.10.14 - 24.10.20

10.14 直角三角形1. 题目2. 解题思路3. 代码实现&#xff08;AC_Code&#xff09; 10.15 回文判定1. 题目2. 解题思路3. 代码实现&#xff08;AC_Code&#xff09; 10.16 二次方程1. 题目2. 解题思路3. 代码实现&#xff08;AC_Code&#xff09; 10.17 互质1. 题目2. 解题思路3…

UE5 gameplay学习1 蓝图修改材质和参数

第一种是直接修改这个材质&#xff0c;比较朴素 这个对象直接Set Material这个很直观就设置了 如果要设置材质的属性&#xff0c;就有一点奇怪了&#xff0c;通常来说get到material就能设置了&#xff0c;这里需要如下操作 create一个dynamic material instance 然后还要指定…

[JAVAEE] 线程安全问题

目录 一. 什么是线程安全 二. 线程安全问题产生的原因 三. 线程安全问题的解决 3.1 解决修改操作不是原子性的问题 > 加锁 a. 什么是锁 b. 没有加锁时 c. 加锁时 d. 死锁 e. 避免死锁 3.2 解决内存可见性的问题 > volatile关键字 (易变的, 善变的) a. 不加…

搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程

参考文章&#xff1a; 安装protoc、protoc-gen-go、protoc-gen-go-grpc-CSDN博客 一、简单介绍 本文开发环境&#xff0c;均为 windows 环境&#xff0c;mac 环境其实也类似 ~ ① 编译proto文件&#xff0c;相关插件 简单介绍&#xff1a; protoc 是编译器&#xff0c;用于将…

AUTOSAR_EXP_ARAComAPI的5章笔记(17)

☞返回总目录 相关总结&#xff1a;AutoSar AP CM通信组总结 5.7 通信组 5.7.1 目标 通信组&#xff08;Communication Group&#xff0c;CG&#xff09;是由 AUTOSAR 定义的复合服务模板。它提供了一个通信框架&#xff0c;允许在 AUTOSAR 应用程序之间以对等方式和广播模…

AMBA-CHI协议详解(十)

AMBA-CHI协议详解&#xff08;一&#xff09;- Introduction AMBA-CHI协议详解&#xff08;二&#xff09;- Channel fields / Read transactions AMBA-CHI协议详解&#xff08;三&#xff09;- Write transactions AMBA-CHI协议详解&#xff08;四&#xff09;- Other transac…

【设计模式系列】抽象工厂模式

一、什么是抽象工厂模式 抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;它提供了一个接口&#xff0c;用于创建一系列相关或相互依赖的对象&#xff0c;而无需指定它们具体的类。这种模式允许客户端使用抽象的接口来创建一组…

一小时快速入门Android GPU Inspector

本文介绍如何使用 Android GPU Inspector (AGI) 对Android 应用进行系统性能分析和帧性能分析 。面向熟悉Android图形的开发者。 待分析应用需要的前置条件 (1) 将应用设置为可调试状态 <application [...] android:debuggable"true">&#xff08;2&#xff09…

LabVIEW水质监测系统

在面对全球性的海洋污染问题时&#xff0c;利用先进技术进行水质监测成为了保护海洋环境的关键手段之一。开发了一种基于LabVIEW的海洋浮标水质监测系统&#xff0c;该系统能够实时监测并评估近海水域的水质状况&#xff0c;旨在为海洋保护和污染防治提供科技支持。 项目背景 …

svn-拉取与更新代码

右键项目文件 进行更新与提交代码&#xff0c;提交代码选择更改的文件以及填写commit

电子部授课1

今天下午有院科协的授课&#xff0c;涉及电赛知识&#xff0c;单片机环境构建和模拟方向讲解。感觉要学知识还是很多呜呜呜 这是电赛讲解&#xff0c;主要是五个方面&#xff0c;有一个讲太快了没有听清哈哈哈 后面是全程搜概念的模拟&#xff0c;真的有很多知识不太明白 慌乱…

Java项目-基于springboot框架的会员制医疗预约服务管理信息系统项目实战(附源码+文档)

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 开发运行环境 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/…

云曦10月13日awd复现

一、防御 1、改用户密码 passwd <user> 2、改数据库密码 进入数据库 mysql -uroot -proot 改密码 update mysql.user set passwordpassword(新密码) where userroot; 查看用户信息密码 select host,user,password from mysql.user; 改配置文件&#xff0c;将密码改为自己…

Quartus Ⅱ仿真 1.半加器

真服了&#xff0c;csdn上一搜全是收费&#xff0c;服啦服啦&#xff0c;我就自己来写一个吧 仿真波形&#xff1a; 输出结果&#xff1a; 介绍&#xff1a; 半加器&#xff08;Half Adder&#xff09;是数字电路中的一种基本组件&#xff0c;用于实现两个一位二进制数的加…

基于Leaflet和SpringBoot的全球国家综合检索WebGIS可视化

目录 前言 一、Java后台程序设计 1、业务层设计 2、控制层设计 二、WebGIS可视化实现 1、侧边栏展示 2、空间边界信息展示 三、标注成果展示 1、面积最大的国家 2、国土面积最小的国家 3、海拔最低的国家 4、最大的群岛国家 四、总结 前言 在前面的博文中&#xff…

HCIP-HarmonyOS Application Developer 习题(十五)

&#xff08;判断&#xff09;1、在HarmonyOs中发布带权限公共事件&#xff0c;发布者首先要在config.json中申请所需的权限。 答案&#xff1a;正确 分析&#xff1a;发布携带权限的公共事件&#xff1a;构造CommonEventPublishInfo对象&#xff0c;设置订阅者的权限。 &#…

[C++]ecplise C++新建项目跑hello world

测试通过版本&#xff1a; ecplise-cpp 2024-09 ecplise-cpp 2020-09 【前提】 安装好MinGW环境&#xff0c;实际测试不需要下载什么CDT插件就可以运行了。 步骤&#xff1a; &#xff08;1&#xff09;打开ecplise,选择launch 选择File->New->C/C Project 选择C M…

Win11右键默认显示更多选项

Win11默认显示 想要效果 解决方案1 先按住Shift键&#xff0c;再按右键试试。 解决方案2 1.启动命令行&#xff0c;输入命令 reg.exe add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve2.显示操作成功完成&#…