website-01/components/front/ContactForm.tsx
2026-06-22 14:43:46 +08:00

159 lines
4.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
);
}