如何在一个 Vue 项目中,实现一个复杂的多步骤表单,支持步骤跳转、数据暂存和动态校验?

各位观众老爷,大家好!我是你们的老朋友,今天咱们来聊聊 Vue 项目里让人头疼,又不得不面对的“多步骤表单”这玩意儿。别害怕,我会用最接地气的方式,把这个看似复杂的任务,拆解成一个个小 case,保证你们听完之后,也能优雅地驾驭它。

开场白:多步骤表单是个啥?为什么要用它?

想象一下,你要填一份特别长的申请表,里面包含个人信息、工作经历、家庭情况、兴趣爱好等等。如果把所有内容都堆在一个页面上,用户估计直接就崩溃了。这时候,“多步骤表单”就派上用场了。

简单来说,它就是把一个复杂的表单拆分成多个步骤,用户一步一步地填写,可以前进、后退,而且数据还能保存,简直不要太人性化。

第一步:搭建基本框架,先别慌!

首先,我们需要一个 Vue 项目。如果还没建好,赶紧用 Vue CLI 初始化一个。然后,我们来创建一个 MultiStepForm.vue 组件,作为我们多步骤表单的容器。

<template>
  <div class="multi-step-form">
    <!-- 步骤指示器 -->
    <div class="steps">
      <div
        v-for="(step, index) in steps"
        :key="index"
        class="step"
        :class="{ active: currentStep === index }"
        @click="goToStep(index)"
      >
        {{ step.title }}
      </div>
    </div>

    <!-- 表单内容区域 -->
    <div class="form-content">
      <component :is="currentStepComponent" :formData="formData" @update="updateFormData" @next="nextStep" @prev="prevStep"></component>
    </div>

    <!-- 按钮区域 -->
    <div class="buttons">
      <button v-if="currentStep > 0" @click="prevStep">上一步</button>
      <button v-if="currentStep < steps.length - 1" @click="nextStep">下一步</button>
      <button v-else @click="submitForm">提交</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'MultiStepForm',
  data() {
    return {
      currentStep: 0,
      formData: {}, // 用于存储所有步骤的数据
      steps: [
        { title: '个人信息', component: 'PersonalInformation' },
        { title: '工作经历', component: 'WorkExperience' },
        { title: '家庭情况', component: 'FamilyInformation' },
      ],
    };
  },
  computed: {
    currentStepComponent() {
      return this.steps[this.currentStep].component;
    },
  },
  components: {
    PersonalInformation: {
      template: '<div>个人信息表单</div>',
    },
    WorkExperience: {
      template: '<div>工作经历表单</div>',
    },
    FamilyInformation: {
      template: '<div>家庭情况表单</div>',
    },
  },
  methods: {
    goToStep(stepIndex) {
      this.currentStep = stepIndex;
    },
    nextStep() {
      this.currentStep++;
    },
    prevStep() {
      this.currentStep--;
    },
    updateFormData(data) {
      // 合并数据,避免覆盖
      this.formData = { ...this.formData, ...data };
    },
    submitForm() {
      // 提交表单,这里可以发送请求到后端
      console.log('提交表单', this.formData);
      alert('提交成功,数据已打印到控制台');
    },
  },
};
</script>

<style scoped>
.multi-step-form {
  width: 500px;
  margin: 0 auto;
  border: 1px solid #ccc;
  padding: 20px;
}

.steps {
  display: flex;
  justify-content: space-around;
  margin-bottom: 20px;
}

.step {
  padding: 10px 20px;
  border: 1px solid #ccc;
  cursor: pointer;
}

.step.active {
  background-color: #eee;
}

.buttons {
  display: flex;
  justify-content: space-between;
}
</style>

这段代码做了什么呢?

  • steps: 定义了表单的步骤,每个步骤包含一个标题 (title) 和一个组件名 (component)。
  • currentStep: 当前显示的步骤索引。
  • formData: 一个对象,用于存储所有步骤的数据。
  • currentStepComponent: 计算属性,根据 currentStep 动态返回当前步骤对应的组件。
  • goToStep: 跳转到指定步骤。
  • nextStep: 下一步。
  • prevStep: 上一步。
  • updateFormData: 更新 formData 中的数据。
  • submitForm: 提交表单。

现在,你已经有了一个多步骤表单的基本框架,虽然每个步骤的内容还是空的,但至少能跑起来了。

第二步:打造个性化表单步骤,各有千秋!

接下来,我们要为每个步骤创建对应的组件。为了让大家看得更清楚,我把每个组件都单独放在一个文件里。

  • PersonalInformation.vue
<template>
  <div>
    <h2>个人信息</h2>
    <label>姓名:</label>
    <input type="text" v-model="name" @input="updateData" /><br />
    <label>年龄:</label>
    <input type="number" v-model="age" @input="updateData" /><br />
    <label>邮箱:</label>
    <input type="email" v-model="email" @input="updateData" /><br />
  </div>
</template>

<script>
export default {
  name: 'PersonalInformation',
  props: ['formData'],
  data() {
    return {
      name: this.formData.name || '',
      age: this.formData.age || '',
      email: this.formData.email || '',
    };
  },
  watch: {
    formData: {
      handler(newVal) {
        this.name = newVal.name || '';
        this.age = newVal.age || '';
        this.email = newVal.email || '';
      },
      deep: true,
    },
  },
  methods: {
    updateData() {
      this.$emit('update', {
        name: this.name,
        age: this.age,
        email: this.email,
      });
    },
  },
};
</script>
  • WorkExperience.vue
<template>
  <div>
    <h2>工作经历</h2>
    <label>公司名称:</label>
    <input type="text" v-model="company" @input="updateData" /><br />
    <label>职位:</label>
    <input type="text" v-model="position" @input="updateData" /><br />
  </div>
</template>

<script>
export default {
  name: 'WorkExperience',
  props: ['formData'],
  data() {
    return {
      company: this.formData.company || '',
      position: this.formData.position || '',
    };
  },
  watch: {
    formData: {
      handler(newVal) {
        this.company = newVal.company || '';
        this.position = newVal.position || '';
      },
      deep: true,
    },
  },
  methods: {
    updateData() {
      this.$emit('update', {
        company: this.company,
        position: this.position,
      });
    },
  },
};
</script>
  • FamilyInformation.vue
<template>
  <div>
    <h2>家庭情况</h2>
    <label>家庭成员:</label>
    <input type="text" v-model="familyMembers" @input="updateData" /><br />
  </div>
</template>

<script>
export default {
  name: 'FamilyInformation',
  props: ['formData'],
  data() {
    return {
      familyMembers: this.formData.familyMembers || '',
    };
  },
  watch: {
    formData: {
      handler(newVal) {
        this.familyMembers = newVal.familyMembers || '';
      },
      deep: true,
    },
  },
  methods: {
    updateData() {
      this.$emit('update', {
        familyMembers: this.familyMembers,
      });
    },
  },
};
</script>

这些组件都做了什么呢?

  • 每个组件都有自己的 data,用于存储当前步骤的数据。
  • 每个组件都接受一个 formDataprop,用于初始化数据。
  • 每个组件都有一个 updateData 方法,用于更新 formData 中的数据,并通过 $emit 触发 update 事件,将数据传递给父组件 (MultiStepForm)。
  • 使用 watch 监听 formData 的变化,当 formData 变化时,更新组件内部的数据,确保数据同步。

现在,你已经有了一些基本的表单步骤组件,可以尝试在 MultiStepForm.vue 中引入它们,看看效果。

第三步:数据暂存,留住用户的“心血”!

数据暂存是多步骤表单的灵魂。用户填写了一些信息,如果刷新页面或者关闭浏览器,数据就丢失了,那用户肯定会骂娘。所以,我们需要把数据暂存起来。

这里,我们使用 localStorage 来存储数据。当然,你也可以使用 sessionStorage 或者其他方式。

修改 MultiStepForm.vue 组件:

<script>
export default {
  name: 'MultiStepForm',
  data() {
    return {
      currentStep: 0,
      formData: this.loadFormData() || {}, // 从 localStorage 加载数据
      steps: [
        { title: '个人信息', component: 'PersonalInformation' },
        { title: '工作经历', component: 'WorkExperience' },
        { title: '家庭情况', component: 'FamilyInformation' },
      ],
    };
  },
  computed: {
    currentStepComponent() {
      return this.steps[this.currentStep].component;
    },
  },
  components: {
    PersonalInformation: () => import('./components/PersonalInformation.vue'),
    WorkExperience: () => import('./components/WorkExperience.vue'),
    FamilyInformation: () => import('./components/FamilyInformation.vue'),
  },
  watch: {
    formData: {
      handler(newVal) {
        this.saveFormData(newVal); // 监听 formData 的变化,保存到 localStorage
      },
      deep: true,
    },
  },
  methods: {
    goToStep(stepIndex) {
      this.currentStep = stepIndex;
    },
    nextStep() {
      this.currentStep++;
    },
    prevStep() {
      this.currentStep--;
    },
    updateFormData(data) {
      // 合并数据,避免覆盖
      this.formData = { ...this.formData, ...data };
    },
    submitForm() {
      // 提交表单,这里可以发送请求到后端
      console.log('提交表单', this.formData);
      alert('提交成功,数据已打印到控制台');
    },
    saveFormData(data) {
      localStorage.setItem('multiStepFormData', JSON.stringify(data));
    },
    loadFormData() {
      const data = localStorage.getItem('multiStepFormData');
      return data ? JSON.parse(data) : null;
    },
  },
};
</script>

这段代码做了什么呢?

  • loadFormData: 从 localStorage 加载数据,如果 localStorage 中没有数据,则返回 null
  • saveFormData: 将数据保存到 localStorage
  • watch: 监听 formData 的变化,当 formData 变化时,调用 saveFormData 方法,将数据保存到 localStorage

现在,你可以尝试填写一些数据,然后刷新页面或者关闭浏览器,看看数据是否还在。

第四步:动态校验,让表单更严谨!

光能填数据还不够,我们还需要对数据进行校验,确保用户填写的信息是有效的。这里,我们使用 Vue 的 computed 属性和一些简单的 JavaScript 逻辑来实现动态校验。

修改 PersonalInformation.vue 组件:

<template>
  <div>
    <h2>个人信息</h2>
    <label>姓名:</label>
    <input type="text" v-model="name" @input="updateData" /><br />
    <span v-if="nameError" style="color: red;">{{ nameError }}</span><br/>

    <label>年龄:</label>
    <input type="number" v-model="age" @input="updateData" /><br />
    <span v-if="ageError" style="color: red;">{{ ageError }}</span><br/>

    <label>邮箱:</label>
    <input type="email" v-model="email" @input="updateData" /><br />
    <span v-if="emailError" style="color: red;">{{ emailError }}</span><br/>
  </div>
</template>

<script>
export default {
  name: 'PersonalInformation',
  props: ['formData'],
  data() {
    return {
      name: this.formData.name || '',
      age: this.formData.age || '',
      email: this.formData.email || '',
    };
  },
  computed: {
    nameError() {
      if (!this.name) {
        return '姓名不能为空';
      }
      return '';
    },
    ageError() {
      if (!this.age) {
        return '年龄不能为空';
      }
      if (isNaN(this.age) || this.age < 0 || this.age > 150) {
        return '年龄必须是 0-150 之间的数字';
      }
      return '';
    },
    emailError() {
      if (!this.email) {
        return '邮箱不能为空';
      }
      if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(this.email)) {
        return '邮箱格式不正确';
      }
      return '';
    },
  },
  watch: {
    formData: {
      handler(newVal) {
        this.name = newVal.name || '';
        this.age = newVal.age || '';
        this.email = newVal.email || '';
      },
      deep: true,
    },
  },
  methods: {
    updateData() {
      this.$emit('update', {
        name: this.name,
        age: this.age,
        email: this.email,
      });
    },
  },
};
</script>

这段代码做了什么呢?

  • computed: 定义了三个计算属性 nameErrorageErroremailError,用于存储校验错误信息。
  • 每个计算属性都根据对应的数据进行校验,如果数据不符合要求,则返回错误信息,否则返回空字符串。
  • 在模板中使用 v-if 指令,根据错误信息是否为空来显示错误提示。

现在,你可以尝试填写一些错误的数据,看看是否会显示错误提示。

第五步:阻止步骤跳转,错误必须改正!

我们希望用户在当前步骤填写的数据都正确之后,才能跳转到下一步。所以,我们需要阻止步骤跳转,直到所有数据都通过校验。

修改 MultiStepForm.vue 组件:

<template>
  <div class="multi-step-form">
    <!-- 步骤指示器 -->
    <div class="steps">
      <div
        v-for="(step, index) in steps"
        :key="index"
        class="step"
        :class="{ active: currentStep === index, disabled: step.disabled }"
        @click="goToStep(index)"
      >
        {{ step.title }}
      </div>
    </div>

    <!-- 表单内容区域 -->
    <div class="form-content">
      <component :is="currentStepComponent" :formData="formData" @update="updateFormData" @next="nextStep" @prev="prevStep" :isValid="isValid"></component>
    </div>

    <!-- 按钮区域 -->
    <div class="buttons">
      <button v-if="currentStep > 0" @click="prevStep">上一步</button>
      <button v-if="currentStep < steps.length - 1" @click="nextStep" :disabled="!isValid">下一步</button>
      <button v-else @click="submitForm" :disabled="!isValid">提交</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'MultiStepForm',
  data() {
    return {
      currentStep: 0,
      formData: this.loadFormData() || {}, // 从 localStorage 加载数据
      steps: [
        { title: '个人信息', component: 'PersonalInformation', disabled: false },
        { title: '工作经历', component: 'WorkExperience', disabled: true },
        { title: '家庭情况', component: 'FamilyInformation', disabled: true },
      ],
      validations: { // 存储每个步骤的校验状态
        'PersonalInformation': false,
        'WorkExperience': false,
        'FamilyInformation': false,
      },
    };
  },
  computed: {
    currentStepComponent() {
      return this.steps[this.currentStep].component;
    },
    isValid() {
      return this.validations[this.currentStepComponent];
    },
  },
  components: {
    PersonalInformation: () => import('./components/PersonalInformation.vue'),
    WorkExperience: () => import('./components/WorkExperience.vue'),
    FamilyInformation: () => import('./components/FamilyInformation.vue'),
  },
  watch: {
    formData: {
      handler(newVal) {
        this.saveFormData(newVal); // 监听 formData 的变化,保存到 localStorage
      },
      deep: true,
    },
  },
  methods: {
    goToStep(stepIndex) {
        if (!this.steps[stepIndex].disabled) {
            this.currentStep = stepIndex;
        }
    },
    nextStep() {
      if (this.isValid) {
        this.currentStep++;
        if (this.currentStep < this.steps.length) {
            this.steps[this.currentStep].disabled = false; //解锁下一步
        }
      }
    },
    prevStep() {
      this.currentStep--;
    },
    updateFormData(data) {
      // 合并数据,避免覆盖
      this.formData = { ...this.formData, ...data };
    },
    submitForm() {
      // 提交表单,这里可以发送请求到后端
      if (this.isValid) {
          console.log('提交表单', this.formData);
          alert('提交成功,数据已打印到控制台');
      }
    },
    saveFormData(data) {
      localStorage.setItem('multiStepFormData', JSON.stringify(data));
    },
    loadFormData() {
      const data = localStorage.getItem('multiStepFormData');
      return data ? JSON.parse(data) : null;
    },
    setValidation(componentName, isValid) {
      this.validations[componentName] = isValid;
    },
  },
};
</script>

修改 PersonalInformation.vue 组件:

<template>
  <div>
    <h2>个人信息</h2>
    <label>姓名:</label>
    <input type="text" v-model="name" @input="validate" /><br />
    <span v-if="nameError" style="color: red;">{{ nameError }}</span><br/>

    <label>年龄:</label>
    <input type="number" v-model="age" @input="validate" /><br />
    <span v-if="ageError" style="color: red;">{{ ageError }}</span><br/>

    <label>邮箱:</label>
    <input type="email" v-model="email" @input="validate" /><br />
    <span v-if="emailError" style="color: red;">{{ emailError }}</span><br/>
  </div>
</template>

<script>
export default {
  name: 'PersonalInformation',
  props: ['formData'],
  data() {
    return {
      name: this.formData.name || '',
      age: this.formData.age || '',
      email: this.formData.email || '',
      isValid: false,
    };
  },
  computed: {
    nameError() {
      if (!this.name) {
        return '姓名不能为空';
      }
      return '';
    },
    ageError() {
      if (!this.age) {
        return '年龄不能为空';
      }
      if (isNaN(this.age) || this.age < 0 || this.age > 150) {
        return '年龄必须是 0-150 之间的数字';
      }
      return '';
    },
    emailError() {
      if (!this.email) {
        return '邮箱不能为空';
      }
      if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(this.email)) {
        return '邮箱格式不正确';
      }
      return '';
    },
  },
  watch: {
    formData: {
      handler(newVal) {
        this.name = newVal.name || '';
        this.age = newVal.age || '';
        this.email = newVal.email || '';
        this.validate();
      },
      deep: true,
    },
  },
  mounted() {
      this.validate();
  },
  methods: {
    validate() {
        const isValid = !this.nameError && !this.ageError && !this.emailError;
        this.isValid = isValid;
        this.$emit('update', {
          name: this.name,
          age: this.age,
          email: this.email,
        });
        this.$emit('validate', isValid);
    },
    updateData() {
        this.validate();
    },
  },
  emits: ['update', 'validate'],
};
</script>
  • MultiStepForm.vue
    • steps 数组中每个元素添加 disabled 属性,用于控制步骤是否可点击。 默认只有第一个步骤可以点击。
    • 添加 validations 对象,用于存储每个步骤的校验状态。
    • 添加 isValid 计算属性,判断当前步骤是否通过校验。
    • nextStep 方法中,只有当 isValidtrue 时,才能跳转到下一步。
    • 步骤指示器添加 disabled class, 根据 step.disabled 属性控制样式。
  • PersonalInformation.vue
    • data 中添加 isValid 属性,用于存储当前步骤的校验状态。
    • 添加 validate 方法,用于进行校验,并根据校验结果更新 isValid 属性。
    • 添加 $emit('validate', isValid) 事件,将校验结果传递给父组件。
    • updateData 方法中调用 validate 方法,确保每次数据变化都进行校验。
    • 添加 mounted 钩子函数,在组件加载时进行校验。
    • 声明 emits 属性,明确组件可以触发的事件。

第六步:锦上添花,优化用户体验!

  • 步骤指示器高亮显示: 可以根据 currentStep 的值,给当前步骤的指示器添加一个特殊的样式,让用户更清楚地知道自己在哪一步。
  • 动画效果: 可以在步骤切换的时候,添加一些动画效果,让用户体验更流畅。
  • 自定义校验规则: 可以根据实际需求,自定义一些校验规则,例如,手机号码、身份证号码等等。
  • 异步校验: 有些校验需要向后端发送请求,例如,用户名是否已存在等等。可以使用 async/await 来实现异步校验。

总结:多步骤表单,不再是拦路虎!

通过以上步骤,你已经掌握了 Vue 项目中多步骤表单的实现方法。当然,这只是一个基本的框架,你可以根据实际需求进行扩展和优化。

记住,多步骤表单的核心在于:

  • 拆分: 将复杂的表单拆分成多个步骤。
  • 暂存: 保存用户填写的数据,避免数据丢失。
  • 校验: 确保用户填写的数据是有效的。
  • 体验: 优化用户体验,让用户更乐于填写表单。

希望今天的分享对你有所帮助。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注