Appearance
多步骤表单
概述
多步骤表单(Multi-Step Form)将复杂的表单分解为多个简单的步骤,能够显著提升用户体验,降低表单放弃率。本文将深入探讨多步骤表单的实现方式、状态管理、用户体验优化和最佳实践。
基础实现
简单的多步骤表单
jsx
import { useState } from 'react';
function BasicMultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
// Step 1
firstName: '',
lastName: '',
email: '',
// Step 2
address: '',
city: '',
zipCode: '',
// Step 3
cardNumber: '',
cvv: '',
expiry: '',
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const nextStep = () => setStep(step + 1);
const prevStep = () => setStep(step - 1);
const handleSubmit = async () => {
try {
await submitForm(formData);
alert('提交成功!');
} catch (error) {
alert('提交失败: ' + error.message);
}
};
// 步骤1: 个人信息
const Step1 = () => (
<div>
<h2>个人信息</h2>
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="名"
/>
<input
type="text"
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="姓"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="邮箱"
/>
<button onClick={nextStep}>下一步</button>
</div>
);
// 步骤2: 地址信息
const Step2 = () => (
<div>
<h2>地址信息</h2>
<input
type="text"
name="address"
value={formData.address}
onChange={handleChange}
placeholder="地址"
/>
<input
type="text"
name="city"
value={formData.city}
onChange={handleChange}
placeholder="城市"
/>
<input
type="text"
name="zipCode"
value={formData.zipCode}
onChange={handleChange}
placeholder="邮编"
/>
<button onClick={prevStep}>上一步</button>
<button onClick={nextStep}>下一步</button>
</div>
);
// 步骤3: 支付信息
const Step3 = () => (
<div>
<h2>支付信息</h2>
<input
type="text"
name="cardNumber"
value={formData.cardNumber}
onChange={handleChange}
placeholder="卡号"
/>
<input
type="text"
name="cvv"
value={formData.cvv}
onChange={handleChange}
placeholder="CVV"
/>
<input
type="text"
name="expiry"
value={formData.expiry}
onChange={handleChange}
placeholder="有效期"
/>
<button onClick={prevStep}>上一步</button>
<button onClick={handleSubmit}>提交</button>
</div>
);
return (
<div>
{step === 1 && <Step1 />}
{step === 2 && <Step2 />}
{step === 3 && <Step3 />}
</div>
);
}进度指示器
jsx
function StepIndicator({ currentStep, totalSteps, steps }) {
return (
<div className="step-indicator">
{steps.map((stepName, index) => {
const stepNumber = index + 1;
const isActive = stepNumber === currentStep;
const isCompleted = stepNumber < currentStep;
return (
<div
key={stepNumber}
className={`step ${isActive ? 'active' : ''} ${isCompleted ? 'completed' : ''}`}
>
<div className="step-number">
{isCompleted ? '✓' : stepNumber}
</div>
<div className="step-label">{stepName}</div>
{stepNumber < totalSteps && <div className="step-line" />}
</div>
);
})}
</div>
);
}
// 使用进度指示器
function FormWithIndicator() {
const [step, setStep] = useState(1);
const steps = ['个人信息', '地址信息', '支付信息'];
return (
<div>
<StepIndicator
currentStep={step}
totalSteps={steps.length}
steps={steps}
/>
{/* 表单内容 */}
</div>
);
}
// CSS样式
const styles = `
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 2rem;
position: relative;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-bottom: 0.5rem;
z-index: 1;
}
.step.active .step-number {
background: #2196F3;
color: white;
}
.step.completed .step-number {
background: #4CAF50;
color: white;
}
.step-label {
font-size: 0.875rem;
color: #666;
}
.step.active .step-label {
color: #2196F3;
font-weight: 600;
}
.step-line {
position: absolute;
top: 20px;
left: 50%;
right: -50%;
height: 2px;
background: #e0e0e0;
z-index: 0;
}
.step.completed .step-line {
background: #4CAF50;
}
`;高级状态管理
使用useReducer管理多步骤表单
jsx
import { useReducer } from 'react';
// 定义action类型
const ACTIONS = {
NEXT_STEP: 'NEXT_STEP',
PREV_STEP: 'PREV_STEP',
GO_TO_STEP: 'GO_TO_STEP',
UPDATE_FIELD: 'UPDATE_FIELD',
UPDATE_STEP_DATA: 'UPDATE_STEP_DATA',
SET_ERRORS: 'SET_ERRORS',
RESET: 'RESET',
};
// Reducer函数
function multiStepReducer(state, action) {
switch (action.type) {
case ACTIONS.NEXT_STEP:
return {
...state,
currentStep: Math.min(state.currentStep + 1, state.totalSteps),
};
case ACTIONS.PREV_STEP:
return {
...state,
currentStep: Math.max(state.currentStep - 1, 1),
};
case ACTIONS.GO_TO_STEP:
return {
...state,
currentStep: action.step,
};
case ACTIONS.UPDATE_FIELD:
return {
...state,
data: {
...state.data,
[action.field]: action.value,
},
};
case ACTIONS.UPDATE_STEP_DATA:
return {
...state,
data: {
...state.data,
...action.data,
},
};
case ACTIONS.SET_ERRORS:
return {
...state,
errors: {
...state.errors,
[state.currentStep]: action.errors,
},
};
case ACTIONS.RESET:
return action.initialState;
default:
return state;
}
}
function MultiStepFormWithReducer() {
const initialState = {
currentStep: 1,
totalSteps: 3,
data: {
personal: { firstName: '', lastName: '', email: '' },
address: { street: '', city: '', zipCode: '' },
payment: { cardNumber: '', cvv: '', expiry: '' },
},
errors: {},
};
const [state, dispatch] = useReducer(multiStepReducer, initialState);
const nextStep = () => {
dispatch({ type: ACTIONS.NEXT_STEP });
};
const prevStep = () => {
dispatch({ type: ACTIONS.PREV_STEP });
};
const updateStepData = (data) => {
dispatch({ type: ACTIONS.UPDATE_STEP_DATA, data });
};
const setErrors = (errors) => {
dispatch({ type: ACTIONS.SET_ERRORS, errors });
};
return (
<div>
<StepIndicator currentStep={state.currentStep} totalSteps={state.totalSteps} />
{state.currentStep === 1 && (
<PersonalInfoStep
data={state.data.personal}
onNext={(data) => {
updateStepData({ personal: data });
nextStep();
}}
/>
)}
{state.currentStep === 2 && (
<AddressStep
data={state.data.address}
onNext={(data) => {
updateStepData({ address: data });
nextStep();
}}
onPrev={prevStep}
/>
)}
{state.currentStep === 3 && (
<PaymentStep
data={state.data.payment}
onSubmit={(data) => {
updateStepData({ payment: data });
// 提交表单
}}
onPrev={prevStep}
/>
)}
</div>
);
}Context管理多步骤表单
jsx
import { createContext, useContext, useReducer } from 'react';
// 创建Context
const MultiStepContext = createContext();
// Provider组件
function MultiStepProvider({ children, initialData, totalSteps }) {
const [state, dispatch] = useReducer(multiStepReducer, {
currentStep: 1,
totalSteps,
data: initialData,
errors: {},
completed: {},
});
const value = {
...state,
nextStep: () => dispatch({ type: 'NEXT_STEP' }),
prevStep: () => dispatch({ type: 'PREV_STEP' }),
goToStep: (step) => dispatch({ type: 'GO_TO_STEP', step }),
updateData: (data) => dispatch({ type: 'UPDATE_DATA', data }),
setErrors: (errors) => dispatch({ type: 'SET_ERRORS', errors }),
markStepCompleted: (step) => dispatch({ type: 'MARK_COMPLETED', step }),
};
return (
<MultiStepContext.Provider value={value}>
{children}
</MultiStepContext.Provider>
);
}
// 自定义Hook
function useMultiStep() {
const context = useContext(MultiStepContext);
if (!context) {
throw new Error('useMultiStep must be used within MultiStepProvider');
}
return context;
}
// 使用示例
function RegistrationWizard() {
const initialData = {
personal: {},
account: {},
preferences: {},
};
return (
<MultiStepProvider initialData={initialData} totalSteps={3}>
<WizardContent />
</MultiStepProvider>
);
}
function WizardContent() {
const {
currentStep,
totalSteps,
data,
nextStep,
prevStep,
updateData,
} = useMultiStep();
return (
<div>
<StepIndicator currentStep={currentStep} totalSteps={totalSteps} />
{currentStep === 1 && <PersonalInfoStep />}
{currentStep === 2 && <AccountSetupStep />}
{currentStep === 3 && <PreferencesStep />}
</div>
);
}
// 步骤组件
function PersonalInfoStep() {
const { data, updateData, nextStep } = useMultiStep();
const [localData, setLocalData] = useState(data.personal || {});
const handleNext = () => {
updateData({ personal: localData });
nextStep();
};
return (
<div>
<h2>个人信息</h2>
{/* 表单字段 */}
<button onClick={handleNext}>下一步</button>
</div>
);
}表单验证
分步验证
jsx
function ValidatedMultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
step1: { firstName: '', lastName: '', email: '' },
step2: { address: '', city: '', zipCode: '' },
step3: { cardNumber: '', cvv: '', expiry: '' },
});
const [errors, setErrors] = useState({});
// 验证规则
const validators = {
1: (data) => {
const errors = {};
if (!data.firstName) errors.firstName = '名字不能为空';
if (!data.lastName) errors.lastName = '姓氏不能为空';
if (!data.email.includes('@')) errors.email = '邮箱格式不正确';
return errors;
},
2: (data) => {
const errors = {};
if (!data.address) errors.address = '地址不能为空';
if (!data.city) errors.city = '城市不能为空';
if (!/^\d{6}$/.test(data.zipCode)) errors.zipCode = '邮编格式不正确';
return errors;
},
3: (data) => {
const errors = {};
if (!/^\d{16}$/.test(data.cardNumber)) errors.cardNumber = '卡号格式不正确';
if (!/^\d{3}$/.test(data.cvv)) errors.cvv = 'CVV格式不正确';
if (!/^\d{2}\/\d{2}$/.test(data.expiry)) errors.expiry = '有效期格式不正确';
return errors;
},
};
const validateStep = (stepNumber) => {
const stepData = formData[`step${stepNumber}`];
const stepErrors = validators[stepNumber](stepData);
setErrors({ ...errors, [`step${stepNumber}`]: stepErrors });
return Object.keys(stepErrors).length === 0;
};
const handleNext = () => {
if (validateStep(step)) {
setStep(step + 1);
}
};
const handlePrev = () => {
setStep(step - 1);
};
const handleSubmit = async () => {
if (validateStep(step)) {
try {
await submitForm(formData);
alert('提交成功!');
} catch (error) {
alert('提交失败: ' + error.message);
}
}
};
const updateStepData = (stepNum, field, value) => {
setFormData({
...formData,
[`step${stepNum}`]: {
...formData[`step${stepNum}`],
[field]: value,
},
});
};
return (
<div>
{step === 1 && (
<Step1
data={formData.step1}
errors={errors.step1 || {}}
onChange={(field, value) => updateStepData(1, field, value)}
onNext={handleNext}
/>
)}
{step === 2 && (
<Step2
data={formData.step2}
errors={errors.step2 || {}}
onChange={(field, value) => updateStepData(2, field, value)}
onNext={handleNext}
onPrev={handlePrev}
/>
)}
{step === 3 && (
<Step3
data={formData.step3}
errors={errors.step3 || {}}
onChange={(field, value) => updateStepData(3, field, value)}
onSubmit={handleSubmit}
onPrev={handlePrev}
/>
)}
</div>
);
}异步验证
jsx
function AsyncValidatedStep() {
const { data, updateData, nextStep } = useMultiStep();
const [email, setEmail] = useState(data.email || '');
const [validating, setValidating] = useState(false);
const [emailError, setEmailError] = useState('');
useEffect(() => {
const validateEmail = async () => {
if (!email) {
setEmailError('');
return;
}
setValidating(true);
setEmailError('');
try {
const response = await fetch(`/api/validate-email?email=${email}`);
const result = await response.json();
if (!result.available) {
setEmailError('该邮箱已被使用');
}
} catch (error) {
setEmailError('验证失败,请重试');
} finally {
setValidating(false);
}
};
const debounce = setTimeout(validateEmail, 500);
return () => clearTimeout(debounce);
}, [email]);
const handleNext = () => {
if (!emailError && !validating) {
updateData({ email });
nextStep();
}
};
return (
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
/>
{validating && <span>验证中...</span>}
{emailError && <span className="error">{emailError}</span>}
<button onClick={handleNext} disabled={!!emailError || validating}>
下一步
</button>
</div>
);
}用户体验优化
保存进度
jsx
function PersistentMultiStepForm() {
const STORAGE_KEY = 'multi-step-form-progress';
const [state, dispatch] = useReducer(multiStepReducer, null, () => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
return JSON.parse(saved);
}
return {
currentStep: 1,
totalSteps: 4,
data: {},
completed: {},
};
});
// 自动保存
useEffect(() => {
const saveTimer = setTimeout(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}, 1000);
return () => clearTimeout(saveTimer);
}, [state]);
// 清除保存的进度
const clearProgress = () => {
localStorage.removeItem(STORAGE_KEY);
dispatch({ type: 'RESET', initialState: getInitialState() });
};
// 恢复提示
const [showRestorePrompt, setShowRestorePrompt] = useState(() => {
const saved = localStorage.getItem(STORAGE_KEY);
return saved && JSON.parse(saved).currentStep > 1;
});
if (showRestorePrompt) {
return (
<div className="restore-prompt">
<p>检测到未完成的表单,是否继续?</p>
<button onClick={() => setShowRestorePrompt(false)}>继续填写</button>
<button onClick={() => {
clearProgress();
setShowRestorePrompt(false);
}}>
重新开始
</button>
</div>
);
}
return (
<div>
<div className="progress-indicator">
已保存进度 - 第 {state.currentStep} / {state.totalSteps} 步
</div>
{/* 表单内容 */}
</div>
);
}导航控制
jsx
function NavigableMultiStepForm() {
const { currentStep, totalSteps, completed, goToStep } = useMultiStep();
const canNavigateTo = (step) => {
// 只能导航到已完成的步骤或下一个步骤
if (step === currentStep) return false;
if (step < currentStep) return true;
if (step === currentStep + 1) return completed[currentStep];
return false;
};
const StepNavigation = () => (
<div className="step-navigation">
{Array.from({ length: totalSteps }, (_, i) => i + 1).map(step => (
<button
key={step}
onClick={() => goToStep(step)}
disabled={!canNavigateTo(step)}
className={`
step-nav-btn
${step === currentStep ? 'active' : ''}
${completed[step] ? 'completed' : ''}
`}
>
{step}. {stepNames[step - 1]}
</button>
))}
</div>
);
return (
<div>
<StepNavigation />
{/* 步骤内容 */}
</div>
);
}动画过渡
jsx
import { motion, AnimatePresence } from 'framer-motion';
function AnimatedMultiStepForm() {
const [step, setStep] = useState(1);
const [direction, setDirection] = useState(1); // 1 for next, -1 for prev
const nextStep = () => {
setDirection(1);
setStep(step + 1);
};
const prevStep = () => {
setDirection(-1);
setStep(step - 1);
};
const slideVariants = {
enter: (direction) => ({
x: direction > 0 ? 1000 : -1000,
opacity: 0,
}),
center: {
x: 0,
opacity: 1,
},
exit: (direction) => ({
x: direction > 0 ? -1000 : 1000,
opacity: 0,
}),
};
return (
<div className="form-container">
<AnimatePresence mode="wait" custom={direction}>
<motion.div
key={step}
custom={direction}
variants={slideVariants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.3 }}
>
{step === 1 && <Step1 onNext={nextStep} />}
{step === 2 && <Step2 onNext={nextStep} onPrev={prevStep} />}
{step === 3 && <Step3 onPrev={prevStep} />}
</motion.div>
</AnimatePresence>
</div>
);
}
// 淡入淡出效果
function FadeTransitionForm() {
const [step, setStep] = useState(1);
return (
<AnimatePresence mode="wait">
<motion.div
key={step}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{step === 1 && <Step1 />}
{step === 2 && <Step2 />}
{step === 3 && <Step3 />}
</motion.div>
</AnimatePresence>
);
}实战案例
电商订单流程
jsx
function CheckoutWizard() {
const [state, dispatch] = useReducer(checkoutReducer, {
step: 1,
cart: [],
shipping: {},
payment: {},
review: {},
});
// Step 1: 购物车
const CartStep = () => {
const { cart } = state;
return (
<div>
<h2>购物车</h2>
{cart.map(item => (
<CartItem key={item.id} item={item} />
))}
<div className="cart-summary">
<p>小计: ${calculateSubtotal(cart)}</p>
<button onClick={() => dispatch({ type: 'NEXT_STEP' })}>
结算
</button>
</div>
</div>
);
};
// Step 2: 配送信息
const ShippingStep = () => {
const [shipping, setShipping] = useState(state.shipping);
const [errors, setErrors] = useState({});
const validateAndNext = () => {
const validationErrors = validateShipping(shipping);
if (Object.keys(validationErrors).length === 0) {
dispatch({ type: 'UPDATE_SHIPPING', shipping });
dispatch({ type: 'NEXT_STEP' });
} else {
setErrors(validationErrors);
}
};
return (
<div>
<h2>配送信息</h2>
<input
type="text"
value={shipping.address}
onChange={(e) => setShipping({ ...shipping, address: e.target.value })}
placeholder="配送地址"
/>
{errors.address && <span>{errors.address}</span>}
<select
value={shipping.method}
onChange={(e) => setShipping({ ...shipping, method: e.target.value })}
>
<option value="standard">标准配送 (5-7天)</option>
<option value="express">快速配送 (2-3天)</option>
<option value="overnight">隔日送达</option>
</select>
<button onClick={() => dispatch({ type: 'PREV_STEP' })}>
返回购物车
</button>
<button onClick={validateAndNext}>
继续支付
</button>
</div>
);
};
// Step 3: 支付信息
const PaymentStep = () => {
const [payment, setPayment] = useState(state.payment);
const [processing, setProcessing] = useState(false);
const handlePayment = async () => {
setProcessing(true);
try {
const result = await processPayment(payment);
dispatch({ type: 'UPDATE_PAYMENT', payment });
dispatch({ type: 'NEXT_STEP' });
} catch (error) {
alert('支付失败: ' + error.message);
} finally {
setProcessing(false);
}
};
return (
<div>
<h2>支付信息</h2>
<input
type="text"
value={payment.cardNumber}
onChange={(e) => setPayment({ ...payment, cardNumber: e.target.value })}
placeholder="卡号"
/>
<div className="card-details">
<input
type="text"
value={payment.expiry}
onChange={(e) => setPayment({ ...payment, expiry: e.target.value })}
placeholder="MM/YY"
/>
<input
type="text"
value={payment.cvv}
onChange={(e) => setPayment({ ...payment, cvv: e.target.value })}
placeholder="CVV"
/>
</div>
<button onClick={() => dispatch({ type: 'PREV_STEP' })}>
返回
</button>
<button onClick={handlePayment} disabled={processing}>
{processing ? '处理中...' : '支付'}
</button>
</div>
);
};
// Step 4: 确认
const ReviewStep = () => {
const { cart, shipping, payment } = state;
return (
<div>
<h2>订单确认</h2>
<section>
<h3>商品</h3>
{cart.map(item => (
<div key={item.id}>{item.name} x {item.quantity}</div>
))}
</section>
<section>
<h3>配送信息</h3>
<p>{shipping.address}</p>
<p>配送方式: {shipping.method}</p>
</section>
<section>
<h3>支付信息</h3>
<p>卡号: **** **** **** {payment.cardNumber.slice(-4)}</p>
</section>
<div className="order-summary">
<p>总计: ${calculateTotal(cart, shipping)}</p>
</div>
<button onClick={() => dispatch({ type: 'GO_TO_STEP', step: 1 })}>
修改订单
</button>
<button onClick={submitOrder}>
确认订单
</button>
</div>
);
};
return (
<div className="checkout-wizard">
<StepIndicator
currentStep={state.step}
steps={['购物车', '配送', '支付', '确认']}
/>
{state.step === 1 && <CartStep />}
{state.step === 2 && <ShippingStep />}
{state.step === 3 && <PaymentStep />}
{state.step === 4 && <ReviewStep />}
</div>
);
}注册向导
jsx
function RegistrationWizard() {
const steps = [
{ id: 'account', title: '账户信息', component: AccountStep },
{ id: 'profile', title: '个人资料', component: ProfileStep },
{ id: 'preferences', title: '偏好设置', component: PreferencesStep },
{ id: 'verification', title: '验证', component: VerificationStep },
];
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [formData, setFormData] = useState({});
const [completedSteps, setCompletedSteps] = useState(new Set());
const currentStep = steps[currentStepIndex];
const CurrentStepComponent = currentStep.component;
const handleStepComplete = (stepData) => {
setFormData({ ...formData, [currentStep.id]: stepData });
setCompletedSteps(new Set([...completedSteps, currentStepIndex]));
if (currentStepIndex < steps.length - 1) {
setCurrentStepIndex(currentStepIndex + 1);
} else {
submitRegistration(formData);
}
};
const canGoToStep = (index) => {
if (index === currentStepIndex) return false;
if (index < currentStepIndex) return true;
if (index === currentStepIndex + 1) return completedSteps.has(currentStepIndex);
return false;
};
return (
<div className="registration-wizard">
<ProgressBar
steps={steps}
currentStep={currentStepIndex}
completedSteps={completedSteps}
onStepClick={(index) => {
if (canGoToStep(index)) {
setCurrentStepIndex(index);
}
}}
/>
<CurrentStepComponent
data={formData[currentStep.id] || {}}
onComplete={handleStepComplete}
onBack={() => setCurrentStepIndex(currentStepIndex - 1)}
isFirstStep={currentStepIndex === 0}
isLastStep={currentStepIndex === steps.length - 1}
/>
</div>
);
}最佳实践
1. 合理划分步骤
jsx
// 好的步骤划分
const goodSteps = [
{ id: 'personal', fields: ['name', 'email', 'phone'] }, // 3个字段
{ id: 'address', fields: ['street', 'city', 'zipCode'] }, // 3个字段
{ id: 'payment', fields: ['cardNumber', 'expiry', 'cvv'] }, // 3个字段
];
// 不好的步骤划分
const badSteps = [
{ id: 'step1', fields: ['name'] }, // 太少
{ id: 'step2', fields: ['email', 'phone', 'address', 'city', 'zipCode', 'country'] }, // 太多
];2. 提供清晰的反馈
jsx
function FeedbackExample() {
const [status, setStatus] = useState('idle');
return (
<div>
{status === 'validating' && (
<div className="feedback validating">
<Spinner /> 正在验证...
</div>
)}
{status === 'success' && (
<div className="feedback success">
✓ 验证成功
</div>
)}
{status === 'error' && (
<div className="feedback error">
✗ 验证失败,请重试
</div>
)}
</div>
);
}3. 支持键盘导航
jsx
function KeyboardNavigableForm() {
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleNext();
}
if (e.key === 'Escape') {
handleCancel();
}
};
return (
<form onKeyDown={handleKeyDown}>
{/* 表单内容 */}
</form>
);
}总结
多步骤表单关键要点:
- 状态管理:使用useReducer或Context管理复杂状态
- 进度指示:清晰展示当前步骤和完成进度
- 验证策略:分步验证,及时反馈
- 用户体验:保存进度、支持导航、添加动画
- 错误处理:友好的错误提示和恢复机制
- 可访问性:键盘导航、屏幕阅读器支持
合理设计的多步骤表单能够显著提升复杂表单的用户体验。