效果展示
思路
- 使用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切片工具也可以实现拖拽排序、菜单排序、导航排序等更多功能!