【前言】
相信很多小伙伴平时都有一些日常的业务表单迭代需求,如果把这些十分通用的表单封装起来,下次遇到新的业务也就能很快开发完成啦,就能用更多的时间研究别的事情,不在为业务发愁。
下面是基于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!!