动态表单
-
目前的动态表单基本都是基于vuedraggable拖拽库进行延伸开发
-
在动态表单的基础上实现自定义布局,主要是依赖于vue3-draggable-resizable库实现的移动位置和缩放等
-
控件的实现方式 主要是利用的vue的render函数,方便维护
-
以下只列举其中几个控件
import { resolveComponent } from 'vue'
/**
* 在 3.x 中,由于 VNode 是上下文无关的,不能再用字符串 ID 隐式查找已注册组件。取而代之的是,需要使用一个导入的 resolveComponent 方法
*/
import { elementData } from '@/utils/type'
export default (h:Function, _self:any) => {
return [h(resolveComponent('el-input'),
{
placeholder: _self.obj.placeholder || '这是一个输入框',
// maxlength: parseInt(_self.obj.maxLength) || 20,
modelValue: _self.obj.value || '',
oninput: function(value:any) {
_self.obj.value = value.currentTarget.value
}
}
)]
}
export const inputConf:elementData = {
// 对应数据库内类型
type: 'input',
// 是否可配置
config: true,
// 控件左侧label内容
label: '输入框',
placeholder: '',
// 是否显示行内元素
inlineBlock: false,
// 是否必填
require: false,
// 最大长度
maxLength: 10,
// 选项内数据
items: [{ 'label_value': null, 'label_name': '' }],
value: '',
// 表单name
name: '',
// 验证错误提示信息
ruleError: '该字段不能为空',
// 是否关联字段
relation: false,
// 关联字段name
relation_name: '',
// 关联字段value
relation_value: '',
// 是否被渲染
visibility: true
}
//--------------------------------------
import { resolveComponent } from 'vue'
import { elementData } from '@/utils/type'
export default (h: Function, _self: any) => {
return [
h(resolveComponent('el-select'),
{
modelValue: _self.obj.value,
placeholder: _self.obj.placeholder,
filterable: true,
onChange: function (val: String) {
_self.obj.value = val
}
},
() => _self.obj.items.map((v: { label: String, value: Number | String | Array<any> }) => {
return h(resolveComponent('el-option'),
{
label: v.label,
value: v.value
})
})
)
]
}
export const selectConf:elementData = {
// 对应数据库内类型
type: 'select',
// 是否可配置
config: true,
// 控件左侧label内容
label: '下拉框',
placeholder: '请选择',
// 是否显示行内元素
inlineBlock: false,
// 是否必填
require: false,
// 最大长度
maxLength: 10,
// 选项内数据
items: [1, 2, 3, 4, 5].map((v, i) => {
return {
label: `选项${i + 1}`,
value: i + ''
}
}),
value: '',
// 表单name
name: '',
// 验证错误提示信息
ruleError: '该字段不能为空',
// 是否关联字段
relation: false,
// 关联字段name
relation_name: '',
// 关联字段value
relation_value: '',
// 是否被渲染
visibility: true
}
定义完控件以后,新建一个renders组件统一引入
import Input from '../element_builder/Input'
import Select from '../element_builder/Select'
import Radio from '../element_builder/Radio'
import Rate from '../element_builder/Rate'
import Checkbox from '../element_builder/Checkbox'
import DatePicker from '../element_builder/DatePicker'
import Divider from '../element_builder/Divider'
import Switch from '../element_builder/Switch'
import TimePicker from '../element_builder/TimePicker'
import Title from '../element_builder/Title'
import Cascader from '../element_builder/Cascader'
import { defineComponent, h, resolveComponent } from 'vue'
// import trigger from './trigger'
const formlist: {[propName:string]:any} = {
Title, Input, Select, Cascader, Radio, Checkbox, DatePicker, TimePicker, Rate, Switch, Divider
}
export default defineComponent({
name: 'Renders',
props: {
ele: {
type: String,
default: ''
},
obj: {
type: Object,
default() {
return {}
}
}
},
render() :any {
// 获取当前渲染控件
var arr = (formlist[this.ele] && formlist[this.ele](h, this))
// 已被绑定name,且require为必填,视为校验字段
const validate = this.obj.require
if (['title', 'hr', 'p'].indexOf((this.ele.toLowerCase())) < 0) {
const FormItem = {
label: (this.obj.label || this.ele) + ':',
// 指定验证name
prop: this.obj.name || 'temp',
rules: {
required: validate
}
}
return h(
resolveComponent('el-form-item'), FormItem,
() => arr
)
} else {
return h(
'div',
{
style: {
'text-align': 'center',
'width': '100%'
},
class: {
items: true
}
},
arr
)
}
}
})
//-----------------------------------
import render from './render'
const form_builder = {
install: function(Vue:Object) {}
}
form_builder.install = function(Vue:Object) {
Vue['component'](render.name, render)
}
export default form_builder
最后在页面组件中引用
<template>
<div class="row">
<div class="from-left">
<el-form
ref="ruleFormLeft"
:model="ruleForm1"
label-width="80px"
class="demo-ruleForm"
>
<draggable
:list="form_list1"
:clone="cloneData"
:group="{ name: 'formDynamic', pull: 'clone', put: false }"
item-key="ele"
:sort="false"
@change="changeLocation"
>
<template #item="{ element }">
<div class="from-left-item">
<div class="left-item">
<Menu style="width: 1em; height: 1em; margin-right: 8px;" /> {{ element.obj.label }}
</div>
<operation style="width: 1em; height: 1em; margin-right: 8px;" />
</div>
</template>
</draggable>
</el-form>
</div>
<div class="from-center">
<el-radio-group v-model="tabPosition" style="margin-bottom: 30px">
<el-radio-button label="1">设计</el-radio-button>
<el-radio-button label="2">预览</el-radio-button>
</el-radio-group>
<el-form
ref="ruleFormRight"
:model="ruleForm2"
label-width="120px"
class="demo-ruleForm"
>
<draggable
v-if="tabPosition==='1'"
:list="form_list2"
tag="transition-group"
group="formDynamic"
item-key="ele"
:component-data="componentData"
:animation="100"
@change="changeLocation"
>
<template #item="{ element,index }">
<div class="from-center-item" @click="checkItem(element)">
<div class="delete">
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
:icon="InfoFilleds"
icon-color="red"
title="确定删除当前控件吗?"
@confirm="confirmEventDel(index)"
>
<template #reference>
<DeleteFilled style="width: 1em; height: 1em; margin-right: 8px;" />
</template>
</el-popconfirm>
</div>
<renders :ele="element.ele" :obj="element.obj || {}" @changeCascader="changeCascader" />
</div>
</template>
</draggable>
<template v-else>
<div v-for="(element,index) in form_list2" :key="index" class="look">
<renders :ele="element.ele" :obj="element.obj || {}" @changeCascader="changeCascader" />
</div>
</template>
</el-form>
</div>
<div class="item-options">
<el-form
ref="form1"
class="item-options-form"
:model="itemOptions"
label-width="120px"
>
<el-form-item label="控件名称:" prop="label">
<el-input v-model="itemOptions.label" />
</el-form-item>
<el-form-item label="placeholder:" prop="placeholder">
<el-input v-model="itemOptions.placeholder" />
</el-form-item>
<el-form-item label="最大长度:" prop="maxLength">
<el-input v-model="itemOptions.maxLength" />
</el-form-item>
<el-form-item label="校验错误:" prop="ruleError">
<el-input v-model="itemOptions.ruleError" />
</el-form-item>
<el-form-item label="是否必填:" prop="require">
<el-radio-group v-model="itemOptions.require">
<el-radio :label="false">否</el-radio>
<el-radio :label="true">是</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">保存</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script lang="ts">
import { Event } from 'element-plus/es/hooks/use-events'
import { defineComponent, reactive, toRefs, shallowRef } from 'vue'
import draggable from 'vuedraggable'
import formList from '@/components/custom/formList'
import { Operation, Menu, DeleteFilled, InfoFilled } from '@element-plus/icons'
// ts接口定义
interface fromObj {
form_list1: Array<Object>,
form_list2: Array<Object>,
componentData: Object,
ruleForm1: Object,
ruleForm2: Object,
itemOptions: any,
name: String,
[propName:string]:any
}
interface ItemObj{
ele:String,
obj:{
[propName: string]: any
}
}
export default defineComponent({
name: 'Clone',
components: {
draggable, Operation, Menu, DeleteFilled
},
setup() {
const Data = reactive<fromObj>({
form_list1: formList,
form_list2: [],
componentData: {
type: 'transition',
name: 'flip-list'
},
ruleForm1: {},
ruleForm2: {},
itemOptions: {},
name: '',
tabPosition: '1'
})
const InfoFilleds = shallowRef(InfoFilled)
const changeLocation = (evt: Event): void => {}
const checkItem = (val: ItemObj): void => {
Data.itemOptions = val.obj
}
const confirmEventDel = (id:any) => {
Data.form_list2.splice(id, 1)
}
const onSubmit = () => {
console.log(Data.itemOptions)
}
const cloneData = (original: any) => {
// 深拷贝对象,防止默认空对象被更改
return JSON.parse(JSON.stringify(original))
}
const changeCascader = (value:any) => {
console.log(value)
}
return {
...toRefs(Data),
cloneData,
onSubmit,
changeLocation,
checkItem,
confirmEventDel,
InfoFilleds, changeCascader
}
}
})
</script>