如何实现一个动态表单的设计?

2,450 阅读3分钟

各位掘友好,我是庚云。还在为重复写表单而头疼?学会这招,后端配置自动生成,效率提升300%!前端必备技能!

下面将提供 Vue + React 两个版本的具体实现,感觉有用的点赞收藏。

✨ 本文亮点

✅ 配置即页面 - 后端返回配置,前端自动渲染
✅ 智能条件显示 - 字段联动,动态显隐
✅ 强大校验系统 - 必填验证+自定义规则+远程校验
✅ 极致性能优化 - computed + 动态组件,减少重复渲染
✅ 完整实战代码 - 即拷即用,轻松上手

🎯 适用场景

  • 低代码平台表单构建
  • 动态问卷系统
  • 可配置化管理后台
  • 需要频繁变更的表单业务

一、React版本

功能说明

后端驱动:从接口获取表单配置
字段类型支持:文本框、下拉框、日期选择、复选框
动态增删:可添加、删除字段
数据校验:必填、格式校验
实时数据预览:表单数据展示

完整React代码

import React, { useState, useEffect } from "react";
import { Form, Input, Select, DatePicker, Checkbox, Button } from "antd";
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
import dayjs from "dayjs";

const fetchFormConfig = async () => {
  return [
    { label: "姓名", type: "input", key: "name", required: true },
    { label: "性别", type: "select", key: "gender", options: ["男", "女"] },
    { label: "出生日期", type: "date", key: "birthDate" },
    { label: "爱好", type: "checkbox", key: "hobbies", options: ["阅读", "旅行", "运动"] },
    { label: "职业", type: "select", key: "job", options: ["工程师", "设计师", "教师"] },
    { label: "公司名称", type: "input", key: "company", dependsOn: "job", validation: (value, form) => !!form.getFieldValue("job") || "请先选择职业" },
    { label: "工作年限", type: "input", key: "experience", dependsOn: "job", validation: (value) => (Number(value) > 0 && Number(value) <= 50) || "工作年限必须在 1-50 之间" },
  ];
};

const fetchRemoteOptions = async () => {
  return ["工程师", "设计师", "教师", "医生", "律师"];
};

const LowCodeForm = () => {
  const [formConfig, setFormConfig] = useState([]);
  const [jobOptions, setJobOptions] = useState([]);
  const [form] = Form.useForm();
  const job = Form.useWatch("job", form);

  useEffect(() => {
    fetchFormConfig().then(setFormConfig);
    fetchRemoteOptions().then(setJobOptions);
  }, []);

  return (
    <div style={{ maxWidth: 600, margin: "0 auto" }}>
      <Form form={form} layout="vertical">
        {formConfig.map(({ label, type, key, options, required, dependsOn, validation }) => (
          (!dependsOn || (dependsOn && form.getFieldValue(dependsOn))) && (
            <Form.Item 
              key={key} 
              label={label} 
              name={key} 
              rules={[{ required, message: `${label} 必填` }, validation && { validator: (_, value) => validation(value, form) ? Promise.resolve() : Promise.reject(validation(value, form)) }].filter(Boolean)}
            > 
              {type === "input" && <Input placeholder={`请输入${label}`} />}
              {type === "select" && (
                <Select 
                  options={(key === "job" ? jobOptions : options).map(o => ({ value: o, label: o }))} 
                  placeholder="请选择" 
                />
              )}
              {type === "date" && <DatePicker style={{ width: "100%" }} />}
              {type === "checkbox" && <Checkbox.Group options={options} />}
            </Form.Item>
          )
        ))}
        
        {/* 动态字段 */}
        <Form.List name="experiences">
          {(fields, { add, remove }) => (
            <>
              {fields.map(({ key, name }) => (
                <Form.Item key={key} label={`工作经历 ${name + 1}`}>
                  <Input placeholder="请输入工作经历" style={{ width: "80%" }} />
                  <MinusCircleOutlined onClick={() => remove(name)} style={{ marginLeft: 8 }} />
                </Form.Item>
              ))}
              <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>添加工作经历</Button>
            </>
          )}
        </Form.List>

        <Button type="primary" onClick={() => console.log(form.getFieldsValue())}>提交</Button>
      </Form>
    </div>
  );
};

export default LowCodeForm;

功能细节

  1. 后端驱动fetchFormConfig 模拟后端 API 获取表单配置。

  2. 动态渲染:遍历 formConfig 生成输入框、下拉框、日期选择等。

  3. 支持动态增删字段:可以动态添加/删除“工作经历”字段。

  4. 数据提交:点击“提交”按钮,在控制台打印当前表单数据。

联动逻辑: "公司名称" 字段仅在选择了 "职业" 后显示。

远程数据源: "职业" 选项从远程接口 fetchRemoteOptions 获取,而不是写死在前端。

复杂校验: 必填校验:如果字段 required: true,则必须填写,否则报错。

动态校验规则

  • "公司名称" 只有在选择了 "职业" 后才能填写,否则报错。
  • "工作年限" 需在 1-50 年之间,否则提示错误。

条件字段控制: "公司名称" 和 "工作年限" 只有在选择职业后才会显示。

数据联动计算(可扩展): 目前的规则基础上,可以进一步添加动态计算字段,比如:根据工作年限自动计算退休年龄

二、Vue版本

功能说明

动态校验规则(如工作年限限制)
条件字段控制(如选择职业后才显示公司名称)
数据联动计算(如动态获取职业选项)
动态字段管理(如添加/删除工作经历)

Vue3低代码动态表单Demo

<template>
  <div style="max-width: 600px; margin: auto;">
    <el-form :model="form" label-width="100px" ref="formRef">
      <template v-for="item in filteredFormConfig" :key="item.key">
        <el-form-item :label="item.label" :prop="item.key" :rules="getValidationRules(item)">
          <el-input v-if="item.type === 'input'" v-model="form[item.key]" :placeholder="`请输入${item.label}`" />
          <el-select v-else-if="item.type === 'select'" v-model="form[item.key]" placeholder="请选择">
            <el-option v-for="option in (item.key === 'job' ? jobOptions : item.options)" :key="option" :label="option" :value="option" />
          </el-select>
          <el-date-picker v-else-if="item.type === 'date'" v-model="form[item.key]" type="date" style="width: 100%;" />
          <el-checkbox-group v-else-if="item.type === 'checkbox'" v-model="form[item.key]">
            <el-checkbox v-for="option in item.options" :key="option" :label="option" />
          </el-checkbox-group>
        </el-form-item>
      </template>
      
      <!-- 动态字段 -->
      <el-form-item label="工作经历">
        <div v-for="(exp, index) in form.experiences" :key="index" style="display: flex; align-items: center;">
          <el-input v-model="form.experiences[index]" placeholder="请输入工作经历" style="width: 80%;" />
          <el-button type="danger" icon="el-icon-minus" @click="removeExperience(index)" />
        </div>
        <el-button type="dashed" icon="el-icon-plus" @click="addExperience">添加工作经历</el-button>
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="submitForm">提交</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { ElMessage } from 'element-plus';

const formRef = ref(null);
const form = ref({ experiences: [] });
const jobOptions = ref([]);
const formConfig = ref([]);

const fetchFormConfig = async () => {
  return [
    { label: "姓名", type: "input", key: "name", required: true },
    { label: "性别", type: "select", key: "gender", options: ["男", "女"] },
    { label: "出生日期", type: "date", key: "birthDate" },
    { label: "爱好", type: "checkbox", key: "hobbies", options: ["阅读", "旅行", "运动"] },
    { label: "职业", type: "select", key: "job", options: [] },
    { label: "公司名称", type: "input", key: "company", dependsOn: "job" },
    { label: "工作年限", type: "input", key: "experience", dependsOn: "job", validation: (value) => Number(value) > 0 && Number(value) <= 50 || "工作年限必须在 1-50 之间" },
  ];
};

const fetchRemoteOptions = async () => {
  return ["工程师", "设计师", "教师", "医生", "律师"];
};

onMounted(async () => {
  formConfig.value = await fetchFormConfig();
  jobOptions.value = await fetchRemoteOptions();
});

const filteredFormConfig = computed(() => {
  return formConfig.value.filter(item => !item.dependsOn || form.value[item.dependsOn]);
});

const getValidationRules = (item) => {
  const rules = [];
  if (item.required) {
    rules.push({ required: true, message: `${item.label} 必填` });
  }
  if (item.validation) {
    rules.push({ validator: (_, value, callback) => {
      if (item.validation(value)) {
        callback();
      } else {
        callback(new Error(item.validation(value)));
      }
    }});
  }
  return rules;
};

const addExperience = () => {
  form.value.experiences.push('');
};

const removeExperience = (index) => {
  form.value.experiences.splice(index, 1);
};

const submitForm = async () => {
  try {
    await formRef.value.validate();
    console.log(form.value);
    ElMessage.success("提交成功!");
  } catch (error) {
    ElMessage.error("请完善表单信息");
  }
};
</script>

<style scoped>
.el-button {
  margin-top: 10px;
}
</style>

Vue优化版本

优化 Vue3 代码,采用 computed 计算字段显示,并 使用动态组件 来减少 if 判断。
✅ 逻辑更清晰
✅ 易扩展,可自由增删字段

<template>
  <div style="max-width: 600px; margin: auto;">
    <el-form :model="form" label-width="100px" ref="formRef">
      <el-form-item v-for="item in visibleFields" :key="item.key" :label="item.label" :prop="item.key" :rules="getValidationRules(item)">
        <component :is="fieldTypes[item.type]" v-model="form[item.key]" v-bind="item.props">
          <el-option v-for="option in getOptions(item)" :key="option" :label="option" :value="option" />
        </component>
      </el-form-item>
      
      <!-- 动态字段 -->
      <el-form-item label="工作经历">
        <div v-for="(exp, index) in form.experiences" :key="index" style="display: flex; align-items: center;">
          <el-input v-model="form.experiences[index]" placeholder="请输入工作经历" style="width: 80%;" />
          <el-button type="danger" icon="el-icon-minus" @click="removeExperience(index)" />
        </div>
        <el-button type="dashed" icon="el-icon-plus" @click="addExperience">添加工作经历</el-button>
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="submitForm">提交</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { ElMessage } from 'element-plus';

const formRef = ref(null);
const form = ref({ experiences: [] });
const jobOptions = ref([]);
const formConfig = ref([]);

const fieldTypes = {
  input: "el-input",
  select: "el-select",
  date: "el-date-picker",
  checkbox: "el-checkbox-group",
};

const fetchFormConfig = async () => {
  return [
    { label: "姓名", type: "input", key: "name", required: true },
    { label: "性别", type: "select", key: "gender", options: ["男", "女"] },
    { label: "出生日期", type: "date", key: "birthDate" },
    { label: "爱好", type: "checkbox", key: "hobbies", options: ["阅读", "旅行", "运动"] },
    { label: "职业", type: "select", key: "job", options: [] },
    { label: "公司名称", type: "input", key: "company", dependsOn: "job" },
    { label: "工作年限", type: "input", key: "experience", dependsOn: "job", validation: (value) => Number(value) > 0 && Number(value) <= 50 || "工作年限必须在 1-50 之间" },
  ];
};

const fetchRemoteOptions = async () => {
  return ["工程师", "设计师", "教师", "医生", "律师"];
};

onMounted(async () => {
  formConfig.value = await fetchFormConfig();
  jobOptions.value = await fetchRemoteOptions();
});

const visibleFields = computed(() => {
  return formConfig.value.filter(item => !item.dependsOn || form.value[item.dependsOn]);
});

const getValidationRules = (item) => {
  const rules = [];
  if (item.required) {
    rules.push({ required: true, message: `${item.label} 必填` });
  }
  if (item.validation) {
    rules.push({ validator: (_, value, callback) => {
      if (item.validation(value)) {
        callback();
      } else {
        callback(new Error(item.validation(value)));
      }
    }});
  }
  return rules;
};

const getOptions = (item) => {
  return item.key === "job" ? jobOptions.value : item.options;
};

const addExperience = () => {
  form.value.experiences.push('');
};

const removeExperience = (index) => {
  form.value.experiences.splice(index, 1);
};

const submitForm = async () => {
  try {
    await formRef.value.validate();
    console.log(form.value);
    ElMessage.success("提交成功!");
  } catch (error) {
    ElMessage.error("请完善表单信息");
  }
};
</script>

<style scoped>
.el-button {
  margin-top: 10px;
}
</style>