159 lines
4.2 KiB
TypeScript
159 lines
4.2 KiB
TypeScript
|
|
'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>
|
|||
|
|
);
|
|||
|
|
}
|