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

简介: movable-area 是 uniapp 的可移动区域组件。它用于定义可移动视图容器,在其内部可拖拽移动子视图。

效果展示

在这里插入图片描述

思路

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

目录
相关文章
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的汉服交易小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的汉服交易小程序的详细设计和实现(源码+lw+部署文档+讲解等)
20 7
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的宠物医院微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的宠物医院微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
28 7
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的美食推荐小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的美食推荐小程序的详细设计和实现(源码+lw+部署文档+讲解等)
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的蛋糕订购小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的蛋糕订购小程序的详细设计和实现(源码+lw+部署文档+讲解等)
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的仓库点单小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的仓库点单小程序的详细设计和实现(源码+lw+部署文档+讲解等)
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的“鼻护灵”微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的“鼻护灵”微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的“财来财往”微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的“财来财往”微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的自驾游拼团小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的自驾游拼团小程序的详细设计和实现(源码+lw+部署文档+讲解等)
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的传统戏曲推广微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的传统戏曲推广微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
|
14天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的博达驾校预约小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的博达驾校预约小程序的详细设计和实现(源码+lw+部署文档+讲解等)