一. 购物车-基本静态布局
说明: 购物车 数据联动关系 较多,且通常会封装一些 小组件
所以为了便于维护,一般都会将购物车的数据基于vuex 分模块管理
需求分析:
(1).基本静态结构(快速实现)
(2).构建vuex cart 模块,获取数据存储
(3).基于数据 动态渲染 购物车列表
(4).封装 getters 实现动态统计
(5).全选反选功能
(6).数字框修改数量功能
(7).编辑切换状态,删除功能
(8).空购物车处理
1. 引入组件
utils / vant-ui.js
// 按需导入
import Vue from 'vue'
import {Checkbox} from 'vant'
Vue.use(Checkbox)
2. 基本静态结构
views / layout / cart.vue
<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>
3. 更换固定数字框 为 数字框组件
views / layout / cart.vue
<!--原代码-->
<div class="count-box">
<button class="minus">-</button>
<input class="inp" :value="4" type="text" readonly>
<button class="add">+</button>
</div>
<!--更换为数字框组件-->
<CountBox></CountBox>
<script>
import CountBox from '@/components/CountBox.vue'
export default {
name: 'CartPage',
components: {
CountBox
}
}
</script>
二. 构建 vuex模块 - 获取数据存储
1. 新建 modules/cart.js模块
store / modules / cart.js
export default {
namespaced: true,
state () {
return {
cartList: []
}
},
mutations: {},
actions: {},
getters: {}
}
2. 挂载到 store 上面
store / index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import cart from './modules/cart' // 导入cart仓库
Vue.use(Vuex)
export default new Vuex.Store({
getters: {
// 配置全局的token
token (state) {
return state.user.userInfo.token
}
},
modules: {
user,
cart // 挂载
}
})
3. 封装购物车数据接口
api / cart.js
import request from '@/utils/request'
// 加入购物车
...
// 获取购物车列表
export const getCartList = () => {
return request.get('/cart/list')
}
4. 封装action 和 mutations
store / modules / cart.js
import { getCartList } from '@/api/cart'
export default {
namespaced: true,
state () {
return {
cartList: []
}
},
mutations: {
// 提供一个设置 cartList 的 mutations
setCartList (state, newList) {
state.cartList = newList
}
},
actions: {
async getCartAction (context) {
// 调用购物车列表请求接口
const { data } = await getCartList()
// 后台返回的数据中,不包含复选框的选中状态,为了实现将来的功能
// 需要手动维护数据,给每一项,添加一个 isChecked 状态(标记当前商品是否选中)
data.list.forEach(item => {
item.isChecked = true
})
// 调用mutations 修改 state数据
context.commit('setCartList', data.list)
}
},
getters: {}
}
5. 页面中调用
views / layout / cart.vue
<script>
import CountBox from '@/components/CountBox.vue'
export default {
name: 'CartPage',
components: {
CountBox
},
created () {
// 必须是登陆过的用户,才能获取购物车列表
if (this.$store.getters.token) {
this.$store.dispatch('cart/getCartAction')
}
}
}
</script>
6. 验证
浏览器 vue调试工具查看仓库是否有数据
三. 购物车- mapState渲染购物车列表
1. 将数据映射到页面
views / layout / cart.vue
<script>
import CountBox from '@/components/CountBox.vue'
import { mapState } from 'vuex'
export default {
name: 'CartPage',
components: {
CountBox
},
computed: {
// 解包数据
...mapState('cart', ['cartList'])
},
created () {
// 必须是登陆过的用户,才能获取购物车列表
if (this.$store.getters.token) {
this.$store.dispatch('cart/getCartAction')
}
}
}
</script>
2. 动态渲染
views / layout / cart.vue
<!-- 购物车列表 -->
<div class="cart-list">
<!-- 1. 循环cartList -->
<div class="cart-item" v-for="item in cartList" :key="item.goods_id">
<!-- 2. 设置选中状态 -->
<van-checkbox :value="item.isChecked"></van-checkbox>
<div class="show">
<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>
四. 购物车- 封装getters动态计算展示
1. 封装getters
store / modules / cart.js
商品总数 / 选中的商品列表 / 选中的商品总数 / 选中的商品总价
getters: {
// 求所有商品累加总数总数 reduce((每次累加结果 , 每一项, 下标))
cartTotal (state) {
return state.cartList.reduce((sum, item) => sum + item.goods_num, 0)
},
// 选中的商品项
selCartList (state) {
return state.cartList.filter(item => item.isChecked)
},
// 选中的总数
selCount (state, getters) {
return getters.selCartList.reduce((sum, item) => sum + item.goods_num, 0)
},
// 选中的总价
selPrice (state, getters) {
return getters.selCartList.reduce((sum, item) => {
return sum + item.goods_num * item.goods.goods_price_min
}, 0).toFixed(2)
}
}
2. 页面中mapGetters映射使用
views / layout / cart.vue
<template>
<!-- 购物车开头 -->
<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>
<!--商品小于0不允许点击-->
<div v-if="true" class="goPay" :class="{disabled:selCount ===0}">结算({{selCount}})</div>
<div v-else class="delete" :class="{disabled:selCount ===0}">删除</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
...
computed: {
// 映射数据
...
...mapGetters('cart', ['cartTotal', 'selCartList', 'selCount', 'selPrice'])
},
</script>
五. 购物车 - 全选反选
1. 全选getters
store / modules / cart.js
getters: {
...
// 是否全选
isAllChecked (state) {
// every()用于检测数组中的所有元素是否都满足指定条件
return state.cartList.every(item => item.isChecked)
}
}
views / layout / cart.vue
<script>
computed: {
// 映射数据
...
...mapGetters('cart', ['...', 'isAllChecked'])
},
</script>
<template>
<div class="all-check">
<van-checkbox :value="isAllChecked" icon-size="18"></van-checkbox>
全选
</div>
</template>
2. 点击小选,修改状态
views / layout / cart.vue
<template>
<!-- 复选框定义点击事件 -->
<van-checkbox @click="toggleCheck(item.goods_id)" :value="item.isChecked"></van-checkbox>
</template>
<script>
methods: {
toggleCheck (goodsId) {
this.$store.commit('cart/toggleCheck', goodsId)
}
}
</script>
store / modules / cart.js
mutations: {
...
toggleCheck (state, goodsId) {
// 让对应的 id 的项,状态取反
const goods = state.cartList.find(item => item.goods_id === goodsId)
goods.isChecked = !goods.isChecked
}
},
3. 点击全选,-重置状态
views / layout / cart.vue
<template>
<!-- 全选定义点击事件 -->
<div @click="toggleAllCheck" class="all-check">
<van-checkbox :value="isAllChecked" icon-size="18"></van-checkbox>
全选
</div>
</template>
<script>
methods: {
...
toggleAllCheck () {
// 对全选框取反
this.$store.commit('cart/toggleAllCheck', !this.isAllChecked)
}
}
</script>
store / modules / cart.js
mutations: {
...
toggleAllCheck (state, flag) {
// 让所有的小选框,同步设置
state.cartList.forEach(item => {
item.isChecked = flag
})
}
},
六. 购物车-数字框修改数量
1. 封装api接口
api / cart.js
// 更新购物车商品的数量
export const changeCount = (goodsId, goodsNum, goodsSkuId) => {
return request.post('cart/update', {
goodsId,
goodsNum,
goodsSkuId
})
2. 页面注册点击事件,传递数据
views / layout / cart.vue
<template>
<!-- 既希望保留原本的形参,又需要通过覅用函数传参 => 箭头函数包装一层 -->
<!-- <CountBox @input="chageCount" :value="item.goods_num"></CountBox> -->
<CountBox @input="(value) => chageCount(value,item.goods_id,item.goods_sku_id)" :value="item.goods_num"></CountBox>
</template>
<script>
methods: {
...
chageCount (goodsNum, goodsId, goodsSkuId) {
// console.log(goodsNum, goodsId, goodsSkuId)
// 调用 vuex 的 action,进行数量的修改
this.$store.dispatch('cart/changeCountAction', {
goodsNum,
goodsId,
goodsSkuId
})
}
}
</script>
3. 提供 action发送请求, commit mutation
store / modules / cart.js
import { getCartList, changeCount } from '@/api/cart'
mutations: {
...
changeCount1 (state, { goodsId, goodsNum }) {
const goods = state.cartList.find(item => item.goods_id === goodsId)
goods.goods_num = goodsNum
}
},
actions: {
...
async changeCountAction (context, obj) {
const { goodsNum, goodsId, goodsSkuId } = obj
// 先本地修改
context.commit('changeCount1', { goodsNum, goodsId })
// 在同步到后台
const res = await changeCount(goodsId, goodsNum, goodsSkuId)
console.log(res)
}
},
七. 购物车- 编辑切换状态 & 删除
1. 编辑切换状态
views / layout / cart.vue
(1). data提供数据,定义是否在编辑删除的状态
<script>
data () {
return {
isEdit: false
}
},
</script>
(2). 注册点击事件,修改状态
<template>
<span class="edit" @click="isEdit=!isEdit">
<van-icon name="edit" />
编辑
</span>
</template>
(3). 底下按钮根据状态变化
<div v-if="!isEdit" class="goPay" :class="{ disabled: selCount === 0 }">结算({{ selCount }})</div>
<div v-else class="delete" :class="{ disabled: selCount === 0 }">删除</div>
(4). 监视编辑状态,动态控制复选框状态
watch: {
isEdit (value) {
if (value) {
this.$store.commit('cart/toggleAllCheck', false)
} else {
this.$store.commit('cart/toggleAllCheck', true)
}
}
}
2. 删除功能
(1). 封装API
api / cart.js
// 删除购物车商品
export const delSelect = (cartIds) => {
return request.post('/cart/clear', {
cartIds // 购物车数据的id
})
}
(2). 注册删除点击事件
views / layout / cart.vue
<template>
<div v-else @click="handleDel" class="delete" :class="{ disabled: selCount === 0 }">删除</div>
</template>
<script>
methods: {
...
async handleDel () {
if (this.selCount === 0) return
await this.$store.dispatch('cart/delSelects')
this.isEdit = false
}
},
</script>
(3). 提供 actions
store / modules / cart.js
import { getCartList, changeCount, delSelect } from '@/api/cart'
actions: {
...
// 删除购物车数据
async delSelects (context) {
const selCartList = context.getters.selCartList
const cartIds = selCartList.map(item => item.id)
await delSelect(cartIds)
Toast('删除成功')
// 重新拉取最新的购物车数据(重新渲染)
context.dispatch('getCartAction')
}
},
八. 空购物车处理
1. 页面样式判断
views / layout / cart.vue
<template>
<!--外面包个大盒子(div),添加 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>
</template>
<script>
computed: {
...
isLogin () {
return this.$store.getters.token
}
},
created () {
// 必须是登陆过的用户,才能获取购物车列表
if (this.isLogin) {
this.$store.dispatch('cart/getCartAction')
}
},
</script>
2. 静态页面
views / layout / cart.vue
.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;
}
}