昨天写一个表单十几个二十个字段,写起来有点麻烦,中途还出现了改动,一大串一大串 el-form-item
看起来有点烦,就算是用循环写也差点意思, 刚好今天有闲余的时间,就写了个生成器,自动生成表单。
// 暂时支持这些类型(全是element-plus的组件,我把前面的el给省略了)
export type fieldType =
| "input"
| "number"
| "select"
| "textarea"
| "date"
| "time"
| "datetime"
| "cascader"
| "tree-select"
| "radio"
| "checkbox";
<FormGenerator
ref="formGeneratorRef"
:formColumns="formColumns"
:model="form"
:rules="rules"
:property="property"
>
<template #nameComponent> 这是个name template>
<template #number> 数量组件取代 form-item template>
FormGenerator>
// 绑定表单
const form = reactive({ name: "", number: 10, number2: 2 });
const list = [
{ label: "项目1", value: 1 },
// 项目2不可选中
{ label: "项目2", value: 2, disabled: true },
];
const treeList = [
{ label: "项目1", value: 1 },
{
label: "项目2",
value: 2,
children: [
{ label: "项目2子项1", value: 21 },
// 项目2不可选中
{ label: "项目2子项2", value: 22, disabled: true },
],
},
];
// 表单数据
const formColumns: FormItemVO[] = [
// label: form-item 组件的 label 属性
// fileType: 为上面支持的fieldType类型 必填
// prop:绑定的数据模型属性 必填
// 其他参数:和原本element-plus组件参数保持一致
{ label: "名称", fileType: "input", prop: "name" },
{ label: "测试参数", fileType: "number", prop: "filed1" },
{ label: "测试参数2", fileType: "select", options: list, prop: "filed2" },
{ label: "测试参数3", fileType: "radio", options: list, prop: "filed3" },
{ label: "测试参数4", fileType: "checkbox", options: list, prop: "filed4" },
{ label: "测试参数5", fileType: "textarea", prop: "filed5" },
// 测试参数6 树形选择器
{
label: "测试参数6",
fileType: "tree-select",
// data为原本的数据
data: treeList,
prop: "filed6",
},
];
interface FormAttributes {
// 行内模式
inline?: boolean;
// lable位置
labelPosition?: "left" | "right" | "top";
// label宽度
labelWidth?: string | number;
// 表单大小
size?: "" | "large" | "default" | "small";
// 栅格数 inline 为true时无效
col?: number;
// label 后缀
labelSuffix?: string;
// label右边组件的宽度
componentWidth?: string;
}
const property = reactive<FormAttributes>({
col: 2,
labelSuffix: ":",
componentWidth: "100%",
});
// 表单校验,与原本el-form rules书写一致
const rules = {
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
};
const formGeneratorRef = ref<FormGeneratorRef>(null);
// 获取表单实例
const formRef = formGeneratorRef.value?.getForm();
const valid = await formRef?.validate();
// 其他Form Exposes 写法与原本一致
// const valid = await formRef?.validateField("filed1")
// 因为项目有按下enter进行搜索的需求
// 所以抛出了 onEnter方法 【@keyup.enter】
const onEnter = (value) => {
console.log("onEnter", value);
}
const formColumns: FormItemVO[] = [
{ label: "名称", fileType: "input", prop: "name" onEnter },
// ...
]
// 目录结构
-FormGenerator
-index.vue
-types.ts
-components
-FormItemRenderer.vue
import { DICT_TYPE } from '@/utils/dict'
export type fieldType =
| 'input'
| 'number'
| 'select'
| 'textarea'
| 'date'
| 'time'
| 'datetime'
| 'cascader'
| 'tree-select'
| 'radio'
| 'checkbox'
export interface OptionsPropsVO {
label?: string
value?: string
children?: string
}
export interface FormItemVO {
prop: string
label: string
fileType: fieldType
options?: OptionVO[]
optionsProps?: OptionsPropsVO
// number 组件
min?: number
max?: number
precision?: number
placeholder?: string
rows?: number
clearable?: boolean
multiple?: boolean
filterable?: boolean
allowCreate?: boolean
// 字典类型
dictType?: DICT_TYPE
// tree-select 组件
data?: any[]
// functon 键盘enter事件
onEnter?(value: any): void
// 还有很多element-plus组件的属性没写
}
export interface OptionVO {
label: string
value: any
children?: OptionVO[]
// [key: string]: any
}
// 默认options映射
export const defaultOptionsPropsVO: OptionsPropsVO = {
label: 'label',
value: 'value',
children: 'children',
}
// element-plus组件本身的type,使用fiedType
export const unchangedTypes: fieldType[] = ["textarea", "date", "datetime", "time"];
// 【placeholder】提示词为 请选择${label}的 组件类型
export const selectPlaceholder: fieldType[] = ["select", "cascader", "tree-select"]
<ElFormItem :label="useLabel" :prop="item.prop">
<slot>
<component
@keyup.enter="useOnEnter"
:is="componentType"
v-model="useValue"
v-bind="bindItem"
:style="{ width: property?.componentWidth }"
>
<component
:is="itemComponentType"
v-for="(option, index) in useOptions"
:key="index"
v-bind="option"
/>
component>
slot>
ElFormItem>
<script setup name="FormItemRenderer" lang="ts">
// 字典
import { getDictOptions } from "@/utils/dict";
import {
ElFormItem,
ElInputNumber,
ElSelect,
ElOption,
ElInput,
ElDatePicker,
ElTimePicker,
ElCascader,
ElRadioGroup,
ElRadio,
ElCheckboxGroup,
ElCheckbox,
ElTreeSelect,
} from "element-plus";
import type { FormItemVO, OptionVO } from "../types";
import {
unchangedTypes,
selectPlaceholder,
defaultOptionsPropsVO,
} from "../types";
interface PropertyVO {
// label 后缀
labelSuffix?: string;
componentWidth?: string;
}
export interface PropsVO {
item: FormItemVO;
value: any;
property?: PropertyVO;
}
const props = defineProps<PropsVO>();
const emit = defineEmits(["update:value"]);
// label
const useLabel = computed(() => {
const item = props.item;
const property = props.property;
if (!property?.labelSuffix) {
return item.label;
}
return item.label + property.labelSuffix;
});
const useValue = computed({
get() {
return props.value;
},
set(val) {
// 触发 update:page 事件,更新 limit 属性,从而更新 pageNo
emit("update:value", val);
},
});
// element-plus 组件所需的参数
const bindItem = computed(() => {
const { item } = props;
return {
...item,
rows: useRows.value,
type: useType.value,
placeholder: usePlaceholder.value,
clearable: useClearable.value,
};
});
const useOptionsPops = computed(() => {
const { optionsProps } = props.item;
return { ...defaultOptionsPropsVO, ...(optionsProps || {}) };
});
/** 组件列表数据 */
const useOptions = computed(() => {
let list: OptionVO[] = [];
const { options, dictType } = props.item;
if (options) list = options;
if (dictType) list = getIntDictOptions(dictType);
const { value, label, children } = useOptionsPops.value;
//字段映射
return list.map((item) => {
return {
...item,
label: item[label as string],
value: item[value as string],
children: item[children as string],
};
});
});
const componentType = computed(() => {
switch (props.item.fileType) {
case "number":
return ElInputNumber;
case "select":
return ElSelect;
case "date":
case "datetime":
return ElDatePicker;
case "time":
return ElTimePicker;
case "cascader":
return ElCascader;
case "radio":
return ElRadioGroup;
case "checkbox":
return ElCheckboxGroup;
case "tree-select":
return ElTreeSelect;
default:
return ElInput;
}
});
const itemComponentType = computed(() => {
switch (props.item.fileType) {
case "select":
return ElOption;
case "radio":
return ElRadio;
case "checkbox":
return ElCheckbox;
default:
return;
}
});
// 组件本身的type
const useType = computed(() => {
const { fileType } = props.item;
if (unchangedTypes.includes(fileType)) return fileType;
return "";
});
// 默认行数为 3
const useRows = computed(() => {
if (props.item.rows) return props.item.rows;
return 3;
});
const isUndefine = (val: any) => {
return val === undefined;
};
// 默认清空 为true
const useClearable = computed(() => {
if (!isUndefine(props.item.clearable)) props.item.clearable;
return true;
});
// 默认提示 请输入${label} | 请选择${label}
const usePlaceholder = computed(() => {
const { placeholder, label, fileType } = props.item;
if (!isUndefine(placeholder)) return placeholder;
if (selectPlaceholder.includes(fileType)) return `请选择${label}`;
return `请输入${label}`;
});
const getIntDictOptions = (dictType: string) => {
// 根据 dictType 获取选项列表,返回一个包含 label 和 value 属性的对象数组
// 请根据你的应用需求完善这个函数
return getDictOptions(dictType);
};
const useOnEnter = () => {
const fn = () => {}
if (!props?.item?.onEnter) return fn
return props.item.onEnter(useValue.value)
}
</script>
<el-form
@submit.prevent
:model="model"
:rules="rules"
:class="useColClass"
:size="property?.size"
:inline="property?.inline"
:label-width="property?.labelWidth"
:label-position="property?.labelPosition"
ref="formRef"
>
<template v-for="item in formColumns" :key="item.prop">
<slot :name="item.prop">
<FormItemRenderer
:item="item"
:property="property"
v-model:value="useModel[item.prop]"
>
<template #default>
<slot :name="item.prop + 'Component'" />
template>
FormItemRenderer>
slot>
template>
el-form>
<script setup lang="ts" name="FormGenerator">
import FormItemRenderer from "./components/FormItemRenderer.vue";
import { ElForm } from "element-plus";
import type { FormRules } from "element-plus";
import type { FormItemVO } from "./types";
interface FormAttributes {
// 行内模式
inline?: boolean;
// lable位置
labelPosition?: "left" | "right" | "top";
// label宽度
labelWidth?: string | number;
// 表单大小
size?: "" | "large" | "default" | "small";
// 栅格数 inline 为true时无效
col?: number;
// ------ 渲染组件所使用的参数 ------
// label 后缀
labelSuffix?: string;
// label右边组件的宽度
componentWidth?: string;
}
interface PropsVO {
/** 表单数据 */
formColumns: FormItemVO[];
model: Record<string, any>;
rules?: FormRules;
property?: FormAttributes;
}
const props = defineProps<PropsVO>();
const emit = defineEmits(["update:model"]);
const useModel = computed({
get() {
return props.model;
},
set(val) {
// 触发 update:page 事件,更新 limit 属性,从而更新 pageNo
emit("update:model", val);
},
});
// 栅格class
const useColClass = computed(() => {
const { inline, col } = props.property || {};
// 行内模式 | 栅格数为空 跳出
if (inline || !col) return "";
return `grid items-start grid-gap-0-20 grid-cols-${col}`;
});
const formRef = ref()
// 表单实例
const getForm = () => {
return formRef.value
}
defineExpose({ getForm });
</script>
// 栅格相关scss代码
.grid {
display: -ms-grid;
display: grid;
}
.grid-gap-0-20 {
gap: 0 20px;
}
.items-start {
-webkit-box-align: start;
-ms-flex-align: start;
-webkit-align-items: flex-start;
align-items: flex-start;
}
@for $i from 1 through 4 {
.grid-cols-#{$i} {
grid-template-columns: repeat($i, minmax(0, 1fr));
}
}