自定义图表--随意拖拽拉伸功能的实现

自定义图表--随意拖拽拉伸功能的实现

随意拖拽、拉伸元素的功能是现在大热的自定义图表的重要组成功能,本文以最简单的视角搞懂随意拖拽、拉伸元素功能,完成这个功能需要先了解原生 drag && vue-ruler-tool && @smallwei/avue

demo在线体验地址:zhao-wenchao110.gitee.io/customdrag

一、了解HTML5原生拖拽 drag

1-1、了解拖拽事件的流程

其实拖拽功能的实现无非就是三个步骤:
选中元素 ---> 拖动元素 ---> 释放元素;

1-2、如何选中元素

HTML5中只需要将元素身上设置属性 draggabletrue,那么就可以按住鼠标左键选中元素,进行拖放了,其中 draggable的属性可以设置几个值:

  • true:允许拖动
  • false:禁止拖动
  • auto:跟随浏览器定义是否可以拖动
<div draggable="true"></div>
复制代码

1-3、拖动事件

针对对象事件名称说明
被拖动的元素dragstart在元素开始被拖动时候触发
drag在元素被拖动时反复触发
dragend在拖动操作完成时触发
目的地对象dragenter当被拖动元素进入目的地元素所占据的屏幕空间时触发
dragover当被拖动元素在目的地元素内时触发
dragleave当被拖动元素没有放下就离开目的地元素时触发

其中需要注意的是 dragenter && dragover两个事件,他们是拒绝接收所有被拖放的元素,所以在使用时需要 .preventDefault 阻止默认的事件冒泡。

1-4、释放

针对对象事件名称说明
目的地对象drop当被拖动元素在目的地元素里放下时触发,一般需要取消浏览器的默认行为。

本文只能做最简单的知识点普及,如果需要更深入了解原生drag事件,我个人推荐两篇文章

另外使用vue框架也可以使用二次封装的 vuedraggable

二、了解标尺功能 vue-ruler-tool

使用后便会在上边和左边出现标尺,如上图所见;

2-1、安装

npm install --save vue-ruler-tool
复制代码

2-2、使用


<vue-ruler-tool
  v-model="dashboard.presetLine"
  class="vueRuler"
  :step-length="50"
  :parent="true"
  :position="'relative'"
  :is-scale-revise="true"
  :visible.sync="dashboard.presetLineVisible"
>
  <div></div>        
</vue-ruler-tool>

<script>
  import VueRulerTool from 'vue-ruler-tool'
  
  export default {
    components: {
      VueRulerTool
    },
  }
</script>

复制代码

2-3、属性

parent

类型:Boolean

默认值: false

限制组件大小在父级内部

<vue-ruler-tool :parent="true" >
复制代码

position

类型:String

默认值: relative

可能值:['absolute', 'fixed', 'relative', 'static', 'inherit']

规定标尺工具的定位类型

<vue-ruler-tool :position="'fixed'" >
复制代码
复制代码

isHotKey

类型: Boolean

默认值: true

快捷键键开关,目前仅支持快捷键R标尺显示开关

<vue-ruler-tool :is-hot-key="ture" >
复制代码
复制代码

visible

类型: Boolean

默认值: true

是否显示,如果设为false则隐藏,可通过.sync接收来自R快捷键的修改

<v-ruler :visible.sync="visible" >
data() {
  return {
    visible: true
  }
}
复制代码

isScaleRevise

类型: Boolean

默认值: false

刻度修正(根据content进行刻度重置),意思就是从内容的位置开始从0计数

<vue-ruler-tool :is-scale-revise="ture" >
复制代码
复制代码

topSpacing

类型: Number

默认值: 0,

标尺与窗口的上间距,如果你的position不为fixed,此项必填

leftSpacing

类型: Number

默认值: 0

标尺与窗口的左间距,如果你的position不为fixed,此项必填

presetLine

类型: Array

默认值: []

接受格式:[{ type: 'l', site: 50 }, { type: 'v', site: 180 }]

预置参考线l代表水平线,v代表垂直线,site为Number类型

<vue-ruler-tool :preset-line="[{ type: 'l', site: 100 }, { type: 'v', site: 200 }]" >
复制代码
复制代码

contentLayout

类型: Object

默认值: { top: 50, left: 50 }

内容部分布局分布,及内容摆放位置

<vue-ruler-tool :content-layout="{left:200,top:100}" >
复制代码

2-4、方法

quickGeneration

参数:[{ type: 'l', site: 100 }, { type: 'v', site: 200 }]

快速设置参考线,一般用来通过弹窗让用户输入

<vue-ruler-tool ref='rulerTool' >
let params=[
        { type: 'l', site: 100 },
        { type: 'v', site: 200 }
]
this.$refs.rulerTool.quickGeneration(params)
复制代码

三、Avue

该组件使用文档地址:avuejs.com/default/dra…

3-1、安装

yarn add  @smallwei/avue -S # 或者:npm i @smallwei/avue -S
复制代码

3-2、使用

// src/main
import Avue from '@smallwei/avue';
import '@smallwei/avue/lib/index.css';
Vue.use(Avue);
复制代码

四、自定义拖拽 demo 实现

4-1、思路

首先我们要了解自定义图表中拖拽功能的基本功能要求:

  • 每个组件图表拖拽到工作台中都是需要复制的
  • 图表组件宽与高都是可以拉伸的
  • 组件的拖拽体验需要丝滑
  • 最后工作台的组件都是需要持久化存储的

4-2、设置拖拽组件区域

template 部分

<ul class="col toolsBox">
  <!-- 组件区 -->
  <li v-for="item in arr1" :key="item.id" class="li" draggable="true" @dragstart="dragStartFn(item.id)" @dragend="dragEnd()">
    <div class="item">{{ item.name }}</div>
  </li>
  <li class="save" @click="saveWidgetsFn">
    保存
  </li>
</ul>
复制代码

data中的数据

arr1: [
  { id: 'a', name: '1' },
  { id: 'b', name: '2' },
  { id: 'c', name: '3' },
  { id: 'd', name: '4' },
  { id: 'e', name: '5' }
],
复制代码

1、固定组件根据data中的数据结构渲染,数据内最重要的是id,是用来判定组件的type种类,具有唯一性;
2、并且要给每个组件都设置 draggable="true" 表示为可拖拽;
3、设置 dragstart 元素被拖拽时触发事件存储组件唯一 id,在拖拽完成时触发 dragend 清空组件 id;

4-3、工作台区域

template 部分

<div class="col big-father">
  <!-- 标尺 -->
  <vue-ruler-tool
    v-model="dashboard.presetLine"  // 此处绑定辅助线坐标
    class="vueRuler"
    :step-length="50"  // 标尺间隔
    :parent="true"    // 限制组件大小在父级内部
    :position="'relative'" 
    :is-scale-revise="true" // 刻度从 0 开始
    :visible.sync="dashboard.presetLineVisible" // 辅助线是否显示
  >
    <!-- 工作台 -->
    <div  //此元素是内部工作台区域范围
      id="workbench"
      class="workbench"
      @drop="widgetOnDraggedFn($event)" // 组件被拖拽到工作台中复制一个新组件数据,并且将定位保存到其中
      @dragover="dragOverFn($event)" // 设置阻止冒泡事件
      @mousedown.prevent="handleMousedown" // 设置点击工作台,取消avue组件外部一圈的辅助线
    >
      // 该组件可以改变内部元素的宽高及定位
      <avue-draggable
        v-for="(item2,index2) in widgets" // widgets是工作台组件数据容器
        :key="index2" // 此处key用index,因为组件可能会重复使用,但是index不会重复,并且后面取消avue组件外部一圈的辅助线需要index
        ref="draggableAvue" 
        style="position: absolute;" // 子绝父相
        :index="index2" // 将唯一表示index绑定在组件身上,后面又用
        :width="item2.value.width"
        :height="item2.value.height"
        :left="item2.value.left"
        :top="item2.value.top"
        @focus="handleFocus" // 聚焦到组件时触发
        @blur="handleBlur" // 失焦时触发,在失焦时需要将最后调整好的宽高定位保存到data中的widgets,并且需要获取辅助线和移除辅助线
      >
        <div
          class="item2"
        >
          {{ item2.name }}
        </div>
      </avue-draggable>
    </div>
  </vue-ruler-tool>
</div>
复制代码

js 部分

<script>
import VueRulerTool from 'vue-ruler-tool'

export default {
  components: {
    VueRulerTool
  },
  data() {
    return {
      // 定义要被拖拽对象的数组
      arr1: [
        { id: 'a', name: '1' },
        { id: 'b', name: '2' },
        { id: 'c', name: '3' },
        { id: 'd', name: '4' },
        { id: 'e', name: '5' }
      ],
      dragWidgetId: '', // 当前从工具栏拖拽的组件种类Id
      currentIndex: '', // 当前工作台上操作的组件index
      // 工作台大屏画布,保存到表gaea_report_dashboard中
      dashboard: {
        id: null,
        title: '', // 大屏页面标题
        backgroundColor: '', // 大屏背景色
        backgroundImage: '', // 大屏背景图片
        presetLine: [], // 辅助线
        presetLineVisible: true // 辅助线是否显示
      },
      // 大屏画布中的组件
      widgets: [
        // {
        //   // type和value最终存到数据库中去,保存到gaea_report_dashboard_widget中
        //   id: '',
        //   value: {
        //     width: 200,
        //     height: 200,
        //     left: 200,
        //     top: 200
        //   }
        // }
      ] // 工作区中拖放的组件
    }
  },
  created() {
    // 持久化数据操作
    if (JSON.parse(localStorage.getItem('saveWidgetsFn'))) {
      this.widgets = JSON.parse(localStorage.getItem('saveWidgetsFn'))
    }
  },
  methods: {
    dragStartFn(id) {
      this.dragWidgetId = id
    },
    dragEnd() {
      this.dragWidgetId = ''
    },
    /* 拖拽到的内容区域 */
    widgetOnDraggedFn(e) {
      // 获取结束坐标和列名
      const eventX = e.clientX // 结束在屏幕的x坐标
      const eventY = e.clientY // 结束在屏幕的y坐标

      // 获取工作台 dom 的 top&left 定位
      const workbenchPosition = this.getDomTopLeftById('workbench')
      const widgetTopInWorkbench = eventY - workbenchPosition.top - 25
      const widgetLeftInWorkbench = eventX - workbenchPosition.left - 50

      // 找出复制元素的 数据内容
      const obj = this.arr1.find(item => item.id === this.dragWidgetId)

      // 在工作台增加 一个新标签
      this.widgets.push({
        id: this.dragWidgetId,
        name: obj.name,
        value: {
          width: 100,
          height: 50,
          left: widgetLeftInWorkbench,
          top: widgetTopInWorkbench
        }
      })
    },
    dragOverFn(e) {
      e.preventDefault()
      e.stopPropagation()
    },
    // 获取dom在屏幕中的top和left
    getDomTopLeftById(id) {
      const dom = document.getElementById(id)
      let top = 0
      let left = 0
      if (dom != null) {
        top = dom.getBoundingClientRect().top
        left = dom.getBoundingClientRect().left
      }
      return { top: top, left: left }
    },
    // avue
    handleFocus({ index, left, top, width, height }) {
    },
    handleBlur({ index, left, top, width, height }) {
      this.$refs.draggableAvue[index].setActive(true)
      // 保存新增修改的组件
      if (left !== 0 && top !== 0) {
        this.widgets[index].value = {
          width,
          height,
          left,
          top
        }
      }
      // 判定如果 currentIndex 数组超过1个,则需要把上一个选中取消,只保留下一个选中
      if (this.currentIndex !== index && this.currentIndex !== '') {
        this.$refs.draggableAvue[this.currentIndex].setActive(false)
      }

      this.currentIndex = index
    },
    // 存储 widgets
    saveWidgetsFn() {
      localStorage.setItem('saveWidgetsFn', JSON.stringify(this.widgets))
      alert('存储成功!')
    },
    handleMousedown() {
      if (this.currentIndex !== '') this.$refs.draggableAvue[this.currentIndex].setActive(false)
    }
  }
}
</script>
复制代码

结语

本文只是简单的写了个demo实现了随意拖拽拉伸元素功能的实现,仅仅只是自定义图表众多复杂功能中的一环,以后还会更新其余功能的实现demo与思路,希望大家多多支持~!;

gitee 本文Demo的代码地址

封面优秀开源的自定义图表,本人也是使用了该图表时所学甚多