怎么方便表单维护?

250 阅读3分钟

🙏前言

还在为表单太多而烦恼吗,想要统一修改表单的某一个属性而烦恼吗,想要轻松维护表单?

😇根据element-plus表单进行二次封装

因为表单由各种各样的表单项组合而成的,所以表单项的封装也必不可少,我这里表单项是通过表单类型配置去处理的

以下代码是通过element-plus + vue3 + ts封装的

  • types.ts
/**
 * 表单配置对象类型
 */
export type FormObjType = {
  [key: string]: FormObjItemType
};

/**
 * 表单项类型
 */
export type FormObjItemType = {
  /**
   * 文本
   * @default ''
   * @type String
   */
  label?: String;

  /**
   * 组件类型
   * @default undefined
   * @type ComponentTypes
   */
  type?: ComponentTypes;

  /**
   * 选项数组
   * @default undefined
   * @type optionsType[]
   */
  options?: optionsType[];
}

表单项的组件类型我目前只加了几个类型,类型可以根据需求扩展

/**
 * 组件类型
 */
export type ComponentTypes = 'input' | 'select' 

那有同学就有疑问了,那表单输出的参数值怎么存储,在这里我是通过配置的value字段去存储的

/**
 * 表单项类型
 */
export type FormObjItemType = {
  /**
   * 文本
   * @default ''
   * @type String
   */
  label?: String;

  /**
   * 组件类型
   * @default undefined
   * @type ComponentTypes
   */
  type?: ComponentTypes;

  /**
   * 选项数组
   * @default undefined
   * @type optionsType[]
   */
  options?: optionsType[];

  /**
   * 值
   * @default null
   * @type any
   */
  value?: any;
}

例如select类型的选项的属性是根据以下对象去处理的

/**
 * 选项类型
 */
export type optionsType = {
  /**
   * 文本
   * @type String | Number
   */
  label: String | Number

  /**
   * 值
   * @type String | Number
   */
  value: String | Number | Boolean

  /**
   * 是否禁用
   * @type Boolean
   */
  disabled?: Boolean
}

🌰以下封装表单组件引入实现示例

<template>
    <div>
        <base-form
            :form-data="formConfig"
            :submit-request="onRequest"
            @cancel="onCancel"
            @confirm="onConfirm"
        />
    </div>
</template>
<script lang="ts">
import { FormObjType } from '@/components/ReForm/types';
import { BaseForm } from '@/components/ReForm';

export default defineComponent({
    components: {
        BaseForm
    },
    setup() {
        const formConfig: FormObjType = {
            name: {
                label: '名称',
                type: 'input',
                value: '',
            },
            project: {
                label: '项目',
                type: 'select',
                options: [
                    {
                        label: '项目1',
                        value: 1,
                    },
                    {
                        label: '项目2',
                        value: 2,
                    },
                    {
                        label: '项目3',
                        value: 3,
                    }
                ]
                value: null
            }
        }

        /**
         * 异步提交操作
         * @params params 这个参数是提交表单处理后的数据
         */
        function onRequest(params:Object) {
            // 如果要做异步操作就return出去一个异步方法
            return submitApi(params);
        }

        /** 取消表单事件 */
        function onCancel() {
            console.log('取消成功回调');
        }

        /** 表单提交成功之后回调 */
        function onConfirm() {
            console.log('确定提交成功回调');
        }

        return {
            formConfig,
            onRequest,
            onCancel,
            onConfirm
        }
    }
})

</script>

onRequest方法是用于异步提交的一个方法,接收到的参数是表单组件提交处理成key和值组成的数据,当然你如果只是需要拿到参数不请求接口直接拿走参数即可

{
    name: '我的名字',
    project: 1
}

这样就只需要维护表单配置里面的选项就行,不管是统一新增一个属性还行根据某个表单项进行限制,只修改修改该表单选的默认配置项就行

const formConfig: FormObjType = {
    name: {
        label: '名称',
        type: 'input',
        value: '',
        formItemProps: {
            required: true // 表单项设置为必选
        }
        props: {
            type: 'textarea', // 输入框改为多行输入框
        },
        on: {
            change(value: string) {
                console.log(value);
            },
        },
        children: {

        }
    },
}

  • formItemProps:可以设置el-form-item里面所有的属性,例如修改验证规则和是否必选
  • props:是可以设置type类型里面对应的组件属性
  • on:是可以设置type类型里面对应的组件的事件处理

⁉️如果提交获取到的参数是对象呢,可以用children进行嵌套

const formConfig: FormObjType = {
    extra: {
        value: {},
        children: {
            extra1: {
                label: '额外参数1',
                type: 'input',
                value: ''
            },
            extra2: {
                label: '额外参数2',
                type: 'input',
                value: ''
            }
        }
    }
}

提交输出格式如下:

{
    extra: {
        extra1: '',
        extra2: '',
    }
}

🦩完整表单组件代码参考

  • BaseForm.vue
<template>
  <div>
    <!-- 表单 -->
    <el-scrollbar
      :max-height="maxHeight"
    >
      <el-form
        ref="ruleFormDom"
        :model="formData"
        status-icon
        style="padding-bottom: 10px;"
        v-bind="formProps?formProps:{}"
        @submit.prevent="confirm"
      >
        <base-form-item
          :form-data="formData"
          :default-label-width="defaultLabelWidth"
        />
      </el-form>
    </el-scrollbar>
    <!-- 是否显示按钮 -->
    <div
      v-if="isShowButton"
      class="form-control"
    >
      <el-button @click="cancel">
        取消
      </el-button>
      <el-button
        type="primary"
        :loading="isLoading"
        @click="confirm"
      >
        确定
      </el-button>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, toRefs } from 'vue';
import { ElButton, ElForm, ElMessage } from 'element-plus';
import BaseFormItem from './BaseFormItem.vue';
import { FormObjType, FormParamsType } from '../types';

export default defineComponent({
  components: {
    BaseFormItem,
    ElForm,
    ElButton
  },
  props: {
    // 表单最大高度
    maxHeight: {
      type: [Number, String],
      default: '100%'
    },
    // 表单数据
    formData: {
      type: Object,
      required: true,
    },
    // 是否显示按钮
    isShowButton: {
      type: Boolean,
      required: false,
      default: true
    },
    // 表单属性
    formProps: {
      type: Object,
      required: false,
      default: ()=> {
        return {};
      }
    },
    // 默认文本宽度
    defaultLabelWidth: {
      type: Number,
      required: false,
      default: 80
    },
    // 提交请求
    submitRequest: {
      type: Function,
      default: ()=>{
        return Promise.resolve();
      }
    }
  },
  emits: ['cancel', 'confirm'],
  setup(props, { emit }) {
    const { formData } = toRefs(props);
    const ruleFormDom = ref(null as any);

    /** 取消 */
    function cancel() {
      ruleFormDom.value.resetFields();
      emit('cancel', false);
    }

    /** 确定 */
    async function confirm() {
      let is_succeed = false;

      // 表单验证
      await ruleFormDom.value.validate((valid:Boolean, formRuleList:Object) => {
        is_succeed = ruleFormTips(valid, formRuleList);
      });
      
      if (!is_succeed){
        return;
      }

      const formParams = paramsFormat(formData.value);

      if (formParams !== null) {
        submitData(formParams);
      }
    }

    /** 
     * 参数格式化
     * @param obj 参数配置对象
     */
    function paramsFormat(obj:Object):FormParamsType | null {
      let formParams:FormParamsType = {};

      for (const key in obj) {
        if (obj[key]) {
          const item:FormObjType = obj[key];
          const paramsValue = item.value;
          if (typeof paramsValue !== 'undefined' && paramsValue !== null) {
            if (item.children) {
              formParams[key] = paramsFormat(item.children);
            } else {
              formParams[key] = item.value;
            }
          }
        }
      }

      if (JSON.stringify(formParams) === '{}') {
        formParams = undefined;
      }
      return formParams;
    }

    const isLoading = ref(false);

    /**
     * 提交数据
     * @param params 提交参数
     */
    function submitData(params:Object = {}) {
      isLoading.value = true;
      if (props.submitRequest) {
        props.submitRequest(params).then(()=>{
          isLoading.value = false;
          ruleFormDom.value.resetFields();
          emit('confirm', params);   
        }).catch(()=>{
          isLoading.value = false;
        });
      } else {
        emit('confirm', params);
        isLoading.value = false; 
      }
      
    }

    /**
     * 表单验证提示
     * @param valid 是否有效
     * @param formRuleList 表单验证列表
     */
    function ruleFormTips(valid:Boolean, formRuleList:Object) {
      if (valid) {
        return true;
      } else {
        for (const key in formRuleList) {
          if (formRuleList[key]) {
            const item = formRuleList[key][0];
            ElMessage.warning(item.message);
            break;
          }
        }
        return false;
      }
    }
    return {
      ruleFormDom,
      isLoading,
      cancel,
      confirm
    };
  }
});
</script>
<style lang="scss" scoped>
.form-control {
    margin-top: 30px;
    text-align: right;
}
</style>
  • BaseFormItem.vue
<template>
  <div style="display: inline">
    <el-form-item
      v-for="(formItem, formKey) in formData"
      :key="formKey"
      :label="formItem.label"
      :prop="parentKey + formKey + '.value'"
      :label-width="formItem.label ? defaultLabelWidth : 0"
      :rules="formItem.rules"
      v-bind="formItem.formItemProps"
      :style="{ padding: formItem.children ? 0 : null, margin: formItem.children ? 0 : null }"
    >
      <!-- 表单文本提示 -->
      <template
        v-if="formItem.labelTips"
        #label
      >
        {{ formItem.label }}
        <el-tooltip
          effect="dark"
          :content="formItem.labelTips"
          placement="bottom-start"
        >
          <el-icon><question-filled /></el-icon>
        </el-tooltip>
      </template>
      <!-- 文本 -->
      <label
        v-if="formItem.type === 'label'"
        :style="{
          'word-wrap': 'break-word'
        }"
        v-bind="formItem.props"
        v-on="formItem.on ? formItem.on : {}"
      >{{ formItem.value }}</label>
      <!-- 输入框 -->
      <el-input
        v-if="formItem.type === 'input'"
        v-model="formItem.value"
        placeholder="请输入"
        clearable
        v-bind="formItem.props"
        v-on="formItem.on ? formItem.on : {}"
      />
      <!-- 数字输入框 -->
      <el-input-number
        v-if="formItem.type === 'input-number'"
        v-model="formItem.value"
        placeholder="请输入"
        clearable
        v-bind="formItem.props"
        v-on="formItem.on ? formItem.on : {}"
      />
      <!-- 选项框 -->
      <el-select
        v-if="formItem.type === 'select'"
        v-model="formItem.value"
        style="width: 100%"
        placeholder="请选择"
        clearable
        collapse-tags
        v-bind="formItem.props"
        @change="selectChange(formItem)"
        v-on="formItem.on ? formItem.on : {}"
      >
        <div
          v-if="formItem.isAll !== undefined"
          style="padding: 5px 20px"
        >
          <el-checkbox
            v-model="formItem.isAll"
            :indeterminate="formItem.isIndeterminate"
            @change="selectAllChange(formItem.isAll, formItem)"
          >
            全选
          </el-checkbox>
        </div>
        <el-option
          v-for="(item, index) in formItem.options"
          :key="index"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
      <!-- 级联选择框 -->
      <el-cascader
        v-if="formItem.type === 'cascader'"
        v-model="formItem.value"
        style="width: 100%"
        :options="formItem.options"
        clearable
        v-bind="formItem.props"
        v-on="formItem.on ? formItem.on : {}"
      />
      <!-- 日期选择器 -->
      <el-date-picker
        v-if="formItem.type === 'date-picker'"
        v-model="formItem.value"
        placeholder="请选择"
        clearable
        v-bind="formItem.props"
        v-on="formItem.on ? formItem.on : {}"
      />
      <!-- 单选框 -->
      <el-radio-group
        v-if="formItem.type === 'radio-group'"
        v-model="formItem.value"
        v-bind="formItem.props"
        v-on="formItem.on ? formItem.on : {}"
      >
        <template v-if="formItem.isButton">
          <el-radio-button
            v-for="(item, index) in formItem.options"
            :key="index"
            :label="item.value"
          >
            {{ item.label }}
          </el-radio-button>
        </template>
        <template v-else>
          <el-radio
            v-for="(item, index) in formItem.options"
            :key="index"
            :label="item.value"
          >
            {{ item.label }}
          </el-radio>
        </template>
      </el-radio-group>
      <!-- 开关 -->
      <el-switch
        v-if="formItem.type === 'switch'"
        v-model="formItem.value"
      />
      <!-- 是否存在子级 -->
      <div
        v-if="formItem.children"
        class="w-full"
      >
        <base-form-item
          :default-label-width="defaultLabelWidth"
          :form-data="formItem.children"
          :parent-key="formKey ? parentKey + formKey + '.children.' : ''"
        />
      </div>
    </el-form-item>
  </div>
</template>

<script lang='ts'>
import { defineComponent } from 'vue';
import {
  ElTooltip,
  ElIcon,
  ElFormItem,
  ElSwitch,
  ElInput,
  ElInputNumber,
  ElRadioGroup,
  ElRadio,
  ElRadioButton,
  ElCascader,
  ElSelect,
  ElOption,
  ElDatePicker
} from 'element-plus';

import { QuestionFilled } from '@element-plus/icons-vue';
import { FormObjItemType } from '../types';

export default defineComponent({
  components: {
    ElTooltip,
    ElIcon,
    QuestionFilled,
    ElFormItem,
    ElSwitch,
    ElInput,
    ElInputNumber,
    ElRadioGroup,
    ElRadio,
    ElRadioButton,
    ElCascader,
    ElSelect,
    ElOption,
    ElDatePicker
  },
  props: {
    // 表单数据
    formData: {
      type: Object,
      required: true,
    },
    // 父级键值
    parentKey: {
      type: String,
      required: false,
      default: ''
    },
    // 默认文本宽度
    defaultLabelWidth: {
      type: Number,
      required: false,
      default: 80
    }
  },
  setup() {
    /**
     * 选择框变化事件
     * @param formItem 表单项
     */
    function selectChange(formItem: FormObjItemType) {
      // 不存在全选项或选项不存在时不执行以下代码
      if (formItem.isAll === undefined || formItem.options === undefined) {
        return;
      }
      
      // 判断是否全选
      if (formItem.value.length >= 0 && formItem.value.length < formItem.options.length) {
        formItem.isAll = false;
        formItem.isIndeterminate = formItem.value.length === 0 ? false : true;
      } else {
        formItem.isIndeterminate = false;
        formItem.isAll = true;
      }
    }

    /**
     * 选择全选变化事件
     * @param isAll 是否全选
     * @param formItem 表单项
     */
    function selectAllChange(isAll:Boolean, formItem: FormObjItemType) {
      if (isAll) {
        formItem.value = formItem.options?.map(item => item.value);
      } else {
        formItem.value = [];
      }

      if (formItem.isIndeterminate) {
        formItem.isIndeterminate = false;
      }
    }

    return {
      selectChange,
      selectAllChange
    };
  },
});
</script>
<style lang="scss" scoped>
.el-form-item {
  padding: 9px 0;
  margin-bottom: 0;
}
</style>
import { Rule } from 'async-validator';

/**
 * 表单对象类型
 */
export type FormObjType = {
  [key: string]: FormObjItemType
};

/**
 * 表单项类型
 */
export type FormObjItemType = {
  /**
   * 文本
   * @default ''
   * @type String
   */
  label?: String;

  /**
   * 组件类型
   * @default undefined
   * @type ComponentTypes
   */
  type?: ComponentTypes;

  /**
   * 选项数组
   * @default undefined
   * @type optionsType[]
   */
  options?: optionsType[];

  /**
   * 组件属性
   * @default undefined
   * @type Object
   */
  props?: Record<string, any>

  /**
   * 组件事件
   * @default {}
   * @type Object
   */
  on?: {
    [key: string]: any
  };

  /**
   * 表单选项属性
   * @default {}
   * @type Object
   */
  formItemProps?: {
    prop?: string,
    labelWidth?: string | number,
    rules?: Rule,
    required?: Boolean,
    [key:string]: any
  };

  /**
   * 验证规则
   * @default {}
   */
   rules?: Array<Object> | Object;

  /**
   * 表单对象子级
   * @default {[key]:{}}
   * @type FormObjType
   */
  children?: FormObjType;

  /**
   * 表单文本提示
   */
  labelTips?: string;

  /**
   * 是否全选
   * @default undefined
   * @type Boolean
   */
  isAll?: Boolean;

  /**
   * 选中个数不为0,且未全选
   * @default undefined
   * @type Boolean
   */
  isIndeterminate?: Boolean;

  /**
   * 值
   * @default null
   * @type any
   */
  value?: any;

  /**
   * 是否请求列表
   * @default true
   * @type Boolean
   */
  isRequest?: Boolean;
  
  /**
   * 额外扩展参数
   * @default {}
   * @type Object
   */
  extra?: {
    [key: string]: any
  };
};

/**
 * 选项类型
 */
export type optionsType = {
  /**
   * 文本
   * @type String | Number
   */
  label: String | Number

  /**
   * 值
   * @type String | Number
   */
  value: String | Number | Boolean

  /**
   * 是否禁用
   * @type Boolean
   */
  disabled?: Boolean
}

/**
 * 组件类型
 */
export type ComponentTypes = 'label' | 'switch' | 'radio-group' | 'input' | 'input-number' | 'select' | 'cascader' | 'date-picker'

/**
 * 表单参数类型
 */
export type FormParamsType = Object | undefined

🍐写在最后

代码仅供参考学习,可以随意扩展自己的想法,大家一起学习🦏,觉得有用的同学动动小手点个赞