01. 购物车 - 静态布局
基本结构
< template>
< div class = " cart" >
< van-nav-bar title = " 购物车" fixed />
< div class = " cart-title" >
< span class = " all" > 共< i> 4</ i> 件商品</ span>
< span class = " edit" >
< van-icon name = " edit" />
编辑
</ span>
</ div>
< div class = " cart-list" >
< div class = " cart-item" v-for = " item in 10" :key = " item" >
< van-checkbox> </ van-checkbox>
< div class = " show" >
< img src = " http://cba.itlike.com/public/uploads/10001/20230321/a072ef0eef1648a5c4eae81fad1b7583.jpg" alt = " " >
</ div>
< div class = " info" >
< span class = " tit text-ellipsis-2" > 新Pad 14英寸 12+128 远峰蓝 M6平板电脑 智能安卓娱乐十核游戏学习二合一 低蓝光护眼超清4K全面三星屏5GWIFI全网通 蓝魔快本平板</ span>
< span class = " bottom" >
< div class = " price" > ¥ < span> 1247.04</ span> </ div>
< div class = " count-box" >
< button class = " minus" > -</ button>
< input class = " inp" :value = " 4" type = " text" readonly >
< button class = " add" > +</ button>
</ div>
</ span>
</ div>
</ div>
</ div>
< div class = " footer-fixed" >
< div class = " all-check" >
< van-checkbox icon-size = " 18" > </ van-checkbox>
全选
</ div>
< div class = " all-total" >
< div class = " price" >
< span> 合计:</ span>
< span> ¥ < i class = " totalPrice" > 99.99</ i> </ span>
</ div>
< div v-if = " true" class = " goPay" > 结算(5)</ div>
< div v-else class = " delete" > 删除</ div>
</ div>
</ div>
</ div>
</ template>
< script>
export default {
name : 'CartPage'
}
</ script>
< style lang = " less" scoped >
// 主题 padding
.cart {
padding-top : 46px;
padding-bottom : 100px;
background-color : #f5f5f5;
min-height : 100vh;
.cart-title {
height : 40px;
display : flex;
justify-content : space-between;
align-items : center;
padding : 0 10px;
font-size : 14px;
.all {
i {
font-style : normal;
margin : 0 2px;
color : #fa2209;
font-size : 16px;
}
}
.edit {
.van-icon {
font-size : 18px;
}
}
}
.cart-item {
margin : 0 10px 10px 10px;
padding : 10px;
display : flex;
justify-content : space-between;
background-color : #ffffff;
border-radius : 5px;
.show img {
width : 100px;
height : 100px;
}
.info {
width : 210px;
padding : 10px 5px;
font-size : 14px;
display : flex;
flex-direction : column;
justify-content : space-between;
.bottom {
display : flex;
justify-content : space-between;
.price {
display : flex;
align-items : flex-end;
color : #fa2209;
font-size : 12px;
span {
font-size : 16px;
}
}
.count-box {
display : flex;
width : 110px;
.add,
.minus {
width : 30px;
height : 30px;
outline : none;
border : none;
}
.inp {
width : 40px;
height : 30px;
outline : none;
border : none;
background-color : #efefef;
text-align : center;
margin : 0 5px;
}
}
}
}
}
}
.footer-fixed {
position : fixed;
left : 0;
bottom : 50px;
height : 50px;
width : 100%;
border-bottom : 1px solid #ccc;
background-color : #fff;
display : flex;
justify-content : space-between;
align-items : center;
padding : 0 10px;
.all-check {
display : flex;
align-items : center;
.van-checkbox {
margin-right : 5px;
}
}
.all-total {
display : flex;
line-height : 36px;
.price {
font-size : 14px;
margin-right : 10px;
.totalPrice {
color : #fa2209;
font-size : 18px;
font-style : normal;
}
}
.goPay, .delete {
min-width : 100px;
height : 36px;
line-height : 36px;
text-align : center;
background-color : #fa2f21;
color : #fff;
border-radius : 18px;
&.disabled {
background-color : #ff9779;
}
}
}
}
</ style>
按需导入组件
import { Checkbox } from 'vant'
Vue. use ( Checkbox)
02. 购物车 - 构建 vuex 模块 - 获取数据存储
新建 modules/cart.js
模块
export default {
namespaced : true ,
state ( ) {
return {
cartList : [ ]
}
} ,
mutations : {
} ,
actions : {
} ,
getters : {
}
}
挂载到 store 上面
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import cart from './modules/cart'
Vue. use ( Vuex)
export default new Vuex. Store ( {
getters : {
token : state => state. user. userInfo. token
} ,
modules : {
user,
cart
}
} )
封装 API 接口 api/cart.js
export const getCartList = ( ) => {
return request. get ( '/cart/list' )
}
封装 action 和 mutation
mutations : {
setCartList ( state, newList ) {
state. cartList = newList
} ,
} ,
actions : {
async getCartAction ( context ) {
const { data } = await getCartList ( )
data. list. forEach ( item => {
item. isChecked = true
} )
context. commit ( 'setCartList' , data. list)
}
} ,
页面中 dispatch 调用
computed : {
isLogin ( ) {
return this . $store. getters. token
}
} ,
created ( ) {
if ( this . isLogin) {
this . $store. dispatch ( 'cart/getCartAction' )
}
} ,
03. 购物车 - mapState - 渲染购物车列表
将数据映射到页面
import { mapState } from 'vuex'
computed : {
... mapState ( 'cart' , [ 'cartList' ] )
}
动态渲染
< div class = " cart-list" >
< div class = " cart-item" v-for = " item in cartList" :key = " item.goods_id" >
< van-checkbox icon-size = " 18" :value = " item.isChecked" > </ van-checkbox>
< div class = " show" @click = " $router.push(`/prodetail/${item.goods_id}`)" >
< img :src = " item.goods.goods_image" alt = " " >
</ div>
< div class = " info" >
< span class = " tit text-ellipsis-2" > {{ item.goods.goods_name }}</ span>
< span class = " bottom" >
< div class = " price" > ¥ < span> {{ item.goods.goods_price_min }}</ span> </ div>
< CountBox :value = " item.goods_num" > </ CountBox>
</ span>
</ div>
</ div>
</ div>
04. 购物车 - 封装 getters - 动态计算展示
封装 getters:商品总数 / 选中的商品列表 / 选中的商品总数 / 选中的商品总价
getters : {
cartTotal ( state ) {
return state. cartList. reduce ( ( sum, item, index ) => sum + item. goods_num, 0 )
} ,
selCartList ( state ) {
return state. cartList. filter ( item => item. isChecked)
} ,
selCount ( state, getters ) {
return getters. selCartList. reduce ( ( sum, item, index ) => sum + item. goods_num, 0 )
} ,
selPrice ( state, getters ) {
return getters. selCartList. reduce ( ( sum, item, index ) => {
return sum + item. goods_num * item. goods. goods_price_min
} , 0 ) . toFixed ( 2 )
}
}
页面中 mapGetters 映射使用
computed: {
...mapGetters('cart', ['cartTotal', 'selCount', 'selPrice']),
},
< div class = " cart-title" >
< span class = " all" > 共< i> {{ cartTotal || 0 }}</ i> 件商品</ span>
< span class = " edit" >
< van-icon name = " edit" />
编辑
</ span>
</ div>
< div class = " footer-fixed" >
< div class = " all-check" >
< van-checkbox icon-size = " 18" > </ van-checkbox>
全选
</ div>
< div class = " all-total" >
< div class = " price" >
< span> 合计:</ span>
< span> ¥ < i class = " totalPrice" > {{ selPrice }}</ i> </ span>
</ div>
< div v-if = " true" :class = " { disabled: selCount === 0 }" class = " goPay" >
结算({{ selCount }})
</ div>
< div v-else :class = " { disabled: selCount === 0 }" class = " delete" >
删除({{ selCount }})
</ div>
</ div>
</ div>
05. 购物车 - 全选反选功能
全选 getters
getters : {
isAllChecked ( state ) {
return state. cartList. every ( item => item. isChecked)
}
}
... mapGetters ( 'cart' , [ 'isAllChecked' ] ) ,
< div class = "all-check" >
< van- checkbox : value= "isAllChecked" icon- size= "18" > < / van- checkbox>
全选
< / div>
点击小选,修改状态
< van- checkbox @click= "toggleCheck(item.goods_id)" ... > < / van- checkbox>
toggleCheck ( goodsId ) {
this . $store. commit ( 'cart/toggleCheck' , goodsId)
} ,
mutations : {
toggleCheck ( state, goodsId ) {
const goods = state. cartList. find ( item => item. goods_id === goodsId)
goods. isChecked = ! goods. isChecked
} ,
}
点击全选,重置状态
< div @click= "toggleAllCheck" class = "all-check" >
< van- checkbox : value= "isAllChecked" icon- size= "18" > < / van- checkbox>
全选
< / div>
toggleAllCheck ( ) {
this . $store. commit ( 'cart/toggleAllCheck' , ! this . isAllChecked)
} ,
mutations : {
toggleAllCheck ( state, flag ) {
state. cartList. forEach ( item => {
item. isChecked = flag
} )
} ,
}
06. 购物车 - 数字框修改数量
封装 api 接口
export const changeCount = ( goodsId, goodsNum, goodsSkuId ) => {
return request. post ( '/cart/update' , {
goodsId,
goodsNum,
goodsSkuId
} )
}
页面中注册点击事件,传递数据
< CountBox : value= "item.goods_num" @input= "value => changeCount(value, item.goods_id, item.goods_sku_id)" > < / CountBox>
changeCount ( value, goodsId, skuId ) {
this . $store. dispatch ( 'cart/changeCountAction' , {
value,
goodsId,
skuId
} )
} ,
提供 action 发送请求, commit mutation
mutations : {
changeCount ( state, { goodsId, value } ) {
const obj = state. cartList. find ( item => item. goods_id === goodsId)
obj. goods_num = value
}
} ,
actions : {
async changeCountAction ( context, obj ) {
const { goodsId, value, skuId } = obj
context. commit ( 'changeCount' , {
goodsId,
value
} )
await changeCount ( goodsId, value, skuId)
} ,
}
07. 购物车 - 编辑切换状态
data 提供数据, 定义是否在编辑删除的状态
data () {
return {
isEdit: false
}
},
注册点击事件,修改状态
< span class = " edit" @click = " isEdit = !isEdit" >
< van-icon name = " edit" />
编辑
</ span>
底下按钮根据状态变化
< div v-if = " !isEdit" :class = " { disabled: selCount === 0 }" class = " goPay" >
去结算({{ selCount }})
</ div>
< div v-else :class = " { disabled: selCount === 0 }" class = " delete" > 删除</ div>
监视编辑状态,动态控制复选框状态
watch : {
isEdit ( value ) {
if ( value) {
this . $store. commit ( 'cart/toggleAllCheck' , false )
} else {
this . $store. commit ( 'cart/toggleAllCheck' , true )
}
}
}
08. 购物车 - 删除功能完成
查看接口,封装 API ( 注意:此处 id 为获取回来的购物车数据的 id )
export const delSelect = ( cartIds ) => {
return request. post ( '/cart/clear' , {
cartIds
} )
}
注册删除点击事件
< div v- else : class = "{ disabled: selCount === 0 }" @click= "handleDel" class = "delete" >
删除 ( { { selCount } } )
< / div>
async handleDel ( ) {
if ( this . selCount === 0 ) return
await this . $store. dispatch ( 'cart/delSelect' )
this . isEdit = false
} ,
提供 actions
actions : {
async delSelect ( context ) {
const selCartList = context. getters. selCartList
const cartIds = selCartList. map ( item => item. id)
await delSelect ( cartIds)
Toast ( '删除成功' )
context. dispatch ( 'getCartAction' )
}
} ,
09. 购物车 - 空购物车处理
外面包个大盒子,添加 v-if 判断
< div class = " cart-box" v-if = " isLogin && cartList.length > 0" >
< div class = " cart-title" >
...
</ div>
< div class = " cart-list" >
...
</ div>
< div class = " footer-fixed" >
...
</ div>
</ div>
< div class = " empty-cart" v-else >
< img src = " @/assets/empty.png" alt = " " >
< div class = " tips" >
您的购物车是空的, 快去逛逛吧
</ div>
< div class = " btn" @click = " $router.push('/')" > 去逛逛</ div>
</ div>
相关样式
.empty-cart {
padding : 80px 30px;
img {
width : 140px;
height : 92px;
display : block;
margin : 0 auto;
}
.tips {
text-align : center;
color : #666;
margin : 30px;
}
.btn {
width : 110px;
height : 32px;
line-height : 32px;
text-align : center;
background-color : #fa2c20;
border-radius : 16px;
color : #fff;
display : block;
margin : 0 auto;
}
}