59 - 综合案例 - 智慧商城-11 - 购物车

一. 购物车-基本静态布局

说明: 购物车 数据联动关系 较多,且通常会封装一些 小组件

所以为了便于维护,一般都会将购物车的数据基于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;
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值