各位观众老爷,晚上好!我是今天的主讲人,咱们今晚聊聊 Vue 项目里那个让人头疼,但又不得不做的家伙——复杂的多步骤表单。别怕,今天咱们把它拆开揉碎了,保证让你回去也能轻松驾驭。
咱们今天的主题是:Vue多步骤表单:跳着舞填表,数据不跑路,还能自动纠错!
咱们要实现的目标是:
- 步骤跳转: 用户可以自由地在各个步骤之间切换,想先填哪个就填哪个,不再被流程束缚。
- 数据暂存: 即使刷新页面或者切换步骤,之前填写的数据也要保存下来,不能让用户白填。
- 动态校验: 每个步骤都有自己的校验规则,只有通过校验才能进入下一步,而且还要能根据数据变化动态调整校验规则。
第一幕:搭好舞台,准备开演
首先,我们需要一个 Vue 项目。如果还没有,用 Vue CLI 快速创建一个:
vue create my-multi-step-form
选择你喜欢的配置,一路回车就行。项目创建好之后,进入项目目录:
cd my-multi-step-form
接下来,我们需要一些基本的组件。咱们先创建一个 components
目录,然后在里面创建几个组件,分别代表不同的步骤。例如,Step1.vue
,Step2.vue
,Step3.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
来动态地渲染当前步骤的组件。 - 定义了
nextStep
、prevStep
和goToStep
方法来切换步骤。 - 使用了动态组件
<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.vue
,Step2.vue
,Step3.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')
现在,我们可以在组件中使用 ValidationProvider
和 ValidationObserver
来进行校验了。
修改 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-validate
的 expression
校验规则。
假设我们在 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
中初始化 gender
和 height
字段:
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,来定义更复杂的校验规则。
- 添加更友好的用户界面,例如进度条、动画效果等。
- 支持服务端校验,将校验逻辑放在服务端,可以提高安全性。
希望今天的讲座对你有所帮助,咱们下次再见!