DynamicForm 动态表单组件
DynamicForm 是一个基于 ant-design-vue 的动态表单组件,用于快速构建和管理复杂的表单界面。它通过配置化的方式来生成表单,大大减少了表单开发的重复工作。
特性
- 🚀 配置驱动:通过简单的 JSON 配置即可生成完整表单
- 🎨 支持多种表单项:内置支持 Input、Select、Radio、Checkbox、DatePicker 等多种表单控件
- 🔄 表单联动:支持表单项之间的联动显示和校验
- 📦 灵活扩展:支持自定义表单项和插槽
- 🎯 校验集成:内置表单验证功能,支持自定义校验规则
- 💫 动态渲染:支持动态更新表单配置和选项
适用场景
- 需要快速构建表单的场景
- 表单结构经常变动的场景
- 需要处理复杂表单联动的场景
- 需要统一表单交互和样式的场景
Props
| 参数 | 说明 | 类型 | 必填 |
|---|---|---|---|
| value | 表单数据 | Record<string, any> | 是 |
| config | 表单配置 | DynamicFormConfig | 是 |
| formProps | 表单配置 | FormProps | 否 |
config
| 参数 | 说明 | 类型 | 必填 |
|---|---|---|---|
| formItems | 表单项配置 | FormItemConfig[] | 是 |
| formProps | 表单配置 | FormProps | 否 |
FormItems 配置说明
formItems 是一个数组,每个元素都是一个表单项配置对象,具有以下属性:
基础属性
key: string - 表单项的唯一标识,也是表单数据的字段名label: string | VNode - 表单项标签文本,如需要自定义label,可以使用VNode,示例:h( Tooltip, { title: '爱好', }, () => [h(QuestionCircleOutlined), ' 爱好'] )type: string - 表单项类型,支持以下类型:'input'- 输入框'select'- 下拉选择框'radio'- 单选框组'checkbox'- 复选框组'datePicker'- 日期选择器'timePicker'- 时间选择器'inputNumber'- 数字输入框'switch'- 开关'slot'- 自定义插槽
可选属性
tips: string | TipsConfig - 表单项后提示,区别于 formItemProps.tooltip(在label后)rules: RuleObject[] - 校验规则数组,遵循 ant-design-vue Form 的规则配置visible: boolean | ((formData: IFormData) => boolean) - 控制表单项是否显示options: { label: string; value: any; }[] - 用于 select/radio/checkbox 的选项列表props: object - 传递给表单控件的属性formItemProps: object - 传递给 Form.Item 的属性
示例配置
const formItems = [
{
key: 'username',
label: '用户名',
type: 'input',
rules: [
{ required: true, message: '请输入用户名' }
],
props: {
placeholder: '请输入用户名'
}
},
{
key: 'gender',
label: '性别',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
]
},
{
key: 'birthday',
label: '生日',
type: 'datePicker',
props: {
format: 'YYYY-MM-DD'
}
},
{
key: 'customField',
label: '自定义字段',
type: 'slot',
visible: (formData) => formData.gender === 'male'
}
]
特殊说明
- 当 type 为 'slot' 时,需要在模板中提供对应的具名插槽
- visible 函数可以根据表单数据动态控制表单项的显示/隐藏
- datePicker 类型会自动添加一些默认配置,可通过 props 覆盖
- select/radio/checkbox 类型必须提供 options 配置
Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| update:modelValue | 表单数据更新时触发 | (value: Record<string, any>) => void |
表单方法
| 方法名 | 说明 | 回调参数 |
|---|---|---|
| validate | 校验表单 | (nameList?: string[]) => Promise |
| validateFields | 校验表单 | (nameList?: string[]) => Promise |
| clearValidate | 清除校验 | (nameList?: string[]) => void |
| resetFields | 重置表单 | () => void |
| scrollToField | 滚动到指定表单项 | (name: string) => void |
使用方法
<template>
<DynamicForm
:value="formData"
:config="formConfig"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { DynamicFormConfig } from '@/components/DynamicForm/types';
const formData = ref({})
const formConfig: DynamicFormConfig = {
formItems: [
{
key: 'name',
label: '姓名',
type: 'input',
rules: [{ required: true, message: '请输入姓名' }]
},
{
key: 'gender',
label: '性别',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
]
}
],
formProps: {
layout: 'horizontal',
}
}
</script>
动态设置下拉选项
<template>
<DynamicForm :config="formConfig" :value="formData" ref="dynamicFormRef">
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { DynamicFormConfig } from '@/components/DynamicForm/types';
const formData = ref({});
export const hobbyOptions = ref<OptionItem[]>([]); // 动态设置下拉选项,适用后端接口返回数据的情况
const formConfig: DynamicFormConfig = {
formItems: [
{
key: 'hobby',
label: '爱好',
type: 'select',
options: hobbyOptions,
},
],
};
const setHobbyOptions = () => {
hobbyOptions.value = [
{ label: '篮球', value: 'basketball' },
{ label: '足球', value: 'football' },
{ label: '羽毛球', value: 'badminton' },
];
};
</script>
动态设置表单项是否显示
通过表单项的visible属性控制表单项的显示隐藏,提供2种方式,visible可以是一个布尔值,也可以是一个函数,函数返回布尔值
<template>
<DynamicForm :config="formConfig" :value="formData" ref="dynamicFormRef">
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { DynamicFormConfig } from '@/components/DynamicForm/types';
const formData = ref({});
const hobbyVisible = ref(false) //通过非表单项值控制显示隐藏,手动设置hobbyVisible的值即可
const formConfig: DynamicFormConfig = {
formItems: [
{
key: 'age',
label: '年龄',
type: 'select',
options: [
{
label: '10',
value: 10,
},
{
label: '11',
value: 11,
},
],
},
{
key: 'name',
label: '姓名',
type: 'input',
visible: (data) => data.age === 10, //通过表单项控制
},
{
ke y: 'hobby',
label: '爱好',
type: 'input',
visible: ()=>{
return hobbyVisible.value
},
},
],
};
</script>
插槽
通过插槽可以自定义表单项,插槽的值是一个函数,函数返回一个VNode,插槽的值可以是一个VNode,也可以是一个函数,函数返回一个VNode
<template>
<DynamicForm :config="formConfig" :value="formData" ref="dynamicFormRef">
<template #rate>
<Button type="primary">插槽</Button>
</template>
</DynamicForm>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { FormChangeValue } from '@/components/DynamicForm/types';
const formData = ref({});
const formConfig: DynamicFormConfig = {
formItems: [
{
key: 'rate',
label: '插槽',
type: 'slot',
},
],
};
</script>
一行多个表单项的情况
这个例子演示了一个表单中包含多个表单控件的情况。
<template>
<DynamicForm :config="formConfig" :value="formData" ref="dynamicFormRef">
<template #space>
<a-space>
<a-form-item name="area" :rules="[{ required: true, message: '请输入area' }]">
<a-input v-model:value="formData.area" placeholder="请输入area" />
</a-form-item>
<a-form-item name="price" :rules="[{ required: true, message: '请输入price' }]">
<a-input v-model:value="formData.price" />
</a-form-item>
</a-space>
</template>
</DynamicForm>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { FormChangeValue } from '@/components/DynamicForm/types';
const formData = ref<IFormData & { rate: number; area: string; price: string }>({
name: '张三',
rate: 3,
area: '',
price: '456',
});
const formConfig: DynamicFormConfig = {
formItems: [
{
key: 'space',
label: '一行多个',
type: 'slot',
},
],
};
</script>
表单项后展示tips
- 直接显示文本
const formConfig: DynamicFormConfig = {
formItems: [
{
key: 'area',
label: 'area',
type: 'input',
tips: '请输入area', // 表单项后展示tips
},
],
};
- 显示图标,hover展示提示
const formConfig: DynamicFormConfig = {
formItems: [
{
key: 'area',
label: 'area',
type: 'input',
tips: {
withIcon: true,
text: '请输入area',
},
},
],
};
表单onChange
表单会emit出来onChange事件,可以监听这个事件,来获取表单数据的变化
<template>
<DynamicForm :config="formConfig" :value="formData" ref="dynamicFormRef" onChange='onChangeForm' />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { DynamicFormConfig, FormChangeValue } from '@/components/DynamicForm/types';
const onChangeForm = ({key,value,formValues}: FormChangeValue) => {
console.log('----val----36', key, value,formValues);
};
</script>
tips
- 如果不想显示label,可以设置 formItemProps.noStyle 为true
const formConfig: DynamicFormConfig = {
formItems: [
{
key: 'rate',
label: '插槽',
type: 'slot',
formItemProps:{
noStyle:true //隐藏label
}
},
],
};
源码:
<script lang="ts" setup>
import { ref, computed, unref, reactive, toRaw, defineEmits } from 'vue';
import {
Input,
Select,
RadioGroup,
CheckboxGroup,
DatePicker,
TimePicker,
InputNumber,
Switch,
Cascader,
Form,
TreeSelect,
Tooltip,
} from 'ant-design-vue';
import type { DynamicFormConfig, FormItemConfig, IFormData, FormChangeValue } from './types';
import type { FormInstance } from 'ant-design-vue';
const emit = defineEmits<{
change: [value: FormChangeValue];
}>();
const formRef = ref<FormInstance>();
const props = defineProps<{
config: DynamicFormConfig;
value: IFormData;
}>();
const modelValue = reactive<IFormData>(toRaw(props.value));
// 渲染表单项
const renderFormItem = (item: FormItemConfig) => {
// 根据type渲染不同的组件
const component = {
input: Input,
select: Select,
radio: RadioGroup,
checkbox: CheckboxGroup,
datePicker: DatePicker,
timePicker: TimePicker,
inputNumber: InputNumber,
switch: Switch,
cascader: Cascader,
treeSelect: TreeSelect,
};
// 添加事件
item.props = {
...item.props,
onChange: () => {
emit('change', { key: item.key, value: modelValue[item.key], formValues: toRaw(modelValue) });
},
};
if (['input', 'inputNumber'].includes(item.type)) {
item.props = {
placeholder: '请输入',
...item.props,
};
}
// 设置options
if (['checkbox', 'select', 'radio', 'cascader'].includes(item.type)) {
item.props = {
placeholder: '请选择',
...item.props,
options: unref(item.options) || [],
};
}
if (item.type === 'treeSelect') {
item.props = {
placeholder: '请选择',
...item.props,
treeData: unref(item.options) || [],
};
}
// 对于 DatePicker,我们可以添加一些默认配置
if (item.type === 'datePicker') {
item.props = {
allowClear: true,
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
...item.props,
};
}
return component[item.type as keyof typeof component];
};
// 校验规则
const rulesRef = computed(() => {
const rules: Record<string, NonNullable<FormItemConfig['rules']>> = {};
props.config.formItems.forEach((item) => {
if (item.rules && Array.isArray(item.rules) && item.rules.length > 0) {
rules[item.key] = item.rules;
}
});
return rules;
});
// 展示的表单项
const visibleItems = computed(() => {
return props.config.formItems.filter((item) => {
if (item.visible === false) return false;
if (typeof item.visible === 'function') {
return (item.visible as (data: IFormData) => boolean)(modelValue);
}
return true;
});
});
// 定义表单方法
const formMethods = {
validate: () => formRef.value?.validate(),
validateFields: (nameList?: string[]) => formRef.value?.validateFields(nameList),
clearValidate: (nameList?: string[]) => formRef.value?.clearValidate(nameList),
resetFields: () => formRef.value?.resetFields(),
scrollToField: (name: string) => formRef.value?.scrollToField(name),
};
// 校验表单
defineExpose(formMethods);
</script>
<template>
<div class="dynamic-form">
<Form v-bind="props.config.formProps" ref="formRef" :model="modelValue" :rules="rulesRef">
<Form.Item
v-for="item in visibleItems"
:key="item.key"
:label="item.label"
:name="item.key"
:rules="item.rules"
v-bind="item.formItemProps"
>
<template v-if="item.type === 'slot'">
<slot :name="item.key" :data="modelValue" :item="item" />
</template>
<div v-else class="form-item-content">
<component
class="__component"
:is="renderFormItem(item)"
v-model:value="modelValue[item.key]"
v-bind="{ ...item.props }"
/>
<div class="__right">
<span v-if="item.tips && typeof item.tips === 'string'" class="tips">{{ item.tips }}</span>
<Tooltip
class="tips"
v-else-if="item.tips && typeof item.tips === 'object' && item.tips.withIcon"
:title="item.tips.text"
>
<QuestionCircleOutlined />
</Tooltip>
</div>
</div>
</Form.Item>
</Form>
</div>
</template>
<style scoped lang="scss">
.dynamic-form {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.form-item-content {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
.__left {
max-width: 600px;
}
.__component {
width: 80%;
min-width: 200px;
}
.__right {
flex-shrink: 0;
flex: 1;
.tips {
color: #999;
}
}
}
}
</style>