全栈开发

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>
  )
}