feat: 实现联系表单提交与邮件通知功能

添加联系表单提交接口和邮件通知功能,支持从环境变量读取 SMTP 配置
重构 SEO 配置到 site.json,新增 robots.txt 和 sitemap.xml 生成
更新公司电话并添加 PM2 生产运行配置
This commit is contained in:
JanYork
2026-03-18 13:20:40 +08:00
parent 59e80bf938
commit e2850586a9
20 changed files with 872 additions and 28 deletions

View File

@@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
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"
@@ -9,7 +9,7 @@ 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, React.ReactNode> = {
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" />,
@@ -27,10 +27,46 @@ export function ContactSection() {
}))) 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 = (e: React.FormEvent) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
setSubmitted(true)
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 (
@@ -91,38 +127,46 @@ export function ContactSection() {
<div>
<h3 className="text-xl font-bold text-foreground mb-2"></h3>
<p className="text-muted-foreground text-sm">
2
2
</p>
</div>
<Button variant="outline" onClick={() => setSubmitted(false)} className="mt-2">
<Button
variant="outline"
onClick={() => {
setSubmitted(false)
setErrorMessage(null)
}}
className="mt-2"
>
</Button>
</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col gap-5" noValidate>
<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" placeholder={contact.form.namePlaceholder} required className="h-11" />
<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" placeholder={contact.form.companyPlaceholder} required className="h-11" />
<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" type="tel" placeholder={contact.form.phonePlaceholder} required className="h-11" />
<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" placeholder={contact.form.scalePlaceholder} className="h-11" />
<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"
@@ -131,13 +175,19 @@ export function ContactSection() {
<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">
{submitLabel}
{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>