动态表单
config.ts
typescript
import type { Component } from 'vue'
import { transformModelValue } from '@cyb/shared'
import {
Cascader,
Checkbox,
CheckboxGroup,
DatePicker,
Input,
InputNumber,
Radio,
RadioGroup,
Select,
Slider,
Switch,
Textarea,
TimePicker,
TreeSelect,
} from 'ant-design-vue'
import { isString } from 'lodash-es'
import { h } from 'vue'
const formItemMap = new Map<string, Component>([
['input', transformModelValue(Input)],
['textarea', transformModelValue(Textarea)],
['number', transformModelValue(InputNumber)],
['time', transformModelValue(TimePicker)],
[
'date',
transformModelValue((props, { slots, attrs }) =>
h(DatePicker, { valueFormat: 'YYYY-MM-DD', ...attrs, ...props }, slots),
),
],
['cascader', Cascader],
['slider', Slider],
['checkbox', transformModelValue(Checkbox, 'checked')],
['checkboxGroup', CheckboxGroup],
['radio', Radio],
['radioGroup', transformModelValue(RadioGroup)],
['switch', Switch],
['treeSelect', TreeSelect],
['select', transformModelValue(Select)],
])
export function getFormItemComponent(type?: string | Component): Component {
if (type && !isString(type))
return type
// @ts-expect-error
return (type && formItemMap.get(type)) || formItemMap.get('input')
}
index.ts
typescript
export { default as VFormBuilder } from './index.vue'
export * from './types'
index.vue
vue
<template>
<a-form ref="formRef" :model="formData" :rules="rules" v-bind="formConfig">
<a-row>
<a-col v-for="item of formItemsComputed" :key="item.field" :span="item.span || span">
<a-form-item :name="item.field" v-bind="getFormItemProps(item)">
<template #label>
<span v-if="typeof item.label === 'function'">
<component :is="item.label as Function" />
</span>
<span v-else>{{ item.label }}</span>
</template>
<slot :name="item.field">
<component :is="ComponentItem" :item="item" />
</slot>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue'
import type { IFormItem } from './types'
import { h } from 'vue'
import { getFormItemComponent } from './config'
defineOptions({
name: 'VFormBuilder',
})
// 定义组件属性并设置默认值
const props = withDefaults(defineProps<Props>(), {
formItems: () => [], // 默认表单项为空数组
formConfig: () => ({}), // 默认表单配置为空对象
span: 24, // 默认列跨度为24
})
const baseFieldReg = /^(type|label|props|on|span|key|hidden|required|rules|col|formProps)$/
interface Props {
formItems: IFormItem[]
formConfig?: Record<string, any>
span?: number // 列跨度
rules?: Record<string, any>
}
// 定义表单数据模型
const formData = defineModel<Record<string, any>>({
default: () => ({}), // 默认值为空对象
})
// 表单实例引用
const formRef = ref<FormInstance>()
// 默认标签宽度
const defaultLabelWidth = '80px'
// 计算表单项,过滤掉隐藏的项
const formItemsComputed = computed(() => {
return props.formItems.filter(item => item.hidden !== true)
})
/**
* 获取表单项属性
*/
function getFormItemProps(formItem: IFormItem) {
const { formProps = {} } = formItem
const { labelCol = {}, ...rest } = formProps
const formLabelWidth = props?.formConfig?.labelCol?.style?.width
const labelWidth = labelCol?.style?.width === '0px' ? formLabelWidth : labelCol?.style?.width
return {
labelCol: {
style: {
width: labelWidth || defaultLabelWidth,
},
},
...rest, // 其他属性
}
}
const selectType = new Set(['select', 'date', 'time', 'treeSelect'])
const ComponentItem = {
props: ['item'],
setup({ item }) {
const props = Object.keys(item).reduce<Record<string, any>>(
(prev, key) => {
if (!baseFieldReg.test(key)) {
prev[key] = item[key]
}
return prev
},
{ ...item.props, formData: formData.value },
)
if (!('placeholder' in props)) {
const { type } = item
const text = selectType.has(type) ? '请选择' : '请输入'
props.placeholder = text + item.label
}
const tag = getFormItemComponent(item.type)
return () =>
h(
tag,
{
...props,
'modelValue': formData.value[item.field],
'onUpdate:modelValue': (val: string) => {
formData.value[item.field] = val
}, // 更新 modelValue
},
item.slots, // 插槽内容
)
},
}
/**
* 验证表单
*/
function validate() {
return formRef.value?.validate()
}
defineExpose({
validate,
})
</script>
types.ts
typescript
import type { Component, VNode } from 'vue'
/**
* 表单项
*/
export interface IFormItem<T extends object = Record<string, any>> {
// 表单项 label
label?: string | (() => VNode)
// 表单项绑定字段
field: string
// 占位符
placeholder?: any
// 禁用标识
disabled?: boolean
// 表单项属性,组件会将所有的 props 传递给 type 绑定的组件
props?: T
// 组件类型,根据所传递的类型,动态渲染表单项,默认显示为 input 输入框
type?: string | Component
// 表单项栅格数
span?: number
// 表单项唯一标识,未传递时会使用 field 作为唯一标识,若表单项中存在相同的 field 则必须传递 key
key?: string
// 隐藏标识
hidden?: boolean
// 是否必填
required?: boolean
// 栅格配置
[key: string]: any
}
使用示例
vue
<script setup lang="ts">
import { VFormBuilder } from '@cyb/core'
import locale from 'ant-design-vue/es/locale/zh_CN'
// dayjs 设置 dayjs 为中文语言
const formInstance = useTemplateRef('form')
const formData = ref({})
const formItems = [
{
type: 'input',
label: '姓名',
field: 'name',
placeholder: '请输入姓名',
},
{
type: 'date',
label: '出生日期',
field: 'birthDate',
placeholder: '请选择出生日期',
format: 'YYYY-MM-DD',
},
]
const rules = {
name: [{ required: true, message: '请输入姓名' }],
birthDate: [{ required: true, message: '请选择出生日期' }],
}
async function submit() {
await formInstance.value.validate()
console.log('formData.value ==> ', formData.value)
}
</script>
<template>
<a-config-provider :locale="locale">
<VFormBuilder
ref="form"
v-model="formData"
:rules="rules"
:form-items="formItems"
/>
<a-button @click="submit">
提交
</a-button>
</a-config-provider>
</template>