本文主要是介绍表格封装之 useForm 封装,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
在日常开发中,后端管理系统中增删改查的开发大多是重复机械式的工作,为了减少解放自己的双手,我们可以对这部分工作的内容进行封装。
一般的增删改查页面,由顶部搜索区,中部表格区,底部功能区(分页、多选等)三部分组成,我们将分文三个部分进行封装处理、本篇文章我们将实现 useForm
及组价的封装。
- 本文基于
Vue3
、TypeScript
、ElementPlus
进行封装。- 本文中不便于描述的功能在代码示例中注释标注。
封装目标
useForm
是对顶部搜索区域的封装,通过封装基于配置化实现表单的渲染,同时实现数据项的双向绑定和自定义插槽的功能。
如下示例代码为我们的目标实现
<template><Form v-model="formValue" :column="column" @search="onSearch"><el-button type="default" @click="onReset">重置</el-button></Form>
</template><script lang="ts" setup>
import { reactive } from 'vue'
import Form, { useForm, FormItem } from '@/hooks/useForm'const _column = reactive<FormItem[]>([{ type: 'input', prop: 'id', label: 'ID' },{type: 'select',prop: 'sex',label: '性别',options: [{ label: '男', value: 1 },{ label: '女', value: 0 },],},
])const { formValue, column, onReset } = useForm(_column)
const onSearch = () => {}
</script>
定义类型
既然是封装表单,我们需要考虑存在那些组件以及组件的特殊属性,比如以下的示例中定义了 Input 输入
Date 时间
Select 下拉
Cascader 级联
Slot 自定义
组件,当然你也可以根据具体的业务而进行修改。
// /hooks/useForm/type.d.ts
import type { VNodeChild } from 'vue'/*** 表单默认配置*/
interface FormDefault {label: stringplaceholder?: string
}/*** 输入框*/
export interface FormInput extends FormDefault {type: 'input'prop: stringvalue?: stringdataType?: 'number' | 'string'
}/*** 日期时间选择器*/
interface FormDateDefault extends FormDefault {type: 'date'prop: stringdateType?: 'date' | 'datetime'valueFormat?: stringvalue?: string
}interface FormDateRange extends FormDefault {type: 'date'dateType: 'daterange'prop: [string, string]value?: [string, string] | null
}export type FormDate = FormDateDefault | FormDateRange/*** 下拉框*/
export interface FormSelect1 extends FormDefault {type: 'select'prop: stringmultiple?: booleanvalue?: string | numberoptions: any[]labelKey?: string | 'label'valueKey?: string | 'value'
}
export interface FormSelect2 extends FormDefault {type: 'select'prop: stringmultiple?: booleanvalue?: string | numberapi: (any) => Promise<any>labelKey?: string | 'label'valueKey?: string | 'value'
}
export interface FormSelect3 extends FormDefault {type: 'select'prop: stringmultiple?: booleanvalue?: string | numbersearchApi: (any) => Promise<any>labelKey?: string | 'label'valueKey?: string | 'value'
}export type FormSelect = FormSelect1 | FormSelect2 | FormSelect3/*** 级联选择器*/
interface FormCascader1 extends FormDefault {type: 'cascader'prop: stringmultiple?: booleanvalue?: string | numberoptions: any[]labelKey?: string | 'label'valueKey?: string | 'value'childrenKey?: string | 'children'
}interface FormCascader2 extends FormDefault {type: 'cascader'prop: stringmultiple?: booleanvalue?: string | numberapi: (any) => Promise<any>labelKey?: string | 'label'valueKey?: string | 'value'childrenKey?: string | 'children'
}export type FormCascader = FormCascader1 | FormCascader2/*** 自定义组件*/
export interface FormSlot extends FormDefault {type: 'slot'prop: string[] | stringvalue?: anyrender: (row: any) => VNodeChild
}/*** 表单合集*/
export type FormItem = FormInput | FormDate | FormSelect | FormCascader | FormSlot
封装 Hook
根据组件的功能属性进行初始化处理,拿到初始的表单值、对表单项的配置进行初始化,以及封装通用的函数。
// /hooks/useForm/hooks.ts
import { reactive, toRefs } from 'vue'
import { FormItem } from './types.d'
import { isEmpty } from '@/utils/utils'interface FormParams {formValue: any
}export const useForm = (column: FormItem[]) => {const state = reactive<FormParams>({formValue: {},})// 拿到初始化 formValue 的值function initForm() {column.forEach(async item => {// 下拉框if (item.type === 'select') {const { prop, options, api, searchApi } = item as any// 字段检验if (isEmpty(api) && isEmpty(options) && isEmpty(searchApi)) {console.warn(`[useForm] ${prop} 字段配置 api 、searchApi 或 options 不能同时为空`)return}const _options: any[] = options || [];(item as any).options = _optionsstate.formValue[item.prop] = item.value || null// 下拉框的选项可能来自于远程,在这里获取if (api) {let v = await api({})// 返回的结果可能格式不一致,兼容处理v instanceof Array && (v = { total: v.length, data: v });(item as any).options = _options.concat(v.data)state.formValue[item.prop] = item.value || null}return}// 级联选择器if (item.type === 'cascader') {const { prop, options, api } = item as any// 字段检验if (isEmpty(api) && isEmpty(options)) {console.warn(`[useForm] ${prop} 字段配置 api 或 options 不能同时为空`)return}const _options: any[] = options || [];(item as any).options = _optionsstate.formValue[item.prop] = item.value || null// 级联选择器的选项可能来自于远程,在这里获取if (api) {let v = await api({})// 返回的结果可能格式不一致,兼容处理v instanceof Array && (v = { total: v.length, data: v });(item as any).options = _options.concat(v.data)state.formValue[item.prop] = item.value || null}return}// 时间if (item.type === 'date') {const { dateType } = item// 时间区间可能为两个字段if (dateType === 'daterange') {state.formValue[item.prop[0]] = item.value ? item.value[0] : nullstate.formValue[item.prop[1]] = item.value ? item.value[1] : nullreturn}state.formValue[item.prop as string] = item.value || nullreturn}// 自定义if (item.type === 'slot') {if (item.prop instanceof Array) {item.prop.forEach((v: string, i: number) => {state.formValue[v] = (item.value && item.value[i]) || null})return}}state.formValue[item.prop as string] = item.value || null})}// 重置表单时间function onReset() {column.forEach((item: any) => {// 时间区间if (item.type === 'daterange') {state.formValue[item.prop[0]] = nullstate.formValue[item.prop[1]] = nullitem.value = void 0return}// 时间区间if (item.type === 'time') {state.formValue[item.prop] = nullitem.value = void 0return}// 自定义if (item.type === 'slot') {if (item.prop instanceof Array) {item.prop.forEach((v: string) => {state.formValue[v] = null})return}}state.formValue[item.prop as string] = null})}// 初始化initForm()return {...toRefs(state),column,onReset,}
}
封装 useForm
可以实现基于配置化得到 formValue
、column
、onReset
。
-
formValue
基于表单项拿得初始化的表单值,也就是原始值,可用于表单的数据双向绑定或提供给外部使用。
-
column
对表单项进行初始化,对一些需要从接口中获取数据的组件进行请求处理。
-
onReset
通用函数,重置表单。
渲染组件
通过 useForm()
我们可拿到 formValue
column
onReset
,接下来我们基于以上参数进行组件的封装。
<template><el-card shadow="never"><el-form :inline="true" :model="modelValue" label-width="auto"><el-row :gutter="20"><el-col :xs="12" :sm="12" :md="8" :lg="6" :xl="4" v-for="(prop, i) in column" :key="i"><el-form-item :label="prop.label"><template v-if="prop.type === 'input'"><template v-if="prop.dataType === 'number'"><el-input-numberv-model="modelValue[prop.prop]":placeholder="prop.placeholder || `请输入${prop.label}`"controls-position="right"@change="emits('search')"@keyup.enter="emits('search')"/></template><template v-else><el-inputv-model="modelValue[prop.prop]":placeholder="prop.placeholder || `请输入${prop.label}`"clearable@blur="emits('search')"@keyup.enter="emits('search')"/></template></template><template v-if="prop.type === 'date'"><template v-if="prop.dateType === 'daterange'"><el-date-pickerv-model="prop.value":type="prop.dateType"clearablestart-placeholder="起始时间"end-placeholder="结束时间"value-format="YYYY-MM-DD HH:mm:ss"@change="(v: [Date, Date] | null) => handleChangeDateRange(v, prop.prop)"/></template><template v-else><el-date-pickerv-model="modelValue[prop.prop]":type="prop.dateType || 'date'"clearable:placeholder="prop.placeholder || `请选择${prop.label}`":value-format="prop.valueFormat || 'YYYY-MM-DD HH:mm:ss'"@change="emits('search')"/></template></template><template v-if="prop.type === 'select'"><el-select-v2v-model="modelValue[prop.prop]":props="{label: prop.labelKey || 'label',value: prop.valueKey || 'value',}"filterableclearable:remote="!!(prop as any).searchApi":loading="(prop as any).loading":remote-method="(v: string | null) => handleRemoteMethod(v, prop)":multiple="prop.multiple":options="(prop as any).options":placeholder="prop.placeholder || `请选择${prop.label}`"@change="emits('search')"/></template><template v-if="prop.type === 'cascader'"><el-cascaderv-model="modelValue[prop.prop]":props="{multiple: prop.multiple,emitPath: false,label: prop.labelKey || 'label',value: prop.valueKey || 'value',children: prop.childrenKey || 'children',}":options="(prop as any).options"clearable@change="emits('search')"/></template><template v-if="prop.type === 'slot'"><component :is="RenderComponent(prop.render(modelValue))" /></template></el-form-item></el-col><!-- 判断是否存在 default slot --><template v-if="$slots.default"><el-col :xs="12" :sm="12" :md="8" :lg="6" :xl="4"><el-form-item><slot /></el-form-item></el-col></template></el-row></el-form></el-card>
</template><script setup lang="ts">
import { h } from 'vue'
import dayjs from 'dayjs'
import type { FormItem } from './types.d'const emits = defineEmits<{'update:modelValue': [val: any]search: []reset: []
}>()interface Props {modelValue: anycolumn: FormItem[]
}const props = defineProps<Props>()/*** 日期范围选择器切换事件* @param {Date} val 日期范围* @param {string} prop 日期范围对应的字段*/const handleChangeDateRange = (val: [Date, Date] | null, prop: [string, string]) => {const [start, end] = val || [null, null]props.modelValue[prop[0]] = start// 结束时间默认添加1天props.modelValue[prop[1]] = dayjs(end).add(1, 'day').format('YYYY-MM-DD HH:mm:ss')emits('update:modelValue', props.modelValue)emits('search')
}/*** 搜索结果* @param val 搜索关键字* @param item 表单项*/
const handleRemoteMethod = async (val: string | null, item: any) => {if (!item.searchApi) return!val && (item.options = [])if (!val) returnitem.loading = trueconst v = await item.searchApi({ name: val })item.loading = falseitem.options = v.data
}// 渲染组件包装层
const RenderComponent = (component: any) => {// 判断 component 是否是一个h函数if (typeof component === 'object' && component.type) {return component}// 数组、字符串return h('div', component)
}
</script>
总结
封装 useForm
可以实现基于配置化得到 formValue
、column
、onReset
。
基于 useForm
的结果封装 Form
组件。
最后
感谢你的阅读~
如果你有任何的疑问欢迎您在后台私信,我们一同探讨学习!
如果觉得这篇文章对你有所帮助,点赞、在看是最大的支持!
这篇关于表格封装之 useForm 封装的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!