收址模块
准备 地址管理分包页面 和 添加地址分包页面
新增和修改地址
import type { AddressParams, AddressItem } from '@/types/address'
import { http } from '@/utils/http'
/**
* 添加收货地址
* @param data 请求参数
*/
export const postMemberAddressAPI = (data: AddressParams) => {
return http({
method: 'POST',
url: '/member/address',
data,
})
}
/**
* 获取收货地址列表
*/
export const getMemberAddressAPI = () => {
return http<AddressItem[]>({
method: 'GET',
url: '/member/address',
})
}
/**
* 获取收货地址详情
* @param id 地址id(路径参数)
*/
export const getMemberAddressByIdAPI = (id: string) => {
return http<AddressItem>({
method: 'GET',
url: `/member/address/${id}`,
})
}
/**
* 修改收货地址
* @param id 地址id(路径参数)
* @param data 表单数据(请求体参数)
*/
export const putMemberAddressByIdAPI = (id: string, data: AddressParams) => {
return http({
method: 'PUT',
url: `/member/address/${id}`,
data,
})
}
/**
* 删除收货地址
* @param id 地址id(路径参数)
*/
export const deleteMemberAddressByIdAPI = (id: string) => {
return http({
method: 'DELETE',
url: `/member/address/${id}`,
})
}
/** 添加收货地址: 请求参数 */
export type AddressParams = {
/** 收货人姓名 */
receiver: string
/** 联系方式 */
contact: string
/** 省份编码 */
provinceCode: string
/** 城市编码 */
cityCode: string
/** 区/县编码 */
countyCode: string
/** 详细地址 */
address: string
/** 默认地址,1为是,0为否 */
isDefault: number
}
/** 收货地址项 */
export type AddressItem = AddressParams & {
/** 收货地址 id */
id: string
/** 省市区 */
fullLocation: string
}
<script setup lang="ts">
import {
postMemberAddressAPI,
getMemberAddressByIdAPI,
putMemberAddressByIdAPI,
} from '@/services/address'
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
// 获取页面参数
const query = defineProps<{
id: string
}>()
// 动态设置标题
uni.setNavigationBarTitle({ title: query.id ? '修改地址' : '新建地址' })
// 表单数据
const form = ref({
receiver: '', // 收货人
contact: '', // 联系方式
fullLocation: '', // 省市区(前端展示)
provinceCode: '', // 省份编码(后端参数)
cityCode: '', // 城市编码(后端参数)
countyCode: '', // 区/县编码(后端参数)
address: '', // 详细地址
isDefault: 0, // 默认地址,1为是,0为否
})
// 收集所在地区
const onRegionChange: UniHelper.RegionPickerOnChange = (ev) => {
// 省市区(前端展示)
form.value.fullLocation = ev.detail.value.join(' ')
// 省市区(后端参数)
const [provinceCode, cityCode, countyCode] = ev.detail.code!
// 合并数据
Object.assign(form.value, { provinceCode, cityCode, countyCode })
}
// 收集是否默认收货地址
const onSwitchChange: UniHelper.SwitchOnChange = (ev) => {
form.value.isDefault = ev.detail.value ? 1 : 0
}
// 定义校验规则
const rules: UniHelper.UniFormsRules = {
receiver: {
rules: [{ required: true, errorMessage: '请输入收货人姓名' }],
},
contact: {
rules: [
{ required: true, errorMessage: '请输入联系方式' },
{ pattern: /^1[3-9]\d{9}$/, errorMessage: '手机号格式不正确' },
],
},
fullLocation: {
rules: [{ required: true, errorMessage: '请选择所在地区' }],
},
address: {
rules: [{ required: true, errorMessage: '请选择详细地址' }],
},
}
// 获取表单组件实例,用于调用表单方法
const formRef = ref<UniHelper.UniFormsInstance>()
// 提交表单
const onSubmit = async () => {
try {
// 表单校验
await formRef.value?.validate?.()
// 判断当前页面是否有地址 id
if (query.id) {
// 修改地址请求
await putMemberAddressByIdAPI(query.id, form.value)
} else {
// 新建地址请求
await postMemberAddressAPI(form.value)
}
// 成功提示
uni.showToast({ icon: 'success', title: query.id ? '修改成功' : '添加成功' })
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 400)
} catch (error) {
uni.showToast({ icon: 'error', title: '请填写完整信息' })
}
}
// 获取收货地址详情数据
const getMemberAddressByIdData = async () => {
// 有 id 才调用接口
if (query.id) {
// 发送请求
const res = await getMemberAddressByIdAPI(query.id)
// 把数据合并到表单中
Object.assign(form.value, res.result)
}
}
// 页面加载
onLoad(() => {
getMemberAddressByIdData()
})
</script>
<template>
<view class="content">
<uni-forms :rules="rules" :model="form" ref="formRef">
<!-- 表单内容 -->
<uni-forms-item name="receiver" class="form-item">
<text class="label">收货人</text>
<input class="input" placeholder="请填写收货人姓名" v-model="form.receiver" />
</uni-forms-item>
<uni-forms-item name="contact" class="form-item">
<text class="label">手机号码</text>
<input class="input" placeholder="请填写收货人手机号码" v-model="form.contact" />
</uni-forms-item>
<uni-forms-item name="fullLocation" class="form-item">
<text class="label">所在地区</text>
<picker
@change="onRegionChange"
class="picker"
mode="region"
:value="form.fullLocation.split(' ')"
>
<view v-if="form.fullLocation">{{ form.fullLocation }}</view>
<view v-else class="placeholder">请选择省/市/区(县)</view>
</picker>
</uni-forms-item>
<uni-forms-item name="address" class="form-item">
<text class="label">详细地址</text>
<input class="input" placeholder="街道、楼牌号等信息" v-model="form.address" />
</uni-forms-item>
<view class="form-item">
<label class="label">设为默认地址</label>
<switch
class="switch"
color="#27ba9b"
:checked="form.isDefault === 1"
@change="onSwitchChange"
/>
</view>
</uni-forms>
</view>
<!-- 提交按钮 -->
<button @tap="onSubmit" class="button">保存并使用</button>
</template>
表单校验:
- 结构: 使用uni-forms 和 uni-forms-item组件实现表单数据的校验
- 规则: 通过rules属性指定校验规则, 通过model属性指定表单数据, 通过name属性指定校验项
- 校验: 通过formRef拿到组件实例, 调用组件实例的方法, 触发校验功能
地址管理页列表渲染
<script setup lang="ts">
import { getMemberAddressAPI } from '@/services/address'
import type { AddressItem } from '@/types/address'
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'
// 获取收货地址列表数据
const addressList = ref<AddressItem[]>([])
const getMemberAddressData = async () => {
const res = await getMemberAddressAPI()
addressList.value = res.result
}
// 初始化调用(页面显示)
onShow(() => {
getMemberAddressData()
})
</script>
<template>
<view class="viewport">
<!-- 地址列表 -->
<scroll-view class="scroll-view" scroll-y>
<view v-if="true" class="address">
<view class="address-list">
<!-- 滑动操作分区 -->
<uni-swipe-action>
<!-- 滑动操作项 -->
<uni-swipe-action-item v-for="item in addressList" :key="item.id">
<!-- 默认插槽 -->
<!-- 收获地址项 -->
<view class="item">
<view class="item-content" @tap="onChangeAddress(item)">
<view class="user">
{{ item.receiver }}
<text class="contact">{{ item.contact }}</text>
<text v-if="item.isDefault" class="badge">默认</text>
</view>
<view class="locate">{{ item.fullLocation }} {{ item.address }}</view>
<navigator
class="edit"
hover-class="none"
:url="`/pagesMember/address-form/address-form?id=${item.id}`"
@tap.stop="() => {}"
@tap.prevent="() => {}"
>
修改
</navigator>
</view>
</view>
<!-- 右侧插槽 -->
<template #right>
<button class="delete-button">删除</button>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
</view>
<view v-else class="blank">暂无收货地址</view>
</scroll-view>
<!-- 添加按钮 -->
<view class="add-btn">
<navigator hover-class="none" url="/pagesMember/address-form/address-form">
新建地址
</navigator>
</view>
</view>
</template>
- onLoad() 页面加载,只执行一次
- onShow() 页面展示,反复执行
删除地址
<script setup lang="ts">
import { deleteMemberAddressByIdAPI } from '@/services/address'
import type { AddressItem } from '@/types/address'
import { useAddressStore } from '@/stores/modules/address'
import { onShow } from '@dcloudio/uni-app'
// 删除收货地址
const onDeleteAddress = (id: string) => {
// 二次确认
uni.showModal({
content: '删除地址?',
success: async (res) => {
if (res.confirm) {
// 根据id删除收货地址
await deleteMemberAddressByIdAPI(id)
// 重新获取收货地址列表
getMemberAddressData()
}
},
})
}
// 修改收货地址
const onChangeAddress = (item: AddressItem) => {
// 修改地址
const addressStore = useAddressStore()
addressStore.changeSelectedAddress(item)
// 返回上一页
uni.navigateBack()
}
</script>
<template>
<view class="viewport">
<!-- 地址列表 -->
<scroll-view class="scroll-view" scroll-y>
<view v-if="true" class="address">
<view class="address-list">
<!-- 滑动操作分区 -->
<uni-swipe-action>
<!-- 滑动操作项 -->
<uni-swipe-action-item v-for="item in addressList" :key="item.id">
<!-- 默认插槽 -->
<!-- 收获地址项 -->
<view class="item">
<view class="item-content" @tap="onChangeAddress(item)">
<view class="user">
{{ item.receiver }}
<text class="contact">{{ item.contact }}</text>
<text v-if="item.isDefault" class="badge">默认</text>
</view>
<view class="locate">{{ item.fullLocation }} {{ item.address }}</view>
<navigator
class="edit"
hover-class="none"
:url="`/pagesMember/address-form/address-form?id=${item.id}`"
@tap.stop="() => {}"
@tap.prevent="() => {}"
>
修改
</navigator>
</view>
</view>
<!-- 右侧插槽 -->
<template #right>
<button class="delete-button" @tap="onDeleteAddress(item.id)">删除</button>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
</view>
<view v-else class="blank">暂无收货地址</view>
</scroll-view>
<!-- 添加按钮 -->
<view class="add-btn">
<navigator hover-class="none" url="/pagesMember/address-form/address-form">
新建地址
</navigator>
</view>
</view>
</template>
sku模块
概念: 存货单位( Stock Keeping Unit ), 库存管理的最小单元, 通常称为'单品'
SKU常见于电商领域, 对于前端工程师而言, 更多关注SKU算法,基于后端的SKU数据渲染页面并实现交互
uni-app插件市场可以是官方插件集中地, 其中可以找到SKU组件使用
- 找到合适的组件后, 直接下载即可
- 下载完成后, 跟随文档的指引, 把组件安装到自己的项目中即可
- 当前项目使用eslint进行代码校验, 但是上述插件没有进行校验,导致代码无法通过提交检查,
- 改动作者代码成本巨高, 所以要禁用当前文件的eslint的校验
- 在script标签的第一行, 添加禁用当前文件的语法校验
使用SKU组件, 渲染商品信息
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
}
}
<script setup lang="ts">
// 获取商品详情
const goods = ref<GoodsResult>()
const getGoodsData = 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) => ({ 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, // 注意:需要乘以 100
stock: v.inventory,
sku_name_arr: v.specs.map((vv) => vv.valueName),
})),
}
}
// 控制sku组件
const isShowSku = ref(false)
// 商品信息
const localdata = ref({} as SkuPopupLocaldata)
</script>
<template>
<!-- sku弹窗组件 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="localdata"
add-cart-background-color="#FFA868"
buy-now-background-color="#27BA9B"
:actived-style="{
color: '#27BA9B',
borderColor: '#27BA9B',
backgroundColor: '#E9F8F5',
}"
/>
<scroll-view scroll-y class="viewport">
<!-- 基本信息 -->
<view class="goods">
... ...
<!-- 操作面板 -->
<view class="action">
<view class="item arrow" @tap="isShowSku = true">
<text class="label">选择</text>
<text class="text ellipsis"> {{ selectArrText }} </text>
</view>
... ...
</view>
</view>
</scroll-view>
</template>
设置按钮模式: 加入购物车/立即购买/选择都可以打开sku弹窗, sku弹窗展示的按钮不同
<script setup lang="ts">
import type {
SkuPopupLocaldata
} from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'
// 控制sku组件
const isShowSku = ref(false)
// 商品信息
const localdata = ref({} as SkuPopupLocaldata)
// 按钮模式
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"
ref="skuPopupRef"
@add-cart="onAddCart"
@buy-now="onBuyNow"
add-cart-background-color="#FFA868"
buy-now-background-color="#27BA9B"
:actived-style="{
color: '#27BA9B',
borderColor: '#27BA9B',
backgroundColor: '#E9F8F5',
}"
/>
<scroll-view scroll-y class="viewport">
<!-- 基本信息 -->
<view class="goods">
... ...
<!-- 操作面板 -->
<view class="action">
<view class="item arrow" @tap="openSkuPopup(1)">
<text class="label">选择</text>
<text class="text ellipsis"> {{ selectArrText }} </text>
</view>
... ...
</view>
</view>
</scroll-view>
<!-- 用户操作 -->
<view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
... ...
<view class="buttons">
<view class="addcart" @tap="openSkuPopup(2)"> 加入购物车 </view>
<view class="buynow" @tap="openSkuPopup(3)"> 立即购买 </view>
</view>
</view>
</template>
- sku组件可以用过mode选项控制组件内按钮按需展示隐藏, 1展示全部按钮, 2展示加入购物车按钮,3展示立即购买按钮
- 直接指定1,2,3语义化不好, 我们使用枚举进行指定
在商品界面展示SKU组件中选中的值
<script setup lang="ts">
// 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"
ref="skuPopupRef"
add-cart-background-color="#FFA868"
buy-now-background-color="#27BA9B"
:actived-style="{
color: '#27BA9B',
borderColor: '#27BA9B',
backgroundColor: '#E9F8F5',
}"
/>
<scroll-view scroll-y class="viewport">
<!-- 基本信息 -->
<view class="goods">
... ...
<!-- 操作面板 -->
<view class="action">
<view class="item arrow" @tap="openSkuPopup(1)">
<text class="label">选择</text>
<text class="text ellipsis"> {{ selectArrText }} </text>
</view>
<view class="item arrow" @tap="openPopop('address')">
<text class="label">送至</text>
<text class="text ellipsis"> 请选择收获地址 </text>
</view>
<view class="item arrow" @tap="openPopop('service')">
<text class="label">服务</text>
<text class="text ellipsis"> 无忧退 快速退款 免费包邮 </text>
</view>
</view>
</view>
... ...
</scroll-view>
</template>
加入购物车和立即购买
<script setup lang="ts">
import type {
SkuPopupEvent,
} from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'
import { postMemberCartAPI } from '@/services/cart'
// 加入购物车事件
const onAddCart = async (ev: SkuPopupEvent) => {
await postMemberCartAPI({ skuId: ev._id, count: ev.buy_num })
uni.showToast({ title: '添加成功' })
isShowSku.value = false
}
// 立即购买
const onBuyNow = (ev: SkuPopupEvent) => {
uni.navigateTo({ url: `/pagesOrder/create/create?skuId=${ev._id}&count=${ev.buy_num}` })
isShowSku.value = false
}
</script>
<template>
<!-- sku弹窗组件 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="localdata"
:mode="mode"
ref="skuPopupRef"
@add-cart="onAddCart"
@buy-now="onBuyNow"
add-cart-background-color="#FFA868"
buy-now-background-color="#27BA9B"
:actived-style="{
color: '#27BA9B',
borderColor: '#27BA9B',
backgroundColor: '#E9F8F5',
}"
/>
... ...
<!-- 用户操作 -->
<view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
... ...
<view class="buttons">
<view class="addcart" @tap="openSkuPopup(2)"> 加入购物车 </view>
<view class="buynow" @tap="openSkuPopup(3)"> 立即购买 </view>
</view>
</view>
</template>
购物车模块
渲染列表
/** 购物车类型 */
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
}
import type { CartItem } from '@/types/cart'
import { http } from '@/utils/http'
/**
* 加入购物车
* @param data 请求体参数
*/
export const postMemberCartAPI = (data: { skuId: string; count: number }) => {
return http({
method: 'POST',
url: '/member/cart',
data,
})
}
/**
* 获取购物车列表
*/
export const getMemberCartAPI = () => {
return http<CartItem[]>({
method: 'GET',
url: '/member/cart',
})
}
/**
* 删除/清空购物车单品
* @param data 请求体参数 ids SKUID 集合
*/
export const deleteMemberCartAPI = (data: { ids: string[] }) => {
return http({
method: 'DELETE',
url: '/member/cart',
data,
})
}
/**
* 修改购物车单品
* @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,
})
}
/**
* 购物车全选/取消全选
* @param data selected 是否选中
*/
export const putMemberCartSelectedAPI = (data: { selected: boolean }) => {
return http({
method: 'PUT',
url: '/member/cart/selected',
data,
})
}
<script setup lang="ts">
import {
getMemberCartAPI,
} from '@/services/cart'
import { useMemberStore } from '@/stores'
import type { CartItem } from '@/types/cart'
import { onShow } from '@dcloudio/uni-app'
import { ref, computed } from 'vue'
// 获取会员Store
const memberStore = useMemberStore()
// 获取购物车数据
const cartList = ref<CartItem[]>([])
const getMemberCartData = async () => {
const res = await getMemberCartAPI()
cartList.value = res.result
}
// 初始化调用: 页面显示触发
onShow(() => {
// 用户已登录才允许调用
if (memberStore.profile) {
getMemberCartData()
}
})
</script>
<template>
<scroll-view scroll-y class="scroll-view">
<!-- 已登录: 显示购物车 -->
<template v-if="memberStore.profile">
<!-- 购物车列表 -->
<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">
<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: true }">
去结算({{ 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>
<!-- 猜你喜欢 -->
<XtxGuess ref="guessRef"></XtxGuess>
<!-- 底部占位空盒子 -->
<view class="toolbar-height"></view>
</scroll-view>
</template>
删除商品
<script setup lang="ts">
import {
deleteMemberCartAPI,
} from '@/services/cart'
// 点击删除按钮
const onDeleteCart = (skuId: string) => {
// 弹窗二次确认
uni.showModal({
content: '是否删除',
success: async (res) => {
if (res.confirm) {
// 后端删除单品
await deleteMemberCartAPI({ ids: [skuId] })
// 重新获取列表
getMemberCartData()
}
},
})
}
</script>
<template>
<scroll-view scroll-y class="scroll-view">
<!-- 已登录: 显示购物车 -->
<template v-if="memberStore.profile">
<!-- 购物车列表 -->
<view class="cart-list" v-if="cartList.length">
<!-- 滑动操作分区 -->
<uni-swipe-action>
<!-- 滑动操作项 -->
<uni-swipe-action-item v-for="item in cartList" :key="item.skuId" class="cart-swipe">
<!-- 商品信息 -->
<view class="goods">
... ...
</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>
</template>
</scroll-view>
</template>
修改商品数量
import { Component } from '@uni-helper/uni-app-types'
/** 步进器 */
export type InputNumberBox = Component<InputNumberBoxProps>
/** 步进器实例 */
export type InputNumberBoxInstance = InstanceType<InputNumberBox>
/** 步进器属性 */
export type InputNumberBoxProps = {
/** 输入框初始值(默认1) */
modelValue: number
/** 用户可输入的最小值(默认0) */
min: number
/** 用户可输入的最大值(默认99999) */
max: number
/** 步长,每次加或减的值(默认1) */
step: number
/** 是否禁用操作,包括输入框,加减按钮 */
disabled: boolean
/** 输入框宽度,单位rpx(默认80) */
inputWidth: string | number
/** 输入框和按钮的高度,单位rpx(默认50) */
inputHeight: string | number
/** 输入框和按钮的背景颜色(默认#F2F3F5) */
bgColor: string
/** 步进器标识符 */
index: string
/** 输入框内容发生变化时触发 */
onChange: (event: InputNumberBoxEvent) => void
/** 输入框失去焦点时触发 */
onBlur: (event: InputNumberBoxEvent) => void
/** 点击增加按钮时触发 */
onPlus: (event: InputNumberBoxEvent) => void
/** 点击减少按钮时触发 */
onMinus: (event: InputNumberBoxEvent) => void
}
/** 步进器事件对象 */
export type InputNumberBoxEvent = {
/** 输入框当前值 */
value: number
/** 步进器标识符 */
index: string
}
/** 全局组件类型声明 */
declare module '@vue/runtime-core' {
export interface GlobalComponents {
'vk-data-input-number-box': InputNumberBox
}
}
<script setup lang="ts">
import type { InputNumberBoxEvent } from '@/components/vk-data-input-number-box/vk-data-input-number-box'
import {
putMemberCartBySkuIdAPI,
} from '@/services/cart'
// 修改商品数量
const onChangeCount = (ev: InputNumberBoxEvent) => {
putMemberCartBySkuIdAPI(ev.index, { count: ev.value })
}
</script>
<template>
<scroll-view scroll-y class="scroll-view">
<template v-if="memberStore.profile">
<!-- 购物车列表 -->
<view class="cart-list" v-if="cartList.length">
... ...
<!-- 滑动操作分区 -->
<uni-swipe-action>
<!-- 滑动操作项 -->
<uni-swipe-action-item v-for="item in cartList" :key="item.skuId" class="cart-swipe">
<!-- 商品信息 -->
<view class="goods">
... ...
<!-- 商品数量 -->
<view class="count">
<vk-data-input-number-box
v-model="item.count"
:min="1"
:max="item.stock"
:index="item.skuId"
@change="onChangeCount"
/>
</view>
</view>
... ...
</uni-swipe-action-item>
</uni-swipe-action>
</view>
</template>
</scroll-view>
</template>
商品状态修改
<script setup lang="ts">
import {
putMemberCartBySkuIdAPI,
putMemberCartSelectedAPI,
} from '@/services/cart'
import type { CartItem } from '@/types/cart'
import { onShow } from '@dcloudio/uni-app'
import { ref, computed } from 'vue'
// 修改选中状态-单品修改
const onChangeSelected = (item: CartItem) => {
// 前端数据更新-是否选中取反
item.selected = !item.selected
// 后端数据更新
putMemberCartBySkuIdAPI(item.skuId, { selected: item.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 })
}
</script>
<template>
<scroll-view scroll-y class="scroll-view">
<template v-if="memberStore.profile">
<!-- 购物车列表 -->
<view class="cart-list" v-if="cartList.length">
<!-- 滑动操作分区 -->
<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>
</view>
... ...
</uni-swipe-action-item>
</uni-swipe-action>
</view>
<!-- 吸底工具栏 -->
<view class="toolbar">
<text class="all" @tap="onChangeSelectedAll" :class="{ checked: isSelectedAll }">全选</text>
... ...
</view>
</template>
</scroll-view>
</template>
底部结算信息
<script setup lang="ts">
import { ref, computed } from 'vue'
// 计算选中单品列表
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 = () => {
if (selectedCartListCount.value === 0) {
return uni.showToast({
icon: 'none',
title: '请选择商品',
})
}
// 跳转到结算页
uni.navigateTo({ url: '/pagesOrder/create/create' })
}
</script>
<template>
<scroll-view scroll-y class="scroll-view">
<template v-if="memberStore.profile">
... ...
<!-- 吸底工具栏 -->
<view class="toolbar">
<text class="text">合计:</text>
<text class="amount">{{ selectedCartListMoney }}</text>
<view class="button-grounp">
<view @tap="gotoPayment" class="button payment-button" :class="{ disabled: true }">
去结算({{ selectedCartListCount }})
</view>
</view>
</view>
</template>
</scroll-view>
</template>
购物车页面复用: 小程序跳转到tabbar页面时, 会关闭其他所有非tabbar页, 所以小程序tabbar页没有后退按钮
<script setup lang="ts">
// 获取购物车数据
const cartList = ref<CartItem[]>([])
const getMemberCartData = async () => {
const res = await getMemberCartAPI()
cartList.value = res.result
}
// 初始化调用: 页面显示触发
onShow(() => {
// 用户已登录才允许调用
if (memberStore.profile) {
getMemberCartData()
}
})
</script>
<template>
<scroll-view scroll-y class="scroll-view">
... ...
</scroll-view>
</template>
<script setup lang="ts">
import CartMain from './components/CartMain.vue'
</script>
<template>
<CartMain />
</template>
<style lang="scss">
page {
height: 100%;
}
</style>
<script setup lang="ts">
import CartMain from './components/CartMain.vue'
</script>
<template>
<CartMain />
</template>
<style lang="scss">
page {
height: 100%;
}
</style>
{
"pages": [
... ...
// 购物车tabber页
{
"path": "pages/cart/cart",
"style": {
"navigationBarTitleText": "购物车"
}
},
// 购物车普通页
{
"path": "pages/cart/cart2",
"style": {
"navigationBarTitleText": "购物车"
}
},
... ...
],
}
<template>
<!-- 用户操作 -->
<view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
<view class="icons">
... ...
<navigator class="icons-button" url="/pages/cart/cart2" open-type="navigate">
<text class="icon-cart"></text>购物车
</navigator>
</view>
... ...
</view>
</template>