文章
next js用户登陆demo
1、_types
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email("输入邮箱格式错误!"),
password: z.string().min(6, "密码长度至少6位!"),
});
type LoginSchema = z.infer<typeof loginSchema>;
const loginDefaultValues: LoginSchema = {
email: "",
password: ""
};
export { loginDefaultValues, loginSchema, type LoginSchema };
返回值类型
export type ApiResponseType = {
success: boolean;
message: string;
code?: number;
data?: any
}
2、_services
"use server";
import { LoginSchema } from "@/app/(public)/login/_types/login-schema";
import { clearAuthTokens, setAuthTokens } from "@/lib/token";
import { requestAction } from "@/lib/request-action";
import { ApiResponseType } from '@/types/req-types';
export async function login(data: LoginSchema): Promise<ApiResponseType> {
const result: ApiResponseType = await requestAction("server", "/auth/login", {
method: "POST",
body: JSON.stringify(data)
});
if (result.success) {
const { accessToken} = result.data;
await setAuthTokens({accessToken:accessToken});
}
return result;
}3、_components
"use client";
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Field,
FieldGroup,
} from "@/components/ui/field"
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { loginDefaultValues, loginSchema, LoginSchema } from "@/app/(public)/login/_types/login-schema"
import { ControlledInput } from "@/components/controlled/controlled-input";
import { login } from "@/app/(public)/login/_services/auth";
import { useMutation } from "@tanstack/react-query";
import { useRouter, useSearchParams } from "next/navigation";
import { toast } from "sonner";
import { ApiResponseType } from "@/types/req-types";
import { useState } from "react";
export function LoginForm({
className,
...props
}: React.ComponentProps<"div">) {
const router = useRouter();
const [cooldown, setCooldown] = useState(false);
// 获取页面路由的next跳转地址
const searchParams = useSearchParams();
const nextUrl = searchParams.get("next");
const form = useForm<LoginSchema>({
defaultValues: loginDefaultValues,
resolver: zodResolver(loginSchema),
});
const mutation = useMutation({
mutationFn: (data: LoginSchema) => login(data),
onSuccess: (result: ApiResponseType) => {
// console.log(result)
if (result.success) {
toast.success("登录成功");
// 跳转到 nextUrl 或默认 /dashboard
const redirectUrl = nextUrl && nextUrl.startsWith('/') ? nextUrl : "/dashboard";
router.push(redirectUrl);
} else {
if ("fetch failed" === result.message) {
toast.error("服务端获取数据失败!")
} else {
toast.error(result.message);
}
// 失败时,500ms 后允许重试
setTimeout(() => setCooldown(false), 500);
}
},
});
const onSubmit: SubmitHandler<LoginSchema> = (data) => {
// 防止重复提交(逻辑层防护)
if (mutation.isPending || cooldown) {
return;
}
setCooldown(true);
mutation.mutate(data);
}
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader>
<CardTitle>登录平台</CardTitle>
<CardDescription>
请输入正确的邮箱和密码登录平台
</CardDescription>
</CardHeader>
<CardContent>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<ControlledInput
name="email"
label="邮箱:"
type="email"
placeholder="m@example.com"
/>
<ControlledInput
name="password"
label="密码:"
type="password"
placeholder="••••••••"
/>
<Field>
<Button
type="submit"
disabled={mutation.isPending || cooldown}
>
{mutation.isPending ? "登录中..." : "登录"}
</Button>
</Field>
</FieldGroup>
</form>
</FormProvider>
</CardContent>
</Card>
</div>
)
}