企业级配置式表单组件封装

概述

实际项目中,基本上离不开表单,一般都是直接使用组件库form组件进行编写,直接使用对应组件其实也没什么大问题,就是复用起来比较麻烦,每次都需要手动复制一个个的表单item,当表单很多,联动较多的时候就会占大篇幅的代码,并且不易阅读和统一维护。

配置式表单demon

传统的表单组件格式

demon

js 复制代码
<template>
  <el-form :inline="true" :model="formInline" class="demo-form-inline">
    <el-form-item label="Approved by">
      <el-input v-model="formInline.user" placeholder="Approved by" clearable />
    </el-form-item>
    <el-form-item label="Activity zone">
      <el-select
        v-model="formInline.region"
        placeholder="Activity zone"
        clearable
      >
        <el-option label="Zone one" value="shanghai" />
        <el-option label="Zone two" value="beijing" />
      </el-select>
    </el-form-item>
    <el-form-item label="Activity time">
      <el-date-picker
        v-model="formInline.date"
        type="date"
        placeholder="Pick a date"
        clearable
      />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="onSubmit">Query</el-button>
    </el-form-item>
  </el-form>
</template>

<script lang="ts" setup>
import { reactive } from 'vue'

const formInline = reactive({
  user: '',
  region: '',
  date: '',
})

const onSubmit = () => {
  console.log('submit!')
}
</script>

<style>
.demo-form-inline .el-input {
  --el-input-width: 220px;
}

.demo-form-inline .el-select {
  --el-select-width: 220px;
}
</style>

缺点

  • 大量的el-form-item组件重复
  • 随着组件越来越复杂,不方便统一维护
  • 阅读负担
  • 不方便扩展(每次新增表单可能都需要改很多地方)

配置式表单

组件源码仓库地址:github.com/vgnip/vue3-...

dmeon

js 复制代码
<template>
 <MyForm @register="register">
      <template #selectA="{ model, field }">
        <ElSelect v-model="model[field]" collapse-tags multiple placeholder="Select">
          <ElOption v-for="item in TestData.colors" :key="item.value" :label="item.label" :value="item.value">
            <div class="ls-flex ls-items-center">
              <el-tag :color="item.value" style="margin-right: 8px" size="small" class="ls-h-[20px] ls-w-[20px]" />
              <span :style="{ color: item.value }">{{ item.label }}</span>
            </div>
          </ElOption>
          <template #tag>
            <el-tag v-for="color in model[field]" :key="color" :color="color" size="small" class="ls-h-[20px] ls-w-[20px]" />
          </template>
        </ElSelect>
      </template>
      <template #DatePickerA="{ model, field }">
        <ElDatePicker
          v-model="model[field]" type="date" placeholder="slot选择日期" format="YYYY-MM-DD"
          value-format="YYYY-MM-DD" style="width: 100%" @change="pickerChange"
        />
      </template>
    </MyForm>
</template>
<script lang="ts" setup>
import { useForm } from '@/components/form/hooks/UseForm'
import MyForm from '@/components/form/index.vue'
//表单渲染数据项
const schemas: FormSchema[] = [
  {
    field: 'field1',
    changeEvent: 'input',
    component: ComponentType.Input,
    label: 'field1',
    helpMessage: ['天王盖地虎', '小鸡炖蘑菇'],
    labelLength: 7,
    required: true,
    componentProps: () => {
      return {
        placeholder: '自定义placeholder',
        onChange: (e: any) => {
          console.log(e)
        },
      }
    },
    renderComponentContent: () => {
      return {
        suffix: () => h('span', { style: 'color: red;' }, 'suffix插槽'),
      }
    },
  },
  {
    field: 'field2',
    component: ComponentType.InputNumber,
    label: 'field2',
    required: true,
    rules: [{ trigger: 'blur', validator: TestData.customVaildate }],
    componentProps: {
      style: {
        width: '100%',
      },
    },
    colProps: {
      span: 8,
    },
  },
  {
    field: 'field3',
    changeEvent: 'input',
    component: ComponentType.Input,
    label: 'field3',
    defaultValue: '默认值',
    // colProps: {
    //   span: 8,
    // },
  },
  // el-select组件需要配合el-option使用,但是底层formItem只会渲染一层组件,如果想直接使用el-select组件,有三种方法.1.基于el-select二次封装一次组件.2.使用schema中的render参数,自定义渲染.3.使用slot插槽,在模板中实现.
  {
    field: 'select1',
    component: ComponentType.Select,
    label: 'select1',
    defaultValue: '',
    componentProps: {
      options: TestData.optionsA.value,
      multiLang: true,
    },
    colProps: {
      span: 8,
    },
    // 也可用直接写布尔值,这里的两个区别一个是v-if,一个是v-show
    ifShow: ({ model, field }) => {
      return model[field] !== '1'
    },
    show: ({ model, field }) => {
      return model[field] !== '1'
    },
  },
  {
    field: 'select2',
    component: ComponentType.Select,
    label: 'select2 ',
    defaultValue: '',
    render: ({ model, field }) => {
      return renderElSelect({ model, field })
    },
  },
  {
    field: 'select3',
    component: ComponentType.Select,
    label: 'select3',
    slot: 'selectA',
    colProps: {
      span: 8,
    },
    itemProps: {
      labelWidth: 100,
    },
  },
  {
    field: 'switch1',
    component: ComponentType.Switch,
    label: 'switch1',
    componentProps: ({ formModel, formEvents, schema }) => {
      return {
        onChange: (val: boolean) => {
          formEvents.setFieldsValue({ field1: 'switch1事件触发' })
          console.log('Switch1Change:', val, formModel, formEvents, schema)
        },
      }
    },
  },
  {
    field: 'DatePicker1',
    component: ComponentType.DatePicker,
    label: 'DatePicker1',
    slot: 'DatePickerA',
    colProps: {
      span: 8,
    },
  },
  {
    field: 'DatePicker2',
    component: ComponentType.DatePicker,
    label: 'DatePicker2',
    render: ({ model, field }) => {
      return renderElDatePicker({ model, field })
    },
  },
  {
    field: 'DatePicker3',
    component: ComponentType.DatePicker,
    label: 'DatePicker3',
    componentProps: {
      style: {
        width: '100%',
      },
      type: 'date',
      placeholder: '选择日期',
      format: 'YYYY-MM-DD',
      valueFormat: 'YYYY-MM-DD',
      onChange: (val: any) => {
        console.log(val)
      },
      onVisibleChange: (val: any) => {
        console.log(val)
      },
    },
    colProps: {
      span: 8,
    },
  },
  {
    field: 'Slider1',
    component: ComponentType.Slider,
    label: 'Slider1',
    componentProps: {
      min: 0,
      max: 100,
      range: true,
      marks: {
        20: '20°C',
        60: '60°C',
      },
    },
    colProps: {
      span: 8,
    },
  },
  {
    field: 'TreeSelect1',
    component: ComponentType.TreeSelect,
    label: 'TreeSelect1',
    componentProps: {
      data: TestData.treeData,
      renderAfterExpand: false,
    },
  },
  {
    field: 'AutoComplete1',
    component: ComponentType.AutoComplete,
    label: 'AutoComplete1',
    componentProps: {
      fetchSuggestions: querySearch,
      clearable: true,
      style: {
        width: '100%',
      },
    },
    colProps: {
      span: 8,
    },
  },
  {
    field: 'Divider1',
    component: ComponentType.Divider,
    label: '',
    colProps: {
      span: 24,
    },
  },
  {
    field: 'TimePicker1',
    component: ComponentType.TimePicker,
    label: 'TimePicker',
    colProps: {
      span: 24,
    },
  },
  {
    field: 'name',
    component: ComponentType.MultilingualInput,
    label: 'MultilingualInput',
    componentProps: ({ formModel }) => {
      return {
        inputType: 'textarea',
        defaultValue: { ...formModel.name },
        maxlength: 100, // 默认100
        rules: [{ required: true, message: '请输入内容', trigger: 'blur' }],
      }
    },
    rules: [{
      required: true,
      message: '请输入内容',
      trigger: 'blur',
      validator: (_rule: any, model) => {
        return !isEmpty(model[currentLanguage])
      },
    }],
    colProps: {
      span: 8,
    },
  },
  {
    field: 'tip',
    component: ComponentType.Input,
    label: '提示',
    helpMessage: '提示icon',
    colProps: {
      span: 8,
    },
  },
]
// form表单数据
const formModel = reactive({
  field1: '1',
  name: {
    en: '默认英文',
    cn: '默认中文',
  },
})
const formProps: FormProps = reactive({
  modelValue: formModel,
  labelPosition: LabelPosition.right,
  rowProps: {
    gutter: 10,
    justify: 'start',
    align: 'top',
    tag: 'div',
  },
  labelWidth: 160,
  autoSetPlaceHolder: true,
  // baseRowStyle: {
  //   background: 'red',
  //   padding: '10px',
  // },
  baseColProps: {
    span: 7, // 栅格占据的列数
    offset: 1, // 栅格左侧的间隔格数
    // push: 1, // 栅格向右移动格数
    pull: 1, // 栅格向左移动格数
  },
  disabled: false,
  rulesMessageJoinLabel: true,
  schemas,
  labelSuffix: ':',
})
//初始化表单钩子
const [register, method] = useForm(formProps)

</script>

优点

  • 表单数据视图渲染分离
  • 大量减少重复代码量
  • 动态可配置,只需要提供配置项即可
  • 方便团队统一规范维护
  • 大大提高开发效率
  • 表单项可随意定制(样式、联动、数据等等)
  • 支持jsx和模版语法
  • 可根据实际情况改写多语言形式

配置式表单组件使用和配置

使用方法

按照上述的配置式demon即可配置一个表单组件

表单配置项类型

类型

源码类型位置:\src\components\form\types

js 复制代码
export interface FormProps {
    // 表单数据收集对线
  modelValue?: Recordable
  //对齐方式
  labelPosition?: LabelPosition
  // 整个表单的行配置
  rowProps?: RowProps
  // 整个表单中所有项目的宽度
  labelWidth?: number | string
  // 整个表单的label后缀
  labelSuffix?: string
  // 自动根据szchema中的label设置placeholder
  autoSetPlaceHolder?: boolean
  // 表单的内部组件尺寸
  size?: '' | 'large' | 'default' | 'small'
  // row的自定义样式
  baseRowStyle?: CSSProperties
  // col的自定义样式
  baseColProps?: Partial<ColEx>
  // 表单配置规则
  schemas?: FormSchema[]
  // 是否禁用
  disabled?: boolean
  // 检查标签中是否添加了信息
  rulesMessageJoinLabel?: boolean
  // 自定义重置函数
  resetFunc?: () => Promise<void>
  // 是否详情展示
  isView?: boolean
}

使用

js 复制代码
const formProps: FormProps = reactive({
  modelValue: formModel,
  labelPosition: LabelPosition.right,
  rowProps: {
    gutter: 10,
    justify: 'start',
    align: 'top',
    tag: 'div',
  },
  labelWidth: 160,
  autoSetPlaceHolder: true,
  // baseRowStyle: {
  //   background: 'red',
  //   padding: '10px',
  // },
  baseColProps: {
    span: 7, // 栅格占据的列数
    offset: 1, // 栅格左侧的间隔格数
    // push: 1, // 栅格向右移动格数
    pull: 1, // 栅格向左移动格数
  },
  disabled: false,
  rulesMessageJoinLabel: true,
  schemas,
  labelSuffix: ':',
})

表单项配置

类型

源码类型位置:\src\components\form\types

js 复制代码
export interface FormSchema {
  // 表单值
  field: string
  // 表单值变化的监听事件名 默认为change
  changeEvent?: string
  // v-model双向绑定的值的字段,默认为model-value
  valueField?: string
  component: ComponentType
  label: string | (() => string)
  // 组件入参
  componentProps?: ((opt: { schema: FormSchema, formEvents: FormEvents, formModel: Recordable }) => Recordable) | object
  // 栅格配置
  colProps?: any
  // 默认值
  defaultValue?: any
  // 是否校验
  required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean)
  // 校验规则
  rules?: Arrayable<Rules>
  // 是否自动拼接label和校验信息
  rulesMessageJoinLabel?: boolean
  // js控制显隐
  ifShow?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean)
  // css控制显隐
  show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean)
  // el-form-item的参数
  itemProps?: any
  // 自定义插槽, 插入到el-form-item中
  slot?: string
  // 类似插槽方式,不过是通过函数来实现dom结构,而不是像插槽那样写到template中
  render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string
  // 也是自定义插槽, 不过需要自己写el-form-item
  itemSlot?: string
  // 同render 和 itemSlot
  renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string
  // 自定义渲染组件内部的slot
  renderComponentContent?: ((renderCallbackParams: RenderCallbackParams) => any) | VNode | VNode[] | string
  // 标签右侧提示icon
  helpMessage?: string | string[] | ((renderCallbackParams: RenderCallbackParams) => string | string[])
  suffix?: string | number | ((values: RenderCallbackParams) => string | number)
  subLabel?: string
  labelLength?: number
  commonRules?: boolean
}

使用

一个formItem就是一个个数组项,只需要配置数据项即可,组件类型有内置的,也可以灵活控制自己渲染想要的类型。

js 复制代码
const schemas: FormSchema[] = [
  {
    field: 'field1',
    changeEvent: 'input',
    component: ComponentType.Input,
    label: 'field1',
    helpMessage: ['天王盖地虎', '小鸡炖蘑菇'],
    labelLength: 7,
    required: true,
    componentProps: () => {
      return {
        placeholder: '自定义placeholder',
        onChange: (e: any) => {
          console.log(e)
        },
      }
    },
    renderComponentContent: () => {
      return {
        suffix: () => h('span', { style: 'color: red;' }, 'suffix插槽'),
      }
    },
  },
  {
    field: 'field2',
    component: ComponentType.InputNumber,
    label: 'field2',
    required: true,
    rules: [{ trigger: 'blur', validator: TestData.customVaildate }],
    componentProps: {
      style: {
        width: '100%',
      },
    },
    colProps: {
      span: 8,
    },
  },
  {
    field: 'field3',
    changeEvent: 'input',
    component: ComponentType.Input,
    label: 'field3',
    defaultValue: '默认值',
    // colProps: {
    //   span: 8,
    // },
  },
  {
    field: 'select1',
    component: ComponentType.Select,
    label: 'select1',
    defaultValue: '',
    componentProps: {
      options: TestData.optionsA.value,
      multiLang: true,
    },
    colProps: {
      span: 8,
    },
    // 也可用直接写布尔值,这里的两个区别一个是v-if,一个是v-show
    ifShow: ({ model, field }) => {
      return model[field] !== '1'
    },
    show: ({ model, field }) => {
      return model[field] !== '1'
    },
  },
  ]

配置式表单常用API

动态修改对齐方式

js 复制代码
 const changeLabelPosition = () => {
    const labelPositions: any = ['left', 'right', 'top']
    const currentIndex = labelPositions.indexOf(formProps.labelPosition)
    const nextIndex = (currentIndex + 1) % labelPositions.length
    formProps.labelPosition = labelPositions[nextIndex]
  }

禁用表单

js 复制代码
  const disabledForm = () => {
    formProps.disabled = !formProps.disabled
  }

校验表单

js 复制代码
 const validate = async () => {
    const valid = (valid: any) => {
      console.log(valid)
    }
    const result = await formEvents.validate(valid)
    const msg = result ? '校验成功' : '校验失败'
    ElMessage(msg)
  }

自定义校验

js 复制代码
const validateField = async (field: string) => {
    const valid = (valid: any) => {
      console.log(valid)
    }
    const result = await formEvents.validateField(field, valid)
    const msg = result ? '校验成功' : '校验失败'
    ElMessage(msg)
  }

移除特定表单项校验

js 复制代码
method.clearValidate('field2')

重置表单

js 复制代码
method.resetFields

获取表单值

js 复制代码
 const getFieldsValue = () => {
    console.log('表单值========>', formEvents.getFieldsValue())
    ElMessage(JSON.stringify(formEvents.getFieldsValue()))
  }
#### 设置特项定表单值

```js
    formEvents.setFieldsValue({ field1: '设置的值' })

新增表单项

js 复制代码
 const appendSchemaByField = (schema: FormSchema, prefixField?: string | undefined, first?: boolean | undefined) => {
    formEvents.appendSchemaByField(schema, prefixField, first)
  }
  
  const newSchemas: FormSchema = {
  field: 'newField',
  changeEvent: 'input',
  component: ComponentType.Input,
  label: '新增出来的表单项',
  defaultValue: '新增出来的表单项',
  colProps: {
    span: 24,
  },
}
  appendSchemaByField(newSchemas, 'field2')

删除表单项

js 复制代码
  const removeSchemaByFiled = (field: string | string[]) => {
    formEvents.removeSchemaByFiled(field)
  }
removeSchemaByFiled('field1')

更新表单项

js 复制代码
 const updateSchema = (schema: Partial<FormSchema> | Partial<FormSchema>[]) => {
    formEvents.updateSchema(schema)
  }
  
  const field1NewSchema: FormSchema = {
  field: 'field1',
  component: ComponentType.Input,
  label: '更新字段1的label',
}
updateSchema(field1NewSchema)

重置表单项

js 复制代码
  const resetSchema = (schema: Partial<FormSchema> | Partial<FormSchema>[]) => {
    formEvents.resetSchema(schema)
  }

设置为详情展示

js 复制代码
method.setProps({ isView: true })

总结

上面的配饰和表单组件是vite+vue3搭建的,总体思路通过配置式数据进行开发,省去传统表单开发一个个去复制的麻烦过程,思路类似于低代码平台开发配置式数据渲染页面一样的道理(JSON schema),上面示例基本上遇到的表单都能完成,更加复杂的情况可以通过JSX实现,上述组件内置了接口。

相关推荐
一只叫煤球的猫3 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
excel4 小时前
Three.js 材质(Material)详解 —— 区别、原理、场景与示例
前端
掘金安东尼4 小时前
抛弃自定义模态框:原生Dialog的实力
前端·javascript·github
hj5914_前端新手8 小时前
javascript基础- 函数中 this 指向、call、apply、bind
前端·javascript
薛定谔的算法8 小时前
低代码编辑器项目设计与实现:以JSON为核心的数据驱动架构
前端·react.js·前端框架
Hilaku8 小时前
都2025年了,我们还有必要为了兼容性,去写那么多polyfill吗?
前端·javascript·css
yangcode8 小时前
iOS 苹果内购 Storekit 2
前端
LuckySusu8 小时前
【js篇】JavaScript 原型修改 vs 重写:深入理解 constructor的指向问题
前端·javascript
LuckySusu8 小时前
【js篇】如何准确获取对象自身的属性?hasOwnProperty深度解析
前端·javascript