当前位置:   article > 正文

vue3 + elememt-plus 表单生成器_form generator vue3

form generator vue3

title: vue3 + elememt-plus 表单生成器
date: 2023-10-27 16:12:57
categorys: [“技术”]
tags: [“前端”, “vue3”]

缘由

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

生成器使用

支持组件类型

// 暂时支持这些类型(全是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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

表单绑定

// 绑定表单
const form = reactive({ name: "", number: 10, number2: 2 });
  • 1
  • 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",
  },
];
  • 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

额外属性配置 property 属性

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
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
效果

在这里插入图片描述

提交表单验证

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

在这里插入图片描述

onEnter
// 因为项目有按下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
  • 7
  • 8
  • 9

在这里插入图片描述

表单生成器代码

目录结构

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

FormGenerator/types.ts

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"]

  • 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

FormGenerator/components/FormItemRenderer.vue

<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
<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>

  • 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

FormGenerator/index.vue

<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
<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
  • 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
// 栅格相关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));
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/797409
推荐阅读
相关标签
  

闽ICP备14008679号