基于vue+element的动态表单的设计与实现(1)

5 篇文章 0 订阅

动态表单的设计与实现

备注:细节可能有误,主要提供思路

实现功能

目前主要实现了以下4个功能

  1. 表单的可配置化;
  2. 具体字段的实时监听(观察者模式);
  3. 一个字段控制另一个字段的是否可编辑(发布订阅模式);
  4. 支持三种模式:新建、更新、详情

基础结构的设计

首先定义我们需要解析的json配置,基础结构如下:

[
  {
    groupNo: "basic",
    groupName: "基本信息",
    fields: [
      {
        component: "el-input",
        fieldId: "name",
        name: "姓名",
        default: "",	// 默认值
        display: 1,		// 是否展示
        required: 1,	// 是否必填
        edit: 1,		// 是否可编辑
        minLength: 5,	// 最小长度
        maxLength: 10	// 最大长度
      },
      {
        component: "el-select",
        fieldId: "country",
        name: "国家",
        display: 1,
        required: 1,
        edit: 1,
        default: "",
        multiple: false,	// 是否多选
        remote: false,		// 是否远程搜索
        options: [
          {
            label: "中国",
            value: "CN"
          },
          {
            label: "美国",
            value: "US"
          },
          {
            label: "日本",
            value: "JPN"
          }
        ],
      },
      {
        component: "el-radio",
        fieldId: "sex",
        name: "性别",
        display: 1,
        required: 1,
        edit: 1,
        default: 0,
        remote: false,
        options: [
          {
            label: "男",
            value: 0
          },
          {
            label: "女",
            value: 1
          },
        ],
      },
      {
        component: "el-checkbox",
        fieldId: "education",
        name: "学历",
        display: 1,
        required: 1,
        edit: 1,
        default: 1,
        remote: false,
        options: [
          {
            label: "大专",
            value: 0
          },
          {
            label: "本科",
            value: 1
          },
          {
            label: "硕士",
            value: 2
          },
          {
            label: "博士",
            value: 3
          },
        ],
      }
    ]
  }
]

代码实现

依照该配置,我们将整个表单组件的代码结构设计为:
入口文件:Index.vue
表单分组:GroupItem.vue
表单单元:FormItem.vue
最基础的实现如下:
Index.vue:

<template>
  <el-collapse v-model="activeNames">
    <GroupItem
      v-for="tplItem in tplList"
      :ref="tplItem.groupNo"
      :key="tplItem.groupName"
      :tpl="tplItem"
      :form-mode="formMode"
      :form-data="formData"
      @change="groupFormDataChange($event, tplItem)"
    />
  </el-collapse>
</template>
<script>
import GroupItem from "./components/GroupItem.vue";
export default {
  components: {
    GroupItem,
  },
  props: {
    formMode: {
      type: String,
      default: "create", // create,update,detail
    },
    formConfig: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      formData: {},
      tplList: [],
      activeNames: [],
    };
  },
  created() {
    this.tplList = this.formConfig;
  },
  methods: {
  	/**
     * 组件表单数据变化
     */
    groupFormDataChange(formData, groupTpl) {
      this.formData[groupTpl.groupNo] = formData;
    },
  }
};
</script>

GroupItem.vue:

<template>
  <el-collapse-item :title="tpl.groupName" :name="tpl.groupName">
    <el-form
      ref="hclForm"
      :model="groupFormData"
      :rules="rules"
      :inline="true"
      :validate-on-rule-change="false"
      size="small"
      label-width="180px"
      class="hcl-group-form"
    >
      <template v-for="fieldItem in tpl.fields">
        <el-form-item
          v-if="fieldItem.display"
          :key="fieldItem.fieldId"
          :ref="fieldItem.fieldId"
          :prop="fieldItem.fieldId"
          :label="fieldItem.name"
          class="hcl-el-form-parse"
        >
          <FormItem
            v-model="groupFormData[fieldItem.fieldId]"
            :item="fieldItem"
            :form-data="formData"
            :form-type="formType"
            :tpl="tpl"
            @change="handleFormChange"
          />
        </el-form-item>
      </template>
    </el-form>
  </el-collapse-item>
</template>
<script>
import FormItem from "./FormItem.vue";
export default {
  components: {
    FormItem
  },
  props: {
    tpl: {
      type: Object,
      default: () => ({}),
    },
    formData: {
      type: Object,
      default: () => ({}),
    },
    formType: {
      type: String,
      default: "create",
    },
  },
  data() {
    return {
      groupFormData: {},
      rules: {},
    };
  },
  methods: {
    handleFormChange() {

    }
  }
};
</script>

FormItem.vue:

<template>
  <el-input
    v-if="item.component === 'el-input'"
    v-model="inpValue"
    clearable
    :maxlength="inpMaxLength"
    :type="item.inputType || 'text'"
    :placeholder="placeholder"
    :disabled="inpDisabled"
    @change="elInputChange"
  ></el-input>
  <el-select
    v-else-if="item.component === 'el-select'"
    v-model="inpValue"
    clearable
    collapse-tags
    :placeholder="placeholder"
    :multiple="item.multiple"
    :filterable="item.filterable"
    :remote="item.remote"
    :remote-method="selectRemoteEvt"
    :disabled="inpDisabled"
    @change="elSelectChange"
  >
    <el-option
      v-for="(optionItem, index) in optionList"
      :key="`${optionItem.value}_${index}`"
      :label="optionItem.label"
      :value="optionItem.value"
    />
  </el-select>
  <el-radio-group
    v-else-if="item.component === 'el-radio'"
    v-model="inpValue"
    :disabled="inpDisabled"
    @change="elRadioChange"
  >
    <el-radio
      v-for="(radioItem, index) in optionList"
      :key="`${radioItem.value}_${index}`"
      :label="radioItem.value"
    >
      {{ radioItem.label }}
    </el-radio>
  </el-radio-group>
  <el-checkbox-group
    v-else-if="item.component === 'el-checkbox'"
    v-model="inpValue"
    :disabled="inpDisabled"
    @change="elCheckBoxChange"
  >
    <el-checkbox
      v-for="(checkboxItem, index) in optionList"
      :key="`${checkboxItem.value}_${index}`"
      :label="checkboxItem.label"
    >
      {{ checkboxItem.label }}
    </el-checkbox>
  </el-checkbox-group>
  <el-date-picker
    v-else-if="item.component === 'el-date-picker'"
    v-model="inpValue"
    type="date"
    value-format="yyyy-MM-dd"
    :placeholder="placeholder"
    :readonly="inpReadonly"
    :disabled="inpDisabled"
    :picker-options="item.pickerOptions ? item.pickerOptions : {}"
    @change="elInputChange"
  />
</template>
<script>
export default {
  name: "",
  props: {
    formData: {
      type: Object,
      default: () => ({}),
    },
    item: {
      type: Object,
      default: () => ({}),
    },
    tpl: {
      type: Object,
      default: () => ({}),
    },
    value: null,
  },
  data() {
    return {
      inpValue: null,
      optionList: [],
      defaultInputMaxLength: 20,
      defaultTextareaMaxLength: 100,
      defaultPlaceholder: {
        input: "请输入",
        select: "请选择",
      },
    };
  },
  computed: {
    inpDisabled() {
      return this.item.edit === 0;
    },
    inpMaxLength() {
      if (this.item.maxlength) {
        return this.item.maxlength;
      }
      if (this.item.component === "el-input-textarea") {
        return this.defaultTextareaMaxLength;
      } else {
        return this.defaultInputMaxLength;
      }
    },
    placeholder() {
      if (this.item.component === "el-input") {
        return this.item.placeholder || this.defaultPlaceholder.input;
      }
      return this.item.placeholder || this.defaultPlaceholder.select;
    },
  },
  watch: {
    value: {
      handler(val) {
        this.inpValue = val;
      },
      immediate: true,
    },
    inpValue: {
      handler(val) {
        this.$emit("input", val);
      },
      immediate: true,
    },
  },
  mounted() {
    this.init();
  },
  methods: {
    /**
     * 初始化
     * 默认值和options
     */
    init() {
      if (this.item.default !== undefined) {
        this.inpValue = this.item.default;
      }
      const { component, options } = this.item;
      switch (component) {
        case "el-select":
          if (this.item.multiple) {
            this.inpValue = [];
          }
          this.optionList = options;
          break;
        case "el-radio":
          this.optionList = options;
          break;
        case "el-checkbox":
          this.inpValue = [];
          this.optionList = options;
          break;
      }
    },
    /**
     * select远程搜索
     * 这里根据实际业务自行补调整
     */
    selectRemoteEvt(label) {
      const { searchUrl, searchField } = this.item;
      const params = {};
      if (label) {
        params[searchField] = label;
      }
      request({
        url: searchUrl,
        method: "get",
        params,
      }).then((res) => {
        // 根据实际数据结构进行调整
        this.optionList = res;
      });
    },
    elInputChange(val) {},
    elSelectChange(val) {},
    elRadioChange(val) {},
    elCheckBoxChange(val) {},
  },
};
</script>

调用方法

<template>
  <DynamicForm
    ref="dynamic-form"
    :form-mode="formMode"
    :form-config="formConfig"
  />
</template>
<script>
// 上面Index.vue的路径
import DynamicForm from "@/components/dynamicForm/Index.vue";
// 上面配置的配置文件路径
import formConfig from "./config.js";
export default {
  components: {
    DynamicForm,
  },
  data() {
    return {
      formMode: "create",
      formConfig: formConfig,
    };
  },
};
</script>

到这里,整个动态表单的雏形已经完成了。下面进行对表单验证进行补充
新建目录rulesModel,为不同的表单类型创建规则的模型,这里以el-input为例:

export default function (origin) {
  const {
    required,
    minLength,
    maxLength
  } = origin;
  const rule = [];
  // 读取规则
  if (required === 1) {
    rule.push({
      required: true,
      message: '请输入',
      trigger: 'change',
    });
  }
  if (minLength || maxLength) {
    const ruleItem = {};
    if (minLength) {
      ruleItem.min = minLength;
    }
    if (maxLength) {
      ruleItem.max = maxLength;
    }
    if (minLength && maxLength) {
      ruleItem.message = `长度必须在${minLength}-${maxLength}之间`;
    } else
    if (maxLength) {
      ruleItem.message = `最大长度为${maxLength}`;
    } else
    if (minLength) {
      ruleItem.message = `最小长度为${minLength}`;
    }
    rule.push({
      ...ruleItem,
      trigger: 'change'
    });
  }

  return rule;
};

新建index.js,将所有的模型引入

import elInput from "./input";
import elSelect from "./select";
import elRadio from "./radio";
import elCheckbox from "./checkbox";

export default {
  "el-input": elInput,
  "el-select": elSelect,
  "el-radio": elRadio,
  "el-checkbox": elCheckbox,
}

ok,基本的规则已经创建完成,接下来将规则注入到表单中。
新建form-format.js,添加如下内容,这里对我们的配置文件进行解析处理

import rulesModel from "./rulesModel";
import validate from './utils/validate';
/**
 * 解析表单配置
 */
function format(list = []) {
  if (!Array.isArray(list)) {
    return [];
  }
  const tplList = list.map((i) => {
	const groupItem = { ...i };
  	const {
      fields,
      formData,
      rules,
    } = handleFormFieldList.call(this, i.fields, groupItem);
    groupItem.fields = fields;
    groupItem.$form = {
      formData,
      rules,
    };
    return groupItem;
  });
  return {
    tplList
  }
}
/**
 * 解析配置中的每个字段
 */
function handleFormFieldList(originFields, groupItem) {
	const formData = {};
	const rules = {};
	const fields = originFields.map(o => {
		const fieldItem = { ...o };
		const model = rulesModel[o.component];
		if (!model) {
	      return fieldItem;
	    }
	    rules[o.fieldId] = model.call(this, o);
	    // 为每个分组的字段赋初始值
	    // validate.isSet是用于判断该变量是否设置了值,这里不贴源码了
	    formData[o.fieldId] = validate.isSet(o.default) ? o.default: '';
	    return fieldItem;
	});
	return {
		formData,
		fields,
		rules,
	}
}

export default format;

回到Index.vue,修改以下内容:

import formFormat from "./form-format.js";

created() {
  // this.tplList = this.formConfig;
  this.formatTpls();
},
methods: {
  formatTpls() {
    const { tplList } = formFormat.call(this, this.formConfig);
    this.tplList = tplList;
  }
}

回到GroupItem.vue,新增如下内容,将rules注入

created() {
  this.init();
},
methods: {
	init() {
      if (!this.tpl.$form) {
        return;
      }
      const { formData = {}, rules = {} } = this.tpl.$form;
      this.groupFormData = {
        ...formData,
      };
      this.rules = {
        ...rules,
      };
    },
}

好了,到这里,整个动态表单的基本功能就已经完成了。
下一篇继续分析字段的监听和字段的控制。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值