B2C-08-购物车

364 阅读15分钟

购物车

购物车功能分析

目的:了解购物车两种状态下的操作逻辑,方便后续的开发理解。

image.png

总结:

  • 购物车的各种操作都会有两种状态的区分,但是不会在组件中去区分。

  • 而是在封装在vuex中的actions中去区分,在组件上只需调用actions即可。

  • 在actions中通过user信息去区分登录状态

    • 未登录,通过mutations修改vuex中的数据即可,vuex已经实现持久化,会同步保持在本地。

    • 已登录,通过api接口去服务端操作,响应成功后通过mutations修改vuex中的数据即可,它也会同步在本地。

  • 不管何种操作何种状态返回一个promise,然后组件能够判断操作是否完毕是否成功,再而去做其他事情。

注意:

  • 登录后,需要合并本地购物车到服务端。
  • 退出后,清空vuex数据也会同步清空本地数据。

加入购物车-本地

目的:完成商品详情的添加购物车操作,支持未登录状态。

大致步骤:

  • 约定本地存储的信息内容:要和加入购物车的接口参数保持一致
  • 编写mutaions添加购物车逻辑
  • 编写actions进行添加操作
  • 在商品详情页实现添加逻辑

落地代码:

  • vuex中的修改数据,获取数据 src/store/module/cart.js
  // 本地:id skuId name picture price nowPrice count attrsText selected stock isEffective
  // 线上:比上面多 isCollect 有用 discount 无用 两项项信息
  mutations: {
    // 把商品添加到购物车
    insertCart (state, goods) {
      // goods参数表示商品详情信息(包含我们需要的相关参数)
      // 如果多次加入同一件商品,那么需要进行商品数量的累加操作
      // 根据skuId判断是否是同一件商品
      const index = state.list.findIndex(item => item.skuId === goods.skuId)
      if (index !== -1) {
        // 有这件商品,进行商品数量的累加操作
        goods.count = state.list[index].count + 1
        // 删除原来的商品
        state.list.splice(index, 1)
      }
      state.list.unshift(goods)
    }
  },
  actions: {
    // action方法触发后的默认返回值是Promise实例对象
    insertCart (context, goods) {
      // 判断当前用户是否处于登录状态
      // context.rootState可以获取所有的模块的状态
      const token = context.rootState.user.profiletoken
      if (token) {
        // 已经登录,调用接口加入购物车
      } else {
        // 尚未登录,添加商品到本地购物车
        context.commit('insertCart', goods)
      }
    }
  }
  • 商品详情点击加入购物车 src/views/goods/index.vue
  setup () {
    const goodsDetail = useGoods()
    // sku改变时候触发
    const skuInfo = (sku) => {
      if (sku.skuId) {
        goodsDetail.value.price = sku.price
        goodsDetail.value.oldPrice = sku.oldPrice
        goodsDetail.value.inventory = sku.inventory
        // 记录当前选中的sku信息
+        currentSku.value = sku
      } else {
+        currentSku.value = null
      }
    }
    // 选择的数量
    const num = ref(1)
    // 加入购物车逻辑
+ // 获取当前组件的实例
+ const instance = getCurrentInstance()
+ // 加入购物车
+ const insertCart = () => {
+   // 判断是否选中了规格
+   if (!currentSku.value) {
+     // 进行消息提示
+     return Massage({ type: 'warn', text: '请选择规格' })
+   }
+   // 判断是否还有库存
+   // 判断是否还有库存
+   if (num.value > goodsDetail.inventory) {
+     // 库存不够,进行提示
+     // instance.proxy表示组件的实例对象
+     return instance.proxy.$message({ type: 'warn', text: '库存不够' })
+     // return Massage({ type: 'warn', text: '库存不够' })
+   }
+   // 触发加入购物车的action
+   // 加入购物车的信息 id skuId name picture price nowPrice count attrsText selected stock isEffective
+   const goodsInfo = {
+     // 当前商品的id
+     id: goodsDetail.value.id,
+     // 加入购物车的商品的skuId
+     skuId: currentSku.value.id,
+     // 商品名称
+     name: goodsDetail.value.name,
+     // 商品的第一张图片,在购物进行显示
+     picture: goodsDetail.value.mainPictures[0],
+     // 默认价格
+     price: currentSku.value.price,
+     // 当前价格
+     nowPrice: currentSku.value.price,
+     // 购买的数量
+     count: num.value,
+     // 规格参数
+     attrsText: currentSku.value.attrsText,
+     // 当前商品是否选中:默认设置为选中,用于支付
+     selected: true,
+     // 当前库存
+     stock: currentSku.value.inventory,
+     // 当前商品是否有效:默认值有效,这个商品依然在售
+     isEffective: true
+   }
+   store.dispatch('cart/insertCart', goodsInfo)
+ }

总结:

  1. 收集商品的数据,添加到购物车列表(vuex)
  2. 获取组件的实例对象  const instance = getCurrentInstance() ;  instance.proxy

回顾

  • QQ登录

    • 配置QQ登录的基本环境:配置DNS;配置webpack
    • 实现QQ登录图标按钮:点击后跳转到QQ授权登录的页面
    • 准备回跳的页面:基本布局和路由配置
    • 完善回跳页面的基本布局
      1. 完善用户的登录信息:尚未注册小兔鲜账号

      2. 绑定手机号:已经注册小兔鲜账号,但是尚未绑定手机号

      3. 扫码后直接登录:已经注册小兔鲜账号,并且已经绑定手机号

  • 购物车

    • 熟悉核心的流程:本地购物车;登录后远程购物车

    • 添加本地购物车:

      1. 实现添加购物车的mutation方法

      2. 实现添加购物车的action方法

      3. 触发添加的动作

    • 基于vue3提供的方法访问组件的实例对象

头部购物车-基础布局

目的:在网站头部购物车图片处,鼠标经过展示购物车列表。

大致步骤:

  • 提取头部购物车组件,完成基础布局。
  • 通过getters返回,有效商品总数,和有效商品列表。
  • 渲染组件。

落的代码:

  • 新建组件 src/views/layout/components/top-header-cart.vue
<template>
  <div class="cart">
    <a class="curr" href="#"> <i class="iconfont icon-cart"></i><em>2</em> </a>
  </div>
</template>
<script>
export default {
  name: 'AppHeaderCart'
}
</script>
<style scoped lang="less">
.cart {
  width: 50px;
  .curr {
    height: 32px;
    line-height: 32px;
    text-align: center;
    position: relative;
    display: block;
    .icon-cart {
      font-size: 22px;
    }
    em {
      font-style: normal;
      position: absolute;
      right: 0;
      top: 0;
      padding: 1px 6px;
      line-height: 1;
      background: @helpColor;
      color: #fff;
      font-size: 12px;
      border-radius: 10px;
      font-family: Arial;
    }
  }
}
</style>
  • src/views/layout/components/top-header.vue文件中的图标代码迁移到购物车组件中。
      <div class="search">
        <i class="iconfont icon-search"></i>
        <input type="text" placeholder="搜一搜">
      </div>
+     <TopHeaderCart />
    </div>
  </header>
</template>

<script>
import AppHeaderNav from './app-header-nav'
+import TopHeaderCart from './top-header-cart.vue'
export default {
  name: 'AppHeader',
+  components: { AppHeaderNav, TopHeaderCart }
}
</script>
  • 基础布局和弹出效果  src/components/app-header-cart.vue
<template>
  <div class="cart">
    <a class="curr" href="javascript:;">
      <i class="iconfont icon-cart"></i><em>2</em>
    </a>
    <div class="layer">
      <div class="list">
        <div class="item" v-for="i in 4" :key="i">
          <RouterLink to="">
            <img src="https://yanxuan-item.nosdn.127.net/ead73130f3dbdb3cabe1c7b0f4fd3d28.png" alt="">
            <div class="center">
              <p class="name ellipsis-2">和手足干裂说拜拜 ingrams手足皲裂修复霜</p>
              <p class="attr ellipsis">颜色:修复绿瓶 容量:150ml</p>
            </div>
            <div class="right">
              <p class="price">&yen;45.00</p>
              <p class="count">x2</p>
            </div>
          </RouterLink>
          <i class="iconfont icon-close-new"></i>
        </div>
      </div>
      <div class="foot">
        <div class="total">
          <p>共 3 件商品</p>
          <p>&yen;135.00</p>
        </div>
        <XtxButton type="plain">去购物车结算</XtxButton>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'TopHeaderCart'
}
</script>
<style scoped lang="less">
.cart {
  width: 50px;
  position: relative;
  z-index: 600;
  .curr {
    height: 32px;
    line-height: 32px;
    text-align: center;
    position: relative;
    display: block;
    .icon-cart {
      font-size: 22px;
    }
    em {
      font-style: normal;
      position: absolute;
      right: 0;
      top: 0;
      padding: 1px 6px;
      line-height: 1;
      background: @helpColor;
      color: #fff;
      font-size: 12px;
      border-radius: 10px;
      font-family: Arial;
    }
  }
  &:hover {
    .layer {
      opacity: 1;
      transform: none
    }
  }
  .layer {
    opacity: 0;
    transition: all .4s .2s;
    transform: translateY(-200px) scale(1, 0);
    width: 400px;
    height: 400px;
    position: absolute;
    top: 50px;
    right: 0;
    box-shadow: 0 0 10px rgba(0,0,0,0.2);
    background: #fff;
    border-radius: 4px;
    padding-top: 10px;
    &::before {
      content: "";
      position: absolute;
      right: 14px;
      top: -10px;
      width: 20px;
      height: 20px;
      background: #fff;
      transform: scale(0.6,1) rotate(45deg);
      box-shadow: -3px -3px 5px rgba(0,0,0,0.1);
    }
    .foot {
      position: absolute;
      left: 0;
      bottom: 0;
      height: 70px;
      width: 100%;
      padding: 10px;
      display: flex;
      justify-content: space-between;
      background: #f8f8f8;
      align-items: center;
      .total {
        padding-left: 10px;
        color: #999;
        p {
          &:last-child {
            font-size: 18px;
            color: @priceColor;
          }
        }
      }
    }
  }
  .list {
    height: 310px;
    overflow: auto;
    padding: 0 10px;
    &::-webkit-scrollbar{
      width:10px;
      height:10px;
    }
    &::-webkit-scrollbar-track{
      background: #f8f8f8;
      border-radius: 2px;
    }
    &::-webkit-scrollbar-thumb{
      background: #eee;
      border-radius:10px;
    }
    &::-webkit-scrollbar-thumb:hover{
      background: #ccc;
    }
    .item {
      border-bottom: 1px solid #f5f5f5;
      padding: 10px 0;
      position: relative;
      i {
          position: absolute;
          bottom: 38px;
          right: 0;
          opacity: 0;
          color: #666;
          transition: all .5s;
      }
      &:hover {
        i {
          opacity: 1;
          cursor: pointer;
        }
      }
      a {
        display: flex;
        align-items: center;
        img {
          height: 80px;
          width: 80px;
        }
        .center {
          padding: 0 10px;
          width: 200px;
          .name {
            font-size: 16px;
          }
          .attr {
            color: #999;
            padding-top: 5px;
          }
        }
        .right {
          width: 100px;
          padding-right: 20px;
          text-align: center;
          .price {
            font-size: 16px;
            color: @priceColor;
          }
          .count {
            color: #999;
            margin-top: 5px;
            font-size: 16px;
          }
        }
      }
    }
  }
}
</style>

总结:封装购物车头部列表的组件

  • 使用getters得到有效商品列表和期种件数 src/store/modules/cart.js
getters: {
  // 有效商品列表
  validList (state) {
    return state.list.filter(item => item.stock > 0 && item.isEffective)
  },
  // 有效商品件数
  validTotal (state, getters) {
    return getters.validList.reduce((p, c) => p + c.count, 0)
  },
  // 有效商品总金额
  validAmount (state, getters) {
    return getters.validList.reduce((p, c) => p + c.nowPrice * 100 * c.count, 0) / 100
  }
}
  • 渲染头部购物车信息
<template>
  <div class="cart">
    <a class="curr" href="javascript:;">
      <i class="iconfont icon-cart"></i><em>{{$store.getters['cart/validTotal']}}</em>
    </a>
    <div class="layer">
      <div class="list">
        <div class="item" v-for="item in $store.getters['cart/validList']" :key="item.skuId">
          <RouterLink to="">
            <img :src="item.picture" alt="">
            <div class="center">
              <p class="name ellipsis-2">{{item.name}}</p>
              <p class="attr ellipsis">{{item.attrsText}}</p>
            </div>
            <div class="right">
              <p class="price">&yen;{{item.nowPrice}}</p>
              <p class="count">x{{item.count}}</p>
            </div>
          </RouterLink>
          <i class="iconfont icon-close-new"></i>
        </div>
      </div>
      <div class="foot">
        <div class="total">
          <p>共 {{$store.getters['cart/validTotal']}} 件商品</p>
          <p>&yen;{{$store.getters['cart/validAmount']}}</p>
        </div>
        <XtxButton type="plain">去购物车结算</XtxButton>
      </div>
    </div>
  </div>
</template>

总结:

  1. 通过getters动态计算有效的购物车商品的列表;总数;总价

头部购物车-列表状态修改-本地

目的:根据本地存储的商品获取最新的库存价格和有效状态。

顶部的购物车展示的商品有可能失效,所以需要调用接口查询相应规格的商品是否依然有效。

大致步骤:

  • 定义获取最新信息的API
  • 定义修改购物车商品信息的mutations
  • 定义获取购物车列表信息的actions
  • 在头部购物车组件初始化的时候更新列表信息

落的代码:

  • 定义获取最新信息的API src/api/cart.js
import request from '@/utils/request'

//  获取新的商品信息(根据规格的skuId)
export const getNewCartGoods = (skuId) => {
  return request({
    method: 'get',
    url: `/goods/stock/${skuId}`
  })
}
  • 定义修改购物车商品信息的mutations   src/store/module/cart.js
// 修改购物车商品
updateCart (state, goods) {
  // goods中字段有可能不完整,goods有的信息才去修改。
  // 1. goods中必需又skuId,才能找到对应的商品信息
  const updateGoods = state.list.find(item => item.skuId === goods.skuId)
  for (const key in goods) {
    if (goods[key] !== null && goods[key] !== undefined && goods[key] !== '') {
      updateGoods[key] = goods[key]
    }
  }
}
  • 定义获取购物车列表信息的actions src/store/module/cart.js
import { getNewCartGoods } from '@/api/cart.js'
// 更新商品最新信息的action
findCartList (context) {
  // 遍历每一件购物车的商品,分别查询每一件商品的最新信息
  const promiseArr = []
  context.state.list.forEach(item => {
    const ret = getNewCartGoods(item.skuId)
    promiseArr.push(ret)
  })
  // 并发触发多个异步任务,所有任务的结果获取后,可以通过then的回调的参数result得到所有的结果
  Promise.all(promiseArr).then(result => {
    // result表示所有商品的查询结果
    result.forEach((goods, i) => {
      context.commit('updateCart', {
        skuId: context.state.list[i].skuId,
        ...goods.result
      })
    })
  }).catch(() => {
    // 获取购物车最新商品数据失败
    // console.log(err)
    Massage({ type: 'error', text: '获取购物车最新商品数据失败' })
  })
},
  • 再头部购物车组件初始化的时候更新列表信息 src/views/layout/components/top-header-cart.vue
setup () {
  const store = useStore()
  store.dispatch('cart/findCartList')
}

总结:

  1. 准备更新购物车商品的action(批量更新)
  2. Promise.all触发多个任务用法

头部购物车-删除操作-本地

目的:完成头部购物车删除操作,支持未登录状态。

大致步骤:

  • 编写mutaions删除购物车商品逻辑
  • 编写actions进行删除操作
  • 在头部购物车进行删除逻辑

落的代码:

  • vuex的mutations和actions代码 src/store/module/cart.js
mutations: {
    // ... 省略
    // 删除顶部购物车单独的一件商品
    deleteCart (state, skuId) {
      const index = state.list.findIndex(item => item.skuId === skuId)
      if (index !== -1) {
        // 删除数组中一个元素
        state.list.splice(index, 1)
      }
    },
},
actions: {
  // ... 省略
  // 删除顶部购物车的单件商品
  deleteCart (context, skuId) {
    const token = context.rootState.user.profiletoken
    if (token) {
      // 已经登录
    } else {
      // 未登录
      context.commit('deleteCart', skuId)
    }
  },
}
  • 头部组件实现删除逻辑 src/layout/views/components/top-header-cart.vue
+ 购物车无商品不显示弹出层,并且不是在购物车页面
+<div class="layer" v-if="$store.getters['cart/validTotal']&&$route.path!=='/cart'">
+ 绑定点击事件传入skuId
+<i @click="deleteCart(item.skuId)" class="iconfont icon-close-new"></i>
import { useStore } from 'vuex'
import Massage from '@/components/library/Message.js'

export default {
  name: 'TopHeaderCart',
  setup () {
    // 触发更新购物车商品信息的action
    const store = useStore()
    store.dispatch('cart/updateCart')
    // 删除购物车商品
    const deleteCart = (skuId) => {
      store.dispatch('cart/deleteCart', skuId).then(() => {
        // 删除成功
        Massage({ type: 'success', text: '删除购物车商品成功' })
      }).catch(() => {
        Massage({ type: 'error', text: '删除购物车商品失败' })
      })
    }
    return { deleteCart }
  }
}

总结:

  1. 准备删除购物车商品的mutation
  2. 准备删除购物车的action
  3. 组件中触发action即可

购物车页面-基础布局

目的:完成购物车组件基础布局和路由配置与跳转链接。

image.png

大致步骤:

  • 完成头部组件,购物车图标,购物车结算按钮,点击跳转购物车路由。商品点击跳转详情的操作。
  • 配置购物车路由和组件,完成基础布局。

落的代码:

  • 跳转功能  src/layout/views/components/top-header-cart.vue
<RouterLink to="/cart" class="curr">
  <i class="iconfont icon-cart"></i><em>{{$store.getters['cart/validTotal']}}</em>
</RouterLink>
<div class="item" v-for="item in $store.getters['cart/validList']" :key="item.skuId">
+  <RouterLink :to="`/product/${item.id}`">
<img :src="item.picture" alt="">
  • 组件与路由 src/views/cart/index.vue
<template>
  <div class="xtx-cart-page">
    <div class="container">
      <XtxBread>
        <XtxBreadItem to="/">首页</XtxBreadItem>
        <XtxBreadItem>购物车</XtxBreadItem>
      </XtxBread>
      <div class="cart">
        <table>
          <thead>
            <tr>
              <th width="120"><XtxCheckbox>全选</XtxCheckbox></th>
              <th width="400">商品信息</th>
              <th width="220">单价</th>
              <th width="180">数量</th>
              <th width="180">小计</th>
              <th width="140">操作</th>
            </tr>
          </thead>
          <!-- 有效商品 -->
          <tbody>
            <tr v-for="i in 3" :key="i">
              <td><XtxCheckbox /></td>
              <td>
                <div class="goods">
                  <RouterLink to="/"><img src="https://yanxuan-item.nosdn.127.net/13ab302f8f2c954d873f03be36f8fb03.png" alt=""></RouterLink>
                  <div>
                    <p class="name ellipsis">和手足干裂说拜拜 ingrams手足皲裂修复霜</p>
                    <!-- 选择规格组件 -->
                  </div>
                </div>
              </td>
              <td class="tc">
                <p>&yen;200.00</p>
                <p>比加入时降价 <span class="red">&yen;20.00</span></p>
              </td>
              <td class="tc">
                <XtxNumbox />
              </td>
              <td class="tc"><p class="f16 red">&yen;200.00</p></td>
              <td class="tc">
                <p><a href="javascript:;">移入收藏夹</a></p>
                <p><a class="green" href="javascript:;">删除</a></p>
                <p><a href="javascript:;">找相似</a></p>
              </td>
            </tr>
          </tbody>
          <!-- 无效商品 -->
          <tbody>
            <tr><td colspan="6"><h3 class="tit">失效商品</h3></td></tr>
            <tr v-for="i in 3" :key="i">
              <td><XtxCheckbox style="color:#eee;" /></td>
              <td>
                <div class="goods">
                  <RouterLink to="/"><img src="https://yanxuan-item.nosdn.127.net/13ab302f8f2c954d873f03be36f8fb03.png" alt=""></RouterLink>
                  <div>
                    <p class="name ellipsis">和手足干裂说拜拜 ingrams手足皲裂修复霜</p>
                    <p class="attr">颜色:粉色 尺寸:14cm 产地:中国</p>
                  </div>
                </div>
              </td>
              <td class="tc"><p>&yen;200.00</p></td>
              <td class="tc">1</td>
              <td class="tc"><p>&yen;200.00</p></td>
              <td class="tc">
                <p><a class="green" href="javascript:;">删除</a></p>
                <p><a href="javascript:;">找相似</a></p>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
      <!-- 操作栏 -->
      <div class="action">
        <div class="batch">
          <XtxCheckbox>全选</XtxCheckbox>
          <a href="javascript:;">删除商品</a>
          <a href="javascript:;">移入收藏夹</a>
          <a href="javascript:;">清空失效商品</a>
        </div>
        <div class="total">
          共 7 件商品,已选择 2 件,商品合计:
          <span class="red">¥400</span>
          <XtxButton type="primary">下单结算</XtxButton>
        </div>
      </div>
      <!-- 猜你喜欢 -->
      <GoodRelevant />
    </div>
  </div>
</template>
<script>
import GoodRelevant from '@/views/goods/components/goods-relevant'
export default {
  name: 'XtxCartPage',
  components: { GoodRelevant }
}
</script>
<style scoped lang="less">
.tc {
  text-align: center;
  .xtx-numbox {
    margin: 0 auto;
    width: 120px;
  }
}
.red {
  color: @priceColor;
}
.green {
  color: @xtxColor
}
.f16 {
  font-size: 16px;
}
.goods {
  display: flex;
  align-items: center;
  img {
    width: 100px;
    height: 100px;
  }
  > div {
    width: 280px;
    font-size: 16px;
    padding-left: 10px;
    .attr {
      font-size: 14px;
      color: #999;
    }
  }
}
.action {
  display: flex;
  background: #fff;
  margin-top: 20px;
  height: 80px;
  align-items: center;
  font-size: 16px;
  justify-content: space-between;
  padding: 0 30px;
  .xtx-checkbox {
    color: #999;
  }
  .batch {
    a {
      margin-left: 20px;
    }
  }
  .red {
    font-size: 18px;
    margin-right: 20px;
    font-weight: bold;
  }
}
.tit {
  color: #666;
  font-size: 16px;
  font-weight: normal;
  line-height: 50px;
}
.xtx-cart-page {
  .cart {
    background: #fff;
    color: #666;
    table {
      border-spacing: 0;
      border-collapse: collapse;
      line-height: 24px;
      th,td{
        padding: 10px;
        border-bottom: 1px solid #f5f5f5;
        &:first-child {
          text-align: left;
          padding-left: 30px;
          color: #999;
        }
      }
      th {
        font-size: 16px;
        font-weight: normal;
        line-height: 50px;
      }
    }
  }
}
</style>

总结:准备购物车详情页面的路由配置和组件布局

购物车页面-列表展示-本地

目的:实现本地状态下的,购物车商品列表展示功能。

大致步骤:

  • 准备失效商品列表数据。已选择商品列表数据。已选择商品件数数据。需要支付的金额数据。
  • 渲染模版

落的代码:

  • 准备数据 src/store/module/cart.js
// 选中商品的总价格
selectedTotal (state, getters) {
  return getters.selectedList.reduce((total, item) => total + item.nowPrice * item.count, 0)
},
// 选中的商品的数量
selectedCount (state, getters) {
  return getters.selectedList.reduce((count, item) => count + item.count, 0)
},
// 选中的商品列表(需要付款)
selectedList (state) {
  return state.list.filter(item => item.selected)
},
// 无效的商品列表: 没有库存的;状态是失效的
invalidList (state) {
  return state.list.filter(item => item.stock === 0 || !item.isEffective)
},
  • 渲染列表
      <div class="cart">
        <table>
          <thead>
            <tr>
+              <th width="120"><XtxCheckbox :modelValue="$store.getters['cart/isCheckAll']">全选</XtxCheckbox></th>
              <th width="400">商品信息</th>
              <th width="220">单价</th>
              <th width="180">数量</th>
              <th width="180">小计</th>
              <th width="140">操作</th>
            </tr>
          </thead>
          <!-- 有效商品 -->
          <tbody>
+            <tr v-for="item in $store.getters['cart/validList']" :key="item.skuId">
+             <td><XtxCheckbox :modelValue="item.selected" /></td>
              <td>
                <div class="goods">
+                  <RouterLink :to="`/product/${item.id}`">
+                    <img :src="item.picture" alt="">
                  </RouterLink>
                  <div>
+                    <p class="name ellipsis">{{item.name}}</p>
                    <!-- 选择规格组件 -->
+                    <p class="attr">{{item.attrsText}}</p>
                  </div>
                </div>
              </td>
              <td class="tc">
+                <p>&yen;{{item.nowPrice}}</p>
              </td>
              <td class="tc">
+                <XtxNumbox :modelValue="item.count" />
              </td>
+              <td class="tc"><p class="f16 red">&yen;{{item.nowPrice*100*item.count/100}}</p></td>
              <td class="tc">
                <p><a href="javascript:;">移入收藏夹</a></p>
                <p><a class="green" href="javascript:;">删除</a></p>
                <p><a href="javascript:;">找相似</a></p>
              </td>
            </tr>
          </tbody>
          <!-- 无效商品 -->
          <tbody v-if="$store.getters['cart/invalidList'].length>0">
            <tr><td colspan="6"><h3 class="tit">失效商品</h3></td></tr>
            <tr v-for="item in $store.getters['cart/validList']" :key="item.skuId">
              <td><XtxCheckbox style="color:#eee;" /></td>
              <td>
                <div class="goods">
                  <RouterLink :to="`/product/${item.id}`">
                    <img :src="item.picture" alt="">
                  </RouterLink>
                  <div>
                    <p class="name ellipsis">{{item.name}}</p>
                    <p class="attr">{{item.attrsText}}</p>
                  </div>
                </div>
              </td>
              <td class="tc"><p>&yen;{{item.nowPrice}}</p></td>
              <td class="tc">{{item.count}}</td>
              <td class="tc"><p>&yen;{{item.nowPrice*100*item.count/100}}</p></td>
              <td class="tc">
                <p><a class="green" href="javascript:;">删除</a></p>
                <p><a href="javascript:;">找相似</a></p>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
      <!-- 操作栏 -->
      <div class="action">
        <div class="batch">
+          <XtxCheckbox :modelValue="$store.getters['cart/isCheckAll']">全选</XtxCheckbox>
          <a href="javascript:;">删除商品</a>
          <a href="javascript:;">移入收藏夹</a>
          <a href="javascript:;">清空失效商品</a>
        </div>
        <div class="total">
+          共 {{$store.getters['cart/validTotal']}} 件商品,已选择 {{$store.getters['cart/selectedTotal']}} 件,商品合计:
+          <span class="red">¥{{$store.getters['cart/selectedAmount']}}</span>
          <XtxButton type="primary">下单结算</XtxButton>
        </div>
      </div>

总结:购物车商品信息动态渲染

购物车页面-单选操作-本地

目的:实现本地状态下的,选中商品操作。

大致步骤:

  • 使用购物车商品修改信息的mutations(已实现)
  • 定义购物车商品选中状态的actions
  • 在购物车页面绑定单选的复选框change事件
  • 在事件中调用actions的修改函数

落的代码:

  • 定义修改购物车商品选中状态的mutations src/store/module/cart.js
    // 修改购物车商品
    updateCart (state, goods) {
      // goods中字段有可能不完整,goods有的信息才去修改。
      // 1. goods中必需又skuId,才能找到对应的商品信息
      const updateGoods = state.list.find(item => item.skuId === goods.skuId)
      for (const key in goods) {
        // 布尔类型 false 值需要使用
+       if (goods[key] !== null && goods[key] !== undefined && goods[key] !== '') {
          updateGoods[key] = goods[key]
        }
      }
    },
  • 定义修改购物车商品的actions src/store/module/cart.js
// 切换单个商品的选中状态
toggleCartOne (context, goods) {
    const token = context.rootState.user.profiletoken
    if (token) {
        // 已经登录
    } else {
        // 未登录
        context.commit('updateCart', goods)
    }
},
  • 在购物车页面绑定单选的复选框change事件并处理选中  src/views/cart/index.vue
<td><XtxCheckbox @change="$event=>checkOne(item.skuId,$event)" :modelValue="item.selected" /></td>
import GoodRelevant from '@/views/goods/components/goods-relevant'
import { useStore } from 'vuex'
export default {
  name: 'XtxCartPage',
  components: { GoodRelevant },
  setup () {
    const store = useStore()
    // 单选
    const checkOne = (skuId, selected) => {
      // 根据SKUId的值修改单件商品的状态
      store.dispatch('cart/toggleCartOne', { skuId, selected })
    }
    return { checkOne }
  }
}

总结:点击复选框,传递skuId和切换后的状态,提供给action,action触发mutation进行修改即可

购物车页面-全选操作-本地

目的:实现本地状态下的,全选商品操作。

大致步骤:

  • 修改购物车所有有效商品选中状态的actions
  • 在购物车页面修改调用actions的代码
  • 在购物车页面绑定全选的复选框change事件
  • 在事件中调用actions的修改函数

落的代码

  • 修改购物车商品选中状态的actions让其支持全选 src/store/module/cart.js
// 做有效商品的全选&反选
checkAllCart (ctx, selected) {
  return new Promise((resolve, reject) => {
    if (ctx.rootState.user.profile.token) {
      // 登录 TODO
    } else {
      // 本地
      // 1. 获取有效的商品列表,遍历的去调用修改mutations即可
      ctx.getters.validList.forEach(item => {
        ctx.commit('updateCart', { skuId: item.skuId, selected })
      })
      resolve()
    }
  })
},
  • 在购物车页面修改调用actions的代码   src/views/cart/index.vue
// 实现所有商品的选中控制
const toggleAll = (selected) => {
  store.dispatch('cart/checkAllCart', selected)
}
return { toggleOne, toggleAll }
  • 在购物车页面绑定全选的复选框change事件并处理选中  src/views/cart/index.vue
<!-- 两处都需要加 --> 
<XtxCheckbox @change="toggleAll" :modelValue="$store.getters['cart/isAllCart']">全选</XtxCheckbox>

总结:

  1. 添加action控制全选,组件中触发action
  2. 复选框组件上监听点击操作,事件函数中处理选中,添加getters计算默认的全选按钮的状态

购物车页面-删除操作-本地

目的:实现本地状态下,购物车商品删除

大致步骤:

  • 绑定删除点击事件指定处理函数,调用删除actions
  • 处理无商品展示界面(没有商品时,给一个提示)

落的代码:

  • 绑定删除点击事件指定处理函数,调用删除actions      src/views/cart/index.vue
<!-- 两处删除都绑定 -->
<p><a @click="deleteCart(item.skuId)" class="green" href="javascript:;">删除</a></p>
// 删除
const deleteCart = (skuId) => {
  store.dispatch('cart/deleteCart', skuId)
}
return { checkOne, checkAll, deleteCart }
  • 处理无商品展示界面

组件 src/views/cart/components/cart-none.vue

<template>
  <div class="cart-none">
    <img src="@/assets/images/none.png" alt="" />
    <p>购物车内暂时没有商品</p>
    <div class="btn">
      <XtxButton type="primary" @click="$router.push('/')">继续逛逛</XtxButton>
    </div>
  </div>
</template>
<script>
export default {
  name: 'CartNone'
}
</script>
<style scoped lang="less">
.cart-none {
  text-align: center;
  padding: 150px 0;
  background: #fff;
  img {
    width: 180px;
  }
  p {
    color: #999999;
    padding: 20px 0;
  }
}
</style>

使用 src/views/cart/index.vue

+import CartNone from './components/cart-none.vue'
import { useStore } from 'vuex'
export default {
  name: 'XtxCartPage',
+  components: { GoodRelevant, CartNone },
<!-- 有效商品 -->
<tbody>
  <tr v-if="$store.getters['cart/validList'].length===0">
    <td colspan="6">
      <CartNone />
    </td>
  </tr>

总结:

  1. 实现删除单个商品的功能,同时添加没有商品的提示效果。

购物车页面-确认框组件

目的:通过vue实例调用$confirm函数弹出确认框。import导入函数使用也需要支持。

image.png

大致步骤:

  • 实现组件基础结构和样式。
  • 实现函数式调用组件方式和完成交互。
  • 加上打开时动画效果。
  • 给购物车删除加上确认框。
  • 给vue挂载原型函数$confirm。

落地代码:

  • 实现组件基础结构和样式。

组件 src/components/library/xtx-confirm.vue

<template>
  <div class="xtx-confirm">
    <div class="wrapper">
      <div class="header">
        <h3>温馨提示</h3>
        <a href="JavaScript:;" class="iconfont icon-close-new"></a>
      </div>
      <div class="body">
        <i class="iconfont icon-warning"></i>
        <span>您确认从购物车删除该商品吗?</span>
      </div>
      <div class="footer">
        <XtxButton size="mini" type="gray">取消</XtxButton>
        <XtxButton size="mini" type="primary">确认</XtxButton>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'XtxConfirm'
}
</script>
<style scoped lang="less">
.xtx-confirm {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 8888;
  background: rgba(0,0,0,.5);
  .wrapper {
    width: 400px;
    background: #fff;
    border-radius: 4px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
    .header,.footer {
      height: 50px;
      line-height: 50px;
      padding: 0 20px;
    }
    .body {
      padding: 20px 40px;
      font-size: 16px;
      .icon-warning {
        color: @priceColor;
        margin-right: 3px;
        font-size: 16px;
      }
    }
    .footer {
      text-align: right;
      .xtx-button {
        margin-left: 20px;
      }
    }
    .header {
      position: relative;
      h3 {
        font-weight: normal;
        font-size: 18px;
      }
      a {
        position: absolute;
        right: 15px;
        top: 15px;
        font-size: 20px;
        width: 20px;
        height: 20px;
        line-height: 20px;
        text-align: center;
        color: #999;
        &:hover {
          color: #666;
        }
      }
    }
  }
}
</style>

为了看到布局在购物车页面用下

  • 实现函数式调用组件方式和完成交互。

image.png

定义函数  src/components/library/Confirm.js

import { createVNode, render } from 'vue'
import XtxConfirm from './xtx-confirm'

// 准备div
const div = document.createElement('div')
div.setAttribute('class', 'xtx-confirm-container')
document.body.appendChild(div)

// 该函数渲染XtxConfirm组件,标题和文本
// 函数的返回值是promise对象
export default ({ title, text }) => {
  return new Promise((resolve, reject) => {
    const submitCallback = () => {
      render(null, div)
      resolve()
    }
    const cancelCallback = () => {
      render(null, div)
      reject(new Error('点击取消'))
    }
    // 1. 渲染组件
    // 2. 点击确认按钮,触发resolve同时销毁组件
    // 3. 点击取消按钮,触发reject同时销毁组件
    const vnode = createVNode(XtxConfirm, { title, text, submitCallback, cancelCallback })
    render(vnode, div)
  })
}

组件逻辑 src/components/library/xtx-confirm.vue

<template>
  <div class="xtx-confirm" :class="{fade}">
    <div class="wrapper"  :class="{fade}">
      <div class="header">
        <h3>{{title}}</h3>
        <a @click="cancelCallback()" href="JavaScript:;" class="iconfont icon-close-new"></a>
      </div>
      <div class="body">
        <i class="iconfont icon-warning"></i>
        <span>{{text}}</span>
      </div>
      <div class="footer">
        <XtxButton @click="cancelCallback()" size="mini" type="gray">取消</XtxButton>
        <XtxButton @click="submitCallback()" size="mini" type="primary">确认</XtxButton>
      </div>
    </div>
  </div>
</template>
<script>
// 当前组件不是在APP下进行渲染,无法使用APP下的环境(全局组件,全局指令,原型属性函数)
import XtxButton from '@/components/library/xtx-button'
import { onMounted, ref } from 'vue'
export default {
  name: 'XtxConfirm',
  components: { XtxButton },
  props: {
    title: {
      type: String,
      default: '温馨提示'
    },
    text: {
      type: String,
      default: ''
    },
    submitCallback: {
      type: Function
    },
    cancelCallback: {
      type: Function
    }
  },
  setup () {
    const fade = ref(false)
    onMounted(() => {
      // 当元素渲染完毕立即过渡的动画不会触发
      setTimeout(() => {
        fade.value = true
      }, 0)
    })
    return { fade }
  }
}
</script>
<style scoped lang="less">
.xtx-confirm {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 8888;
  background: rgba(0,0,0,0);
  &.fade {
    transition: all 0.4s;
    background: rgba(0,0,0,.5);
  }
  .wrapper {
    width: 400px;
    background: #fff;
    border-radius: 4px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-60%);
    opacity: 0;
    &.fade {
      transition: all 0.4s;
      transform: translate(-50%,-50%);
      opacity: 1;
    }
    .header,.footer {
      height: 50px;
      line-height: 50px;
      padding: 0 20px;
    }
    .body {
      padding: 20px 40px;
      font-size: 16px;
      .icon-warning {
        color: @priceColor;
        margin-right: 3px;
        font-size: 16px;
      }
    }
    .footer {
      text-align: right;
      .xtx-button {
        margin-left: 20px;
      }
    }
    .header {
      position: relative;
      h3 {
        font-weight: normal;
        font-size: 18px;
      }
      a {
        position: absolute;
        right: 15px;
        top: 15px;
        font-size: 20px;
        width: 20px;
        height: 20px;
        line-height: 20px;
        text-align: center;
        color: #999;
        &:hover {
          color: #666;
        }
      }
    }
  }
}
</style>

总结:

  1. 封装确认框组件结构
  2. 基于组件结构分支渲染函数Confirm,支持Promise的API
  3. 完善确认框组件的数据绑定

注意:全局组件中使用的全局特性(全局组件、全局指令等),需要单独导入,不可以直接使用

  • 使用函数 src/views/cart/index.vue
// 删除
const deleteCart = (skuId) => {
    // store.dispatch('cart/deleteCart', skuId)
    Confirm({ text: '您确定从购物车删除该商品吗?' }).then(() => {
        console.log('点击确认')
    }).catch(e => {
        console.log('点击取消')
    })
}
return { checkOne, checkAll, deleteCart }
  • 给购物车删除加上确认框   src/views/cart/index.vue
const deleteCart = (item) => {
    Confirm(app, { text: ' 您确认从购物车删除该商品吗?' }).then(() => {
      // console.log('点击确认')
      store.dispatch('cart/deleteCart', item.skuId)
    }).catch(e => {
      // console.log('点击取消')
    })
}
  • 给vue挂载原型函数

实现:src/components/library/index.js

import Confirm from './Confirm'
// 如果你想挂载全局的属性,能够通过组件实例调用的属性   this.$message
app.config.globalProperties.$message = Message
+    app.config.globalProperties.$confirm = Confirm

测试:

mounted () {
  this.$confirm({ text: 'xxx' })
},

总结:

  1. 导入方法Confirm并调用,通过then或者catch获取操作结果
  2. 通过this.$confirm方法调用方法

购物车页面-批量删除-本地

目的:实现本地批量删除选中商品功能。

大致的步骤:

  • 定义一个批量删除商品的actions支持批量操作
  • 遍历选中商品,调用单个删除调用mutations函数即可
  • 绑定批量删除点击事件指定处理函数,调用actions进行删除。

落地代码:

  • 批量操作商品的actions支持 src/store/module/cart.js
// 批量删除选中的商品
batchDeleteCart (context) {
  const token = context.rootState.user.profiletoken
  if (token) {
    // 已经登录
  } else {
    // 未登录
    // 遍历所有有效的选中的商品列表,分别进行删除
    context.getters.selectedList.forEach(item => {
      context.commit('deleteCart', item.skuId)
    })
  }
},
  • 绑定批量删除点击事件指定处理函数,调用actions进行删除。  src/views/cart/index.vue
<a @click="batchDeleteCart()" href="javascript:;">删除商品</a>
// 批量删除
const batchDeleteCart = () => {
    Confirm({ text: '您确定从购物车删除选中的商品吗?' }).then(() => {
        store.dispatch('cart/batchDeleteCart')
    }).catch(e => {})
}
return { checkOne, checkAll, deleteCart, batchDeleteCart }

总结:点击删除按钮,触发action,批量删除选中的商品

购物车页面-无效商品-本地

目的:实现本地清空无效商品功能。

大致思路:

  • 去修改批量删除的actions让它适用于两个场景
    • 批量删除选中的
    • 批量删除失效的  isClear

落地代码:

  • 绑定清空无效商品点击事件指定处理函数,调用actions进行删除。  src/views/cart/index.vue
<a href="javascript:;" @click='batchDeleteCart(false)'>删除商品</a>
<a href="javascript:;" @click="batchDeleteCart(true)" >清空失效商品</a>
    // 批量删除
+    const batchDeleteCart = (isClear) => {
+      Confirm({ text: `您确定从购物车删除${isClear ? '失效' : '选中'}的商品吗?` }).then(() => {
        store.dispatch('cart/batchDeleteCart', isClear)
      }).catch(e => {})
    }
    return { checkOne, checkAll, deleteCart, batchDeleteSelectedCart, batchDeleteInvalidCart }
  • 批量删除商品的actions支持清空无效 src/store/module/cart.js
// 批量删除选中商品
+ batchDeleteCart (ctx, isClear) {
  return new Promise((resolve, reject) => {
    if (ctx.rootState.user.profile.token) {
      // 登录 TODO
    } else {
      // 本地
      // 1. 获取选中商品列表,进行遍历调用deleteCart mutataions函数
+     ctx.getters[isClear ? 'invalidList' : 'selectedList'].forEach(item => {
        ctx.commit('deleteCart', item.skuId)
      })
      resolve()
    }
  })
},

总结:基于标志位重构删除的逻辑代码

购物车页面-修改数量-本地

目的:实现本地版本的修改商品数量。

大致的步骤:

  • 绑定xtx-numbox组件的change事件指定处理函数
  • 在函数种调用vuex的cart/updateCart函数修改数量

落的代码:

  • 绑定xtx-numbox组件的change事件指定处理函数
<XtxNumbox @change='changeCount(item.skuId, $event)' :modelValue="item.count" :max='item.stock' />
  • 在函数种调用vuex的cart/updateCart函数修改数量
// 修改数量
const changeCount = (skuId, count) => {
  store.dispatch('cart/updateCart', { skuId, count })
}
return { checkOne, checkAll, deleteCart, batchDeleteCart, changeCount }
  • xtx-numbox组件内部需要触发 change事件
// 通过change事件把计算的值传递回父组件
emit('change', num)

总结:基于xtx-numbox组件获取变更的商品数量,然后vuex状态的值

购物车页面-修改规格-本地

目的:封装一个购物车SKU组件,来修改规格。

image.png

大致步骤:

  • 定义一个组件完成基础结构
  • 完成展开收起操作
  • 展开的时候根据skuId得到商品信息(specs,skus)渲染商品规格。
  • 选择完毕后,点击确认后,修改当前商品规格。

落的代码:

  • 1.定义一个组件完成基础结构

定义组件  src/views/cart/components/cart-sku.vue

<template>
  <div class="cart-sku">
    <div class="attrs">
      <span class="ellipsis">颜色:粉色 尺寸:14cm 产地:中国</span>
      <i class="iconfont icon-angle-down"></i>
    </div>
    <div class="layer">
      <div class="loading"></div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'CartSku'
}
</script>
<style scoped lang="less">
.cart-sku {
  height: 28px;
  border: 1px solid #f5f5f5;
  padding: 0 6px;
  position: relative;
  margin-top: 10px;
  display:inline-block;
  .attrs {
    line-height: 24px;
    display: flex;
    span {
      max-width: 230px;
      font-size: 14px;
      color: #999;
    }
    i {
      margin-left: 5px;
      font-size: 14px;
    }
  }
  .layer {
    position: absolute;
    left: -1px;
    top: 40px;
    z-index: 10;
    width: 400px;
    border: 1px solid @xtxColor;
    box-shadow: 2px 2px 4px lighten(@xtxColor,50%);
    background: #fff;
    border-radius: 4px;
    font-size: 14px;
    padding: 20px;
    &::before {
      content: "";
      width: 12px;
      height: 12px;
      border-left: 1px solid @xtxColor;
      border-top: 1px solid @xtxColor;
      position: absolute;
      left: 12px;
      top: -8px;
      background: #fff;
      transform: scale(.8,1) rotate(45deg);
    }
    .loading {
      height: 224px;
      background: url(../../../assets/images/loading.gif) no-repeat center;
    }
  }
}
</style>

使用组件 src/views/cart/index.vue

+import CartSku from './components/cart-sku'
export default {
  name: 'XtxCartPage',
+  components: { GoodRelevant, CartNone, CartSku },
<div>
  <p class="name ellipsis">{{item.name}}</p>
  <!-- 选择规格组件 -->
+ <CartSku :attrsText='item.attrsText' />
</div>
  • 2.完成展开收起操作  src/views/cart/components/cart-sku.vue
  <div class="cart-sku" ref="target">
    <div class="attrs" @click="toggle()">
      <span class="ellipsis">{{attrsText}}</span>
<script>
import { ref } from 'vue'
import { getSpecsAndSkus } from '@/api/product.js'
import { onClickOutside } from '@vueuse/core'
import GoodsSku from '@/views/goods/components/goods-sku.vue'
export default {
  name: 'CartSku',
  components: { GoodsSku },
  props: {
    attrsText: {
      type: String,
      default: ''
    },
    skuId: {
      type: String,
      required: true
    }
  },
  setup (props) {
    // 根据SKUId获取的商品规格数据
    const goods = ref(null)
    const visible = ref(false)
    const target = ref(null)
    onClickOutside(target, () => {
      // 点击target指定的DOM之外的区域,触发回调函数:隐藏弹窗
      visible.value = false
      goods.value = null
    })
    // 控制显示和隐藏弹窗
    const toggle = () => {
      visible.value = !visible.value
      if (!visible.value) {
        // 关闭弹窗,重置goods数据
        goods.value = null
      }
      getSpecsAndSkus(props.skuId).then(ret => {
        goods.value = ret.result
      })
    }
    return { visible, toggle, target, goods }
  }
}
</script>
  • 3.展开的时候根据skuId得到商品信息(specs,skus)渲染商品规格。

接口API  src/api/product.js

//  获取商品的specs和skus
export const getSpecsAndSkus = (skuId) => {
  return request({
    method: 'get',
    url: `/goods/sku/${skuId}`
  })
}

使用组件传人skuId  src/views/cart/index.vue

<div>
  <p class="name ellipsis">{{item.name}}</p>
  <!-- 选择规格组件 -->
+  <CartSku attrs-text="item.attrsText" :skuId="item.skuId" />
</div>
skuId: {
  type: String,
  default: ''
}
  • 请求数据渲染  src/views/cart/components/cart-sku.vue
<div class="layer" v-if="visible">
    <div v-if="!goods" class="loading"></div>
    <GoodsSku v-if="goods" :skuId="skuId" :skus="goods.skus" :specs='goods.specs' />
    <XtxButton v-if="goods" type="primary" size="mini" style="margin-left:60px">确认</XtxButton>
</div>
+    const goods = ref(null)
    const show = () => {
      visible.value = true
+      // 获取当前spec和sku数据
+      getSpecsAndSkus(props.skuId).then(data => {
+        goods.value = data.result
+      })
    }
    const hide = () => {
      visible.value = false
+      goods.value = null
    }
  • 4.选择完毕后,点击确认后,修改当前商品规格: cart-sku.vue组件确认后传出sku信息
// 选择SKU时候触发
const currSku = ref(null)
const changeSku = (sku) => {
    currSku.value = sku
}
// 更新sku信息:点击确定之后的动作
const submit = () => {
  // 判断当前选中的sku信息是否完整
  if (currentSku.value && currentSku.value.skuId && currentSku.value.skuId !== props.skuId) {
    // 直接触发action,更新sku即可
    store.dispatch('cart/updateCartSku', { skuId: props.skuId, sku: currentSku.value })
    // 关闭弹窗
    visible.value = false
  }
}
return { visible, toggle, target, goods, changeSku, submit }
<XtxButton v-if="goods" size="mini" type="primary" @click="submit()">确认</XtxButton>
  • 再在actions种实现逻辑  src/store/modules/cart.js
// 根据skuId更新商品的规格参数
updateCartSku (context, goods) {
    // 1、根据skuId查询之前的商品数据
    const oldGoods = context.state.list.find(item => item.skuId === goods.skuId)
    // 2、删除之前的商品
    context.commit('deleteCart', goods.skuId)
    // 3、准备要更新数据
    const { skuId, price: nowPrice, inventory: stock, specsText: attrsText } = goods.sku
    // 用新的数据覆盖旧的数据
    const newGoods = { ...oldGoods, skuId, nowPrice, stock, attrsText }
    // 4、加入准备要更新的商品数据
    context.commit('insertCart', newGoods)
},

总结:

  1. 修改规格后,记录修改后的sku信息
  2. 点击确定后,触发action更新商品的规格数据

登录后-合并购物车

目的:登录后需要把把本地购物车合并,且清空本地购物车。

image.png

大致步骤:

  • 编写合并购物车的API接口函数
  • 编写设置购物车数据的mutations目的是清空购物车
  • 编写合并购物车的actions函数,实现合并后清空本地
  • 在登录完成后调用合并购物车函数

落地代码:

  • 编写合并购物车的API接口函数 src/api/cart.js
/**
 * 合并本地购物车
 * @param {Array<object>} cartList - 本地购物车数组
 * @param {String} item.skuId - 商品SKUID
 * @param {Boolean} item.selected - 是否选中
 * @param {Integer} item.count - 数量
 */
export const mergeLocalCart = (cartList) => {
  return request({
    method: 'post',
    url: '/member/cart/merge', 
    data: cartList
  })
}
  • 编写设置购物车数据的mutations目的是清空购物车  src/store/module/cart.js
// 设置购物车列表
setCartList (state, list) {
  state.list = list
}
  • 编写合并购物车的actions函数,实现合并后清空本地  src/store/module/cart.js
// 购物车状态
import { mergeCart } from '@/api/cart'
// 合并本地购物车
async mergeLocalCart (ctx) {
  // 存储token后调用合并API接口函数进行购物合并
  const cartList = ctx.getters.validList.map(({ skuId, selected, count }) => {
    return { skuId, selected, count }
  })
  await mergeLocalCart(cartList)
  // 合并成功将本地购物车删除
  ctx.commit('setCartList', [])
},
  • 在登录完成(绑定成功,完善信息成功)后调用合并购物车函数 login/components/login-form.vue
// 合并购物车操作
store.dispatch('cart/mergeLocalCart').then(() => {
    // 2. 提示
    Message({ type: 'success', text: '登录成功' })
    // 3. 跳转
    router.push('/')
})
  • 绑定手机号成功后login/components/login-bind.vue
// 合并购物车操作
store.dispatch('cart/mergeLocalCart').then(() => {
    // 2. 提示
    Message({ type: 'success', text: '绑定成功' })
    // 3. 跳转
    router.push('/')
})
  • 完善信息后login/components/login-patch.vue
// 合并购物车操作
store.dispatch('cart/mergeLocalCart').then(() => {
    // 2. 提示
    Message({ type: 'success', text: '完善信息成功' })
    // 3. 跳转
    router.push( '/')
})
  • 登录成功后login/callback.vue
store.dispatch('cart/mergeLocalCart').then(() => {
    // 2. 跳转到来源页或者首页
    router.push('/')
    // 3. 成功提示
    Message({ type: 'success', text: 'QQ登录成功' })
})

登录后-商品列表

目标:实现登陆后获取购物车商品列表。

大致步骤:

  • 编写获取商品列表的API接口函数
  • 在actions原有预留TODO位置获取列表
  • 退出登录需要清空购物车

落地代码:

  • 编写获取商品列表的API接口函数  src/api/cart.js
/**
 * 获取登录后的购物车列表
 * @returns Promise
 */
export const findCartList = () => {
  return request({
    method: 'get',
    url: '/member/cart'
  })
}
  • 在actions原有预留TODO位置获取列表   src/store/module/cart.js
    // 获取购物车列表
    findCartList (ctx) {
      return new Promise((resolve, reject) => {
        if (ctx.rootState.user.profile.token) {
+          // 登录 TODO
+          findCartList().then(data => {
+            ctx.commit('setCartList', data.result)
+            resolve()
+          })
        }
  • 退出登录需要清空购物车 src/components/app-navbar.vue
    // 退出登录
    // 1. 清空本地存储信息和vuex的用户信息
    // 2. 跳转登录
    const router = useRouter()
    const logout = () => {
      store.commit('user/setUser', {})
      // 清空购物车
+      store.commit('cart/setCartList', [])
      router.push('/login')
    }

登录后-加入购物车

目标:实现登陆后加入购物车。

大致步骤:

  • 编写加入购物车的API接口函数
  • 在actions原有预留TODO位置加入购物车

落地代码:

  • 编写加入购物车的API接口函数 src/api/cart.js
/**
 * 加入购物车
 * @param {String} skuId - 商品SKUID
 * @param {Integer} count - 商品数量
 * @returns Promise
 */
export const insertCart = ({ skuId, count }) => {
  return request({
    method: 'post',
    url: '/member/cart',
    data: { skuId, count }
  })
}
  • 在actions原有预留TODO位置加入购物车  src/store/module/cart.js
    // 加入购物车
    insertCart (ctx, goods) {
      // ctx.state 当前模块状态 ctx.rootState 根状态对象
      return new Promise((resolve, reject) => {
        if (ctx.rootState.user.profile.token) {
+          // 登录 TODO
+          insertCart(goods).then(() => {
+            return findCartList()
+          }).then((data) => {
+            ctx.commit('setCartList', data.result)
+            resolve()
+          })
        }

登录后-删除操作

目标:实现登陆后删除购物车商品操作(批量删除,清空无效)

大致步骤:

  • 编写删除购物车商品的API接口函数
  • 在actions原有预留TODO位置删除购物车商品

落地代码:

  • 编写删除购物车商品的API接口函数 src/api/cart.js
/**
 * 删除商品(支持批量删除)
 * @param {Array<string>} ids - skuId集合
 * @returns Promise
 */
export const deleteCart = (ids) => {
  return request({
    method: 'delete',
    url: '/member/cart',
    data: {ids}
  })
}
  • 在actions原有预留TODO位置删除购物车商品   src/store/module/cart.js
    // 删除购物车商品
    deleteCart (ctx, skuId) {
      return new Promise((resolve, reject) => {
        if (ctx.rootState.user.profile.token) {
+         // 登录 TODO
+         deleteCart([skuId]).then(() => {
+           return findCartList()
+         }).then((data) => {
+           ctx.commit('setCartList', data.result)
+           resolve()
+         })
        }

登录后-批量删除

目标:完成批量删除选中商品,完成清空失效的商品

大概步骤:

  • 完成cart.js模块中的批量删除actions的登录状态下逻辑

落的代码:

    // 批量删除选中商品
    batchDeleteCart (ctx, isClear) {
      return new Promise((resolve, reject) => {
        if (ctx.rootState.user.profile.token) {
+          // 登录 TODO
+          // 得到需要删除的商品列表(失效,选中)的skuId集合
+          const ids = ctx.getters[isClear ? 'invalidList' : 'selectedList'].map(item => item.skuId)
+          deleteCart(ids).then(() => {
+            return findCartList()
+          }).then((data) => {
+            ctx.commit('setCartList', data.result)
+            resolve()
+          })
        } else {

登录后-选中状态&修改数量

目的:实现登录后的选中操作。

大致步骤:

  • 编写修改购物车商品的API接口函数
  • 在actions原有预留TODO位置修改购物车商品

落地代码:

  • 编写修改购物车商品的API接口函数   src/api/cart.js
/**
 * 修改购物车商品的状态和数量
 * @param {String} goods.skuId - 商品sku
 * @param {Boolean} goods.selected - 选中状态
 * @param {Integer} goods.count - 商品数量
 * @returns Promise
 */
export const updateCart = (goods) => {
  return request({
    method: 'put',
    url: '/member/cart/' + goods.skuId, 
    data: goods
  })
}
  • 在actions原有预留TODO位置修改购物车商品  src/store/module/cart.js
    // 修改购物车商品
    updateCart (ctx, goods) {
      // goods 中:必须有skuId,其他想修改的属性 selected  count
      return new Promise((resolve, reject) => {
        if (ctx.rootState.user.profile.token) {
+          // 登录 TODO
+          updateCart(goods).then(() => {
+            return findCartList()
+          }).then((data) => {
+            ctx.commit('setCartList', data.result)
+            resolve()
+          })
        } else {

登录后-全选反选

目标:完成有效商品的全选与反选功能

大概步骤:

  • 准备全选与反选的API接口函数
  • 去完善actions,全选与反选的中的 登录 TODO 的地方

落的代码:

src/api/cart.js

/**
 * 全选反选
 * @param {Boolean} selected - 选中状态
 * @param {Array<string>} ids - 有效商品skuId集合
 * @returns Promise
 */
export const checkAllCart = ({ selected, ids }) => {
  return request({
    method: 'put',
    url: '/member/cart/selected',
    data: { selected, ids }
  })
}

src/store/modules/cart.js

    // 做有效商品的全选&反选
    checkAllCart (ctx, selected) {
      return new Promise((resolve, reject) => {
        if (ctx.rootState.user.profile.token) {
+          // 登录 TODO
+          const ids = ctx.getters.validList.map(item => item.skuId)
+          checkAllCart({ selected, ids }).then(() => {
+            return findCartList()
+          }).then((data) => {
+            ctx.commit('setCartList', data.result)
+            resolve()
+          })
        } else {

登录后-修改规格

目的:实现登录后的修改规格操作。

大致步骤:

  • 由于没有修改接口的接口。通过删除旧商品,插入新商品,完成修改规格。
  • 去完善actions,修改规格的 登录 TODO 的地方

落地代码:

  • 在actions原有预留TODO位置修改购物车商品规格  src/store/module/cart.js
    // 修改sku规格函数
    updateCartSku (ctx, { oldSkuId, newSku }) {
      return new Promise((resolve, reject) => {
        if (ctx.rootState.user.profile.token) {
+          // 登录 TODO
+          // 1. 获取原先商品的数量
+          // 2. 删除原先商品
+          // 3. 获取修改的skuId 和 原先商品数量 做一个加入购物车操作
+          // 4. 更新列表
+          const oldGoods = ctx.state.list.find(item => item.skuId === oldSkuId)
+          deleteCart([oldSkuId]).then(() => {
+            return insertCart({ skuId: newSku.skuId, count: oldGoods.count })
+          }).then(() => {
+            return findCartList()
+          }).then((data) => {
+            ctx.commit('setCartList', data.result)
+            resolve()
+          })
        }

下单结算

目的:去结算,未登录给确认框提示。

大致需求:

  • 绑定下单结算按钮指定处理函数

  • 函数中:

    • 判断是否选中有效商品。

    • 判断是否登录,给确认框提示,点击确认

    • 满足以上条件去填写订单(结算)页面。

  • member/xxx 的域名需要登录,所以做路由拦截。

落的代码:

  • 下单结束点击后逻辑 src/views/cart/index.vue
import Message from '@/components/library/Message'
// 跳转结算页面
const router = useRouter()
const goCheckout = () => {
  // 1. 判断是否选择有效商品
  // 2. 判断是否已经登录,未登录 弹窗提示
  // 3. 进行跳转 (需要做访问权限控制)
  if (store.getters['cart/selectedTotal'] === 0) return Message({ text: '至少选中一件商品才能结算' })
  if (!store.state.user.profile.token) {
    Confirm({ text: '下单结算需要登录,您是否去登录?' }).then(() => {
      // 点击确认
      router.push('/member/checkout')
    }).catch(e => {})
  } else {
     router.push('/member/checkout')
  }
}
return { checkOne, checkAll, deleteCart, batchDeleteCart, changeCount, updateCartSku, goCheckout }
<XtxButton type="primary" @click="goCheckout()">下单结算</XtxButton>
  • 路由拦截 src/router/index.js
import store from '@/store'
// 前置导航守卫
router.beforeEach((to, from, next) => {
  // 用户信息
  const { token } = store.state.user.profile
  // 跳转去member开头的地址却没有登录
  if (to.path.startsWith('/member') && !token) {
    return next({ path: '/login', query: { redirectUrl: to.fullPath } })
  }
  next()
})