聯絡我們
聯絡我們的表單。送出前先驗證輸入,並在畫面上顯示「送出中」和「送出完成」。
聯絡我們
重點
- 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="zhiming.lin@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> ) }