全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2024-03-27_手撸一个简易 Grid 拖拽布局

您的位置:首页 >> 新闻 >> 行业资讯

手撸一个简易 Grid 拖拽布局 点击关注公众号,“技术干货”及时达!最近有个需求需要实现自定义首页布局,需要将屏幕按照 6 列 4 行进行等分成多个格子,然后将组件可拖拽对应格子进行渲染展示。 对比一些已有的插件,发现想要实现产品的交互效果,没有现成可用的。本身功能并不是太过复杂,于是决定自己基于 vue 手撸一个简易的 Grid 拖拽布局。 完整源码在此,在线体验 概况需要实现 Grid 拖拽布局,主要了解这两个东西就行 拖放 API,关于拖放 API 介绍文章有很多 ,可以直接看 MDN 里拖放 API介绍,可以说很详细了。Grid 布局, Grid 布局与 Flex 布局很相似,但是 Grid 像是二维布局,Flex 则为一维布局,Grid 布局远比 Flex 布局强大。MDN 关于网格布局介绍需要实现主要包含: 组件物料栏拖拽到布局容器布局容器 Grid 布局放置时是否重叠判断拖拽时样式放置后样式容器内二次拖拽拖放操作实现拖拽中主要使用到的事件如下 被拖拽元素事件:事件触发时刻dragstart当用户开始拖拽一个元素或选中的文本时触发。drag当拖拽元素或选中的文本时触发。dragend当拖拽操作结束时触发放置容器事件:事件触发时刻dragenter当拖拽元素或选中的文本到一个可释放目标时触发。dragleave当拖拽元素或选中的文本离开一个可释放目标时触发。dragover当元素或选中的文本被拖到一个可释放目标上时触发。drop当元素或选中的文本在可释放目标上被释放时触发。可拖拽元素让一个元素能够拖拽只需要给元素设置 「draggable="true"」 即可拖拽,拖拽事件 API 提供了 「DataTransfer」 对象,可以用于设置拖拽数据信息,但是仅仅只能 「drop」 事件中获取到,但是我们需要在拖拽中就需要获取到拖拽信息,用来显示拖拽时样式,所以需要我们自己存储起来,以便读取。 需要处理主要是,在拖拽时将 将当前元素信息设置到 「dragStore」 中,结束时清空当前信息 scriptsetuplang="ts" import{dragStore}from"./drag"; constprops=defineProps{ data:DragItem; groupName?:string; } constonDragstart=(e)=dragStore.set(props.groupName,{...props.data constonDragend=()=dragStore.remove(props.groupName); /script template divclass="drag-item__el"draggable="true"@dragstart="onDragstart"@dragend="onDragend"/div /template 封装一个存储方法,然后通过配置相同 key ,可以在同时存在多个放置区域时候,区分开来。 classDragStoreTextendsDragItemData{ moveItem=newMapstring,DragItemData set(key:string,data:T){ this.moveItem.set(key,data); } remove(key:string){ this.moveItem.delete(key); } get(key:string):undefined|DragItemData{ returnthis.moveItem.get(key); } } 可放置区域首先时需要告诉浏览器当前区域是可以放置的,只需要在元素监听 「dragenter」、「dragleave」、「dragover」 事件即可,然后通过 「preventDefault」 来阻止浏览器默认行为。可以在这三个事件中处理判断当前位置是否可以放置等等。 示例: scriptsetuplang="ts" //进入放置目标 constonDragenter=(e)={ e.preventDefault(); //在目标中移动 constonDragover=(e)={ e.preventDefault(); //离开目标 constonDragleave=(e)={ e.preventDefault(); /script template div@dragenter="onDragenter($event)"@dragover="onDragover($event)"@dragleave="onDragleave($event)"@drop="onDrop($event)"/div /template 上面的代码已经可以让,元素可以拖拽,然后当元素拖到可防止区域时候,可以看到鼠标样式会变为可放置样式了。 Grid 布局我们是需要进行 Grid 拖拽布局,所以先对上面放置容器进行改造,首先就是需要将容器进行格子划分区域显示。 计算 Grid 格子大小我这里直接使用了 「@vueuse/core」 的 「useElementSize」 的 hooks 去获取容器元素大小变动,也可以自己通过 「ResizeObserver」 去监听元素变动,然后根据设置列数、行数、间隔去计算单个格子大小。 import{useElementSize}from"@vueuse/core"; /** *容器等分尺寸 *@param{*}target容器HTML *@param{*}column列数 *@param{*}row行数 *@param{*}gap间隔 *@returns */ exportconstuseBoxSize=(target:RefHTMLElement|undefined,column:number,row:number,gap:number)={ const{width,height}=useElementSize(target); returncomputed(()=({ width:(width.value-(column-1)*gap)/column, height:(height.value-(row-1)*gap)/row, }; 设置 Grid 样式根据列数和行数循环生成格子数,「rowCount」、「columnCount」为行数和列数。 divclass="drop-content__drop-container"@dragenter="onDragenter($event)"@dragover="onDragover($event)"@dragleave="onDragleave($event)"@drop="onDrop($event)" templatev-for="xinrowCount" divclass="bg-column"v-for="yincolumnCount":key="`${x}-${y}`"/div /template /div 设置 Grid 样式,下面变量中 「gap」 为格子间隔,「repeat」 是 Grid 用来重复设置相同值的,「grid-template-columns: repeat(2,100px)」 等效于 「grid-template-columns: 100px 100px」。因为我们只需在容器里监听拖拽放置事件,所以我们还需要将 所有的 「bg-column」 事件去掉,设置 「pointer-events: none」 即可。 .drop-content__drop-container { display: grid; row-gap: v-bind("gap+'px'"); column-gap: v-bind("gap+'px'"); grid-template-columns: repeat(v-bind("columnCount"), v-bind("boxSize.width+'px'")); grid-template-rows: repeat(v-bind("rowCount"), v-bind("boxSize.height+'px'")); .bg-column { background-color: #fff; border-radius: 6px; pointer-events: none; } } 效果如下: 放置元素放置元素时我们需要先计算出元素在 Grid 位置信息等,这样才知道元素应该放置那哪个地方。 拖拽位置计算当元素拖拽进容器中时,我们可以通过 「offsetX」、「offsetY」两个数据获取当前鼠标距离容器左上角位置距离,我们可以根据这两个值计算出对应的在 Grid 中做坐标。 计算方式: //计算x坐标 constgetX=(num)=parseInt(num/(boxSizeWidth+gap)); //计算y坐标 constgetY=(num)=parseInt(num/(boxSizeHeight+gap)); 需要注意的是上面计算坐标是 0,0 开始的,而 Grid 是 1,1 开始的。 获取拖拽信息我们在进入容器时,通过上面封装 「dragData」 来获取当前拖拽元素信息,获取它尺寸信息等等。 //拖拽中的元素 constcurrent=reactive({ show:booleanfalse, id:undefined|numberundefined, column:number0,//宽 row:number0,//高 x:number0,//列 y:number0,//行 }); //进入放置目标 constonDragenter=(e)={ e.preventDefault(); constdragData=dragStore.get(props.groupName); if(dragData){ current.column=dragData.column; current.row=dragData.row; current.x=getX(e.offsetX); current.y=getY(e.offsetY); current.show=true; } }; //在目标中移动 constonDragover=(e)={ e.preventDefault(); constdragData=dragStore.get(props.groupName); if(dragData){ current.x=getX(e.offsetX); current.y=getY(e.offsetY); } }; constonDragleave=(e)={ e.preventDefault(); current.show=false; current.id=undefined; }; 在 drop 事件中,我们将当前拖拽元素存放起来,list 会存放每一次拖拽进来元素信息。 constlist=ref([]); //放置在目标上 constonDrop=async(e)={ e.preventDefault(); current.show=false; constitem=dragStore.get(props.groupName); list.value.push({ ...item, x:current.x, y:current.y, id:newDate().getTime(), }; 计算碰撞在上面还需要计算当前拖拽的位置是否可以放置,需要处理是否包含在容器内,是否与其他已放置元素存在重叠等等。 计算是否在容器内这个是比较好计算的,只需要当前拖拽位置左上角坐标 = 容器左上角的坐标,然后右下角的坐标 = 容器的右下角的坐标,就是在容器内的。 代码实现: /** *判断是否在当前四边形内 *@param{*}p1父容器 *@param{*}p2 *对应是左上角坐标和右下角坐标 *[0,0,1,1]=左上角坐标0,0右下角1,1 */ exportconstbooleanWithin=(p1:[number,number,number,number],p2:[number,number,number,number])={ returnp1[0]=p2[0]p1[1]=p2[1]p1[2]=p2[2]p1[3]=p2[3]; }; 计算是否与现有的相交两个矩形相交情况有很多种,计算比较麻烦,但是我们可以计算他们不相交,然后在取反方式判断是否相交。 不相交情况只有四种,假设有 p1、p2 连个矩形,它们不相交的情况只有四种: p1 在 p2 左边p1 在 p2 右边p1 在 p2 上边p1 在 p2 下边代码实现: /** *判断是两四边形是否相交 *@param{*}p1父容器 *@param{*}p2 *对应是左上角坐标和右下角坐标 *[0,0,1,1]=左上角坐标0,0右下角1,1 */ exportconstbooleanIntersects=(p1:[number,number,number,number],p2:[number,number,number,number])={ return!(p1[2]=p2[0]||p2[2]=p1[0]||p1[3]=p2[1]||p2[3]=p1[1]); }; 在放置前判断可以通过计算属性去计算,在后面拖拽中处理样式也可以用到。修改 「drop」 中方法,然后在 「drop」 中根据 「isPutDown」 是否有效。 //是否可以放置 constisPutDown=computed(()={ constcurrentXy=[current.x,current.y,current.x+current.column,current.y+current.row]; return( booleanWithin([0,0,columnCount.value,rowCount.value],currentXy)// list.value.every((item)=item.id===current.id||!booleanIntersects([item.x,item.y,item.x+item.column,item.y+item.row],currentXy)) }); 拖拽时样式上处理了基本拖放数据处理逻辑,为了更好的交互,我们可以在拖拽中显示元素预占位信息,更加直观的显示元素占位大小,类似这样: 我们可以根据上面 「current」 中信息去计算大小信息,还可以根据 「isPutDown」 去判断当前位置是否可以放置,用来显示不同交互效果。 可以直接通过 Grid 的 grid-area 属性,快速计算出放置位置信息,应为我们上面计算的 x 、y 是从 0 开始的,所以这里需要 +1。 grid-area:`${y+1}/${x+1}/${y+row+1}/${x+column+1}` 预览容器在元素放置后,我们还需要根据 list 中数据,生成元素占位样式处理,我们可以拖拽容器上层在放置一个容器,专门用来显示放置后的样式,也是可以直接使用 Grid 布局去处理。 预览样式样式基本上和 「drop-container」 样式抱持一致即可,需要注意的时需要为预览容器设置 「pointer-events: none」,避免遮挡了 「drop-container」 事件监听。 .drop-content__preview, .drop-content__drop-container { // ... } 每个元素位置信息计算方式,基本和拖拽时样式计算方式一致,直接通过 「grid-area」 去布局就可以了。 grid-area:`${y+1}/${x+1}/${y+row+1}/${x+column+1}` 二次拖拽当元素拖拽进来后,我们还需要对放置的元素支持继续拖拽。因为上面我们将预览事件通过 「pointer-events」 去除了,所以我们需要给每个子元素都加上去。然后给子元素添加 「draggable=true」,然后处理拖拽事件,基本上和上面处理方式一样,在 「dragstart」、「dragend」 处理拖拽元素信息。 然后我们还需在 「onDrop」 进行一番修改,如果是二次拖拽时只需要修改坐标信息,修改原 「onDrop」 处理方式: if(item.id){ item.x=current.x; item.y=current.y; }else{ list.value.push({ ...item, x:current.x, y:current.y, id:newDate().getTime(), } 位置偏移优化当你对元素二次拖拽时,会发现元素会存在偏移问。比如你放置了一个 1x2 元素后,当你从下面拖拽,你会发现拖拽中的占位样式和你拖拽元素位置存在偏差。 效果如下图 出现这情况应为上面我们时根据鼠标位置为左上角进行计算的,所以会存在这种偏差问题,我们可在拖拽前计算出偏移量来校正位置。 我们可以在二次拖拽时,获取到鼠标在当前元素内位置信息 constonDragstart=(e)={ constdata=props.data; data.offsetX=e.offsetX; data.offsetY=e.offsetY; dragStore.set(props.groupName,data); }; 在 「drop-container」 内计算 x、y 值时候减去偏移量,对 「onDragenter」、「onDragover」 进行如下调整修改 current.x=getX(e.offsetX)-getX(dragData?.offsetX??0); current.y=getY(e.offsetY)-getY(dragData?.offsetY??0); 拖拽元素优化因为上面我们将预览元素添加了 「pointer-events: all」,所以在我们拖拽到现有元素上时,会挡住 「drop-container」 事件的触发,在二次拖拽时,比如将一个 2x2 元素我们需要往下移动一格时,会发现也会被自己挡住。 预览元素遮挡问题,可以在拖拽时将其他元素都设置为 「none」,二次拖拽时要做自己设置为 「all」 否则会无法拖拽:style="{pointerEvents:current.showitem.id!==current.id?'none':'all'}"` 二次拖拽时自己位置遮挡问题 我们可以在拖拽时增加标识,将自己通过 「transform」 移除到多拽容器外去moveing.value ?{ opacity:0, transform:`translate(-999999999px,-9999999999px)`, } : 结语到目前为止基本上的 Grid 拖拽布局大致实现了,已经满足基本业务需求了,当然有需要朋友还可以在上面增加支持拖拉调整大小、碰撞后自动调整位置等等。 完整源码在此,在线体验:https://stackblitz.com/edit/vitejs-vite-rkwugn?file=README.md 点击关注公众号,“技术干货”及时达! 阅读原文

上一篇:2024-06-15_已开源!视频生成新SOTA,一键生成美女跳舞视频 下一篇:2022-08-04_8.7青岛站,影视大爬梯不见不散(一录同行第9站)

TAG标签:

18
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价