今日目标
✔ 掌握登录和非登陆状态下购物车的处理逻辑。
购物车功能分析
购物车的各种操作都会有两种状态的区分,登录和未登录。
所有操作都封装到 Pinia
中,组件只需要触发 actions
函数。
在 actions 中通过 user 信息去区分登录状态。
已登录,通过调用接口去服务端操作,响应成功会通过 actions
修改 Pinia
中的数据即可。
未登录时,通过 actions
修改 Pinia
中的数据即可, Pinia
实现持久化,同步保存在本地。
头部购物车-已登录
准备购物车 Store
- 为购物车业务定义专属的 Store,多个组件都要用到购物车数据,
store/modules/cart.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { defineStore } from 'pinia'
const useCartStore = defineStore('cart', { state() { return { list: [], } }, getters: {}, actions: {}, })
export default useCartStore
|
- 合并购物车模块,
store/index.ts
。
1 2 3 4 5 6
| import useCartStore from './modules/cart' export default function useStore() { return { cart: useCartStore(), } }
|
加入购物车
校验
- 给加入购物车按钮注册点击事件,
views/goods/index.vue
。
1
| <XtxButton type="primary" style="margin-top:20px;" @click="addCart"> 加入购物车 </XtxButton>
|
1 2 3 4 5 6 7 8
| const currentSkuId = ref('') const addCart = () => { if (!currentSkuId.value) { return Message.warning('请选择完整信息') } console.log('加入购物车') }
|
- 当全选了就传递对应的 skuId 到父组件,否则传空字符串,
views/goods/components/goods-sku.vue
。
1 2 3 4 5 6 7
| if (selected.length === props.goods.specs.length) { const key = selected.join('★') const [skuId] = pathMap[key] emit('changeSku', skuId) } else { emit('changeSku', '') }
|
- 父组件传递过来的 skuId 到 currentSkuId,
views/goods/index.vue
。
1 2 3 4 5 6 7 8 9 10 11
| const changeSku = (skuId: string) => { currentSkuId.value = skuId const sku = info.value.skus.find((item) => item.id === skuId) if (sku) { info.value.price = sku.price info.value.oldPrice = sku.oldPrice info.value.inventory = sku.inventory } }
|
内容
- 封装 action,
store/modules/cart.ts
。
1 2 3
| async addCart(data: { skuId: string; count: number }) { await request.post('/member/cart', data) }
|
- 点击按钮调用接口,
views/goods/index.vue
。
1 2 3 4 5 6 7 8 9 10 11
| const count = ref(1) const addCart = async () => { if (!currentSkuId.value) { return Message.warning('请选择完整信息') } await cart.addCart({ skuId: currentSkuId.value, count: count.value, }) Message.success('加入购物车成功') }
|
- 统一携带 Token,
utils/request.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| instance.interceptors.request.use( function (config) { const { user } = useStore() if (user.profile.token) { config.headers!.Authorization = `Bearer ${user.profile.token}` } return config }, function (error) { return Promise.reject(error) } )
|
头部购物车
结构
- 头部购物车结构和样式,
views/layout/components/app-header-cart.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
| <script setup lang="ts" name="AppHeaderCart"></script>
<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">¥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>¥135.00</p> </div> <XtxButton type="plain">去购物车结算</XtxButton> </div> </div> </div> </template>
<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 0.2s 0.1s; 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 0.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>
|
- 使用头部购物车组件,
views/layout/components/app-header.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script lang="ts" setup name="AppHeader"> import AppHeaderNavVue from './app-header-nav.vue' import AppHeaderCart from './app-header-cart.vue' </script>
<template> <header class="app-header"> <div class="container"> <h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1> <AppHeaderNavVue /> <div class="search"> <i class="iconfont icon-search"></i> <input type="text" placeholder="搜一搜" /> </div> <AppHeaderCart /> </div> </header> </template>
|
渲染
- 提供购物车的数据类型,
types/cart.d.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export interface CartItem { id: string skuId: string name: string attrsText: string picture: string price: string nowPrice: string nowOriginalPrice: string selected: boolean stock: number count: number isEffective: boolean isCollect: boolean postFee: number }
|
- 新增 action,用于获取购物车列表数据,
store/modules/cart.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { CartItem } from '@/types/cart' import { IApiRes } from '@/types/data' import request from '@/utils/request' import { defineStore } from 'pinia'
const useCartStore = defineStore('cart', { state() { return { list: [] as CartItem[], } }, actions: { async addCart(data: { skuId: string; count: number }) { await request.post('/member/cart', data) }, async getCartList() { const res = await request.get<IApiRes<CartItem[]>>('/member/cart') this.list = res.data.result }, }, })
export default useCartStore
|
- 提供计算属性,
store/modules/cart.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| getters: { effectiveList(): CartItem[] { return this.list.filter((item) => item.stock > 0 && item.isEffective) }, effectiveListCounts(): number { return this.effectiveList.reduce((sum, item) => sum + item.count, 0) }, effectiveListPrice(): string { return this.effectiveList .reduce((sum, item) => sum + item.count * Number(item.nowPrice), 0) .toFixed(2) } },
|
- 渲染头部购物车,
views/layout/components/app-header-cart.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <script setup lang="ts" name="AppHeaderCart"> import useStore from '@/store' const { cart } = useStore() cart.getCartList() </script>
<template> <div class="cart"> <a class="curr" href="javascript:;"> <i class="iconfont icon-cart"></i><em>{{ cart.effectiveListCounts }}</em> </a> <div class="layer"> <div class="list"> <div class="item" v-for="item in cart.effectiveList" :key="item.skuId"> <RouterLink :to="`/goods/${item.id}`"> <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">¥{{ item.price }}</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>共 {{ cart.effectiveListCounts }} 件商品</p> <p>¥{{ cart.effectiveListPrice }}</p> </div> <XtxButton type="plain">去购物车结算</XtxButton> </div> </div> </div> </template>
|
加入购物车优化
添加购物车成功后,需要重新渲染,store/modules/cart.ts
。
1 2 3 4
| async addCart(data: { skuId: string; count: number }) { await request.post('/member/cart', data) this.getCartList() }
|
删除功能实现
- 准备删除的 action,
store/modules/cart.ts
。
1 2 3 4 5 6
| async deleteCart(skuIds: string[]) { await request.delete('/member/cart', { data: { ids: skuIds } }) this.getCartList() }
|
- 注册事件,
views/layout/components/app-header-cart.vue
。
1
| <i class="iconfont icon-close-new" @click="cart.deleteCart([item.skuId])"></i>
|
- 小优化,
views/layout/components/app-header-cart.vue
。
购物车没有数据的时候或者当前页就在购物车页面,移入时不需要显示头部购物车列表。
1
| <div class="layer" v-if="cart.effectiveList.length && $route.path !== '/cart'"></div>
|
购物车页面-已登录
基本结构
目标:完成购物车组件基础布局和路由配置与跳转链接。
- 购物车页面和样式,
views/cart/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
| <script setup lang="ts" name="Cart"> </script>
<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> <p class="attr">商品规格</p> </div> </div> </td> <td class="tc"> <p>¥200.00</p> </td> <td class="tc"> <XtxNumbox :model-value="1" /> </td> <td class="tc"><p class="f16 red">¥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> </table> </div> <div class="action"> <div class="batch"></div> <div class="total"> 共 7 件有效商品,已选择 2 件,商品合计: <span class="red">¥400</span> <XtxButton type="primary">下单结算</XtxButton> </div> </div> </div> </div> </template>
<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>
|
- 准备路由,
router/index.ts
。
1 2 3 4
| { path: '/cart', component: () => import('@/views/cart/index.vue') }
|
- 点击购物车按钮跳转,
views/layout/components/app-header-cart.vue
。
1 2 3
| <RouterLink to="/cart" class="curr"> <i class="iconfont icon-cart"></i><em>{{ cart.effectiveListCounts }}</em> </RouterLink>
<XtxButton @click="$router.push('/cart')" type="plain">去购物车结算</XtxButton>
|
渲染列表
views/cart/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <script setup lang="ts" name="Cart"> import useStore from '@/store'
const { cart } = useStore() cart.getCartList() </script> <template> <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="item in cart.effectiveList" :key="item.skuId"> <td><XtxCheckbox :model-value="item.selected" /></td> <td> <div class="goods"> <RouterLink :to="`/goods/${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>¥{{ item.nowPrice }}</p> </td> <td class="tc"> <XtxNumbox :model-value="item.count" :max="item.stock" /> </td> <td class="tc"> <p class="f16 red">¥{{ (Number(item.nowPrice) * item.count).toFixed(2) }}</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> </table> </template>
|
删除操作
拷贝素材中的 confirm 组件到 views/components
文件夹。
给删除按钮绑定事件触发删除的 action,views/cart/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script setup lang="ts" name="Cart"> import useStore from '@/store' import Confirm from '@/components/confirm' import Message from '@/components/message'
const { cart } = useStore() const delCart = async (skuIds: string[]) => { await Confirm({ title: '温馨提示', text: '你确定要删除吗?', }) await cart.deleteCart(skuIds) Message.success('删除成功') } </script> <template> <p> <a @click="delCart([item.skuId])" class="green" href="javascript:;">删除</a> </p> </template>
|
- 购物车删除完毕之后使用元素占位。
1 2 3 4 5 6 7 8 9 10 11 12
| <tr v-if="cart.effectiveList.length === 0"> <td colspan="6"> <div class="cart-none" style="text-align: center"> <img src="@/assets/images/none.png" alt="" /> <p>购物车内暂时没有商品</p> <div class="btn" style="margin: 20px"> <XtxButton type="primary"> 继续逛逛 </XtxButton> </div> </div> </td> </tr>
|
单选操作
- 封装修改商品的接口,
store/modules/cart.ts
。
1 2 3 4 5
| async updateCart(skuId: string,data: { selected?: boolean; count?: number }) { await request.put(`/member/cart/${skuId}`, data) this.getCartList() }
|
- 组件中注册点击事件,
views/cart/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup lang="ts" name="Cart"> import useStore from '@/store' const { cart } = useStore() cart.getCartList() const changeSelected = async (skuId: string, checked: boolean) => { await cart.updateCart(skuId, { selected: checked, }) Message.success('修改状态成功') } </script>
<template> <XtxCheckbox :model-value="item.selected" @update:modelValue="changeSelected(item.skuId, $event)" /> </template>
|
修改数量
views/cart/index.vue
1 2 3 4 5 6 7 8 9 10 11 12
| <script setup lang="ts" name="Cart"> const changeCount = (skuId: string, count: number) => { cart.updateCart(skuId, { count, }) } </script> <template> <td class="tc"> <XtxNumbox :model-value="item.count" :max="item.stock" @update:model-value="changeCount(item.skuId, $event)" /> </td> </template>
|
全选切换
- 封装接口,
store/modules/cart.ts
。
1 2 3 4 5 6
| getters: { isAllSelected(): boolean { return (this.effectiveList.length !== 0 && this.effectiveList.every((item) => item.selected)) } },
|
1 2 3 4 5 6 7 8
| async updateCartAllSelected(isSelected: boolean) { await request.put('/member/cart/selected', { selected: isSelected, }) this.getCartList() },
|
- 页面中调用,
views/cart/index.vue
。
1 2 3
| <th width="120"> <XtxCheckbox :model-value="cart.isAllSelected" @update:model-value="cart.updateCartAllSelected(!cart.isAllSelected)"> 全选 </XtxCheckbox> </th>
|
操作栏渲染
- 提供计算属性,
store/modules/cart.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| getters: { selectedList(): CartItem[] { return this.effectiveList.filter((item) => item.selected) }, selectedListCounts(): number { return this.selectedList.reduce((sum, item) => sum + item.count, 0) }, selectedListPrice(): string { return this.selectedList .reduce((sum, item) => sum + item.count * Number(item.nowPrice), 0) .toFixed(2) } },
|
- 渲染操作栏,
views/cart/index.vue
。
1 2 3 4 5 6 7 8 9
| <div class="action" v-if="cart.effectiveList.length > 0"> <div class="batch"></div> <div class="total"> 共 {{ cart.effectiveListCounts }} 件有效商品,已选择 {{ cart.selectedListCounts }} 件,商品合计: <span class="red">¥{{ cart.selectedListPrice }}</span> <XtxButton type="primary">下单结算</XtxButton> </div> </div>
|
退出登录-清空购物车
需求:退出后清除本地购物车数据。
- 准备 action,
store/modules/cart.ts
。
1 2 3
| clearCart() { this.list = [] }
|
- 完善 logout,
store/modules/user.ts
。
1 2 3 4 5 6 7
| logout() { this.profile = {} as Profile removeProfile() const { cart } = useStore() cart.clearCart() }
|
购物车操作-未登录
区分未登录和已登录
- cart 模块增加计算属性 isLogin,
store/modules/cart.ts
。
1 2 3 4 5 6
| getters: { isLogin(): boolean { const { user } = useStore() return !!user.profile.token } },
|
- 修改添加购物车和获取购物车的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| async addCart(data: { skuId: string; count: number }) { if (this.isLogin) { await request.post('/member/cart', data) this.getCartList() } else { console.log('本地提那家购物车') } },
async getCartList() { if (this.isLogin) { const res = await request.get<IApiRes<CartItem[]>>('/member/cart') this.list = res.data.result } else { console.log('获取本地购物车') } }
|
加入购物车
- 商品详情页,
views/goods/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const addCart = async () => { if (!currentSkuId.value) { return Message.warning('请选择完整信息') } const sku = info.value.skus.find((item) => item.id === currentSkuId.value)! const attrsText = sku.specs.map((item) => item.name + ':' + item.valueName).join(' ') await cart.addCart({ skuId: currentSkuId.value, count: count.value, id: info.value.id, name: info.value.name, picture: info.value.mainPictures[0], price: info.value.price, attrsText, selected: true, nowPrice: info.value.price, stock: info.value.inventory, isEffective: true, } as CartItem) Message.success('加入购物车成功') }
|
- 修改加入购物车的逻辑,
store/modules/cart.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| async addCart(data: CartItem) { if (this.isLogin) { const { skuId, count } = data await request.post('/member/cart', { skuId, count }) this.getCartList() } else { const goods = this.list.find((item) => item.skuId === data.skuId) if (goods) { goods.count += data.count } else { this.list.unshift(data) } } },
|
数据持久化
目标:实现 Pinia
购物车列表的持久化存储,https://github.com/prazdevs/pinia-plugin-persistedstate。
- 安装
Pinia
持久化存储插件,在模块中开启插件功能即可。
1
| yarn add pinia-plugin-persistedstate
|
- 配置插件,
main.ts
。
1 2 3 4 5
| import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia() pinia.use(piniaPluginPersistedstate)
|
- 开启自动持久化插件,
store/modules/cart.ts
。
1 2 3 4 5 6
| const useCartStore = defineStore({ id: 'cart', persist: { key: '@_@', }, })
|
删除购物车
store/modules/cart.ts
1 2 3 4 5 6 7 8 9 10 11 12
| async delCart(skuIds: string[]) { if (this.isLogin) { await request.delete('/member/cart', { data: { ids: skuIds } }) this.getCartList() } else { this.list = this.list.filter((item) => !skuIds.includes(item.skuId)) } },
|
选中和数量
实现商品选中状态的切换和修改商品数量,store/modules/cart.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| async updateCart(skuId: string, data: { selected?: boolean; count?: number }) { if (this.isLogin) { await request.put(`/member/cart/${skuId}`, data) this.getCartList() } else { const goods = this.effectiveList.find((item) => item.skuId === skuId) if (data.selected !== undefined) { goods!.selected = data.selected } if (data.count !== undefined) { goods!.count = data.count } } },
|
全选切换操作
目标:实现商品选中状态的切换,store/modules/cart.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| async updateCartAllSelected(isSelected: boolean) { if (this.isLogin) { await request.put('/member/cart/selected', { selected: isSelected, }) this.getCartList() } else { this.list.forEach((item) => { item.selected = isSelected }) } },
|
主动更新本地购物车信息
优化:页面刷新时,需要获取服务端最新的购物车信息并同步到本地,store/modules/cart.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| async getCartList() { if (this.isLogin) { const res = await request.get<ApiRes<CartItem[]>>('/member/cart') this.list = res.data.result } else { this.list.forEach(async (cartItem) => { const { skuId } = cartItem const res = await request.get<ApiRes<CartItem>>( `/goods/stock/${skuId}` ) const lastCartInfo = res.data.result cartItem.nowPrice = lastCartInfo.nowPrice cartItem.stock = lastCartInfo.stock cartItem.isEffective = lastCartInfo.isEffective }) } },
|
登录后合并购物车
目标:登录后把本地的购物车数据合并到后端服务。
- 编写合并购物车的 actions 函数,
store/modules/cart.ts
。
1 2 3 4 5 6 7 8 9 10 11
| async mergeLocalCart() { const data = this.list.map(({ skuId, selected, count }) => ({ skuId, selected, count })) await request.post('/member/cart/merge', data) this.getCartList() }
|
- 登录完成功后,调用合并购物车
actions
函数,views/login/components/login-form.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const login = async () => { const res = await validate() if (type.value === 'account') { if (res.errors.account || res.errors.password || res.errors.isAgree) return await user.login(account.value, password.value) } else { if (res.errors.mobile || res.errors.code || res.errors.isAgree) return await user.mobileLogin(mobile.value, code.value) } cart.mergeLocalCart() Message.success('登录成功') router.push('/') }
|