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

2023-11-05

效果展示

在这里插入图片描述

思路

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

具体步骤

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

在这里插入图片描述

使用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 通常搭配使用,来实现可拖拽排序的列表效果。

在这里插入图片描述
//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, //触发删除的itemID
				showDelete: false, //删除按钮状态
				IsDeleteAfter: false, //是否为删除后
				IsCancelDelete: false, //是否为取消后
				moviewShow: false, //滑块状态
				areaBoxInfo: null, //保存滑动区域盒子dom信息
				inBoxXY: {}, //鼠标在item中的坐标
				touchIndex: 0, //被移动index
				touchItem: '', //备份被移动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' + idx
							this.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 = false
					this.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 = false
				uni.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切片工具也可以实现拖拽排序、菜单排序、导航排序等更多功能!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

小程序封装拖拽菜单组件(uniapp拖拽排序,自定义菜单) 的相关文章

随机推荐

  • 2022-12-27 使用lodash库实现两个非空对象的深拷贝并输出这两个对象的并集

    问题描述 遇到这样一个题 如下 const a fruits apple banana series apple C banana A B const b fruits banana orange animals pig series ba
  • ESP32S系列芯片通过EC20完成OTA统一流程

    uint32 t end size 0 最后一包的大小 uint16 t page number 0 总包数 uint16 t now number 0 当前写入页数 uint32 t bin addr 0 文件地址指针偏移量 uint16
  • DSMM数据安全概述

    数据安全生命周期分为采集 传输 存储 处理 交换 销毁几个阶段 其中数据处理阶段是整个周期的核心阶段 数据处理安全与否直接关系到整体数据安全 那么今天分享内容就是数据处理安全的相关要求和实现目标 DSMM是Data Security cap
  • URP学习--LitShader

    我们来看一下URP下的LitShader LitShader也是基于物理渲染的 很多方法和属性看过默认管线PBR代码的应该都会很熟悉 我们现在再过一遍 加深一下印象 同时疏通一下以前可能没有掌握的地方 先看Shader的Properties
  • mysql workbench 建库建表

    选择Create Schema建库 选择Create Table建表 改表名 上拉箭头选择更多 此处最好全屏 不然显示不全
  • UE5实现物体高亮描边效果(含UE相关源码浅析)

    文章目录 1 实现目标 2 实现过程 2 1 UE Editor中相关源码 2 2 深度值描边 2 3 半透明材质处理 2 4 遮挡处理 2 5 视口边缘处理 3 参考资料 1 实现目标 在UE5中实现物体边缘高亮效果 且在被遮挡时在边缘显
  • Android报Caused by: android.content.res.Resources$NotFoundException: String resource ID #0x0 .解决办法

    如果遇到这个问题 首先考虑以下原因 你是否在setText 方法中设置了一个int型变量 比如setText 10 这样Android系统就会主动去资源文件当中寻找 但是它不是一个资源文件ID 所以就会报出这个bug 解决办法 将int型业
  • 多维时序

    多维时序 MATLAB实现LSTM长短期记忆神经网络多变量时间序列预测 考虑历史特征的影响 多指标 多图输出 目录 多维时序 MATLAB实现LSTM长短期记忆神经网络多变量时间序列预测 考虑历史特征的影响 多指标 多图输出 效果一览 基本
  • linux - 文件利用率快满了 - mongo日志

    https zhuanlan zhihu com p 82430795 查看文件利用率 df h 文件夹大小 ls lh 逐级搜索大文件或目录 du sh cd 返回上一层 ps ef grep mongo 启动 mongodb bin m
  • 布尔值(python)

    while语句 格式 while 表达式 语句 逻辑 当程序执行到while语句时 首先计算表达式的值 如果表达式的值为假 那么结束整个while语句 如果为真 则先执行语句 再去计算表达式的值 如果表达式的值为假 那么结束整个while语
  • 从匿名函数到函数式编程

    什么是匿名函数 匿名函数是一种没有名称的函数 也被称为 lambda 函数或者函数字面量 和普通函数不同 匿名函数不需要在定义时指定函数名 通常是在需要的时候直接定义和使用 匿名函数通常被用作一次性的函数 例如作为其他函数的参数或者返回值
  • IDEA调试技巧

    在项目开发的时候很多小伙伴能非常熟练的使用IDEA编写代码 但是对于IDEA调试的技巧掌握的不是很好 只会F7 F8 F9等这些基本调试功能 而像一些复杂的调试场景就无从下手 如 在for循环中调试某个特定值 修改变量的运行值等 本文介绍了
  • Kubernetes 动态分配存储卷之 NFS-Subdir-External-Provisioner

    NFS Provisioner 已经停止维护 现在新的 NFS 动态分配器已经切换为 NFS Subdir External Provisioner 该组件也是一个自动配置卷程序 它使用现有的和已配置的 NFS 服务器来支持通过持久卷声明动
  • 二、C++语言初阶:类与对象

    2 类与对象 2 1 认识类与对象 什么是类 class 类 class 是类型 type 是用户自定义的类型 为什么不叫它type 因为借用Simula语言中的class关键字 为什么要有类 基于便利性的考虑 现实世界中物 object
  • 本地连腾讯云CentOS7主机Unable to connect to Redis异常

    问题描述 SpringBoot集成Redis操作 Unable to connect to Redis异常 1 yml中的spring redis的host和port配置无误 2 云主机的6379端口开放 3 redis conf配置文件b
  • 委托构造函数详解,小白也可以看懂

    委托构造函数 什么是委托构造函数 为什么要有委托构造函数 代码讲解 注意事项 参考链接 什么是委托构造函数 当我还不知道这个东西的时候 看到名字 顾名思义 我理解为委托其他构造函数帮忙构造 这和定义也是相近的 C 11 引入了委托构造的概念
  • Sublime Text 3 无法运行install package 的有效解决方法

    Sublime Text 3 无法运行install package 的有效解决方法 无法找到install package命令的解决方法 能搜到install package 但点击install package没反应的解决方法 无法找到
  • mysql中插入、更新数据时Duplicate entry '' for key 'PRIMARY'的解决方案

    今日小艾在修改数据时mysql报错1062 Duplicate entry for key mobile 一开始以为是字段类型错误 后面发现是该表字段设置了索引键 禁止改字段有重复的数据 要插入数据的主键数据 已经存在 不能再重复添加了 例
  • 浅谈JDBC及JDBC的基本使用

    目录 JDBC概述 数据持久化 Java中的数据存储技术 JDBC介绍 JDBC体系结构 JDBC程序编写步骤 获取数据库连接 Driver接口实现类 加载与注册JDBC驱动 url 几种常用数据库的 JDBC URL 用户名和密码 连接方
  • 小程序封装拖拽菜单组件(uniapp拖拽排序,自定义菜单)

    效果展示 思路 使用movable area作为可移动区域 并在内部循环渲染列表项view 绑定touch事件 在mounted生命周期函数内获取区域movable area的dom信息 记录列表项的坐标信息 在methods中定义了列表项