Vue3 +ElementPlus 表单组件的封装

在系统中,表单作为用户与后端交互的重要传递组件使用频率极高,故对其进行封装是必然的,也是一个编写规范代码的前端程序员必须做的一件事。
在Vue3中封装组件时,能感受到与Vue2有着很大的不同,故作此记录。

form文件夹

在这里插入图片描述

  1. FormItem.tsx文件是Typescript中的新特性之一,详细可查阅TS中文文档
  2. index.vue是主体文件
  3. 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
  }
}

在页面中引用

在这里插入图片描述

  1. changCarrier.vue是主题页面,用来显示表单
  2. userForm.ts是对表单进行渲染的数据项

index.vue

<template>
  <el-dialog v-model="show"
             v-if="show"
             :title="`${title}人员`"
             :before-close="handleClose"
             width="60%">
      <Form ref="FormRef"
            :data="formData"></Form>
    <template #footer>
      <el-button @click="handleClose">关 闭</el-button>
      <el-button type="primary"
                 v-show="!isDetail"
                 @click="submit">提 交</el-button>
    </template>
  </el-dialog>
</template>

<script lang="ts" setup>
import { reactive, ref, defineEmits } from 'vue'
// import api from '@/api'
import { ElMessage } from 'element-plus'
import useForm from './hooks/useForm'	//表单的
import api from '@/api/index'

enum types {
  'default' = '',
  'add' = '新增',
  'unData' = '编辑',
  'detail' = '详情',
}
const show = ref(false)	//控制表单开关
const title = ref(types.default)	//表单标题
const FormRef = ref()	//表单DOM
const emit = defineEmits(['refresh'])	//父组件传过来的方法,作用:在表单提交后触发,刷新数据
defineExpose({	//向父组件暴露其属性及方法,实例:父组件点击添加,触发formRef中的addData行为
  show,
  title,
  setData,
  addData,
  delData,
})

// 表单生成
let formData = useForm()

//新增
function addData() {
  handleOpen('add')
}

// 编辑设置数据
function setData(data: object) {	//父组件点击编辑,将值通过方法传过来
  formData.data = reactive({ ...data })	
  handleOpen('unData')
}

//删除
async function delData(data: number) {
  const res: any = await api.gasSite.deleteQueueApply({
    idList: [data],
  })
  emit('refresh')
}

// 请求
async function request() {
  let res: any
  // formData是否存在id值, 存在id值表示编辑, 不存在则为添加
  if (!formData.data?.id) {
    //编辑提交
    res = await api.gasSite.addQueueApply(formData.data)
  } else if (formData.data?.id) {
    //新增提交
    res = await api.gasSite.updateStartWarehouse(formData.data)
  }
  if (res?.status.state === '00') {
    ElMessage.success('操作成功')
    title.value = types.default
    emit('refresh')	//刷新数据
    show.value = false
  } else if (res?.status.state !== '00') {
    ElMessage.error(res?.status.state)
  }
  show.value = false
}

//清除验证信息
async function reset() {
  await FormRef.value.resetFields()
}
//新增表单打开事件
function handleOpen(type: any) {
  formData.formItems = useForm().formItems //表单item
  formData.editable = true //打开表单编辑
  title.value = types[type]
  show.value = true //表单的打开
}
//表单关闭事件
function handleClose() {
  show.value = false
  reset()	//重置该表单项,将其值重置为初始值,并移除校验结果
}
// 提交
async function submit() {
  const result = await FormRef.value.validate()
  if (result) request()
}
</script>

useForm

import { commonForm } from '@/components/common/form/type'
import { reactive } from 'vue'

export default () => {
  const rules = {
    name: [
      { required: true, message: '人员名称', trigger: 'blur' }
    ]
  }
  const form = reactive(
    new commonForm({
      data: [],
      editable: true,
      rules: rules,
      formItems: [
        {
          label: '人员名称',
          type: 'select',
          prop: 'name',
        },
        {
          label: '日期范围',
          type: 'daterange',
          prop: 'queueDate',
          format:'YYYY-MM-DD',
          valueFormat:'YYYY-MM-DD',
          startPlaceholder:'开始时间',
          endPlaceholder:'结束时间',
          span: 6,
        },
        {
          label: '时间段范围',
          type: 'time',
          prop: 'timeSlot',
          format:'HH:mm',
          valueFormat:'HH:mm',
          start:'开始时间',
          end:'结束时间',
          isRange:true,
          span: 6,
        },
        {
          label: '允许排队数量',
          type: 'input',
          prop: 'queueNum',
          span: 6,
        },
        {
          label: '生效类型',
          type: 'select',
          prop: 'isDelay',
          options: [
            {
              label: '当日生效',
              value: 0,
            },
            {
              label: '次日生效',
              value: 1,
            }
          ],
          span: 6,
        },
        {
          label: '生效时间',
          type: 'date',
          prop: 'effectiveTime',
          format:'YYYY-MM-DD',
          valueFormat:'YYYY-MM-DD',
          span: 6,
        },
      ],
    }),
  )

  return form
}

总结

一百个人有一百个编写代码的习惯,其上实现是基于模块化的思想,可能看起来有点累,但是我相信能帮助到你。

Logo

前往低代码交流专区

更多推荐