Files
company-site/components/sections/contact-section.tsx
JanYork e2850586a9 feat: 实现联系表单提交与邮件通知功能
添加联系表单提交接口和邮件通知功能,支持从环境变量读取 SMTP 配置
重构 SEO 配置到 site.json,新增 robots.txt 和 sitemap.xml 生成
更新公司电话并添加 PM2 生产运行配置
2026-03-18 13:20:40 +08:00

205 lines
9.0 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, type FormEvent, type ReactNode } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { SectionReveal } from "@/components/ui/section-reveal"
import { Phone, Mail, MapPin, Clock3, ArrowRight, CheckCircle2 } from "lucide-react"
import homeData from "@/data/home.json"
const iconMap: Record<string, ReactNode> = {
phone: <Phone className="w-5 h-5" />,
mail: <Mail className="w-5 h-5" />,
mappin: <MapPin className="w-5 h-5" />,
clock: <Clock3 className="w-5 h-5" />,
}
export function ContactSection() {
const { contact } = homeData
const sectionBadge = (contact as any).sectionBadge ?? (contact as any).sectionTag
const info = ((contact as any).info ??
((contact as any).infoItems ?? []).map((item: any) => ({
...item,
icon: String(item.icon ?? "").toLowerCase(),
sub: item.sub ?? "",
}))) as Array<{ icon: string; label: string; value: string; sub?: string }>
const submitLabel = (contact.form as any).submitLabel ?? (contact.form as any).submit
const [submitted, setSubmitted] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const form = e.currentTarget
const formData = new FormData(form)
setErrorMessage(null)
setIsSubmitting(true)
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: String(formData.get("name") ?? ""),
company: String(formData.get("company") ?? ""),
phone: String(formData.get("phone") ?? ""),
scale: String(formData.get("scale") ?? ""),
message: String(formData.get("message") ?? ""),
}),
})
const result = (await response.json().catch(() => null)) as { error?: string } | null
if (!response.ok) {
throw new Error(result?.error ?? "提交失败,请稍后重试。")
}
form.reset()
setSubmitted(true)
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : "提交失败,请稍后重试。")
} finally {
setIsSubmitting(false)
}
}
return (
<section id="contact" className="section-panel bg-background">
<SectionReveal className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" delay={140}>
{/* Header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<Badge variant="secondary" className="mb-4 text-primary border-primary/20 bg-primary/8 px-3 py-1">
{sectionBadge}
</Badge>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-foreground text-balance tracking-tight mb-4">
{contact.title}
</h2>
<p className="text-muted-foreground text-lg leading-relaxed text-pretty">
{contact.subtitle}
</p>
</div>
<div className="grid lg:grid-cols-5 gap-10 items-start">
{/* Left: info */}
<div className="lg:col-span-2 flex flex-col gap-5">
{info.map((item) => (
<div
key={item.label}
className="flex items-start gap-4 p-5 bg-muted rounded-2xl border border-border hover:border-primary/30 transition-colors"
>
<div className="w-11 h-11 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0">
{iconMap[item.icon] ?? iconMap.phone}
</div>
<div>
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-widest mb-1">{item.label}</p>
<p className="font-bold text-foreground text-sm">{item.value}</p>
<p className="text-xs text-muted-foreground mt-0.5">{item.sub}</p>
</div>
</div>
))}
{/* Trust note */}
<div className="bg-primary/6 border border-primary/15 rounded-2xl p-5">
<p className="text-sm text-primary font-semibold mb-1.5"></p>
<p className="text-xs text-muted-foreground leading-relaxed">
{" "}
<strong className="text-foreground">2 </strong>
{" "}
</p>
</div>
</div>
{/* Right: form */}
<div className="lg:col-span-3 bg-white border border-border rounded-3xl p-8 shadow-lg shadow-primary/5">
{submitted ? (
<div className="flex flex-col items-center justify-center py-16 gap-5 text-center">
<div className="w-16 h-16 rounded-full bg-green-50 border-2 border-green-200 flex items-center justify-center">
<CheckCircle2 className="w-8 h-8 text-green-500" />
</div>
<div>
<h3 className="text-xl font-bold text-foreground mb-2"></h3>
<p className="text-muted-foreground text-sm">
2
</p>
</div>
<Button
variant="outline"
onClick={() => {
setSubmitted(false)
setErrorMessage(null)
}}
className="mt-2"
>
</Button>
</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
<h3 className="text-lg font-bold text-foreground"></h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground" htmlFor="name"></label>
<Input id="name" name="name" placeholder={contact.form.namePlaceholder} required className="h-11" />
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground" htmlFor="company"> / </label>
<Input id="company" name="company" placeholder={contact.form.companyPlaceholder} required className="h-11" />
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground" htmlFor="phone"></label>
<Input id="phone" name="phone" type="tel" placeholder={contact.form.phonePlaceholder} required className="h-11" />
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground" htmlFor="scale"></label>
<Input id="scale" name="scale" placeholder={contact.form.scalePlaceholder} className="h-11" />
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground" htmlFor="message"></label>
<Textarea
id="message"
name="message"
placeholder={contact.form.messagePlaceholder}
rows={4}
className="resize-none"
/>
</div>
<Button
type="submit"
size="lg"
disabled={isSubmitting}
className="w-full bg-primary hover:bg-primary-dark text-white h-12 text-base font-semibold group shadow-lg shadow-primary/25"
>
<span className="flex items-center gap-2">
{isSubmitting ? "提交中..." : submitLabel}
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
</span>
</Button>
{errorMessage ? (
<p className="text-sm text-red-600 text-center" aria-live="polite">
{errorMessage}
</p>
) : null}
<p className="text-xs text-muted-foreground text-center">
{" "}
<span className="text-primary cursor-pointer hover:underline"></span>
</p>
</form>
)}
</div>
</div>
</SectionReveal>
</section>
)
}