'use client'; import { useState, useEffect, useRef } from 'react'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Toast } from '@/components/ui/toast'; import { sanitizeInput } from '@/lib/sanitize'; import { generateCSRFToken, setCSRFTokenToStorage, getCSRFTokenFromStorage } from '@/lib/csrf'; import { generateCaptcha } from '@/lib/security/captcha'; import { useFormAutosave } from '@/hooks/use-form-autosave'; import { Mail, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2, RefreshCw, Save } from 'lucide-react'; import { COMPANY_INFO } from '@/lib/constants'; const contactFormSchema = z.object({ name: z.string().min(2, '姓名至少需要2个字符'), phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号码'), email: z.string().email('请输入有效的邮箱地址'), message: z.string().min(10, '留言内容至少需要10个字符'), }); type ContactFormData = z.infer; interface FormErrors { name?: string; phone?: string; email?: string; message?: string; captcha?: string; } export function ContactSection() { const [isVisible, setIsVisible] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); const [showToast, setShowToast] = useState(false); const [toastMessage, setToastMessage] = useState(''); const [toastType, setToastType] = useState<'success' | 'error'>('success'); const [errors, setErrors] = useState({}); const [captcha, setCaptcha] = useState(generateCaptcha('simple')); const [captchaAnswer, setCaptchaAnswer] = useState(''); const sectionRef = useRef(null); // 使用表单自动保存功能 const { data: formData, updateData, lastSaved, isRestored, clearSavedData, } = useFormAutosave({ key: 'contact_form', initialData: { name: '', phone: '', email: '', message: '', }, debounceMs: 1000, }); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry?.isIntersecting) { setIsVisible(true); } }, { threshold: 0.1 } ); if (sectionRef.current) { observer.observe(sectionRef.current); } const csrfToken = generateCSRFToken(); setCSRFTokenToStorage(csrfToken); return () => observer.disconnect(); }, []); const validateField = (field: keyof ContactFormData, value: string) => { try { contactFormSchema.shape[field].parse(value); setErrors((prev) => ({ ...prev, [field]: undefined })); } catch (error) { if (error instanceof z.ZodError) { const fieldError = error.issues[0]; if (fieldError) { setErrors((prev) => ({ ...prev, [field]: fieldError.message })); } } } }; const handleChange = (field: keyof ContactFormData, value: string) => { const sanitizedValue = sanitizeInput(value); updateData({ [field]: sanitizedValue }); if (errors[field]) { validateField(field, sanitizedValue); } }; const handleBlur = (field: keyof ContactFormData, value: string) => { validateField(field, value); }; const handleCaptchaRefresh = () => { setCaptcha(generateCaptcha('simple')); setCaptchaAnswer(''); setErrors((prev) => ({ ...prev, captcha: undefined })); }; const handleCaptchaChange = (value: string) => { setCaptchaAnswer(value); if (errors.captcha) { setErrors((prev) => ({ ...prev, captcha: undefined })); } }; async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const storedToken = getCSRFTokenFromStorage(); if (!storedToken) { setToastMessage('安全验证失败,请刷新页面重试。'); setToastType('error'); setShowToast(true); return; } const result = contactFormSchema.safeParse(formData); if (!result.success) { const fieldErrors: FormErrors = {}; result.error.issues.forEach((issue) => { const field = issue.path[0] as keyof ContactFormData; fieldErrors[field] = issue.message; }); setErrors(fieldErrors); return; } if (!captchaAnswer || parseInt(captchaAnswer) !== captcha.answer) { setErrors((prev) => ({ ...prev, captcha: '验证码错误,请重新计算' })); handleCaptchaRefresh(); return; } setIsSubmitting(true); try { const response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ...formData, csrfToken: storedToken, mathHash: captcha.hash, mathTimestamp: captcha.timestamp, mathAnswer: captcha.answer, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '提交失败'); } const newCsrfToken = generateCSRFToken(); setCSRFTokenToStorage(newCsrfToken); setIsSubmitting(false); setIsSubmitted(true); clearSavedData(); // 提交成功后清除保存的数据 setToastMessage('表单提交成功!我们会尽快与您联系。'); setToastType('success'); setShowToast(true); } catch (error) { setIsSubmitting(false); setToastMessage(error instanceof Error ? error.message : '提交失败,请稍后重试。'); setToastType('error'); setShowToast(true); } } return (
{showToast && ( setShowToast(false)} data-testid="toast-notification" /> )}
联系我们

开启 合作

无论您有任何问题或合作意向,我们都很乐意与您交流

联系方式

地址

{COMPANY_INFO.address}

工作时间

周一至周五 9:00 - 18:00

我们的承诺

工作日 2 小时内快速响应您的咨询

提供免费的业务咨询和方案评估服务

根据您的需求量身定制最优解决方案

发送消息

{/* 自动保存状态指示器 */}
{lastSaved && ( <> 已保存 {lastSaved.toLocaleTimeString()} )}
{/* 数据恢复提示 */} {isRestored && (
已恢复您上次未提交的表单内容
)} {isSubmitted ? (

消息已发送

感谢您的留言,我们会尽快与您联系!

) : (
handleChange('name', e.target.value)} onBlur={(e) => handleBlur('name', e.target.value)} error={errors.name} /> handleChange('phone', e.target.value)} onBlur={(e) => handleBlur('phone', e.target.value)} error={errors.phone} />
handleChange('email', e.target.value)} onBlur={(e) => handleBlur('email', e.target.value)} error={errors.email} />