新起Vue3项目表单组件编写没有表单组件封装,表单编写大量的重复el-col、el-form-item等组件,费时费力,大篇幅代码也不利于维护。这里基于Vue2及之前无为低代码平台的一些经验,封装了一份Vue3+Ts版本的配置化表单基础组件。
你已经是一个成熟的表单了,你要学会:
此组件在element-ui、ant-design-vue项目中均可直接使用,实现原理vue3+ts组件库同时兼容多种ui框架
最终实现的效果是这样滴!!!
我们先看下上述效果图的配置化JOSN实例,最终我们将实现所有表单都能通过这样一个表单JSON实现渲染,表单需要的属性,统统放入json里面,最后通过一个简单的调用即可渲染一个form表单
调用
上述编码我们即可渲染一个from表单
先看配置JSON对象ts接口定义
/*
* @Author: 陈宇环
* @Date: 2022-05-30 14:29:12
* @LastEditTime: 2023-04-20 21:04:32
* @LastEditors: 陈宇环
* @Description:
*/
// 表单组件config配置接口
export interface formConfig {
columns: columnsBase[] // 表单项配置
colNum?: number // columns项默认宽度(1-24整数)
labelWidth?: string // label宽度
disabled?: boolean // 是否禁用
loading?: boolean // 是否加载中
notOpBtn?: boolean // 不需要(搜索,重置,导出)操作按钮
opBtnCol?: number // 操作按钮col宽度(24等分)
isSearch?: boolean // 是否需要搜索按钮
searchFn?: () => any // 搜索按钮点击触发函数
isExport?: boolean // 是否需要导出按钮
exportFn?: () => any // 搜索按钮点击触发函数
isReset?: boolean // 是否需要重置按钮
resetFn?: () => any // 搜索按钮点击触发函数
isExpand?: boolean // 是否需要展示/收起按钮
appendOpBtn?: () => any | void // 附加操作按钮render
nativeProps?: { // ui框架原生属性
[key: string]: any
}
}
// 所有表单控件的联合类型
export type columnsBase =
| inputProps
| selectProps
| radioProps
| checkboxProps
| numberProps
| dateProps
| dateRangeProps
| numberRangeProps
| cascaderProps
| switchProps
| uploadProps
| textProps
| renderProps
// 基础属性接口
interface defaultProps {
prop: string // key值
label?: string // label值
colNum?: number // 列宽 24等分
labelWidth?: number | string // label宽度
hide?: boolean // 是否隐藏(隐藏直接销毁dom)
disabled?: boolean // 是否禁用
required?: boolean // 是否必填
placeholder?: string // 描述字符
clearable?: boolean // 是否需要清除按钮
expandDefault?: boolean // 该字段展开收起默认值
prop2?: string // 附加字段(部分selelct等需要绑定两个key)
rules?: any[] // 附加检验规则
change?: (e: any) => void // change事件触发函数
nativeProps?: { // ui框架原生属性
[key: string]: any
}
}
// options选项 select、radio、checkbox、cascader(可能包含children)选项接口
export type optionsType = {
[label: string]: any, children?: any[]
}[] // 直接传数组对象
| { type: 'dic'; key: string } // 字典获取
| { // 接口获取
type: 'api'
getData: () => Promise<{ [label: string]: any, children?: any[] }[]> // 必须返回对象数组,不一定是label,value格式
}
// select、radio、checkbox 选项格式化函数(对应element-plus组件插槽)
export type format = (item: any) => any
// 输入框控件props
export interface inputProps extends defaultProps {
type: 'input' | 'textarea' | 'password' // 这里还能添加很多类型 参考:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types
showPassword?: boolean // 是否需要密码*号 显示隐藏开关
minlength?: number
maxlength?: number
rows?: number // textarea 行数
}
// 数字输入控件
export interface numberProps extends defaultProps {
type: 'number'
min?: number
max?: number
step?: number
precision?: number
controls?: boolean
}
// 下拉菜单控件props
export interface selectProps extends defaultProps {
type: 'select'
prop2?: string
filterable?: boolean
remote?: boolean
remoteMethod?: (query: string) => Promise<{ [label: string]: any }[]>
multiple?: boolean
collapseTags?: boolean // 多选时,是否需要折叠展示
collapseTagsTooltip?: boolean // 多选并折叠展示时,鼠标放上去是否需要Tooltip展示
multipleLimit?: number // 多选限制个数
labelKey?: string
valueKey?: string
reserveKeyword?: boolean // 搜索状态下,选择一个项之后,是否保留当前关键字
format?: format // 格式化函数
options?: optionsType
}
// 单选控件props
export interface radioProps extends defaultProps {
type: 'radio'
labelKey?: string
valueKey?: string
border?: boolean
showType?: 'button' | undefined // 用按钮的形式展示 button
format?: format
options?: optionsType
}
// 多选控件props
export interface checkboxProps extends defaultProps {
type: 'checkbox'
labelKey?: string
valueKey?: string
border?: boolean
showType?: 'button' | undefined // 用按钮的形式展示 button
format?: format
options?: optionsType
}
// 日期控件props
export interface dateProps extends defaultProps {
// year/month/week/date/datetime/dates
type: 'year' | 'month' | 'week' | 'date' | 'datetime' | 'dates'
}
// 日期控件范围props
export interface dateRangeProps extends defaultProps {
type: 'yearRange' | 'monthRange' | 'dateRange' | 'weekRange' | 'datetimeRange'
propEnd?: string // 范围选择控件(dateRange、numberRange)结束key
disabledDate?: (date: any) => boolean // 禁止选择的日期
disabledEndDate?: (date: any) => boolean // 禁止选择的日期
}
// 数字范围控件props
export interface numberRangeProps extends defaultProps {
type: 'numberRange'
propEnd?: string // 范围选择控件(dateRange、numberRange)结束key
min?: number
max?: number
step?: number
precision?: number
controls?: boolean
}
// 联动Cascader
export interface cascaderProps extends defaultProps {
type: 'cascader'
multiple?: boolean
labelKey?: string,
valueKey?: string,
childrenKey?: string,
emitPath?: boolean,
props?: any,
format?: (node: any, data: any) => any
options?: optionsType
}
// 开关控件props
export interface switchProps extends defaultProps {
type: 'switch'
activeValue?: boolean | string | number, // switch 状态为 on 时的值
inactiveValue?: boolean | string | number, // switch 状态为 off 时的值
}
export interface uploadProps extends defaultProps {
type: 'upload'
maxNum?: number // 最多上传文件数量
accept?: string[] // 允许上传文件类型
maxSize?: number // 文件最大Size 单位:M
multiple?: boolean // 是否允许多传
annexType?: string, // 财务oss相关配置 默认:IMPORT_FILE
businessType?: string, // 财务oss相关配置 默认:SOSIAL_SECURITY
ossFolder?: string, // 财务oss相关配置 默认:SOSIAL_SECURITY
}
// 文本控件props
export interface textProps extends defaultProps {
type: 'text',
defaultText?: string | number,
}
// 自定义render函数(只替换form-item-conent部分,label不会被render)
export interface renderProps extends defaultProps {
type: 'render'
render: () => any // 自定义组件render
}
// 实例是否是columnsOtherBase类型
// export const isColumnsOtherBase = (item: columnsBase): item is columnsOtherBase => {
// return (item as columnsOtherBase).fullRender !== undefined
// }
遍历columns字段渲染对应表单子组件,简略版代码如下:
// @/components/BaseForm/index
cloneConfig.columns.map((item) => {
return <>
{
item.hide !== true &&
{
item.type === 'render' ? // 自定义render函数(只替换form-item-conent部分,label不会被render)
item?.render() : // ep-form-item__content 部分的render函数
componentRender(item) // 根据item:columnsFormBase中的type属性获取对应的自定义组件
}
}
>
})
其中componentRender函数将通过columns中每一项type字段来渲染对应的组件,实现如下:
// @/components/BaseForm/index
import * as widget from './components/index'
const componentRender = (item: columnsBase) => {
const componentInstance = widget.getComponentByType(item)
return {
item?.change && item?.change(params)
updateModelValue()
}}
onSetProp2={(value: any) => {
item.prop2 && setProp2(item.prop2, value)
}}
/>
}
所有表单组件入口:
// @/components/BaseForm/components/index
import { defineAsyncComponent } from 'vue'
import { columnsBase } from '../interface/index'
export const BaseInput = defineAsyncComponent(() => import('./BaseInput'))
export const BaseNumber = defineAsyncComponent(() => import('./BaseNumber'))
export const BaseSelect = defineAsyncComponent(() => import('./BaseSelect'))
export const BaseRadio = defineAsyncComponent(() => import('./BaseRadio'))
export const BaseCheckbox = defineAsyncComponent(() => import('./BaseCheckbox'))
export const BaseDate = defineAsyncComponent(() => import('./BaseDate'))
export const BaseDateRange = defineAsyncComponent(() => import('./BaseDateRange'))
export const BaseNumberRange = defineAsyncComponent(() => import('./BaseNumberRange'))
export const BaseCascader = defineAsyncComponent(() => import('./BaseCascader'))
export const BaseSwitch = defineAsyncComponent(() => import('./BaseSwitch'))
export const BaseText = defineAsyncComponent(() => import('./BaseText'))
export const BaseUpload = defineAsyncComponent(() => import('./BaseUpload'))
// 根据item.type返回对应的组件
export const getComponentByType = (item: columnsBase): any => {
switch (item.type) {
case 'input':
case 'textarea':
return BaseInput
case 'number':
return BaseNumber
case 'select':
return BaseSelect
case 'radio':
return BaseRadio
case 'checkbox':
return BaseCheckbox
case 'year':
case 'month':
case 'week':
case 'date':
case 'datetime':
case 'dates':
return BaseDate
case 'yearRange':
case 'monthRange':
case 'dateRange':
case 'weekRange':
case 'datetimeRange':
return BaseDateRange
case 'numberRange':
return BaseNumberRange
case 'cascader':
return BaseCascader
case 'switch':
return BaseSwitch
case 'upload':
return BaseUpload
case 'text':
return BaseText
default:
throw new Error('配置项控件${col.type}不存在')
}
}
布局这里沿用了 UI框架的 栅栏布局,分为根据屏幕宽度分为xs、sm、md、lg、xl,colNum配置在表单最外配置上以及表单项配置上都有配置,优先级为:表单项中的配置>表单外层配置>默认配置
// @/components/BaseForm/index
// 默认-自适应列宽
const getSpan = (item: columnsBase): number[] => {
const spanArray = [12, 8, 8, 6, 6] // [xs,sm,md,lg,xl]
// 对区间做特殊处理
if (item.type.indexOf('Range') !== -1) {
// 区间为分栏数量的两倍
return spanArray.map((v) => v * 2)
}
return spanArray
}
{
cloneConfig.columns.map((item) => {
return <>
{
item.hide !== true &&
...
}
....
>
})
}
监听config配置变化,遍历columns项根据prop字段、required字段及rules字段生成UI框架表单的rules对象,这里需要处理支持表单配置项中的required,另外需要将表单配置中的附加规则rules,拼接到结果rules对象对应的字段数组中去,部分代码如下:
// @/components/BaseForm/index
watch(() => props.config, () => {
...
initRulesFn()
}, { immediate: true, deep: true })
const initRulesFn = () => {
const cloneRules: { [key: string]: any } = {}
cloneConfig.columns.forEach((item: columnsBase) => {
if (!item.hide) {
cloneRules[item.prop] = [
{ required: item.required, message: `${['input', 'textarea'].includes(item.type) ? '请输入' : '请选择'}${item.label}`, trigger: 'change' },
...(item.rules ? item.rules : []),
]
}
})
Object.assign(rules, cloneRules)
}
接口定义修改,避免访问没有定义的属性ts报错,增加
nativeProps?: { // ui框架原生属性
}
// @/components/BaseForm/components/index
interface defaultProps {
prop: string // key值
label?: string // label值
colNum?: number // 列宽 24等分
labelWidth?: number | string // label宽度
hide?: boolean // 是否隐藏(隐藏直接销毁dom)
disabled?: boolean // 是否禁用
required?: boolean // 是否必填
placeholder?: string // 描述字符
clearable?: boolean // 是否需要清除按钮
expandDefault?: boolean // 该字段展开收起默认值
prop2?: string // 附加字段(部分selelct等需要绑定两个key)
rules?: any[] // 附加检验规则
change?: (e: any) => void // change事件触发函数
nativeProps?: { // ui框架原生属性
[key: string]: any
}
}
子组件修改:
所有@/components/BaseForm/components/…中的组件增加{…props.config.nativeProps}
// @/components/BaseForm/components/BaseInput
实现效果如下,通过自定义组件并绑定form表单值:
{
label: '测试render',
prop: 'test',
type: 'render',
render: () => {
return
},
placeholder: '请输入',
},
实现思路:
首先表单接口定义columnsBase增加如下类型
// @/components/BaseForm/components/index
export interface renderProps extends defaultProps {
type: 'render'
render: () => any // 自定义组件render
}
表单渲染模块增加
// @/components/BaseForm/index
{
item.type === 'render' ? // 自定义render函数(只替换form-item-conent部分,label不会被render)
item?.render() : // ep-form-item__content 部分的render函数
componentRender(item) // 根据item:columnsFormBase中的type属性获取对应的自定义组件
}
接口定义:
// form表单配置接口
export interface formConfig {
...
isExpand?: boolean // 是否需要展示/收起按钮
...
}
// 子组件公用字段接口
interface defaultProps {
...
expandDefault?: boolean // 展开收起默认值
...
}
展开收起方法:
const currentExportState = ref(false) // 当前收起展开状态 默认收起
const expandFn = () => {
currentExportState.value = !currentExportState.value
const columns = _.cloneDeep(cloneConfig.columns)
columns.forEach((item: columnsBase) => {
if (!item.hide && item?.expandDefault !== undefined) {
item.expandDefault = currentExportState.value
}
})
cloneConfig.columns = columns
}
{cloneConfig.isExpand && (
{
expandFn()
}}
>
{currentExportState.value ? '收起' : '展开'}
)}
首先明确options配置需要支持3中方式:
接口定义:
// @/components/BaseForm/interface/index
export interface optionsFace {
options: {
[label: string]: any, children?: any[]
}[] // 直接传数组对象
| { type: 'dic'; key: string } // 字典获取
| { // 接口获取
type: 'api'
getData: () => Promise<{ [label: string]: any, children?: any[] }[]> // 必须返回对象数组,不一定是label,value格式
}
}
解析方式
// @/components/BaseForm/components/BaseSelect
const options = ref([])
const optionsLoading = ref(false)
watch(() => props.config.options, async() => {
if (Array.isArray(props.config.options)) { // 传入对象数组
options.value = props.config.options
} else if (Object.prototype.toString.call(props.config.options) === '[object Object]') { // 字典/接口获取
if (props.config.options.type === 'api') {
optionsLoading.value = true
options.value = await props.config.options.getData()
optionsLoading.value = false
} else if (props.config.options.type === 'dic') {
options.value = utils.getDicByKey(props.config.options.key)
}
}
}, { immediate: true, deep: true })
这里获取的options数组,其中的option项不一定都需要是label、value这种键值对,可以通过labelKey、valueKey来调整,参考接口配置:
// @/components/BaseForm/interface/index
// select组件接口定义
// defaultProps 基础属性定义、optionsFace options获取定义
export interface selectProps extends defaultProps, optionsFace {
....
labelKey?: string
valueKey?: string
...
}
表单及表单子组件均使用tsx语法编写,样式修改以及deep深度样式问题处理
vue.config.js增加配置,开启css module
// vue.config.js
...
css: {
requireModuleExtension: true, // 样式隔离 文件名必须为 *.module.* // https://cli.vuejs.org/zh/config/#css-requiremoduleextension
},
...
增加style.module.scss样式,一定要以***.module.scss结尾
.width100 {
width: 100%;
}
.BaseNumber, .BaseDateRange {
width: 100%;
:global(.ep-input-number) {
width: 100%;
}
:global(.ep-input-number .ep-input__wrapper) {
padding-left: 11px !important;
padding-right: 11px !important;
}
:global(.textLeft .ep-input__inner) {
text-align: left;
}
}
.BaseNumberRange {
width: 100%;
display: flex;
.inputNumber {
flex: 1;
}
.noControls {
:global(.ep-input__inner) {
text-align: left;
}
}
}
.BaseUpload {
.imgwrap {
.btn {
transform: scale(0.5);
display: none;
position: absolute;
top: 0px;
right: 0px;
}
&:hover {
.btn {
display: block;
}
}
}
.fileItem {
&:hover {
background: rgba(0,0,0, 0.1);
}
}
.elIconUploadDis {
width: 100%;
cursor: not-allowed;
:global(.ep-upload),
:global(.ep-upload--picture-card),
:global(.ep-upload-dragger) {
width: 100%;
cursor: not-allowed;
}
}
}
组件中导入及使用
// @/components/BaseForm/components/BaseInput
import styles from '@/components/BaseForm/style.module.scss'
// 使用
深度样式处理:
样式文件中添加:global既可样式穿透
:global(.ep-input-number) {
width: 100%;
}
解决:修改.eslintrc.js
// .eslintrc.js
...
parserOptions: {
...
parser: '@typescript-eslint/parser' // 修改或增加此行配置
}
https://chenyuhuan.gitee.io/backstage-vue3/#/form
https://blog.csdn.net/junner443/article/details/131302051
作者:快落的小海疼