website-01/components/front/ContactForm.tsx

159 lines
4.2 KiB
TypeScript
Raw Normal View History

2026-06-22 14:43:46 +08:00
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { publicApi } from '@/lib/services';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
const ERROR_INPUT_CLASS = 'border-red-500 focus-visible:ring-red-500';
const schema = z.object({
name: z
.string()
.trim()
.min(1, '请输入姓名')
.max(50, '姓名不能超过 50 个字'),
phone: z
.string()
.trim()
.min(1, '请输入手机号')
.regex(/^1[3-9]\d{9}$/, '请输入正确的手机号11 位)'),
email: z
.string()
.trim()
.max(254, '邮箱长度不能超过 254')
.email('邮箱格式不正确')
.optional()
.or(z.literal('')),
content: z
.string()
.trim()
.min(5, '留言内容至少 5 个字')
.max(2000, '留言内容不能超过 2000 个字'),
});
type FormData = z.infer<typeof schema>;
export function ContactForm() {
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(schema),
mode: 'onTouched',
reValidateMode: 'onChange',
defaultValues: { name: '', phone: '', email: '', content: '' },
});
const [serverError, setServerError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const onSubmit = async (data: FormData) => {
setServerError(null);
setSuccess(false);
try {
await publicApi.submitMessage({
name: data.name,
phone: data.phone,
email: data.email || undefined,
content: data.content,
});
setSuccess(true);
reset();
} catch (e) {
setServerError((e as Error).message);
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
void handleSubmit(onSubmit)(e);
}}
className="space-y-4"
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field label="姓名" error={errors.name?.message} required>
<Input
placeholder="请输入您的姓名"
autoComplete="name"
maxLength={50}
className={errors.name ? ERROR_INPUT_CLASS : ''}
{...register('name')}
/>
</Field>
<Field label="手机号" error={errors.phone?.message} required>
<Input
placeholder="请输入 11 位手机号"
inputMode="numeric"
autoComplete="tel"
maxLength={11}
className={errors.phone ? ERROR_INPUT_CLASS : ''}
{...register('phone')}
/>
</Field>
</div>
<Field label="邮箱(可选)" error={errors.email?.message}>
<Input
placeholder="example@company.com"
inputMode="email"
autoComplete="email"
maxLength={254}
className={errors.email ? ERROR_INPUT_CLASS : ''}
{...register('email')}
/>
</Field>
<Field label="留言内容" error={errors.content?.message} required>
<Textarea
rows={5}
placeholder="请简要描述您的需求(至少 5 个字)"
maxLength={2000}
className={errors.content ? ERROR_INPUT_CLASS : ''}
{...register('content')}
/>
</Field>
{serverError && <p className="text-sm text-red-600">{serverError}</p>}
{success && (
<p className="text-sm text-green-600">
</p>
)}
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中…' : '提交留言'}
</Button>
</form>
);
}
function Field({
label,
required,
error,
children,
}: {
label: string;
required?: boolean;
error?: string;
children: React.ReactNode;
}) {
return (
<div className="space-y-1.5">
<Label>
{label}
{required && <span className="ml-1 text-red-500">*</span>}
</Label>
{children}
{error && <p className="text-xs text-red-600">{error}</p>}
</div>
);
}