动态表单
对 el-form
二次封装,支持动态添加、删除表单项。
App.vue
<script setup lang="ts">
import { computed, ref } from "vue";
import FormProvider from "~/components/FormProvider.vue";
export interface FormSchemaItem {
label: string;
key: string;
type?: any;
props?: Record<string, any>;
[key: string]: any;
}
/** 表单数据 */
const formData = ref<Record<string, any>>({
name: undefined,
age: undefined,
gender: "male",
});
/**
* 表单配置
* type 可以是字符串,也可以是函数,返回组件
* key 是表单项的 key,也是 slot 的 name
*/
const formSchema = computed<FormSchemaItem[]>(() => [
{
label: "Name",
key: "name",
type: "input",
placeholder: "请输入姓名",
},
{
label: "Age",
key: "age",
type: "number",
props: {
placeholder: "请输入年龄",
},
},
{
label: "Gender",
key: "gender",
// 可以通过插槽来处理性别,无需传入 type 和 props
// type: 'radio',
// props: {
// options: [
// { label: '男', value: 'male' },
// { label: '女', value: 'female' },
// ],
// },
},
]);
/** 表单验证规则 */
const rules = ref<Record<string, any>>({
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
age: [{ required: true, message: "请输入年龄", trigger: "blur" }],
gender: [{ required: true, message: "请选择性别", trigger: "blur" }],
});
</script>
<template>
<FormProvider v-model="formData" :schema="formSchema" :rules="rules" style="width: 500px">
<template #gender> 我要自己处理性别 </template>
</FormProvider>
</template>
FormProvider.vue
<script setup lang="ts">
import type { FormInstance } from "element-plus";
import type { FormSchemaItem } from "~/pages/index.vue";
import { ElCheckbox, ElInput, ElInputNumber, ElRadio, ElRadioGroup, ElSelect } from "element-plus";
import { omit } from "lodash-es";
import { useTemplateRef } from "vue";
const { schema, rules } = defineProps<{
schema: FormSchemaItem[];
rules: Record<string, any>;
}>();
const modelValue = defineModel<Record<string, any>>({
required: true,
});
const COMPONENTS_MAP: Record<string, any> = {
input: ElInput,
number: ElInputNumber,
select: ElSelect,
checkbox: ElCheckbox,
radio: ElRadioGroup,
};
const formInstance = useTemplateRef<FormInstance>("formRef");
/** 暴露方法 */
defineExpose({
validate(...args: any[]) {
return formInstance.value?.validate(...args);
},
resetFields(...args: any[]) {
formInstance.value?.resetFields(...args);
},
});
/** 获取 Props */
function getProps(item: FormSchemaItem) {
if (item.props) return item.props;
return omit(item, ["type", "label", "key"]);
}
/** 获取组件 */
function getComponent(item: FormSchemaItem) {
const { type } = item;
if (type && typeof type !== "string") return type;
return COMPONENTS_MAP[type || "input"];
}
</script>
<template>
<el-form ref="formRef" :model="modelValue" :rules="rules" label-width="auto">
<el-form-item v-for="item in schema" :key="item.key" :label="item.label" :prop="item.key">
<slot :name="item.key">
<component :is="getComponent(item)" v-model="modelValue[item.key]" v-bind="getProps(item)">
<template v-if="item.type === 'radio'">
<ElRadio v-for="option in item.options || item.props?.options" :key="option.value" :value="option.value">
{{ option.label }}
</ElRadio>
</template>
</component>
</slot>
</el-form-item>
</el-form>
</template>
useFormProvider.ts
import { h, reactive, ref } from "vue";
import FormProvider from "~/components/FormProvider.vue";
export function useFormProvider(props) {
const formRef = ref();
const Component = (_, { slots }) => {
// 加了 reactive 是为了让 props 里面的响应值可以自动解包
return h(FormProvider, { ...reactive(props), ref: formRef }, slots);
};
return {
FormProvider: Component,
validate: formRef.value?.validate,
};
}