vue实现移动端touch拖拽排序

Olathe ·
更新时间:2024-09-20
· 1495 次阅读

目录

功能介绍:

大致需求:

整体思路:

简单效果展示:

具体实现:

一、display:flex+v-for布局:

二、touch事件绑定:

三、卡片移动:

四、获取手指所在位置:

五、操作数组(删除或插入元素):

六、手指离开屏幕:

七、备注:

八、完整代码:

本文实例为大家分享了vue实现移动端touch拖拽排序的具体代码,供大家参考,具体内容如下

功能介绍:

在移动端开发中,希望实现类似支付宝应用管理页面的可拖拽排序交互。

大致需求:

1、卡片按照一定顺序排序,超出横向范围换行显示;
2、手指长按卡片,可进行拖拽控制,卡片追随手指移动;
3、卡片移动到相应位置,该位置上的卡片向后或向前更换位置,当前位置空出;
4、松开手指,卡片可回到原位置或新位置进行展示;

整体思路:

1、卡片实行flex弹性布局,通过数组的遍历可自动显示在相应位置;
2、手指长按可使用定时器来判断,若手指松开,则关闭定时器,等待下次操作再启用;
3、跟随手指移动的卡片可使用absolute定位控制,同时根据手指位置判断当前所在位置;
4、位置发生改变时,控制数组添加或删除相应元素,从而实现换位效果;

简单效果展示:

具体实现: 一、display:flex+v-for布局:

使用弹性布局实现

<!-- 外层ul控制卡片范围 --> <ul>     <li class="libox" v-for="(item, ind) in list" :key="ind">         <div>         <!-- div显示数组内容 -->           {{item.name}}         </div>     </li> </ul> data() {     return {         list: [             { name: '1' }, // 卡片内容             { name: '2' },             { name: '3' }         ]     } }, ul {     width: 100%;     height: 100%;     display: flex; // 弹性布局     flex-wrap: wrap;     overflow: hidden; // 超出部分隐藏,目的阻止横向滚动     .libox {       width: 25%; // 这里以4列为例       height: 70px;      >div {         background-color:#eee;         width: calc(100% - 10px);         height: 36px;         border-radius: 18px;       }     } } 二、touch事件绑定:

应用到touchstart,touchmove,touchend事件,使用定时器实现长按效果:

<div        @touchstart="touchstart($event, item)"       @touchmove="touchMove($event, item)"       @touchend="touchEnd($event, item)" >       {{item.name}} </div> data() {     return {         timeOutEvent: 0     }; }, methods: {     // 手指触摸事件     touchstart(ev, item) {         // 定时器控制长按时间,超过500毫秒开始进行拖拽         this.timeOutEvent = setTimeout(() => {             this.longClick = 1;         }, 500);     },     // 手指在屏幕上移动     touchMove(ev) {         // 未达到500毫秒就移动则不触发长按,清空定时器         clearTimeout(this.timeOutEvent);     },     // 手指离开屏幕     touchEnd() {         clearTimeout(this.timeOutEvent);     } } 三、卡片移动:

在ul中增加一个独立的不在循环中的li标签,改为absolute定位,通过动态修改li标签top、left属性实现跟随手指移动效果。

<ul>     <li v-show="selectItem.name" class="selectBox" ref="selectBox">         {{selectItem.name}}     </li> </ul> ul {     position: relative;     // 此li标签的样式与循环li标签内的div样式保持一致     // 背景色加深,代表被手指选中     .selectBox {         position: absolute;         width: calc(25% - 10px);         height: 36px;         border-radius: 18px;         background-color:#6981c8;         color:white;     } }

当卡片被选中,将卡片内容赋值给全局变量,判断卡片显示隐藏(v-show判断,隐藏但占位),实现选中元素位置空出效果:

手指位置通过touchmove获取:

<div      @touchstart="touchstart($event, item)"     @touchmove="touchMove($event, item)"     @touchend="touchEnd($event, item)"     @click="listClickHandler(item)"     v-show="item.name !== selectItem.name" >     {{item.name}} </div> touchstart(ev, item) {     this.timeOutEvent = setTimeout(() => {         this.longClick = 1;         this.selectItem = item; // 将卡片内容赋值给全局变量         const selectDom = ev.target; // li元素         // 元素初始位置         this.oldNodePos = {             x: selectDom.offsetLeft,             y: selectDom.offsetTop         };         // 鼠标原始位置         this.oldMousePos = {             x: ev.touches[0].pageX,             y: ev.touches[0].pageY         };         const lefts = this.oldMousePos.x - this.oldNodePos.x; // x轴偏移量         const tops = this.oldMousePos.y - this.oldNodePos.y; // y轴偏移量         const { pageX, pageY } = ev.touches[0]; // 手指位置         this.$refs.selectBox.style.left = `${pageX - lefts}px`;         this.$refs.selectBox.style.top = `${pageY - tops}px`;     }, 500); }, touchMove(ev) {     clearTimeout(this.timeOutEvent);     // this.longClick === 1判断是否长按     if (this.longClick === 1) {         const selectDom = ev.target.parentNode; // li元素         const lefts = this.oldMousePos.x - this.oldNodePos.x; // x轴偏移量         const tops = this.oldMousePos.y - this.oldNodePos.y; // y轴偏移量         const { pageX, pageY } = ev.touches[0]; // 手指位置         this.$refs.selectBox.style.left = `${pageX - lefts}px`;         this.$refs.selectBox.style.top = `${pageY - tops}px`;     } } 四、获取手指所在位置: cardIndex(selDom, moveleft, movetop) {     const liWid = selDom.clientWidth; // li宽度     const liHei = selDom.clientHeight; // li高度     const newWidNum = Math.ceil((moveleft / liWid)); // 手指所在列     const newHeiNum = Math.ceil((movetop / liHei)); // 手指所在行     const newPosNum = (newHeiNum - 1) * 4 + newWidNum; // 手指所在位置     // 判断是否是新位置并且没有超出列表数量范围     if (this.oldIndex !== newPosNum &&          newPosNum <= this.list.length) {         // 将新的位置赋值给全局变量oldIndex         this.oldIndex = newPosNum;     } } 五、操作数组(删除或插入元素):

监听oldIndex的值,若发生改变则执行操作数组函数

watch: {     oldIndex(newVal) {         const oldIndex = this.list.indexOf(this.selectItem);         this.list.splice(oldIndex, 1);         this.list.splice(newVal - 1, 0, this.selectItem);     } }, 六、手指离开屏幕:

手指离开屏幕,清空选中的元素selectItem,跟随手指移动的卡片(li.selectBox)自动隐藏,在循环中隐藏的卡片(li)则会显示,实现换位效果。

touchEnd() {     clearTimeout(this.timeOutEvent);     this.selectItem = {}; } 七、备注:

上面的代码是基于div容器内只有文字没有其他dom元素实现,后发现若div中存在dom元素例如svg,则【$event】选中的值会变成其子元素,且拖拽排序出现问题,希望知道原因的小伙伴可以评论或私信告诉我一下,非常感谢。
粗暴的解决方式:
div容器增加after蒙版,可设置为透明色:

div    position: relative;   &::after {     content: '';     width: 100%;     height: 100%;     background: rgba(255, 177, 177, 0.3); // 背景色     position: absolute;     top: 0;     left: 0;   } } 八、完整代码: <template>   <div>     <ul>       <li          class="libox"          v-for="(item, index) in list"         :key="index"         :id="'card' + (index + 1)"       >         <div           @touchstart="touchstart($event, item)"           @touchmove="touchMove($event, item)"           @touchend="touchEnd($event, item)"           v-show="item.name !== selectItem.name"         >           {{item.name}}           <svg class="icon svg-icon" aria-hidden="true">             <use :xlink:href="item.icon" rel="external nofollow" ></use>           </svg>         </div>       </li>       <li v-show="selectItem.name" class="selectBox" ref="selectBox">         {{selectItem.name}}         <svg class="icon svg-icon" aria-hidden="true">           <use :xlink:href="selectItem.icon" rel="external nofollow" ></use>         </svg>       </li>     </ul>   </div> </template> <script> export default {   data() {     return {       // 列表数据       list: [         { name: '1', selected: true, icon: '#icon-mianxingbenzivg' },         { name: '2', selected: true, icon: '#icon-mianxingchizi' },         { name: '3', selected: true, icon: '#icon-mianxingdiannao' },         { name: '4', selected: true, icon: '#icon-mianxingdayinji' },         { name: '5', selected: true, icon: '#icon-mianxingdingshuqi' },         { name: '6', selected: true, icon: '#icon-mianxingheiban' },         { name: '7', selected: true, icon: '#icon-mianxinggangbi' },         { name: '8', selected: true, icon: '#icon-mianxingboshimao' },         { name: '9', selected: true, icon: '#icon-mianxingjisuanqi' },         { name: '10', selected: true, icon: '#icon-mianxinghuaxue' },         { name: '11', selected: true, icon: '#icon-mianxingqianbi' },         { name: '12', selected: true, icon: '#icon-mianxingshubao' },         { name: '13', selected: true, icon: '#icon-mianxingshuicaibi' },         { name: '14', selected: true, icon: '#icon-mianxingtushu' },       ],       // 选中元素内容       selectItem: {},       timeOutEvent: 0,       oldNodePos: {         x: 0,         y: 0,       },       oldMousePos: {         x: 0,         y: 0       },       oldIndex: 0,       // 长按标识       longClick: 0     };   },   watch: {     oldIndex(newVal) {       const oldIndex = this.list.findIndex(r=> r.name === this.selectItem.name);       this.list.splice(oldIndex, 1);       this.list.splice(newVal, 0, this.selectItem);     }   },   methods: {     touchstart(ev, item) {       this.longClick = 0;       const that = this;       const selectDom = ev.currentTarget; // div元素       this.timeOutEvent = setTimeout(() => {         that.longClick = 1;         that.selectItem = item;         // 元素初始位置         that.oldNodePos = {           x: selectDom.offsetLeft,           y: selectDom.offsetTop         };         // 鼠标原始位置         that.oldMousePos = {           x: ev.touches[0].pageX,           y: ev.touches[0].pageY         };         const lefts = that.oldMousePos.x - that.oldNodePos.x; // x轴偏移量         const tops = that.oldMousePos.y - that.oldNodePos.y; // y轴偏移量         const { pageX, pageY } = ev.touches[0]; // 手指位置         that.$refs.selectBox.style.left = `${pageX - lefts}px`;         that.$refs.selectBox.style.top = `${pageY - tops}px`;       }, 500);     },     touchMove(ev) {       clearTimeout(this.timeOutEvent);       const selectDom = ev.currentTarget.parentNode; // li元素       if (this.longClick === 1) {         const lefts = this.oldMousePos.x - this.oldNodePos.x; // x轴偏移量         const tops = this.oldMousePos.y - this.oldNodePos.y; // y轴偏移量         const { pageX, pageY } = ev.touches[0]; // 手指位置         this.$refs.selectBox.style.left = `${pageX - lefts}px`;         this.$refs.selectBox.style.top = `${pageY - tops}px`;         this.cardIndex(selectDom, pageX, pageY);       }     },     touchEnd() {       clearTimeout(this.timeOutEvent);       this.selectItem = {};     },     /**      * 计算当前移动卡片位于卡片的哪一行哪一列      */     cardIndex(selDom, moveleft, movetop) {       const liWid = selDom.clientWidth;       const liHei = selDom.clientHeight;       const newWidthNum = Math.ceil((moveleft / liWid)); // 哪一列       const newHeightNum = Math.ceil((movetop / liHei)); // 哪一行       const newPositionNum = (newHeightNum - 1) * 4 + newWidthNum;       if (this.oldIndex !== newPositionNum - 1) {           if (newPositionNum <= this.list.length) {             this.oldIndex = newPositionNum - 1;           } else {             this.oldIndex = this.list.length - 1;           }       }     }   } } </script> <style lang="scss" scoped>   @mixin myFlexCenter{     display: flex;     justify-content: center;     align-items: center;   }   ul {     width: 100%;     height: 100%;     display: flex;     flex-wrap: wrap;     position: relative;     overflow: hidden;     .libox {       width: 25%;       height: 100px;       border-right: 1px dashed #cccccc;       border-bottom: 1px dashed #cccccc;       box-sizing: border-box;       @include myFlexCenter;       >div {         width: calc(100% - 10px);         height: 75px;         border-radius: 18px;         @include myFlexCenter;         position: relative;         &::after {             content: '';             width: 100%;             height: 100%;             background: rgba(255, 177, 177, 0.3);             position: absolute;             top: 0;             left: 0;         }         >svg {           width: 75px;           height: 75px;         }       }     }     .selectBox{       position: absolute;       width: calc(25% - 10px);       height: 75px;       border-radius: 18px;       >svg {         width: 75px;         height: 75px;       }       background-color: rgba(0, 0, 0, 0.1);       color:white;       @include myFlexCenter;       -moz-user-select:none; /*火狐*/       -webkit-user-select:none; /*webkit浏览器*/       -ms-user-select:none; /*IE10*/       -khtml-user-select:none; /*早期浏览器*/       user-select:none;     }   } </style>



VUE touch 排序

需要 登录 后方可回复, 如果你还没有账号请 注册新账号