表单数据由React状态管理
表单数据由DOM自身管理
在React中,受控组件是指表单元素的值由React的state控制,并通过事件处理器更新的组件。
import React, { useState } from 'react';
function ControlledForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
rememberMe: false
});
// 处理输入变化
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
};
// 处理表单提交
const handleSubmit = (e) => {
e.preventDefault();
console.log('表单数据:', formData);
// 这里可以发送数据到服务器
};
return (
<form onSubmit={handleSubmit} className="controlled-form">
<div className="form-group">
<label htmlFor="username">用户名:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
placeholder="请输入用户名"
/>
</div>
<div className="form-group">
<label htmlFor="email">邮箱:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="请输入邮箱"
/>
</div>
<div className="form-group">
<label htmlFor="password">密码:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="请输入密码"
/>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
name="rememberMe"
checked={formData.rememberMe}
onChange={handleChange}
/>
记住我
</label>
</div>
<button type="submit">提交</button>
<button type="button" onClick={() => setFormData({
username: '',
email: '',
password: '',
rememberMe: false
})}>
重置
</button>
</form>
);
}
function AllFormElements() {
const [formState, setFormState] = useState({
// 文本输入
textInput: '',
emailInput: '',
passwordInput: '',
textareaInput: '',
// 选择框
selectInput: 'option1',
multiSelect: ['option1'],
// 单选和多选
radioInput: 'male',
checkboxGroup: {
option1: false,
option2: true,
option3: false
},
// 其他
rangeInput: 50,
dateInput: '',
colorInput: '#61dafb'
});
// 通用变化处理器
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
if (type === 'checkbox' && name in formState.checkboxGroup) {
// 处理checkbox group
setFormState({
...formState,
checkboxGroup: {
...formState.checkboxGroup,
[name]: checked
}
});
} else if (type === 'checkbox') {
// 处理单个checkbox
setFormState({
...formState,
[name]: checked
});
} else if (type === 'select-multiple') {
// 处理多选select
const selectedOptions = Array.from(e.target.selectedOptions).map(
option => option.value
);
setFormState({
...formState,
[name]: selectedOptions
});
} else {
// 处理其他输入类型
setFormState({
...formState,
[name]: value
});
}
};
return (
<form>
{/* 文本输入 */}
<input
type="text"
name="textInput"
value={formState.textInput}
onChange={handleChange}
placeholder="文本输入"
/>
{/* 邮箱输入 */}
<input
type="email"
name="emailInput"
value={formState.emailInput}
onChange={handleChange}
placeholder="邮箱"
/>
{/* 密码输入 */}
<input
type="password"
name="passwordInput"
value={formState.passwordInput}
onChange={handleChange}
placeholder="密码"
/>
{/* 文本区域 */}
<textarea
name="textareaInput"
value={formState.textareaInput}
onChange={handleChange}
placeholder="多行文本"
/>
{/* 下拉选择 */}
<select
name="selectInput"
value={formState.selectInput}
onChange={handleChange}
>
<option value="option1">选项1</option>
<option value="option2">选项2</option>
<option value="option3">选项3</option>
</select>
{/* 多选下拉 */}
<select
name="multiSelect"
value={formState.multiSelect}
onChange={handleChange}
multiple
>
<option value="option1">选项1</option>
<option value="option2">选项2</option>
<option value="option3">选项3</option>
</select>
{/* 单选按钮组 */}
<div>
<label>
<input
type="radio"
name="radioInput"
value="male"
checked={formState.radioInput === 'male'}
onChange={handleChange}
/>
男
</label>
<label>
<input
type="radio"
name="radioInput"
value="female"
checked={formState.radioInput === 'female'}
onChange={handleChange}
/>
女
</label>
</div>
{/* 复选框组 */}
<div>
{['option1', 'option2', 'option3'].map(option => (
<label key={option}>
<input
type="checkbox"
name={option}
checked={formState.checkboxGroup[option]}
onChange={handleChange}
/>
{option}
</label>
))}
</div>
{/* 范围选择器 */}
<input
type="range"
name="rangeInput"
min="0"
max="100"
value={formState.rangeInput}
onChange={handleChange}
/>
{/* 日期选择器 */}
<input
type="date"
name="dateInput"
value={formState.dateInput}
onChange={handleChange}
/>
{/* 颜色选择器 */}
<input
type="color"
name="colorInput"
value={formState.colorInput}
onChange={handleChange}
/>
</form>
);
}
非受控组件使用ref直接从DOM获取表单值,而不是通过state控制。
import React, { useRef } from 'react';
function UncontrolledForm() {
const formRef = useRef(null);
const usernameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// 方式1:通过ref获取单个输入的值
console.log('用户名:', usernameRef.current.value);
console.log('邮箱:', emailRef.current.value);
// 方式2:通过FormData获取所有表单数据
const formData = new FormData(formRef.current);
const data = Object.fromEntries(formData.entries());
console.log('所有表单数据:', data);
// 方式3:遍历表单元素
const elements = formRef.current.elements;
const values = {};
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.name) {
if (element.type === 'checkbox') {
values[element.name] = element.checked;
} else if (element.type === 'radio') {
if (element.checked) {
values[element.name] = element.value;
}
} else {
values[element.name] = element.value;
}
}
}
console.log('遍历获取的值:', values);
};
// 设置默认值
const setDefaultValues = () => {
usernameRef.current.value = '默认用户';
emailRef.current.value = 'default@example.com';
};
return (
<form ref={formRef} onSubmit={handleSubmit} className="uncontrolled-form">
<div className="form-group">
<label htmlFor="uname">用户名:</label>
<input
type="text"
id="uname"
name="username"
ref={usernameRef}
placeholder="请输入用户名"
defaultValue="访客" // 使用defaultValue而不是value
/>
</div>
<div className="form-group">
<label htmlFor="uemail">邮箱:</label>
<input
type="email"
id="uemail"
name="email"
ref={emailRef}
placeholder="请输入邮箱"
/>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
name="rememberMe"
defaultChecked={true} // 使用defaultChecked而不是checked
/>
记住我
</label>
</div>
<div className="form-group">
<label>性别:</label>
<label>
<input
type="radio"
name="gender"
value="male"
defaultChecked // 使用defaultChecked而不是checked
/>
男
</label>
<label>
<input
type="radio"
name="gender"
value="female"
/>
女
</label>
</div>
<button type="submit">提交</button>
<button type="button" onClick={setDefaultValues}>
设置默认值
</button>
</form>
);
}
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 状态管理 | React状态控制 | DOM自身管理 |
| 值获取 | 通过state获取 | 通过ref或FormData获取 |
| 值设置 | 通过state设置 | 通过defaultValue/defaultChecked设置 |
| 实时验证 | 容易实现 | 较难实现 |
| 性能 | 每次输入都触发渲染 | 只在需要时获取值 |
| 代码复杂度 | 较高 | 较低 |
| 适用场景 | 需要实时验证、即时反馈 | 简单表单、文件上传、第三方库集成 |
defaultValue而不是value设置初始值defaultChecked而不是checked设置复选框和单选按钮表单验证是确保用户输入符合要求的重要环节。React中可以轻松实现各种验证逻辑。
// 表单验证代码将在这里显示
import React, { useState } from 'react';
function FormWithValidation() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
// 验证规则
const validationRules = {
name: (value) => {
if (!value.trim()) return '姓名不能为空';
if (value.length < 2) return '姓名至少2个字符';
if (value.length > 20) return '姓名不能超过20个字符';
return '';
},
email: (value) => {
if (!value.trim()) return '邮箱不能为空';
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) return '请输入有效的邮箱地址';
return '';
},
password: (value) => {
if (!value.trim()) return '密码不能为空';
if (value.length < 6) return '密码至少6个字符';
if (!/[A-Z]/.test(value)) return '密码必须包含大写字母';
if (!/[a-z]/.test(value)) return '密码必须包含小写字母';
if (!/[0-9]/.test(value)) return '密码必须包含数字';
return '';
},
confirmPassword: (value) => {
if (value !== formData.password) return '两次输入的密码不一致';
return '';
}
};
// 处理输入变化
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
// 如果已经触摸过该字段,立即验证
if (touched[name]) {
validateField(name, value);
}
};
// 处理字段失去焦点
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched({
...touched,
[name]: true
});
validateField(name, value);
};
// 验证单个字段
const validateField = (name, value) => {
const error = validationRules[name] ? validationRules[name](value) : '';
setErrors({
...errors,
[name]: error
});
return !error;
};
// 验证整个表单
const validateForm = () => {
const newErrors = {};
let isValid = true;
Object.keys(validationRules).forEach(name => {
const error = validationRules[name](formData[name]);
if (error) {
newErrors[name] = error;
isValid = false;
}
});
setErrors(newErrors);
// 标记所有字段为已触摸
setTouched(Object.keys(formData).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {}));
return isValid;
};
// 处理表单提交
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('表单验证通过:', formData);
// 提交数据到服务器
alert('表单提交成功!');
} else {
console.log('表单验证失败:', errors);
alert('请修正表单错误后再提交。');
}
};
// 检查字段是否有效
const getFieldStatus = (name) => {
if (!touched[name]) return ''; // 未触摸
return errors[name] ? 'is-invalid' : 'is-valid';
};
return (
<form onSubmit={handleSubmit} className="form-with-validation">
<h3>注册表单</h3>
<div className="form-group">
<label htmlFor="name">姓名 *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
onBlur={handleBlur}
className={`form-control ${getFieldStatus('name')}`}
placeholder="请输入姓名"
/>
{errors.name && touched.name && (
<div className="invalid-feedback">{errors.name}</div>
)}
{!errors.name && touched.name && (
<div className="valid-feedback">姓名格式正确</div>
)}
</div>
<div className="form-group">
<label htmlFor="email">邮箱 *</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
className={`form-control ${getFieldStatus('email')}`}
placeholder="请输入邮箱"
/>
{errors.email && touched.email && (
<div className="invalid-feedback">{errors.email}</div>
)}
{!errors.email && touched.email && (
<div className="valid-feedback">邮箱格式正确</div>
)}
</div>
<div className="form-group">
<label htmlFor="password">密码 *</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
className={`form-control ${getFieldStatus('password')}`}
placeholder="请输入密码"
/>
{errors.password && touched.password && (
<div className="invalid-feedback">{errors.password}</div>
)}
{!errors.password && touched.password && (
<div className="valid-feedback">密码格式正确</div>
)}
<small className="form-text text-muted">
密码需包含大小写字母和数字,至少6个字符
</small>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">确认密码 *</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
className={`form-control ${getFieldStatus('confirmPassword')}`}
placeholder="请再次输入密码"
/>
{errors.confirmPassword && touched.confirmPassword && (
<div className="invalid-feedback">{errors.confirmPassword}</div>
)}
{!errors.confirmPassword && touched.confirmPassword && (
<div className="valid-feedback">密码一致</div>
)}
</div>
<div className="form-group form-check">
<input
type="checkbox"
id="terms"
name="terms"
required
className="form-check-input"
/>
<label htmlFor="terms" className="form-check-label">
我已阅读并同意服务条款
</label>
<div className="invalid-feedback">
必须同意服务条款才能提交
</div>
</div>
<button type="submit" className="btn btn-primary">
注册
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setFormData({
name: '',
email: '',
password: '',
confirmPassword: ''
});
setErrors({});
setTouched({});
}}
>
重置
</button>
<div className="form-summary mt-3">
<p>
验证状态: {Object.keys(errors).length === 0 ?
'✓ 所有字段验证通过' :
`✗ 有 ${Object.keys(errors).length} 个字段需要修正`}
</p>
</div>
</form>
);
}
动态表单允许用户动态添加、删除和编辑表单字段。
import React, { useState } from 'react';
function DynamicContactForm() {
const [contacts, setContacts] = useState([
{ id: 1, name: '', email: '', phone: '' }
]);
// 添加联系人
const addContact = () => {
const newId = contacts.length > 0 ?
Math.max(...contacts.map(c => c.id)) + 1 : 1;
setContacts([
...contacts,
{ id: newId, name: '', email: '', phone: '' }
]);
};
// 删除联系人
const removeContact = (id) => {
if (contacts.length <= 1) return; // 至少保留一个
setContacts(contacts.filter(contact => contact.id !== id));
};
// 更新联系人信息
const updateContact = (id, field, value) => {
setContacts(contacts.map(contact =>
contact.id === id ? { ...contact, [field]: value } : contact
));
};
// 处理表单提交
const handleSubmit = (e) => {
e.preventDefault();
// 验证所有联系人
const hasErrors = contacts.some(contact =>
!contact.name.trim() || !contact.email.trim()
);
if (hasErrors) {
alert('请填写所有必填字段');
return;
}
console.log('提交的联系人:', contacts);
alert(`成功提交 ${contacts.length} 个联系人`);
};
return (
<form onSubmit={handleSubmit} className="dynamic-contact-form">
<h3>联系人表单</h3>
<p>您可以添加多个联系人信息</p>
{contacts.map((contact, index) => (
<div key={contact.id} className="contact-card">
<div className="card-header">
<h5>联系人 #{index + 1}</h5>
{contacts.length > 1 && (
<button
type="button"
onClick={() => removeContact(contact.id)}
className="btn-remove"
>
删除
</button>
)}
</div>
<div className="card-body">
<div className="form-group">
<label>姓名 *</label>
<input
type="text"
value={contact.name}
onChange={(e) => updateContact(contact.id, 'name', e.target.value)}
placeholder="请输入姓名"
required
/>
{!contact.name.trim() && (
<small className="error-text">姓名不能为空</small>
)}
</div>
<div className="form-group">
<label>邮箱 *</label>
<input
type="email"
value={contact.email}
onChange={(e) => updateContact(contact.id, 'email', e.target.value)}
placeholder="请输入邮箱"
required
/>
{!contact.email.trim() && (
<small className="error-text">邮箱不能为空</small>
)}
</div>
<div className="form-group">
<label>电话</label>
<input
type="tel"
value={contact.phone}
onChange={(e) => updateContact(contact.id, 'phone', e.target.value)}
placeholder="请输入电话"
/>
</div>
</div>
</div>
))}
<div className="form-actions">
<button
type="button"
onClick={addContact}
className="btn-add"
>
+ 添加联系人
</button>
<button type="submit" className="btn-submit">
提交所有联系人
</button>
<button
type="button"
onClick={() => setContacts([{ id: 1, name: '', email: '', phone: '' }])}
className="btn-reset"
>
重置表单
</button>
</div>
<div className="form-info">
<p>当前联系人数量: {contacts.length}</p>
<p>* 为必填字段</p>
</div>
</form>
);
}
对于复杂的表单需求,可以使用第三方表单库来简化开发。
最流行的React表单库,简化表单处理流程。
import { Formik, Form, Field } from 'formik';
<Formik
initialValues={{ email: '' }}
onSubmit={(values) => {
console.log(values);
}}
>
<Form>
<Field type="email" name="email" />
<button type="submit">提交</button>
</Form>
</Formik>
高性能表单库,使用React Hooks,非受控组件。
import { useForm } from 'react-hook-form';
const { register, handleSubmit } = useForm();
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
<button type="submit">提交</button>
</form>
高性能表单库,订阅式API,支持复杂的表单逻辑。
import { Form, Field } from 'react-final-form';
<Form
onSubmit={onSubmit}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field name="email" component="input" />
<button type="submit">提交</button>
</form>
)}
/>
import React, { useState, useRef } from 'react';
function FileUploadForm() {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const fileInputRef = useRef(null);
// 处理文件选择
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile) {
// 验证文件类型
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(selectedFile.type)) {
alert('只支持JPG、PNG和GIF格式的图片');
return;
}
// 验证文件大小(最大5MB)
if (selectedFile.size > 5 * 1024 * 1024) {
alert('文件大小不能超过5MB');
return;
}
setFile(selectedFile);
// 创建预览
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
};
reader.readAsDataURL(selectedFile);
}
};
// 模拟文件上传
const handleUpload = async () => {
if (!file) {
alert('请先选择文件');
return;
}
// 创建FormData对象
const formData = new FormData();
formData.append('file', file);
formData.append('uploadedAt', new Date().toISOString());
try {
// 模拟上传进度
const interval = setInterval(() => {
setUploadProgress(prev => {
const newProgress = prev + 10;
if (newProgress >= 100) {
clearInterval(interval);
return 100;
}
return newProgress;
});
}, 200);
// 这里应该是实际的API调用
// const response = await fetch('/api/upload', {
// method: 'POST',
// body: formData
// });
setTimeout(() => {
clearInterval(interval);
setUploadProgress(100);
alert('文件上传成功!');
resetForm();
}, 2000);
} catch (error) {
console.error('上传失败:', error);
alert('文件上传失败');
}
};
// 重置表单
const resetForm = () => {
setFile(null);
setPreview(null);
setUploadProgress(0);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
// 拖拽上传处理
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
const droppedFile = e.dataTransfer.files[0];
// 模拟文件选择事件
const event = {
target: {
files: [droppedFile]
}
};
handleFileChange(event);
}
};
return (
<div className="file-upload-form">
<h3>文件上传</h3>
<div
className={`drop-zone ${preview ? 'has-file' : ''}`}
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
{preview ? (
<div className="file-preview">
<img src={preview} alt="预览" />
<p>已选择: {file.name}</p>
<p>大小: {(file.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
) : (
<div className="drop-zone-content">
<i className="fas fa-cloud-upload-alt fa-3x"></i>
<p>点击或拖拽文件到这里</p>
<p className="upload-hint">支持JPG、PNG、GIF,最大5MB</p>
</div>
)}
</div>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/jpeg,image/png,image/gif"
style={display: 'none'}
/>
{file && (
<div className="upload-progress">
<div className="progress-bar">
<div
className="progress-fill"
style={width: `${uploadProgress}%`}
></div>
</div>
<span>{uploadProgress}%</span>
</div>
)}
<div className="upload-actions">
<button
type="button"
onClick={handleUpload}
disabled={!file || uploadProgress > 0}
className="btn-upload"
>
上传文件
</button>
<button
type="button"
onClick={resetForm}
className="btn-reset"
>
重置
</button>
</div>
</div>
);
}
htmlFor和id关联标签和输入框
import React, { useState } from 'react';
function UserRegistrationForm() {
const [formData, setFormData] = useState({
personalInfo: {
firstName: '',
lastName: '',
birthDate: '',
gender: ''
},
contactInfo: {
email: '',
phone: '',
address: '',
city: '',
zipCode: ''
},
accountInfo: {
username: '',
password: '',
confirmPassword: '',
newsletter: true,
termsAccepted: false
}
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeSection, setActiveSection] = useState('personal');
// 表单验证规则
const validate = () => {
const newErrors = {};
// 验证个人信息
if (!formData.personalInfo.firstName.trim()) {
newErrors.firstName = '名字不能为空';
}
if (!formData.personalInfo.lastName.trim()) {
newErrors.lastName = '姓氏不能为空';
}
// 验证联系信息
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.contactInfo.email.trim()) {
newErrors.email = '邮箱不能为空';
} else if (!emailRegex.test(formData.contactInfo.email)) {
newErrors.email = '请输入有效的邮箱地址';
}
// 验证账户信息
if (!formData.accountInfo.username.trim()) {
newErrors.username = '用户名不能为空';
} else if (formData.accountInfo.username.length < 3) {
newErrors.username = '用户名至少3个字符';
}
if (!formData.accountInfo.password.trim()) {
newErrors.password = '密码不能为空';
} else if (formData.accountInfo.password.length < 6) {
newErrors.password = '密码至少6个字符';
}
if (formData.accountInfo.password !== formData.accountInfo.confirmPassword) {
newErrors.confirmPassword = '两次输入的密码不一致';
}
if (!formData.accountInfo.termsAccepted) {
newErrors.termsAccepted = '必须接受服务条款';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理输入变化
const handleChange = (section, field, value) => {
setFormData({
...formData,
[section]: {
...formData[section],
[field]: value
}
});
// 清除该字段的错误
if (errors[field]) {
setErrors({
...errors,
[field]: ''
});
}
};
// 处理表单提交
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) {
alert('请修正表单错误后再提交');
return;
}
setIsSubmitting(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 2000));
console.log('表单数据:', formData);
alert('注册成功!');
// 重置表单
setFormData({
personalInfo: {
firstName: '',
lastName: '',
birthDate: '',
gender: ''
},
contactInfo: {
email: '',
phone: '',
address: '',
city: '',
zipCode: ''
},
accountInfo: {
username: '',
password: '',
confirmPassword: '',
newsletter: true,
termsAccepted: false
}
});
setErrors({});
setActiveSection('personal');
} catch (error) {
console.error('注册失败:', error);
alert('注册失败,请稍后重试');
} finally {
setIsSubmitting(false);
}
};
// 表单部分组件
const PersonalInfoSection = () => (
<div className="form-section">
<h4>个人信息</h4>
<div className="form-row">
<div className="form-group">
<label>名字 *</label>
<input
type="text"
value={formData.personalInfo.firstName}
onChange={(e) => handleChange('personalInfo', 'firstName', e.target.value)}
className={errors.firstName ? 'error' : ''}
/>
{errors.firstName && <span className="error-message">{errors.firstName}</span>}
</div>
<div className="form-group">
<label>姓氏 *</label>
<input
type="text"
value={formData.personalInfo.lastName}
onChange={(e) => handleChange('personalInfo', 'lastName', e.target.value)}
className={errors.lastName ? 'error' : ''}
/>
{errors.lastName && <span className="error-message">{errors.lastName}</span>}
</div>
</div>
<div className="form-row">
<div className="form-group">
<label>出生日期</label>
<input
type="date"
value={formData.personalInfo.birthDate}
onChange={(e) => handleChange('personalInfo', 'birthDate', e.target.value)}
/>
</div>
<div className="form-group">
<label>性别</label>
<select
value={formData.personalInfo.gender}
onChange={(e) => handleChange('personalInfo', 'gender', e.target.value)}
>
<option value="">请选择</option>
<option value="male">男</option>
<option value="female">女</option>
<option value="other">其他</option>
</select>
</div>
</div>
</div>
);
const ContactInfoSection = () => (
<div className="form-section">
<h4>联系信息</h4>
<div className="form-group">
<label>邮箱 *</label>
<input
type="email"
value={formData.contactInfo.email}
onChange={(e) => handleChange('contactInfo', 'email', e.target.value)}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label>电话</label>
<input
type="tel"
value={formData.contactInfo.phone}
onChange={(e) => handleChange('contactInfo', 'phone', e.target.value)}
/>
</div>
<div className="form-group">
<label>地址</label>
<textarea
value={formData.contactInfo.address}
onChange={(e) => handleChange('contactInfo', 'address', e.target.value)}
rows="3"
/>
</div>
<div className="form-row">
<div className="form-group">
<label>城市</label>
<input
type="text"
value={formData.contactInfo.city}
onChange={(e) => handleChange('contactInfo', 'city', e.target.value)}
/>
</div>
<div className="form-group">
<label>邮政编码</label>
<input
type="text"
value={formData.contactInfo.zipCode}
onChange={(e) => handleChange('contactInfo', 'zipCode', e.target.value)}
/>
</div>
</div>
</div>
);
const AccountInfoSection = () => (
<div className="form-section">
<h4>账户信息</h4>
<div className="form-group">
<label>用户名 *</label>
<input
type="text"
value={formData.accountInfo.username}
onChange={(e) => handleChange('accountInfo', 'username', e.target.value)}
className={errors.username ? 'error' : ''}
/>
{errors.username && <span className="error-message">{errors.username}</span>}
</div>
<div className="form-row">
<div className="form-group">
<label>密码 *</label>
<input
type="password"
value={formData.accountInfo.password}
onChange={(e) => handleChange('accountInfo', 'password', e.target.value)}
className={errors.password ? 'error' : ''}
/>
{errors.password && <span className="error-message">{errors.password}</span>}
</div>
<div className="form-group">
<label>确认密码 *</label>
<input
type="password"
value={formData.accountInfo.confirmPassword}
onChange={(e) => handleChange('accountInfo', 'confirmPassword', e.target.value)}
className={errors.confirmPassword ? 'error' : ''}
/>
{errors.confirmPassword && <span className="error-message">{errors.confirmPassword}</span>}
</div>
</div>
<div className="form-check-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={formData.accountInfo.newsletter}
onChange={(e) => handleChange('accountInfo', 'newsletter', e.target.checked)}
/>
订阅新闻和更新
</label>
<label className={`checkbox-label ${errors.termsAccepted ? 'error' : ''}`}>
<input
type="checkbox"
checked={formData.accountInfo.termsAccepted}
onChange={(e) => handleChange('accountInfo', 'termsAccepted', e.target.checked)}
/>
我同意服务条款和隐私政策 *
</label>
{errors.termsAccepted && (
<span className="error-message">{errors.termsAccepted}</span>
)}
</div>
</div>
);
return (
<form onSubmit={handleSubmit} className="user-registration-form">
<h2>用户注册</h2>
<div className="form-navigation">
<button
type="button"
className={`nav-btn ${activeSection === 'personal' ? 'active' : ''}`}
onClick={() => setActiveSection('personal')}
>
个人信息
</button>
<button
type="button"
className={`nav-btn ${activeSection === 'contact' ? 'active' : ''}`}
onClick={() => setActiveSection('contact')}
>
联系信息
</button>
<button
type="button"
className={`nav-btn ${activeSection === 'account' ? 'active' : ''}`}
onClick={() => setActiveSection('account')}
>
账户信息
</button>
</div>
{activeSection === 'personal' && <PersonalInfoSection />}
{activeSection === 'contact' && <ContactInfoSection />}
{activeSection === 'account' && <AccountInfoSection />}
<div className="form-actions">
<button
type="button"
onClick={() => {
if (activeSection === 'contact') setActiveSection('personal');
if (activeSection === 'account') setActiveSection('contact');
}}
disabled={activeSection === 'personal'}
className="btn-prev"
>
上一步
</button>
<button
type="button"
onClick={() => {
if (activeSection === 'personal') setActiveSection('contact');
if (activeSection === 'contact') setActiveSection('account');
}}
disabled={activeSection === 'account'}
className="btn-next"
>
下一步
</button>
<button
type="submit"
disabled={isSubmitting}
className="btn-submit"
>
{isSubmitting ? (
<>
<i className="fas fa-spinner fa-spin"></i> 提交中...
</>
) : (
'完成注册'
)}
</button>
</div>
<div className="form-footer">
<p>* 为必填字段</p>
<p>总共有 {Object.keys(errors).length} 个错误需要修正</p>
</div>
</form>
);
}