需求:
想要实现这样的一个需求,左边是组件库。中间是展示。拖拉组件到中间就形成一个组件。
刚开始用的form-create,发现不行,又用的form-create-design,但vue3版本的只有element plus库,系统刚开始用的ant-design,而且左边我只需要单行输入框和多行输入框组件就够了,有点用宰牛刀的感觉,而且右侧还不能自定义,看悟空CRM的源码看的头大,他还是用的vue2版本+element ui,只能自己撸一了。
刚开始也是看的vuedraggable文档,https://www.itxst.com/vue-draggable-next/tutorial.html,看了半天没看懂,写了个锤子。
后面找啊找,终于找到了一篇让我搞清了逻辑;,按这个一步步写最终也出来了,这个文章看一下,改下option,另一篇又看下又改下option。搞到最后啥都没有,空白页一个。啊!差点要疯了
安装
npm i -S vuedraggable@next
//导入
import draggable from 'vuedraggable'
直接在使用页面导入,因为刚开始文件改了删,删了改,结果一直报错,后面才发现没引入。。。
首先建立三个div,分别展示上图的内容
Vue3版本里不允许在里面进行for循环了,要用<template #item="{ element }"></template>,element 不能随意修改,就是for循环迭代的item
左侧draggable配置:
<draggable
:list="leftMenu"
ghost-class="ghost"
:force-fallback="true"
:group="{ name: 'list', pull: 'clone' }"
:sort="false"
itemKey="id"
:clone="clone"
>
<template #item="{ element }">
<div class="item move">
<a-button >
<template #icon v-if="element.type === 'Input'"><edit-outlined /></template>
<template #icon v-if="element.type === 'Textarea'"><ordered-list-outlined /></template>
{{ element.name }}
</a-button>
</div>
</template>
</draggable>
JS
// 左侧组件菜单,不要响应式,不然右侧修改,按钮的名称也会改
const leftMenu =[
{ name: "单行文本", type: 'Input', isChecked: true, placeholder: '请输入'},
{ name: "多行文本", type: 'Textarea',isChecked: false, placeholder: '请输入'}
]
// clone一个新的拖动组件的值
const clone =(obj :any)=> {
// 深拷贝一个对象,否则三个数据指向的都是一个地址,
const newObj =JSON.parse(JSON.stringify(obj))
return newObj
}
然后这样左侧的菜单就显示了,a-button里是根据name生成不同的icon,icon使用记得引入下,element就看下他的按钮图标怎么用的
import {
EditOutlined,
OrderedListOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue'
中间draggable:
<draggable
:list="centerData"
ghost-class="ghost"
itemKey="id"
:force-fallback="true"
group="list"
:fallback-class="true"
:fallback-on-body="true"
class="draggable"
>
<template #item="{element}">
<div class="item move">
<label class="move">{{ element.name }}</label>
<div><a-input v-model:value="input" placeholder="请输入" @focus="focusInput(element.name)" v-if="element.type === 'Input'" /></div>
</div>
</template>
</draggable>
template写要展示的组件,也可以自定义。然后根据name来控制显隐,左边拖进去的数据就会添加到centerData里,然后遍历判断就可以了,组件少可以这么搞,组件多的建议使用component标签来映射,可以看上边的第二个链接,反正原理现在就弄明白了
JS
// 中间数据
const centerData = reactive([])
// 鼠标聚焦输入框时,显示右侧内容
const focusInput = (item: any) => {
rightData.value = item
}
右侧就是自己来写就ok了,然后数据绑定成中间数据的字段就可以了,这样就可以实现右边改,中间变化的效果了。
右侧代码:
<div class="right" v-if="rightData">
<div class="title">{{ rightData.name }}</div> 这个是根据左侧选择单行输入还是多行输入显示title文字
<div class="rightContent">
<a-form ref="formRef" :model="rightData" labelAlign="left"> 把label设为在input上边,还要结合下css
<a-form-item name="name" label="标识名" :rules="[{ required: true, message: '标识名不能为空' }]">
<a-input v-model:value="rightData.name" :placeholder="input" />
</a-form-item>
<a-form-item name="placeholder" label="提示语">
<a-input v-model:value="rightData.placeholder" />
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="rightData.isChecked">是否必填</a-checkbox>
</a-form-item>
</a-form>
</div>
</div>
JS
// 右侧
const rightData = ref()
// 鼠标聚焦输入框时,显示右侧内容
const focusInput = (item: any) => {
rightData.value = item
}
CSS
.ant-form-item{
display: block;
}
这样基本就完成了
获取生成表单的内容直接获取centerData就可以了
基本原理就是左侧数据是非响应式,拖进去中间的数据变为响应式,且根据左侧的某一字段来判断显示那一个组件,点击中间组件的时候,将点击的数据传给rightData(也是响应式),然后修改右侧数据,就可以实现中间数据的变动了。中间的样式就直接修改要显示的组件样式就可以了
最后赋一下源码(具体以下面的代码为准,上面的代码可能会有出入,改bug之类没改到)
<template>
<div class="page">
<div class="left">
<div class="title">字段库</div>
<draggable
:list="leftMenu"
ghost-class="ghost"
:force-fallback="true"
:group="{ name: 'list', pull: 'clone' }"
:clone="clone"
:sort="false"
itemKey="id"
animation="300"
>
<template #item="{ element, index }">
<a-button class="btns move" :key="index">
<template #icon v-if="element.type === 'Input'"><edit-outlined /></template>
<template #icon v-if="element.type === 'Textarea'"><ordered-list-outlined /></template>
{{ element.name }}
</a-button>
</template>
</draggable>
</div>
<div class="center">
<div class="center-title">编辑字段</div>
<draggable
:list="centerData"
ghost-class="ghost"
itemKey="id"
:force-fallback="true"
group="list"
:fallback-class="true"
:fallback-on-body="true"
class="draggable"
animation="300"
>
<template #item="{element, index}">
<div class="inputItem move" @click="focusInput(element)">
<div class="form-title move">{{ element.name }}</div>
<div><a-input v-model:value="input" @focus="focusInput(element)" v-if="element.type === 'Input'" :placeholder="element.placeholder" :key="index"/></div>
<div>
<a-textarea
:key="index"
v-model:value="textarea"
show-count :maxlength="200"
@focus="focusInput(element)"
:placeholder="element.placeholder"
v-if="element.type === 'Textarea'"
/>
</div>
<div class="delItem" @click="deleteComponent(index)">
<delete-outlined style="font-size: 14px; opacity: 0.8"/>
</div>
</div>
</template>
</draggable>
</div>
<div class="right" v-if="rightData">
<div class="title">{{ rightData.name }}</div>
<div class="rightContent">
<a-form ref="formRef" :model="rightData" labelAlign="left">
<a-form-item name="name" label="标识名" :rules="[{ required: true, message: '标识名不能为空' }]">
<a-input v-model:value="rightData.name" :placeholder="input" />
</a-form-item>
<a-form-item name="placeholder" label="提示语">
<a-input v-model:value="rightData.placeholder" />
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="rightData.isChecked">是否必填</a-checkbox>
</a-form-item>
</a-form>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {
EditOutlined,
OrderedListOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue'
import { message, Modal } from "ant-design-vue";
import {ref, defineExpose, reactive, createVNode} from "vue";
import draggable from 'vuedraggable'
const formRef = ref()
const input = ref('')
const textarea = ref('')
// 左侧组件菜单
const leftMenu = [
{
name: "单行文本",
type: 'Input',
isChecked: true,
placeholder: '请输入'
},
{
name: "多行文本",
type: 'Textarea',
isChecked: false,
placeholder: '请输入'
}
]
// clone一个新的拖动组件的值
const clone =(obj :any)=> {
// 深拷贝一个对象,否则三个数据指向的都是一个地址,
const newObj =JSON.parse(JSON.stringify(obj))
return newObj
}
// 中间数据
const centerData = reactive([])
// 右侧
let rightData = ref()
// 鼠标聚焦输入框时,显示右侧内容
const focusInput = (item: any) => {
rightData.value = item
}
// 删除
const deleteComponent = (index: number) => {
Modal.confirm({
title: '确定要删除该组件吗?',
icon: createVNode(ExclamationCircleOutlined),
onOk() {
centerData.splice(index,1)
}
})
}
// 表单校验
const checkForm = () => {
formRef.value.validateFields().then( async () => {
// console.log(centerData)
})
}
// 获取中间内容
defineExpose({
checkForm
})
</script>
<style scoped lang="scss">
.ant-form-item{
display: block;
}
.page{
position: relative;
display: flex;
justify-content: center;
.left{
width: 20%;
position: absolute;
left: 10px;
.btns{
margin: 0 10px;
}
}
.center{
background-color: #ffffff;
width: 35%;
height: 100%;
border-radius: 8px;
padding: 20px 30px;
&-title{
font-size: 16px;
font-weight: 700;
}
.inputItem{
padding: 10px 20px 30px 20px;
background-color: #deebff;
border-radius: 5px;
margin: 15px;
cursor: move;
position: relative;
.form-title{
margin-bottom: 10px;
margin-left: 5px;
}
.delItem{
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
box-shadow: 0 2px 4px 0 hsla(0,0%,63.9%,.5);
background-color: #ffffff;
color: #001529;
text-align: center;
position: absolute;
bottom: -10px;
right: 10px;
}
.delItem:hover{
cursor: pointer;
background-color: #4169E1;
color: #ffffff;
}
}
.draggable{
height: 100%;
}
}
.right{
width: 15%;
height: 100%;
position: absolute;
right: 0;
.rightContent{
background-color: #ffffff;
padding: 10px 20px;
}
}
}
.title{
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
</style>