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

最后实现的效果

相关推荐
bug_kada2 小时前
前端路由:深入理解History模式
前端·面试
XTransfer技术2 小时前
RN也有微前端框架了? Xtransfer的RN优化实践(一)多bundle架构
前端·react native
Mintopia2 小时前
Next 全栈之 API 测试:Supertest 与 MSW 双雄记 🥷⚔️
前端·javascript·next.js
泽泽爱旅行2 小时前
awk 语法解析-前端学习
linux·前端
鹏多多2 小时前
纯前端人脸识别利器:face-api.js手把手深入解析教学
前端·javascript·人工智能
无奈何杨3 小时前
CoolGuard增加枚举字段支持,条件编辑优化,展望指标取值不同
前端·后端
掘金安东尼3 小时前
工具过多:如何管理前端工具泛滥?
前端
江城开朗的豌豆3 小时前
从生命周期到useEffect:我的React函数组件进化之旅
前端·javascript·react.js
brzhang3 小时前
当AI接管80%的执行,你“不可替代”的价值,藏在这20%里
前端·后端·架构