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

实例效果

image

相关代码

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>

posted @ 2025-06-27 20:41  Felix_Openmind  阅读(83)  评论(0)    收藏  举报
*{cursor: url(https://files-cdn.cnblogs.com/files/morango/fish-cursor.ico),auto;}