vue3 + elememt-plus 表单生成器

灯火 Lv3

缘由

昨天写一个表单十几个二十个字段,写起来有点麻烦,中途还出现了改动,一大串一大串 el-form-item 看起来有点烦,就算是用循环写也差点意思, 刚好今天有闲余的时间,就写了个生成器,自动生成表单。

生成器使用

支持组件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
// 暂时支持这些类型(全是element-plus的组件,我把前面的el给省略了)
export type fieldType =
| "input"
| "number"
| "select"
| "textarea"
| "date"
| "time"
| "datetime"
| "cascader"
| "tree-select"
| "radio"
| "checkbox";

表单渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 每个formColumns子项目支持两个插槽 -->
<!-- slot=${prop} 完全替代form-item -->
<!-- slot=${prop}Component 替代该form-item下的组件, label不变动 -->
<FormGenerator
ref="formGeneratorRef"
:formColumns="formColumns"
:model="form"
:rules="rules"
:property="property"
>
<template #nameComponent> 这是个name </template>
<template #number> 数量组件取代 form-item </template>
</FormGenerator>

表单绑定

1
2
// 绑定表单
const form = reactive({ name: "", number: 10, number2: 2 });

表单列表数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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",
},
];

额外属性配置 property 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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%",
});

效果

表单生成器

提交表单验证

1
2
3
4
5
6
7
8
9
10
// 表单校验,与原本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")

效果

表单生成器

onEnter

1
2
3
4
5
6
7
8
9
// 因为项目有按下enter进行搜索的需求
// 所以抛出了 onEnter方法 【@keyup.enter】
const onEnter = (value) => {
console.log("onEnter", value);
}
const formColumns: FormItemVO[] = [
{ label: "名称", fileType: "input", prop: "name" onEnter },
// ...
]

表单生成器

表单生成器代码

目录结构

1
2
3
4
5
6
// 目录结构
-FormGenerator
-index.vue
-types.ts
-components
-FormItemRenderer.vue

FormGenerator/types.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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"]

FormGenerator/components/FormItemRenderer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<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>

FormGenerator/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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 -->
<slot :name="item.prop + 'Component'" />
</template>
</FormItemRenderer>
</slot>
</template>
</el-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 栅格相关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));
}
}

  • 标题: vue3 + elememt-plus 表单生成器
  • 作者: 灯火
  • 创建于 : 2023-10-27 16:12:57
  • 更新于 : 2024-05-17 08:34:04
  • 链接: https://blog.juniverse.top/2023/10/27/vue3-elememt-plus-FormGenerator/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论