vue3结合Element Plus动态表单组件
- 背景介绍
- 使用场景
- 组件简介
- 代码部分
- 使用示例
背景介绍
在开发公司内部使用的管理后台时,通过原型发现表单较多,内容层级复杂,而且包含一些联动。
例如:
- 通过input在输入内容后需自动生成对应后缀,以便编辑者了解含义;
- 通过select选择不同的值展示不同的表单项;
- 通过switch设置某个值的开关去控制子组件的显示隐藏;
- 通过input-number输入值控制子表单项个数;
所以决定结合Element已有的组件再重新单独封装一套组件,这样只需要传入js配置文件即可动态生成成对应子项,无需复杂的写很多冗余的html文件。
使用场景
表单较多的后台管理系统
表单内容项多
动态生成表单项
表单联动交互多
组件简介
- 传入配置文件动态生成对应的输入框,文本框,按钮,switch开关,下来选择器,树形选择器,日期选择器,时间选择器,日期范围选择器,日期时间选择器,颜色选择器等表单项。
- 将表单配置项传入(例如tag名称,绑定属性名,是否必填,是否可填,最大值,最小值,可输入最大长度,下拉选择器的数据源,提示语,tips语等,详见下方formConfig.js文件),再将各组件的修改值事件抛出,在父组件中去处理相关值以及联动交互。
- 考虑到层级问题,有可能同一个表单下有多级子级,故组件分为两部分,
els.vue
和index.vue
- 目录
form
index.vue
els.vue
代码部分
- 表单项组件
els.vue
参数描述
参数名 | 描述 | 类型 | 示例 |
---|---|---|---|
el | 表单单项配置 | Object | { el: ‘input-number’, prop: ‘time’, label: ‘活动时长’, min: 1, max: 999 } |
data | 表单值 | Any | |
isView | 是否只读 | Boolean | true/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 | 是否显示确认按钮 | Boolean | true |
showCancel | 是否显示取消按钮 | Boolean | true |
showClear | 是否显示清除按钮 | Boolean | false |
ddata | 当前表单值 | object | {} |
form | 表单配置项 | object | 示例如下formConfig.js 文件 |
name | 表单名称 | String | |
flex | 表单主轴弹性方向 | String | col |
isView | 是否只读模式 | Boolean | false |
回调描述
方法名 | 描述 | 回调形参 | 参数描述 |
---|---|---|---|
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>
使用示例
- 创建
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": "任务领奖后隐藏活动"
}
]
}
]
- 在父组件中使用
<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>