危险

为之则易,不为则难

0%

05_小兔鲜

✔ 掌握 SKU 模块的业务逻辑。

✔ 掌握购物车模块的业务逻辑。

学会使用插件市场,下载并使用 SKU 组件,实现商品详情页规格展示和交互。

存货单位(SKU)

SKU 概念

存货单位(Stock Keeping Unit),库存管理的最小可用单元,通常称为“单品”。

SKU 常见于电商领域,对于前端工程师而言,更多关注 SKU 算法用户交互体验

插件市场

SKU 属于电商常见业务,插件市场有现成的 SKU 插件,我们在 uni-app 插件市场 下载并在项目中使用即可。

插件市场

下载 SKU 插件

经过综合评估,我们选择该SKU 插件,扫码可以体验。

插件市场

Q:如何评估第三方插件的质量?

A:查看插件的评分、评价、下载量、更新频率以及文档完整性,以确保插件具有良好的社区口碑、兼容性、性能和维护状况。

使用 SKU 插件

  1. 复制 vk-data-goods-sku-popupvk-data-input-number-box 到项目的根 components 目录下。

  2. 禁用 eslintvk-data-goods-sku-popup.vuevk-data-input-number-box.vue 组件。

1
2
3
4
<script>
/* eslint-disable */
// ...
</script>
  1. 复制例子代码到 pages/cart/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
<!-- 静态数据演示版本 适合任何后端 -->
<template>
<view class="app">
<button @click="openSkuPopup()">打开SKU组件</button>

<vk-data-goods-sku-popup
ref="skuPopup"
v-model="skuKey"
border-radius="20"
:localdata="goodsInfo"
:mode="skuMode"
@open="onOpenSkuPopup"
@close="SkuPopup"
@add-cart="addCart"
@buy-now="buyNow"
></vk-data-goods-sku-popup>

</view>
</template>

<script>
var that; // 当前页面对象
export default {
data() {
return {
// 是否打开SKU弹窗
skuKey:false,
// SKU弹窗模式
skuMode:1,
// 后端返回的商品信息
goodsInfo:{}
}
},
// 监听 - 页面每次【加载时】执行(如:前进)
onLoad(options) {
that = this;
that.init(options);
},
methods: {
// 初始化
init(options = {}){

},
// 获取商品信息,并打开sku弹出
openSkuPopup(){
/**
* 获取商品信息
* 这里可以看到每次打开SKU都会去重新请求商品信息,为的是每次打开SKU组件可以实时看到剩余库存
*/
// 此处写接口请求,并将返回的数据进行处理成goodsInfo的数据格式,
// goodsInfo是后端返回的数据
that.goodsInfo = {
"_id":"002",
"name": "迪奥香水",
"goods_thumb":"https://res.lancome.com.cn/resources/2020/9/11/15998112890781924_920X920.jpg?version=20200917220352530",
"sku_list": [
{
"_id": "004",
"goods_id": "002",
"goods_name": "迪奥香水",
"image": "https://res.lancome.com.cn/resources/2020/9/11/15998112890781924_920X920.jpg?version=20200917220352530",
"price": 19800,
"sku_name_arr": ["50ml/瓶"],
"stock": 100
},
{
"_id": "005",
"goods_id": "002",
"goods_name": "迪奥香水",
"image": "https://res.lancome.com.cn/resources/2020/9/11/15998112890781924_920X920.jpg?version=20200917220352530",
"price": 9800,
"sku_name_arr": ["70ml/瓶"],
"stock": 100
}
],
"spec_list": [
{
"list": [
{
"name": "20ml/瓶"
},
{
"name": "50ml/瓶"
},
{
"name": "70ml/瓶"
}
],
"name": "规格"
}
]
}
that.skuKey = true;
},
// sku组件 开始-----------------------------------------------------------
onOpenSkuPopup(){
console.log("监听 - 打开sku组件");
},
SkuPopup(){
console.log("监听 - 关闭sku组件");
},
// 加入购物车前的判断
addCartFn(obj){
let { selectShop } = obj;
// 模拟添加到购物车,请替换成你自己的添加到购物车逻辑
let res = {};
let name = selectShop.goods_name;
if(selectShop.sku_name != "默认"){
name += "-"+selectShop.sku_name_arr;
}
res.msg = `${name} 已添加到购物车`;
if(typeof obj.success == "function") obj.success(res);
},
// 加入购物车按钮
addCart(selectShop){
console.log("监听 - 加入购物车");
that.addCartFn({
selectShop : selectShop,
success : function(res){
// 实际业务时,请替换自己的加入购物车逻辑
that.toast(res.msg);
setTimeout(function() {
that.skuKey = false;
}, 300);
}
});
},
// 立即购买
buyNow(selectShop){
console.log("监听 - 立即购买");
that.addCartFn({
selectShop : selectShop,
success : function(res){
// 实际业务时,请替换自己的立即购买逻辑
that.toast("立即购买");
}
});
},
toast(msg){
uni.showToast({
title: msg,
icon:"none"
});
}
}
}
</script>

<style lang="scss" scoped>
.app {
padding: 30rpx;
font-size: 28rpx;
}
</style>

小结一下

插件类型问题

尽管该插件未采用 TS 开发,但作者提供了详细的插件文档,我们可以依据文档为插件添加 TS 类型声明文件,从而提高项目数据校验的安全性。

src/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup.d.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
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
import { Component } from '@uni-helper/uni-app-types'

/** SKU 弹出层 */
export type SkuPopup = Component<SkuPopupProps>

/** SKU 弹出层实例 */
export type SkuPopupInstance = InstanceType<SkuPopup>

/** SKU 弹出层属性 */
export type SkuPopupProps = {
/** 双向绑定,true 为打开组件,false 为关闭组件 */
modelValue: boolean
/** 商品信息本地数据源 */
localdata: SkuPopupLocaldata
/** 按钮模式 1:都显示 2:只显示购物车 3:只显示立即购买 */
mode?: 1 | 2 | 3
/** 该商品已抢完时的按钮文字 */
noStockText?: string
/** 库存文字 */
stockText?: string
/** 点击遮罩是否关闭组件 */
maskCloseAble?: boolean
/** 顶部圆角值 */
borderRadius?: string | number
/** 最小购买数量 */
minBuyNum?: number
/** 最大购买数量 */
maxBuyNum?: number
/** 每次点击后的数量 */
stepBuyNum?: number
/** 是否只能输入 step 的倍数 */
stepStrictly?: boolean
/** 是否隐藏库存的显示 */
hideStock?: false
/** 主题风格 */
theme?: 'default' | 'red-black' | 'black-white' | 'coffee' | 'green'
/** 默认金额会除以100(即100=1元),若设置为0,则不会除以100(即1=1元) */
amountType?: 1 | 0
/** 自定义获取商品信息的函数(已知支付宝不支持,支付宝请改用localdata属性) */
customAction?: () => void
/** 是否显示右上角关闭按钮 */
showClose?: boolean
/** 关闭按钮的图片地址 */
closeImage?: string
/** 价格的字体颜色 */
priceColor?: string
/** 立即购买 - 按钮的文字 */
buyNowText?: string
/** 立即购买 - 按钮的字体颜色 */
buyNowColor?: string
/** 立即购买 - 按钮的背景颜色 */
buyNowBackgroundColor?: string
/** 加入购物车 - 按钮的文字 */
addCartText?: string
/** 加入购物车 - 按钮的字体颜色 */
addCartColor?: string
/** 加入购物车 - 按钮的背景颜色 */
addCartBackgroundColor?: string
/** 商品缩略图背景颜色 */
goodsThumbBackgroundColor?: string
/** 样式 - 不可点击时,按钮的样式 */
disableStyle?: object
/** 样式 - 按钮点击时的样式 */
activedStyle?: object
/** 样式 - 按钮常态的样式 */
btnStyle?: object
/** 字段名 - 商品表id的字段名 */
goodsIdName?: string
/** 字段名 - sku表id的字段名 */
skuIdName?: string
/** 字段名 - 商品对应的sku列表的字段名 */
skuListName?: string
/** 字段名 - 商品规格名称的字段名 */
specListName?: string
/** 字段名 - sku库存的字段名 */
stockName?: string
/** 字段名 - sku组合路径的字段名 */
skuArrName?: string
/** 字段名 - 商品缩略图字段名(未选择sku时) */
goodsThumbName?: string
/** 被选中的值 */
selectArr?: string[]

/** 打开弹出层 */
onOpen: () => void
/** 关闭弹出层 */
onClose: () => void
/** 点击加入购物车时(需选择完SKU才会触发)*/
onAddCart: (event: SkuPopupEvent) => void
/** 点击立即购买时(需选择完SKU才会触发)*/
onBuyNow: (event: SkuPopupEvent) => void
}

/** 商品信息本地数据源 */
export type SkuPopupLocaldata = {
/** 商品 ID */
_id: string
/** 商品名称 */
name: string
/** 商品图片 */
goods_thumb: string
/** 商品规格列表 */
spec_list: SkuPopupSpecItem[]
/** 商品SKU列表 */
sku_list: SkuPopupSkuItem[]
}

/** 商品规格名称的集合 */
export type SkuPopupSpecItem = {
/** 规格名称 */
name: string
/** 规格集合 */
list: { name: string }[]
}

/** 商品SKU列表 */
export type SkuPopupSkuItem = {
/** SKU ID */
_id: string
/** 商品 ID */
goods_id: string
/** 商品名称 */
goods_name: string
/** 商品图片 */
image: string
/** SKU 价格 * 100, 注意:需要乘以 100 */
price: number
/** SKU 规格组成, 注意:需要与 spec_list 数组顺序对应 */
sku_name_arr: string[]
/** SKU 库存 */
stock: number
}

/** 当前选择的sku数据 */
export type SkuPopupEvent = SkuPopupSkuItem & {
/** 商品购买数量 */
buy_num: number
}

/** 全局组件类型声明 */
declare module '@vue/runtime-core' {
export interface GlobalComponents {
'vk-data-goods-sku-popup': SkuPopup
}
}

核心业务

渲染商品规格

pages/goods/goods.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
// #1
const isShowSku = ref(false)
const localdata = ref({} as SkuPopupLocaldata)

const getGoodsByIdData = async () => {
const res = await getGoodsByIdAPI(query.id)
goods.value = res.result
// #3
localdata.value = {
_id: res.result.id,
name: res.result.name,
goods_thumb: res.result.mainPictures[0],
spec_list: res.result.specs.map((v) => ({
name: v.name,
list: v.values
})),
sku_list: res.result.skus.map((v) => ({
_id: v.id,
goods_id: res.result.id,
goods_name: res.result.name,
image: v.picture,
price: v.price * 100, // 插件要求
stock: v.inventory,
sku_name_arr: v.specs.map((vv) => vv.valueName)
}))
}
}
1
2
3
4
<!-- #2 `v-model` 双向绑定,显示/隐藏组件,`localdata` 绑定商品 `SKU` 数据来源 -->
<vk-data-goods-sku-popup v-model="isShowSku" :localdata="localdata" />
<scroll-view scroll-y class="viewport"></scroll-view>
</vk-data-goods-sku-popup>

小结一下

打开弹窗交互

SKU 弹窗的按钮有三种形式。

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
<script setup lang="ts">
// 按钮模式
enum SkuMode {
Both = 1,
Cart = 2,
Buy = 3,
}
const mode = ref<SkuMode>(SkuMode.Cart)
// 打开SKU弹窗修改按钮模式
const openSkuPopup = (val: SkuMode) => {
// 显示SKU弹窗
isShowSku.value = true
// 修改按钮模式
mode.value = val
}
</script>

<template>
<!-- SKU弹窗组件 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="localdata"
:mode="mode"
add-cart-background-color="#FFA868"
buy-now-background-color="#27BA9B"
/>

<!-- 显示两个按钮 -->
<view @tap="openSkuPopup(SkuMode.Both)" class="item arrow">请选择商品规格</view>
<!-- 显示一个按钮 -->
<view @tap="openSkuPopup(SkuMode.Cart)" class="addcart"> 加入购物车 </view>
<view @tap="openSkuPopup(SkuMode.Buy)" class="payment"> 立即购买 </view>
</template>

小结一下

渲染被选中的值

  1. 通过 ref 获取组件实例。

  2. 通过 computed 计算出被选中的值,渲染到界面中。

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
<script setup lang="ts">
import type { SkuPopupInstance } from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'
// SKU 组件实例
const skuPopupRef = ref<SkuPopupInstance>()
// 计算被选中的值
const selectArrText = computed(() => {
return skuPopupRef.value?.selectArr?.join(' ').trim() || '请选择商品规格'
})
</script>

<template>
<!-- SKU弹窗组件 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="localdata"
:mode="mode"
add-cart-background-color="#FFA868"
buy-now-background-color="#27BA9B"
ref="skuPopupRef"
:actived-style="{
color: '#27BA9B',
borderColor: '#27BA9B',
backgroundColor: '#E9F8F5',
}"
/>
<!-- 操作面板 -->
<view class="action">
<view @tap="openSkuPopup(SkuMode.Both)" class="item arrow">
<text class="label">选择</text>
<text class="text ellipsis"> {{ selectArrText }} </text>
</view>
</view>
</template>

至此,已经完成 SKU 组件的交互,接下来进入到购物车模块,并实现加入购物车功能。

小结一下