我正在参加「掘金·启航计划」
背景
在我近期的后台管理系统相关的业务需求开发中,非常多的表单需求,各个表单之间极其雷同,可以考虑通用配置化表单。
通用配置化表单优点:
- 快速构建不同类型的表单,满足不同场景下的需求,提高工作效率。
- 良好的扩展性和可维护性。基于统一的配置模板和组件,可以方便地进行维护和更新,适应业务的发展和变化。
如果您的业务需要构建表单,你想使用通用配置化表单,请您驻足继续往下浏览与审视✌️
技术框架:vue3+ElementUI+TypeScript
通用表单的具体实现
功能
- 通过一份配置生成表单,除了可以渲染elementUI的下拉、输入等组件,还可以渲染二次封装的组件。
- 表单值的改变可以影响另一项的显示隐藏,减少v-if、v-show的逻辑编写。
- 表单值的改变可以影响另一项的值的改变,减少@change的。
- 表单值的改变可以清空另一项的值,解决联动项需要清空其他项的问题。
- 配置:不同类型的配置使用TypeScript进行静态代码检查。
实现方式
- 配置表的定义,读取配置表,根据不同类型渲染不同类型的组件。
- 根据配置生成一份数据和一份初始化数据(用来重置或者清除筛选项)
- 处理表单显示隐藏(联动单个值、多个值、或自定义)
- 清空相关联动项(包括关联项身上绑定的其他关联项的清空)
具体交互如下图:
通用表单公共组件
封装核心组件
typeToComponent()根据类型渲染组件typeToPropsData()根据类型渲染propstypeToComEvents()根据类型返回监听事件
通过动态组件的方式将组件引用进来,就可以根据类型渲染不同的组件,实现不同内容的渲染与监听。
// BaseConfigForm.vue
<template>
// props.configItem为当前项的值,props.config.value为解决表单多层级使用
<component
:is="typeToComponent(props.configItem)"
v-bind="typeToPropsData(props.configItem,props.config.value)"
v-on="typeToComEvents(props.configItem,props.config.value)" />
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from "vue";
import BaseSelect from "@/components/BaseSelect.vue";
import RadioGroup from "@/components/RadioGroup.vue";
import CheckboxGroup from "@/components/CheckboxGroup.vue";
import InputWildCard from "@/components/InputWildCard.vue";
import DateTimePicker from "@/components/DateTimePicker.vue";
import SelectInput from "@/components/SelectInput.vue";
// ......引入组件,定义自己的组件增加
// TypeScript的类型校验
import { Field } from "./type";
const props = defineProps(["configItem", "config", "customForm", "optionDataMap"]);
const emit = defineEmits(["clearValues"]);
// 根据类型渲染组件
const typeToComponent = (field: Field): any => {
switch (field.type) {
case "select":
return BaseSelect;
case "switch":
return "el-switch";
case "radioGroup":
return RadioGroup;
case "checkboxGroup":
return CheckboxGroup;
case "inputwildcard":
return InputWildCard;
case "datetimepicker":
return DateTimePicker;
case "selectinput":
return SelectInput;
default:
return "el-input";
}
};
// 根据类型渲染props
const typeToPropsData = (field: Field, configValue: string): any => {
switch (field.type) {
case "select":
return {
type: field.selectType,
params: field.params,
placeholder: field.placeholder || field.label,
filterable: field.filterable || true,
clearable: field.clearable || true,
multiple: field.multiple || false,
multipleLimit: field.multipleLimit,
disabled: field.disabled || false,
optionList: field.optionsList
};
case "switch":
return {
"active-value": field["active-value"],
"inactive-value": field["inactive-value"],
disabled: field.disabled || false
};
case "radioGroup":
return {
radioType: field.radioType,
radioList: field.optionsList,
disabledOptions: field.disabledOptions
};
case "checkboxGroup":
return {
checkboxList: field.optionsList,
disabledOptions: field.disabledOptions
};
case "inputwildcard":
return {
params: field.params
};
case "datetimepicker":
return {
bidTimeStr: field.value
};
case "selectinput":
return {
selectValue: handleConfigValue(configValue, field.selectformKey),
placeholder: field.placeholder,
selectPlaceholder: field.selectPlaceholder,
optionsList: field.optionsList,
disabled: field.disabled || false,
disabledSelect: field.disabledSelect || false
};
default:
return {
clearable: field.clearable || true,
disabled: field.disabled || false,
placeholder: field.placeholder || field.label
};
}
};
// 根据类型返回监听事件
const typeToComEvents = (field: Field, configValue: string): any => {
switch (field.type) {
case "select":
return {
change: (val: any) => {
// 如果存在联动项,清空相关联动项
clearValues(field, configValue);
if (field.change) {
return field.change(val);
}
},
optionData: (val: any) => {
props.optionDataMap[field.formKey] = val;
}
};
case "switch":
return {
change: (val: any) => {
// 如果存在联动项,清空相关联动项
clearValues(field, configValue);
if (field.change) {
return field.change(val);
}
}
};
case "radioGroup":
return {
change: (val: any) => {
// 如果存在联动项,清空相关联动项
clearValues(field, configValue);
if (field.change) {
return field.change(val);
}
}
};
case "checkboxGroup":
return {
change: (val: any) => {
// 如果存在联动项,清空相关联动项
clearValues(field, configValue);
if (field.change) {
return field.change(val);
}
}
};
case "datetimepicker":
return {
getDateFn: (val: any) => {
console.log(field.label, val);
// 如果存在联动项,清空相关联动项
clearValues(field, configValue);
if (field.change) {
return field.change(val);
}
}
};
case "selectinput":
return {
"update:selectValue": (val: string | number) => {
handleSetConfigValue(configValue, field.selectformKey, val);
},
change: (inputValue: any, selectValue:any) => {
if (field.change) {
return field.change(inputValue, selectValue);
}
},
selectChange: (val:any) => {
if (field.selectChange) {
return field.selectChange(val);
}
}
};
default:
return {
change: (val: any) => {
// 如果存在联动项,清空相关联动项
clearValues(field, configValue);
if (field.change) {
return field.change(val);
}
}
};
}
};
// 清空相关联动项
const clearValues = (field: Field, configValue: string) => {
emit("clearValues", field, configValue);
};
// 处理表单值的绑定
const handleConfigValue = (configValue: string, selectformKey:string) => {
if (configValue) {
return props.customForm[configValue][selectformKey];
} else {
return props.customForm[selectformKey];
}
};
// 处理表单值的绑定
const handleSetConfigValue = (configValue: string, selectformKey:string, val: any) => {
if (configValue) {
props.customForm[configValue][selectformKey] = val;
} else {
props.customForm[selectformKey] = val;
}
};
</script>
配置文件示例解读
下列代码定义渲染了一个输入框和一个开关,当开关启用的时候,输入框展示。当开关值改变的时候,清空输入框的内容。
const customConfigs = reactive<Array<Field>>([
{
type: "el-input", // 类型定义
formKey: "taskName", // 对应表单字段
label: "任务名称", // 对应表单名称
value: "", // 表单默认值
showOn: {
taskStatus: "1" // 任务状态开启时候展示
},
rules: [{ required: true, message: "此项为必填项", trigger: ["blur"] }] // 校验规则
},
{
type: "switch",
formKey: "taskStatus",
label: "任务状态",
value: "1",
"active-value": "1",
"inactive-value": "0",
clearItems: ["taskName"], // 值改变清除任务名称
change: (val: any) => { // 监听表单值改变
console.log(val);
}
}
]);
配置表单具体使用
<template>
<div style="padding:16px;text-align: left;">
<el-form
ref="ruleFormRef"
:model="customForm"
label-width="120px">
<div
v-for="(item,itemIndex) in customConfigs"
:key="itemIndex">
<div v-if="showField(item)">
<el-form-item
:label="item.label"
:prop="`${item.formKey}`"
:rules="item.rules">
<!-- 使用通用配置表单 -->
<BaseConfigForm
v-model:modelValue="customForm[item.formKey]"
:config-item="item"
:config="customConfigs"
:custom-form="customForm"
:option-data-map="optionDataMap"
@clearValues="clearValues" />
</el-form-item>
</div>
</div>
<el-form-item>
<el-button
type="primary"
@click="submitForm(ruleFormRef)">
创 建
</el-button>
<el-button @click="resetForm(ruleFormRef)">Reset</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, computed } from "vue";
import type { FormInstance } from "element-plus";
// 引入通用配置表单
import BaseConfigForm from "./BaseConfigForm.vue";
// 引入TS
import { Field } from "./type";
// 配置表单
const customConfigs = reactive<Array<Field>>([
{
type: "select",
formKey: "taskId",
label: "任务下拉选择",
value: "",
optionsList: [
{ label: "option1", value: "option1" },
{ label: "option2", value: "option2" },
{ label: "option3", value: "option3" }
],
change: (val:any) => {
console.log(val);
},
rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
},
{
type: "el-input",
formKey: "taskName",
label: "任务名称",
value: "",
change: (val:any) => {
console.log(val);
},
rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
},
{
type: "switch",
formKey: "taskStatus",
label: "任务状态",
value: "1",
"active-value": "1",
"inactive-value": "0",
clearItems: ["taskBudget", "taskRank"], // 清除任务难度和任务预算
change: (val: any) => {
console.log(val);
},
rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
},
{
type: "radioGroup",
label: "任务难度",
formKey: "taskRank",
value: 1,
optionsList: [
{ label: "简单", value: 1 },
{ label: "困难", value: 2 },
{ label: "非常困难", value: 3 }
],
change: (val: any) => {
console.log(val);
},
rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
},
{
type: "checkboxGroup",
label: "任务分类",
formKey: "taskType",
value: [2],
optionsList: [
{ label: "前端", value: 1 },
{ label: "后端", value: 2 },
{ label: "产品", value: 3 }
],
change: (val: any) => {
console.log(val);
},
rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
},
{
type: "datetimepicker",
label: "任务时间",
formKey: "taskTime",
value: "",
change: (val: any) => {
console.log(val);
}
},
{
type: "inputwildcard",
label: "任务规则名称",
formKey: "taskRuleName",
value: "",
rules: [{ required: true, message: "此项为必填项", trigger: ["blur", "change"] }]
},
{
type: "selectinput",
label: "任务预算",
formKey: "taskBudget",
placeholder: "请输入预算金额",
value: "",
selectformKey: "taskBudgetType",
selectPlaceholder: "预算类型",
selectValue: 1,
optionsList: [
{ label: "日预算", value: 1 },
{ label: "总预算", value: 2 }
],
disabled: false,
disabledSelect: false,
selectChange: (val:any) => {
console.log("selectChange", val);
},
showOn: {
taskStatus: "1" // 任务状态开启时候展示
}
}
]);
// 根据配置生成一份数据和一份初始化数据(用来重置或者清除筛选项)
const customForm = ref({} as any);
const customInitForm = {};
const optionDataMap = reactive({} as any); // 存放所有的枚举值
customConfigs.forEach(async (item: any) => {
console.log("item", item);
if (item.formKey) {
customForm.value[item.formKey] = item.value;
customInitForm[item.formKey] = item.value;
}
if (item.selectformKey) {
customForm.value[item.selectformKey] = item.selectValue;
customInitForm[item.selectformKey] = item.selectValue;
}
if (item.optionsList) {
optionDataMap[item.formKey] = item.optionsList;
}
});
// watch(customForm, (val) => {
// console.log("customForm", val);
// }, { deep: true });
const ruleFormRef = ref<FormInstance>();
// 表单提交
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
console.log("submit!");
} else {
console.log("error submit!", fields);
}
});
};
// 表单重置
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
// 根据formKey查找配置
const findConfig = (formKey: string) => {
let formItem = {} as Field;
customConfigs.forEach((item: any) => {
if (item.formKey === formKey) {
formItem = item;
}
});
return { formItem };
};
// 处理表单显示隐藏
const showField = (field: any): any => {
// 一、关联单个值的显示隐藏
// extBudget: 1 // 单个值,满足条件即展示。表示当extBudget === 1时,展示
// extBudget: [1, 2] // 数组,||或的关系。表示当extBudget === 1 || extBudget === 2时,展示
// extBudget: { extra: "!", value: 1 } // 对象,!非的关系。表示当extBudget !==1 时,展示
// extBudget: { extra: "!", value: [1, 2] } // 对象,!非的关系。表示当 !(extBudget === 1 || extBudget === 2) 时,展示
// 二、关联多个值的显示隐藏
// 1.默认&&的关系,表示当 (extBudget===1 && nameTemplateV2 === '222') 时,展示
// extBudget: 1,
// nameTemplateV2: "2222"
// 2.属性前有||表或的关系,表示当 (extBudget===1 || nameTemplateV2 === '222') 时,展示
// extBudget: 1,
// "||nameTemplateV2": "222"
// 三、自定义的显示隐藏
// cusShow: () => {
// return false;
// }
if (field.showOn) {
// 自定义的显示隐藏
if (field.showOn.cusShow) {
return field.showOn.cusShow();
}
// 关联单个值的显示隐藏
if (Object.keys(field.showOn).length === 1) {
const key = Object.keys(field.showOn)[0];
const definedValue = field.showOn[key];
const relateValue = customForm.value[key];
// 判断值的类型
if (typeof definedValue === "object") {
if (definedValue.extra === "!") {
if (Array.isArray(definedValue.value)) {
return definedValue.value.includes(relateValue);
}
return definedValue.value === relateValue;
}
}
if (Array.isArray(definedValue)) {
return definedValue.includes(relateValue);
}
return definedValue === relateValue;
}
// todo: 关联多个值的显示隐藏
// if (Object.keys(field.showOn).length > 1) {}
}
return true;
};
// 清空相关联动项
const clearValues = (field: Field) => {
if (field.clearItems && field.clearItems.length) {
field.clearItems.forEach((itemKey: any) => {
if (typeof itemKey === "string") {
customForm.value[itemKey] = customInitForm[itemKey];
// 根据当前清空项itemKey,在配置中customConfigs查看是否还有关联的清空项
const { formItem } = findConfig(itemKey);
if (formItem && formItem.change) formItem.change(customForm.value[itemKey]);
if (formItem) clearValues(formItem);
}
});
}
};
</script>