创建联系表单的页面 | 自在学创建联系表单的页面
表单是网站与用户交互的重要方式。在这一个小结中,我们将学习如何创建联系表单,实现表单验证,处理表单提交,并使用 Server Actions 来处理服务器端逻辑。

步骤 1:创建基础表单结构
在我们正式实现完整的联系表单功能前,先来构建一个基础的表单框架。这个初始版本仅包含最核心的表单字段(如姓名、邮箱和消息内容),让你直观理解表单的结构和基本组件。
// app/contact/page.tsx
export default function ContactPage() {
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="container mx-auto px-4 max-w-2xl">
<h1 className="text-4xl font-bold text-gray-900 mb-8">联系我们</h1>
<form className="bg-white rounded-lg shadow-md p-8 space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-semibold text-gray-700 mb-2">
姓名
</label>
<input
type="text"
id="name"
name="name"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-semibold text-gray-700 mb-2">
邮箱
</label>
<input
type="email"
id="email"
name="email"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-semibold text-gray-700 mb-2">
消息
</label>
<textarea
id="message"
name="message"
rows={6}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
></textarea>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-600 transition-colors"
>
发送消息
</button>
</form>
</div>
</div>
)
}
这是一个基础的 HTML 表单,使用了 Tailwind CSS 进行样式设计。目前它只是一个静态表单,还没有处理用户输入和提交的逻辑。
步骤 2:添加 React 状态管理
为了让表单能够响应用户输入,我们需要使用 React 的 useState Hook 来管理表单数据。注意这里需要添加 'use client' 指令,因为我们需要使用客户端功能:
'use client'
import { useState } from 'react'
export default function ContactPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
})
const handleChange = (e: React.ChangeEvent
现在表单可以捕获用户输入了。我们使用 useState 来存储表单数据,并通过 handleChange 函数更新状态。当用户提交表单时,handleSubmit 函数会阻止默认的表单提交行为,并打印出表单数据。
步骤 3:添加表单验证和错误处理
为了确保用户能够顺利填写并提交表单,我们接下来将为联系表单添加详细的表单验证功能。具体来说,我们会实现以下几点:
- 输入校验:在用户输入时实时检测每个字段的有效性(如姓名不能为空,邮箱格式正确,消息内容不小于一定字数)。
- 即时反馈:如果用户的输入不符合要求,会在对应的输入框下方立刻显示出详细且清晰的错误提示,帮助用户快速发现并纠正问题。
- 提交校验:在表单提交时统一再次校验所有字段,只有全部通过校验后才允许发送数据,避免无效或不完整的信息提交到后台。
- 用户体验优化:通过合理的交互和提示,提高表单的易用性和专业感,降低用户操作的疑惑和焦虑感。
'use client'
import { useState } from 'react'
interface FormErrors {
name?: string
email?: string
message?: string
}
export default function ContactPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
当用户输入时,会清除对应字段的错误;提交时会验证所有字段,并在有错误时显示错误信息。同时,我们还添加了 isSubmitting 状态来防止重复提交。
步骤 4:使用 Server Actions 处理表单提交
Server Actions 是 Next.js 14+ 引入的功能,允许你在服务器端处理表单提交,无需创建 API 路由。让我们创建一个 Server Action 来处理表单提交:
首先,创建 Server Action 文件:
// app/actions/contact.ts
'use server'
export async function submitContactForm(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
然后在表单组件中使用 Server Action。注意,使用 Server Actions 时,我们可以直接使用表单的 action 属性,而不需要手动处理 onSubmit:
'use client'
import { useState } from 'react'
import { submitContactForm } from '@/app/actions/contact'
export default function ContactPage() {
const [status, setStatus] = useState<{ success: boolean; message?: string; error?: string } | null>(null)
Server Actions 的优势在于类型安全、无需 API 路由,并且能够自动处理 CSRF 保护。这使得表单处理更加简单和安全。
完整的联系表单代码
接下来,我们将详细讲解集成了所有关键特性的联系表单完整实现代码。下面的代码不仅涵盖了 React 状态管理、表单各字段的实时验证、错误提示、提交结果反馈(如发送成功或失败),而且安全高效地利用了 Next.js 的 Server Actions 处理服务端逻辑。
'use client'
import { useState } from 'react'
import { submitContactForm } from '@/app/actions/contact'
export default function ContactPage() {
const [status, setStatus] = useState<{ success: boolean; message?: string; error?: string } | null>(null)
下一步
在这一小结中,我们学习了如何创建联系表单,包括状态管理、表单验证、错误处理和 Server Actions。这样用户终于有地方可以联系我们了。
你可能觉得我们已经有了一个功能完全的网站了,但并不是这样的,如果你想让别人更有可能的搜索出你的网站,你需要做一些SEO优化,这将是我们马上就要接触到的。
<
HTMLInputElement
|
HTMLTextAreaElement
>)
=>
{
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log('表单数据:', formData)
// 这里处理表单提交
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="container mx-auto px-4 max-w-2xl">
<h1 className="text-4xl font-bold text-gray-900 mb-8">联系我们</h1>
<form onSubmit={handleSubmit} className="bg-white rounded-lg shadow-md p-8 space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-semibold text-gray-700 mb-2">
姓名
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-semibold text-gray-700 mb-2">
邮箱
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-semibold text-gray-700 mb-2">
消息
</label>
<textarea
id="message"
name="message"
rows={6}
value={formData.message}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
></textarea>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-600 transition-colors"
>
发送消息
</button>
</form>
</div>
</div>
)
}
message: '',
})
const [errors, setErrors] = useState<FormErrors>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const validate = () => {
const newErrors: FormErrors = {}
if (!formData.name.trim()) {
newErrors.name = '姓名是必填项'
} else if (formData.name.trim().length < 2) {
newErrors.name = '姓名至少需要 2 个字符'
}
if (!formData.email.trim()) {
newErrors.email = '邮箱是必填项'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = '请输入有效的邮箱地址'
}
if (!formData.message.trim()) {
newErrors.message = '消息是必填项'
} else if (formData.message.trim().length < 10) {
newErrors.message = '消息至少需要 10 个字符'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData({
...formData,
[name]: value,
})
// 清除该字段的错误
if (errors[name as keyof FormErrors]) {
setErrors({
...errors,
[name]: undefined,
})
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validate()) {
return
}
setIsSubmitting(true)
try {
// 这里处理表单提交
await new Promise(resolve => setTimeout(resolve, 1000)) // 模拟 API 调用
alert('消息发送成功!')
setFormData({ name: '', email: '', message: '' })
} catch (error) {
alert('发送失败,请稍后重试')
} finally {
setIsSubmitting(false)
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="container mx-auto px-4 max-w-2xl">
<h1 className="text-4xl font-bold text-gray-900 mb-8">联系我们</h1>
<form onSubmit={handleSubmit} className="bg-white rounded-lg shadow-md p-8 space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-semibold text-gray-700 mb-2">
姓名
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={`w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.name ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.name && (
<p className="mt-1 text-sm text-red-500">{errors.name}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-semibold text-gray-700 mb-2">
邮箱
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={`w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.email ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-500">{errors.email}</p>
)}
</div>
<div>
<label htmlFor="message" className="block text-sm font-semibold text-gray-700 mb-2">
消息
</label>
<textarea
id="message"
name="message"
rows={6}
value={formData.message}
onChange={handleChange}
className={`w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.message ? 'border-red-500' : 'border-gray-300'
}`}
></textarea>
{errors.message && (
<p className="mt-1 text-sm text-red-500">{errors.message}</p>
)}
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? '发送中...' : '发送消息'}
</button>
</form>
</div>
</div>
)
}
// 验证数据
if (!name || !email || !message) {
return { success: false, error: '所有字段都是必填项' }
}
// 这里可以保存到数据库或发送邮件
console.log('收到联系表单:', { name, email, message })
// 模拟处理时间
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '消息发送成功!' }
}
const [isSubmitting, setIsSubmitting] = useState(false)
async function handleSubmit(formData: FormData) {
setIsSubmitting(true)
setStatus(null)
try {
const result = await submitContactForm(formData)
setStatus(result)
if (result.success) {
// 重置表单
const form = document.getElementById('contact-form') as HTMLFormElement
form?.reset()
}
} catch (error) {
setStatus({
success: false,
error: '发送失败,请稍后重试',
})
} finally {
setIsSubmitting(false)
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="container mx-auto px-4 max-w-2xl">
<h1 className="text-4xl font-bold text-gray-900 mb-8">联系我们</h1>
{status && (
<div className={`mb-6 p-4 rounded-lg border ${
status.success
? 'bg-green-50 text-green-800 border-green-200'
: 'bg-red-50 text-red-800 border-red-200'
}`}>
<div className="flex items-center gap-2">
<span>{status.success ? '✓' : '✗'}</span>
<span>{status.success ? status.message : status.error}</span>
</div>
</div>
)}
<form
id="contact-form"
action={handleSubmit}
className="bg-white rounded-lg shadow-md p-8 space-y-6"
>
<div>
<label htmlFor="name" className="block text-sm font-semibold text-gray-700 mb-2">
姓名 <span className="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
placeholder="请输入您的姓名"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-semibold text-gray-700 mb-2">
邮箱 <span className="text-red-500">*</span>
</label>
<input
type="email"
id="email"
name="email"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
placeholder="your.email@example.com"
required
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-semibold text-gray-700 mb-2">
主题
</label>
<input
type="text"
id="subject"
name="subject"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
placeholder="消息主题(可选)"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-semibold text-gray-700 mb-2">
消息 <span className="text-red-500">*</span>
</label>
<textarea
id="message"
name="message"
rows={6}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors resize-none"
placeholder="请输入您的消息..."
required
></textarea>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isSubmitting ? (
<>
<span className="animate-spin">⏳</span>
<span>发送中...</span>
</>
) : (
'发送消息'
)}
</button>
</form>
</div>
</div>
)
}
const [isSubmitting, setIsSubmitting] = useState(false)
async function handleSubmit(formData: FormData) {
setIsSubmitting(true)
setStatus(null)
try {
const result = await submitContactForm(formData)
setStatus(result)
if (result.success) {
const form = document.getElementById('contact-form') as HTMLFormElement
form?.reset()
}
} catch (error) {
setStatus({
success: false,
error: '发送失败,请稍后重试',
})
} finally {
setIsSubmitting(false)
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="container mx-auto px-4 max-w-2xl">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">联系我们</h1>
<p className="text-lg text-gray-600">
有任何问题或建议?我们很乐意听到你的声音
</p>
</div>
{status && (
<div className={`mb-6 p-4 rounded-lg border ${
status.success
? 'bg-green-50 text-green-800 border-green-200'
: 'bg-red-50 text-red-800 border-red-200'
}`}>
<div className="flex items-center gap-2">
<span>{status.success ? '✓' : '✗'}</span>
<span>{status.success ? status.message : status.error}</span>
</div>
</div>
)}
<form
id="contact-form"
action={handleSubmit}
className="bg-white rounded-lg shadow-md p-8 space-y-6"
>
<div>
<label htmlFor="name" className="block text-sm font-semibold text-gray-700 mb-2">
姓名 <span className="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
placeholder="请输入您的姓名"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-semibold text-gray-700 mb-2">
邮箱 <span className="text-red-500">*</span>
</label>
<input
type="email"
id="email"
name="email"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
placeholder="your.email@example.com"
required
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-semibold text-gray-700 mb-2">
主题
</label>
<input
type="text"
id="subject"
name="subject"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
placeholder="消息主题(可选)"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-semibold text-gray-700 mb-2">
消息 <span className="text-red-500">*</span>
</label>
<textarea
id="message"
name="message"
rows={6}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors resize-none"
placeholder="请输入您的消息..."
required
></textarea>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isSubmitting ? (
<>
<span className="animate-spin">⏳</span>
<span>发送中...</span>
</>
) : (
'发送消息'
)}
</button>
</form>
<div className="mt-8 text-center text-gray-600">
<p>或者通过以下方式联系我们:</p>
<div className="mt-4 space-y-2">
<p>邮箱: info@freeeducation.com</p>
<p>电话: +86 123 456 7890</p>
</div>
</div>
</div>
</div>
)
}