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

各位观众老爷,晚上好!我是今天的主讲人,咱们今晚聊聊 Vue 项目里那个让人头疼,但又不得不做的家伙——复杂的多步骤表单。别怕,今天咱们把它拆开揉碎了,保证让你回去也能轻松驾驭。

咱们今天的主题是:Vue多步骤表单:跳着舞填表,数据不跑路,还能自动纠错!

咱们要实现的目标是:

  1. 步骤跳转: 用户可以自由地在各个步骤之间切换,想先填哪个就填哪个,不再被流程束缚。
  2. 数据暂存: 即使刷新页面或者切换步骤,之前填写的数据也要保存下来,不能让用户白填。
  3. 动态校验: 每个步骤都有自己的校验规则,只有通过校验才能进入下一步,而且还要能根据数据变化动态调整校验规则。

第一幕:搭好舞台,准备开演

首先,我们需要一个 Vue 项目。如果还没有,用 Vue CLI 快速创建一个:

vue create my-multi-step-form

选择你喜欢的配置,一路回车就行。项目创建好之后,进入项目目录:

cd my-multi-step-form

接下来,我们需要一些基本的组件。咱们先创建一个 components 目录,然后在里面创建几个组件,分别代表不同的步骤。例如,Step1.vueStep2.vueStep3.vue

mkdir src/components
touch src/components/Step1.vue src/components/Step2.vue src/components/Step3.vue

每个组件都先简单写个占位符:

Step1.vue:

<template>
  <div>
    <h2>Step 1: 个人信息</h2>
    <input type="text" placeholder="姓名" v-model="formData.name">
    <button @click="nextStep">下一步</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        name: ''
      }
    };
  },
  methods: {
    nextStep() {
      this.$emit('next');
    }
  }
};
</script>

Step2.vue:

<template>
  <div>
    <h2>Step 2: 联系方式</h2>
    <input type="email" placeholder="邮箱" v-model="formData.email">
    <button @click="prevStep">上一步</button>
    <button @click="nextStep">下一步</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        email: ''
      }
    };
  },
  methods: {
    prevStep() {
      this.$emit('prev');
    },
    nextStep() {
      this.$emit('next');
    }
  }
};
</script>

Step3.vue:

<template>
  <div>
    <h2>Step 3: 兴趣爱好</h2>
    <input type="text" placeholder="爱好" v-model="formData.hobby">
    <button @click="prevStep">上一步</button>
    <button @click="submit">提交</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        hobby: ''
      }
    };
  },
  methods: {
    prevStep() {
      this.$emit('prev');
    },
    submit() {
      alert('提交成功!');
    }
  }
};
</script>

第二幕:导演中心,掌控全局

现在,我们需要一个“导演”,也就是父组件,来控制整个表单的流程。咱们修改 App.vue

<template>
  <div id="app">
    <h1>多步骤表单</h1>
    <div class="steps">
      <button
        v-for="(step, index) in steps"
        :key="index"
        :class="{ active: currentStep === index + 1 }"
        @click="goToStep(index + 1)"
      >
        {{ step.title }}
      </button>
    </div>

    <component
      :is="currentStepComponent"
      @next="nextStep"
      @prev="prevStep"
      :form-data="formData"
    />

    <pre>{{ formData }}</pre> <!-- 展示数据,方便调试 -->
  </div>
</template>

<script>
import Step1 from './components/Step1.vue';
import Step2 from './components/Step2.vue';
import Step3 from './components/Step3.vue';

export default {
  components: {
    Step1,
    Step2,
    Step3
  },
  data() {
    return {
      currentStep: 1,
      steps: [
        { title: '个人信息', component: 'Step1' },
        { title: '联系方式', component: 'Step2' },
        { title: '兴趣爱好', component: 'Step3' }
      ],
      formData: {} // 存储所有步骤的数据
    };
  },
  computed: {
    currentStepComponent() {
      return this.steps[this.currentStep - 1].component;
    }
  },
  methods: {
    nextStep() {
      if (this.currentStep < this.steps.length) {
        this.currentStep++;
      }
    },
    prevStep() {
      if (this.currentStep > 1) {
        this.currentStep--;
      }
    },
    goToStep(step) {
      this.currentStep = step;
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

.steps {
  margin-bottom: 20px;
}

.steps button {
  padding: 10px 20px;
  margin: 0 5px;
  border: 1px solid #ccc;
  cursor: pointer;
}

.steps button.active {
  background-color: #4caf50;
  color: white;
}
</style>

在这个 App.vue 中,我们做了这些事情:

  • 定义了 currentStep 来控制当前显示的步骤。
  • 定义了 steps 数组,描述了每个步骤的标题和对应的组件。
  • 使用 computed 属性 currentStepComponent 来动态地渲染当前步骤的组件。
  • 定义了 nextStepprevStepgoToStep 方法来切换步骤。
  • 使用了动态组件 <component :is="currentStepComponent"> 来渲染不同的步骤组件。
  • 将所有步骤的数据都存储在 formData 对象中,并通过 props 传递给子组件。

现在运行项目 npm run serve,你应该能看到一个可以切换步骤的表单了。但是,现在数据还没有真正地保存起来。

第三幕:数据持久化,防止数据丢失

为了防止数据丢失,我们需要将数据持久化。最简单的方法是使用 localStorage

修改 App.vue,在 data 中添加一个 localStorageKey,然后在 created 钩子中从 localStorage 中读取数据,在 watch 中监听 formData 的变化,并将其保存到 localStorage 中。

<script>
import Step1 from './components/Step1.vue';
import Step2 from './components/Step2.vue';
import Step3 from './components/Step3.vue';

export default {
  components: {
    Step1,
    Step2,
    Step3
  },
  data() {
    return {
      currentStep: 1,
      steps: [
        { title: '个人信息', component: 'Step1' },
        { title: '联系方式', component: 'Step2' },
        { title: '兴趣爱好', component: 'Step3' }
      ],
      localStorageKey: 'my-multi-step-form-data', // 定义 localStorage 的 key
      formData: {} // 存储所有步骤的数据
    };
  },
  computed: {
    currentStepComponent() {
      return this.steps[this.currentStep - 1].component;
    }
  },
  watch: {
    formData: {
      handler(newValue) {
        localStorage.setItem(this.localStorageKey, JSON.stringify(newValue));
      },
      deep: true // 深度监听,确保所有嵌套属性的变化都能被监听到
    }
  },
  created() {
    // 从 localStorage 中读取数据
    const storedData = localStorage.getItem(this.localStorageKey);
    if (storedData) {
      this.formData = JSON.parse(storedData);
    }
  },
  methods: {
    nextStep() {
      if (this.currentStep < this.steps.length) {
        this.currentStep++;
      }
    },
    prevStep() {
      if (this.currentStep > 1) {
        this.currentStep--;
      }
    },
    goToStep(step) {
      this.currentStep = step;
    }
  }
};
</script>

现在,无论你刷新页面还是关闭浏览器,之前填写的数据都会被保存下来。

接下来,我们需要让子组件能够修改父组件的 formData。修改 Step1.vueStep2.vueStep3.vue,使用 v-model 的语法糖,将 formData 传递给子组件:

Step1.vue:

<template>
  <div>
    <h2>Step 1: 个人信息</h2>
    <input type="text" placeholder="姓名" v-model="name">
    <button @click="nextStep">下一步</button>
  </div>
</template>

<script>
export default {
  props: {
    formData: {
      type: Object,
      required: true
    }
  },
  computed: {
    name: {
      get() {
        return this.formData.name || '';
      },
      set(value) {
        this.$emit('update:formData', { ...this.formData, name: value });
      }
    }
  },
  methods: {
    nextStep() {
      this.$emit('next');
    }
  }
};
</script>

Step2.vue:

<template>
  <div>
    <h2>Step 2: 联系方式</h2>
    <input type="email" placeholder="邮箱" v-model="email">
    <button @click="prevStep">上一步</button>
    <button @click="nextStep">下一步</button>
  </div>
</template>

<script>
export default {
  props: {
    formData: {
      type: Object,
      required: true
    }
  },
  computed: {
    email: {
      get() {
        return this.formData.email || '';
      },
      set(value) {
        this.$emit('update:formData', { ...this.formData, email: value });
      }
    }
  },
  methods: {
    prevStep() {
      this.$emit('prev');
    },
    nextStep() {
      this.$emit('next');
    }
  }
};
</script>

Step3.vue:

<template>
  <div>
    <h2>Step 3: 兴趣爱好</h2>
    <input type="text" placeholder="爱好" v-model="hobby">
    <button @click="prevStep">上一步</button>
    <button @click="submit">提交</button>
  </div>
</template>

<script>
export default {
  props: {
    formData: {
      type: Object,
      required: true
    }
  },
  computed: {
    hobby: {
      get() {
        return this.formData.hobby || '';
      },
      set(value) {
        this.$emit('update:formData', { ...this.formData, hobby: value });
      }
    }
  },
  methods: {
    prevStep() {
      this.$emit('prev');
    },
    submit() {
      alert('提交成功!');
    }
  }
};
</script>

同时修改 App.vue 中动态组件的传递方式:

    <component
      :is="currentStepComponent"
      @next="nextStep"
      @prev="prevStep"
      :form-data.sync="formData"
    />

注意这里的 :form-data.sync="formData".sync 修饰符允许子组件修改父组件的 props。

第四幕:动态校验,确保数据质量

光能保存数据还不够,我们还要确保数据的质量。我们需要在每个步骤中添加校验规则,只有通过校验才能进入下一步。

这里我们使用 vee-validate 这个 Vue 校验库。首先安装它:

npm install vee-validate@3 --save

注意,这里我们安装的是 vee-validate@3,因为 vee-validate@4 的 API 有很大的变化。

然后在 main.js 中引入并配置 vee-validate

import Vue from 'vue'
import App from './App.vue'
import { ValidationObserver, ValidationProvider, extend, configure } from 'vee-validate';
import { required, email } from 'vee-validate/dist/rules';

Vue.config.productionTip = false

// 配置 vee-validate
Vue.component('ValidationObserver', ValidationObserver);
Vue.component('ValidationProvider', ValidationProvider);

// 定义校验规则
extend('required', required);
extend('email', email);

// 配置校验提示信息
configure({
  defaultMessage: (field, values) => {
    // values._field_ = field; // 这里可以自定义提示信息,例如将字段名也显示出来
    return `The ${field} field is required.`;
  }
});

new Vue({
  render: h => h(App),
}).$mount('#app')

现在,我们可以在组件中使用 ValidationProviderValidationObserver 来进行校验了。

修改 Step1.vue

<template>
  <div>
    <h2>Step 1: 个人信息</h2>
    <ValidationObserver v-slot="{ invalid }">
      <ValidationProvider rules="required" v-slot="{ errors }">
        <input type="text" placeholder="姓名" v-model="name">
        <span>{{ errors[0] }}</span>
      </ValidationProvider>
      <button @click="nextStep" :disabled="invalid">下一步</button>
    </ValidationObserver>
  </div>
</template>

<script>
export default {
  props: {
    formData: {
      type: Object,
      required: true
    }
  },
  computed: {
    name: {
      get() {
        return this.formData.name || '';
      },
      set(value) {
        this.$emit('update:formData', { ...this.formData, name: value });
      }
    }
  },
  methods: {
    nextStep() {
      this.$emit('next');
    }
  }
};
</script>

这里我们使用了 ValidationProvider 来包裹 input 元素,并指定了 rules="required",表示这个字段是必填的。v-slot="{ errors }" 可以获取到校验错误信息,并将其显示出来。

ValidationObserver 用来包裹整个表单,v-slot="{ invalid }" 可以获取到整个表单的校验状态,如果表单中有任何一个字段校验失败,invalid 就会是 true,我们就可以禁用下一步按钮。

修改 Step2.vue

<template>
  <div>
    <h2>Step 2: 联系方式</h2>
    <ValidationObserver v-slot="{ invalid }">
      <ValidationProvider rules="required|email" v-slot="{ errors }">
        <input type="email" placeholder="邮箱" v-model="email">
        <span>{{ errors[0] }}</span>
      </ValidationProvider>
      <button @click="prevStep">上一步</button>
      <button @click="nextStep" :disabled="invalid">下一步</button>
    </ValidationObserver>
  </div>
</template>

<script>
export default {
  props: {
    formData: {
      type: Object,
      required: true
    }
  },
  computed: {
    email: {
      get() {
        return this.formData.email || '';
      },
      set(value) {
        this.$emit('update:formData', { ...this.formData, email: value });
      }
    }
  },
  methods: {
    prevStep() {
      this.$emit('prev');
    },
    nextStep() {
      this.$emit('next');
    }
  }
};
</script>

这里我们使用了 rules="required|email",表示这个字段是必填的,并且必须是有效的邮箱地址。

Step3.vue 类似,就不重复写了。

第五幕:动态规则,灵活应变

有时候,校验规则需要根据其他字段的值来动态调整。例如,只有当用户选择了某个选项,才需要填写某个字段。

为了实现动态校验,我们可以使用 vee-validateexpression 校验规则。

假设我们在 Step1.vue 中添加一个 性别 选择框,只有当用户选择了 ,才需要填写 身高

首先,在 Step1.vue 中添加 性别身高input 元素:

<template>
  <div>
    <h2>Step 1: 个人信息</h2>
    <ValidationObserver v-slot="{ invalid }">
      <ValidationProvider rules="required" v-slot="{ errors }">
        <input type="text" placeholder="姓名" v-model="name">
        <span>{{ errors[0] }}</span>
      </ValidationProvider>

      <select v-model="gender">
        <option value="">请选择性别</option>
        <option value="male">男</option>
        <option value="female">女</option>
      </select>

      <ValidationProvider :rules="{ required: gender === 'male' }" v-slot="{ errors }">
        <input type="number" placeholder="身高" v-model="height">
        <span>{{ errors[0] }}</span>
      </ValidationProvider>

      <button @click="nextStep" :disabled="invalid">下一步</button>
    </ValidationObserver>
  </div>
</template>

<script>
export default {
  props: {
    formData: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      gender: '',
    };
  },
  computed: {
    name: {
      get() {
        return this.formData.name || '';
      },
      set(value) {
        this.$emit('update:formData', { ...this.formData, name: value });
      }
    },
    height: {
      get() {
        return this.formData.height || '';
      },
      set(value) {
        this.$emit('update:formData', { ...this.formData, height: value });
      }
    }
  },
  methods: {
    nextStep() {
      this.$emit('next');
    }
  }
};
</script>

注意这里的 :rules="{ required: gender === 'male' }",我们使用了对象语法来定义校验规则,required 属性的值是一个表达式,只有当 gender === 'male' 时,required 规则才会生效。

同时,我们需要在 App.vue 中初始化 genderheight 字段:

data() {
    return {
      currentStep: 1,
      steps: [
        { title: '个人信息', component: 'Step1' },
        { title: '联系方式', component: 'Step2' },
        { title: '兴趣爱好', component: 'Step3' }
      ],
      localStorageKey: 'my-multi-step-form-data', // 定义 localStorage 的 key
      formData: {
        gender: '',
        height: ''
      } // 存储所有步骤的数据
    };
  },

总结:谢幕

至此,我们已经实现了一个复杂的多步骤表单,支持步骤跳转、数据暂存和动态校验。当然,这只是一个基础的例子,你可以根据自己的需求进行扩展。

以下是一些可以改进的地方:

  • 使用更高级的状态管理方案,例如 Vuex,来管理表单数据。
  • 使用更强大的校验库,例如 Yup,来定义更复杂的校验规则。
  • 添加更友好的用户界面,例如进度条、动画效果等。
  • 支持服务端校验,将校验逻辑放在服务端,可以提高安全性。

希望今天的讲座对你有所帮助,咱们下次再见!

发表回复

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