基于AntDesign封装DynamicForm实现动态表单生成
实例效果

相关代码
DynamicForm.vue
<template>
<a-form ref="formRef" :model="formModel" v-bind="$attrs">
<a-row :gutter="gutter">
<a-col v-for="(item, index) in formSchema" :key="index" :span="colSpan">
<a-form-item
:label="item.label"
:label-col="{span: wrapperLabelCol}"
:name="item.field"
v-bind="item.formItemProps"
>
<span v-if="item.loading">
<LoadingOutlined style="margin-right: 4px" />数据加载中...
</span>
<!-- 定制化表单组件 start--->
<a-input v-else-if="item.component === 'FWNX'" v-model:value="formModel['fwnx']" readOnly />
<a-input
placeholder="请选择带辅民警"
@click="onUserSelectModal('DFMJ')"
v-else-if="item.component === 'USER_MODAL' && item.field === 'dfmjmc'"
v-model:value="customState.dfmjmc" readOnly />
<a-input
placeholder="请选择责任领导"
@click="onUserSelectModal('ZRLD')"
v-else-if="item.component === 'USER_MODAL' && item.field === 'zrldmc'"
v-model:value="customState.zrldmc" readOnly />
<a-tree-select
v-model:value="formModel['deptid']"
allow-clear
@select="handleTreeClick"
placeholder="请选择所在部门"
:treeData="deptTreeData"
v-if="item.component === 'DeptTreeSelect'">
</a-tree-select>
<!-- 定制化表单组件 end --->
<component
:is="componentsMap[item.component]"
v-else
v-model:value="formModel[item.field]"
v-bind="item.componentProps"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<UserSelectModal @control-visible="onModalVisibleChange"
:title="useModalTitle"
:visible="isModalVisible"
@select="handleConfirmSelect"/>
</template>
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { componentsMap, defaultComponentProps } from '@/utils/config'
import { LoadingOutlined } from '@ant-design/icons-vue'
import dayjs from 'dayjs'
import { isEmpty } from 'lodash'
import { deptTree } from '@/api/smart/system'
import UserSelectModal from '@/views/pages/archive/components/ModalComs/AddUpdateComs/UserSelectModal.vue'
import { useHzModal } from '@/views/pages/archive/components/ModalComs/AddUpdateComs/extends/useHzModal'
import { message } from 'ant-design-vue'
import { useStore } from 'vuex'
const {isModalVisible,
onModalVisibleChange,
handleOk,
handleCancel,
modalLoading,
onModalLoadingChange,} = useHzModal()
const onSelectDfmj = (row) => {
console.log('output-> row DFMJ', row)
}
const onSelectZrld = (row) => {
console.log('output-> row ZRLD', row)
}
const activeModalType = ref('DFMJ')
const handleConfirmSelect = (payload) => {
if(activeModalType.value === 'DFMJ') {
customState.dfmjmc = payload.a0101
customState.dfmj = payload.personid
}
if(activeModalType.value === 'ZRLD') {
customState.zrldmc = payload.a0101
customState.zrld = payload.personid
}
}
const useModalTitle = ref('')
const onUserSelectModal = (type = 'DFMJ') => {
activeModalType.value = type
useModalTitle.value = type === 'DFMJ' ? '带辅民警选择' : '责任领导选择'
onModalVisibleChange(true)
}
const props = defineProps({
gutter: {
type: [Number, Array],
default: 16
},
colSpan: {
type: [Number, Array],
default: 8
},
wrapperLabelCol: {
type: [Number, Array],
default: 10
},
// 表单项配置
schema: {
type: Array,
default: () => []
},
// 表单model配置,一般用于默认值、回显数据
model: {
type: Object,
default: () => ({})
},
// 组件属性配置
componentProps: {
type: Object,
default: () => ({})
}
})
const formRef = ref(null)
const formSchema = ref([])
const formModel = ref({})
const customState = reactive({
dfmj: '',
dfmjmc: '',
zrld: '',
zrldmc: '',
deptid: '',
dwmc: '',
})
// 组件placeholder
const getPlaceholder = (x) => {
let placeholder = ''
switch (x.component) {
case 'Text':
case 'Textarea':
placeholder = `请输入${x.label}`
break
case 'RangePicker':
placeholder = ['开始时间', '结束时间']
break
case 'TreeSelect':
case 'Select':
case 'Cascader':
case 'DatePicker':
placeholder = `请选择${x.label}`
break
default:
placeholder = ''
break
}
return placeholder
}
const store = useStore()
const isEditMode = computed(() => store.state.archive.modalStatus === '修改')
watch(() => isEditMode.value, () => {
initForm()
}, {deep: true})
// 组件属性componentProps, 注意优先级:组件自己配置的componentProps > props.componentProps > config.js中的componentProps
const getComponentProps = (x) => {
if (!x?.componentProps) x.componentProps = {}
// 使得外层可以直接配置options
if (x.hasOwnProperty('options') && x.options) {
x.componentProps.options = []
const isFunction = typeof x.options === 'function'
const isArray = Array.isArray(x.options)
if (isFunction || isArray) {
// 函数时先赋值空数组
x.componentProps.options = isFunction ? [] : x.options
x.componentProps.treeData = isFunction ? [] : x.options
}
if (x.component === 'TreeSelect' && Array.isArray(x.componentProps.options)) {
console.log('output-> x.componentProps', x.componentProps)
x.componentProps.treeData = x.componentProps.options
x.componentProps.placeholder = getPlaceholder(x)
delete x.componentProps.options // 防止错误渲染
}
}
return {
placeholder: x?.componentProps?.placeholder ?? getPlaceholder(x),
...(defaultComponentProps[x.component] || {}), // config.js带过来的基础componentProps默认配置
...(props.componentProps[x.component] || {}), // props传进来的组件componentProps配置
...x.componentProps // 组件自身的componentProps
}
}
// 表单属性formItemProps
const getFormItemProps = (x) => {
let result = { ...(x.formItemProps || {}) }
// 使得外层可以直接配置required必填项
if (x.hasOwnProperty('required') && x.required) {
result.rules = [
...(x?.formItemProps?.rules || []),
{
required: true,
message: getPlaceholder(x),
trigger: 'blur'
}
]
}
return result
}
// 各组件为空时的默认值
const getDefaultEmptyValue = (x) => {
let defaultEmptyValue = ''
switch (x.component) {
case 'Text':
case 'Textarea':
defaultEmptyValue = ''
break
case 'Select': // 单选
defaultEmptyValue = ['tag', 'multiple'].includes(x?.componentProps?.mode)
? ''
: undefined
break;
case 'Cascader':
// x.componentProps.treeDefaultExpandAll = true
// x.componentProps.allowClear = true
defaultEmptyValue = x?.value?.length ? x.value : []
break
case 'TreeSelect': // 单选
defaultEmptyValue = x?.value?.length ? x.value : ''
break;
default:
defaultEmptyValue = undefined
break
}
return defaultEmptyValue
}
// 格式化各组件值
const getValue = (x) => {
let formatValue = x.value
if (!!x.value) {
switch (x.component) {
case 'DatePicker':
formatValue = dayjs(x.value, 'YYYY-MM-DD')
break
default:
formatValue = x.value
break
}
}
return formatValue
}
const getSchemaConfig = (x) => {
return {
...x,
componentProps: getComponentProps(x),
formItemProps: getFormItemProps(x),
value: x.value ?? getDefaultEmptyValue(x)
}
}
const emits = defineEmits(['handleEchoData'])
watch(() => formModel.value, () => {
emits('handleEchoData', formModel.value)
}, {deep: true})
const setFormModel = () => {
formModel.value = formSchema.value.reduce((pre, cur) => {
if (!pre[cur.field]) {
// 表单初始数据(默认值)
pre[cur.field] = getValue(cur)
return pre
}
}, {})
}
watch(
() => formModel.value.jrdwsj,
(newVal) => {
if (newVal) {
const startYear = dayjs(newVal).year() // 入职年份 eg: 2018
const currentYear = dayjs().year() // 当前年份 eg: 2025
const yearsOfWork = currentYear - startYear + 1 // eg: 2025 - 2018 + 1 = 8
formModel.value.fwnx = yearsOfWork
}
},
{
immediate: false,
deep: false
}
)
// 表单初始化
const initForm = () => {
if (props.schema?.map === undefined) return
formSchema.value = props.schema.map((x) => getSchemaConfig(x))
// model初始数据
setFormModel()
// options-获取异步数据
formSchema.value.forEach(async (x) => {
if (x.component === 'TreeSelect' && x.options && typeof x.options === 'function') {
x.loading = true
x.componentProps.treeData = await x.options(formModel.value)
x.loading = false
}
if (['Select', 'Radio'].includes(x.component) && x.options && typeof x.options === 'function') {
x.loading = true
x.componentProps.options = await x.options(formModel.value)
x.loading = false
}
})
}
// watch(() => props.schema, () => {
// if (!isEmpty(props.schema)) {
// initForm()
// }
// }, { deep: true, immediate: true })
const recursiveDept = (arr) => {
return arr.map(item => {
return {
key: item.id,
label: item.label,
value: item.id,
children: isEmpty(item.children) ? [] : recursiveDept(item.children)
}
})
}
const deptTreeData = ref([])
const generateDeptTreeData = async () => {
const resData = await deptTree({ deptName: '', showStatus: 1 })
deptTreeData.value = recursiveDept(resData.data.data)
}
const handleTreeClick = (value, node, extra) => {
customState.deptid = value
customState.dwmc = node.label
}
onMounted(() => {
generateDeptTreeData()
initForm()
watch(
() => props.model,
(newVal) => {
// model重新赋值给formSchema,注意:model会覆盖schema配置的value值
formSchema.value.forEach((x) => {
for (const key in newVal) {
if (x.field === key) {
x.value = newVal[key]
}
}
})
setFormModel()
},
{
immediate: true,
deep: true
}
)
})
const hasLoadingSchema = computed(() =>
formSchema.value.some((x) => x.loading)
)
// 表单验证
const validateFields = () => {
// if (hasLoadingSchema.value) {
// console.log("正在加载表单项数据...");
// return;
// }
return new Promise((resolve, reject) => {
formRef.value
.validateFields()
.then((formData) => {
resolve(formData);
})
.catch((err) => {
message.warn('请完善表单内容后再提交');
reject(err)
});
});
}
// 表单重置
const resetFields = (isInit = true) => {
// 是否清空默认值
if (isInit) {
formModel.value = {}
}
formRef.value.resetFields()
}
// 暴露方法
defineExpose({
validateFields,
customState,
resetFields
})
</script>
<style>
.ant-input::placeholder {
color: grey !important;
}
</style>
config.js
import {
Input,
Textarea,
InputNumber,
Select,
RadioGroup,
CheckboxGroup,
DatePicker,
TreeSelect
} from 'ant-design-vue'
// 表单域组件类型
export const componentsMap = {
Text: Input,
TreeSelect: TreeSelect,
Textarea,
Number: InputNumber,
Select,
Radio: RadioGroup,
Checkbox: CheckboxGroup,
DatePicker,
DateYearPicker: DatePicker,
}
// 配置各组件属性默认值,相关配置项请查看ant-design官网各组件api属性配置
export const defaultComponentProps = {
TreeSelect: {
allowClear: true,
bordered: true,
disabled: false,
showArrow: true,
treeCheckable: false,
labelInValue: false,
dropdownStyle: { maxHeight: '400px', overflow: 'auto' }, // 优化下拉高度
placeholder: '请选择', // 添加默认 placeholder
virtual: true, // 启用虚拟滚动(节点多时建议打开)
treeDefaultExpandAll: false, // 默认不展开所有节点,性能更好
style: {
width: '100%'
}
},
Text: {
allowClear: true,
bordered: true,
disabled: false,
showCount: false,
maxlength: 20,
},
Textarea: {
allowClear: true,
autoSize: { minRows: 4, maxRows: 4 },
showCount: true,
maxlength: 200,
style: {
width: '100%'
}
},
Select: {
allowClear: true,
bordered: true,
disabled: false,
showArrow: true,
optionFilterProp: 'label',
optionLabelProp: 'label',
showSearch: true,
},
DateYearPicker: {
allowClear: true,
bordered: true,
disabled: false,
valueFormat: 'YYYY',
format: 'YYYY',
picker: 'year',
style: {
width: '100%'
}
},
DatePicker: {
allowClear: true,
bordered: true,
disabled: false,
valueFormat: 'YYYY-MM-DD',
format: 'YYYY-MM-DD',
picker: 'date',
style: {
width: '100%'
}
},
}
AddUser.vue 使用上述组件
<template>
<main class="basic-info">
<div class="right-form">
<DynamicForm ref="formRef" :labelCol="{ span: 4 }" :model="model" :schema="realScheme"
:wrapperCol="{ span: 20 }" />
</div>
</main>
</template>
<script setup>
import DynamicForm from './DynamicForm.vue'
import { ref, watch } from 'vue'
import { listDict } from '@/api/smart/dict'
import { isEmpty } from 'lodash'
import { useDict } from '@/hooks/useDict'
const props = defineProps({
visible: {
type: Boolean,
default: false
}}
)
// 工具函数统一封装 options
const { generateTreeData, generateOptions} = useDict()
const realScheme = ref([
{
label: '姓名', field: 'xm', component: 'Text',
required: true
},
{
label: '身份证号', field: 'sfzh', component: 'Text',
required: true
},
{
label: '警辅工号', field: 'fjh', component: 'Text',
required: false
},
{
label: '所在部门',
field: 'deptid',
component: 'DeptTreeSelect',
required: true,
},
{
label: '岗位名称', field: 'gwmc', component: 'TreeSelect',
required: false,
options: generateTreeData('fjgwmc_tree')
},
{
label: '岗位类别', field: 'fjgwlb', component: 'Select',
required: false,
options: generateOptions('fjgwlb')
},
{
label: '进入单位时间', field: 'jrdwsj', component: 'DatePicker',
required: false
},
{
label: '服务年限', field: 'fwnx', component: 'FWNX',
required: false,
readOnly: true,
},
{
label: '保障渠道', field: 'bzqd', component: 'TreeSelect',
required: false,
options: generateTreeData('fjbzqd_tree'),
},
{
label: '工资卡号', field: 'gzkh', component: 'Text',
required: false
},
{
label: '带辅民警', field: 'dfmjmc', component: 'USER_MODAL',
required: false
},
{
label: '责任领导', field: 'zrldmc', component: 'USER_MODAL',
required: false
},
{
label: '手机号码', field: 'sjhm', component: 'Text',
required: false
},
{
label: '外线', field: 'wx', component: 'Text',
required: false
},
{
label: '内线', field: 'nx', component: 'Text',
required: false
},
{
label: '劳动关系隶属单位', field: 'lwpqgs', component: 'Text',
required: false
},
{
label: '性别', field: 'xb', component: 'Select',
required: false,
options: generateOptions('AX')
},
{
label: '出生日期', field: 'csrq', component: 'DatePicker',
required: false
},
{
label: '血型', field: 'xx', component: 'Select',
required: false,
options: generateOptions('XX')
},
{
label: '民族', field: 'mz', component: 'Select',
required: false,
options: generateOptions('AE')
},
{
label: '籍贯', field: 'jg', component: 'TreeSelect',
required: false,
options: generateTreeData('AB_tree')
},
{
label: '出生地', field: 'csd', component: 'Text',
required: false
},
{
label: '居住地', field: 'xzd', component: 'Text',
required: false
},
{
label: '户籍所在地', field: 'hjd', component: 'TreeSelect',
required: false,
options: generateTreeData('AB_tree')
},
{
label: '政治面貌', field: 'zzmm', component: 'Select',
required: false,
options: generateTreeData('AT')
},
{
label: '入党(团)时间', field: 'rdtsj', component: 'DatePicker',
required: false
},
{
label: '健康状况', field: 'jkzk', component: 'TreeSelect',
required: false,
options: generateOptions('BF_tree')
},
{
label: '婚姻状况', field: 'hyzk', component: 'Select',
required: false,
options: generateOptions('BG')
},
{
label: '有无重大病史', field: 'ywzdbs', component: 'Select',
required: false,
options: () => {
return [
{ value: '有', label: '有' },
{ value: '无', label: '无' }
]
}
},
{
label: '学历', field: 'xl', component: 'TreeSelect',
required: false,
options: generateTreeData('AM_tree'),
},
{
label: '专业类别', field: 'zy', component: 'TreeSelect',
required: false,
options: generateTreeData('fjzymc_tree')
},
{
label: '毕业时间', field: 'bysj', component: 'DatePicker',
required: false
},
{
label: '毕业院校', field: 'xxmc', component: 'Text',
required: false
},
{
label: '是否服兵役', field: 'sffby', component: 'Text',
required: false
},
{
label: '持有驾照', field: 'cyjz', component: 'Text',
required: false
},
{
label: '专业特长', field: 'zytc', component: 'Text',
required: false
},
{
label: '任职状态', field: 'ryzt', component: 'Select',
required: true,
options: generateOptions('fjrzzt'),
},
{
label: '岗位层级', field: 'gwcj', component: 'Select',
required: false,
options: generateOptions('fjgwcj'), // 岗位层级
},
{
label: '用工方式', field: 'ygfs', component: 'Radio',
required: false,
options: generateOptions('fjygfs'),
},
{
label: '初次参加辅警工作时间', field: 'cjgagzsj', component: 'DatePicker',
required: false
},
{
label: '离职时间', field: 'lzsj', component: 'DatePicker',
required: false
},
{
label: '身份性质', field: 'sfxz', component: 'Select',
required: false,
options: generateOptions('fjsfxz'),
}
])
const mockSchema = ref([
{
label: '姓名',
field: 'name',
component: 'Text',
required: false
},
{
label: '性别',
field: 'sex',
component: 'Radio',
options: [
{ value: 1, label: '男' },
{ value: 2, label: '女' },
{ value: 3, label: '保密' }
],
value: 1,
required: false
},
{
label: '生日',
field: 'birthday',
component: 'DatePicker',
required: false
},
{
label: '兴趣',
field: 'hobby',
component: 'Checkbox',
// options: async () => {
// // 后台返回的数据list
// const list = [
// { value: 1, label: '足球' },
// { value: 2, label: '篮球' },
// { value: 3, label: '排球' }
// ]
// return await getRemoteData(list)
// }
},
{
label: '国家',
field: 'country',
component: 'Select',
options: [
{ value: 1, label: '中国' },
{ value: 2, label: '美国' },
{ value: 3, label: '俄罗斯' }
]
},
{
label: '简介',
field: 'desc',
component: 'Textarea'
}
])
const model = ref({})
const formRef = ref(null)
// 提交
const handleSubmit = async () => {
const formData = await formRef.value.validateFields()
const customState = formRef.value.customState
return {
...formData,
...customState,
}
// console.log('output-> formData::: ', formData)
// if (formData.birthday) {
// formData.birthday = dayjs(formData.birthday).format("YYYY-MM-DD");
// }
// console.log("提交信息:", formData);
}
// 重置
const handleReset = (isInit) => {
if(formRef.value && formRef.value.resetFields) {
formRef.value.resetFields(isInit)
}
}
watch(() => props.visible, (val) => {
if(val) {
handleReset(true)
}
}, {deep: true, immediate: true})
defineExpose({
handleSubmitFunc: handleSubmit,
resetFunc: handleReset
})
</script>
<style lang="scss" scoped>
.basic-info {
padding: 10px 48px;
display: flex;
.left-user {
}
.right-form {
padding-right: 10px;
height: 660px;
overflow-y: scroll;
overflow-x: hidden;
}
}
</style>
学而不思则罔,思而不学则殆!

浙公网安备 33010602011771号