基于vue3+ts+naive ui 封装的通用化表单(带规则校验)

【前言】

相信很多小伙伴平时都有一些日常的业务表单迭代需求,如果把这些十分通用的表单封装起来,下次遇到新的业务也就能很快开发完成啦,就能用更多的时间研究别的事情,不在为业务发愁。

下面是基于vue3+ts+naive ui 封装的通用化表单(带规则校验)

第一步,定义表单的属性,props.ts

props.ts 复制代码
import type { FormItemProps, FormItemRule, FormProps, InputProps, SelectProps, SwitchProps } from 'naive-ui'

export interface BasicFormProp extends FormProps {
  title?: string
}

export enum FormItemType {
  input = 'n-input',
  select = 'n-select',
  input_number = 'n-input-number',
  switch = 'n-switch',
  info = 'info',
  c_checkboxGroup = 'n-checkbox-group',
  c_radioGroup = 'c_radioGroup',
  d_select = 'n-dynamic-input',
  d_input = 'n-dynamic-input',
  img_upload = 'ImageUpload',
}

export interface FormItemProp extends FormItemProps {
  // 属性字段名称
  field: string
  defaultValue?: any
  required?: boolean
  display?: boolean
  displayField?: string
  displayChoose?: Array<string | number>
  displayElse?: Array<string | number>
  rule?: FormItemRule
  // 组件类型
  component: FormItemType
  // 组件属性值
  componentProps?: InputProps | SelectProps | SwitchProps | any
  selectOptionsRet?: any
}

export type FormItemsProp = FormItemProp[]

第二步,定义表单的hooks,分别是表单校验规则、表单初始化默认值、以及表单项input框、select框等为空的友好提示

useFormRule.ts 复制代码
import type { ComputedRef } from 'vue'
import { computed, unref } from 'vue'
import type { FormItemsProp } from '../types/props'
export function useFormRule(formItems: ComputedRef<FormItemsProp>) {
  return computed(() => {
    const rules = {}
    for (const item of unref(formItems)) {
      if (item.required) {
        rules[item.field] = [
          {
            required: true,
            message: '此项为必填项',
          },
        ]
      }
      if (item.rule)
        rules[item.field] = item.rule
    }
    return rules
  })
}

这里可以根据业务来定义规则,我这里只写了必填项的校验规则

然后是表单初始化的值

useFormValue.ts 复制代码
import type { ComputedRef } from 'vue'
import { unref } from 'vue'
import { FormItemType } from '../types/props'
import type { FormItemsProp } from '../types/props'

export function useFormValue(formModel: any, formItems: ComputedRef<FormItemsProp>) {
  function initFormValue(FormValue?: any) {
    unref(formItems).forEach((item) => {
      const { field, defaultValue, component } = item
      // 如果有修改用修改的值
      const formItemValue = FormValue ? FormValue[field] : (defaultValue || '')
      switch (component) {
        case FormItemType.input:
          formModel[field] = formItemValue
          break
        case FormItemType.input_number:
          formModel[field] = formItemValue || 0
          break
        case FormItemType.select:
          formModel[field] = formItemValue
          break
        case FormItemType.c_checkboxGroup:
          formModel[field] = formItemValue || []
          break
        case FormItemType.c_radioGroup:
          formModel[field] = formItemValue
          break
        case FormItemType.d_input:
          formModel[field] = formItemValue || []
          break
        case FormItemType.d_select:
          formModel[field] = formItemValue || []
          break
        case FormItemType.img_upload:
          formModel[field] = formItemValue
          break
        case FormItemType.switch:
          formModel[field] = formItemValue || '0'
          item.componentProps = {
            'checked-value': '1',
            'unchecked-value': '0',
          }
          break
      }
    })
  }
  //重置函数
  function restFormValue() {
    initFormValue()
  }

  return {
    initFormValue,
    restFormValue,
  }
}

接下来是定义表单项input框、select框等为空的友好提示

useFormItemsProps.ts 复制代码
import type { ComputedRef } from 'vue'
import { computed, ref, unref } from 'vue'
import type { FormItemsProp } from '../types/props'
import { FormItemType } from '../types/props'
import { isUndefined } from '~/src/utils/common'
export function useFormItemsProps(formItems: ComputedRef<FormItemsProp>) {
  const getFormItemsProps = computed(() => {
    const props = ref<any[]>([])
    unref(formItems).forEach(async (item) => {
      const { componentProps, label } = item
      const prop: any = {}
      if (isUndefined(componentProps?.placeholder)) {
        switch (item.component) {
          case FormItemType.input:
            prop.placeholder = `请输入${label}`
            break
          case FormItemType.select:
            prop.placeholder = `请选择${label}`
            // 在传入的选项中没有对应当前值的选项时,这个值应该对应的选项。如果设为 false,不会为找不到对应选项的值生成回退选项也不会显示它,未在选项中的值会被视为不合法,操作过程中会被组件清除掉
            prop['fallback-option'] = false

            break
        }
      }
      // 表单项校验
      prop.path = item.field
      prop.clearable = true
      prop.filterable = true
      props.value.push({
        ...prop,
        ...componentProps,
      })
    })

    return {
      ...props.value,
    }
  })

  return {
    getFormItemsProps,
  }
}

最后,就是表单的vue文件代码了(imageupload是我写的多功能上传图片组件,有兴趣可以去看,另一篇文章有)

js 复制代码
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { FormItemType } from './types/props'
import type { BasicFormProp, FormItemsProp } from './types/props'
import { useFormRule } from './hooks/useFormRule'
import { useFormItemsProps } from './hooks/useFormItemsProps'
import { useFormValue } from './hooks/useFormValue'
import ImageUpload from '~/src/components/library/FastBusiness/ImageUpload/ImageUpload.vue'

interface Props {
  form?: BasicFormProp
  addRequest?: (opt?: any) => void
  editRequest?: (opt?: any) => void
  requestCallback?: (opt?: any) => void
  formItems: FormItemsProp
}

const props = defineProps<Props>()
const formRef = ref<any>(null)
const showModal = ref(false)
const saveLoading = ref(false)
const formModel = reactive<any>({})
const getFormItemProps = computed(() => props.formItems)
const { getFormItemsProps } = useFormItemsProps(getFormItemProps)
const { initFormValue } = useFormValue(formModel, getFormItemProps)
const formRule = useFormRule(getFormItemProps)
const getFormProps = computed(() => {
  return {
    ...props,
  }
})

// 混合值,用于无关与表单项的值
const difference = {
  isAdd: true,
  excess: {},
}

// 编辑
function showEditForm(formValue, fieldArr: [string]) {
  // 追踪一下
  initFormValue(formValue)
  fieldArr?.forEach((item) => {
    difference.excess[item] = formValue[item]
  })
  difference.isAdd = false
  showModal.value = true
}

// 新增
function showAfterInitForm(value?: any) {
  initFormValue()
  if (value)
    difference.excess = { ...value }
  else
    difference.excess = {}
  difference.isAdd = true
  showModal.value = true
}

// 关闭表单
function handleClose() {
  showModal.value = false
}

// 表单提交
function handleSubmit() {
  saveLoading.value = true
  formRef.value?.validate(async (errors) => {
    // 必填项的校验,没有报错才会执行下面
    if (!errors) {
      if (difference.isAdd && getFormProps.value.addRequest) {
        const res: any = await getFormProps.value.addRequest({ ...difference.excess, ...formModel })
        // 添加的接口调用返回不是失败的回调才关闭表单
        if (!res || res.error === null) {
          handleClose()
          if (props.requestCallback)
            props.requestCallback(true)
        }
        else {
          if (props.requestCallback)
            props.requestCallback(false)
        }
      }
      if (!difference.isAdd && getFormProps.value.editRequest) {
        const res: any = await getFormProps.value.editRequest({ ...difference.excess, ...formModel })
        // 编辑的接口调用返回不是失败的回调才关闭表单
        if (!res || res.error === null) {
          handleClose()
          if (props.requestCallback)
            props.requestCallback(true)
        }
        else {
          if (props.requestCallback)
            props.requestCallback(false)
        }
      }
      saveLoading.value = false
    }
    else {
      saveLoading.value = false
    }
  })
}

defineExpose({
  showAfterInitForm,
  showEditForm,
})
</script>

<template>
  <div>
    <n-modal
      v-model:show="showModal"
      preset="dialog"
      title="操作"
      style="width:500px;"
    >
      <n-form
        ref="formRef"
        :rules="formRule"
        :model="formModel"
        v-bind="getFormProps"
        label-placement="left"
        label-width="100"
        style="display: flex;flex-wrap: wrap;justify-content: space-between;padding-top: 6px;"
      >
        <div v-for="(item, nindex) in getFormItemProps" :key="nindex" style="min-width:430px">
          <n-form-item :label="item.label" :path="item.path || item.field" :span="12">
            <template v-if="(item.component === FormItemType.d_select)">
              <n-dynamic-input
                v-model:value="formModel[item.field]"
                :min="1"
                :max="5"
              >
                <template #default="{ index }">
                  <n-select
                    v-model:value="formModel[item.field][index]"
                    :options="item.componentProps.options"
                    :placeholder="`请选择${item.label}`"
                    :fallback-option="false"
                  />
                </template>
              </n-dynamic-input>
            </template>

            <template v-if="(item.component === FormItemType.d_input)">
              <n-dynamic-input
                v-model:value="formModel[item.field]"
                :min="1"
                :max="5"
              >
                <template #default="{ index }">
                  <n-input
                    v-model:value="formModel[item.field][index]"
                    :placeholder="`请选择${item.label}`"
                  />
                </template>
              </n-dynamic-input>
            </template>

            <template v-if="(item.component === FormItemType.info)">
              <n-input :disabled="true" :value="item.defaultValue" />
              <slot v-if="item.componentProps && item.componentProps.name" :name="item.componentProps.name" />
              <slot v-else />
            </template>

            <template v-else-if="(item.component === FormItemType.c_checkboxGroup)">
              <n-checkbox-group v-model:value="formModel[item.field]">
                <n-space item-style="display: flex;">
                  <n-checkbox
                    v-for="(checkbox, cindex) in (item.componentProps as any).options" :key="cindex"
                    :value="checkbox.value" :label="checkbox.label"
                  />
                </n-space>
              </n-checkbox-group>
            </template>

            <template v-else-if="(item.component === FormItemType.c_radioGroup)">
              <n-radio-group v-model:value="formModel[item.field]">
                <n-space item-style="display: flex;">
                  <n-radio
                    v-for="(checkbox, cindex) in (item.componentProps as any).options" :key="cindex"
                    :value="checkbox.value" :label="checkbox.label"
                  />
                </n-space>
              </n-radio-group>
            </template>

            <template v-if="(item.component === FormItemType.img_upload)">
              <ImageUpload v-model="formModel[item.field]" :space="item.componentProps.space" :max="item.componentProps.max" :size="item.componentProps.size" />
            </template>

            <component
              v-bind="getFormItemsProps[nindex]" :is="item.component" v-else
              v-model:value="formModel[item.field]"
            />
          </n-form-item>
        </div>
      </n-form>
      <template #action>
        <n-space justify="end">
          <n-button @click="handleClose">
            取消
          </n-button>
          <n-button type="primary" :loading="saveLoading" @click="handleSubmit">
            确认
          </n-button>
        </n-space>
      </template>
    </n-modal>
  </div>
</template>

然后,在需要用到该表单的地方引入

javascript 复制代码
import BasicForm from './components/Form/BasicForm.vue'

使用(callback就是表单新增或者编辑成功后的操作,可以用于刷新表格数据)

ruby 复制代码
<BasicForm ref="basicFormRef" :form-items="addItems" :add-request="handleAddblock" :edit-request="fetchEditblock" :request-callback="handleSearch" />
forms.ts 复制代码
import { reactive, ref } from 'vue'
import type { FormItemsProp } from '../banner/components/Form/types/props'
import { FormItemType } from '../banner/components/Form/types/props'
export const bankName = ref()
export const messageFlag = ref()

/** 新增/修改 表单 */
export const addItems: FormItemsProp = reactive([
  { label: '选择小程序', field: 'info', component: FormItemType.info, defaultValue: '1111111111' },
  { label: '服务名称', field: 'name', component: FormItemType.input, required: true },
  { label: '跳转链接', field: 'jump_link', component: FormItemType.select, componentProps: { options: [{ label: 'H5', value: '111' }, { label: '小程序', value: '222' }], filterable: true }, required: true },
  { label: '序号', field: 'rank', component: FormItemType.input_number, required: true },
  { label: '是否上架', field: 'display', component: FormItemType.switch, required: true },
  { label: '图片', field: 'picture', component: FormItemType.img_upload, componentProps: { max: 1, size: 3, space: 'bank' }, required: true },
  { label: '备注', field: 'note', component: FormItemType.input, componentProps: { type: 'textarea' } },
])

最后就是示例部分:

编辑操作如下图:

新增操作如下图:

总结:以上的封装其实也没有想到很细的地方:比如:动态的选择框,根据某一项来控制下一项的显示和隐藏等等,因为各式各样的需求都有,开始封装的很好,总有一些想不到的地方,也希望我分享的能帮到在座的小伙伴,感谢大家的观看,多多点赞辣,thanks!!

相关推荐
高山我梦口香糖13 分钟前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_7482352416 分钟前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240251 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar1 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人2 小时前
前端知识补充—CSS
前端·css
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245522 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v2 小时前
webpack最基础的配置
前端·webpack·node.js
pubuzhixing2 小时前
开源白板新方案:Plait 同时支持 Angular 和 React 啦!
前端·开源·github
2401_857600952 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js