SKU 模块 - 下载 SKU 插件
DCloud 插件市场 是 uni-app 官方插件生态集中地,有数千款插件
使用SKU插件:
组件安装到自己的项目
注意事项:项目进行 git 提交时会校验文件,可添加 /* eslint-disable */ 禁用检查
<script>
/* eslint-disable */
// 省略组件源代码
</script>
打开购物车弹框,渲染商品信息 goods.vue
<!-- SKU 弹窗组件 -->
<vk-data-goods-sku-popup v-model="isShowSku" :localdata="localdata" />
// 是否显示 SKU 组件
const isShowSku = ref(false)
// 商品信息
const localdata = ref({} as SkuPopupLocaldata)
// 渲染商品信息
// 获取商品详情信息
const goods = ref<GoodsResult>()
const getGoodsByIdData = async () => {
const res = await getGoodsByIdAPI(query.id)
goods.value = res.result
// SKU 组件所需格式
localdata.value = {
_id: res.result.id,
name: res.result.name,
goods_thumb: res.result.mainPictures[0],
spec_list: res.result.specs.map((v) => {
return {
name: v.name,
list: v.values,
}
}),
sku_list: res.result.skus.map((v) => {
return {
_id: v.id,
goods_id: res.result.id,
goods_name: res.result.name,
image: v.picture,
price: v.price * 100, // 注意:需要乘以 100
stock: v.inventory,
sku_name_arr: v.specs.map((vv) => vv.valueName),
}
}),
}
}
打开sku 弹窗 渲染商品
打开SKU弹窗 =》 设置按钮模式 =》 微调组件样式
<!-- SKU 弹窗组件 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="localdata"
:mode="mode"
add-cart-background-color="#ffa868"
buy-now-background-color="#27ba98"
:active-style="{
color: '#27ba9b',
borderColor: '#27ba9b',
backgroundCColor: '#e9f8f5',
}"
/>
// mode 设置按钮模式
// add-cart-background-color 设置即入购物车按钮背景色
// buy-now-background-color 设置立即购买按钮背景色
// :active-style 选择商品规格时的激活样式
// 按钮模式 枚举
enum SkuMode {
Both = 1, // 购物车和立即购买都显示
Cart = 2, // 只显示购物车
Buy = 3, // 只显示立即购买
}
const mode = ref<SkuMode>(SkuMode.Both)
// 打开sku 弹窗 修改按钮模式
const openSkuPopup = (val: SkuMode) => {
// 显示sku组件
isShowSku.value = true
// 修改按钮模式
mode.value = val
}
<view class="item arrow" @tap="openSkuPopup(SkuMode.Both)">
<text class="label">选择</text>
<text class="text ellipsis"> 请选择商品规格 </text>
</view>
<view class="buttons">
<view class="addcart" @tap="openSkuPopup(SkuMode.Cart)"> 加入购物车 </view>
<view class="buynow" @tap="openSkuPopup(SkuMode.Buy)"> 立即购买 </view>
</view>
加入购物车事件 加入购物车在商品详情页面 goods.vue
<!-- 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',
}"
@add-cart="onAddCart"
/>
// 加入购物车事件
const onAddCart = (e: SkuPopupEvent) => {
console.log(e)
}
控制台打印数据
封装购物车接口:cart.ts
1、加入购物车接口封装
import { http } from "@/utils/http"
/**
* 加入购物车
* @param data 请求体参数
* @returns
*/
export const postMemberCartAPI = (data: { skuId: string; count: number}) => {
return http({
method: 'POST',
url: '/member/cart',
data,
})
}
完善商品详情页面的加入购物车功能
// 加入购物车事件
const onAddCart = async (e: SkuPopupEvent) => {
console.log(e)
await postMemberCartAPI({ skuId: e._id, count: e.buy_num })
uni.showToast({ icon: 'success', title: '已加入购物车' })
// 关闭弹窗
isShowSku.value = false
}
完整的商品详情页面代码:goods.vue
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app'
import { ref, computed } from 'vue'
import { getGoodsByIdAPI } from '@/services/goods'
import { postMemberCartAPI } from '@/services/cart'
import type { GoodsResult } from '@/types/goods'
import AddressPanel from './components/AddressPanel.vue'
import ServicePanel from './components/ServicePanel.vue'
import PageSkeleton from './components/PageSkeleton.vue'
import type {
SkuPopupEvent,
SkuPopupInstanceType,
SkuPopupLocaldata,
} from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'
// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()
// 接收页面参数
const query = defineProps<{
id: string
}>()
// 获取商品详情信息
const goods = ref<GoodsResult>()
const getGoodsByIdData = async () => {
const res = await getGoodsByIdAPI(query.id)
goods.value = res.result
// SKU 组件所需格式
localdata.value = {
_id: res.result.id,
name: res.result.name,
goods_thumb: res.result.mainPictures[0],
spec_list: res.result.specs.map((v) => {
return {
name: v.name,
list: v.values,
}
}),
sku_list: res.result.skus.map((v) => {
return {
_id: v.id,
goods_id: res.result.id,
goods_name: res.result.name,
image: v.picture,
price: v.price * 100, // 注意:需要乘以 100
stock: v.inventory,
sku_name_arr: v.specs.map((vv) => vv.valueName),
}
}),
}
}
// 是否数据加载完成
const isFinish = ref(false)
// 页面加载
onLoad(async () => {
await getGoodsByIdData()
isFinish.value = true
})
// 轮播图变化时
const currentIndex = ref(0)
const onChange: UniHelper.SwiperOnChange = (e) => {
currentIndex.value = e.detail!.current
}
// 点击图片时
const onTapImage = (url: string) => {
// 大图预览
uni.previewImage({
current: url, // 当前显示图片的链接
urls: goods.value!.mainPictures, // 需要预览的图片链接列表 数组
})
}
// uni-ui 弹出层组件 ref
const popup = ref<{
open: (type?: UniHelper.UniPopupType) => void
close: (type?: UniHelper.UniPopupType) => void
}>()
// 弹出层渲染
const popupName = ref<'address' | 'service'>()
const openPopup = (name: typeof popupName.value) => {
// 修改弹出层名称
popupName.value = name
popup.value?.open()
}
// 是否显示 SKU 组件
const isShowSku = ref(false)
// 商品信息
const localdata = ref({} as SkuPopupLocaldata)
// 按钮模式
enum SkuMode {
Both = 1, // 购物车和立即购买都显示
Cart = 2, // 只显示购物车
Buy = 3, // 只显示立即购买
}
const mode = ref<SkuMode>(SkuMode.Both)
// 打开sku 弹窗 修改按钮模式
const openSkuPopup = (val: SkuMode) => {
// 显示sku组件
isShowSku.value = true
// 修改按钮模式
mode.value = val
}
// SKU组件实例
const skuPopupRef = ref<SkuPopupInstanceType>()
// 计算被选中的值
const selectArrText = computed(() => {
return skuPopupRef.value?.selectArr?.join(' ').trim() || '请选择商品规格'
})
// 加入购物车事件
const onAddCart = async (e: SkuPopupEvent) => {
console.log(e)
await postMemberCartAPI({ skuId: e._id, count: e.buy_num })
uni.showToast({ icon: 'success', title: '已加入购物车' })
// 关闭弹窗
isShowSku.value = false
}
</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',
}"
@add-cart="onAddCart"
/>
<scroll-view scroll-y class="viewport" v-if="isFinish">
<!-- 基本信息 -->
<view class="goods">
<!-- 商品主图 -->
<view class="preview">
<swiper circular @change="onChange">
<swiper-item v-for="item in goods?.mainPictures" :key="item">
<image @tap="onTapImage(item)" mode="aspectFill" :src="item" />
</swiper-item>
</swiper>
<view class="indicator">
<text class="current">{{ currentIndex + 1 }}</text>
<text class="split">/</text>
<text class="total">{{ goods?.mainPictures.length }}</text>
</view>
</view>
<!-- 商品简介 -->
<view class="meta">
<view class="price">
<text class="symbol">¥</text>
<text class="number">{{ goods?.price }}</text>
</view>
<view class="name ellipsis">{{ goods?.name }} </view>
<view class="desc"> {{ goods?.desc }} </view>
</view>
<!-- 操作面板 -->
<view class="action">
<view class="item arrow" @tap="openSkuPopup(SkuMode.Both)">
<text class="label">选择</text>
<text class="text ellipsis"> {{ selectArrText }} </text>
</view>
<view class="item arrow" @tap="openPopup('address')">
<text class="label">送至</text>
<text class="text ellipsis"> 请选择收获地址 </text>
</view>
<view class="item arrow" @tap="openPopup('service')">
<text class="label">服务</text>
<text class="text ellipsis"> 无忧退 快速退款 免费包邮 </text>
</view>
</view>
</view>
<!-- 商品详情 -->
<view class="detail panel">
<view class="title">
<text>详情</text>
</view>
<view class="content">
<view class="properties">
<!-- 属性详情 -->
<view class="item" v-for="item in goods?.details.properties" :key="item.name">
<text class="label">{{ item.name }}</text>
<text class="value">{{ item.value }}</text>
</view>
</view>
<!-- 图片详情 -->
<image
v-for="item in goods?.details.pictures"
:key="item"
mode="widthFix"
:src="item"
></image>
</view>
</view>
<!-- 同类推荐 -->
<view class="similar panel">
<view class="title">
<text>同类推荐</text>
</view>
<view class="content">
<navigator
v-for="item in goods?.similarProducts"
:key="item"
class="goods"
hover-class="none"
:url="`/pages/goods/goods?id=${item.id}`"
>
<image class="image" mode="aspectFill" :src="item.picture"></image>
<view class="name ellipsis">{{ item.name }}</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">{{ item.price }}</text>
</view>
</navigator>
</view>
</view>
</scroll-view>
<PageSkeleton v-else />
<!-- 用户操作 -->
<view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
<view class="icons">
<button class="icons-button"><text class="icon-heart"></text>收藏</button>
<button class="icons-button" open-type="contact">
<text class="icon-handset"></text>客服
</button>
<navigator class="icons-button" url="/pages/cart/cart" open-type="switchTab">
<text class="icon-cart"></text>购物车
</navigator>
</view>
<view class="buttons">
<view class="addcart" @tap="openSkuPopup(SkuMode.Cart)"> 加入购物车 </view>
<view class="buynow" @tap="openSkuPopup(SkuMode.Buy)"> 立即购买 </view>
</view>
</view>
<!-- uni-ui 弹出层 -->
<uni-popup ref="popup" type="bottom">
<AddressPanel v-if="popupName === 'address'" @close="popup?.close()" />
<ServicePanel v-if="popupName === 'service'" @close="popup?.close()" />
</uni-popup>
</template>
购物车列表页面:cart.vue
获取登录的用户信息 --> 条件渲染(是否登录) --> 初始化调用 --> 列表渲染
封装购物车列表类型数据:cart.d.ts
/** 购物车类型 */
export type CartItem = {
/** 商品 ID */
id: string
/** SKU ID */
skuId: string
/** 商品名称 */
name: string
/** 图片 */
picture: string
/** 数量 */
count: number
/** 加入时价格 */
price: number
/** 当前的价格 */
nowPrice: number
/** 库存 */
stock: number
/** 是否选中 */
selected: boolean
/** 属性文字 */
attrsText: string
/** 是否为有效商品 */
isEffective: boolean
}
封装购物车列表接口:cart.ts
import type { CartItem } from '@/types/cart';
import { http } from '@/utils/http'
/**
* 获取购物车列表数据
* @returns
*/
export const getMemberCartAPI = () => {
return http<CartItem[]>({
method: 'GET',
url: '/member/cart',
})
}
初始化调用:cart.vue
// 获取购物车列表数据
const cartList = ref<CartItem>([])
const getMemberCartData = async () => {
const res = await getMemberCartAPI()
cartList.value = res.result
}
// onShow:页面显示就触发 页面初始化调用 因为加入购物车不是在这个页面的,所以用onShow调用更合适
onShow(() => {
// 判断用户是否已经登录了
if (memberStore.profile) {
getMemberCartData()
}
})
删除购物车列表中的商品:封装API、按钮绑定事件、弹窗二次确认、调用API、重新获取列表
封装购物车删除API 接口:
/**
* 删除/清空购物车单品
* @param data 请求体参数 ids SKUID 集合
*/
export const deleteMemberCartAPI = (data: { ids: string[] }) => {
return http({
method: 'DELETE',
url: '/member/cart',
data,
})
}
点击删除按钮 - 删除购物车商品 cart.vue
// 点击删除按钮 - 删除购物车
const onDeleteCart = (skuId: string) => {
// 弹窗二次确认
uni.showModal({
content: '是否确定删除?',
success: async (res) => {
if (res.confirm) {
await deleteMemberCartAPI({ ids: [skuId] })
// 更新购物车列表
getMemberCartData()
}
},
})
}
删除成功
修改商品数量:步进器组件
<view class="count">
<!-- <text class="text">-</text>
<input class="input" type="number" :value="item.count.toString()" />
<text class="text">+</text> -->
<vk-data-input-number-box
v-model="item.count"
:min="1"
:max="item.stock"
:index="item.skuId"
@change="onChangeCount"
/>
</view>
封装修改API
/**
* 修改购物车单品
* @param skuId SKUID
* @param data selected 选中状态 count 商品数量
*/
export const putMemberCartBySkuIdAPI = ( skuId: string, data: { selected?: boolean; count?: number }) => {
return http({
method: 'PUT',
url: `/member/cart/${skuId}`,
data,
})
}
修改方法:
// 修改商品数量
const onChangeCount = (e) => {
console.log(e)
putMemberCartBySkuIdAPI(e.index, { count: e.value })
}
修改商品的选中状态,即单选和全选功能实现
<!-- 选中状态 -->
<text
@tap="onChangeSelected(item)"
class="checkbox"
:class="{ checked: item.selected }"
></text>
封装全选 / 取消全选API
/**
* 购物车全选/取消全选
* @param data selected 是否选中
*/
export const putMemberCartSelectedAPI = (data: { selected: boolean }) => {
return http({
method: 'PUT',
url: '/member/cart/selected',
data,
})
}
// 修改选中状态 - 单选修改
const onChangeSelected = (good: CartItem) => {
console.log(good)
// 前端数据更新 - 是否选中 取反
good.selected = !good.selected
// 后端数据更新 与修改数量接口是同一条接口 传递的参数不同
putMemberCartBySkuIdAPI(good.skuId, { selected: good.selected })
}
// 计算全选状态
const isSelectedAll = computed(() => {
return cartList.value.length && cartList.value.every((v) => v.selected)
})
// 修改选中状态-全选修改
const onChangeSelectedAll = () => {
// 全选状态取法
const _isSelectedAll = !isSelectedAll.value
// 前端数据更新
cartList.value.forEach((item) => {
item.selected = _isSelectedAll
})
// 后端更新
putMemberCartSelectedAPI({ selected: _isSelectedAll })
}
购物车页面 - 底部结算信息
<!-- 底部结算 -->
<view class="toolbar">
<text class="all" @tap="onChangeSelectedAll" :class="{ checked: isSelectedAll }">全选</text>
<text class="text">合计:</text>
<text class="amount">{{ selectedCartListMoney }}</text>
<view class="button-grounp">
<view
@tap="gotoPayment"
class="button payment-button"
:class="{ disabled: selectedCartListCount === 0 }"
>
去结算({{ selectedCartListCount }})
</view>
</view>
</view>
逻辑实现:
// 计算选中的商品列表
const selectedCartList = computed(() => {
return cartList.value.filter((v) => v.selected)
})
// 计算选中商品的总件数
const selectedCartListCount = computed(() => {
return selectedCartList.value.reduce((sum, item) => sum + item.count, 0)
})
// 计算选中商品的总金额
const selectedCartListMoney = computed(() => {
return selectedCartList.value
.reduce((sum, item) => sum + item.count * item.nowPrice, 0)
.toFixed(2)
})
// 去结算按钮
const gotoPayment = () => {
// 判断用户是否选择了商品 即商品数量不能为 0
if (selectedCartListCount.value === 0) {
return uni.showToast({ icon: 'none', title: '请选择商品' })
}
// 跳转到计算页面
uni.showToast({ title: '此功能还未写' })
}
完整的购物车列表页面组件代码:cart.vue
<script setup lang="ts">
import { onShow } from '@dcloudio/uni-app'
import { ref, computed } from 'vue'
import {
deleteMemberCartAPI,
getMemberCartAPI,
putMemberCartBySkuIdAPI,
putMemberCartSelectedAPI,
} from '@/services/cart'
import { useMemberStore } from '@/stores/index'
import type { CartItem } from '@/types/cart'
import type { InputNumberBoxEvent } from '@/components/vk-data-input-number-box/vk-data-input-number-box'
// 获取会员 Store
const memberStore = useMemberStore()
// 获取购物车列表数据
const cartList = ref<CartItem>([])
const getMemberCartData = async () => {
const res = await getMemberCartAPI()
cartList.value = res.result
}
// onShow:页面显示就触发 页面初始化调用 因为加入购物车不是在这个页面的,所以用onShow调用更合适
onShow(() => {
// 判断用户是否已经登录了
if (memberStore.profile) {
getMemberCartData()
}
})
// 点击删除按钮 - 删除购物车
const onDeleteCart = (skuId: string) => {
// 弹窗二次确认
uni.showModal({
content: '是否确定删除?',
success: async (res) => {
if (res.confirm) {
await deleteMemberCartAPI({ ids: [skuId] })
// 更新购物车列表
getMemberCartData()
}
},
})
}
// 修改商品数量
const onChangeCount = (e: InputNumberBoxEvent) => {
console.log(e)
putMemberCartBySkuIdAPI(e.index, { count: e.value })
}
// 修改选中状态 - 单品修改
const onChangeSelected = (good: CartItem) => {
console.log(good)
// 前端数据更新 - 是否选中 取反
good.selected = !good.selected
// 后端数据更新
putMemberCartBySkuIdAPI(good.skuId, { selected: good.selected })
}
// 计算全选状态
const isSelectedAll = computed(() => {
return cartList.value.length && cartList.value.every((v) => v.selected)
})
// 修改选中状态-全选修改
const onChangeSelectedAll = () => {
// 全选状态取法
const _isSelectedAll = !isSelectedAll.value
// 前端数据更新
cartList.value.forEach((item) => {
item.selected = _isSelectedAll
})
// 后端更新
putMemberCartSelectedAPI({ selected: _isSelectedAll })
}
// 计算选中的商品列表
const selectedCartList = computed(() => {
return cartList.value.filter((v) => v.selected)
})
// 计算选中商品的总件数
const selectedCartListCount = computed(() => {
return selectedCartList.value.reduce((sum, item) => sum + item.count, 0)
})
// 计算选中商品的总金额
const selectedCartListMoney = computed(() => {
return selectedCartList.value
.reduce((sum, item) => sum + item.count * item.nowPrice, 0)
.toFixed(2)
})
// 去结算按钮
const gotoPayment = () => {
// 判断用户是否选择了商品 即商品数量不能为 0
if (selectedCartListCount.value === 0) {
return uni.showToast({ icon: 'none', title: '请选择商品' })
}
// 跳转到计算页面
uni.showToast({ title: '此功能还未写' })
}
</script>
<template>
<scroll-view scroll-y class="scroll-view">
<!-- 已登录: 显示购物车 -->
<template v-if="memberStore.profile.token">
<!-- 购物车列表 -->
<view class="cart-list" v-if="cartList.length">
<!-- 优惠提示 -->
<view class="tips">
<text class="label">满减</text>
<text class="desc">满1件, 即可享受9折优惠</text>
</view>
<!-- 滑动操作分区 -->
<uni-swipe-action>
<!-- 滑动操作项 -->
<uni-swipe-action-item v-for="item in cartList" :key="item.skuId" class="cart-swipe">
<!-- 商品信息 -->
<view class="goods">
<!-- 选中状态 -->
<text
@tap="onChangeSelected(item)"
class="checkbox"
:class="{ checked: item.selected }"
></text>
<navigator
:url="`/pages/goods/goods?id=${item.id}`"
hover-class="none"
class="navigator"
>
<image mode="aspectFill" class="picture" :src="item.picture"></image>
<view class="meta">
<view class="name ellipsis">{{ item.name }}</view>
<view class="attrsText ellipsis">{{ item.attrsText }}</view>
<view class="price">{{ item.nowPrice }}</view>
</view>
</navigator>
<!-- 商品数量 -->
<view class="count">
<!-- <text class="text">-</text>
<input class="input" type="number" :value="item.count.toString()" />
<text class="text">+</text> -->
<vk-data-input-number-box
v-model="item.count"
:min="1"
:max="item.stock"
:index="item.skuId"
@change="onChangeCount"
/>
</view>
</view>
<!-- 右侧删除按钮 -->
<template #right>
<view class="cart-swipe-right">
<button @tap="onDeleteCart(item.skuId)" class="button delete-button">删除</button>
</view>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
<!-- 购物车空状态 -->
<view class="cart-blank" v-else>
<image src="/static/images/blank_cart.png" class="image" />
<text class="text">购物车还是空的,快来挑选好货吧</text>
<navigator open-type="switchTab" url="/pages/index/index" hover-class="none">
<button class="button">去首页看看</button>
</navigator>
</view>
<!-- 吸底工具栏 -->
<view class="toolbar">
<text class="all" @tap="onChangeSelectedAll" :class="{ checked: isSelectedAll }">全选</text>
<text class="text">合计:</text>
<text class="amount">{{ selectedCartListMoney }}</text>
<view class="button-grounp">
<view
@tap="gotoPayment"
class="button payment-button"
:class="{ disabled: selectedCartListCount === 0 }"
>
去结算({{ selectedCartListCount }})
</view>
</view>
</view>
</template>
<!-- 未登录: 提示登录 -->
<view class="login-blank" v-else>
<text class="text">登录后可查看购物车中的商品</text>
<navigator url="/pages/login/login" hover-class="none">
<button class="button">去登录</button>
</navigator>
</view>
<!-- 猜你喜欢 -->
<Guess ref="guessRef"></XtxGuess>
<!-- 底部占位空盒子 -->
<view class="toolbar-height"></view>
</scroll-view>
</template>