赞
踩
昨天写一个表单十几个二十个字段,写起来有点麻烦,中途还出现了改动,一大串一大串 el-form-item
看起来有点烦,就算是用循环写也差点意思, 刚好今天有闲余的时间,就写了个生成器,自动生成表单。
// 暂时支持这些类型(全是element-plus的组件,我把前面的el给省略了)
export type fieldType =
| "input"
| "number"
| "select"
| "textarea"
| "date"
| "time"
| "datetime"
| "cascader"
| "tree-select"
| "radio"
| "checkbox";
<!-- 每个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>
// 绑定表单
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 --> <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)); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。