> 文章列表 > 小程序封装拖拽菜单组件(uniapp拖拽排序,自定义菜单)

小程序封装拖拽菜单组件(uniapp拖拽排序,自定义菜单)

小程序封装拖拽菜单组件(uniapp拖拽排序,自定义菜单)

效果展示

在这里插入图片描述

思路

  • 使用movable-area作为可移动区域,并在内部循环渲染列表项view,绑定touch事件。
  • 在mounted生命周期函数内获取区域movable-area的dom信息,记录列表项的坐标信息。
  • 在methods中定义了列表项的touchstart、touchmove和touchend事件的方法,用于实现列表项的拖拽移动和位置变更。
  • watch监听列表项数据listData的变化,并抛出事件,通知列表变更。

具体步骤

1, 在components文件夹新建healer-dragList文件夹,在healer-dragList文件夹下新建AppList.vue组件

小程序封装拖拽菜单组件(uniapp拖拽排序,自定义菜单)

使用movable-area创建一个可移动区域容器

movable-area 是 uniapp 的可移动区域组件。它用于定义可移动视图容器,在其内部可拖拽移动子视图。
在 movable-area 组件中,可以使用 movable-view 组件定义可移动的子视图。movable-view 必须是 movable-area 的直接子节点,不支持嵌套 movable-view。

movable-area 的属性有:

  • scale - 手势缩放比例,默认为1, 范围0~10
  • direction - 可移动方向,值有 ‘all’,‘vertical’,‘horizontal’,‘none’
    movable-view 的属性有:
  • direction - 可移动方向,同 movable-area 的 direction
  • inertia - 是否启用滚动惯性,默认false
  • outOfBounds - 超出可移动区域后,movable-view 的行为,可选值有 ‘none’、‘hidden’、‘bounce’
  • x/y - movable-view 的位置
  • damping - 阻尼系数,用于控制x或y变化的动画和过界回弹的衰减速度。取值范围[0, 1]。
  • friction - 摩擦系数,用于控制x或y变化的动画和过界回弹的摩擦力。取值范围[0, 1]。

movable-area 和 movable-view 通常搭配使用,来实现可拖拽排序的列表效果。

小程序封装拖拽菜单组件(uniapp拖拽排序,自定义菜单)
//AppList.vue

<template><view><!-- 可移动区域容器 --><movable-area class="movarea" ref="areaBox" id="areaBox"><!-- 这块只是循环出固定内容,监听其元素touch事件获取坐标 --><view class="appList"><view class="app-li text-blue" v-for="(appItem,index) in listData_c" :key="appItem.name":id="'appLi' + index" :class="(hoverClass==='appLi'+index)?'select':''"@touchstart="AppLi_touchstart(index,$event)" @touchmove="AppLi_touchmove"@touchend="AppLi_touchend(index)"><uni-icons type="minus-filled" size="20" class='rightIcon' @click="dleIcon(index,appItem)"v-if="isEdti"></uni-icons><image class="appIcon" :src="appItem.appIcon" mode="widthFix"></image><text class="appName">{{appItem.appName}}</text> <text class="appicon cuIcon-roundclosefill":class="deleteAppID===appItem.appId && showDelete?'':'hide'" @tap="deleteAppItem(index)"></text></view><view class="app-li text-blue" @tap="addAppItem"><text class="appicon cuIcon-roundadd"></text></view></view><!-- 滑块 --><movable-view v-if="moviewShow" :animation="false" class="moveV text-blue" :x="moveX" :y="moveY"direction="all" :style="{ width: moveViewSize + 'px', height: 160 + 'rpx' }"><image class="appIcon" :src="touchItem.appIcon" mode="widthFix"></image><text class="appName">{{touchItem.appName}}</text></movable-view></movable-area><!-- 新增模态 --><!-- <Modal ref="addAppItem" title="添加" @confirm="confirm"><InputUnify ref="addAppInput" title="名字" placeholder="请输入应用名"></InputUnify></Modal> --></view>
</template><script>// import InputUnify from "@/components/unify-input.vue"export default {name: "AppList",props: {listData: {type: Array,default: () => {return []}},isEdti: {type: Boolean,default: false}},data() {return {listData_c: this.listData, //缓存props,(不建议直接修改props)// CheckAppId: null,deleteAppID: null, //触发删除的itemIDshowDelete: false, //删除按钮状态IsDeleteAfter: false, //是否为删除后IsCancelDelete: false, //是否为取消后moviewShow: false, //滑块状态areaBoxInfo: null, //保存滑动区域盒子dom信息inBoxXY: {}, //鼠标在item中的坐标touchIndex: 0, //被移动indextouchItem: '', //备份被移动item数据moveX: 0, //相对滑动盒子的坐标moveY: 0, //相对滑动盒子的坐标hoverClass: '',hoverClassIndex: null, //最终index};},watch: {listData_c(val) {this.$emit("listChange", val)}},computed: {moveViewSize() {if (this.areaBoxInfo && this.areaBoxInfo.width) {return this.areaBoxInfo.width / 5} else {return 0}}},components: {// InputUnify},mounted() {// 获取dom信息this.resetListDom()},methods: {dleIcon(a, b) {this.$emit("lowAppList", a);},getDomInfo(id, callBack) {const query = uni.createSelectorQuery().in(this);query.select('#' + id).boundingClientRect().exec(function(res) {callBack(res[0]);});},// 添加addAppItem() {this.$refs.addAppItem.ModalStatus()},confirm() {let appItem = {appId: this.listData_c.length + 1,appIcon: "cuIcon-pic",appName: this.$refs.addAppInput.value,appLink: ""};this.listData_c.push(appItem);this.$refs.addAppInput.resetVal();this.$nextTick(() => {this.resetListDom()});},AppLi_touchstart(index, event) {this.touchItem = this.listData_c[index];// 行为判断if (this.showDelete) {// 取消删除if (this.touchItem.appId != this.deleteAppID) {this.deleteAppID = null;this.showDelete = false;this.IsCancelDelete = true;}// 删除// if(this.touchItem.appId==this.deleteAppID){// 	this.deleteAppItem(index)// }}// 过时触发(touchEnd中清除此定时器)this.Loop = setTimeout(() => {// 触感反馈(安卓上是150毫秒,ios无短触控反馈)uni.vibrateShort();this.showDelete = true;this.deleteAppID = this.touchItem.appId;// 拖动逻辑//显示可移动方块this.moviewShow = true//保存当前所选择的索引this.touchIndex = index;// 设置可移动方块的初始位置为当前所选中图片的位置坐标this.moveX = this.listData_c[index].x;this.moveY = this.listData_c[index].y;var x = event.changedTouches[0].clientX - this.areaBoxInfo.left;var y = event.changedTouches[0].clientY - this.areaBoxInfo.top;// 保存鼠标在图片内的坐标this.inBoxXY = {x: x - this.listData_c[index].x,y: y - this.listData_c[index].y,}},500);},AppLi_touchmove(event) {// 每次endTouch清除startTouch删除按钮定时器if (this.Loop) {clearTimeout(this.Loop);this.Loop = null;}if (this.showDelete) {let areaBoxTop = this.areaBoxInfo.top;let areaBoxLeft = this.areaBoxInfo.left;//重置为以拖拽盒子左上角为坐标原点var x = event.changedTouches[0].clientX - areaBoxLeft;var y = event.changedTouches[0].clientY - areaBoxTop;this.moveX = x - this.inBoxXY.x;this.moveY = y - this.inBoxXY.y;let setIng = false;this.listData_c.forEach((item, idx) => {if (x > item.x && x < item.x + 80 && y > item.y && y < item.y + 80) {this.hoverClass = 'appLi' + idxthis.hoverClassIndex = idx;setIng = true}});// 都不存在代表脱离if (!setIng) {this.hoverClass = ""this.hoverClassIndex = null;}}},AppLi_touchend(index) {if (!this.showDelete && !this.IsDeleteAfter && !this.IsCancelDelete) {this.getInto(this.touchItem)} else {// 为下次getInto清除状态this.IsDeleteAfter = false;this.IsCancelDelete = false;// 移动结束隐藏可移动方块if (this.hoverClassIndex != null && this.touchIndex != this.hoverClassIndex) {this.$set(this.listData_c, this.touchIndex, this.listData_c[this.hoverClassIndex]);this.$set(this.listData_c, this.hoverClassIndex, this.touchItem);this.showDelete = false;this.resetListDom()}this.touchItem = ""this.moviewShow = falsethis.hoverClass = ""this.hoverClassIndex = null;}// 每次endTouch清除startTouch删除按钮定时器if (this.Loop) {clearTimeout(this.Loop);this.Loop = null;}},deleteAppItem(index) {this.listData_c.splice(index, 1)this.showDelete = false;this.checkIndex = null;this.IsDeleteAfter = true;this.resetListDom()},getInto(e) {if (e.appName == '更多') {return;}if (this.isEdti) return;uni.navigateTo({url: e.appLink,})},resetListDom() {let _this = this;this.getDomInfo('areaBox', info => {_this.areaBoxInfo = info;// 设置区域内所有图片的左上角坐标_this.listData_c.forEach((item, idx) => {_this.getDomInfo('appLi' + idx, res => {item.x = res.left - info.left;item.y = res.top - info.top;});});});},boxClick() {this.deleteAppID = null;this.showDelete = false;}}}
</script><style lang="scss" scoped>.rightIcon {position: absolute;right: 5rpx;top: -10rpx;}.movarea {width: 100%;height: auto;}.appList {width: 100%;display: flex;flex-wrap: wrap;}.app-li {width: 20%;// height: 160rpx;text-align: center;display: flex;flex-direction: column;justify-content: space-around;position: relative;margin-bottom: 30rpx;.appIcon {font-size: 60rpx;width: 50%;margin: 0 auto;}.appName {font-size: 24rpx;}.cuIcon-roundadd {font-size: 60rpx;color: #CCCCCC;}.cuIcon-roundclosefill {position: absolute;top: 12rpx;right: 12rpx;font-size: 36rpx;z-index: 2;&.hide {display: none;}}}.moveV {opacity: 0.8;z-index: 999;width: 100rpx;height: 160rpx;box-sizing: border-box;text-align: center;display: flex;flex-direction: column;justify-content: space-around;padding: 20rpx;.appIcon {font-size: 60rpx;width: 100%;}.appName {font-size: 24rpx;}}.select {// transform: scale(1.3);border-radius: 16rpx;border: 1px dashed #C0C0C0;color: #C0C0C0;}
</style>

2, 在所需页面引用AppList.vue组件

<template><view class="content"><!-- appList start--><view class="topText">点击下方【编辑】按钮,可调整首页功能展示长按图标可调整首页图标展示顺序</view><view class="title">首页已展示功能</view><view class="" style="padding: 0 30rpx;"><AppList :listData="appListData" @listChange="listChange" @lowAppList='lowAppListData' :isEdti='isEdti'></AppList></view><view class="title">其他功能</view><view style="padding: 0 30rpx;"><view class="appList"><view class="app-li text-blue" v-for="(appItem,index) in autherData" :key="appItem.name"><uni-icons type="plus-filled" size="20" class='rightIcon' @tap="addIcon(index)" v-if="isEdti"></uni-icons><image class="appIcon" :src="appItem.appIcon" mode="widthFix" @tap="goAuther(appItem)"></image><text class="appName">{{appItem.appName}}</text><text class="appicon cuIcon-roundclosefill":class="deleteAppID===appItem.appId && showDelete?'':'hide'" @tap="deleteAppItem(index)"></text></view><view class="app-li text-blue" @tap="addAppItem"><text class="appicon cuIcon-roundadd"></text></view></view></view><!-- appList end--><view class="btmBox" v-if="isEdti" @click="setMenuStor">完成</view><view class="btmBox" v-else @click="isEdti=!isEdti">编辑</view></view>
</template>
<script>import AppList from "@/components/healer-dragList/AppList.vue"export default {data() {return {isEdti: false,//这里写你自己页面路由信息appListData: [{appId: 0,appName: '示例菜单跳转页面',appIcon: '/static/img/category/invitation.png',appLink: "/pagesA/inviteAgents/inviteAgents"}],}},components: {AppList,},onLoad() {},onShow() {if (!uni.getStorageSync('MENU_DATA')) {// this.getUseInfoData()console.log('')} else {let data = uni.getStorageSync('MENU_DATA')this.appListData = JSON.parse(data)}if (!uni.getStorageSync('MENU_BTM_DATA')) {// this.getUseInfoData()console.log('')} else {let data = uni.getStorageSync('MENU_BTM_DATA')this.autherData = JSON.parse(data)}},methods: {goAuther(e) {if (e.appName == '更多') {return;}try {uni.navigateTo({url: e.appLink})} catch (err) {uni.showToast({title: '当前模块正在开发...',icon: 'none'})}},setMenuStor() {this.isEdti = falseuni.setStorageSync('MENU_DATA', JSON.stringify(this.appListData))uni.setStorageSync('MENU_BTM_DATA', JSON.stringify(this.autherData))},//菜单上到下lowAppListData(e) {if (this.appListData[e].appName == '更多') {uni.showToast({title: '更多不能被移出首页',icon: 'none'})return}this.autherData.push(this.appListData[e])this.appListData.splice(e, 1)},addIcon(index) {if (this.appListData.length == 10) {uni.showToast({title: '首页菜单不能大于10个',icon: 'none',duration: 2000})return;}this.appListData.push(this.autherData[index])this.autherData.splice(index, 1)},listChange(option) {console.log("listChange", option)}}}
</script><style scoped lang="scss">.rightIcon {position: absolute;right: 5rpx;top: -10rpx;}.btmBox {position: absolute;bottom: 0;width: 100%;height: 80rpx;line-height: 80rpx;background: #427ce7;text-align: center;color: white;}.appList {width: 100%;display: flex;flex-wrap: wrap;}.app-li {width: 20%;// height: 160rpx;text-align: center;display: flex;flex-direction: column;justify-content: space-around;position: relative;margin-bottom: 30rpx;.appIcon {font-size: 60rpx;width: 50%;margin: 0 auto;}.appName {font-size: 24rpx;}.cuIcon-roundadd {font-size: 60rpx;color: #CCCCCC;}.cuIcon-roundclosefill {position: absolute;top: 12rpx;right: 12rpx;font-size: 36rpx;z-index: 2;&.hide {display: none;}}}.topText {color: #427ce7;font-size: 30rpx;text-align: center;padding: 50rpx;}.content {background-color: #ffffff;}.title {padding: 30rpx;color: #808fb4;}
</style>

总结

以上代码实现了uniapp在小程序端实现菜单拖拽排序,以及显示隐藏指定菜单功能,有点小bug,需要原始代码的可以给我私信留言。当然有更简单的办法 uni-app切片工具也可以实现拖拽排序、菜单排序、导航排序等更多功能!