• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

mpvue小程序实现(购物车)左滑删除

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

实现的思路:走了很多弯路,参考了别人的实现方法。左滑删除,就是监听滑动的事件,小程序api中有提供到touchstart和touchmove的事件。当滑动超过一定的距离,触发事件。把删除按钮显示出来,反之。
其实难点是在以什么的方式实现删除按钮的显示和隐藏。源码在最下

(1)弯路:

一开始是想到用scroll-view的组件来实现的。交互效果能实现了,但是@scroll,监听滑动事件时,无法动态改变scroll-left,就无法实现滑动一定距离就显示隐藏按钮。
这里还踩了一个scroll-view的坑,就是设置了scroll-x,但是还无法左右滑动。
https://blog.csdn.net/sinat_26521835/article/details/80358280

平时我们对一组元素进行横向排列时,常用display:flex、float:left或行内元素性质的办法.但是,在小程序的这个组件里display:flex与float:left是行不通的,在这里,我们要使用display:inline-block利用行内元素性质来进行横向排列,并且设置为white-space:nowrap才有效果

(2)弯路:
放弃用scroll-view,直接用普通的div来实现。我就想到直接把删除键放到滑动内容的右侧



手指向左滑动一定距离,内容向左滑动隐藏一部分,然后删除按钮向左滑动显示。就想到用浮动或者display:flex来布局。效果是实现了,但是却很别扭。左滑的时候内容是向左隐藏了一部分,但是删除按钮在向左显示时,‘删除’2字也会动画的显示,很是奇怪。

最终,参考别人的布局方式,用position来实现。
就是滑动的时候,只是内容在滑动(动态设置content的right),跟删除按钮无关,

<template>
    <div
     class="slide"
     @touchstart="touchS"
     @touchmove="touchM"
    >
    <div class="contents_wrapper" :style="'right:' del'px'">
        <slot >
            <!-- <div class="contents">内容1</div> -->
        </slot>
    </div>
    <div class="delete" @click="delt">删除</div>
    </div>
</template>
<script>
export default {
    name:'slide',//左滑删除
    data() {
        return {
            clientX1:'',//滑动开始位置
            clientX2:'',
            del:0,//内容初始的right距离
            btnWidth:80//删除按钮宽度
        }
    },
    methods: {
        touchS(e){
            this.clientX1=e.clientX
        },
        touchM(e){
            this.clientX2=e.clientX
            let disX=this.clientX1-this.clientX2
            if (disX == 0 || disX < 0) {//如果移动距离小于等于0,说明向右滑动,文本层位置不变
                this.del = 0;
            }
            if (disX >= this.btnWidth) {
                //控制手指移动距离最大值为删除按钮的宽度
                this.del =this.btnWidth;
            }
            
        },
        delt(){
            console.log(8888)
        }
    },
}
</script>
<style scoped>
    .slide{
        width: 100%;
        position: relative;
        height: 120px;
    }
    .contents_wrapper{
        width: 100%;
        height: 100%;
        position: absolute;
        transition: right 0.3s ease-in-out;
        z-index: 99;
        background: #fff
    }
    .contents{
        width: 100%;
    }
    .delete{
        color:#fff;
        height: 100%;
        text-align: center;
        width:80px;
        background: red;
        line-height: 120px;
        position: absolute;
        right:0;
    }
</style>

效果已经实现了,但是到对接接口的时候,发现这个组件不能动态插入数据就是 mpvue中的slot不支持变量
坑死人了!!
然后就直接把组件的代码放到页面中,问题又来了,因为不是组件引入。每滑动一个slide,其他的slide都会相应的滑动。因为它们是根据del来改变right的。
于是为了能对分别对不同的slide进行处理,要给每个slide给一个标志位。
思路1:可以这条数据的index来进行标记,当slide 的index和data中的index相等时,添加toslide的样式

<template>
    <div
     class="slide"
     @touchstart="touchS"
     @touchmove="touchM(e,index)"
     v-for="(item,index) of valueData"
     :key="index"
    >
    <div :class=["contents_wrapper",{toslide:index==this.index]]>
 </div>
    <div class="delete" @click="delt">删除</div>
    </div>
</template>
<script>
export default {
    name:'slide',//左滑删除
    data() {
        return {
        	index:'',//初始状态的index
            clientX1:'',//滑动开始位置
            clientX2:'',
            del:0,//内容初始的right距离
            btnWidth:80//删除按钮宽度
        }
    },
    methods: {
        touchS(e){
            this.clientX1=e.clientX
        },
        touchM(e,index){
            this.clientX2=e.clientX
            let disX=this.clientX1-this.clientX2
            if (disX == 0 || disX < 0) {//如果移动距离小于等于0,说明向右滑动,文本层位置不变
                this.indx = '';
            }
            if (disX >= this.btnWidth) {
                //控制手指移动距离最大值为删除按钮的宽度
                this.index=index;
            }
            
        },
        delt(){
            console.log(8888)
        }
    },
}
</script>

<style scoped>
    .slide{
        width: 100%;
        position: relative;
        height: 120px;
    }
    .contents_wrapper{
        width: 100%;
        height: 100%;
        position: absolute;
        transition: right 0.3s ease-in-out;
        z-index: 99;
        background: #fff
    }
    .contents{
        width: 100%;
    }
    .delete{
        color:#fff;
        height: 100%;
        text-align: center;
        width:80px;
        background: red;
        line-height: 120px;
        position: absolute;
        right:0;
    }
    .toslide{
    left:-80px;
    }
</style>

思路2:直接为valueData中的每一条数据加上一个type的标志位,就是在数据获取之后再遍历数据,为每条数据加上一个type属性,然后css样式添加:.contents_wrapper[data-type="0"]{ right:0 } .contents_wrapper[data-type="1"]{ right:80px; }

<template>
    <div
     class="slide"
     @touchstart="touchS"
     @touchmove="touchM"
     v-for="(item,index) of valueData"
     :key="index"
    >
    <div class="contents_wrapper" :data-type="item.type">
        <slot >
            <!-- <div class="contents">内容1</div> -->
        </slot>
    </div>
    <div class="delete" @click="delt">删除</div>
    </div>
</template>
<script>
export default {
    name:'slide',//左滑删除
    data() {
        return {
            clientX1:'',//滑动开始位置
            clientX2:'',
            del:0,//内容初始的right距离
            btnWidth:80//删除按钮宽度
        }
    },
    methods: {
        touchS(e){
            this.clientX1=e.clientX
        },
        touchM(e){
            this.clientX2=e.clientX
            let disX=this.clientX1-this.clientX2
            if (disX == 0 || disX < 0) {//如果移动距离小于等于0,说明向右滑动,文本层位置不变
                this.del = 0;
            }
            if (disX >= this.btnWidth) {
                //控制手指移动距离最大值为删除按钮的宽度
                this.del =this.btnWidth;
            }
            
        },
        delt(){
            console.log(8888)
        },
        async getCartList(selectId){
	        wx.showLoading()
	      let userToken=wx.getStorageSync('userToken')
	      let res=await this.$request.getData('/goods/getShopCart',{user_token:userToken})
	     if(res.data.level=='success'){
	       res.data.data.map((item)=>{
	            item.type=0//1为右划状态
	            item.checked=0;//1为选项选中状态
	        })
	        wx.hideLoading()
	     }
	        wx.hideLoading()
     }
      this.valueData=res.data.data
  }
    },
}
</script>
<style scoped>
    .slide{
        width: 100%;
        position: relative;
        height: 120px;
    }
    .contents_wrapper{
        width: 100%;
        height: 100%;
        position: absolute;
        transition: right 0.3s ease-in-out;
        z-index: 99;
        background: #fff
    }
    .contents{
        width: 100%;
    }
    .delete{
        color:#fff;
        height: 100%;
        text-align: center;
        width:80px;
        background: red;
        line-height: 120px;
        position: absolute;
        right:0;
    }
    .contents_wrapper[data-type="0"]{
        right:0
    }
    .contents_wrapper[data-type="1"]{
        right:80px;
    }`
</style>

分割线-——————————————————————————————————————————————

同理,slide的选中状态也跟思路2的一样,为valueData添加标志位checked,来判断是否选中。引申,也可利用思路1中的方法当前的index等于data中的index时,为选中状态。但注意,这里是可多选的。那么,data中的index就是要为数组,当data中的index数据包含slide的index即为选中状态。

以下为项目源码:

html

<template>
  <div class="cart">
    <!-- 头部 -->
    <div class="header">
      <span>购物车</span>
      <span @click='editor'>
        <span v-if="editorFlag">编辑</span>
        <span v-else>完成</span>
      </span>
      <!-- <span @click='editorFinish'>完成</span> -->
    </div>
    <!-- 头部结束 -->

    <!-- 购物车内容 -->
     <div class="cart_list">
          <checkbox-group>
            <div class="slide" @touchstart="touchS" @touchmove="touchM($event,index)"  v-for="(item,index) of valueData" :key="index">
            
            <div class="contents_wrapper"  :data-type="item.type">
                <label class="cart_list_item" @click.stop="seletItem(index,item.id)">
                    <checkbox  :value="index" :checked="item.checked"/>
                    <img :src="item.goods_info.thumb_Url" class="cart_list_item_pic">
                    <div class="cart_list_item_right">
                        <div class="cart_list_item_name">{{item.goods_info.name}}</div>
                        <div class="cart_list_item_desc">{{item.goods_info.info}}</div>
                        <div class='count'>
                                <div>¥{{item.unit_price}}</div>
                            <div class="count_btn">
                                <div  :class="{decrease:true,decrease_least:item.num==flag}" @click.stop="decrease(item.goods_id,item.num)">-</div>
                                <input type="number" :value='item.num' readonly>
                                <div class="increase" @click.stop="increase(item.goods_id,item.num)">+</div>
                            </div>
                        </div>
                    </div>
                </label>
            </div>
            <div class="delete" @click="delt([item.id])">删除</div>
            </div>
        </checkbox-group>
    </div> 
    <!-- 购物车内容结束 -->
    <!-- 结算栏 -->
    <footer>
      <div class="footer_left">
        <checkbox @click="selectAll" :checked="checkedAll"/>
        <label>全选</label>
      </div>
      <div class="footer_right">
        <div class="footer_right_price" v-if="money!=''">合计:¥{{money}}</div>
        <div  :class="{footer_right_pay:true,hasItem:selectIdArr.length!=0}">
          <span v-if="editorFlag" @click="toOrder" >去结算</span>
          <span v-else @click="delt(selectIdArr)">删除</span>
          </div>
      </div>
    </footer>
    <!-- 结算栏结束 -->
  </div>
</template>

逻辑部分:

<script>

import count from '@/components/count'
export default {
  data() {
    return {
      editorFlag:true,//编辑为true,完成为false
      valueData:'',
      selectIdArr:[],//选中状态的id数组
      checkedAll:false,
      money:'',//总金额
      clientX1:'',//滑动开始位置
      clientX2:'',
      // del:0,//内容初始的right距离
      btnWidth:80,//删除按钮宽度
      flag:1,
      money:0//总金额
    }
  },
  watch:{
      valueData:{
          handler:function(nval){
              this.money=0
              for(let p of nval){
                  if(p.checked==1){
                    this.money+=Number(p.amount_money)
                  }   
              }
          },
          deep:true   
      }
  },
  components:{
    count
  },
  methods: {
    async decrease(id,num){  //计数器的减
            let userToken=wx.getStorageSync('userToken')
            if(num>1){
            let res=await this.$request.postData('/goods/addShopCart',{goods_id:id,'num':-1,user_token:userToken})
            if(res.data.level=='success'){
                this.getCartList()
                }
            }
        },
    async increase(id){//计数器的加
        let userToken=wx.getStorageSync('userToken')
        let res=await this.$request.postData('/goods/addShopCart',{goods_id:id,'num':1,user_token:userToken})
            if(res.data.level=='success'){
                this.getCartList()
                }
    },
    touchS(e){
        // console.log(e)
        this.clientX1=e.clientX
    },
    touchM(e,index){
        console.log(e)
        this.clientX2=e.clientX
        let disX=this.clientX1-this.clientX2
        if (disX == 0 || disX < 0) {//如果移动距离小于等于0,说明向右滑动,文本层位置不变
            // this.del = 0;
            this.$set(this.valueData[index],'type',0)
        }
        if (disX >= this.btnWidth) {
            //控制手指移动距离最大值为删除按钮的宽度
            // this.del =this.btnWidth;
            this.$set(this.valueData[index],'type',1)
        }
        
    },
    async delt(id){//删除选中的购物车商品

        console.log(id,11111)
        let userToken=wx.getStorageSync('userToken')
        let res=await this.$request.postData('/goods/deleteShopCart',{shop_cart_ids:id,user_token:userToken})
        if(res.data.level=="success"){
            this.getCartList()
        }
        
    },
    checkboxChange(e) {//获取checkobx选中的value数组
        // console.log('checkbox发生change事件,携带value值为:', e.mp.detail)
        this.slideIndex=e.mp.detail.value;

    },
    seletItem(index,id){//选中商品
        if(this.valueData[index].checked==0){
            this.$set(this.valueData[index],'checked',1)
            if(!this.selectIdArr.includes(id)){
                this.selectIdArr.push(id)
            }
            console.log(this.selectIdArr,77777)
        }else{
            this.$set(this.valueData[index],'checked',0);
            let arr=this.selectIdArr
            let arr2=[]
            arr.forEach((val)=>{
                if(val!=id){
                    arr2.push(val)
                }
            })
            this.selectIdArr=arr2

        }
        // console.log(this.money)
    },
    editor(){//显示编辑,完成
      let flag=this.editorFlag;
      this.editorFlag=!flag;
    },
    async getCartList(selectId){
        wx.showLoading()
      let userToken=wx.getStorageSync('userToken')
      let res=await this.$request.getData('/goods/getShopCart',{user_token:userToken})
     if(res.data.level=='success' && this.selectIdArr.length==0){
       res.data.data.map((item)=>{
            item.type=0//1为右划状态
            item.checked=0;//1为选项选中状态
        })
        wx.hideLoading()
     }else if(res.data.level=='success' && this.selectIdArr.length!=0){
         res.data.data.map((item)=>{
             item.type=0//1为右划状态
             if(this.selectIdArr.includes(item.id)){
                item.checked=1;//1为选项选中状态
             }else{
                 item.checked=0;//1为选项选中状态
             }  
        })
        wx.hideLoading()
     }
      this.valueData=res.data.data
  },
    async selectAll(){//全选
        let flag=this.checkedAll;
        let arr=this.valueData;
        this.selectIdArr
        if(flag==false){
          this.valueData.map((item)=>{
              item.type=0//1为右划状态
              item.checked=1;//1为选项选中状态
              this.selectIdArr.push(item.id)
          })
          
        }else{
          this.valueData.map((item)=>{
              item.type=0//1为右划状态
              item.checked=0;//1为选项选中状态
              this.selectIdArr=[]
          })
        }
        this.checkedAll=!flag
      },
    async toOrder(){
        wx.showLoading()
        let userToken=wx.getStorageSync('userToken')
        let res=await this.$request.postData('/order/buildOrder',{type:'shop_cart',goods_id:this.selectIdArr,user_token:userToken})
        if(res.data.level=="success"){
            wx.hideLoading()
            wx.navigateTo({url:`/pages/orderConfirm/main?orderId=${res.data.data.order_num}`})

        }  
    }
  },
  onShow(){
      this.getCartList()
    }
  
}
</script>

css样式:

<style>
/* 小程序checkbox的自定义样式,且不能用scoped */
 checkbox .wx-checkbox-input{
    border-radius: 50%;
    height: 20px;
    width: 20px;
    margin-top: -4px;
    border: 1px solid #eee;
}
checkbox .wx-checkbox-input.wx-checkbox-input-checked::before {
  height: 20px;
  width: 20px;
  line-height: 20px;
  text-align: center;
  font-size: 20px;
  padding: 5px;
  color: #fff;
  border-radius: 50%;
  background-color: red;
  border:1px solid #eee
}
</style>
<style scoped>
.cart{
  width:100%;
    min-height: 100%;
  overflow-x:hidden;
  background:#eee;
}
  .header{
    width:100%;
    height: 40px;
    background:#eee;
    display:flex;
    justify-content: space-between;
    align-items:center;
  }
  .header>span:nth-child(1){
    margin-left:10px;
  }
  .header>span:nth-child(2){
    margin-right:10px;
  }

  /* 商品列 */
  .cart_list{
    width:100%;
    padding-bottom:50px;
  }
  


  /* 结算栏 */
  footer{
    background:#fff;
    position:fixed;
    z-index:101;
    bottom:0;
    height:40px;
    width:100%;
    display:flex;
    align-items:center;
    justify-content:space-between;
  }
  .footer_left{
    margin-left:10px;
  }
  .footer_right{
    display:flex;
    align-items:center
  }
  .footer_right_price{
    color:pink;
    margin-right:10px;
  }
  .footer_right_pay{
    background:gray;
    text-align:center;
    color:#fff;
    height: 100%;
    line-height:40px;
    width:80px;
  }
  .hasItem{
      background:pink;
  }

  .slide{
        width: 100%;
        position: relative;
        height: 120px;
    }
    .contents_wrapper[data-type="0"]{
        right:0
    }
    .contents_wrapper[data-type="1"]{
        right:80px;
    }
    .contents_wrapper{
        width: 100%;
        height: 100%;
        position: absolute;
        transition: right 0.3s ease-in-out;
        z-index: 99;
        background: #fff
    }
    .contents{
        width: 100%;
    }
    .delete{
        color:#fff;
        height: 100%;
        text-align: center;
        width:80px;
        background: red;
        line-height: 120px;
        position: absolute;
        right:0;
    }
    .cart_list_item{
    width:100%;
    height: 100%;
    display:flex;
    flex-wrap:wrap;
    margin:0 0 0 10px;
    align-items:center;
  }
  .cart_list_item_pic{
    width:80px;
    height: 100px;
    margin-left:10px;
  }
  .cart_list_item_right{
    margin-left:10px;
    width:60%;
    height: 100%;
  }
  .cart_list_item_name{
    width:100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    margin:10px 0;
  }
  .cart_list_item_desc{
    margin-bottom:10px;
    width:100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }



  .count{
        width:100%;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }
    .count_btn{
        margin-right:40px;
        display: flex;
        width:100px;
        height: 25px;
        line-height: 25px;
        text-align: center;
    }
    .decrease,.increase{
        border: 1px solid #eee;
        width:30px;
        height: 100%;
    }
    .decrease{
        border-radius: 5px 0 0 5px;
    }
    .increase{
        border-radius: 0 5px 5px 0;
    }
    .count_btn input{
        flex:1;
        border-top:1px solid #eee;
        border-bottom:1px solid #eee;
        height:100%;
        min-height: 0 !important;
    }
    .decrease_least{
        background:#eee;
    }
</style>

最后还有一点注意的是,购物车的价钱计算,是由后端返回的,我一开始以为用数据的返回价格乘以数量就可以了。那么就涉及到valueData的重新获取,每次数量的加减或者删除都要重新发请求获取数据。这样会引发一个问题:就是重新获取valueData之后,我的checked和type标志位都是重新添加上去的。这样用户在选中多个slide时,只删除其中一项。数据刷新了,其他的选中状态就没有了,这样体验很不友好
改进方法:在每次的slide选中时,data中的selectIdArr相应添加一个slide的id,当重新获取valuedata的时候,如果selectIdArr中有相应id就为选中状态即可。

还有最最后一点是,一开始的时候我把计数器写成一个组件,虽然mpvue不支持slot中插入变量,但还是支持父组件向子组件提供数据props的。但奇葩的是当我触发touchmove时间时候,为valueData添加、删除属性type时,计数器的数字会恢复到最初valueData的数据

我以为是动态改变了valueData的数据导致了valueData中的num,所以才会使它的数据恢复。但是却不是,我换了一个input框,输入内容后,选中slide后,input框内容会恢复。找了很久也找不到什么原因。不用组件,直接把count组件写在页面内,这就成了。

后续:在真机上测试,点击到checkbox时,会闪一下,然后再选中,原因是我用label标签,并且checkbox的选中状态时根据data的item.checked来判断的。点击label时,先出发原生的checkbox的选中事件。于是不用label,并且为checkbox一个容器,并且给个layer避免直接触发到checkbox的事件。

<div class="checked_wrapper">
    <checkbox  :value="index" :checked="item.checked"/>
       <div class="checked_layer"></div>
   </div>


.cart_list{
    width:100%;
    padding-bottom:50px;
  }
  
  .checked_wrapper{
      position: relative;
      width:40px;
      height: 100%;
  }
  .checked_wrapper>checkbox{
      position:absolute;
      top:50%;
      left:50%;
      transform:translate(-50%)
  }

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
菜谱小程序_云应用程序食谱发布时间:2022-07-18
下一篇:
微信小程序仿‘得到app’分类列表页发布时间:2022-07-18
热门推荐
    热门话题
    阅读排行榜

    扫描微信二维码

    查看手机版网站

    随时了解更新最新资讯

    139-2527-9053

    在线客服(服务时间 9:00~18:00)

    在线QQ客服
    地址:深圳市南山区西丽大学城创智工业园
    电邮:jeky_zhao#qq.com
    移动电话:139-2527-9053

    Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap