实现的思路:走了很多弯路,参考了别人的实现方法。左滑删除,就是监听滑动的事件,小程序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%)
}
|
请发表评论