图片拖拽排序
Apr 05, 2024 ·
8 Min Read
📖首先必须要对一下 API 有所了解(如果已经了解了可以请略过)
转入正题,效果图长咋样的呢?

原理
- 把图片设置为可拖动元素。
- 添加拖拽事件,监听拖拽行为。
- 拖拽事件中计算触摸点的位置,计算当前触摸点属于哪张图片上(第几行第几列),在相对把位置修改即可(修改渲染数组)。
基本原理就是这样,剩余的就是体验问题,比如添加个动画等。
可以先不往下看,根据原理尝试一遍,看看是否能完成,或许比往下看收获更多呢。
实现
布局
这里是使用vue 3编写,其他框架版本原理一样。
布局应该难不了各位,不一定按我布局方式来写,或许有更好的方法。
这里我是根据传入的列数,获取父容器宽度,动态划分每个图片模块的宽高;还监听了屏幕的尺寸改变
resize
,不过记得卸载时要移除监听器。
<template> <div class="drag-wrap" ref="dragWrapRef" > <ul class="drag-list" :style="{ height: `${drapWrapHeight}px` }"> <li class="item" v-for="(item, index) in dragList" :key="item.id" :style="{ width: `${imgSize}px`, height: `${imgSize}px`, left: `${imgLeft(index)}px`, top: `${imgTop(index)}px`, }" > <img :src="item.imgUrl" alt="" /> </li> </ul> </div></template>
<script>import { onMounted, reactive, ref, toRefs, toRef, computed, onUnmounted,} from 'vue'export default { props: { column: { type: Number, default: 4, }, dataSource: { type: Array, default: [ { id: 1, name: 'one', imgUrl: 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4008842364,2792264191&fm=26&gp=0.jpg', }, { id: 2, name: 'two', imgUrl: 'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1961155531,4083413222&fm=26&gp=0.jpg', }, { id: 3, name: 'three', imgUrl: 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1127316522,293083506&fm=26&gp=0.jpg', }, { id: 4, name: 'four', imgUrl: 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3423787266,1419039532&fm=26&gp=0.jpg', }, { id: 5, name: 'five', imgUrl: 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3771115016,2161480362&fm=26&gp=0.jpg', }, ], }, }, setup(props) { const { column } = toRefs(props)
const state = reactive({ dragList: props.dataSource })
const IMAGE_PADDING = 10 const dragWrapRef = ref(null) // 父容器对象 const drapWrapHeight = ref(0) // 父容器高度 const imgSize = ref(0) // 图片宽高
// 重新计算尺寸 const handleResize = () => { const wrapW = dragWrapRef.value.clientWidth // 获取宽度 imgSize.value = (wrapW - (column.value - 1) * IMAGE_PADDING) / column.value // 计算图片的宽高 const row = Math.ceil(state.dragList.length / column.value) // 计算出多少行 drapWrapHeight.value = row * imgSize.value + (row - 1) * IMAGE_PADDING // 计算容器的高度 }
// 设置屏幕监听 window.addEventListener('resize', handleResize)
// 计算图片left值 const imgLeft = computed(() => { return (index) => { return (index % column.value) * (imgSize.value + IMAGE_PADDING) } }) // 计算图片top值 const imgTop = computed(() => { return (index) => { const row = Math.floor(index / column.value) return row * imgSize.value + row * IMAGE_PADDING } })
onMounted(() => { handleResize() })
onUnmounted(() => { window.removeEventListener('resize', handleResize) })
return { dragList: toRef(state, 'dragList'), dragWrapRef, imgSize, drapWrapHeight, imgLeft, imgTop, } },}</script>
<style lang="scss" scoped>.drag-wrap { margin: 10px;
.drag-list { position: relative; width: 100%;
.item { overflow: hidden; position: absolute; display: flex; align-items: center; border-radius: 10px; box-shadow: 0 0 10px #ccc; background-color: #eee; transition: all 200ms ease-in-out;
img { display: block; width: 100%; }
&.dragging { transform: scale(1.04); opacity: 0.7; } } }}</style>>
拖拽
❗❗❗ 以下部分可能使用伪代码,请勿直接复制。
现在进行原理二,把元素改为可拖拽元素,并添加拖拽事件
<li ... @dragstart="(e) => handleDragStart(e, item)" draggable="true"> <img :src="item.imgUrl" alt="" draggable="false" /></li>
// 拖动开始const handleDragStart = (e, item) => { // 添加拖拽开始,把元素放大修改透明度的视绝效果 css类 e.target && e.target.classList.add('dragging') // 记录拖拽元素的DOM dragItemRef = e.target // 记录拖拽元素的数据 dragItemData = toRaw(item)}
解析
这里我之所以把 img
改为不可拖拽,是因为 img
元素默认是可以拖拽的,你们可以尝试去掉,就看到会出现什么问题了。
li
标签改为可拖拽,并添加拖拽开始事件。主要是记录数据、添加视觉css。
然后还需要添加一个拖拽结束事件,为了节省内存,设置到父容器上,当然也可以改为为每个拖拽元素设置,但没必要。(dragover
待会讲)
主要是清除数据,移除拖拽css。
<div class="drag-wrap" ref="dragWrapRef" @dragend="handleDragEnd" @dragover="handleDragOver"> ...</div>
// 拖动结束const handleDragEnd = () => { // 隐藏拖动时的css if (dragItemRef) { dragItemRef.classList.remove('dragging') } // 清除数据 dragItemRef = null dragItemData = null}
计算
// 拖动到可释放目标上触发const handleDragOver = (e) => { // 禁止默认事件 e.preventDefault() // 更新渲染列表,重要! updateDragList(e.clientX, e.clientY)}
dragover
当元素或者选择的文本被拖拽到一个有效的放置目标上时,触发 dragover
事件(每几百毫秒触发一次)。
通过它的 event
对象上获取当前鼠标的横轴坐标。然后进行原理三的计算。
/** * @description: 更新列表 * @param {number} x 横坐标 * @param {number} y 纵坐标 * @return {void} */const updateDragList = (x, y) => { if (!dragWrapRef.value || !dragItemRef || !dragItemData) return
const dropRect = dragWrapRef.value.getBoundingClientRect() if (!dropRect) return // 求出鼠标相对父容器的位置 const offsetX = x - dropRect.left const offsetY = y - dropRect.top
// 表示超出容器 if ( offsetX < 0 || offsetX > dropRect.width || offsetY < 0 || offsetY > dropRect.height ) return
// 计算出移动到第几行第几列 const col = Math.floor(offsetX / imgSize.value) // 列 const row = Math.floor(offsetY / imgSize.value) // 行 let currentIndex = row * column.value + col // 目标位置后一个index currentIndex = currentIndex > state.dragList.length - 1 ? state.dragList.length - 1 : currentIndex
const fromIndex = state.dragList.indexOf(dragItemData) // 拖拽元素的index
const newList = insertBefore( toRaw(state.dragList), dragItemData, fromIndex, currentIndex ) // 重新排序后的数组
state.dragList = newList}
/** * @description: 从from插入到to位置,返回改变后的数组 * @param {Array} list 数组 * @param {Object} fromData 拖拽对象数据 * @param {number} from 移动前的index * @param {number} to 目标index * @return {Array} */const insertBefore = (list, fromData, from, to) => { if (from === to) return list const newList = [...list] newList.splice(from, 1) newList.splice(to, 0, fromData) return newList}
解析:
- 获取父容器的相关值,计算鼠标相对父容器的横轴坐标。
- 判断拖拽是否超出父容器,超出
return
。 - 根据相对父容器的横轴坐标,计算出当前是第几行第几列,因为图片的大小、
padding
我们都是知道的嘛,然后求出在数组中拖拽元素的索引,和目标位置的索引。 - 然后对比是否位置改变了,如果改变了就操作数组即可。
注意:需要限制 currentIndex
最大值,否则会超出数组的长度。或者采用‘相对元素定位方法’,即移动后的位置相对于谁的前面,如[a, b, c, d]
,b 移到 c 位置,相对就是在 d 前面,若没有就代表放到最后。
最后,大功告成😉😉😉!!!
Last edited Feb 15