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, // 注意:需要乘以 100stock: 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) => voidclose: (type?: UniHelper.UniPopupType) => void
}>()// 弹出层渲染
const popupName = ref<'address' | 'service'>()
const openPopup = (name: typeof popupName.value) => {// 修改弹出层名称popupName.value = namepopup.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-popupv-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><!-- 图片详情 --><imagev-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"><navigatorv-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-boxv-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 = () => {// 判断用户是否选择了商品 即商品数量不能为 0if (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-boxv-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>