vue3自定义拖拽,vue3-dnd 的使用

资料地址:

入门文档:https://hcg1023.github.io/vue3-dnd/guide/
案例文档:https://hcg1023.github.io/vue3-dnd/example/
react-dnd:https://react-dnd.github.io/react-dnd/docs/overview

踩坑记录:

1、安装时需要安装react-dnd-html5-backend npm install vue3-dnd react-dnd-html5-backend

2、标签要包裹在app外层,而不是放在你想要拖拽的外侧,否则切换路由会报错

 <DndProvider :backend="HTML5Backend"></DndProvider> 
// 标签要包裹在app外层,而不是放在你想要拖拽的外侧,否则切换路由会报错

3、使用useDrag找到对应标签时 drag对应标签中的 :ref=“drag” 因为是通过ref获取到标签,所以标签不能是v-for的,如果是v-for的子集标签则需要单独拎出来一个组件,组件内使用useDrag()

const [, drag] = useDrag(() => ({
    
    })
// 使用useDrag找到对应标签时 drag对应标签中的  :ref="drag" 因为是通过ref获取到标签,所以标签不能是v-for的,如果是v-for的子集标签则需要单独拎出来一个组件,组件内使用useDrag()

4、拖拽预览的自定义(我认为一个巨坑的地方)

(1)、通过导入一个getEmptyImage的空图片将原有的拖拽中的样式隐藏
const [, drag, preview] = useDrag(() => ( // 定义对应的preview

onMounted(() => {
    
    
    preview(getEmptyImage(), {
    
     captureDraggingState: true })
})
(2)、通过useDragLayer定义一个自定义的拖拽层、并且这个拖拽层最好是定义在循环组件外层,只创建一个即可,拖拽时会自动找到这个拖拽层
const collect = useDragLayer((monitor) => {
    
    })
 (3)、然而这样还远没有结束,因为虽然定义好了拖拽层但是拖拽时拖拽层是不会跟着你移动的,它相当于一个固定的DOM在页面上而已   
	:style="getItemStyles(initialOffset, currentOffset, dragParams)" // 需要通过修改它的style,利用组件提供的,
	
	// 其中的参数initialOffset/currentOffset是从useDragLayer收集器导出的 
 	const collect = useDragLayer((monitor) => {
    
    
  		return {
    
    
    		dragParams: monitor.getItem(), // 拖拽信息
    		initialOffset: monitor.getInitialSourceClientOffset(), // 拖动开始时,拖动源的根dom对于客户端初始位置 固定不变)
    		currentOffset: monitor.getSourceClientOffset(), // 当前拖动源的根dom节点相对于客户端偏移量
    		initialClientOffset: monitor.getInitialClientOffset(), // 开始拖动时,鼠标位置(固定不变)
  		}
	})
	const {
    
     initialOffset, currentOffset, dragParams, initialClientOffset } = toRefs(collect)

	// 通过当前拖动的根节点偏移量,改变拖拽预览style的translate,让它跟上鼠标
	let {
    
     x, y } = currentOffset  
	const transform = `translate(${
      
      x}px, ${
      
      y}px)`
	return {
    
    transform} 

另外:还存在一个小坑,如果被拖动的元素比较大,你从元素的某个角拖动起来时拖动,预览的位置会离鼠标指针比较远,考虑到问题并不大,有的同学没有遇到该问题,此处不做过多的赘述了。如果有遇到这个问题,我也在下面分享了我的解决方案。

5、引入toRefs不是通过vue,而是@vueuse/core

import {
    
     toRefs } from '@vueuse/core'

在这里插入图片描述
注:如果你只是需要看一看有什么坑,本人的踩坑记录到这里基本已经完事了。基本功能已经可以照着文档实现一下了。
注:因为接下来的文档中会带有比较多的业务逻辑,看不懂的同学无需气馁,是因为我写的不够好而不是你的问题。(而且很多实现方式我也并不知道是不是最优解)
注:如果你实在感兴趣,那鄙人只能继续展示拙见了。

其他功能:可放置区域颜色高亮(位置限制)、可放置位置展示竖线,拖动预览始终在拖动光标中心

在这里插入图片描述

1、让可放置区域亮起来(如果可放置区域不一定?):

通过:style样式绑定backgroundColor
// canDrop 为收集器导出的可以放置标识
// canDropShow 为自定义的可放置区域
const backgroundColor = computed(() => {
    
    
  return unref(canDrop) && unref(canDropShow) ? '#F8F8FA' : ''
})
// 可放置位置限制,首先限制canDrop(拖动中,并且拖动开始区域拖动类型存在放置区域中),后面的dragItem逻辑可以不断叠加
const canDropShow = computed(() => {
    
    
	if(unref(canDrop) && dragItem.value.from === ''){
    
    
		return true
	}
	return false
})

// 将canDropShow放到useDrop的drop中,来限制可以放置
const [dropCollect, chipDrop] = useDrop(() => ({
    
    
 	drop: (obj: any, monitor: any) => {
    
    
	 	if (unref(canDropShow)) {
    
    
			**放置后的逻辑**
		}
		return // return false即为不可放置
	}
})

// 注意:1)拖拽收集器中提供了canDrop方法,并且此方法支持传入一个函数进行重写,但是重写函数中是不可以使用canDrop,因此没有选择重写此方法,而是重新放了一个computed属性(当然你可以选择试一试可否重写)
	(2)还有一种想象中的实现方式,我也并不知道会不会优于我这种:因为canDrop代表着isDragging&&拖动类型对应,那是否可以给useDrag的type设置独立的拖拽类型,useDrop的accept接收的也是动态的类型,从而让类型对应的可放置。这样我们在放置限制时,只需要使用canDrop即可。可以试一下,欢迎分享

2、使用线来标识可放置的位置(我们期望的是放置后才进行数据结构的改变,再进行视图更新。官网中有一个放在此位置就把下一个标签挤开的案例,是通过hover的时候就改变了数据来实现的,显然不是我们想要的):

// 线的展示隐藏我是通过自定义标签中的伪类进行实现的 (当然也可以通过放置一个div元素,然后移动它的位置来展示相应的线)
// 这里还遇到一个小坑:刚开始我是使用的width来展示线的宽度,但是发现线的宽度在屏幕上不同位置表现不一致,移动leftLine的left会发现线会变粗变细,所以就改用border-left了
.leftLine::before {
    
    
  position: absolute;
  top: 0;
  left: -5px;
  height: 100%;
  content: '';
  border-left: 2px solid #824dfc;
}

.rightLine::after {
    
    
  position: absolute;
  top: 0;
  right: -8px;
  height: 100%;
  content: '';
  border-left: 2px solid #824dfc;
}

在这里插入图片描述
如果是同一行标签过多,会出现换行的情况,处理标签位置时就需要先计算每一行的高度,判断其放在那一行,再计算每一行的第几个位置:

注意:
(1)我刚开始判断可放置线是将每个标签item中 设置了可放置,这样在拖拽到标签上时就可以通过提供的hover回调判断可放置区域,但是这样有一个明显缺陷是在每一行的空白区域其实也是应该可以放置的,因此这个方案成功被放弃。
(2)线的展示应该按照我下面图片的位置进行划分,我们要将放置到空白区域划分到某个标签范围内。
(3)还有一点是,标签的位置是通过原生js获取的,而鼠标位置可以通过dnd插件提供的回调进行获取。原生的getBoundingClientRect是有性能消耗的,虽然不大,但是确实有。
(4)我是将数据处理成了二维数组,每一行内折行都是一个子数组,从而每个子数组的高度都是一样的,把当前鼠标高度确定到每个子数组对应的高度,再在此子数组中找到对应横坐标位置,从而确定线应该出现在哪个位置

在这里插入图片描述

// hover的时候判断线的展示位置
const [dropCollect, chipDrop] = useDrop(() => ({
    
    
  accept: ['可放置区域对应useDrag的type'],
  drop: (obj: any, monitor: any) => {
    
    
   // 处理放置后的数据
  },
  collect(monitor) {
    
    
    return {
    
    
      dragItem: monitor.getItem(),
      handlerId: monitor.getHandlerId(),
      canDrop: monitor.canDrop(),
      isOver: monitor.isOver(),
      didDrop: monitor.didDrop(),
      // isOverCurrent: monitor.isOver({ shallow: true }),
    }
  },
  hover: (_item: any, monitor: any) => {
    
    
	lineShow(monitor) // hover时调用线的展示方法
  },
}))

/**
 * @chipsList 获取当前行中的标签dom
 * @domList 获取当前行dom
 * @curRowPosition 获取当前行的位置
 * @labelPosition 标签位置
 * @mousePosition 鼠标位置
 * @midLine 横向中线
 */
const lineShow = (monitor) => {
    
    
  if (unref(canDropShow)) {
    
    
    let chipsList = calculateHeight()
    const len = chipsList.length
    const mousePosition = monitor.getClientOffset()
    for (let i = 0; i < len; i++) {
    
    
      const labelPosition = chipsList[i]
      const nextLabelPosition = i + 1 < len ? chipsList[i + 1] : null

      // 保证是在一行,区分多行的情况
      if (mousePosition.y > labelPosition.top && mousePosition.y < labelPosition.bottom) {
    
    
        const midLine = labelPosition.left + (labelPosition.right - labelPosition.left) / 2
        // 在标签中线前
        if (mousePosition.x < midLine) {
    
    
          showLineType.value = {
    
    
            index: i,
            location: 'left',
          }
          return showLineType.value
        }
        // 一行的最后一个(不在中线前 且 下一个标签的left<当前标签的right)
        if (
          nextLabelPosition &&
          mousePosition?.x > midLine &&
          nextLabelPosition.left < labelPosition.right
        ) {
    
    
          showLineType.value = {
    
    
            index: i,
            location: 'right',
          }
          return showLineType.value
        }
        // 最后一个
        if (mousePosition?.x > midLine && i === len - 1) {
    
    
          showLineType.value = {
    
    
            index: i,
            location: 'right',
          }
          return showLineType.value
        }
      }
    }
  }
}

我是将数据处理成了二维数组,每一行内折行都是一个子数组,从而每个子数组的高度都是一样的,把当前鼠标高度确定到每个子数组对应的高度,再在此子数组中找到对应横坐标位置,从而确定线应该出现在哪个位置

// 计算出一行内折行的高度数组
const calculateHeight = () => {
    
    
  const domItem: any = document.getElementsByClassName(props.from + 'ChipList')
  const domList: any = document.getElementsByClassName(props.from + 'ChipRow')[0]
  const len = domItem.length
  if (len === 0) return {
    
    }
  const curRowPosition = domList.getBoundingClientRect()
  const multiRowArr: any = [[]] // 同一大行的数据根据内部换行 形成一个二维数组,一维数组中的每一项为当前内部行
  let j = 0
  for (let i = 0; i < len; i++) {
    
    
    const labelPosition = domItem[i].getBoundingClientRect()
    const nextLabelPosition = i + 1 < len ? domItem[i + 1].getBoundingClientRect() : null
    multiRowArr[j].push(labelPosition)
    // 如果有下一项并且下一项的左侧<当前项的右侧 则认为换行,新增一个二维数组
    if (nextLabelPosition && nextLabelPosition.left < labelPosition.right) {
    
    
      multiRowArr[++j] = []
    }
  }
  // console.log('二维数组', multiRowArr)
  // 根据每一内部行的高度 生成一个对应的行高数组 每个index位置对应行数组中的一维的每行index
  const rowHeightArr: any = [] // 每一行的高度数组:[{top,bottom}]
  if (multiRowArr.length === 1) {
    
    
    rowHeightArr.push({
    
    
      top: curRowPosition.top,
      bottom: curRowPosition.bottom,
    })
  } else {
    
    
    const rowDiff = multiRowArr[1][0].top - multiRowArr[0][0].bottom
    // console.log('行中间查', rowDiff)
    for (let i = 0; i < multiRowArr.length; i++) {
    
    
      if (i === 0) {
    
    
        rowHeightArr.push({
    
    
          top: curRowPosition.top,
          bottom: multiRowArr[i][0].bottom + rowDiff / 2,
        })
      } else if (i === length - 1) {
    
    
        rowHeightArr.push({
    
    
          top: multiRowArr[i][0].top - rowDiff / 2,
          bottom: curRowPosition.bottom,
        })
      } else {
    
    
        rowHeightArr.push({
    
    
          top: multiRowArr[i][0].top - rowDiff / 2,
          bottom: multiRowArr[i][0].bottom + rowDiff / 2,
        })
      }
    }
  }

3、拖动放下时先删除上一位置的,再添加到放置位置(也可以反过来,先添加再删除)

因为上面获取到了对应的线出现的位置,可以根据线出现的位置判断放置到了哪里

来自哪里是通过 来自行 和 来自下标来判断的。( drop:(obj,monitor)=>{} 的obj即为drag过来的数据,里面可以放置来自的位置与来自的下标)

4、拖动预览始终在光标的中心

被拖动的元素比较大,你从元素的某个角拖动起来时拖动预览的位置 会离鼠标指针比较远
因此拖拽时想要让预览的标签位于鼠标正中心,就要通过getBoundingClientRect方法获取标签在页面上的位置:(但是不得不说,这个方法会导致页面进行重绘重排,是损耗性能的)
  const tagPosition: any = document.getElementById('dragPreview')
  const {
    
     height: tagHeight, width: tagWidth } = tagPosition.getBoundingClientRect()
  // 计算出标签中心点 与 开始拖动鼠标位置的 距离
  const leftOffset = tagWidth / 2 - (initialClientOffset.x - initialOffset.x)
  const topOffset = tagHeight / 2 - (initialClientOffset.y - initialOffset.y)
  const {
    
     x, y } = currentOffset
  const transform = `translate(${
      
      x - leftOffset}px, ${
      
      y - topOffset}px)`

猜你喜欢

转载自blog.csdn.net/m0_48030026/article/details/126950099