在实际业务中,表单很少只有几个固定字段。动态增减表单项、处理数组类型的数据、以及字段之间的联动校验,才是真正考验表单设计能力的地方。这一节我们用 Vue 3 组合式 API + Element Plus(思路同样适用于其他 UI 库)来逐一拆解这些场景。
动态表单:用户可以增减的字段组
典型场景:一个“工作经历”模块,用户可以点击“添加经历”增加一组输入框(公司、职位、起止时间),也可以删除某一条。
核心思路:用一个响应式数组来驱动表单项的渲染,v-for 遍历这个数组,增减操作就是向数组里 push 一项或 splice 掉某一项。
<template>
<el-form :model="form" label-width="80px">
<div v-for="(item, index) in form.experiences" :key="index" style="margin-bottom: 10px">
<el-form-item label="公司">
<el-input v-model="item.company" />
</el-form-item>
<el-form-item label="职位">
<el-input v-model="item.position" />
</el-form-item>
<el-button type="danger" @click="removeExperience(index)">删除</el-button>
</div>
<el-button type="primary" @click="addExperience">添加经历</el-button>
</el-form>
</template>
<script setup>
import { reactive } from 'vue'
const form = reactive({
experiences: [
{ company: '', position: '' }
]
})
function addExperience() {
form.experiences.push({ company: '', position: '' })
}
function removeExperience(index) {
form.experiences.splice(index, 1)
}
</script>
需要注意的坑:
- 使用
reactive包裹整个 form,内部数组的增删仍然是响应式的(Vue 3 的 Proxy 代理解决了 Vue 2 中数组下标操作的响应式问题)。 - 如果有校验需求,动态表单项的
el-form-item的prop需要带上数组下标,如:prop="'experiences.' + index + '.company'"。
数组字段:一个字段对应多个值
常见于“多个手机号”、“收货地址列表”等。表单模型里某个字段本身就是一个数组,我们可能需要单独为每个元素提供输入框,并支持添加新元素或移除已有元素。
<template>
<el-form :model="form">
<el-form-item label="手机号">
<div v-for="(phone, index) in form.phones" :key="index" style="display: flex; gap: 8px; margin-bottom: 5px">
<el-input v-model="form.phones[index]" placeholder="请输入手机号" />
<el-button @click="removePhone(index)" :disabled="form.phones.length === 1">删除</el-button>
</div>
<el-button @click="addPhone">添加手机号</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { reactive } from 'vue'
const form = reactive({
phones: [''] // 至少保留一个
})
function addPhone() {
form.phones.push('')
}
function removePhone(index) {
form.phones.splice(index, 1)
}
</script>
校验要点:如果需要对数组中每个值进行单独校验(如手机号格式),可以动态生成 prop 和对应的校验规则,或者使用 el-form-item 的 error 插槽手动处理。
联动表单:一个字段的值决定其他字段的显示/可选值
联动分为几种层次:
- 显示/隐藏联动:根据选择显示或隐藏后面的字段。
- 选项联动:省市区三级选择是最典型的例子。
- 校验规则联动:某个字段的值变化后,另一个字段的必填或格式规则也跟着变化。
示例:根据用户类型显示不同字段
<template>
<el-form :model="form">
<el-form-item label="用户类型">
<el-select v-model="form.userType">
<el-option label="个人" value="person" />
<el-option label="企业" value="company" />
</el-select>
</el-form-item>
<el-form-item label="姓名" v-if="form.userType === 'person'">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="公司名称" v-if="form.userType === 'company'">
<el-input v-model="form.companyName" />
</el-form-item>
<el-form-item label="统一社会信用代码" v-if="form.userType === 'company'">
<el-input v-model="form.creditCode" />
</el-form-item>
</el-form>
</template>
<script setup>
import { reactive } from 'vue'
const form = reactive({
userType: 'person',
name: '',
companyName: '',
creditCode: ''
})
</script>
这种方式直接通过 v-if 控制显示,但要注意:被隐藏的字段的值仍然存在,提交时需按需清理无关字段,避免提交脏数据。更稳健的做法是在 userType 变化时,清空另一个类型的字段。
import { watch } from 'vue'
watch(() => form.userType, (newType) => {
if (newType === 'person') {
form.companyName = ''
form.creditCode = ''
} else {
form.name = ''
}
})
示例:省市区三级联动
省市区联动本质上是“选择省 → 更新市的可选列表 → 选择市 → 更新区的可选列表”。可以用一个对象存储每一级的列表,并通过 watch 监听上级变化来获取下级数据。
<template>
<el-form :model="form">
<el-form-item label="省">
<el-select v-model="form.province" @change="onProvinceChange">
<el-option v-for="p in provinces" :key="p.code" :label="p.name" :value="p.code" />
</el-select>
</el-form-item>
<el-form-item label="市">
<el-select v-model="form.city" :disabled="!form.province" @change="onCityChange">
<el-option v-for="c in cities" :key="c.code" :label="c.name" :value="c.code" />
</el-select>
</el-form-item>
<el-form-item label="区">
<el-select v-model="form.district" :disabled="!form.city">
<el-option v-for="d in districts" :key="d.code" :label="d.name" :value="d.code" />
</el-select>
</el-form-item>
</el-form>
</template>
<script setup>
import { reactive, ref } from 'vue'
const form = reactive({ province: '', city: '', district: '' })
const provinces = ref([])
const cities = ref([])
const districts = ref([])
// 模拟获取省列表
fetchProvinces().then(data => provinces.value = data)
async function onProvinceChange(provCode) {
form.city = ''
form.district = ''
districts.value = []
cities.value = await fetchCitiesByProvince(provCode)
}
async function onCityChange(cityCode) {
form.district = ''
districts.value = await fetchDistrictsByCity(cityCode)
}
</script>
真实项目中,省市区数据可一次性加载到本地再做筛选,避免多次请求。这里用异步请求只是为了展示联动的逻辑。
联动校验规则
场景:当“是否同步发票”开关打开时,发票抬头变为必填,否则不必填。我们可以动态修改表单的校验规则。
<template>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="同步发票" prop="needInvoice">
<el-switch v-model="form.needInvoice" />
</el-form-item>
<el-form-item label="发票抬头" prop="invoiceTitle">
<el-input v-model="form.invoiceTitle" />
</el-form-item>
</el-form>
</template>
<script setup>
import { reactive, computed, ref } from 'vue'
const form = reactive({ needInvoice: false, invoiceTitle: '' })
const formRef = ref()
const rules = computed(() => ({
invoiceTitle: form.needInvoice
? [{ required: true, message: '请输入发票抬头', trigger: 'blur' }]
: []
}))
// 当开关变化时,可能需要手动触发一下对应字段的校验
import { watch } from 'vue'
watch(() => form.needInvoice, () => {
// nextTick 后清除或触发校验
formRef.value?.validateField('invoiceTitle')
})
</script>
通过 computed 使校验规则变成响应式,Vue 会在依赖变化时自动更新规则。手动调用 validateField 是为了在切换状态时立即给出校验反馈。
组合起来:复杂的业务表单就是这么来的
真实项目中的表单往往是动态、数组、联动的组合。比如一个“项目管理”表单:可以添加多个团队成员(动态数组),每个成员的角色选择会联动显示不同的技能输入框(联动),且整个表单带有校验。核心方法就是:
- 用
reactive维护一个深层嵌套的对象或数组,组件按层级拆开。 - 动态列表用
v-for渲染,增删操作修改数组。 - 联动逻辑用
watch或computed实现,保持数据流向清晰。 - 校验规则可以用
computed动态生成,配合validateField精准控制。
这种模式虽然看起来代码量不少,但结构是清晰的:数据驱动视图,你只需要操心数据间的逻辑关系,至于界面怎么变,Vue 会处理好。