前言
由于受到低代码功能强大的影响,觉得拖拽组件很方便,一直很想在自己的页面上做一个互动性强的功能,而且非常适用于渲染东西特别多或者想要自定义页面,使得元素按需展现,于是无意之中发现vue的draggable拖拽属性加上简单的js逻辑就能实现这个交互效果,但是肯定没有低代码好用,不可能是根据拖拽生成代码的,所以提前写死,直接做一个switch匹配,但其实如果数据量大起来就不太好,打包的时候内存会很大,只适合少量视图展示
实现效果

基本布局
模仿了低码的页面,布局分为左边组件库和右边拖拽区两个部分,这个可以自己按照实际的需求写,因为我最近在做人事系统,所以我的组件都是相关数据的图表,现在样式都是写死的,后面会增加一些自定义颜色,表名之类的之类,可能要等到下一期再发。

整体布局用了element Plus的布局容器element-plus.org/zh-CN/compo... 如果要用可能要先装好相应的依赖(参考官网给的使用文档),然后把右边的的Aside删掉
组件库
组件库侧边栏涵盖了所有的图的名称还有一些简单的详细讲解
具体的数据如下:
js
const components = ref([
{
id: 1,
icon: "fa-user-check",
title: '员工概况分析',
description: '整体员工数量与基本信息统计',
relatedFields: ['nol', 'name', 'department', 'position', 'workstatus']//关联字段
},
)]
最重要的数据命名components
一定不能改,因为跟后面传值关联性很大,很容易拿不到数据,就没有办法渲染,其次是一定要有id
跟title
的参数,这是后面渲染表的根据
html
<el-aside width="330px">
<h2 class="sidebar-header">组件库</h2>
<!-- <p>员工信息分析与报表</p> -->
<div class="sidebar-content">
<div
v-for="(item, idx) in components"
:key="idx"
:class="['analysis-item', { active: item.active }]"
draggable="true"
@dragstart="handleDragStart($event, item)"
>
<i :class="['fas', item.icon]" class="item-icon"></i>
<div class="item-info">
<h3 class="item-title">{{ item.title }}</h3>
<p class="item-description">{{ item.description }}</p>
<div class="related-fields" v-if="item.relatedFields && item.relatedFields.length">
<!-- <small>关联字段: {{ item.relatedFields.join(', ') }}</small> -->
</div>
</div>
</div>
</div>
</el-aside>
- 给写好的组件加上拖拽属性
draggable="true"
使得组件可以拖动,true
为打开 @dragstart="handleDragStart($event, item)"
:监听组件开始拖拽事件的动作handleDragStart
:是开始拖拽的处理函数名$event
:传入的事件动作,这里指的是@dragstart
开始拖拽的动作item
:被拖拽的组件的参数,这里传的是一整个对象,也就是上面具体数据里的某一组数据
handleDragStart的处理逻辑
在拖放操作时,把获取的component
对象转换成 JSON 字符串格式,以纯文本形式存储到数据传输对象中,方便在拖放的目标位置(如drop
事件中)通过getData('text/plain')
获取并还原这个对象。
(一开始用的application/json格式不知道为什么没有成功,后台返回的数据是undefine,后面改成了纯文本就可以,不过这里传什么不重要,因为后面都会解析回原来的对象,只是不同方法而已)
js
// 处理拖拽开始
const handleDragStart = (event, component) => {
// 存储组件数据到拖拽事件中
event.dataTransfer.setData('text/plain', JSON.stringify(component));
}
-
event.dataTransfer
是拖放事件中专门用于存储和传递数据的对象 -
setData()
是其方法,用于设置要传递的数据,接收两个参数:- 第一个参数
'text/plain'
表示数据类型为纯文本 - 第二个参数
JSON.stringify(component)
是要传递的数据,这里将component
对象序列化为 JSON 字符串(因为setData()
通常只能直接传递字符串)
- 第一个参数
拖拽区
拖拽区首先要确定好你想划分多少个区域进行展示,然后给每一个区域的对象命名,具体的数据参数如下:
js
//拖拽区数据
const dropZones = ref([
{ id: 'zone1', title: '模块一', initialTitle: '模块一', component: null },
{ id: 'zone2', title: '模块二', initialTitle: '模块二', component: null },
{ id: 'zone3', title: '模块三', initialTitle: '模块三', component: null },
{ id: 'zone4', title: '模块四', initialTitle: '模块四', component: null }
])
这里的id和component非常关键,和上面组件区一样非常关键,贯穿所有逻辑层,如果后面出现问题可以多检查数据以及传参
拖拽区的代码如下:
html
<el-main class="main-container">
<h2 class="main-header">编辑区</h2>
<!-- 四个固定模块区域 -->
<div class="grid-container">
<div
v-for="zone in dropZones"
:key="zone.id"
class="drop-zone"
@dragover.prevent="handleDragOver($event, zone.id)"
@dragleave="handleDragLeave($event, zone.id)"
@drop="handleDrop($event, zone.id)"
:class="{ 'drop-zone-active': activeZone === zone.id }"
>
<h3>{{ zone.title }}</h3>
<!-- 模块内容 -->
<div class="zone-content">
<!-- 占位提示 -->
<div v-if="!zone.component" class="placeholder">
<i class="fa fa-arrow-down"></i>
<p>拖入组件</p>
</div>
<!-- 图表组件 -->
<div v-else class="chart-wrapper">
<canvas :id="`chart-${zone.id}`" style="width: 600px; height: 400px;" ></canvas>
<button
class="delete-btn"
@click="removeComponent(zone.id)"
>
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</el-main>
这个部分主要靠以下五个业务逻辑函数实现,逻辑关系图如下:

handleDragOver---拖拽经过区域的处理逻辑
用于实现拖拽功能中的 "经过目标区域" 阶段处理,为后续的元素放置操作做准备
js
// 处理拖拽经过区域
const handleDragOver = (event, zoneId) => {
// 阻止默认行为以允许放置
event.preventDefault()
// 设置当前激活区域
activeZone.value = zoneId
}
- 调用
event.preventDefault()
阻止默认行为,这是实现元素放置功能的必要操作,因为浏览器默认不允许将元素放置在其他元素上 - 将当前经过的区域标识
zoneId
赋值给activeZone.value
,用于记录当前激活的拖拽目标区域(用于后续的视图渲染)
handleDragLeave---拖拽离开区域的处理逻辑
解决拖拽时经过区域内子元素可能误触发 "离开" 事件的问题,确保只有当元素真正离开整个拖拽区域时才执行相应操作,并且离开该区域后之前记录过的激活区域也要相应清除掉
js
// 处理拖拽离开区域
const handleDragLeave = (event, zoneId) => {
// 检查是否真的离开了区域
const relatedTarget = event.relatedTarget
const dropZone = event.currentTarget
if (!dropZone.contains(relatedTarget)) {
activeZone.value = null
}
}
-
判断元素是否真的离开了拖拽区域:
- 通过
event.relatedTarget
获取与事件相关的目标元素(即鼠标进入的新元素) - 通过
event.currentTarget
获取当前的拖拽区域元素
- 通过
-
关键判断:
- 使用
dropZone.contains(relatedTarget)
检查新进入的元素是否仍属于当前拖拽区域内部 - 如果新元素不在当前拖拽区域内(返回 false),则将激活区域
activeZone
设为 null,说明已离开该区域,不再进行下面的逻辑判断和渲染
- 使用
handleDrop---组件放置处理逻辑
完成组件从拖拽源到目标区域的放置,并触发图表的渲染
js
// 处理放置
const handleDrop = (event, zoneId) => {
// 阻止默认行为
event.preventDefault()
// 重置激活区域
activeZone.value = null
// 获取拖拽的组件数据
const componentData = JSON.parse(event.dataTransfer.getData('text/plain'));
// 找到对应的区域并添加组件
const zone = dropZones.value.find(z => z.id === zoneId)
if (zone) {
zone.component = componentData.id
zone.title=componentData.title
// 等待DOM更新后渲染图表
setTimeout(() => {
renderChart(zoneId,componentData.id)
}, 0)
}
}
-
重置激活状态:
activeZone.value = null
用于清除之前标记的激活状态,避免占用阻碍下一次标记。 -
获取拖拽数据:通过
event.dataTransfer.getData('text/plain')
获取之前拖拽过程中传递的数据(主要前后的存储和获取的方法要一致),并用JSON.parse
解析为组件信息对象componentData
。 -
处理放置逻辑:
- 在
dropZones.value
中查找与zoneId
匹配的目标区域,也就是找到目标区域的对象 - 如果找到对应区域,就将拖拽过来的组件信息(ID 和标题)赋值给该区域的对象(之前的数据component为空就是等待这一刻的插入,而标题是为了渲染对应的表头)
- 使用
setTimeout
延迟执行renderChart
函数,目的是等待 DOM 更新完成后再渲染图表,确保图表能正确挂载到 DOM 元素上
- 在
removeComponent---渲染图表的处理逻辑
这里是获取能放置的区域zoneId
以及组件的id
用条件判断把图表渲染上去,图表的制作参考Echarts官网echarts.apache.org/examples/zh... 的示例,我这里只为了看渲染效果,只做了一个if条件判断,后面如果组件少可以用switch+case,组件多可以考虑组件封装。
js
// 渲染图表
const renderChart = (zoneId,Id) => {
const componentId=Id//拿到组件id
var chartDom = document.getElementById(`chart-${zoneId}`)
if(componentId===1){
if (!chartDom) {
console.error(`Canvas element with id chart-${zoneId} not found`)
return
}else{
var myChart = echarts.init(chartDom);
var option;
option = {
textStyle: {
fontSize: 40,
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisLabel: {
fontSize: 20,
},
axisTitle: {
show: true,
text: '星期',
fontSize: 20,
}
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 20,
},
axisTitle: {
show: true,
text: '数值',
fontSize: 20,
}
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar',
label: {
show: true,
fontSize: 12,
position: 'top',
}
}
]
};
renderChart---移除组件的处理逻辑
移除组件,同时妥善处理相关的图表实例,避免内存泄漏,并恢复区域的初始状态
js
// 移除组件
const removeComponent = (zoneId) => {
const zone = dropZones.value.find(z => z.id === zoneId)
if (zone) {
// 销毁图表实例
if (chartInstances.value[zoneId]) {
chartInstances.value[zoneId].destroy()
delete chartInstances.value[zoneId]
}
// 移除组件
zone.component = null
zone.title=zone.initialTitle
}
}
-
接收
zoneId
作为参数,用于定位要操作的区域 -
通过
find
方法从dropZones
中找到对应 ID 的区域对象 -
如果找到该区域:
- 检查是否存在对应的图表实例(
chartInstances
中) - 若存在图表实例,先调用
destroy()
方法销毁它,再从chartInstances
中删除该实例 - 将区域的
component
属性设为null
,清除组件 - 把区域标题重置为初始标题(
initialTitle
)
- 检查是否存在对应的图表实例(