vue3封装Element动态表单组件

271 阅读3分钟

封装组件

<template>
  <el-form
    :model="currentValue"
    ref="dymanicForm"
    v-bind="$attrs"
    @submit.native.prevent
  >
    <el-form-item
      :prop="index"
      :rules="item.rules"
      v-for="(item, index) in currentSchema"
      :key="index"
      v-bind="item.formItem || {}"
    >
      <component
        v-model="currentValue[index]"
        :is="item.componentName"
        v-bind="item"
        v-on="item.methods || {}"
      />
    </el-form-item>
    <slot />
  </el-form>
</template>

<script lang="ts" setup>
import checkValidator from "~/utils/validator/index";

import {
  ElInput,
  ElSelect,
  ElDatePicker,
  ElCheckbox,
  ElRadio,
  ElRadioGroup,
  ElCheckboxGroup,
  ElSwitch,
} from "element-plus";

const componentMap = {
  ElInput: ElInput,
  ElSelect: ElSelect,
  ElDatePicker: ElDatePicker,
  ElCheckbox: ElCheckbox,
  ElRadio: ElRadio,
  ElRadioGroup: ElRadioGroup,
  ElCheckboxGroup: ElCheckboxGroup,
};

const props = defineProps({
  schema: Object,
  modelValue: [String, Number, Boolean, Array, Date, Object],
});
const emit = defineEmits(["update:modelValue"]);
const currentValue = computed({
  get: () => props.modelValue,
  set: (val) => emit("update:modelValue", val),
});

const currentSchema = computed(() => {
  const schema = {};
  for (const key in props.schema) {
    let { visible = true, ...item } = props.schema[key];
    // 动态表单的组件名
    item.componentName = componentMap[item.componentName];
    // 是否显示
    visible =
      typeof visible !== "function" ? visible : visible(currentValue, item);
    if (visible) {
      schema[key] = { clearable: true, ...item };
    }
    // 表单验证规则
    if (schema[key].validator && schema[key].validator.length) {
      if (!schema[key].rules) schema[key].rules = [];
      schema[key].validator?.forEach(([valida, args]) => {
        if (checkValidator[valida]) {
          schema[key].rules.unshift(checkValidator[valida](args));
        }
      });
    }
  }
  return schema;
});
const dymanicForm = ref(null);
const validate = (func) => {
  if (typeof func === "function") {
    dymanicForm.value.validate((valid) => {
      func(valid);
    });
  } else {
    return new Promise((resolve) => {
      dymanicForm.value.validate((valid) => {
        resolve(valid);
      });
    });
  }
};
const resetFields = () => {
  dymanicForm.value.resetFields();
};

defineExpose({
  resetFields,
  validate,
});
</script>

调用组件示例

<template>
  <div class="">
    <DymanicForm
      ref="formRef"
      :inline="true"
      :schema="schema"
      v-model="searchValue"
    >
      <el-form-item>
        <el-button type="primary" @click="handleQuery">查询</el-button>
        <el-button @click="handleReset">重置</el-button>
      </el-form-item>
    </DymanicForm>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import type { ItemSchema } from "~/types/dymanic";
import DymanicForm from "~/components/DymanicForm.vue";

const formRef = ref<InstanceType<typeof DymanicForm>>();
const schema = ref<ItemSchema>({
  userId: {
    formItem: {
      label: "用户ID:",
    },
    style: {
      width: "200px",
    },
    componentName: "ElInput",
    placeholder: "请输入用户ID",
    maxlength: 20,
    validator: [
      [
        "checkRequired",
        {
          msg: "用户ID不能为空",
        },
      ],
    ],
  },
  username: {
    formItem: {
      label: "账号:",
    },
    style: {
      width: "200px",
    },
    componentName: "ElInput",
    placeholder: "请输入账号",
    maxlength: 20,
  },
  name: {
    formItem: { label: "用户名:" },
    style: {
      width: "200px",
    },
    componentName: "ElInput",
    placeholder: "请输入用户名",
    maxlength: 20,
  },
});
const searchValue = reactive({
  userId: "",
  username: "",
  name: "",
});
const handleQuery = () => {
  formRef.value.validate((valid: boolean) => {
    if (valid) {
      console.log("查询", searchValue);
      // 查询逻辑
    }
  });
};

const handleReset = () => {
  formRef.value.resetFields();
};
</script>
<style lang="scss" scoped></style>

校验规则示例

// 校验内容不能为空的验证
const checkRequired = ({ msg, trigger = ['blur'] }) => {
  return {
    required: true,
    message: msg,
    trigger,
  }
}

export default checkRequired

Vue3 +ElementPlus 表单组件的封装

FormItem.tsx,index.vue,type.ts

FormItem.tsx

import filter from '@/utils/filters'
import {
  ElCheckbox,
  ElCheckboxGroup,
  ElDatePicker,
  ElInput,
  ElInputNumber,
  ElOption,
  ElRadio,
  ElRadioGroup,
  ElSelect,
  ElTimePicker
} from 'element-plus'
import { defineComponent } from 'vue'

// 普通显示
const Span = (form: Record<string, any>, data: Record<string, any>) => (
  <span>{data.valueProp ? form[data.valueProp] : (data.filter ? filter(form[data.prop], data.filter) : form[data.prop] || '无')}</span>
)

// 输入框
const Input = (form: Record<string, any>, data: Record<string, any>) => (
  <ElInput
    v-model={form[data.prop]}
    type={data.type}
    size='small'
    show-password={data.type == 'password'}
    clearable
    placeholder={'请输入' + data.label}
    autosize = {{
      minRows: 3,
      maxRows: 4,
    }}
    {...data.props}
  >
  </ElInput>
)

// 数字输入框
const InputNumber = (form: Record<string, any>, data: Record<string, any>) => (
  <ElInputNumber
    size='small'
    v-model={form[data.prop]}
    controls-position="right"
    {...data.props}
  />
)

const setLabelValue = (_item: any, { optionsKey }: any = {}) => {
  return {
    label: optionsKey ? _item[optionsKey.label] : _item.label,
    value: optionsKey ? _item[optionsKey.value] : _item.value,
  }
}
// 选择框
const Select = (form: Record<string, any>, data: Record<string, any>) => (
  <ElSelect
    size='small'
    v-model={form[data.prop]}
    filterable
    clearable 
    placeholder={'请选择' + data.label}
    {...data.props}
  >
    {data.options.map((item: any) => {
      return <ElOption {...setLabelValue(item, data)} />
    })}
  </ElSelect>
)

// 单选/区间日期
const Date = (form: Record<string, any>, data: Record<string, any>) => (
  <ElDatePicker
    size='small'
    v-model={form[data.prop]}
    type={data.type}
    value-format={data.valueFormat}
    format = {data.format}
    range-separator="至"
    start-placeholder={data.startPlaceholder}
    end-placeholder={data.endPlaceholder}
    placeholder={'请选择' + data.label}
    {...data.props}
  />
)

// 单选/区间时间
const Time = (form: Record<string, any>, data: Record<string, any>) => (
  <ElTimePicker
    size='small'
    v-model={[form[data.prop]]}
    value-format={data.valueFormat}
    format = {data.format}
    range-separator="至"
    disabled = {form.editable}
    start-placeholder={data.start}
    is-range={data.isRange}
    end-placeholder={data.end}
    {...data.props}
  />
)

// 单选
const Radio = (form: Record<string, any>, data: Record<string, any>) => (
  <ElRadioGroup v-model={form[data.prop]}>
    {data.radios.map(
      (item: { label: string | number | boolean; value: any }) => {
        return (
          <ElRadio label={setLabelValue(item, data.prop).label}>
            {setLabelValue(item, data.prop).value}
          </ElRadio>
        )
      },
    )}
  </ElRadioGroup>
)

// 多选
const Checkbox = (form: Record<string, any>, data: Record<string, any>) => (
  <ElCheckboxGroup size='small' v-model={form[data.prop]}>
    {data.checkboxs.map(
      (item: { label: string | number | boolean; value: any }) => {
        return (
          <ElCheckbox label={setLabelValue(item, data.prop).label}>
            {setLabelValue(item, data.prop).value}
          </ElCheckbox>
        )
      },
    )}
  </ElCheckboxGroup>
)

const setFormItem = (
  form: Record<string, any> | undefined,
  data: Record<string, any>,
  editable: Boolean,
) => {
  if (!form) return null
  if (!editable) return Span(form, data)
  switch (data.type) {
    case 'input':
      return Input(form, data)
    case 'textarea':
      return Input(form, data)
    case 'password':
      return Input(form, data)
    case 'inputNumber':
      return InputNumber(form, data)
    case 'select':
      return Select(form, data)
    case 'date':
    case 'daterange':
      return Date(form, data)
    case 'time':
      return Time(form, data)
    case 'radio':
      return Radio(form, data)
    case 'checkbox':
      return Checkbox(form, data)
    default:
      return null
  }
}

export default () =>
  defineComponent({
    props: {
      data: Object,
      formData: Object,
      editable: Boolean,
    },
    setup(props) {
      return () =>
        props.data
          ? setFormItem(props.formData, props.data, props.editable)
          : null
    },
  })

index.vue

<template>
  <el-form ref="FormRef"
           :model="prop.data.data"
           :rules="editable ? prop.data.rules : {}"
           :inline="inline"
           :label-position="labelPosition"
           label-width="atuo">
    <el-row :gutter="prop.data.elRowGutter">
      <el-col v-for="item in prop.data.formItems"
              :span="item.span">
        <el-form-item :label="item.label ? item.label + ':' : ''"
                      :prop="item.prop"
                      :label-width="item.width">
          <FormItem :formData="prop.data.data"
                    :editable="editable"
                    :data="item">
          </FormItem>
        </el-form-item>
      </el-col>
      <el-col v-if="btnList && btnList.length"
              :span="24">
        <el-form-item>
          <template v-for="item in btnList">
            <Btn :props="item"
                 @click="onClick(item)"></Btn>
          </template>
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>

<script lang="ts" setup>
import { computed } from '@vue/reactivity'
import type { FormInstance } from 'element-plus'
import { ref } from 'vue'
import formItem from './FormItem'
import type { commonForm } from './type'

interface Props {
  data: commonForm
}

const prop = defineProps<Props>()
const editable = computed(() => !!prop.data?.editable)
const inline = computed(() => !!prop.data.formProps?.inline)
const labelWidth = computed(() => prop.data.formProps?.labelWidth || '100px')
const labelPosition = computed(
  () => prop.data.formProps?.labelPosition || 'top',
)
const btnList = computed(() => {
  return prop.data.formProps?.btn
})
// tsx组件
const FormItem = formItem()
const FormRef = ref<FormInstance>()

// 表单按钮
function onClick(data: { onClick?: () => void }) {
  if (!data.onClick) return
  data.onClick()
}

// 表单校验
async function validate() {
  if (!FormRef.value) return
  const result = await FormRef.value.validate()
  return result
}

// 清除表单验证
async function resetFields() {
  return await FormRef.value.resetFields()
}

defineExpose({
  validate,
  resetFields,
})
</script>
<style scoped>
.el-form-item {
  margin: 0 10px !important;
}
.el-form-item__label {
  position: absolute;
}
.el-form-item__content {
  width: 100%;
  padding-left: 80px;
}
.el-select,
.el-input_inner {
  width: 100%;
}
</style>

type.ts

type itemType =
  | 'input'
  | 'select'
  | 'switch'
  | 'radio'
  | 'date'
  | 'time'
  | 'checkbox'
  | 'daterange'

interface FormProps {
  inline?: Boolean
  labelWidth?: string | number
  labelPosition?: 'left' | 'top' | 'right'
  btn?: object[]
}

interface FormItems {
  type: itemType
  label?: string
  prop: string
  valueProp?: string
  width?: string | number
  span?: number
  filter?: string
}

export class commonForm {
  public data: any
  private rules?: object
  public elRowGutter?: number
  public editable?: boolean
  public formProps?: FormProps
  public formItems: FormItems[]
  public dataArray?:object[]

  constructor({
    data = {},
    rules = {},
    editable = true,
    formProps = {},
    formItems = [],
    elRowGutter = 0,
  }: any) {
    this.data = data
    this.rules = rules
    this.elRowGutter = elRowGutter
    this.editable = editable
    this.formItems = formItems
    this.formProps = formProps
  }
}

``