大家好,我是前端架构师,关注微信公众号【程序员大卫】:
- 回复 [面试] :免费领取“前端面试大全2025(Vue,React等)”
- 回复 [架构师] :免费领取“前端精品架构师资料”
- 回复 [书] :免费领取“前端精品电子书”
- 回复 [软件] :免费领取“Window和Mac精品安装软件”
背景
表单是每一个前端开发者都无法回避的一个“小难题”。如果你是团队的组长,那么在日常的项目开发中,如何设计一个高效的表单配置,将直接决定我们的开发效率是否能达到事半功倍的效果。
设计思路
表单的核心由四大配置组成:
- 字段配置:
FormField - 表单行为配置:
FormProps - 表单状态:
FormState - 表单实例:
FormRef
1. FormField
定义每个字段的类型(type)、字段名称(field)、标签(label)等信息。
其 TypeScript 类型定义如下:
import type { Rule } from "ant-design-vue/es/form";
type BaseField<T extends string, F extends string> = {
type: T;
label?: string;
field: F;
col?: number;
props?: { placeholder: string };
slots?: Record<string, string>;
rules?: Rule[];
};
type Input<F extends string> = BaseField<"input", F>;
下面是一个 Input 字段的简单配置示例:
{
type: "input",
col: 12,
props: { placeholder: "请输入用户名" },
label: "用户名",
field: "username",
slots: { prefix: "input_prefix" },
rules: [{ required: true, message: "用户名不能为空" }],
}
2. FormProps
主要用于控制表单的提交方式、重置逻辑以及底部按钮的渲染方式。
FormConfig行为逻辑层,提供表单事件和交互方式;FormFooter表现层,描述底部 UI 的结构和交互行为;- 属于逻辑与交互的桥梁层。
TS 类型定义如下:
// 事件行为
type FormConfig<T> = {
onSubmit?: (values: T) => void;
onReset?: () => void;
};
// UI 行为
export type FormFooter = {
type?: string;
content: string;
props?: {
type?: string;
class?: string;
onClick?: () => void;
};
};
export type FormProps<T> = {
config: FormConfig<T>;
footer: FormFooter[];
};
3. FormState
FormState 是用户自定义的,它用于描述每一个表单字段的状态。TS 类型定义如下:
interface FormState {
username: string;
password: string;
city: string;
customContent: string;
}
const formState: FormState = {
username: "David",
password: "123456",
city: "ShangHai",
customContent: "hello",
};
它的每一个 key 值和 formField 中的 field 字段一一对应:
const formFields = [
{
type: "input",
label: "用户名",
field: "username", // 一一对应
// ...
},
{
type: "inputPassword",
label: "密码",
field: "password", // 一一对应
// ...
},
{
type: "custom",
component: CustomComponent,
label: "自定义组件",
field: "customContent", // 一一对应
// ...
},
{
type: "select",
label: "选择城市",
field: "city", // 一一对应
// ...
},
];
4. FormRef
控制表单组件对外暴露的操作,比如重置、校验和获取表单值。
由于基于 ant-design-vue 进行二次封装,因此 FormRef 实际上就是 ant-design-vue 表单的实例类型:
import type { FormInstance } from "ant-design-vue/es/form";
export { type FormInstance };
四个关键问题的思考
定义好类型之后,我们还需要思考以下几个核心问题:
1.表单布局
可以通过 FormField 中的 col 字段结合 <a-col :span="fieldItem.col"></a-col> 控制布局。注意:如果两个 FormField 的 col 值相加大于 24,后一个表单项需要自动换行。
提示:
<a-col></a-col>是ant-design-vue的布局组件。
为了解决这个问题,我们可以将字段配置转为一个二维数组,如果两个字段的 col 之和小于等于 24,则放在同一数组中,否则分开。例如:[[a, b], [c]] 或 [[a], [b], [c]]。
核心代码如下:
// 自动分组逻辑
const groupedFields = computed(() => {
const rows: FormField<T>[][] = [[]];
let currentSpan = 0;
props.fields.forEach((item) => {
const itemSpan = item.col || 24;
if (currentSpan + itemSpan > 24) {
rows.push([]);
currentSpan = 0;
}
rows[rows.length - 1].push(item);
currentSpan += itemSpan;
});
return rows;
});
2. 插槽名称重复
由于使用的是第三方组件库进行封装,部分组件的插槽名称存在重复问题,比如 Input 和 InputNumber 都使用了 prefix 插槽。
<script>
import { Input, InputNumber } from "ant-design-vue";
</script>
<template>
<Input>
<template #prefix>
<UserOutlined />
</template>
</Input>
<InputNumber>
<template #prefix>
<UserOutlined />
</template>
</InputNumber>
</template>
为避免插槽冲突,我们在 FormField 中为插槽提供了一个映射:
<script setup lang="ts">
const fields = [
{
type: "input",
slots: { prefix: "input_prefix" },
// ...
},
{
type: "inputNumber",
slots: { prefix: "inputNumber_prefix" },
// ...
},
}
</script>
<template>
<FormCustom v-bind="{ fields }">
<template #input_prefix>
<UserOutlined />
</template>
<template #inputNumber_prefix>
<LockOutlined />
</template>
</FormCustom>
</template>
3. 动态生成组件
我们使用 <component :is="xxx"></component> 实现组件的动态生成,并通过遍历 Field 的每一个 slots 项来渲染动态插槽。
关键代码如下:
<!-- 动态组件生成 -->
<component
:is="getComponent(item)"
v-bind="item.props"
v-model:value="modelValue[item.field]"
>
<!-- 动态插槽 -->
<template v-for="(slotName, slotKey) in item.slots" #[slotKey]>
<slot :name="slotName" />
</template>
</component>
其中 getComponent 用于将字段类型映射为实际的组件名:
<script>
import { Input, Select, TimePicker } from "ant-design-vue";
</script>
// 组件映射
const componentMap = {
input: Input,
inputPassword: Input.Password,
select: Select,
timePicker: TimePicker,
};
const getComponent = (item) => {
return item.type === "custom"
? item.component
: componentMap[item.type] || Input;
};
4. 表单联动
由于我们是基于 ant-design-vue 的二次开发,所以联动校验可以直接利用其内置的校验机制。我们只需要在 rules 中传入 validator 函数即可实现联动逻辑。
关键代码如下:
const fields = [
{
type: "inputPassword",
label: "密码",
field: "password",
rules: [{ required: true, validator: validatePass, trigger: "change" }],
},
{
type: "inputPassword",
label: "确认密码",
field: "confirmPassword",
rules: [
{ required: true, validator: validatePassConfirm, trigger: "change" },
],
},
];
总结
本方案解决了以下关键问题:
- 表单布局通过
col字段自动分组,避免超出一行宽度导致布局错乱。 - 插槽支持使用命名映射,解决组件之间插槽名重复的问题。
- 动态组件渲染配合动态插槽绑定,实现灵活的 UI 拼装。
- 利用
ant-design-vue的校验机制,通过validator函数轻松实现表单联动。