Skip to content

多步骤表单

概述

多步骤表单(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>
  );
}

总结

多步骤表单关键要点:

  1. 状态管理:使用useReducer或Context管理复杂状态
  2. 进度指示:清晰展示当前步骤和完成进度
  3. 验证策略:分步验证,及时反馈
  4. 用户体验:保存进度、支持导航、添加动画
  5. 错误处理:友好的错误提示和恢复机制
  6. 可访问性:键盘导航、屏幕阅读器支持

合理设计的多步骤表单能够显著提升复杂表单的用户体验。