可拖拽、缩放、旋转组件网格效果及使用方法

3,869 阅读5分钟

🌈介绍

基于 vue3.x + CompositionAPI + typescript + vite 的可拖拽、缩放、旋转的组件

  • 拖拽&区域拖拽
  • 支持缩放
  • 旋转
  • 网格拖拽缩放

在线示例

源码地址

上一篇实现细节的文章遗留下了两个问题

  1. 旋转后再缩放会很奇怪
  2. 旋转后鼠标经过缩放圆点上时的样式也不相称

由于这两个问题代码量较多,建议大家直接去看源代码

这篇文章主要分享一下网格拖拽和缩放比的实现及es-drager组件的具体使用,建议大家先看看第一篇文章 可拖拽、缩放、旋转组件实现细节

网格拖拽和缩放比实现

  • 效果展示

05.gif

这块功能主要是最近有朋友在github上提过相关需求,就给es-drager加上了

添加相关props

const props = {
  // ...
  snapToGrid: Boolean, // 是否开启网格
  gridX: { // 网格X大小
    type: Number,
    default: 50
  },
  gridY: { // 网格Y大小
    type: Number,
    default: 50
  },
  scaleRatio: { // 缩放比
    type: Number,
    default: 1
  }
}

在移动的时候校验

主要关注onMousemove里对网格的判断即可

const onMousemove = (e: MouseEvent) => {
  // 使用缩放比后的移动距离计算
  let moveX = (e.clientX - downX) / props.scaleRatio + left
  let moveY = (e.clientY - downY) / props.scaleRatio + top

  // 是否开启网格对齐
  if (props.snapToGrid) {
    // 当前位置
    let { left: curX, top: curY } = dragData.value
    // 移动距离
    const diffX = moveX - curX
    const diffY = moveY - curY

    // 计算网格移动距离
    moveX = calcGridMove(diffX, props.gridX, curX)
    moveY = calcGridMove(diffY, props.gridY, curY)
  }
  
  if (props.boundary) {
    [moveX, moveY] = fixBoundary(moveX, moveY, maxX, maxY)
  }

  dragData.value.left = moveX
  dragData.value.top = moveY
  
  emit && emit('drag', dragData.value)
}

/**
 * @param moveX 移动的X
 * @param moveY 移动的Y
 * @param maxX 最大移动X距离
 * @param maxY 最大移动Y距离
 */
const fixBoundary = (moveX: number, moveY: number, maxX: number, maxY: number) => {
  // 判断x最小最大边界
  moveX = moveX < 0 ? 0 : moveX
  moveX = moveX > maxX ? maxX : moveX

  // 判断y最小最大边界
  moveY = moveY < 0 ? 0 : moveY
  moveY = moveY > maxY ? maxY : moveY
  return [moveX, moveY]
}

/**
 * @param diff 移动的距离
 * @param grid 网格大小
 * @param cur 盒子当前的位置left or top
 */
function calcGridMove(diff: number, grid: number, cur: number) {
  let result = cur
  // 移动距离超过grid的1/2,累加grid,移动距离为负数减掉相应的grid
  if (Math.abs(diff) > grid / 2) {
    result = cur + (diff > 0 ? grid : -grid)
  }

  return result
}

  • 由于父元素或者画布可能会缩放,那么就可以将这个缩放比(scaleRatio)传给es-drager,每次移动需要先将移动的距离和缩放比进行换算一下
// 使用缩放比后的移动距离计算
let moveX = (e.clientX - downX) / props.scaleRatio + left
let moveY = (e.clientY - downY) / props.scaleRatio + top
  • 如果传入snapToGrid为true,则计算网格移动,得到这次移动的距离,如果距离大于传入的gridX或gridY的1/2则移动一个网格距离,calcGridMove函数主要就是这个功能

网格缩放 resize

const onMousemove = (e: MouseEvent) => {
  // 移动的x距离
  let disX = (e.clientX - downX) / props.scaleRatio
  // 移动的y距离
  let disY = (e.clientY - downY) / props.scaleRatio

  // 开启网格缩放
  if (props.snapToGrid) {
    disX = calcGridResize(disX, props.gridX)
    disY = calcGridResize(disY, props.gridY)
  }

  const [side, position] = dotInfo.side.split('-')

  // 是否是上方缩放圆点
  const hasT = side === 'top'
  // 是否是左方缩放圆点
  const hasL = [side, position].includes('left')
  
  let width = elRect.width + (hasL ? -disX : disX)
  let height = elRect.height + (hasT ? -disY : disY)
  
  // 如果是左侧缩放圆点,修改left位置
  let left = elRect.left + (hasL ? disX : 0)

  // 如果是上方缩放圆点,修改top位置
  let top = elRect.top + (hasT ? disY : 0)

  if (!position) { // 如果是四个正方位
    if (['top', 'bottom'].includes(side)) {
      // 上下就不改变宽度
      width = elRect.width
    } else {
      // 左右就不改变高度
      height = elRect.height
    }
  }

  // 处理逆向缩放
  if (width < 0) {
    width = -width
    left -= width
  }
  if (height < 0) {
    height = -height
    top -= height
  }

  dragData.value = { left, top, width, height }
  emit('resize', dragData.value)
}

其它代码上一篇讲过,主要看这几行新增的代码,前两行同样考虑缩放比(没有这个计算,鼠标可能不会在小圆点按下的位置)

  // 移动的x距离
  let disX = (e.clientX - downX) / props.scaleRatio
  // 移动的y距离
  let disY = (e.clientY - downY) / props.scaleRatio

  // 开启网格缩放
  if (props.snapToGrid) {
    disX = calcGridResize(disX, props.gridX)
    disY = calcGridResize(disY, props.gridY)
  }

和上面拖拽一样,如果开启网格则调用calcGridResize函数得到缩放的大小,calcGridResize函数实现如下

/**
 * @param diff 缩放移动距离
 * @param grid 网格大小
 */
 function calcGridResize(diff: number, grid: number) {
  // 得到每次缩放的余数
  const r = Math.abs(diff) % grid

  // 正负grid
  const mulGrid = diff > 0 ? grid : -grid
  let result = 0
  // 余数大于grid的1/2
  if (r > grid / 2) {
    result = mulGrid * Math.ceil(Math.abs(diff) / grid)
  } else {
    result = mulGrid * Math.floor(Math.abs(diff) / grid)
  }

  return result
}

es-drager 的具体使用

安装依赖

npm i es-drager

全局注册

import { createApp } from 'vue'
import App from './App.vue'

import 'es-drager/lib/style.css'
import Drager from 'es-drager'

createApp(App)
  .component('es-drager', Drager)
  .mount('#app')
  • 使用
<template>
  <es-drager>
    drager
  </es-drager>
</template>

组件中直接使用

<template>
  <Drager>
    drager
  </Drager>
</template>

<script setup lang='ts'>
import Drager from 'es-drager'
</script>

浏览器直接引入

直接通过浏览器的 HTML 标签导入 es-drager,然后就可以使用全局变量 ESDrager 了。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://unpkg.com/es-drager/lib/style.css">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <es-drager>drager</es-drager>
  </div>

  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script src="https://unpkg.com/es-drager"></script>
  <script>
    const { createApp } = Vue
    const app = createApp({})
    app.use(ESDrager)
    app.mount('#app')
  </script>
</body>
</html>

基础使用

06.gif

<template>
  <Drager
    v-for="item, index in dragList"
    :key="index"
    :left="120"
    :top="index * 120 + 30"
    v-bind="item"
    :style="{ color: item.color }"
  >
    {{ item.text }}
  </Drager>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
import Drager from 'es-drager'

const dragList = ref([
  { text: '移动', resizable: false },
  { color: '#00c48f', text: '移动+缩放' },
  { color: '#ff9f00', text: '旋转', rotatable: true, resizable: false },
  { color: '#f44336', text: '旋转+缩放', rotatable: true }
])

</script>

事件监听

es-drager 提供了丰富的事件以便完成更细度的操作

<template>
  <Drager
    :left="100"
    :top="100"
    rotatable
    @change="onChange"
    @drag="onDrag"
    @drag-start="onDragStart"
    @drag-end="onDragEnd"
    @resize="onResize"
    @resize-start="onResizeStart"
    @resize-end="onResizeEnd"
    @rotate="onRotate"
    @rotate-start="onRotateStart"
    @rotate-end="onRotateEnd"
  />
  
</template>

<script setup lang='ts'>
import Drager, { type DragData } from 'es-drager'

// drag、resize、rotate
const onChange = (dragData: DragData) => {
  console.log('onChange', dragData)
}

// draging
const onDrag = (dragData: DragData) => {
  console.log('onDrag', dragData)
}
// drag start
const onDragStart = (dragData: DragData) => {
  console.log('onDragStart', dragData)
}
// drag end
const onDragEnd = (dragData: DragData) => {
  console.log('onDragEnd', dragData)
}

// resizing
const onResize = (dragData: DragData) => {
  console.log('onResize', dragData)
}
// resize start
const onResizeStart = (dragData: DragData) => {
  console.log('onResizeStart', dragData)
}
// resize end
const onResizeEnd = (dragData: DragData) => {
  console.log('onResizeEnd', dragData)
}

// rotating
const onRotate = (dragData: DragData) => {
  console.log('onRotate', dragData)
}
// rotate start
const onRotateStart = (dragData: DragData) => {
  console.log('onRotateStart', dragData)
}
// resize end
const onRotateEnd = (dragData: DragData) => {
  console.log('onRotateEnd', dragData)
}

</script>

网格效果

通过 snapToGrid 是否开启网格,gridX、gridY分别表示网格横纵大小

scaleRatio缩放比例(如果父标签或者画布放大或缩小可能会影响拖拽缩放的距离),如果有缩放请传入scaleRatio

我们也可以使用方位按键来控制移动(只会移动已选中的元素),开启网格移动网格距离,否则每次移动1像素 ,也可使用disabledKeyEvent禁用方向键移动

<template>
  <div
    class="es-grid-box"
    :style="gridStyle"
  >
    <Drager
      :top="100"
      :left="100"
      :gridX="gridX"
      :gridY="gridY"
      :snapToGrid="snapToGrid"
      :scaleRatio="scale"
      boundary
    />
  </div>
</template>

<script setup lang='ts'>
import { computed, ref } from 'vue'
import Drager from '../../../src/drager2/drager2.vue'
const snapToGrid = ref(true)
const gridX = ref(50)
const gridY = ref(50)
const scale = ref(1)
const gridStyle = computed(() => {
  return snapToGrid.value ? {
    '--es-grid-width': gridX.value + 'px',
    '--es-grid-height': gridY.value + 'px',
    transform: `scale(${scale.value})`,
    transformOrigin: 'left top'
  } : {}
})

</script>

<style lang='scss' scoped>
.es-grid-box {
  position: relative;
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  background:
        -webkit-linear-gradient(top, transparent calc(var(--es-grid-height) - 1px), #ccc var(--es-grid-height)),
        -webkit-linear-gradient(left, transparent calc(var(--es-grid-width) - 1px), #ccc var(--es-grid-width))
        ;
    background-size: var(--es-grid-width) var(--es-grid-height);
}
</style>

使用插槽

07.gif

  • 默认插槽
  • resize 缩放handle(小圆点)插槽
  • rotate 旋转handle插槽
<template>
  <Drager
    :width="200"
    :height="120"
    :left="100"
    :top="100"
    rotatable
  >
    <img style="width: 100%;height: 100%;" :src="imgUrl">
  </Drager>

  <Drager
    :left="100"
    :top="300"
    rotatable
  >
    resize handle
    <template #resize>
      <div class="custom-resize"></div>
    </template>
  </Drager>

  <Drager
    :left="100"
    :top="450"
    rotatable
  >
    rotate handle
    <template #rotate>
      <div class="custom-rotate">E</div>
    </template>
  </Drager>
  
</template>

<script setup lang='ts'>
import Drager from 'es-drager'
import imgUrl from '../assets/demo.png'

</script>

<style lang='scss' scoped>
.custom-resize {
  width: 6px;
  height: 6px;
  border: 1px solid #0ec3b8;
}

.custom-rotate {
  font-size: 20px;
  font-weight: 700;
  color: #0ec3b8;
}
</style>

echarts 图表

08.gif

也可以插入echarts图表,注意需要将chart元素的宽高都设置为100%

监听es-drager的resize事件然后调用echarts的resize方法进行缩放

<template>
  <Drager
    :width="300"
    :height="200"
    :left="100"
    :top="100"
    @resize="handleResize"
    rotatable
  >
    <Chart ref="chartRef" />
  </Drager>
  
</template>

<script setup lang='ts'>
import { ref } from 'vue'
import Drager from 'es-drager'
import Chart from '@/components/Chart.vue'
const chartRef = ref()
function handleResize() {
  chartRef.value.resize()
}
</script>

Drager完整API

Drager 属性

属性名说明类型默认
width宽度^[number]100
height高度^[number]100
left横坐标偏移^[number]0
top纵坐标偏移^[number]0
angle旋转角度^[number]0
color颜色^[string]#3a7afe
resizable是否可缩放^[boolean]true
rotatable是否可旋转^[boolean]-
boundary是否判断边界(最近定位父节点)^[boolean]-
disabled是否禁用^[boolean]-
minWidth最小宽度^[number]-
minHeight最小高度^[number]-
selected控制是否选中^[boolean]-
snapToGrid开启网格^[boolean]-
gridX网格X大小^[number]50
gridY网格Y大小^[number]50
scaleRatio缩放比^[number]1
disabledKeyEvent禁用方向键移动^[boolean]-

Drager 事件

事件名说明类型
change位置、大小改变^[Function](dragData) => void
drag拖拽中^[Function](dragData) => void
drag-start拖拽开始^[Function](dragData) => void
drag-end拖拽结束^[Function](dragData) => void
resize缩放中^[Function](dragData) => void
resize-start缩放开始^[Function](dragData) => void
resize-end缩放结束^[Function](dragData) => void
rotate旋转中^[Function](dragData) => void
rotate-start旋转开始^[Function](dragData) => void
rotate-end旋转结束^[Function](dragData) => void
  • dragData 类型
export type DragData = {
  width: number
  height: number
  left: number
  top: number
  angle: number
}

Drager 插槽

插槽名说明
default自定义默认内容
resize缩放handle
rotate旋转handle