联系我们

一个联系表单。发送前先校验输入,并在页面上显示“发送中”和“发送完成”。

联系我们

要点

  1. 1

    发送中禁用按钮

    发送期间禁用按钮,并显示“发送中…”。如果按钮还能点,同一条咨询可能会收到两次。

  2. 2

    发送成功后,整个页面切换

    发送完成后,隐藏表单,显示“已发送”的页面。再写明大概什么时候会回复,对方会更安心。

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