今天遇到了一个问题。实现类似淘宝的购物车全选/全不选效果。记录下这里面的问题:
这里面有这样几成逻辑:(我们把图中的京东,网易,天猫等看成一个模块)
1每一个单独模块有一个控制该模块是否全选的按钮,点击全选/全不选切换,该模块下的所有复选框要被勾选/全不勾选
同时每一个模块的全选/全不选按钮也会影响到整个页面最底部的全选按钮状态,当每个模块都会全选状态时,整个页面底部的全选按钮将被勾选
2每一个单独模块下的复选框被逐一勾选时,它会影响到两层元素,其一是它所在的模块。当全部被勾选时,该复选框所在模块的全选按钮将被勾选,其二,所有复选框被逐一勾选时,整个页面底部的全选按钮将被勾选
所以对于每一个复选框按钮,它将都会影响到两层元素。
我们先看一看大致结构:
注意:单个模块复选框我们利用van-check-group进行包裹。再看一看数据结构
每一个模块对应一条json,其中有两个非常重要的属性:isChecked,它用来控制当前模块的选中状态,另一个就是result:它用来记录单个模块中有多少个复选框被勾选了。我们在van-checkbox-group中通过v-model进行了绑定。
代码如下:
这里有2个地方需要解释:
1 result用途到底是啥?
van-checkbox-group的change事件会有一个v-model绑定的值。它的值是一个数组,记录了当前哪些复选框被选中了。我们不用官方写法。把result写在data里。因为所有的模块是循环出来,每一个模块我们都需要知道该模块下到底选中了哪些复选框,所以我们把result写在了arr数组的每一个json里。
2 如下代码到底是啥意思?
item.list.forEach(j=>{
item.result.push(j.title)
})
我们可以这么理解。它就是全选。因为对每一个模块来说,勾选一个,当前模块的result数组就添加了一个。反过来,当当前模块的result都被添满。也就是result的长度和当前模块(arr下的json下list)长度一致的时候。就表明全选了
这个效果已经实现了。但是它需要有两个依赖条件:
1 数据结构需要用这种格式
2 我们对van-checkbox-group需要通过v-model绑定result,而不是其他
知道了基本的套路。我们接下来需要完成类似淘宝的购物车的完整功能。截图如下:
新的功能点包括如下几个:
1当只勾选一个模块内的商品,比如只勾选了京东里的商品。直接跳转至结算页面,如果至少勾选了2个模块,比如同时勾选了京东和天猫的商品,则有一个弹出层,提示单独结算。
2底部结算和合计的数字需要实时统计。实时计算(注意:结算按钮的数字不是统计所有勾选项,而是统计品类,比如本例中网易,天猫,京东等模块。如果有京东商品被勾选,则结算显示的数字是1.如果京东,天猫两个类目商品被勾选则结算按钮的值是2,依次类推)
3删除按钮点击时需要知道当前被勾选的那个品类的那个商品
针对第一个功能。先理清下思路:
(1)弹出层出现的条件就是至少勾选2个大类模块。那如何知道至少勾选了至少2个大类呢?
每次勾选商品。都会被van-checkbox-group里change事件所监听。我们知道,每个大类模块(比如京东)映射的json下都有一个
result:[],表示当前模块下哪个商品被勾选。也就是说只要有大类模块下有商品被勾选,其对应的json下的result数组的长度是不为空的。所有我们其实可以遍历整个大的数组,再去判断每一个json下的result的值是否有值。同时定义一个局部变量num,在遍历的过程中,只要当前json下的result数组有值,就累加。这样如果京东,网易和天猫这三个模块里都有商品被勾选,那么说明这三个json里的result的值都是不为空的。那累加的num就是3,那这个num就和结算的值是吻合的。
而合计的值又怎么计算呢?合计的值其实就是每一个商品的数量乘其单价,最后把所有勾选项的这些值累加。
同样的道理:我们需要一个条件。我们需要知道哪些商品被勾选了,如果能提供一个所有被勾选商品的集合,我们在他里面做文章,这个事情就好办了。
而恰好。result为我们提供了这个桥梁。所有道理是一样的。每次商品被勾选,每个大类json下的result就会记录当前被勾选的数据。如果我们循环整个大的数组,再遍历每一个json的下result,最后把他们push到一个数组里。那这个数组不就记录了所有被勾选的对象。但是有一点,此时我们需要做一点改变。van-checkbox-group下的van-checkbox的name不再和某一个属性,比如id等进行双向绑定,而是直接绑定item。这样result数组里出现的就是所有被勾选的item,每一个item记录了所有的数据,当然就有数量和价格。那总价就出来了。
而如何在勾选的时候合计的值及时更新呢?因为每次勾选都会被van-checkbox-group的change实践监听到。所以只需要在这个事件中再执行下我们上面的逻辑即可?如何在点击+、-按钮时同步更新合计值呢?道理一样。
代码如下:
完整代码如下:
<template>
<div class='shoppingCartpage'>
<div class='header'>
<NavBar title='购物车' backgroundColor='var(--themeBgColor)' zitiColor='#fff'>
<div slot='right' @click='clickNavBarTxt'>{{navBarTxt}}</div>
</NavBar>
</div>
<div class='main'>
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
:immediate-check='false'
:error.sync="error"
error-text="请求失败,点击重新加载"
@load="onLoad"
>
<div class='item p24 boxSize mt24 br20' v-for='(item,i) in arr' :key='i'>
<div class='title'>
<div class='classify'>
<van-checkbox v-model="item.isChecked" checked-color='var(--themeBgColor)'
@click='changeGroupAll(item)'>{{item.title}}
</van-checkbox>
</div>
<div class='freeInfo'>
<!-- <slot name='tipInfo'> -->
<span class='fs26 fw500 mr10'>满99包邮</span>
<van-icon name="warning-o" class='mr34'/>
<span class='theme'>去凑单</span>
<i class='icon icon-jinru fs20 theme'></i>
<!-- </slot> -->
</div>
</div>
<section class='mt50'>
<van-checkbox-group v-model="item.result" @change='changeGroup(item)' >
<div v-for='(innerItem,j) in item.list' :key='j'>
<van-row class='mt34'>
<van-col span='3'>
<van-checkbox :name="innerItem" checked-color='var(--themeBgColor)' ></van-checkbox>
</van-col>
<van-col span='9'>
<img :src="innerItem.defaultImage" alt="" class='thumbnail'>
</van-col>
<van-col span='12'>
<p class='ellipsisTwo'>{{innerItem.goodsName}}</p>
<van-row class='mt50'>
<van-col span='10'>
<div class='numBox '><span class='fs26 theme num'>{{innerItem.goodsPrice}}</span><span class='fs22 gray'>积分</span></div>
</van-col>
<van-col span='14' >
<div class='btnOuter'>
<div class='btnInner'>
<span class='fs28 btn w dis texCenter' @click="decrease(i,j,innerItem)">-</span><input type="text" v-model='innerItem.num' class='fs28 input w texCenter'/><span class='fs28 btn w dis texCenter' @click='increase(i,j,innerItem)'>+</span>
</div>
</div>
</van-col>
</van-row>
</van-col>
</van-row>
</div>
</van-checkbox-group>
</section>
</div>
</van-list>
</div>
<section class='footer w boxSize'>
<div class='checkAll'>
<van-checkbox v-model="isCheckAll" checked-color='var(--themeBgColor)' class='h' @click='checkTotal'><div class='fs26'>全选</div></van-checkbox>
</div>
<div class='accountBox'>
<div slot='tipInfo' class='tipBox'>
<van-row v-if='navBarTxtStatus' >
<van-col span='12'><div class='btn move'>移入关注</div></van-col>
<van-col span='12'><div class='btn del' @click='del'>删除</div></van-col>
</van-row>
<van-row v-else>
<van-col span='4'><div class=' num'>合计:</div></van-col>
<van-col span='8'><div class='theme num'><span class='fsBold point fs32'>{{accountTotal}}</span><span class='fs24 '>积分</span></div></van-col>
<van-col span='12'> <div class='account texCenter fs30 fw500 white center' @click='account'>结算<span>({{accoutNum}})</span></div></van-col>
</van-row>
</div>
</div>
</section>
<van-popup
v-model="showPup"
position="bottom"
round
closeable
>
<section class='pupTitle'>
<p class='tit fs36 fsBold'>请分别结算</p>
<p class='fs28'>京东与其他商品处理规则不同,请分别结算</p>
</section>
<div class='p24 boxSize pupCont'>
<section>
<van-row class='mt24'>
<van-col span='18'>
<p>京东商品</p>
<div><span>共2件</span>合计:<span class='theme ml16'>486.00积分</span></div>
</van-col>
<van-col span='6'>
<van-button plain type="info" hairline size='small' color='var(--themeBgColor)'>去结算</van-button>
</van-col>
</van-row>
<van-row class='mt24'>
<van-col span='18'>
<p>京东商品</p>
<div><span>共2件</span>合计:<span class='theme ml16'>486.00积分</span></div>
</van-col>
<van-col span='6'>
<van-button plain type="info" hairline size='small' color='var(--themeBgColor)'>去结算</van-button>
</van-col>
</van-row>
<div class='center h100 fs34 fsBold p24 boxSize'>取消</div>
</section>
</div>
</van-popup>
</div>
</template>
<script>
import NavBar from "@/components/NavBar"
import CartItem from "./cartItem"
import shoppingCartApi from "@/apis/shoppingCartApi"
import { Dialog } from 'vant';
export default {
name:'shoppingCart',
created(){
//查询购物车
// this.findGoodsInCart()
},
data(){
return{
showPup:false,
arr:[
{
code:"jd",
title:"京东",
isChecked:false,
result:[],
list:[
{
id: 'jd001',defaultImage: "https://imgs.woliwu.com/goods/images/9000018176_default.jpg",
goodsCode: "JD8655www9",goodsName: "京东商品01",goodsPrice: 1,num: 1,stockStatus: "有货",supplierCode: "jd"
},
{
id: 'jd002',defaultImage: "https://imgs.woliwu.com/goods/images/900003072301_default.jpg",goodsCode: "JD8655123048",
goodsName: "京东商品02",goodsPrice: 2,num: 2,stockStatus: null,supplierCode: "jd"
}
]
},
{
code:"wy",
title:"网易",
isChecked:false,
result:[],
list:[
{ id: 'wy001',defaultImage: "https://imgs.woliwu.com/goods/images/900003072301_default.jpg",
goodsCode: "JD8655",goodsPrice: 1,num: 3,stockStatus: null,supplierCode: "wy"
},
{
id: 'wy002',defaultImage: "https://imgs.woliwu.com/goods/images/900003072301_default.jpg",
goodsCode: "JD865509890",
goodsPrice: 2,
num: 3,
stockStatus: null,
supplierCode: "wy"
}
]
},
{
code:"tm",
title:"天猫",
isChecked:false,
result:[],
list:[
{ id: 'tm001',defaultImage: "https://imgs.woliwu.com/goods/images/900003072301_default.jpg",
goodsCode: "JD8655",goodsPrice: 1,num: 3,stockStatus: null,supplierCode: "wy"
},
{
id: 'tm002',defaultImage: "https://imgs.woliwu.com/goods/images/900003072301_default.jpg",
goodsCode: "JD865509890",
goodsPrice: 2,
num: 1,
stockStatus: null,
supplierCode: "tm"
},
{ id: 'tm003',defaultImage: "https://imgs.woliwu.com/goods/images/900003072301_default.jpg",
goodsCode: "JD8655",goodsPrice: 2,num: 1,stockStatus: null,supplierCode: "tm"
},
{
id: 'tm004',defaultImage: "https://imgs.woliwu.com/goods/images/900003072301_default.jpg",
goodsCode: "JD865509890",
goodsPrice: 2,
num: 2,
stockStatus: null,
supplierCode: "tm"
}
]
},
],
isCheckAll:false,
navBarTxt:'编辑',
navBarTxtStatus:false,
loading:false,
finished:false,
error:false,
idArr:[],
accoutNum:0,
accountTotal:0,
groupNum:[]
}
},
components:{NavBar,CartItem},
methods:{
getCartsData(){
},
clickNavBarTxt(){
this.navBarTxtStatus=!this.navBarTxtStatus
this.navBarTxtStatus?this.navBarTxt='完成':this.navBarTxt='编辑'
},
increase(i,j,item){
this.arr[i]['list'][j].num++;
this.accutateNum()
},
decrease(i,j,item){
this.arr[i]['list'][j].num--;
if(this.arr[i]['list'][j].num<1){
this.arr[i]['list'][j].num=1
}
this.accutateNum()
},
del(){
let self=this
var res=false
//判断是否有勾选项
res= this.arr.some(item=>{
return item.result.length>0
})
if(res){
this.arr.forEach(item=>{
item.result.forEach(j=>{
this.idArr.push(j)
})
})
Dialog.confirm({
message: '确认将此商品删除'
}).then(() => {
self.deleteGoodsToCart()
}).catch(() => {
})
}else{
this.$toast('您尚未勾选商品')
}
},
changeGroupAll(item){
item.isChecked=!item.isChecked
item.result=[]
//判断当前模块是否全选
if(item.isChecked){
item.list.forEach(j=>{
// item.result.push(j.id)
item.result.push(j)
})
}else{
item.result=[]
}
//判断是否整体全选
let val=this.arr.every(i=>i.isChecked)
if(val){
this.isCheckAll=true
}else{
this.isCheckAll=false
}
},
changeGroup(item){
//判断单个模块是否全选
console.log('item.result:',item.result)
if(item.result.length==item.list.length){
item.isChecked=true
}else{
item.isChecked=false
}
//获取结算值
this.accutateNum()
//判断整体是否全选
let val=this.arr.every(item=>item.isChecked)
if(val){
this.isCheckAll=true
}else{
this.isCheckAll=false
}
},
//整体全选
checkTotal(){
this.isCheckAll=!this.isCheckAll
if(this.isCheckAll){
this.arr.forEach(item=>{
item.result=[]
item.isChecked=this.isCheckAll
item.list.forEach(j=>{
// item.result.push(j.id)
item.result.push(j)
})
})
}else{
this.arr.forEach(item=>{
item.isChecked=this.isCheckAll
item.list.forEach(j=>{
item.result=[]
})
})
}
},
//查询购物车
async findGoodsInCart (){
try{
let res=await shoppingCartApi.findGoodsInCart()
let list=res.data&&res.data.data||[]
let tempArr=list.map(item=>{
return {...item,result:[],isChecked:false}
})
this.arr=tempArr
}catch(err){
throw new Error(err)
}
},
async deleteGoodsToCart(){
console.log('this.idArr:',this.idArr)
let ids=[]
for(i of this.idArr){
ids.push(i.id)
}
console.log('ids:',ids)
try{
let delRes=await shoppingCartApi.deleteGoodsToCart({ids})
if(delRes.data.code==0){
self.findGoodsInCart()
}
}catch(err){
throw new Error(err)
}
},
//结算
account(){
let allResLen=[]
this.arr.forEach(item=>{
allResLen.push(item.result.length)
})
let val=allResLen.reduce((total, currentValue, currentIndex, arr)=>{
return total+currentValue
})
console.log('val:',val)
if(val==0){//全部未勾选
this.$toast('您尚未勾选商品')
return
}else{//有勾选项
let temp=allResLen.filter(item=>item==0)
console.log('this.allResultLenArr:',allResLen)
console.log('temp:',temp)
console.log('allResLen.length-1:',allResLen.length-1)
//只勾选了单个模块
if(temp.length==allResLen.length-1){
this.$router.push('/submitOrder')
}else{
//勾选了多个模块
this.showPup=true
//统计哪些模块有勾选项
let num=0
this.arr.forEach(item=>{
if(item.result.length>0){
num+=1
}
})
this.accoutNum=num
// console.log('this.accoutNum:',this.accoutNum)
}
}
},
//统计结算数字和总积分数
accutateNum(){
let num=0
let groupNum=[]
let total=0
this.arr.forEach(item=>{
if(item.result.length>0){
num+=1
item.result.forEach(j=>{
groupNum.push(j)
})
}
})
console.log('groupNum:',groupNum)
//计算总数
groupNum.forEach(item=>{
total+=item.num*item.goodsPrice
})
console.log('taotal:',total)
this.accoutNum=num
this.accountTotal=total
},
//上拉刷新
onLoad(){
// this.loading=true
// this.findGoodsInCart()
}
}
}
</script>
<style lang="less" scoped>
.shoppingCartpage{box-shadow:0px 2px 6px 0px rgba(51,51,51,0.04);border-radius:20px;background:#F6F6F7FF;min-height: 100vh}
.header{width: 100%; height: 170px;position: relative;overflow:hidden;padding: 0;margin: 0;}
.header::after{width: 140%;height: 170px;position: absolute;top:0;left:-20%;content: "";
;display: block;border-radius: 0 0 50% 50%;background:var(--themeBgColor);}
.main{margin:-100px auto 0 auto;width: 702px;background: transparent;padding-bottom:273px;position: relative;z-index:1}
.footer{width: 100%; height: 101px;position: fixed;bottom:97px;left: 0;z-index: 2;padding: 10px 24px}
.checkAll{float: left;width: 40%;height: 100%}
.accountBox{float: left;width: 60%;display: flex;justify-content: flex-end}
.account {width:190px;height:80px;background:var(--themeBgColor);border-radius:40px;}
.h{height: 70px;}
.tipBox{width: 100%;}
.white{color:#fff}
.num{height: 101px;line-height: 80px}
.btn{width: 190px; height: 60px;line-height:60px;border-radius:40px;text-align: center}
.move{background: var(--themeBgColor);color:#fff}
.del{border:1px solid var(--themeBgColor);background: #fff;color:var(--themeBgColor)}
.point{color:var(--themeBgColor);margin-right:5px}
.gray{color:#999999FF}
.main{
.item{background:#fff;}
.title{display: flex; height: 40px;line-height: 40px}
.classify{flex:0 0 220px}
.freeInfo{flex:1 0 auto;display:flex;justify-content: flex-end}
.mt51{margin-top:51px}
.thumbnail{width: 185px; height: 185px;display: block}
.rd{width: 40px; height: 40px;border:1px solid #999999FF;background: #fff;position: relative;cursor: pointer;}
.rd::after{display: block;width: 40px; height: 40px;position: absolute;top:0;left: 0;background: #fff;visibility: visible;}
.btn{width: 40px; height: 40px;}
.input{width:80px; height: 37px;border-right:1px solid #CCCCCCFF;;border-left:1px solid #CCCCCCFF;}
.mt50{margin-top:50px}
.dis{display: inline-block}
.btnOuter{display: flex;justify-content: flex-end}
.btnInner{width: 168px;border:1px solid #CCCCCCFF;}
.num{margin-right:6px}
}
.pupTitle{ height: 180px;border-bottom: 1px solid #eee;
.tit{height: 98px;line-height:98px}
.mt10{margin-top:10px}
}
.pupTitle p{text-align: center}
.texColor{color:#7B7B7BFF}
.h100{ height: 100px;}
.pupCont p{font-size:24px;color:#999999FF }
.pupCont div{font-size:26px;color:#333333FF;font-weight: 500%}
.ml16{margin-left:16px;}
.pupCont span{display: inline-block}
.theme{color:var(--themeBgColor)}
</style>