危险

为之则易,不为则难

0%

11_购物车

今日目标

✔ 掌握登录和非登陆状态下购物车的处理逻辑。

购物车功能分析

image-20220226233037218

  • 购物车的各种操作都会有两种状态的区分,登录和未登录。

  • 所有操作都封装到 Pinia 中,组件只需要触发 actions 函数。

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

    1. 已登录,通过调用接口去服务端操作,响应成功会通过 actions 修改 Pinia 中的数据即可。

    2. 未登录时,通过 actions 修改 Pinia 中的数据即可, Pinia 实现持久化,同步保存在本地。

头部购物车-已登录

准备购物车 Store

  1. 为购物车业务定义专属的 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
  1. 合并购物车模块,store/index.ts
1
2
3
4
5
6
import useCartStore from './modules/cart'
export default function useStore() {
return {
cart: useCartStore(),
}
}

加入购物车

校验

  1. 给加入购物车按钮注册点击事件,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 = () => {
// 如果没有 skuId 就打回去
if (!currentSkuId.value) {
return Message.warning('请选择完整信息')
}
console.log('加入购物车')
}
  1. 当全选了就传递对应的 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', '')
}
  1. 父组件传递过来的 skuId 到 currentSkuId,views/goods/index.vue
1
2
3
4
5
6
7
8
9
10
11
const changeSku = (skuId: string) => {
// 1. 根据接收到的 skuId 找到对应的 sku
// 2. 修改商品的价钱库存
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
}
}

内容

image-20220227200120916

  1. 封装 action,store/modules/cart.ts
1
2
3
async addCart(data: { skuId: string; count: number }) {
await request.post('/member/cart', data)
}
  1. 点击按钮调用接口,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('加入购物车成功')
}
  1. 统一携带 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)
}
)

头部购物车

结构

  1. 头部购物车结构和样式,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">&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>

<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>
  1. 使用头部购物车组件,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'
// #1
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>
<!-- !#2 -->
<AppHeaderCart />
</div>
</header>
</template>

渲染

  1. 提供购物车的数据类型,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
// discount?: any;
isCollect: boolean
postFee: number
}
  1. 新增 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
  1. 提供计算属性,store/modules/cart.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
getters: {
// 计算有效商品列表 isEffective = true filter
effectiveList(): CartItem[] {
return this.list.filter((item) => item.stock > 0 && item.isEffective)
},
// 有效商品总数量 把effctiveList中的每一项的count叠加起来
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)
}
},
  1. 渲染头部购物车,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">&yen;{{ 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>&yen;{{ 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()
}

删除功能实现

  1. 准备删除的 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()
}
  1. 注册事件,views/layout/components/app-header-cart.vue
1
<i class="iconfont icon-close-new" @click="cart.deleteCart([item.skuId])"></i>
  1. 小优化,views/layout/components/app-header-cart.vue

购物车没有数据的时候或者当前页就在购物车页面,移入时不需要显示头部购物车列表。

1
<div class="layer" v-if="cart.effectiveList.length && $route.path !== '/cart'"></div>

购物车页面-已登录

基本结构

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

image-20220227204058941

  1. 购物车页面和样式,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>&yen;200.00</p>
</td>
<td class="tc">
<XtxNumbox :model-value="1" />
</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>
</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>
  1. 准备路由,router/index.ts
1
2
3
4
{
path: '/cart',
component: () => import('@/views/cart/index.vue')
}
  1. 点击购物车按钮跳转,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>&yen;{{ item.nowPrice }}</p>
</td>
<td class="tc">
<XtxNumbox :model-value="item.count" :max="item.stock" />
</td>
<td class="tc">
<p class="f16 red">&yen;{{ (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>

删除操作

  1. 拷贝素材中的 confirm 组件到 views/components 文件夹。

  2. 给删除按钮绑定事件触发删除的 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. 购物车删除完毕之后使用元素占位。
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>

单选操作

image-20220227211742692

  1. 封装修改商品的接口,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()
}
  1. 组件中注册点击事件,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>

全选切换

  1. 封装接口,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()
},
  1. 页面中调用,views/cart/index.vue
1
2
3
<th width="120">
<XtxCheckbox :model-value="cart.isAllSelected" @update:model-value="cart.updateCartAllSelected(!cart.isAllSelected)"> 全选 </XtxCheckbox>
</th>

操作栏渲染

  1. 提供计算属性,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)
}
},
  1. 渲染操作栏,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>

退出登录-清空购物车

需求:退出后清除本地购物车数据。

  1. 准备 action,store/modules/cart.ts
1
2
3
clearCart() {
this.list = []
}
  1. 完善 logout,store/modules/user.ts
1
2
3
4
5
6
7
logout() {
this.profile = {} as Profile
removeProfile()
// #mark
const { cart } = useStore()
cart.clearCart()
}

购物车操作-未登录

区分未登录和已登录

  1. cart 模块增加计算属性 isLogin,store/modules/cart.ts
1
2
3
4
5
6
getters: {
isLogin(): boolean {
const { user } = useStore()
return !!user.profile.token
}
},
  1. 修改添加购物车和获取购物车的逻辑。
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('获取本地购物车')
}
}

加入购物车

  1. 商品详情页,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('加入购物车成功')
}
  1. 修改加入购物车的逻辑,store/modules/cart.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// #1
async addCart(data: CartItem) {
if (this.isLogin) {
// #2
const { skuId, count } = data
// #3
await request.post('/member/cart', { skuId, count })
this.getCartList()
} else {
// #4
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。

  1. 安装 Pinia 持久化存储插件,在模块中开启插件功能即可。
1
yarn add pinia-plugin-persistedstate
  1. 配置插件,main.ts
1
2
3
4
5
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
  1. 开启自动持久化插件,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 {
// 遍历发送请求, 校验更新sku商品的库存和价格, 是否有效
this.list.forEach(async (cartItem) => {
const { skuId } = cartItem
// 根据 skuId 获取最新商品信息
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
})
}
},

登录后合并购物车

目标:登录后把本地的购物车数据合并到后端服务。

  1. 编写合并购物车的 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()
}
  1. 登录完成功后,调用合并购物车 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('/')
}