跳转到内容

Vue 复杂联动表单

刀刀
0字
0分钟
2026/8/16

效果展示

效果

进入该页面,筛选「投放区域」下拉框选择数据后,「投放详细」表格联动其数据,自动添加对应的数据到表格中,用户手动输入每个区域对应的委托人。输入完毕后在「目标人群」模块,新增数据添加目标人群,最后点击 "提交" 按钮,将数据提交到后端。

数据格式如下图所示:

参数格式

数据流向梳理

拿到需求先梳理数据流向。不推荐每一个模块的下拉框、表格等各自使用一个变量,因为这样不方便联动数据,需要写很多额外代码处理。

根据上述数据格式,声明一个变量 formData 用来做数据存储,最后作为接口参数给到后端。

「投放区域」的下拉框不能使用v-model语法糖了,而是使用 @change 事件,手动处理数据并赋值到 formData 中。:modul-value 绑定computed计算属性计算出来的数据,当 formData 中的数据发生变化时,自动更新。

「投放详细」表格则直接使用 formData 变量作为数据源,用户修改委托人时直接修改 formData 中的数据。

最后「目标人群」直接修改 formData 中的数据很不方便,因此作为一个子组件,它自己使用自己的数据,父组件点击 “提交” 按钮后,获取到子组件的数据,循环 formData 变量,把从子组件拿到的数据添加到每个对象的 crowd 数组中。

页面架构

先给出页面的相关静态组件架构。

vue
<script setup>
const options = [
  {
    value: '1',
    label: '区域1',
  },
  {
    value: '2',
    label: '区域2',
  },
  {
    value: '3',
    label: '区域3',
  },
  {
    value: '4',
    label: '区域4',
  },
]

const crowdTableRef = ref()

const formData = ref([])

const computedValue = computed(() => {
  return ''
})

const handleChange = (value) => {}

const handleSubmit = () => {}
</script>

<template>
  <div>
    <el-select
      :model-value="computedValue"
      @change="handleChange"
      placeholder="请选择"
      multiple
    >
      <el-option
        v-for="item in options"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      ></el-option>
    </el-select>

    <DetailTable :data="formData" />

    <CrowdTable ref="crowdTableRef" />

    <el-button type="primary" @click="handleSubmit">提交</el-button>
  </div>
</template>
vue
<script setup>
const { data } = defineProps(['data'])
</script>

<template>
  <el-table :data="data">
    <el-table-column label="区域名称" />
    <el-table-column label="投放责任人" />
  </el-table>
</template>
vue
<script setup></script>

<template>
  <el-table :data="data">
    <el-table-column label="人群名称" />
    <el-table-column label="目标量级" />
  </el-table>
  <div>
    <el-button type="primary">新增</el-button>
  </div>
</template>

逻辑实现

Vue/React核心思想:
  1. 数据驱动
  2. 理清数据流向

前面分析了,整个功能都只使用 formData 一个响应式变量保存数据,后续传给后端。

投放区域

「投放区域」下拉框无法使用v-model语法糖简单双向绑定,这样无法把值保存到 formData 数组对象中并自定义格式,因此需要使用 @change 事件,触发事件后按照前面的数据格式,手动保存一个对象。

App.vue
vue
<script setup>
// 计算属性,获取当前选中的值用于下拉框回显
const computedValue = computed(() => {
  return ''
  return formData.value.map(item => item.name) 
})

const handleChange = (value) => {
  // 保存数据到 formData 每一个对象的 name 属性中
  formData.value = value.map(item => { 
    return { 
      name: item, 
      details: { 
        duty: '', 
      } 
      crowd: [] 
    } 
  }) 
}
</script>

投放详细

「投放详细」表格直接使用 formData 作为数据源,用户修改委托人时直接修改 formData 中的 duty 数据。在回显区域名称时,直接使用 name 属性显示的是 code 值,需要匹配对应的 label 值做回显。数组查找每次都需要循环,会造成一定的性能浪费,可以使用 Map 数据结构优化。

vue
<script setup>
const options = [
  {
    value: '1',
    label: '区域1',
  },
  {
    value: '2',
    label: '区域2',
  },
  {
    value: '3',
    label: '区域3',
  },
  {
    value: '4',
    label: '区域4',
  },
]

const optionMap = {} 
options.forEach((item) => {
  optionMap[item.value] = item.label 
}) 
</script>

<template>
  <div>
    <el-select
      :model-value="computedValue"
      @change="handleChange"
      placeholder="请选择"
      multiple
    >
      <el-option
        v-for="item in options"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      ></el-option>
    </el-select>

    <DetailTable :data="formData" :optionMap="optionMap" />

    <CrowdTable ref="crowdTableRef" />

    <el-button type="primary" @click="handleSubmit">提交</el-button>
  </div>
</template>
vue
<script setup>
const { data, optionMap } = defineProps(['data']) 
</script>

<template>
  <el-table :data="data">
    <el-table-column label="区域名称" />
    <el-table-column label="投放责任人" />
    <el-table-column label="区域名称">
      <template #default="{ row }">
        {{ optionMap[row.name] }}
      </template>
    </el-table-column>
    <el-table-column label="投放责任人">
      <template #default="{ row }">
        <el-input v-model="row.details.duty" />
      </template>
    </el-table-column>
  </el-table>
</template>

注意

子组件直接修改了父组件props传递的参数,在React中不能这么做,需要 @change 子传父在父组件修改变量;在Vue中修改其中的属性是允许的,如果整个对象全部替换则不允许。

目标人群

「目标人群」表格无法直接使用 formData 作为数据源,因为这里添加的数据要保存到全部对象的 crowd 数组内,如果直接使用 formData 作为数据源,v-model 只能绑定修改第一个对象内的数据。

解决方法是子组件自己声明一个变量保存数据,自己管自己。

最终父组件点击提交按钮时,获取到子组件的变量 crowdArr,循环 formData 数组,把子组件的变量 crowdArr 赋值给每一条对象的 crowd 数组内。

vue
<script setup>
const crowdTableRef = ref()

const handleSubmit = () => {
  formData.value.forEach((item) => {
    item.crowd = crowdTableRef.value.crowdArr 
  }) 

  axios.get('/xxx', { params: formData.value }).then((res) => {}) // 模拟调用接口
}
</script>
vue
<script setup>
const crowdArr = ref([]) 
const handleAdd = () => {
  crowdArr.value.push({ crowdName: '', crowdSize: '' }) 
} 

;`defineExpose`({ crowdArr }) 
</script>

<template>
  <el-table :data="data">
    <el-table-column label="人群名称" />
    <el-table-column label="目标量级" />
    <el-table-column label="人群名称">
      <template #default="{ row }">
        <el-input v-model="row.crowdName" />
      </template>
    </el-table-column>
    <el-table-column label="目标量级">
      <template #default="{ row }">
        <el-input v-model="row.crowdSize" />
      </template>
    </el-table-column>
  </el-table>
  <div>
    <el-button type="primary">新增</el-button>
    <el-button type="primary" @click="handleAdd">新增</el-button>
  </div>
</template>

优化修改

目前还是有两个需要优化修改调整的地方:

  1. 一开始「投放区域」选择两条数据,「投放详细」填写好「投放责任人」,再删除一条「投放区域」数据,可以发现填写好的「投放责任人」被清掉了
  2. 获取到数据时(比如查看详情操作等)「目标人群」表格数据无法回显

第一条 Bug 是因为最开始「投放区域」下拉框的 @change 事件添加数据时,是直接简单粗暴的添加了空的数据,没有对是否有数据进行判断,导致在删除数据时,删除了之前添加的数据。

第二个问题的解决方案是在子组件再暴露一个方法,父组件在获取到数据时,调用子组件的方法,把数据回显赋值到子组件的变量 crowdArr 中。由于每个对象的 crowd 数组数据都是一样的,因此随便获取任意一个数据的数据即可。

vue
<script setup>
const handleChange = (value) => {
  // 保存数据到 formData 每一个对象的 name 属性中
  formData.value = value.map((item) => {
    let details = formData.value.find((i) => i.name === item)?.details || {} 
    return {
      name: item,
      details: {
        duty: '', 
      }, 
      details, 
      crowd: [],
    }
  })
}

// 回显功能测试
const handleEcho = () => {
  formData.value = [
    {
      name: '北京', 
      details: {
        duty: '张三', 
      }, 
      crowd: [
        {
          crowdName: '人群1', 
          crowdSize: 100, 
        }, 
        {
          crowdName: '人群2', 
          crowdSize: 200, 
        }, 
      ], 
    }, 
    {
      name: '上海', 
      details: {
        duty: '李四', 
      }, 
      crowd: [
        {
          crowdName: '人群1', 
          crowdSize: 100, 
        }, 
        {
          crowdName: '人群2', 
          crowdSize: 200, 
        }, 
      ], 
    }, 
  ] 
  crowdTableRef.value.setCrowd(formData.value[0].crowd) 
} 
</script>
vue
<script setup>
const crowdArr = ref([])
const handleAdd = () => {
  crowdArr.value.push({ crowdName: '', crowdSize: '' })
}
const setCrowd = (value) => {
  crowdArr.value = value 
} 

defineExpose({
  crowdArr,
  setCrowd, 
})
</script>

完整代码

vue
<script setup>
const options = [
  {
    value: '1',
    label: '区域1',
  },
  {
    value: '2',
    label: '区域2',
  },
  {
    value: '3',
    label: '区域3',
  },
  {
    value: '4',
    label: '区域4',
  },
]

const crowdTableRef = ref()

const formData = ref([])

const handleChange = (value) => {
  // 保存数据到 formData 每一个对象的 name 属性中
  formData.value = value.map((item) => {
    let details = formData.value.find((i) => i.name === item)?.details || {}

    return {
      name: item,
      details,
      crowd: [],
    }
  })
}

// 计算属性,获取当前选中的值用于下拉框回显
const computedValue = computed(() => {
  return formData.value.map((item) => item.name)
})

const optionMap = {}

options.forEach((item) => {
  optionMap[item.value] = item.label
})

const handleSubmit = () => {
  formData.value.forEach((item) => {
    item.crowd = crowdTableRef.value.crowdArr
  })

  axios.get('/xxx', { params: formData.value }).then((res) => {}) // 模拟调用接口
}

// 回显功能测试
const handleEcho = () => {
  formData.value = [
    {
      name: '北京',
      details: {
        duty: '张三',
      },
      crowd: [
        {
          crowdName: '人群1',
          crowdSize: 100,
        },
        {
          crowdName: '人群2',
          crowdSize: 200,
        },
      ],
    },
    {
      name: '上海',
      details: {
        duty: '李四',
      },
      crowd: [
        {
          crowdName: '人群1',
          crowdSize: 100,
        },
        {
          crowdName: '人群2',
          crowdSize: 200,
        },
      ],
    },
  ]
  crowdTableRef.value.setCrowd(formData.value[0].crowd)
}
</script>

<template>
  <div>
    <el-select
      :model-value="computedValue"
      @change="handleChange"
      placeholder="请选择"
      multiple
    >
      <el-option
        v-for="item in options"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      ></el-option>
    </el-select>

    <DetailTable :data="formData" :optionMap="optionMap" />

    <CrowdTable ref="crowdTableRef" />

    <el-button type="primary" @click="handleSubmit">提交</el-button>
  </div>
</template>
vue
<script setup>
const { data, optionMap } = defineProps(['data'])
</script>

<template>
  <el-table :data="data">
    <el-table-column label="区域名称">
      <template #default="{ row }">
        {{ optionMap[row.name] }}
      </template>
    </el-table-column>
    <el-table-column label="投放责任人">
      <template #default="{ row }">
        <el-input v-model="row.details.duty" />
      </template>
    </el-table-column>
  </el-table>
</template>
vue
<script setup>
const crowdArr = ref([])
const handleAdd = () => {
  crowdArr.value.push({ crowdName: '', crowdSize: '' })
}
const setCrowd = (value) => {
  crowdArr.value = value
}

defineExpose({ crowdArr, setCrowd })
</script>

<template>
  <el-table :data="data">
    <el-table-column label="人群名称">
      <template #default="{ row }">
        <el-input v-model="row.crowdName" />
      </template>
    </el-table-column>
    <el-table-column label="目标量级">
      <template #default="{ row }">
        <el-input v-model="row.crowdSize" />
      </template>
    </el-table-column>
  </el-table>
  <div>
    <el-button type="primary" @click="handleAdd">新增</el-button>
  </div>
</template>

贡献者

The avatar of contributor named as v_duyilin v_duyilin
The avatar of contributor named as duyidao duyidao
The avatar of contributor named as 刀刀 刀刀

页面历史

刀刀博客累计访客 人;文档累计访问量共