vue3引用draggable实现拖拽组件形成form表单
需求:
想要实现这样的一个需求,左边是组件库。中间是展示。拖拉组件到中间就形成一个组件。
刚开始用的form-create,发现不行,又用的form-create-design,但vue3版本的只有element plus库,系统刚开始用的ant-design,而且左边我只需要单行输入框和多行输入框组件就够了,有点用宰牛刀的感觉,而且右侧还不能自定义,看悟空CRM的源码看的头大,他还是用的vue2版本+element ui,只能自己撸一了。
刚开始也是看的vuedraggable文档,https://www.itxst.com/vue-draggable-next/tutorial.html,看了半天没看懂,写了个锤子。
后面找啊找,终于找到了一篇让我搞清了逻辑;https://blog.csdn.net/qq_38686683/article/details/129732531,按这个一步步写最终也出来了,这个文章看一下,改下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>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)