后台管理系统-通用配置化表单

2,395 阅读2分钟

我正在参加「掘金·启航计划」

背景

在我近期的后台管理系统相关的业务需求开发中,非常多的表单需求,各个表单之间极其雷同,可以考虑通用配置化表单。

通用配置化表单优点:

  1. 快速构建不同类型的表单,满足不同场景下的需求,提高工作效率。
  2. 良好的扩展性和可维护性。基于统一的配置模板和组件,可以方便地进行维护和更新,适应业务的发展和变化。

如果您的业务需要构建表单,你想使用通用配置化表单,请您驻足继续往下浏览与审视✌️

技术框架:vue3+ElementUI+TypeScript

通用表单的具体实现

image.png

功能

  1. 通过一份配置生成表单,除了可以渲染elementUI的下拉、输入等组件,还可以渲染二次封装的组件。
  2. 表单值的改变可以影响另一项的显示隐藏,减少v-if、v-show的逻辑编写。
  3. 表单值的改变可以影响另一项的值的改变,减少@change的。
  4. 表单值的改变可以清空另一项的值,解决联动项需要清空其他项的问题。
  5. 配置:不同类型的配置使用TypeScript进行静态代码检查。

实现方式

  1. 配置表的定义,读取配置表,根据不同类型渲染不同类型的组件。
  2. 根据配置生成一份数据和一份初始化数据(用来重置或者清除筛选项)
  3. 处理表单显示隐藏(联动单个值、多个值、或自定义)
  4. 清空相关联动项(包括关联项身上绑定的其他关联项的清空)

具体交互如下图:

屏幕录制2023-05-11 18.gif

通用表单公共组件

封装核心组件

  • typeToComponent()根据类型渲染组件
  • typeToPropsData()根据类型渲染props
  • typeToComEvents()根据类型返回监听事件

通过动态组件的方式将组件引用进来,就可以根据类型渲染不同的组件,实现不同内容的渲染与监听。

// BaseConfigForm.vue
<template>
  // props.configItem为当前项的值,props.config.value为解决表单多层级使用
  <component
    :is="typeToComponent(props.configItem)"
    v-bind="typeToPropsData(props.configItem,props.config.value)"
    v-on="typeToComEvents(props.configItem,props.config.value)" />
</template>
<script lang="ts" setup>
  import { defineProps, defineEmits } from "vue";
  import BaseSelect from "@/components/BaseSelect.vue";
  import RadioGroup from "@/components/RadioGroup.vue";
  import CheckboxGroup from "@/components/CheckboxGroup.vue";
  import InputWildCard from "@/components/InputWildCard.vue";
  import DateTimePicker from "@/components/DateTimePicker.vue";
  import SelectInput from "@/components/SelectInput.vue";
  // ......引入组件,定义自己的组件增加
  
  // TypeScript的类型校验
  import { Field } from "./type";

  const props = defineProps(["configItem", "config", "customForm", "optionDataMap"]);
  const emit = defineEmits(["clearValues"]);
  
  // 根据类型渲染组件
  const typeToComponent = (field: Field): any => {
    switch (field.type) {
    case "select":
      return BaseSelect;
    case "switch":
      return "el-switch";
    case "radioGroup":
      return RadioGroup;
    case "checkboxGroup":
      return CheckboxGroup;
    case "inputwildcard":
      return InputWildCard;
    case "datetimepicker":
      return DateTimePicker;
    case "selectinput":
      return SelectInput;
    default:
      return "el-input";
    }
  };
  // 根据类型渲染props
  const typeToPropsData = (field: Field, configValue: string): any => {
    switch (field.type) {
    case "select":
      return {
        type: field.selectType,
        params: field.params,
        placeholder: field.placeholder || field.label,
        filterable: field.filterable || true,
        clearable: field.clearable || true,
        multiple: field.multiple || false,
        multipleLimit: field.multipleLimit,
        disabled: field.disabled || false,
        optionList: field.optionsList
      };
    case "switch":
      return {
        "active-value": field["active-value"],
        "inactive-value": field["inactive-value"],
        disabled: field.disabled || false
      };
    case "radioGroup":
      return {
        radioType: field.radioType,
        radioList: field.optionsList,
        disabledOptions: field.disabledOptions
      };
    case "checkboxGroup":
      return {
        checkboxList: field.optionsList,
        disabledOptions: field.disabledOptions
      };
    case "inputwildcard":
      return {
        params: field.params
      };
    case "datetimepicker":
      return {
        bidTimeStr: field.value
      };
    case "selectinput":
      return {
        selectValue: handleConfigValue(configValue, field.selectformKey),
        placeholder: field.placeholder,
        selectPlaceholder: field.selectPlaceholder,
        optionsList: field.optionsList,
        disabled: field.disabled || false,
        disabledSelect: field.disabledSelect || false
      };
    default:
      return {
        clearable: field.clearable || true,
        disabled: field.disabled || false,
        placeholder: field.placeholder || field.label
      };
    }
  };
  // 根据类型返回监听事件
  const typeToComEvents = (field: Field, configValue: string): any => {
    switch (field.type) {
    case "select":
      return {
        change: (val: any) => {
          // 如果存在联动项,清空相关联动项
          clearValues(field, configValue);
          if (field.change) {
            return field.change(val);
          }
        },
        optionData: (val: any) => {
          props.optionDataMap[field.formKey] = val;
        }
      };
    case "switch":
      return {
        change: (val: any) => {
          // 如果存在联动项,清空相关联动项
          clearValues(field, configValue);
          if (field.change) {
            return field.change(val);
          }
        }
      };
    case "radioGroup":
      return {
        change: (val: any) => {
          // 如果存在联动项,清空相关联动项
          clearValues(field, configValue);
          if (field.change) {
            return field.change(val);
          }
        }
      };
    case "checkboxGroup":
      return {
        change: (val: any) => {
          // 如果存在联动项,清空相关联动项
          clearValues(field, configValue);
          if (field.change) {
            return field.change(val);
          }
        }
      };
    case "datetimepicker":
      return {
        getDateFn: (val: any) => {
          console.log(field.label, val);
          // 如果存在联动项,清空相关联动项
          clearValues(field, configValue);
          if (field.change) {
            return field.change(val);
          }
        }
      };
    case "selectinput":
      return {
        "update:selectValue": (val: string | number) => {
          handleSetConfigValue(configValue, field.selectformKey, val);
        },
        change: (inputValue: any, selectValue:any) => {
          if (field.change) {
            return field.change(inputValue, selectValue);
          }
        },
        selectChange: (val:any) => {
          if (field.selectChange) {
            return field.selectChange(val);
          }
        }
      };
    default:
      return {
        change: (val: any) => {
          // 如果存在联动项,清空相关联动项
          clearValues(field, configValue);
          if (field.change) {
            return field.change(val);
          }
        }
      };
    }
  };
  // 清空相关联动项
  const clearValues = (field: Field, configValue: string) => {
    emit("clearValues", field, configValue);
  };
  // 处理表单值的绑定
  const handleConfigValue = (configValue: string, selectformKey:string) => {
    if (configValue) {
      return props.customForm[configValue][selectformKey];
    } else {
      return props.customForm[selectformKey];
    }
  };
  // 处理表单值的绑定
  const handleSetConfigValue = (configValue: string, selectformKey:string, val: any) => {
    if (configValue) {
      props.customForm[configValue][selectformKey] = val;
    } else {
      props.customForm[selectformKey] = val;
    }
  };
  </script>

配置文件示例解读

下列代码定义渲染了一个输入框和一个开关,当开关启用的时候,输入框展示。当开关值改变的时候,清空输入框的内容。

  const customConfigs = reactive<Array<Field>>([
    {
      type: "el-input", // 类型定义
      formKey: "taskName", // 对应表单字段
      label: "任务名称", // 对应表单名称
      value: "", // 表单默认值
      showOn: {
        taskStatus: "1" // 任务状态开启时候展示
      },
      rules: [{ required: true, message: "此项为必填项", trigger: ["blur"] }] // 校验规则
    },
    {
      type: "switch",
      formKey: "taskStatus",
      label: "任务状态",
      value: "1",
      "active-value": "1",
      "inactive-value": "0",
      clearItems: ["taskName"], // 值改变清除任务名称
      change: (val: any) => { // 监听表单值改变
        console.log(val);
      }
    }
  ]);

配置表单具体使用

<template>
  <div style="padding:16px;text-align: left;">
    <el-form
      ref="ruleFormRef"
      :model="customForm"
      label-width="120px">
      <div
        v-for="(item,itemIndex) in customConfigs"
        :key="itemIndex">
        <div v-if="showField(item)">
          <el-form-item
            :label="item.label"
            :prop="`${item.formKey}`"
            :rules="item.rules">
            <!-- 使用通用配置表单 -->
            <BaseConfigForm
              v-model:modelValue="customForm[item.formKey]"
              :config-item="item"
              :config="customConfigs"
              :custom-form="customForm"
              :option-data-map="optionDataMap"
              @clearValues="clearValues" />
          </el-form-item>
        </div>
      </div>
      <el-form-item>
        <el-button
          type="primary"
          @click="submitForm(ruleFormRef)">
          创 建
        </el-button>
        <el-button @click="resetForm(ruleFormRef)">Reset</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script lang="ts" setup>
  import { reactive, ref, computed } from "vue";
  import type { FormInstance } from "element-plus";
  // 引入通用配置表单
  import BaseConfigForm from "./BaseConfigForm.vue";
  // 引入TS
  import { Field } from "./type";

  // 配置表单
  const customConfigs = reactive<Array<Field>>([
    {
      type: "select",
      formKey: "taskId",
      label: "任务下拉选择",
      value: "",
      optionsList: [
        { label: "option1", value: "option1" },
        { label: "option2", value: "option2" },
        { label: "option3", value: "option3" }
      ],
      change: (val:any) => {
        console.log(val);
      },
      rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
    },
    {
      type: "el-input",
      formKey: "taskName",
      label: "任务名称",
      value: "",
      change: (val:any) => {
        console.log(val);
      },
      rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
    },
    {
      type: "switch",
      formKey: "taskStatus",
      label: "任务状态",
      value: "1",
      "active-value": "1",
      "inactive-value": "0",
      clearItems: ["taskBudget", "taskRank"], // 清除任务难度和任务预算
      change: (val: any) => {
        console.log(val);
      },
      rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
    },
    {
      type: "radioGroup",
      label: "任务难度",
      formKey: "taskRank",
      value: 1,
      optionsList: [
        { label: "简单", value: 1 },
        { label: "困难", value: 2 },
        { label: "非常困难", value: 3 }
      ],
      change: (val: any) => {
        console.log(val);
      },
      rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
    },
    {
      type: "checkboxGroup",
      label: "任务分类",
      formKey: "taskType",
      value: [2],
      optionsList: [
        { label: "前端", value: 1 },
        { label: "后端", value: 2 },
        { label: "产品", value: 3 }
      ],
      change: (val: any) => {
        console.log(val);
      },
      rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
    },
    {
      type: "datetimepicker",
      label: "任务时间",
      formKey: "taskTime",
      value: "",
      change: (val: any) => {
        console.log(val);
      }
    },
    {
      type: "inputwildcard",
      label: "任务规则名称",
      formKey: "taskRuleName",
      value: "",
      rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
    },
    {
      type: "selectinput",
      label: "任务预算",
      formKey: "taskBudget",
      placeholder: "请输入预算金额",
      value: "",
      selectformKey: "taskBudgetType",
      selectPlaceholder: "预算类型",
      selectValue: 1,
      optionsList: [
        { label: "日预算", value: 1 },
        { label: "总预算", value: 2 }
      ],
      disabled: false,
      disabledSelect: false,
      selectChange: (val:any) => {
        console.log("selectChange", val);
      },
      showOn: {
        taskStatus: "1" // 任务状态开启时候展示
      }
    }
  ]);

  //  根据配置生成一份数据和一份初始化数据(用来重置或者清除筛选项)
  const customForm = ref({} as any);
  const customInitForm = {};
  const optionDataMap = reactive({} as any); // 存放所有的枚举值
  customConfigs.forEach(async (item: any) => {
    console.log("item", item);
    if (item.formKey) {
      customForm.value[item.formKey] = item.value;
      customInitForm[item.formKey] = item.value;
    }
    if (item.selectformKey) {
      customForm.value[item.selectformKey] = item.selectValue;
      customInitForm[item.selectformKey] = item.selectValue;
    }
    if (item.optionsList) {
      optionDataMap[item.formKey] = item.optionsList;
    }
  });

  // watch(customForm, (val) => {
  //   console.log("customForm", val);
  // }, { deep: true });

  const ruleFormRef = ref<FormInstance>();
  // 表单提交
  const submitForm = async (formEl: FormInstance | undefined) => {
    if (!formEl) return;
    await formEl.validate((valid, fields) => {
      if (valid) {
        console.log("submit!");
      } else {
        console.log("error submit!", fields);
      }
    });
  };
  // 表单重置
  const resetForm = (formEl: FormInstance | undefined) => {
    if (!formEl) return;
    formEl.resetFields();
  };

  //   根据formKey查找配置
  const findConfig = (formKey: string) => {
    let formItem = {} as Field;
    customConfigs.forEach((item: any) => {
      if (item.formKey === formKey) {
        formItem = item;
      }
    });
    return { formItem };
  };

  // 处理表单显示隐藏
  const showField = (field: any): any => {
    // 一、关联单个值的显示隐藏
    // extBudget: 1 // 单个值,满足条件即展示。表示当extBudget === 1时,展示
    // extBudget: [1, 2] // 数组,||或的关系。表示当extBudget === 1 || extBudget === 2时,展示
    // extBudget: { extra: "!", value: 1 } // 对象,!非的关系。表示当extBudget !==1 时,展示
    // extBudget: { extra: "!", value: [1, 2] } // 对象,!非的关系。表示当 !(extBudget === 1 || extBudget === 2) 时,展示

    // 二、关联多个值的显示隐藏
    // 1.默认&&的关系,表示当 (extBudget===1 && nameTemplateV2 === '222') 时,展示
    // extBudget: 1,
    // nameTemplateV2: "2222"
    // 2.属性前有||表或的关系,表示当 (extBudget===1 || nameTemplateV2 === '222') 时,展示
    // extBudget: 1,
    // "||nameTemplateV2": "222"

    // 三、自定义的显示隐藏
    // cusShow: () => {
    //   return false;
    // }
    if (field.showOn) {
      // 自定义的显示隐藏
      if (field.showOn.cusShow) {
        return field.showOn.cusShow();
      }
      // 关联单个值的显示隐藏
      if (Object.keys(field.showOn).length === 1) {
        const key = Object.keys(field.showOn)[0];
        const definedValue = field.showOn[key];
        const relateValue = customForm.value[key];

        // 判断值的类型
        if (typeof definedValue === "object") {
          if (definedValue.extra === "!") {
            if (Array.isArray(definedValue.value)) {
              return definedValue.value.includes(relateValue);
            }
            return definedValue.value === relateValue;
          }
        }
        if (Array.isArray(definedValue)) {
          return definedValue.includes(relateValue);
        }
        return definedValue === relateValue;
      }
      // todo: 关联多个值的显示隐藏
      // if (Object.keys(field.showOn).length > 1) {}
    }
    return true;
  };

  // 清空相关联动项
  const clearValues = (field: Field) => {
    if (field.clearItems && field.clearItems.length) {
      field.clearItems.forEach((itemKey: any) => {
        if (typeof itemKey === "string") {
          customForm.value[itemKey] = customInitForm[itemKey];
          // 根据当前清空项itemKey,在配置中customConfigs查看是否还有关联的清空项
          const { formItem } = findConfig(itemKey);
          if (formItem && formItem.change) formItem.change(customForm.value[itemKey]);
          if (formItem) clearValues(formItem);
        }
      });
    }
  };
</script>