联系我们
一个联系表单。发送前先校验输入,并在页面上显示“发送中”和“发送完成”。
联系我们
要点
- 1
发送中禁用按钮
发送期间禁用按钮,并显示“发送中…”。如果按钮还能点,同一条咨询可能会收到两次。
- 2
发送成功后,整个页面切换
发送完成后,隐藏表单,显示“已发送”的页面。再写明大概什么时候会回复,对方会更安心。
- 3
提前告诉用户能写多少
在正文下方显示“当前字数 / 上限”,超过上限就变成红色。比起发送之后才被告知“太长了”,这样更贴心。
使用的工具
点击查看第 2 部分的比较
修改代码
修改代码,右边的预览会跟着变化。
import { useState } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { CircleCheckIcon } from "lucide-react" import { Button } from "./components/ui/button" import { Input } from "./components/ui/input" import { Label } from "./components/ui/label" // 咨询类型 const kinds = ["使用方法", "问题反馈", "其他"] // 校验规则(Zod) const schema = z.object({ name: z.string().min(1, "请输入姓名"), email: z.string().email("请输入有效的邮箱地址"), kind: z.enum(kinds, { error: "请选择类型" }), message: z.string().min(10, "请至少输入 10 个字").max(500, "最多 500 个字"), }) // 假装发送的函数(耗时 1.5 秒) function send(values) { return new Promise((resolve) => setTimeout(resolve, 1500)) } // 把标签、输入框和错误提示组合在一起 function Field({ id, label, error, children }) { return ( <div className="flex flex-col gap-2"> <Label htmlFor={id}>{label}</Label> {children} {error && <p className="text-sm text-destructive">{error}</p>} </div> ) } export default function App() { // ① 是否已发送完成(一开始是 false),放在 state 里 const [sent, setSent] = useState(false) const { register, handleSubmit, watch, reset, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { name: "", email: "", kind: undefined, message: "" }, }) // 监听正文,用来统计当前字数 const message = watch("message") // 点击“发送”且校验通过后执行 async function onSubmit(values) { await send(values) // 等待发送完成(1.5 秒) // ② 发送完成,把 sent 设为 true setSent(true) } // ③ 发送完成后,先返回完成页面 // 条件:是否已发送完成 if (sent) { return ( <div className="mx-auto my-6 w-full max-w-lg rounded-xl border bg-card p-10 text-center text-card-foreground"> <CircleCheckIcon className="mx-auto size-12 text-emerald-500" /> <p className="mt-4 text-lg font-semibold">已发送</p> <p className="mt-2 text-sm text-muted-foreground"> 感谢您的咨询。我们会在 2 个工作日内通过邮件回复您。 </p> {/* ④ 点击后清空输入(reset),并回到表单页面(把 sent 设为 false) */} <Button variant="outline" className="mt-6" onClick={() => { reset() setSent(false) }}> 再发一次(演示用) </Button> </div > ) } return ( <form onSubmit={handleSubmit(onSubmit)} className="mx-auto my-6 flex w-full max-w-lg flex-col gap-5 rounded-xl border bg-card p-6 text-card-foreground" > <div> <h1 className="text-lg font-semibold">联系我们</h1> <p className="mt-1 text-sm text-muted-foreground">如有任何问题或建议,欢迎随时联系我们。</p> </div> <Field id="name" label="姓名" error={errors.name?.message}> <Input id="name" placeholder="张伟" {...register("name")} /> </Field> <Field id="email" label="邮箱" error={errors.email?.message}> <Input id="email" type="email" placeholder="zhang.wei@example.com" {...register("email")} /> </Field> <div className="flex flex-col gap-2"> <Label>类型</Label> <div className="flex flex-wrap gap-2"> {kinds.map((kind) => ( <label key={kind} className="cursor-pointer rounded-full border px-3 py-1.5 text-sm has-checked:border-primary has-checked:bg-primary has-checked:text-primary-foreground" > <input type="radio" value={kind} className="sr-only" {...register("kind")} /> {kind} </label> ))} </div> {errors.kind && <p className="text-sm text-destructive">{errors.kind.message}</p>} </div> <Field id="message" label="咨询内容" error={errors.message?.message}> <textarea id="message" rows={5} placeholder="请至少输入 10 个字" className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50" {...register("message")} /> {/* ⑦ 超过 500 字时显示红色(text-destructive),否则显示灰色(text-muted-foreground) */} <p className={message.length > 500 ? "text-right text-xs text-destructive" : "text-right text-xs text-muted-foreground"}> {/* ⑥ 正文当前的字数 */} {message.length} / 500 </p> </Field> {/* ⑤ 发送中(isSubmitting 为 true)时不能点击。发送中显示“发送中…”,否则显示“发送” */} <Button type="submit" disabled={isSubmitting}> {isSubmitting ? "发送中…" : "发送"} </Button> </form> ) }