购物车

购物车功能分析

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

image.png

总结:

  • 购物车的各种操作都会有两种状态的区分,但是不会在组件中去区分。
  • 而是在封装在vuex中的actions中去区分,在组件上只需调用actions即可。
  • 在actions中通过user信息去区分登录状态
    • 未登录,通过mutations修改vuex中的数据即可,vuex已经实现持久化,会同步保持在本地。
    • 已登录,通过api接口去服务端操作,响应成功后通过mutations修改vuex中的数据即可,它也会同步在本地。
  • 不管何种操作何种状态返回一个promise,然后组件能够判断操作是否完毕是否成功,再而去做其他事情。

注意:

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

加入购物车-本地

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

大致步骤:

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

落地代码:

  • vuex中的修改数据,获取数据 src/store/module/cart.js
  1. // 本地:id skuId name picture price nowPrice count attrsText selected stock isEffective
  2. // 线上:比上面多 isCollect 有用 discount 无用 两项项信息
  3. mutations: {
  4. // 把商品添加到购物车
  5. insertCart (state, goods) {
  6. // goods参数表示商品详情信息(包含我们需要的相关参数)
  7. // 如果多次加入同一件商品,那么需要进行商品数量的累加操作
  8. // 根据skuId判断是否是同一件商品
  9. const index = state.list.findIndex(item => item.skuId === goods.skuId)
  10. if (index !== -1) {
  11. // 有这件商品,进行商品数量的累加操作
  12. goods.count = state.list[index].count + 1
  13. // 删除原来的商品
  14. state.list.splice(index, 1)
  15. }
  16. state.list.unshift(goods)
  17. }
  18. },
  19. actions: {
  20. // action方法触发后的默认返回值是Promise实例对象
  21. insertCart (context, goods) {
  22. // 判断当前用户是否处于登录状态
  23. // context.rootState可以获取所有的模块的状态
  24. const token = context.rootState.user.profiletoken
  25. if (token) {
  26. // 已经登录,调用接口加入购物车
  27. } else {
  28. // 尚未登录,添加商品到本地购物车
  29. context.commit('insertCart', goods)
  30. }
  31. }
  32. }
  • 商品详情点击加入购物车 src/views/goods/index.vue
  1. setup () {
  2. const goodsDetail = useGoods()
  3. // sku改变时候触发
  4. const skuInfo = (sku) => {
  5. if (sku.skuId) {
  6. goodsDetail.value.price = sku.price
  7. goodsDetail.value.oldPrice = sku.oldPrice
  8. goodsDetail.value.inventory = sku.inventory
  9. // 记录当前选中的sku信息
  10. + currentSku.value = sku
  11. } else {
  12. + currentSku.value = null
  13. }
  14. }
  15. // 选择的数量
  16. const num = ref(1)
  17. // 加入购物车逻辑
  18. + // 获取当前组件的实例
  19. + const instance = getCurrentInstance()
  20. + // 加入购物车
  21. + const insertCart = () => {
  22. + // 判断是否选中了规格
  23. + if (!currentSku.value) {
  24. + // 进行消息提示
  25. + return Massage({ type: 'warn', text: '请选择规格' })
  26. + }
  27. + // 判断是否还有库存
  28. + // 判断是否还有库存
  29. + if (num.value > goodsDetail.inventory) {
  30. + // 库存不够,进行提示
  31. + // instance.proxy表示组件的实例对象
  32. + return instance.proxy.$message({ type: 'warn', text: '库存不够' })
  33. + // return Massage({ type: 'warn', text: '库存不够' })
  34. + }
  35. + // 触发加入购物车的action
  36. + // 加入购物车的信息 id skuId name picture price nowPrice count attrsText selected stock isEffective
  37. + const goodsInfo = {
  38. + // 当前商品的id
  39. + id: goodsDetail.value.id,
  40. + // 加入购物车的商品的skuId
  41. + skuId: currentSku.value.id,
  42. + // 商品名称
  43. + name: goodsDetail.value.name,
  44. + // 商品的第一张图片,在购物进行显示
  45. + picture: goodsDetail.value.mainPictures[0],
  46. + // 默认价格
  47. + price: currentSku.value.price,
  48. + // 当前价格
  49. + nowPrice: currentSku.value.price,
  50. + // 购买的数量
  51. + count: num.value,
  52. + // 规格参数
  53. + attrsText: currentSku.value.attrsText,
  54. + // 当前商品是否选中:默认设置为选中,用于支付
  55. + selected: true,
  56. + // 当前库存
  57. + stock: currentSku.value.inventory,
  58. + // 当前商品是否有效:默认值有效,这个商品依然在售
  59. + isEffective: true
  60. + }
  61. + store.dispatch('cart/insertCart', goodsInfo)
  62. + }

总结:

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

回顾

  • QQ登录
    • 配置QQ登录的基本环境:配置DNS;配置webpack
    • 实现QQ登录图标按钮:点击后跳转到QQ授权登录的页面
    • 准备回跳的页面:基本布局和路由配置
    • 完善回跳页面的基本布局
      1. 完善用户的登录信息:尚未注册小兔鲜账号
      2. 绑定手机号:已经注册小兔鲜账号,但是尚未绑定手机号
      3. 扫码后直接登录:已经注册小兔鲜账号,并且已经绑定手机号
  • 购物车
    • 熟悉核心的流程:本地购物车;登录后远程购物车
    • 添加本地购物车:
      1. 实现添加购物车的mutation方法
      2. 实现添加购物车的action方法
      3. 触发添加的动作
  • 基于vue3提供的方法访问组件的实例对象

头部购物车-基础布局

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

08-购物车 - 图2

大致步骤:

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

落的代码:

  • 新建组件 src/views/layout/components/top-header-cart.vue
  1. <template>
  2. <div class="cart">
  3. <a class="curr" href="#"> <i class="iconfont icon-cart"></i><em>2</em> </a>
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. name: 'AppHeaderCart'
  9. }
  10. </script>
  11. <style scoped lang="less">
  12. .cart {
  13. width: 50px;
  14. .curr {
  15. height: 32px;
  16. line-height: 32px;
  17. text-align: center;
  18. position: relative;
  19. display: block;
  20. .icon-cart {
  21. font-size: 22px;
  22. }
  23. em {
  24. font-style: normal;
  25. position: absolute;
  26. right: 0;
  27. top: 0;
  28. padding: 1px 6px;
  29. line-height: 1;
  30. background: @helpColor;
  31. color: #fff;
  32. font-size: 12px;
  33. border-radius: 10px;
  34. font-family: Arial;
  35. }
  36. }
  37. }
  38. </style>
  • src/views/layout/components/top-header.vue文件中的图标代码迁移到购物车组件中。
  1. <div class="search">
  2. <i class="iconfont icon-search"></i>
  3. <input type="text" placeholder="搜一搜">
  4. </div>
  5. + <TopHeaderCart />
  6. </div>
  7. </header>
  8. </template>
  9. <script>
  10. import AppHeaderNav from './app-header-nav'
  11. +import TopHeaderCart from './top-header-cart.vue'
  12. export default {
  13. name: 'AppHeader',
  14. + components: { AppHeaderNav, TopHeaderCart }
  15. }
  16. </script>
  • 基础布局和弹出效果 src/components/app-header-cart.vue
  1. <template>
  2. <div class="cart">
  3. <a class="curr" href="javascript:;">
  4. <i class="iconfont icon-cart"></i><em>2</em>
  5. </a>
  6. <div class="layer">
  7. <div class="list">
  8. <div class="item" v-for="i in 4" :key="i">
  9. <RouterLink to="">
  10. <img src="https://yanxuan-item.nosdn.127.net/ead73130f3dbdb3cabe1c7b0f4fd3d28.png" alt="">
  11. <div class="center">
  12. <p class="name ellipsis-2">和手足干裂说拜拜 ingrams手足皲裂修复霜</p>
  13. <p class="attr ellipsis">颜色:修复绿瓶 容量:150ml</p>
  14. </div>
  15. <div class="right">
  16. <p class="price">&yen;45.00</p>
  17. <p class="count">x2</p>
  18. </div>
  19. </RouterLink>
  20. <i class="iconfont icon-close-new"></i>
  21. </div>
  22. </div>
  23. <div class="foot">
  24. <div class="total">
  25. <p>共 3 件商品</p>
  26. <p>&yen;135.00</p>
  27. </div>
  28. <XtxButton type="plain">去购物车结算</XtxButton>
  29. </div>
  30. </div>
  31. </div>
  32. </template>
  33. <script>
  34. export default {
  35. name: 'TopHeaderCart'
  36. }
  37. </script>
  38. <style scoped lang="less">
  39. .cart {
  40. width: 50px;
  41. position: relative;
  42. z-index: 600;
  43. .curr {
  44. height: 32px;
  45. line-height: 32px;
  46. text-align: center;
  47. position: relative;
  48. display: block;
  49. .icon-cart {
  50. font-size: 22px;
  51. }
  52. em {
  53. font-style: normal;
  54. position: absolute;
  55. right: 0;
  56. top: 0;
  57. padding: 1px 6px;
  58. line-height: 1;
  59. background: @helpColor;
  60. color: #fff;
  61. font-size: 12px;
  62. border-radius: 10px;
  63. font-family: Arial;
  64. }
  65. }
  66. &:hover {
  67. .layer {
  68. opacity: 1;
  69. transform: none
  70. }
  71. }
  72. .layer {
  73. opacity: 0;
  74. transition: all .4s .2s;
  75. transform: translateY(-200px) scale(1, 0);
  76. width: 400px;
  77. height: 400px;
  78. position: absolute;
  79. top: 50px;
  80. right: 0;
  81. box-shadow: 0 0 10px rgba(0,0,0,0.2);
  82. background: #fff;
  83. border-radius: 4px;
  84. padding-top: 10px;
  85. &::before {
  86. content: "";
  87. position: absolute;
  88. right: 14px;
  89. top: -10px;
  90. width: 20px;
  91. height: 20px;
  92. background: #fff;
  93. transform: scale(0.6,1) rotate(45deg);
  94. box-shadow: -3px -3px 5px rgba(0,0,0,0.1);
  95. }
  96. .foot {
  97. position: absolute;
  98. left: 0;
  99. bottom: 0;
  100. height: 70px;
  101. width: 100%;
  102. padding: 10px;
  103. display: flex;
  104. justify-content: space-between;
  105. background: #f8f8f8;
  106. align-items: center;
  107. .total {
  108. padding-left: 10px;
  109. color: #999;
  110. p {
  111. &:last-child {
  112. font-size: 18px;
  113. color: @priceColor;
  114. }
  115. }
  116. }
  117. }
  118. }
  119. .list {
  120. height: 310px;
  121. overflow: auto;
  122. padding: 0 10px;
  123. &::-webkit-scrollbar{
  124. width:10px;
  125. height:10px;
  126. }
  127. &::-webkit-scrollbar-track{
  128. background: #f8f8f8;
  129. border-radius: 2px;
  130. }
  131. &::-webkit-scrollbar-thumb{
  132. background: #eee;
  133. border-radius:10px;
  134. }
  135. &::-webkit-scrollbar-thumb:hover{
  136. background: #ccc;
  137. }
  138. .item {
  139. border-bottom: 1px solid #f5f5f5;
  140. padding: 10px 0;
  141. position: relative;
  142. i {
  143. position: absolute;
  144. bottom: 38px;
  145. right: 0;
  146. opacity: 0;
  147. color: #666;
  148. transition: all .5s;
  149. }
  150. &:hover {
  151. i {
  152. opacity: 1;
  153. cursor: pointer;
  154. }
  155. }
  156. a {
  157. display: flex;
  158. align-items: center;
  159. img {
  160. height: 80px;
  161. width: 80px;
  162. }
  163. .center {
  164. padding: 0 10px;
  165. width: 200px;
  166. .name {
  167. font-size: 16px;
  168. }
  169. .attr {
  170. color: #999;
  171. padding-top: 5px;
  172. }
  173. }
  174. .right {
  175. width: 100px;
  176. padding-right: 20px;
  177. text-align: center;
  178. .price {
  179. font-size: 16px;
  180. color: @priceColor;
  181. }
  182. .count {
  183. color: #999;
  184. margin-top: 5px;
  185. font-size: 16px;
  186. }
  187. }
  188. }
  189. }
  190. }
  191. }
  192. </style>

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

  • 使用getters得到有效商品列表和期种件数 src/store/modules/cart.js
  1. getters: {
  2. // 有效商品列表
  3. validList (state) {
  4. return state.list.filter(item => item.stock > 0 && item.isEffective)
  5. },
  6. // 有效商品件数
  7. validTotal (state, getters) {
  8. return getters.validList.reduce((p, c) => p + c.count, 0)
  9. },
  10. // 有效商品总金额
  11. validAmount (state, getters) {
  12. return getters.validList.reduce((p, c) => p + c.nowPrice * 100 * c.count, 0) / 100
  13. }
  14. }
  • 渲染头部购物车信息
  1. <template>
  2. <div class="cart">
  3. <a class="curr" href="javascript:;">
  4. <i class="iconfont icon-cart"></i><em>{{$store.getters['cart/validTotal']}}</em>
  5. </a>
  6. <div class="layer">
  7. <div class="list">
  8. <div class="item" v-for="item in $store.getters['cart/validList']" :key="item.skuId">
  9. <RouterLink to="">
  10. <img :src="item.picture" alt="">
  11. <div class="center">
  12. <p class="name ellipsis-2">{{item.name}}</p>
  13. <p class="attr ellipsis">{{item.attrsText}}</p>
  14. </div>
  15. <div class="right">
  16. <p class="price">&yen;{{item.nowPrice}}</p>
  17. <p class="count">x{{item.count}}</p>
  18. </div>
  19. </RouterLink>
  20. <i class="iconfont icon-close-new"></i>
  21. </div>
  22. </div>
  23. <div class="foot">
  24. <div class="total">
  25. <p>共 {{$store.getters['cart/validTotal']}} 件商品</p>
  26. <p>&yen;{{$store.getters['cart/validAmount']}}</p>
  27. </div>
  28. <XtxButton type="plain">去购物车结算</XtxButton>
  29. </div>
  30. </div>
  31. </div>
  32. </template>

总结:

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

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

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

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

大致步骤:

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

落的代码:

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

总结:

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

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

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

大致步骤:

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

落的代码:

  • vuex的mutations和actions代码 src/store/module/cart.js
  1. mutations: {
  2. // ... 省略
  3. // 删除顶部购物车单独的一件商品
  4. deleteCart (state, skuId) {
  5. const index = state.list.findIndex(item => item.skuId === skuId)
  6. if (index !== -1) {
  7. // 删除数组中一个元素
  8. state.list.splice(index, 1)
  9. }
  10. },
  11. },
  1. actions: {
  2. // ... 省略
  3. // 删除顶部购物车的单件商品
  4. deleteCart (context, skuId) {
  5. const token = context.rootState.user.profiletoken
  6. if (token) {
  7. // 已经登录
  8. } else {
  9. // 未登录
  10. context.commit('deleteCart', skuId)
  11. }
  12. },
  13. }
  • 头部组件实现删除逻辑 src/layout/views/components/top-header-cart.vue
  1. + 购物车无商品不显示弹出层,并且不是在购物车页面
  2. +<div class="layer" v-if="$store.getters['cart/validTotal']&&$route.path!=='/cart'">
  1. + 绑定点击事件传入skuId
  2. +<i @click="deleteCart(item.skuId)" class="iconfont icon-close-new"></i>
  1. import { useStore } from 'vuex'
  2. import Massage from '@/components/library/Message.js'
  3. export default {
  4. name: 'TopHeaderCart',
  5. setup () {
  6. // 触发更新购物车商品信息的action
  7. const store = useStore()
  8. store.dispatch('cart/updateCart')
  9. // 删除购物车商品
  10. const deleteCart = (skuId) => {
  11. store.dispatch('cart/deleteCart', skuId).then(() => {
  12. // 删除成功
  13. Massage({ type: 'success', text: '删除购物车商品成功' })
  14. }).catch(() => {
  15. Massage({ type: 'error', text: '删除购物车商品失败' })
  16. })
  17. }
  18. return { deleteCart }
  19. }
  20. }

总结:

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

购物车页面-基础布局

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

image.png

大致步骤:

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

落的代码:

  • 跳转功能 src/layout/views/components/top-header-cart.vue
  1. <RouterLink to="/cart" class="curr">
  2. <i class="iconfont icon-cart"></i><em>{{$store.getters['cart/validTotal']}}</em>
  3. </RouterLink>
  1. <div class="item" v-for="item in $store.getters['cart/validList']" :key="item.skuId">
  2. + <RouterLink :to="`/product/${item.id}`">
  3. <img :src="item.picture" alt="">
  • 组件与路由 src/views/cart/index.vue
  1. <template>
  2. <div class="xtx-cart-page">
  3. <div class="container">
  4. <XtxBread>
  5. <XtxBreadItem to="/">首页</XtxBreadItem>
  6. <XtxBreadItem>购物车</XtxBreadItem>
  7. </XtxBread>
  8. <div class="cart">
  9. <table>
  10. <thead>
  11. <tr>
  12. <th width="120"><XtxCheckbox>全选</XtxCheckbox></th>
  13. <th width="400">商品信息</th>
  14. <th width="220">单价</th>
  15. <th width="180">数量</th>
  16. <th width="180">小计</th>
  17. <th width="140">操作</th>
  18. </tr>
  19. </thead>
  20. <!-- 有效商品 -->
  21. <tbody>
  22. <tr v-for="i in 3" :key="i">
  23. <td><XtxCheckbox /></td>
  24. <td>
  25. <div class="goods">
  26. <RouterLink to="/"><img src="https://yanxuan-item.nosdn.127.net/13ab302f8f2c954d873f03be36f8fb03.png" alt=""></RouterLink>
  27. <div>
  28. <p class="name ellipsis">和手足干裂说拜拜 ingrams手足皲裂修复霜</p>
  29. <!-- 选择规格组件 -->
  30. </div>
  31. </div>
  32. </td>
  33. <td class="tc">
  34. <p>&yen;200.00</p>
  35. <p>比加入时降价 <span class="red">&yen;20.00</span></p>
  36. </td>
  37. <td class="tc">
  38. <XtxNumbox />
  39. </td>
  40. <td class="tc"><p class="f16 red">&yen;200.00</p></td>
  41. <td class="tc">
  42. <p><a href="javascript:;">移入收藏夹</a></p>
  43. <p><a class="green" href="javascript:;">删除</a></p>
  44. <p><a href="javascript:;">找相似</a></p>
  45. </td>
  46. </tr>
  47. </tbody>
  48. <!-- 无效商品 -->
  49. <tbody>
  50. <tr><td colspan="6"><h3 class="tit">失效商品</h3></td></tr>
  51. <tr v-for="i in 3" :key="i">
  52. <td><XtxCheckbox style="color:#eee;" /></td>
  53. <td>
  54. <div class="goods">
  55. <RouterLink to="/"><img src="https://yanxuan-item.nosdn.127.net/13ab302f8f2c954d873f03be36f8fb03.png" alt=""></RouterLink>
  56. <div>
  57. <p class="name ellipsis">和手足干裂说拜拜 ingrams手足皲裂修复霜</p>
  58. <p class="attr">颜色:粉色 尺寸:14cm 产地:中国</p>
  59. </div>
  60. </div>
  61. </td>
  62. <td class="tc"><p>&yen;200.00</p></td>
  63. <td class="tc">1</td>
  64. <td class="tc"><p>&yen;200.00</p></td>
  65. <td class="tc">
  66. <p><a class="green" href="javascript:;">删除</a></p>
  67. <p><a href="javascript:;">找相似</a></p>
  68. </td>
  69. </tr>
  70. </tbody>
  71. </table>
  72. </div>
  73. <!-- 操作栏 -->
  74. <div class="action">
  75. <div class="batch">
  76. <XtxCheckbox>全选</XtxCheckbox>
  77. <a href="javascript:;">删除商品</a>
  78. <a href="javascript:;">移入收藏夹</a>
  79. <a href="javascript:;">清空失效商品</a>
  80. </div>
  81. <div class="total">
  82. 共 7 件商品,已选择 2 件,商品合计:
  83. <span class="red">¥400</span>
  84. <XtxButton type="primary">下单结算</XtxButton>
  85. </div>
  86. </div>
  87. <!-- 猜你喜欢 -->
  88. <GoodRelevant />
  89. </div>
  90. </div>
  91. </template>
  92. <script>
  93. import GoodRelevant from '@/views/goods/components/goods-relevant'
  94. export default {
  95. name: 'XtxCartPage',
  96. components: { GoodRelevant }
  97. }
  98. </script>
  99. <style scoped lang="less">
  100. .tc {
  101. text-align: center;
  102. .xtx-numbox {
  103. margin: 0 auto;
  104. width: 120px;
  105. }
  106. }
  107. .red {
  108. color: @priceColor;
  109. }
  110. .green {
  111. color: @xtxColor
  112. }
  113. .f16 {
  114. font-size: 16px;
  115. }
  116. .goods {
  117. display: flex;
  118. align-items: center;
  119. img {
  120. width: 100px;
  121. height: 100px;
  122. }
  123. > div {
  124. width: 280px;
  125. font-size: 16px;
  126. padding-left: 10px;
  127. .attr {
  128. font-size: 14px;
  129. color: #999;
  130. }
  131. }
  132. }
  133. .action {
  134. display: flex;
  135. background: #fff;
  136. margin-top: 20px;
  137. height: 80px;
  138. align-items: center;
  139. font-size: 16px;
  140. justify-content: space-between;
  141. padding: 0 30px;
  142. .xtx-checkbox {
  143. color: #999;
  144. }
  145. .batch {
  146. a {
  147. margin-left: 20px;
  148. }
  149. }
  150. .red {
  151. font-size: 18px;
  152. margin-right: 20px;
  153. font-weight: bold;
  154. }
  155. }
  156. .tit {
  157. color: #666;
  158. font-size: 16px;
  159. font-weight: normal;
  160. line-height: 50px;
  161. }
  162. .xtx-cart-page {
  163. .cart {
  164. background: #fff;
  165. color: #666;
  166. table {
  167. border-spacing: 0;
  168. border-collapse: collapse;
  169. line-height: 24px;
  170. th,td{
  171. padding: 10px;
  172. border-bottom: 1px solid #f5f5f5;
  173. &:first-child {
  174. text-align: left;
  175. padding-left: 30px;
  176. color: #999;
  177. }
  178. }
  179. th {
  180. font-size: 16px;
  181. font-weight: normal;
  182. line-height: 50px;
  183. }
  184. }
  185. }
  186. }
  187. </style>

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

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

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

大致步骤:

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

落的代码:

  • 准备数据 src/store/module/cart.js
  1. // 选中商品的总价格
  2. selectedTotal (state, getters) {
  3. return getters.selectedList.reduce((total, item) => total + item.nowPrice * item.count, 0)
  4. },
  5. // 选中的商品的数量
  6. selectedCount (state, getters) {
  7. return getters.selectedList.reduce((count, item) => count + item.count, 0)
  8. },
  9. // 选中的商品列表(需要付款)
  10. selectedList (state) {
  11. return state.list.filter(item => item.selected)
  12. },
  13. // 无效的商品列表: 没有库存的;状态是失效的
  14. invalidList (state) {
  15. return state.list.filter(item => item.stock === 0 || !item.isEffective)
  16. },
  • 渲染列表
  1. <div class="cart">
  2. <table>
  3. <thead>
  4. <tr>
  5. + <th width="120"><XtxCheckbox :modelValue="$store.getters['cart/isCheckAll']">全选</XtxCheckbox></th>
  6. <th width="400">商品信息</th>
  7. <th width="220">单价</th>
  8. <th width="180">数量</th>
  9. <th width="180">小计</th>
  10. <th width="140">操作</th>
  11. </tr>
  12. </thead>
  13. <!-- 有效商品 -->
  14. <tbody>
  15. + <tr v-for="item in $store.getters['cart/validList']" :key="item.skuId">
  16. + <td><XtxCheckbox :modelValue="item.selected" /></td>
  17. <td>
  18. <div class="goods">
  19. + <RouterLink :to="`/product/${item.id}`">
  20. + <img :src="item.picture" alt="">
  21. </RouterLink>
  22. <div>
  23. + <p class="name ellipsis">{{item.name}}</p>
  24. <!-- 选择规格组件 -->
  25. + <p class="attr">{{item.attrsText}}</p>
  26. </div>
  27. </div>
  28. </td>
  29. <td class="tc">
  30. + <p>&yen;{{item.nowPrice}}</p>
  31. </td>
  32. <td class="tc">
  33. + <XtxNumbox :modelValue="item.count" />
  34. </td>
  35. + <td class="tc"><p class="f16 red">&yen;{{item.nowPrice*100*item.count/100}}</p></td>
  36. <td class="tc">
  37. <p><a href="javascript:;">移入收藏夹</a></p>
  38. <p><a class="green" href="javascript:;">删除</a></p>
  39. <p><a href="javascript:;">找相似</a></p>
  40. </td>
  41. </tr>
  42. </tbody>
  43. <!-- 无效商品 -->
  44. <tbody v-if="$store.getters['cart/invalidList'].length>0">
  45. <tr><td colspan="6"><h3 class="tit">失效商品</h3></td></tr>
  46. <tr v-for="item in $store.getters['cart/validList']" :key="item.skuId">
  47. <td><XtxCheckbox style="color:#eee;" /></td>
  48. <td>
  49. <div class="goods">
  50. <RouterLink :to="`/product/${item.id}`">
  51. <img :src="item.picture" alt="">
  52. </RouterLink>
  53. <div>
  54. <p class="name ellipsis">{{item.name}}</p>
  55. <p class="attr">{{item.attrsText}}</p>
  56. </div>
  57. </div>
  58. </td>
  59. <td class="tc"><p>&yen;{{item.nowPrice}}</p></td>
  60. <td class="tc">{{item.count}}</td>
  61. <td class="tc"><p>&yen;{{item.nowPrice*100*item.count/100}}</p></td>
  62. <td class="tc">
  63. <p><a class="green" href="javascript:;">删除</a></p>
  64. <p><a href="javascript:;">找相似</a></p>
  65. </td>
  66. </tr>
  67. </tbody>
  68. </table>
  69. </div>
  70. <!-- 操作栏 -->
  71. <div class="action">
  72. <div class="batch">
  73. + <XtxCheckbox :modelValue="$store.getters['cart/isCheckAll']">全选</XtxCheckbox>
  74. <a href="javascript:;">删除商品</a>
  75. <a href="javascript:;">移入收藏夹</a>
  76. <a href="javascript:;">清空失效商品</a>
  77. </div>
  78. <div class="total">
  79. + 共 {{$store.getters['cart/validTotal']}} 件商品,已选择 {{$store.getters['cart/selectedTotal']}} 件,商品合计:
  80. + <span class="red">¥{{$store.getters['cart/selectedAmount']}}</span>
  81. <XtxButton type="primary">下单结算</XtxButton>
  82. </div>
  83. </div>

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

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

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

大致步骤:

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

落的代码:

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

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

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

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

大致步骤:

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

落的代码

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

总结:

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

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

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

大致步骤:

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

落的代码:

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

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

  1. <template>
  2. <div class="cart-none">
  3. <img src="@/assets/images/none.png" alt="" />
  4. <p>购物车内暂时没有商品</p>
  5. <div class="btn">
  6. <XtxButton type="primary" @click="$router.push('/')">继续逛逛</XtxButton>
  7. </div>
  8. </div>
  9. </template>
  10. <script>
  11. export default {
  12. name: 'CartNone'
  13. }
  14. </script>
  15. <style scoped lang="less">
  16. .cart-none {
  17. text-align: center;
  18. padding: 150px 0;
  19. background: #fff;
  20. img {
  21. width: 180px;
  22. }
  23. p {
  24. color: #999999;
  25. padding: 20px 0;
  26. }
  27. }
  28. </style>

使用 src/views/cart/index.vue

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

总结:

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

购物车页面-确认框组件

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

image.png

大致步骤:

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

落地代码:

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

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

  1. <template>
  2. <div class="xtx-confirm">
  3. <div class="wrapper">
  4. <div class="header">
  5. <h3>温馨提示</h3>
  6. <a href="JavaScript:;" class="iconfont icon-close-new"></a>
  7. </div>
  8. <div class="body">
  9. <i class="iconfont icon-warning"></i>
  10. <span>您确认从购物车删除该商品吗?</span>
  11. </div>
  12. <div class="footer">
  13. <XtxButton size="mini" type="gray">取消</XtxButton>
  14. <XtxButton size="mini" type="primary">确认</XtxButton>
  15. </div>
  16. </div>
  17. </div>
  18. </template>
  19. <script>
  20. export default {
  21. name: 'XtxConfirm'
  22. }
  23. </script>
  24. <style scoped lang="less">
  25. .xtx-confirm {
  26. position: fixed;
  27. left: 0;
  28. top: 0;
  29. width: 100%;
  30. height: 100%;
  31. z-index: 8888;
  32. background: rgba(0,0,0,.5);
  33. .wrapper {
  34. width: 400px;
  35. background: #fff;
  36. border-radius: 4px;
  37. position: absolute;
  38. top: 50%;
  39. left: 50%;
  40. transform: translate(-50%,-50%);
  41. .header,.footer {
  42. height: 50px;
  43. line-height: 50px;
  44. padding: 0 20px;
  45. }
  46. .body {
  47. padding: 20px 40px;
  48. font-size: 16px;
  49. .icon-warning {
  50. color: @priceColor;
  51. margin-right: 3px;
  52. font-size: 16px;
  53. }
  54. }
  55. .footer {
  56. text-align: right;
  57. .xtx-button {
  58. margin-left: 20px;
  59. }
  60. }
  61. .header {
  62. position: relative;
  63. h3 {
  64. font-weight: normal;
  65. font-size: 18px;
  66. }
  67. a {
  68. position: absolute;
  69. right: 15px;
  70. top: 15px;
  71. font-size: 20px;
  72. width: 20px;
  73. height: 20px;
  74. line-height: 20px;
  75. text-align: center;
  76. color: #999;
  77. &:hover {
  78. color: #666;
  79. }
  80. }
  81. }
  82. }
  83. }
  84. </style>

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

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

image.png

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

  1. import { createVNode, render } from 'vue'
  2. import XtxConfirm from './xtx-confirm'
  3. // 准备div
  4. const div = document.createElement('div')
  5. div.setAttribute('class', 'xtx-confirm-container')
  6. document.body.appendChild(div)
  7. // 该函数渲染XtxConfirm组件,标题和文本
  8. // 函数的返回值是promise对象
  9. export default ({ title, text }) => {
  10. return new Promise((resolve, reject) => {
  11. const submitCallback = () => {
  12. render(null, div)
  13. resolve()
  14. }
  15. const cancelCallback = () => {
  16. render(null, div)
  17. reject(new Error('点击取消'))
  18. }
  19. // 1. 渲染组件
  20. // 2. 点击确认按钮,触发resolve同时销毁组件
  21. // 3. 点击取消按钮,触发reject同时销毁组件
  22. const vnode = createVNode(XtxConfirm, { title, text, submitCallback, cancelCallback })
  23. render(vnode, div)
  24. })
  25. }

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

  1. <template>
  2. <div class="xtx-confirm" :class="{fade}">
  3. <div class="wrapper" :class="{fade}">
  4. <div class="header">
  5. <h3>{{title}}</h3>
  6. <a @click="cancelCallback()" href="JavaScript:;" class="iconfont icon-close-new"></a>
  7. </div>
  8. <div class="body">
  9. <i class="iconfont icon-warning"></i>
  10. <span>{{text}}</span>
  11. </div>
  12. <div class="footer">
  13. <XtxButton @click="cancelCallback()" size="mini" type="gray">取消</XtxButton>
  14. <XtxButton @click="submitCallback()" size="mini" type="primary">确认</XtxButton>
  15. </div>
  16. </div>
  17. </div>
  18. </template>
  19. <script>
  20. // 当前组件不是在APP下进行渲染,无法使用APP下的环境(全局组件,全局指令,原型属性函数)
  21. import XtxButton from '@/components/library/xtx-button'
  22. import { onMounted, ref } from 'vue'
  23. export default {
  24. name: 'XtxConfirm',
  25. components: { XtxButton },
  26. props: {
  27. title: {
  28. type: String,
  29. default: '温馨提示'
  30. },
  31. text: {
  32. type: String,
  33. default: ''
  34. },
  35. submitCallback: {
  36. type: Function
  37. },
  38. cancelCallback: {
  39. type: Function
  40. }
  41. },
  42. setup () {
  43. const fade = ref(false)
  44. onMounted(() => {
  45. // 当元素渲染完毕立即过渡的动画不会触发
  46. setTimeout(() => {
  47. fade.value = true
  48. }, 0)
  49. })
  50. return { fade }
  51. }
  52. }
  53. </script>
  54. <style scoped lang="less">
  55. .xtx-confirm {
  56. position: fixed;
  57. left: 0;
  58. top: 0;
  59. width: 100%;
  60. height: 100%;
  61. z-index: 8888;
  62. background: rgba(0,0,0,0);
  63. &.fade {
  64. transition: all 0.4s;
  65. background: rgba(0,0,0,.5);
  66. }
  67. .wrapper {
  68. width: 400px;
  69. background: #fff;
  70. border-radius: 4px;
  71. position: absolute;
  72. top: 50%;
  73. left: 50%;
  74. transform: translate(-50%,-60%);
  75. opacity: 0;
  76. &.fade {
  77. transition: all 0.4s;
  78. transform: translate(-50%,-50%);
  79. opacity: 1;
  80. }
  81. .header,.footer {
  82. height: 50px;
  83. line-height: 50px;
  84. padding: 0 20px;
  85. }
  86. .body {
  87. padding: 20px 40px;
  88. font-size: 16px;
  89. .icon-warning {
  90. color: @priceColor;
  91. margin-right: 3px;
  92. font-size: 16px;
  93. }
  94. }
  95. .footer {
  96. text-align: right;
  97. .xtx-button {
  98. margin-left: 20px;
  99. }
  100. }
  101. .header {
  102. position: relative;
  103. h3 {
  104. font-weight: normal;
  105. font-size: 18px;
  106. }
  107. a {
  108. position: absolute;
  109. right: 15px;
  110. top: 15px;
  111. font-size: 20px;
  112. width: 20px;
  113. height: 20px;
  114. line-height: 20px;
  115. text-align: center;
  116. color: #999;
  117. &:hover {
  118. color: #666;
  119. }
  120. }
  121. }
  122. }
  123. }
  124. </style>

总结:

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

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

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

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

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

测试:

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

总结:

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

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

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

大致的步骤:

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

落地代码:

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

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

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

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

大致思路:

  • 去修改批量删除的actions让它适用于两个场景

    • 批量删除选中的
    • 批量删除失效的 isClear


    落地代码:

  • 绑定清空无效商品点击事件指定处理函数,调用actions进行删除。 src/views/cart/index.vue

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

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

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

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

大致的步骤:

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

落的代码:

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

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

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

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

image.png

大致步骤:

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

落的代码:

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

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

  1. <template>
  2. <div class="cart-sku">
  3. <div class="attrs">
  4. <span class="ellipsis">颜色:粉色 尺寸:14cm 产地:中国</span>
  5. <i class="iconfont icon-angle-down"></i>
  6. </div>
  7. <div class="layer">
  8. <div class="loading"></div>
  9. </div>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. name: 'CartSku'
  15. }
  16. </script>
  17. <style scoped lang="less">
  18. .cart-sku {
  19. height: 28px;
  20. border: 1px solid #f5f5f5;
  21. padding: 0 6px;
  22. position: relative;
  23. margin-top: 10px;
  24. display:inline-block;
  25. .attrs {
  26. line-height: 24px;
  27. display: flex;
  28. span {
  29. max-width: 230px;
  30. font-size: 14px;
  31. color: #999;
  32. }
  33. i {
  34. margin-left: 5px;
  35. font-size: 14px;
  36. }
  37. }
  38. .layer {
  39. position: absolute;
  40. left: -1px;
  41. top: 40px;
  42. z-index: 10;
  43. width: 400px;
  44. border: 1px solid @xtxColor;
  45. box-shadow: 2px 2px 4px lighten(@xtxColor,50%);
  46. background: #fff;
  47. border-radius: 4px;
  48. font-size: 14px;
  49. padding: 20px;
  50. &::before {
  51. content: "";
  52. width: 12px;
  53. height: 12px;
  54. border-left: 1px solid @xtxColor;
  55. border-top: 1px solid @xtxColor;
  56. position: absolute;
  57. left: 12px;
  58. top: -8px;
  59. background: #fff;
  60. transform: scale(.8,1) rotate(45deg);
  61. }
  62. .loading {
  63. height: 224px;
  64. background: url(../../../assets/images/loading.gif) no-repeat center;
  65. }
  66. }
  67. }
  68. </style>

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

  1. +import CartSku from './components/cart-sku'
  2. export default {
  3. name: 'XtxCartPage',
  4. + components: { GoodRelevant, CartNone, CartSku },
  1. <div>
  2. <p class="name ellipsis">{{item.name}}</p>
  3. <!-- 选择规格组件 -->
  4. + <CartSku :attrsText='item.attrsText' />
  5. </div>
  • 2.完成展开收起操作 src/views/cart/components/cart-sku.vue
  1. <div class="cart-sku" ref="target">
  2. <div class="attrs" @click="toggle()">
  3. <span class="ellipsis">{{attrsText}}</span>
  1. <script>
  2. import { ref } from 'vue'
  3. import { getSpecsAndSkus } from '@/api/product.js'
  4. import { onClickOutside } from '@vueuse/core'
  5. import GoodsSku from '@/views/goods/components/goods-sku.vue'
  6. export default {
  7. name: 'CartSku',
  8. components: { GoodsSku },
  9. props: {
  10. attrsText: {
  11. type: String,
  12. default: ''
  13. },
  14. skuId: {
  15. type: String,
  16. required: true
  17. }
  18. },
  19. setup (props) {
  20. // 根据SKUId获取的商品规格数据
  21. const goods = ref(null)
  22. const visible = ref(false)
  23. const target = ref(null)
  24. onClickOutside(target, () => {
  25. // 点击target指定的DOM之外的区域,触发回调函数:隐藏弹窗
  26. visible.value = false
  27. goods.value = null
  28. })
  29. // 控制显示和隐藏弹窗
  30. const toggle = () => {
  31. visible.value = !visible.value
  32. if (!visible.value) {
  33. // 关闭弹窗,重置goods数据
  34. goods.value = null
  35. }
  36. getSpecsAndSkus(props.skuId).then(ret => {
  37. goods.value = ret.result
  38. })
  39. }
  40. return { visible, toggle, target, goods }
  41. }
  42. }
  43. </script>
  • 3.展开的时候根据skuId得到商品信息(specs,skus)渲染商品规格。

接口API src/api/product.js

  1. // 获取商品的specs和skus
  2. export const getSpecsAndSkus = (skuId) => {
  3. return request({
  4. method: 'get',
  5. url: `/goods/sku/${skuId}`
  6. })
  7. }

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

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

总结:

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

登录后-合并购物车

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

image.png

大致步骤:

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

落地代码:

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

登录后-商品列表

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

大致步骤:

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

落地代码:

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

登录后-加入购物车

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

大致步骤:

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

落地代码:

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

登录后-删除操作

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

大致步骤:

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

落地代码:

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

登录后-批量删除

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

大概步骤:

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

落的代码:

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

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

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

大致步骤:

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

落地代码:

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

登录后-全选反选

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

大概步骤:

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

落的代码:

src/api/cart.js

  1. /**
  2. * 全选反选
  3. * @param {Boolean} selected - 选中状态
  4. * @param {Array<string>} ids - 有效商品skuId集合
  5. * @returns Promise
  6. */
  7. export const checkAllCart = ({ selected, ids }) => {
  8. return request({
  9. method: 'put',
  10. url: '/member/cart/selected',
  11. data: { selected, ids }
  12. })
  13. }

src/store/modules/cart.js

  1. // 做有效商品的全选&反选
  2. checkAllCart (ctx, selected) {
  3. return new Promise((resolve, reject) => {
  4. if (ctx.rootState.user.profile.token) {
  5. + // 登录 TODO
  6. + const ids = ctx.getters.validList.map(item => item.skuId)
  7. + checkAllCart({ selected, ids }).then(() => {
  8. + return findCartList()
  9. + }).then((data) => {
  10. + ctx.commit('setCartList', data.result)
  11. + resolve()
  12. + })
  13. } else {

登录后-修改规格

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

大致步骤:

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

落地代码:

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

下单结算

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

大致需求:

  • 绑定下单结算按钮指定处理函数
  • 函数中:
    • 判断是否选中有效商品。
    • 判断是否登录,给确认框提示,点击确认
    • 满足以上条件去填写订单(结算)页面。
  • member/xxx 的域名需要登录,所以做路由拦截。

落的代码:

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