SOURCE

<template>
  <el-form
    v-bind="props"
    v-loading="formLoading"
    element-loading-text="加载中..."
    element-loading-background="rgba(255, 255, 255, 0.5)"
    ref="formRef"
    :model="getModelValue"
    :rules="rules"
    inline
    scroll-to-error
  >
    <slot name="header"></slot>
    <el-form-item v-for="item in formItems" v-bind="item" :key="item.field" :prop="item.field" :label="item.label" for=" " :class="getFormItemClass(item)">
      <div class="el-form-item__content__warp" :style="{width: item.width || formItemWidth}">
        <el-select
          v-if="item.compName === 'ElSelect'"
          v-bind="item.props"
          v-model="getModelValue[item.field]"
          filterable
          fit-input-width
          :clearable="!!(item.props?.clearable !== false)"
          style="width: 100%"
        >
          <el-option
            v-for="(oItem, index) in getPropsKeyValue(item, 'options')"
            :key="index"
            :label="oItem[getPropsKeyValue(item)]"
            :value="oItem[getPropsKeyValue(item, 'value')]"
          >
            <BaseToolTip :rows="1" :content="oItem[getPropsKeyValue(item)]">
              {{ oItem[getPropsKeyValue(item)] }}
            </BaseToolTip>
          </el-option>
        </el-select>
        <component
          v-bind="item.props"
          :is="item.compName"
          v-else-if="item.compName"
          v-model="getModelValue[item.field]"
          :clearable="item.props?.clearable !== false"
        ></component>
        <template v-else>
          <el-input
            v-show="!$slots?.[item.field]"
            v-model="getModelValue[item.field]"
            v-bind="item.props"
            :maxlength="item.props?.maxlength || 21"
            :clearable="item.props?.clearable !== false"
            :placeholder="item.props?.placeholder || '请输入'"
            style="width: 100%"
          ></el-input>
          <slot :name="item.field" :item="item"></slot>
        </template>
      </div>
    </el-form-item>
    <el-form-item v-if="isBtns" class="el-form-item--btns" label-position="">
      <slot name="btns">
        <el-button v-baseDebounceClick type="primary" :disabled="getDisabled('submit')" @click="search"> 查 询 </el-button>
        <el-button v-baseDebounceClick plain type="primary" :disabled="getDisabled('reset')" @click="reset">
          重 置
        </el-button>
      </slot>
    </el-form-item>
    <slot></slot>
  </el-form>
</template>
<script setup>
import {ref, toRefs, computed, watch} from 'vue';
import {ElForm} from 'element-plus';
const props = defineProps({
  ...ElForm.props,
  modelValue: {
    type: Object,
    default: () => ({})
  },
  formItems: {
    type: Array,
    required: true,
    validator(value) {
      // field: 字段属性名
      // label:字体名称
      // defaultValue: 默认值
      // width: 宽度
      // compName: element-plus组件名 或者 组件
      // props: (onClear、onFocus、onBlur、onUpdate、onChange、 El-Form-Item 属性或者自定义组件属性)
      // rules: 校验规则
      return value.every(item => {
        return ['field', 'label'].every(key => Object.keys(item).includes(key));
      });
    }
  },
  formItemWidth: {
    type: String,
    default: '250px'
  }, // 通用表单Item宽度
  isBtns: {
    type: Boolean,
    default: true
  }
});

const emits = defineEmits([...Object.keys(ElForm.emits), 'search', 'query', 'reset', 'update:modelValue', 'change']);

const formLoading = ref(false);
const {formItems, modelValue} = toRefs(props);
const formRef = ref(null);
const modelDefaultValue = ref({});

const rules = computed(() => {
  return formItems.value.reduce((prev, current) => {
    if (current?.field && current?.rules) {
      const newCurrent = prev;
      newCurrent[current.field] = current.rules;
      return newCurrent;
    }
    return prev;
  }, {});
});

const getDefaultValue = computed(() => {
  return formItems.value.map(({defaultValue}) => defaultValue);
});

const getRequiredAllValue = computed(() => {
  return formItems.value
    .filter(({required}) => required)
    .map(({field}) => field)
    .every(key => getModelValue.value[key] || [0, false].includes(getModelValue.value[key]));
});

const getModelValue = computed({
  get() {
    return new Proxy(modelValue.value, {
      get: (target, field) => Reflect.get(target, field),
      set: (target, field, value) => {
        updateFieldValue(field, value);
        return true;
      }
    });
  }
});

const updateFieldEvent = (eventType, key, value) => {
  const fIndex = formItems.value.findIndex(({field}) => field === key);
  if (formItems.value?.[fIndex]?.props?.[eventType]) {
    formItems.value[fIndex]['props'][eventType](value, modelValue.value);
  }
};

const updateFieldValue = (field, value) => {
  const target = modelValue.value;
  const oldValue = target[field];
  let flag = false;
  // 如旧值和新值都是数组:需要判断新旧值是否不一样
  if (Array.isArray(value) && Array.isArray(oldValue)) {
    flag = isOldNewArrayChange(oldValue, value);
    // 旧值和新值其中一个是数组,另外一个不是
  } else if (Array.isArray(value) || Array.isArray(oldValue)) {
    flag = true;
  } else {
    flag = oldValue !== value;
  }
  if (flag) {
    emits('change', {field, value});
    target[field] = value;
    emits('update:modelValue', {...target});
  }
  updateFieldEvent('onUpdate', field, value);
};

const getPropsKeyValue = (item, key = 'label') => {
  return item?.props?.[key] || key;
};

const isOldNewArrayChange = (oldArr, newArr) => {
  return oldArr.length === newArr.length ? oldArr.some(item => !newArr.includes(item)) : true;
};

const initModelValue = (status = false) => {
  formItems.value
    .filter(item => 'defaultValue' in item)
    .map(item => {
      const oldValue = getModelValue.value[item.field];
      let flag = status;
      if (!(item.field in getModelValue.value) || Array.isArray(oldValue) || Array.isArray(item.defaultValue)) {
        flag = true;
      } else if (Array.isArray(oldValue) && Array.isArray(item.defaultValue)) {
        flag = isOldNewArrayChange(oldValue, item.defaultValue);
      } else {
        flag = status;
      }
      if (flag) {
        getModelValue.value[item.field] = item.defaultValue;
      }
    });
};

const getFormItemClass = item => {
  let str = item?.class ?? '';
  if (item?.props?.hidden) {
    str += 'is-hidden';
  }
  return str;
};

const doEmits = list => {
  list.map(key => {
    emits(key, JSON.parse(JSON.stringify(getModelValue.value)));
  });
};

const search = () => {
  if (!getDisabled('submit')) {
    formRef.value.validate(valid => {
      if (valid) {
        doEmits(['search', 'query']);
      }
    });
  }
};

const reset = () => {
  initModelValue(true);
  formRef.value.resetFields();
  doEmits(['reset', 'query']);
};

const getDisabled = type => !getRequiredAllValue.value;

watch(
  () => getDefaultValue.value,
  val => {
    initModelValue();
  },
  {immediate: true, deep: true}
);
defineExpose({
  formRef,
  formLoading
});
</script>

<style lang="scss">
.el-form--inline.el-form--label-top .el-form-item {
  &.el-form-item--btns {
    display: flex;
    align-items: flex-end;
  }
  &.is-hidden {
    display: none !important;
    opacity: 0;
    pointer-events: none;
  }
}
</style>
console 命令行工具 X clear

                    
>
console