vue的draggable拖拽属性+Echarts实现可视化自定义数据看板

前言

由于受到低代码功能强大的影响,觉得拖拽组件很方便,一直很想在自己的页面上做一个互动性强的功能,而且非常适用于渲染东西特别多或者想要自定义页面,使得元素按需展现,于是无意之中发现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一定不能改,因为跟后面传值关联性很大,很容易拿不到数据,就没有办法渲染,其次是一定要有idtitle的参数,这是后面渲染表的根据

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>
  1. 给写好的组件加上拖拽属性draggable="true"使得组件可以拖动,true为打开
  2. @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
  }
  1. 调用event.preventDefault()阻止默认行为,这是实现元素放置功能的必要操作,因为浏览器默认不允许将元素放置在其他元素上
  2. 将当前经过的区域标识zoneId赋值给activeZone.value,用于记录当前激活的拖拽目标区域(用于后续的视图渲染)

handleDragLeave---拖拽离开区域的处理逻辑

解决拖拽时经过区域内子元素可能误触发 "离开" 事件的问题,确保只有当元素真正离开整个拖拽区域时才执行相应操作,并且离开该区域后之前记录过的激活区域也要相应清除掉

js 复制代码
  // 处理拖拽离开区域
  const handleDragLeave = (event, zoneId) => {
      // 检查是否真的离开了区域
      const relatedTarget = event.relatedTarget
      const dropZone = event.currentTarget

      if (!dropZone.contains(relatedTarget)) {
          activeZone.value = null
      }
  }
  1. 判断元素是否真的离开了拖拽区域:

    • 通过event.relatedTarget获取与事件相关的目标元素(即鼠标进入的新元素)
    • 通过event.currentTarget获取当前的拖拽区域元素
  2. 关键判断:

    • 使用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)
      }
  }
  1. 重置激活状态: activeZone.value = null 用于清除之前标记的激活状态,避免占用阻碍下一次标记。

  2. 获取拖拽数据:通过 event.dataTransfer.getData('text/plain')获取之前拖拽过程中传递的数据(主要前后的存储和获取的方法要一致),并用 JSON.parse 解析为组件信息对象 componentData

  3. 处理放置逻辑:

    • 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
      }
  }
  1. 接收zoneId作为参数,用于定位要操作的区域

  2. 通过find方法从dropZones中找到对应 ID 的区域对象

  3. 如果找到该区域:

    • 检查是否存在对应的图表实例(chartInstances中)
    • 若存在图表实例,先调用destroy()方法销毁它,再从chartInstances中删除该实例
    • 将区域的component属性设为null,清除组件
    • 把区域标题重置为初始标题(initialTitle

最后实现的效果

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax