vue3结合Element Plus动态表单组件

背景介绍

在开发公司内部使用的管理后台时,通过原型发现表单较多,内容层级复杂,而且包含一些联动。
例如:

  • 通过input在输入内容后需自动生成对应后缀,以便编辑者了解含义;
  • 通过select选择不同的值展示不同的表单项;
  • 通过switch设置某个值的开关去控制子组件的显示隐藏;
  • 通过input-number输入值控制子表单项个数;
    所以决定结合Element已有的组件再重新单独封装一套组件,这样只需要传入js配置文件即可动态生成成对应子项,无需复杂的写很多冗余的html文件。

使用场景

表单较多的后台管理系统
表单内容项多
动态生成表单项
表单联动交互多

组件简介

  • 传入配置文件动态生成对应的输入框,文本框,按钮,switch开关,下来选择器,树形选择器,日期选择器,时间选择器,日期范围选择器,日期时间选择器,颜色选择器等表单项。
  • 将表单配置项传入(例如tag名称,绑定属性名,是否必填,是否可填,最大值,最小值,可输入最大长度,下拉选择器的数据源,提示语,tips语等,详见下方formConfig.js文件),再将各组件的修改值事件抛出,在父组件中去处理相关值以及联动交互。
  • 考虑到层级问题,有可能同一个表单下有多级子级,故组件分为两部分,els.vueindex.vue
  • 目录
form
	index.vue
	els.vue

代码部分

  • 表单项组件 els.vue

参数描述

参数名描述类型示例
el表单单项配置Object{ el: ‘input-number’, prop: ‘time’, label: ‘活动时长’, min: 1, max: 999 }
data表单值Any
isView是否只读Booleantrue/false

回调描述

方法名描述回调形参参数描述
change值被修改Object{res:修改后的值,el:当前表单项配置}

组件代码

<template>
  <div class="els-container">
    <h3 v-show="el.el == 'point-select'">
      <el-icon v-if="data.type" @click.stop="$emit('point-handle', { show: true, type: 'add' })">
        <CirclePlusFilled />
      </el-icon>
      <el-icon v-if="data.id" @click.stop="$emit('point-handle', { show: true, type: 'set' })">
        <EditPen />
      </el-icon>
    </h3>
    <h2 v-if="el.el == 'h2'">{{el.label}}</h2>
    <h3 v-else-if="el.el == 'h3'">{{el.label}}</h3>

    <el-input
      v-else-if="el.el == 'input'"
      :placeholder="`输入${el.label || el.place}`"
      v-model="data[el.prop]"
      :maxlength="el.max || 1000"
      :type="el.type || 'text'"
      :disabled="el.disabled || isView || el.hidden"
      @input="$emit('change', { res: $event, el })"
    ></el-input>
    <el-input-number
      v-else-if="el.el == 'input-number'"
      :placeholder="`${el.place ? '输入' + el.place : ''}`"
      :min="el.min || 0"
      :max="el.max || 9999999999999999"
      v-model="data[el.prop]"
      @input="onInputNumber({ el, res: $event })"
      :disabled="el.disabled || isView || el.hidden"
    />
    <el-input
      v-else-if="el.el == 'text-area'"
      type="textarea"
      autosize
      :placeholder="`输入${el.label || el.place}`"
      v-model="data[el.prop]"
      :maxlength="el.max || 1000"
      show-word-limit
      @input="$emit('change', { res: $event, el })"
      :disabled="el.disabled || isView || el.hidden"
    />
    <el-date-picker
      v-else-if="el.el == 'date'"
      v-model="data[el.prop]"
      type="date"
      placeholder="选择一个日期"
      :disabled="el.disabled || isView || el.hidden"
      :clearable="false"
      value-format="YYYY-MM-DD"
      format="YYYY-MM-DD"
      :default-value="new Date()"
      @change="$emit('change', { res: $event, el })"
    />
    <el-date-picker
      v-else-if="el.el == 'time'"
      v-model="data[el.prop]"
      type="datetime"
      placeholder="选择一个日期时间"
      :disabled="el.disabled || isView || el.hidden"
      value-format="YYYY-MM-DD HH:mm:ss"
      format="YYYY-MM-DD HH:mm:ss"
      @change="$emit('change', { res: $event, el })"
    />
    <el-date-picker
      v-else-if="el.el == 'date-range'"
      v-model="data[el.prop]"
      type="daterange"
      placeholder="选择一个日期时段"
      :disabled="el.disabled || isView || el.hidden"
      :clearable="false"
      value-format="YYYY-MM-DD"
      format="YYYY-MM-DD"
      @change="$emit('change', { res: $event, el })"
    />
    <el-date-picker
      v-else-if="el.el == 'time-range'"
      v-model="data[el.prop]"
      type="datetimerange"
      placeholder="选择一个日期时间范围"
      :disabled="el.disabled || isView || el.hidden"
      :clearable="false"
      value-format="YYYY-MM-DD HH:mm:ss"
      format="YYYY-MM-DD HH:mm:ss"
      @change="$emit('change', { res: $event, el })"
    />
    <el-switch
      v-else-if="el.el == 'switch'"
      v-model="data[el.prop]"
      active-color="#13C75B"
      @change="$emit('change', { res: $event, el })"
      :active-value="el.type ? 1 : true"
      :inactive-value="el.type ? 0 : false"
      :disabled="el.disabled || isView || el.hidden"
    />
    <el-radio-group
      v-else-if="el.el == 'radio'"
      v-model="data[el.prop]"
      :disabled="el.disabled || isView || el.hidden"
    >
      <el-radio
        v-for="oitem in el.options"
        :key="oitem.value"
        :label="oitem.value"
        :disabled="oitem.disabled || el.hidden"
        @click="$emit('change', { res: oitem.value, el })"
      >{{ oitem.label }}</el-radio>
    </el-radio-group>
    <p v-else-if="el.el == 'p'" :class="el.class" @click="$emit('change', { el })">
      {{ el.prop?data[el.prop]:'' }}
    </p>
    <el-tree-select
      v-else-if="el.el == 'three-select'"
      filterable
      v-model="data[el.prop]"
      class="m-2"
      :placeholder="`选择${el.label || el.place}`"
      :multiple="el.multiple"
      :data="el.options"
      :render-after-expand="false"
      :props="el.props"
      @change="$emit('change', { res: $event, el })"
      :disabled="el.disabled || isView || el.hidden"
    />
    <el-select
      v-else
      :disabled="el.disabled || isView || el.hidden"
      :clearable="el.isClear"
      filterable
      v-model="data[el.prop]"
      class="m-2"
      :placeholder="`选择${el.label || el.place}`"
      :multiple="el.multiple"
      @clear="$emit('change',{
                    item: el,
                    res:'',
                    el,
                })"
    >
      <template #prefix v-if="showIcon(data, el)">
        <img :src="data.icon || data.url" class="icon_22" alt />
      </template>
      <el-option
        v-for="(oitem) in el.options"
        :key="el.props.value == 'self' ? oitem : oitem[el.props.value]"
        @click="
                  onSelectChange({
                    item: el,
                    res: el.props.value == 'self' ? oitem : oitem[el.props.value],
                    el,
                    oitem,
                  })
                "
        :label="el.props.label == 'self' ? oitem : oitem[el.props.label]"
        :value="el.props.value == 'self' ? oitem : oitem[el.props.value]"
      >
        <img
          v-if="oitem.url"
          :src="oitem.url"
          alt
          srcset
          class="icon_22"
          style="margin-right: 20px"
        />
        <img
          v-if="oitem.icon"
          :src="oitem.icon"
          alt
          srcset
          class="icon_22"
          style="margin-right: 20px"
        />
        <span>{{ el.props.label == "self" ? oitem : oitem[el.props.label] }}</span>
      </el-option>
    </el-select>
    <p v-if="el.fn">{{ el.fn && el.fn(data[el.prop],data) }}</p>
  </div>
</template>
<script>
export default {
  name: "els-container"
};
</script>
<script setup>
import { defineProps, ref, defineEmits, onMounted, computed } from "vue";
import { onCheck } from "@/utils/rules";
const emits = defineEmits([
  "change"
]);
const props = defineProps({
  el: {
    type: Object,
    default: () => {}
  },
  data: {
    type: Object,
    default: () => {}
  },
  isView: {
    type: Boolean,
    default: false
  }
});

const disabled = computed(() => {
  let { isView, el } = props;
  return isView || el.disabled || el.hidden;
});
const showIcon = (data, item) => {
  return (
    (data.icon || data.url) &&
    item.options &&
    typeof item.options[0] == "object" &&
    (item.options[0].url || item.options[0].icon)
  );
};
const onSelectChange = e => {
 emits("change", e);
};
const onInputNumber = e => {
  let { res, el } = e;
  emits("change", e);
};
</script>
<style scoped lang="scss">
</style>
  • 主文件 index.vue

参数描述

参数名描述类型默认值
sureText表单确认文字String确认
cancelText表单取消文字String取消
showSure是否显示确认按钮Booleantrue
showCancel是否显示取消按钮Booleantrue
showClear是否显示清除按钮Booleanfalse
ddata当前表单值object{}
form表单配置项object示例如下formConfig.js文件
name表单名称String
flex表单主轴弹性方向Stringcol
isView是否只读模式Booleanfalse

回调描述

方法名描述回调形参参数描述
cancel取消修改null
confirm确认修改null
change表单项修改Object{res:当前值,el:表单项配置}
remove清除当前表单项值null

组件代码

<template>
  <div>
    <el-form
      :ref="($event) => setRef($event)"
      :model="ddata"
      label-width="auto"
      class="q-form flex wrap"
      :class="['q-form-' + name, 'q-form-' + name.slice(-4), `flex-${flex}-start`, refName]"
    >
      <div
        v-for="(i, n) in form.filter((i) => i)"
        :key="n"
        :class="[i.class, i.prop]"
        :style="i.hidden && { display: 'none', margin: 0 }"
      >
        <h4 v-if="i.els && i.label" :class="!i.hidden && 'm-required'">{{ i.label }}</h4>
        <slot :name="i.slot" :pItem="i" v-if="!i.hidden">
          <div class="flex has-els" v-if="i.els" :class="[i.prop,i.class]">
            <el-form-item
              v-for="(el, elIndex) in i.els"
              :key="elIndex"
              :class="[el.class, el.prop, el.prop + '_' + el.el]"
              :label="el.label&&!['h2','h3'].includes(el.el)?el.label:''"
              :prop="el.prop"
              :rules="getRules(el)"
              :style="el.hidden && { display: 'none', margin: 0 }"
            >
              <slot :name="el.slot" :sItem="el" v-if="!el.hidden">
                <elsContainer
                  :el="el"
                  :isView="isView"
                  :data="ddata"
                  @change="$emit('change',$event)"
                  @point-handle="$emit('point-handle',$event)"
                />
              </slot>
            </el-form-item>
          </div>
          <div v-else class="no-els" :class="i.prop + '_' + i.el">
            <el-form-item
              :label="i.label&&!['h2','h3'].includes(i.el)?i.label:''"
              :class="[i.class, i.prop]"
              :prop="i.prop"
              :rules="getRules(i)"
              :style="i.hidden && { display: 'none', margin: 0 }"
            >
              <elsContainer
                :el="i"
                :isView="isView"
                :data="ddata"
                @change="$emit('change',$event)"
                @point-handle="$emit('point-handle',$event)"
              />
            </el-form-item>
          </div>
        </slot>
        <slot name="item-handle" :pItem="i"></slot>
      </div>
      <div class="handle">
        <el-button plain type="danger" v-show="showClear" @click.stop="$emit('remove')">清 除</el-button>
        <el-button v-if="showCancel" plain @click="$emit('cancel')">
          {{
          cancelText
          }}
        </el-button>
        <el-button
          v-if="showSure"
          plain
          type="primary"
          @click="
          onCheck(
            refName == 'flowDigFormRef'
              ? flowDigFormRef:FormRef,
            () => $emit('confirm')
          )
        "
        >{{ sureText }}</el-button>
      </div>
    </el-form>
  </div>
</template>
<script>
export default {
  name: "q-form"
};
</script>
<script setup>
import { defineProps, ref, defineEmits } from "vue";
import elsContainer from "./els.vue";
const emits = defineEmits([
  "cancel",
  "confirm",
  "change",
  "check",
]);
const props = defineProps({
  sureText: {
    type: String,
    default: "确 定"
  },
  cancelText: {
    type: String,
    default: "取 消"
  },
  showSure: {
    type: Boolean,
    default: true
  },
  showCancel: {
    type: Boolean,
    default: true
  },
  showClear: {
    type: Boolean,
    default: false
  },
  refName: {
    type: String,
    default: "FormRef"
  },
  ddata: {
    type: Object,
    default: () => {}
  },
  form: {
    type: Array,
    default: () => []
  },
  name: {
    type: String,
    default: ""
  },
  flex: {
    type: String,
    default: "col"
  },
  isView: {
    type: Boolean,
    default: false
  },
});
const FormRef = ref();
const flowDigFormRef = ref();
const setRef = e => {
  let { refName } = props;
  switch (refName) {
    case "FormRef":
      FormRef.value = e;
      break;
    case "flowDigFormRef":
      flowDigFormRef.value = e;
      break;
  }
};

const getRules = e => {
  let { el, hidden, els, required, label, place } = e;
  let message = `${el && el.includes("input") ? "输入" : "选择"}${label ||
    place ||
    "内容"}`;
  let nowrequired;
  if (required == undefined) {
    nowrequired =
      hidden == undefined
        ? ["true", "1"].includes(
            `${
              required == undefined ? !(el == "p" || hidden || els) : required
            }`
          )
        : !hidden;
  } else {
    nowrequired = required;
  }
  return [
    {
      required:nowrequired,
      message,
      trigger: ["blur", "change"]
    }
  ];
};
const onCheck = (formRef, fn) => {
    if (!formRef) return;
    formRef.validate((valid) => {
        if (valid) {
            console.log("submit!");
            fn && fn();
        } else {
            console.error("error submit!");
            return false;
        }
    });
};
</script>
<style scoped lang="scss">
.condition {
  min-width: 100%;
}
.tip {
  p {
    color: #f00;
    font-size: 10px;
    transform: scale(0.9);
    position: absolute;
    margin-left: 68px !important;
    margin-top: 5px;
  }
}
.flex-col-start {
  > div {
    width: 100%;
  }
}
.el-form {
  padding-bottom: 10px;
  > div {
    ::v-deep .el-form-item {
      width: 100%;
      .el-form-item__content {
        > div {
          > p {
            margin-left: 20px;
          }
        }
      }
    }
  }
  .handle {
    width: 100% !important;
  }
  > div {
    // flex: 0 0 50%;
    flex: 0 0 100%;
    // margin-top: 10px;
    align-items: center;
    // background: #f00;
    > h4 {
      margin-bottom: 10px;
      font-size: 15px;
      > i {
        color: #f00;
        margin-right: 4px;
      }
    }
    > div {
      .el-form-item {
        margin-bottom: 15px;
        > div {
          > div {
          }
        }
        ::v-deep .el-form-item__error {
          // top: 105%;
          left: 10px;
        }
      }
    }
  }
  > .handle {
    flex: 0 0 100% !important;
    text-align: right;
  }
}

</style>

使用示例

  1. 创建formConfig.js配置文件代码
let formConfig = [
    {
        "label": "活动名称与别名",
        "els": [
            {
                "label": "活动名称",
                "el": "input",
                "prop": "name",
                "place": "活动名称",
                "max": 15
            },
            {
                "label": "活动别名",
                "el": "input",
                "prop": "alias",
                "place": "活动别名",
                "max": 10
            }
        ]
    },
    {
        "label": "选择入口与分组",
        "prop": "display_object",
        "els": [
            {
                "el": "three-select",
                "prop": "display_object_value",
                "label": "UI入口和分组",
                "options": [
                    {
                        "id": 101,
                        "name": "商店",
                        "group_name": [
                            {
                                "id": 1,
                                "value": "101_1",
                                "label": "分组:日常"
                            }
                        ],
                        "state": 0,
                        "label": "UI入口:商店",
                        "value": "101",
                        "children": [
                            {
                                "id": 1,
                                "value": "101_1",
                                "label": "分组:日常"
                            }
                        ]
                    },
                    {
                        "id": 100,
                        "name": "充值",
                        "group_name": [
                            {
                                "id": 1001,
                                "value": "100_1001",
                                "label": "分组:主入口"
                            }
                        ],
                        "state": 0,
                        "label": "UI入口:充值",
                        "value": "100",
                        "children": [
                            {
                                "id": 1001,
                                "value": "100_1001",
                                "label": "分组:主入口"
                            }
                        ]
                    },
                    
                ]
            },
            {
                "el": "select",
                "props": {
                    "label": "label",
                    "value": "value"
                },
                "prop": "display_object_index",
                "label": "排序等级",
                "options": [
                    {
                        "label": "第1级",
                        "value": 1
                    },
                    {
                        "label": "第2级",
                        "value": 2
                    },
                    {
                        "label": "第3级",
                        "value": 3
                    },
                    
                ],
                "default": 1
            }
        ]
    },
    {
        "label": "任务tab数量和任务tab解锁方式",
        "els": [
            {
                "el": "input-number",
                "prop": "group_num",
                "label": "任务tab数量",
                "mix": 1
            },
            {
                "el": "select",
                "props": {
                    "label": "label",
                    "value": "value"
                },
                "label": "任务tab解锁方式",
                "prop": "group_relation",
                "options": [
                    {
                        "label": "自由完成",
                        "value": 0
                    },
                    {
                        "label": "按日递进",
                        "value": 1
                    },
                    {
                        "label": "按日开放",
                        "value": 2
                    },
                    
                ]
            }
        ]
    },
    {
        "label": "子组模式",
        "els": [
            {
                "label": "子组开关",
                "el": "switch",
                "prop": "sub_group_switch"
            }
        ]
    },
    {
        "label": "积分奖励开关",
        "els": [
            {
                "label": "活动积分",
                "el": "switch",
                "prop": "activity_point_enable"
            },
            {
                "label": "分组积分",
                "el": "switch",
                "prop": "group_point_enable"
            }
        ]
    },
    {
        "label": "奖励是否补发",
        "prop": "reward_reissue_object",
        "els": [
            {
                "el": "switch",
                "prop": "is_reissue",
                "label": "是否补发"
            },
            {
                "label": "奖励补发邮件",
                "el": "select",
                "prop": "mail_id",
                "props": {
                    "label": "name",
                    "value": "id"
                },
                "api": "EMAIL_GET",
                "options": [
                    {
                        "id": 1,
                        "name": "道具奖励补发",
                        "state": 0
                    },
                    {
                        "id": 5,
                        "name": "任务奖励补发",
                        "state": 0
                    },
                    {
                        "id": 6,
                        "name": "招募奖励补发",
                        "state": 0
                    },
                    {
                        "id": 10,
                        "name": "社团请离通知",
                        "state": 0
                    },
                    
                ],
                "hidden": false
            }
        ]
    },
    {
        "label": "选择循环模式",
        "prop": "LoopObject",
        "els": [
            {
                "el": "select",
                "props": {
                    "label": "label",
                    "value": "value"
                },
                "prop": "mode",
                "label": "选择时间循环",
                "options": [
                    {
                        "label": "单次",
                        "value": 0
                    },
                    {
                        "label": "按日重开",
                        "value": 1
                    },
                   
                ]
            },
            {
                "el": "input-number",
                "prop": "cycle",
                "label": "周期参数",
                "default": 0,
                "hidden": true
            },
            {
                "el": "select",
                "prop": "version_upgrade_mode",
                "label": "版本模式",
                "options": [
                    {
                        "label": "提升大版本",
                        "value": 1
                    },
                    {
                        "label": "提升小版本",
                        "value": 2
                    }
                ],
                "default": 1,
                "props": {
                    "label": "label",
                    "value": "value"
                },
                "hidden": true
            },
            {
                "el": "input-number",
                "prop": "loop_num",
                "label": "循环次数",
                "min": 1,
                "hidden": true
            }
        ]
    },
    {
        "label": "奖励转化",
        "els": [
            {
                "label": "奖励转化",
                "el": "switch",
                "prop": "activity_overdue_recycle_switch"
            },
            {
                "label": "转化类型",
                "el": "select",
                "props": {
                    "label": "label",
                    "value": "value"
                },
                "options": [
                    {
                        "label": "清空",
                        "value": 1
                    },
                    {
                        "label": "回收",
                        "value": 2
                    }
                ],
                "prop": "activity_overdue_recycle_recycle_type",
                "hidden": true
            },
            {
                "label": "清空内容",
                "el": "prize-group",
                "prop": "activity_overdue_recycle_a_consumes",
                "person_prop": "activity_overdue_recycle_a_consumes",
                "default": {},
                "prizeType": "all",
                "hidden": true
            },
            {
                "label": "回收配置",
                "slot": "recycle-0101",
                "hidden": true
            },
            {
                "label": "发奖方式",
                "el": "select",
                "prop": "activity_overdue_recycle_reward_get_type",
                "props": {
                    "label": "label",
                    "value": "value"
                },
                "options": [
                    {
                        "label": "邮件领取",
                        "value": 1
                    },
                    {
                        "label": "自动发背包",
                        "value": 2
                    }
                ],
                "hidden": true
            },
            {
                "label": "邮件格式",
                "el": "select",
                "prop": "activity_overdue_recycle_mail_id",
                "props": {
                    "label": "name",
                    "value": "id"
                },
                "api": "EMAIL_GET",
                "options": [
                    {
                        "id": 1,
                        "name": "道具奖励补发",
                        "state": 0
                    },
                    {
                        "id": 5,
                        "name": "任务奖励补发",
                        "state": 0
                    },
                   
                ],
                "hidden": true
            }
        ]
    },
    {
        "label": "活动隐藏",
        "prop": "reward_reissue_object",
        "els": [
            {
                "el": "switch",
                "prop": "hide_after_reward",
                "label": "任务领奖后隐藏活动"
            }
        ]
    }
]
  1. 在父组件中使用
  <el-dialog
      :width="nowDialog.width"
      :title="nowDialog.title"
      v-model="nowDialog.show"
      :close-on-press-escape="false"
      @close="onDialogCancel"
    >
      <m-form
        :ddata="nowDialog.data"
        :form="nowDialog.form"
        :name="nowDialog.name"
        :flex="nowDialog.flex"
        :nowDialog="nowDialog"
        :isView="nowDialog.isView"
        @change="onDialogChange"
        @confirm="onDialogConfirm"
        @cancel="onDialogCancel"
      >
     </m-form>
    </el-dialog>
    
    <script setup>
   import formConfig from './formConfig.js'
	let nowDialog=reactive({
    "title": "任务内容主体编辑",
    "width": 827.11,
    "name": "task",
    "name2": "task",
    "show": true,
    "data": {},
    "form":formConfig,
    "flex": "col"
})
		const onDialogChange=e=>{}
		const onDialogConfirm=e=>{}
		const onDialogCancel=e=>{}
    <script>

GitHub 加速计划 / eleme / element
54.06 K
14.63 K
下载
A Vue.js 2.0 UI Toolkit for Web
最近提交(Master分支:2 个月前 )
c345bb45 6 个月前
a07f3a59 * Update transition.md * Update table.md * Update transition.md * Update table.md * Update transition.md * Update table.md * Update table.md * Update transition.md * Update popover.md 7 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐