首页 > 其他分享 >Vue3 Element-Plus 一站式生成动态表单

Vue3 Element-Plus 一站式生成动态表单

时间:2023-11-18 13:22:24浏览次数:27  
标签:string value Element Plus formItem key Vue3 import label

数据接口设计

type TreeItem = {
  value: string
  label: string
  children?: TreeItem[]
}

export type FormListItem = {
  // 栅格占据的列数
  colSpan?: number
  // 表单元素特有的属性
  props?: {
    placeholder?: string
    defaultValue?: unknown // 绑定的默认值
    clearable?: boolean
    disabled?: boolean | ((data: { [key: string]: any }) => boolean)
    size?: 'large' | 'default' | 'small'
    group?: unknown // 父级特有属性,针对嵌套组件 Select、Checkbox、Radio
    child?: unknown // 子级特有属性,针对嵌套组件 Select、Checkbox、Radio
    [key: string]: unknown
  }
  // 表单元素特有的插槽
  slots?: {
    name: string
    content: unknown
  }[]
  // 组件类型
  typeName?: 'input' | 'select' | 'date-picker' | 'time-picker' | 'switch' | 'checkbox' | 'checkbox-group' | 'checkbox-button' | 'radio-group' | 'radio-button' | 'input-number' | 'tree-select' | 'upload' | 'slider'
  // 表单元素特有的样式
  styles?: {
    [key: string]: number | string
  }
  // select options 替换字段
  replaceField?: { value: string; label: string }
  // 列表项
  options?: {
    value?: string | number | boolean | object
    label?: string | number
    disabled?: ((data: { [key: string]: any }) => boolean) | boolean
    [key: string]: unknown
  }[]
  // <el-form-item> 独有属性,同 FormItem Attributes
  formItem: Partial<FormItemProps & { class: string }>
  // 嵌套<el-form-item>
  children?: FormListItem[]
  // 树形选择器数据
  treeData?: TreeItem[] // 只针对 'tree-select'组件
  // 组件显示条件
  isShow?: ((data: { [key: string]: any }) => boolean) | boolean
}

export type FConfig = {
  form: Partial<InstanceType<typeof ElForm>> // Form Attributes 与Element属性一致
  configs: FormListItem[]  // 表单主体配置
}

常见表单需求

  • 如何控制某个组件的显示隐藏

实现思路,提供一个isShow方法,将方法绑定在对应的组件上,从而组件显示隐藏条件

isShow: (data = {}) => {
  return model.value.region == 'shanghai'
}
....
<el-form-item v-if="isShow(model)" v-bind="item.formItem">
  • 目标组件是否禁用,需要根据某个组件是否有值来判断
disabled: (data = {}) => {
  return !model.value.date1
}
....
<component :disabled="disabled(model)"></component>
  • 组件之间相互赋值,A组件的值赋值给B组件B组件的值赋值给 A组件

image.png

image.png

  • 表单验证
formItem: {
  prop: 'name',
  label: 'Activity name',
  rules: [
    {
      required: true,
      message: 'Please enter content',
      trigger: 'blur'
    }
  ]
}

组件封装

1. 输入框组件

<template>
  <el-input v-bind="attrs.props" ref="elInputRef" :style="attrs.styles">
    <template v-for="item in attrs.slots" #[item.name] :key="item.name">
      <component :is="item.content"></component>
    </template>
  </el-input>
</template>

2. 下拉选择器组件

<template>
  <el-select
    v-bind="attrs.props?.group"
    ref="elSelectRef"
    :style="attrs.styles"
  >
    <el-option
      v-for="item in attrs.options"
      v-bind="attrs.props?.child"
      :key="item[attrs.replaceField?.value || 'value']"
      :label="item[attrs.replaceField?.label || 'label']"
      :value="item[attrs.replaceField?.value || 'value']"
      :disabled="item.disabled"
    ></el-option>
  </el-select>
</template>

3. 日期选择器组件

<template>
  <el-date-picker
    v-bind="attrs.props"
    ref="elDatePickerRef"
    :style="attrs.styles"
  ></el-date-picker>
</template>

封装方法都一致,还有很多组件,这里就不一个个列出来,具体大家就移步源码查看哈

项目路径 src/components/Form

组件整合

<template>
  <el-form v-bind="props.form" ref="formRef" :model="model">
    <el-row :gutter="20">
      <el-col
        v-for="item in props.configs"
        :key="item.formItem.prop"
        :span="item.colSpan"
      >
        <el-form-item v-if="ifShow(item, model)" v-bind="item.formItem">
          <template v-if="item.typeName == 'upload'">
            <el-upload v-bind="item.props">
              <template v-for="it in item.slots" #[it.name] :key="it.name">
                <component :is="it.content"></component>
              </template>
            </el-upload>
          </template>

          <template v-if="!item.children?.length">
            <component
              :is="components[`m-${item.typeName}`]"
              v-bind="item"
              v-model="model[item.formItem.prop as string]"
              :form-data="model"
              :disabled="ifDisabled(item, model)"
            ></component>
          </template>

          <template v-else>
            <el-col
              v-for="(child, index) in item.children"
              :key="index"
              :span="child.colSpan"
            >
              <el-form-item v-bind="child.formItem">
                <component
                  :is="components[`m-${child.typeName}`]"
                  v-bind="child"
                  v-model="model[child.formItem.prop as string]"
                  :form-data="model"
                  :disabled="ifDisabled(child, model)"
                ></component>
              </el-form-item>
            </el-col>
          </template>
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>

<script setup lang="ts">
import cloneDeep from "lodash/cloneDeep";
import { ref, onMounted, watch, computed } from "vue";
import { getType } from "@/utils/util";
import type { ElForm, FormInstance } from "element-plus";
import { FormListItem, FConfig } from "./form";

import mInput from "./components/m-input.vue";
import mSelect from "./components/m-select.vue";
import mDatePicker from "./components/m-date-picker.vue";
import mTimePicker from "./components/m-time-picker.vue";
import mSwitch from "./components/m-switch.vue";
import mCheckbox from "./components/m-checkbox.vue";
import mCheckboxGroup from "./components/m-checkbox-group.vue";
import mCheckboxButton from "./components/m-checkbox-button.vue";
import mRadioGroup from "./components/m-radio-group.vue";
import mRadioButton from "./components/m-radio-button.vue";
import mInputNumber from "./components/m-input-number.vue";
import mTreeSelect from "./components/m-tree-select.vue";
import mSlider from "./components/m-slider.vue";

type Props = FConfig & {
  data: { [key: string]: any };
};
const emits = defineEmits(["update:data"]);
const props = withDefaults(defineProps<Props>(), {});
const model = ref<{ [key: string]: any }>({});
const formRef = ref<FormInstance | null>();
const components: { [key: string]: any } = {
  "m-input": mInput,
  "m-select": mSelect,
  "m-date-picker": mDatePicker,
  "m-time-picker": mTimePicker,
  "m-switch": mSwitch,
  "m-checkbox": mCheckbox,
  "m-checkbox-group": mCheckboxGroup,
  "m-checkbox-button": mCheckboxButton,
  "m-radio-group": mRadioGroup,
  "m-radio-button": mRadioButton,
  "m-input-number": mInputNumber,
  "m-tree-select": mTreeSelect,
  "m-slider": mSlider,
};

const ifDisabled = computed(() => {
  return (column: FormListItem, model: { [key: string]: any }) => {
    let disabled = column.props?.disabled;
    switch (getType(disabled)) {
      case "function":
        disabled = (disabled as any)(model);
        break;
      case "undefined":
        disabled = false;
    }
    return disabled;
  };
});

const ifShow = (column: FormListItem, model: { [key: string]: any }) => {
  let flag = column.isShow;
  switch (getType(flag)) {
    case "function":
      flag = (flag as any)(model);
      break;
    case "undefined":
      flag = true;
      break;
  }
  return flag;
};

// 组件重写表单重置的方法
const resetFields = () => {
  // 重置element-plus 的表单
  formRef.value?.resetFields();
};

// 表单验证
const validate = () => {
  return new Promise((resolve, reject) => {
    formRef.value?.validate((valid) => {
      if (valid) {
        resolve(true);
      } else {
        reject(false);
      }
    });
  });
};

const getFormData = () => {
  return model.value;
};

watch(
  () => model.value,
  (val) => {
    emits("update:data", val);
  }
);

watch(
  () => props.data,
  (val) => {
    model.value = val;
  },
  {
    immediate: true,
  }
);

defineExpose({
  resetFields,
  getFormData,
  validate,
});
</script>

<style scoped></style>

抽离 Form 公共逻辑

// hooks/useForm.ts
import { ref } from 'vue'
import type { FConfig } from '@/components/Form/form'
import { cloneDeep } from 'lodash'

type SelectOption = {
  value?: string | number | boolean | object
  label?: string | number
  disabled?: ((data: { [key: string]: any }) => boolean) | boolean
  [key: string]: unknown
}[]

type TreeItem = {
  value: string
  label: string
  children?: TreeItem[]
}

export const useForm = (formConfig: FConfig) => {
  const model = ref<{ [key: string]: any }>({})
  const config = ref<FConfig>(formConfig)

  const getFormItem = (key: string) => {
    return config['value']?.configs.find((item) => item.formItem.prop == key)
  }
  /**
   * 修改select组件options
   * @param key 对应formItem prop
   * @param options 下拉选项
   */
  const changeSelectOptions = (key: string, options: SelectOption) => {
    const formItem = getFormItem(key)
    if (formItem) {
      formItem.options = options
    }
  }

  /**
   * 修改tree-select组件treeData
   * @param key 对应formItem prop
   * @param options 下拉选项
   */
  const changeTreeSelectOptions = (key: string, options: TreeItem[]) => {
    const formItem = getFormItem(key)
    if (formItem) {
      formItem.treeData = options
    }
  }

  // 构建model绑定数据
  const initModel = () => {
    const configs = config['value']?.configs
    if (configs.length) {
      const m: { [key: string]: any } = {}
      configs.map((item) => {
        if (!item.children?.length) {
          m[item.formItem.prop as string] = item.props?.defaultValue
        } else {
          item.children.map((child) => {
            m[child.formItem.prop as string] = child.props?.defaultValue
          })
        }
      })
      model.value = cloneDeep(m)
    }
  }

  initModel()

  return { config, model, changeSelectOptions, changeTreeSelectOptions }
}

附上完整配置

// views//dynamicForm/form.tsx
import { ElIcon, ElButton } from "element-plus";
import { Search } from "@element-plus/icons-vue";

import { useForm } from "@/hooks/useForm";

export const useFormIterate = (events?: any) => {
  const { model, ...arg } = useForm({
    form: {
      labelWidth: "140px",
    },
    configs: [
      // 输入框
      {
        colSpan: 12,
        typeName: "input",
        props: {
          defaultValue: "",
          clearable: true,
          placeholder: "Please enter content",
        },
        slots: [
          {
            name: "suffix",
            content: () => (
              <ElIcon class="el-input__icon">
                <Search />
              </ElIcon>
            ),
          },
        ],
        formItem: {
          prop: "name",
          label: "Activity name",
          rules: [
            {
              required: true,
              message: "Please enter content",
              trigger: "blur",
            },
          ],
        },
      },
      // 选择器
      {
        colSpan: 12,
        typeName: "select",
        props: {
          placeholder: "Please select content",
          defaultValue: undefined,
          group: {
            clearable: true,
            onChange: events.changeSelect,
          },
          child: {},
        },
        replaceField: { value: "key", label: "title" },
        options: [
          { key: "shanghai", title: "Zone one" },
          { key: "beijing", title: "Zone two" },
        ],
        styles: {
          width: "100%",
        },
        formItem: {
          prop: "region",
          label: "Activity zone",
          rules: [
            {
              required: true,
              message: "Please select Activity zone",
              trigger: "change",
            },
          ],
        },
      },
      // 选择器
      {
        colSpan: 24,
        typeName: "select",
        props: {
          disabled: () => {
            return !model.value.region;
          },
          placeholder: "Please select content",
          defaultValue: undefined,
          group: {
            clearable: true,
            onChange: events.changeSelect,
          },
          child: {},
        },
        replaceField: { value: "key", label: "title" },
        options: [],
        styles: {
          width: "100%",
        },
        formItem: {
          prop: "region1",
          label: "Activity select zone",
          rules: [
            {
              required: true,
              message: "Please select Activity zone",
              trigger: "change",
            },
          ],
        },
      },
      {
        colSpan: 24,
        formItem: {
          required: true,
          label: "Activity time",
        },
        children: [
          // 日期选择器
          {
            colSpan: 12,
            typeName: "date-picker",
            props: {
              type: "datetime",
              clearable: true,
              valueFormat: "YYYY-MM-DD HH:mm:ss",
              placeholder: "Pick a day",
            },
            styles: { width: "100%" },
            formItem: {
              prop: "date1",
              rules: [
                {
                  type: "date",
                  required: true,
                  message: "Please pick a date",
                  trigger: "change",
                },
              ],
            },
          },
          // 时间选择器
          {
            colSpan: 12,
            typeName: "time-picker",
            props: {
              disabled: (data = {}) => {
                return !model.value.date1;
              },
              clearable: true,
              placeholder: "Pick a time",
            },
            styles: { width: "100%" },
            formItem: {
              prop: "date2",
              rules: [
                {
                  type: "date",
                  required: true,
                  message: "Please pick a time",
                  trigger: "change",
                },
              ],
            },
          },
        ],
      },
      // 开关
      {
        colSpan: 24,
        typeName: "switch",
        props: {
          defaultValue: false,
        },
        formItem: {
          prop: "delivery",
          label: "Instant delivery",
        },
      },
      // 多选框
      {
        colSpan: 12,
        typeName: "checkbox-group",
        props: {
          group: {},
          child: {},
        },
        formItem: {
          prop: "type",
          label: "Activity type",
          rules: [
            {
              type: "array",
              required: true,
              message: "Please select at least one activity type",
              trigger: "change",
            },
          ],
        },
        // replaceField: { value: 'value', label: 'label' },
        options: [
          { value: "shanghai", label: "Zone one" },
          { value: "beijing", label: "Zone two" },
        ],
      },
      // 多选按钮框
      {
        colSpan: 12,
        typeName: "checkbox-button",
        props: {
          group: {},
          child: {},
        },
        formItem: {
          prop: "button",
          label: "Activity button",
          rules: [
            {
              type: "array",
              required: true,
              message: "Please select at least one activity type",
              trigger: "change",
            },
          ],
        },
        // replaceField: { value: 'value', label: 'label' },
        options: [
          { value: "shanghai", label: "Zone one" },
          { value: "beijing", label: "Zone two" },
        ],
      },
      // 单选框
      {
        colSpan: 12,
        typeName: "radio-group",
        props: {},
        formItem: {
          prop: "resource",
          label: "Resources",
          rules: [
            {
              required: true,
              message: "Please select activity resource",
              trigger: "change",
            },
          ],
        },
        options: [
          { value: "shanghai", label: "Sponsorship" },
          { value: "beijing", label: "Venue" },
        ],
      },
      // 单选按钮框
      {
        colSpan: 12,
        typeName: "radio-button",
        props: {},
        formItem: {
          prop: "resourceButton",
          label: "Resources button",
          rules: [
            {
              required: true,
              message: "Please select activity resource",
              trigger: "change",
            },
          ],
        },
        options: [
          { value: "shanghai", label: "Sponsorship" },
          { value: "beijing", label: "Venue" },
        ],
      },
      // 文本域
      {
        colSpan: 24,
        typeName: "input",
        formItem: {
          prop: "desc",
          label: "Activity form",
        },
        props: {
          rows: 5,
          type: "textarea",
          clearable: true,
          placeholder: "Please enter content",
        },
        isShow: (data = {}) => {
          return model.value.region == "shanghai";
        },
      },
      // 文件上传
      {
        colSpan: 24,
        typeName: "upload",
        formItem: {
          prop: "fileName",
          label: "Upload File",
          rules: [
            {
              required: true,
              message: "Please select at least one activity type",
              trigger: "change",
            },
          ],
        },
        props: {
          httpRequest: events.httpRequest,
        },
        slots: [
          {
            name: "default",
            content: () => <ElButton type="primary">上传</ElButton>,
          },
          {
            name: "tip",
            content: () => (
              <span style="margin-left:10px">
                jpg/png files with a size less than 500KB
              </span>
            ),
          },
        ],
      },
      // 滑块
      {
        colSpan: 16,
        typeName: "slider",
        props: {
          onChange: (val: number) => {
            model.value.number = val;
          },
        },
        formItem: {
          label: "Activity slider",
          prop: "slider",
          rules: [
            {
              required: true,
              message: "Please enter content",
              trigger: "change",
            },
          ],
        },
      },
      // 数字输入框
      {
        colSpan: 8,
        typeName: "input-number",
        formItem: {
          prop: "number",
          label: "Activity number",
        },
        props: {
          min: 1,
          max: 100,
          onChange: (val: number) => {
            model.value.slider = val;
          },
        },
      },
      // 树形选择器
      {
        colSpan: 24,
        typeName: "tree-select",
        formItem: {
          prop: "tree",
          label: "Activity tree",
        },
        styles: { width: "100%" },
        props: {
          multiple: true,
          showCheckbox: true,
          placeholder: "Please select content",
        },
        treeData: [],
      },
    ],
  });

  return { model, ...arg };
};

到这里可能会有朋友会问,为啥用的 tsx 后缀,而不是用 js/json; 这是因为想通过组件的形式传到 Slot 中(m-input 组件为例),从而进行展示,当然也欢迎大家在评论区提供更好的方案。

实现效果

image.png

image.png

标签:string,value,Element,Plus,formItem,key,Vue3,import,label
From: https://www.cnblogs.com/wp-leonard/p/17840370.html

相关文章

  • uniapp脚手架中vue3项目配置`@`,并且在vscode中有提示
    uniapp脚手架中vue3项目配置@,并且在vscode中有提示在vite.config.js中配置一下代码import{defineConfig}from"vite";importunifrom"@dcloudio/vite-plugin-uni";import{resolve}from"path";//https://vitejs.dev/config/exportdefaultdefine......
  • c++日志库-log4cplus
    《log4cplus日志库》1.Preface  log4cplus是一款开源的c++日志库,具有线程安全,灵活,以及多粒度控制的特点;log4cplus可以将日志按照优先级进行划分,使其可以面向程序的调试,运行,测试,后期维护等软件全生命周期;可以通过配置,选择将日志输出到屏幕,文件,NTeventlog,甚至是远程服务器......
  • element-plus如何隐藏el-row
    在ElementPlus中,el-row是用于布局的组件,如果你想要隐藏el-row,你可以使用CSS的display属性将其设置为none。以下是一个简单的示例:<template><el-rowv-show="shouldShowRow"><!--这里是el-row的内容--></el-row></template><script>exportd......
  • Vue3 + antDesign3.x 汉化 中文(解决日期混合中英文模式
    依赖项版本 "ant-design-vue":"^3.2.20", "dayjs":"^1.11.10", "vue":"^3.0.5",依赖处理main.js中import{createApp}from'vue'importAntdfrom'ant-design-vue'import'an......
  • vue3+element-Plus表格滚动联动
    constTable0=ref()constTable1=ref()functionsyncScroll(){for(leti=0;i<compareData.compareInfo.length;i++){letfirstTable=Table0.value[i].$refs.bodyWrapper.getElementsByClassName('el-scrollbar__wrap')[0]letsec......
  • Vue3 模板引用 ref 的实现原理
    什么是模板引用ref?有时候可以使用 ref attribute为子组件或HTML元素指定引用ID。<template><inputref="input"/></template><script>import{defineComponent,ref}from"vue";exportdefaultdefineComponent({setup(){......
  • Vue3 的 effect、 watch、watchEffect 的实现原理
    所谓watch,就是观测一个响应式数据或者监测一个副作用函数里面的响应式数据,当数据发生变化的时候通知并执行相应的回调函数。Vue3最新的watch实现是通过最底层的响应式类ReactiveEffect的实例化一个reactiveeffect对象来实现的。它的创建过程跟effectAPI的实现类似,所......
  • vue3 使用 store
    在script中使用storehttps://blog.csdn.net/SubStar/article/details/116077737<script>import{getCurrentInstance}from"vue";import{useStore}from"vuex";exportdefault{setup(){//第一种方法:获取路由对象router的方法1constv......
  • mybatisplus关于驼峰命名法与下划线的映射
    今天遇到一个很坑的事情,我在测试之前的案例的时候我有一个字段的名字是typeId,我调试之后发现插入出现了错误。开启sql日志之后我发现mybatisplus自动把我的typeId改成type_id了。无奈之下我只能把数据库、实体类的驼峰命名法改成下划线###SQL:SELECTid,name,description,t......
  • Vue3 Pinia对state的订阅监听($subscribe,$onAction)数据监听
    <template><divclass="main-container":class="{'show-scroll':targetIsVisible}"><div:style="{height:frameHeight+'px'}"class="main-content":class="{'show-......