全栈开发

nextjs 模态框表单 demo

1、 _types

import z from "zod";

// 1. 定义表单字段的验证 schema
const userSchema = z.object({
  fullName: z.string().min(1, "姓名不能为空!").max(255),
  email: z.string().email("输入邮箱格式错误!"),
});

// 2. 推导 TypeScript 类型
type UserSchema = z.infer<typeof userSchema>;

// 3. 默认值(从当前用户状态来)
const userDefaultValues: UserSchema = {
  fullName: "",   // 实际使用时会被当前用户数据覆盖
  email: "",
};

export {userDefaultValues, userSchema, type UserSchema};

2、_services

import { requestAction } from "@/lib/request-action";
import { useQuery } from "@tanstack/react-query";

export const useCurrentUser = () =>
  useQuery({
    queryKey: ["currentUser"],
    queryFn: async () => {
      const result = await requestAction("client", "/api/user/profile", {
        method: "GET",
      });
      return result.data;
    },
    staleTime: 1000 * 60, // 缓存 1 分钟
  });

3、_components

"use client";

import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import {
    Dialog,
    DialogContent,
    DialogHeader,
    DialogTitle,
    DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { ControlledInput } from "@/components/controlled/controlled-input";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { userSchema, UserSchema } from "@/app/(protected)/dashboard/settings/user/_types/user-schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { requestAction } from "@/lib/request-action";
import { ApiResponseType } from "@/types/req-types";

interface UserFormDialogProps {
    currentUser: {
        id: number;
        fullName: string;
        email: string;
    };
}

export function UserFormDialog({ currentUser }: UserFormDialogProps) {
    const [open, setOpen] = useState(false);
    const queryClient = useQueryClient();
    const [cooldown, setCooldown] = useState(false);

    const form = useForm<UserSchema>({
        defaultValues: {
            fullName: currentUser.fullName,
            email: currentUser.email,
        },
        resolver: zodResolver(userSchema),
    });

    useEffect(() => {
        if (open) {
            form.reset({
                fullName: currentUser.fullName,
                email: currentUser.email,
            });
        }
    }, [open, currentUser, form]);

    const mutation = useMutation({
        mutationFn: (data: UserSchema) =>
            requestAction("client", "/api/user/profile/update", {
                method: "PUT",
                body: JSON.stringify({ id: currentUser.id, ...data })
            }),
        onSuccess: (result: ApiResponseType) => {
            if (result.success) {
                // 同时刷新当前用户 query,让其他组件也更新
                queryClient.invalidateQueries({ queryKey: ["currentUser"] });
                toast.success("修改成功");
            } else {
                toast.error(result.message);
            }
        }
    });


    const onSubmit: SubmitHandler<UserSchema> = (data) => {
        // 防止重复提交(逻辑层防护)
        if (mutation.isPending || cooldown) {
            return;
        }
        setCooldown(true);
        mutation.mutate(data, {
            onSuccess: () => setOpen(false),
            onSettled: () => setTimeout(() => setCooldown(false), 500)
        });
    };

    return (
        <Dialog open={open} onOpenChange={setOpen}>
            <DialogTrigger asChild>
                <Button>编辑信息</Button>
            </DialogTrigger>

            <DialogContent>
                <DialogHeader>
                    <DialogTitle>编辑用户信息</DialogTitle>
                </DialogHeader>

                <FormProvider {...form}>
                    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
                        <ControlledInput name="fullName" label="姓名" placeholder="请输入姓名" />
                        <ControlledInput name="email" label="邮箱" placeholder="请输入邮箱" type="email" />
                        <Button type="submit" disabled={mutation.isPending || cooldown}>确认修改</Button>
                    </form>
                </FormProvider>
            </DialogContent>
        </Dialog>
    );
}