一、目标说明
本章目标将实现微信小程序中的购物车功能。如下图:
二、渲染页底部的导航区域
1、基于 uni-ui 提供的GoodsNav组件来实现商品导航区域的效果。
2、在 data 中通过 options 和 buttonGroup 两个数组,来声明商品导航组件的按钮配置对象:
data() {
return {
// 底部左侧按钮组的配置对象
options: [{
icon: 'shop',
text: '店铺'
}, {
icon: 'cart',
text: '购物车',
info: ''
}],
// 底部右侧按钮组的配置对象
buttonGroup: [{
text: '加入购物车',
backgroundColor: '#ff0000',
color: '#fff'
},
{
text: '立即购买',
backgroundColor: '#ffa200',
color: '#fff'
}
]
};
},
3、在页面中使用 uni-goods-nav 商品导航组件,并设置style美化样式将组件固定定位在页面底部。
<!-- 商品导航组件 -->
<view class="goods_nav" :style="{position:'fixed',bottom:'0',left:'0',width:'100%'}">
<!-- fill 控制右侧按钮的样式 -->
<!-- options 左侧按钮的配置项 -->
<!-- buttonGroup 右侧按钮的配置项 -->
<!-- click 左侧按钮的点击事件处理函数 -->
<!-- buttonClick 右侧按钮的点击事件处理函数 -->
<uni-goods-nav :fill="true" :options="options" :buttonGroup="buttonGroup" @click="switchHandler" @buttonClick="buttonClick"/>
</view>
4、在页面methods节点中,声明点击事件按处理函数。点击商品导航组件左侧的按钮,会触发 uni-goods-nav 的 @click 事件处理函数事件对象,然后进行页面跳转。
// 左侧按钮的点击事件处理函数
switchHandler(e) {
if (e.content.text === '购物车') {
// 切换到购物车页面
uni.switchTab({
url: '/pages/cart/cart'
})
}
if(e.content.text==='店铺'){
uni.switchTab({
url:'/pages/home/home'
})
}
},
三、配置vuex
1、在项目根目录中创建 store 文件夹,专门用来存放 vuex 相关的模块。
2、在 store 目录上鼠标右键,选择 新建 -> js文件,新建 store.js 文件。
//store.js
// 1. 导入购物车的 vuex 模块
import {createStore} from 'vuex'
export default createStore({
// TODO:挂载 store 模块
modules:{}
})
3、在store 目录上鼠标右键,选择 新建 -> js文件,新建 cart.js 文件。
// cart.js
export default {
// 为当前模块开启命名空间
namespaced: true,
// 模块的 state 数据
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
cart: [],
}),
// 模块的 mutations 方法
mutations: {},
// 模块的 getters 属性
getters: {},
}
4、在 store/store.js 模块中,导入并挂载cart的 vuex 模块。
//store.js
// 1. 导入购物车的 vuex 模块
import {createStore} from 'vuex'
import moduleCart from '@/store/cart.js'
export default createStore({
modules:{
// 2. 挂载购物车的 vuex 模块,模块内成员的访问路径被调整为 m_cart。
// 购物车模块中 cart 数组的访问路径是 m_cart/cart
m_cart: moduleCart,
}
})
5、在main.js中导入并配置store,此处有点坑说多了都是泪~
//main.js
//导入store 模块
import store from '@/store/store.js'
// #ifndef VUE3
import Vue from 'vue'
import App from './App'
Vue.prototype.$store = store
Vue.config.productionTip = false
// App.mpType = 'app'
const app = new Vue({
store,
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
app.use(store) //此处引入store ,注意与vue2.x的引入方法有所不同。此处必须用.use(store)
return {
app
}
}
// #endif
五、在页面中使用 Store 中的数据
1、在 页面中修改 <script></script> 标签内内容。
2、注意:要映射 mutations 方法,还是 getters 属性,还是 state 中的数据,都需要指定模块的名称,才能进行映射。
// 从 vuex 中按需导出 mapState 辅助方法
import { mapState } from 'vuex'
export default {
computed: {
// 调用 mapState 方法,把 m_cart 模块中的 cart 数组映射到当前页面中,作为计算属性来使用
// ...mapState('模块的名称', ['要映射的数据名称1', '要映射的数据名称2'])
...mapState('m_cart', ['cart']),
},
}
六、实现加入购物车的功能
1、在 store 目录下的 cart.js 模块中,封装一个将商品信息加入购物车的 mutations 方法,命名为 addToCart。
// cart.js
export default {
// 为当前模块开启命名空间
namespaced: true,
// 模块的 state 数据
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
cart: [],
}),
// 模块的 mutations 方法
mutations: {
addToCart(state, goods) {
// 根据提交的商品的Id,查询购物车中是否存在这件商品
// 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
const findResult = state.cart.find((x) => x.goods_id === goods.goods_id)
if (!findResult) {
// 如果购物车中没有这件商品,则直接 push
state.cart.push(goods)
} else {
// 如果购物车中有这件商品,则只更新数量即可
findResult.goods_count++
}
// 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
this.commit('m_cart/saveToStorage')
},
},
}
2、在页面中通过 mapMutations 这个辅助方法,把 vuex 中 m_cart 模块下的 addToCart 方法映射到当前页面的methods节点下。
// 按需导入 mapMutations 这个辅助方法
import { mapMutations } from 'vuex'
export default {
methods: {
// 把 m_cart 模块中的 addToCart 方法映射到当前页面使用
...mapMutations('m_cart', ['addToCart']),
},
}
3、在页面methods节点下,为导航组件 uni-goods-nav 绑定 @buttonClick="buttonClick" 事件处理函数。
// 底部按钮的点击事件处理函数
buttonClick(e){
//判断是否点击了加入购物车按钮
if(e.content.text === '加入购物车'){
//组织一个商品对象
const goods = {
goods_id:this.goodsInfo.goodsId,
goods_name:this.goodsInfo.goodsName,
goods_price:this.goodsInfo.price,
goods_imgurl:this.goodsInfo.goodsImgUrl,
goods_count:1,
goods_state:true
}
//通过 this 调用映射过来的 addToCart 方法,把商品信息对象存储到购物车中
this.addToCart(goods)
}
},
七、动态统计购物车中商品的总数量
1、在 cart.js 模块中,在 getters 节点下定义一个 total 方法,用来统计购物车中商品的总数量
// 模块的 getters 属性
getters: {
// 统计购物车中商品的总数量
total(state){
let c = 0
// 循环统计商品的数量,累加到变量 c 中
state.cart.forEach(goods => c += goods.goods_count)
return c
}
},
2、在页面的 script 标签中,按需导入 mapGetters 方法并进行使用。
// 按需导入 mapGetters 这个辅助方法
import { mapGetters } from 'vuex'
export default {
computed: {
// 把 m_cart 模块中名称为 total 的 getter 映射到当前页面中使用
...mapGetters('m_cart', ['total']),
},
}
3、通过 watch 侦听器,监听计算属性 total 值的变化,从而动态为购物车按钮上的徽标赋值。
export default {
// 监听 total 值的变化,通过第一个形参得到变化后的新值
watch:{
//定义 total 侦听器,指向一个配置对象
total:{
handler(newVal){
//通过数组的 find() 方法,找到购物车按钮的配置对象
const findResult = this.options.find(x => x.text === '购物车')
if (findResult) {
findResult.info = newVal
}
},
// immediate 属性用来声明此侦听器,是否在页面初次加载完毕后立即调用
immediate:true
}
},
八、持久化存储购物车中的数据
1、在 cart.js 模块中,声明一个叫做 saveToStorage 的 mutations 方法,此方法负责将购物车中的数据持久化存储到本地。
2、通过this.commit('m_cart/saveToStorage')方法进行调用。
// 模块的 mutations 方法
mutations: {
addToCart(state, goods) {
// 根据提交的商品的Id,查询购物车中是否存在这件商品
// 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
const findResult = state.cart.find((x) => x.goods_id === goods.goods_id)
if (!findResult) {
// 如果购物车中没有这件商品,则直接 push
state.cart.push(goods)
} else {
// 如果购物车中有这件商品,则只更新数量即可
findResult.goods_count++
}
// 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
this.commit('m_cart/saveToStorage')
},
// 将购物车中的数据持久化存储到本地
saveToStorage(state){
uni.setStorageSync('cart',JSON.stringify(state.cart))
}
},
3、修改 cart.js 模块中的 state 函数,读取本地存储的购物车数据,对 cart 数组进行初始化。
// 模块的 state 数据
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
// 每个商品的信息对象,都包含如下 6 个属性:
// { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
cart: JSON.parse(uni.getStorageSync('cart') || '[]')
}),
4、至此,以下是cart,js的完整代码。
// cart.js
export default {
// 为当前模块开启命名空间
namespaced: true,
// 模块的 state 数据
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
// 每个商品的信息对象,都包含如下 6 个属性:
// { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
cart: JSON.parse(uni.getStorageSync('cart')||'[]'),
}),
// 模块的 mutations 方法
mutations: {
addToCart(state, goods) {
// 根据提交的商品的Id,查询购物车中是否存在这件商品
// 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
const findResult = state.cart.find((x) => x.goods_id === goods.goods_id)
if (!findResult) {
// 如果购物车中没有这件商品,则直接 push
state.cart.push(goods)
} else {
// 如果购物车中有这件商品,则只更新数量即可
findResult.goods_count++
}
// 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
this.commit('m_cart/saveToStorage')
},
// 将购物车中的数据持久化存储到本地
saveToStorage(state){
uni.setStorageSync('cart',JSON.stringify(state.cart))
}
},
// 模块的 getters 属性
getters: {
// 统计购物车中商品的总数量
total(state){
let c = 0
// 循环统计商品的数量,累加到变量 c 中
state.cart.forEach(goods=>c+=goods.goods_count)
return c
}
},
}
九、 动态为 tabBar 页面设置数字徽标
1、需求描述:tabBar页面包含首页、分类、购物车、我的 共计四个页面。在当前页面点击购物车按钮进行页面跳转后,跳转的页面并不会渲染出购物车徽标上的数字。此时可以使用 Vue 提供的mixins特性进行代码抽离。依次在tabBar上每个页面进行调用。
2、在项目根目录中新建 mixins 文件夹,并在 mixins 文件夹之下新建 tabbar-badge.js 文件,用来把设置 tabBar 徽标的代码封装为一个 mixin 文件。
import { mapGetters } from 'vuex'
// 导出一个 mixin 对象
export default {
computed: {
...mapGetters('m_cart', ['total']),
},
onShow() {
// 在页面刚展示的时候,设置数字徽标
this.setBadge()
},
methods: {
setBadge() {
// 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
uni.setTabBarBadge({
index: 2,
text: this.total + '', // 注意:text 的值必须是字符串,不能是数字
})
},
},
}
3、修改 首页、分类、购物车、我的 这 4 个 tabBar 页面的源代码,分别导入 @/mixins/tabbar-badge.js 模块并进行使用。
// 导入自己封装的 mixin 模块
import badgeMix from '@/mixins/tabbar-badge.js'
export default {
// 将 badgeMix 混入到当前的页面中进行使用
mixins: [badgeMix],
}
十、部分代码
<template>
<view>
<!--轮播图区开始-->
<swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular = "true">
<swiper-item v-for="(item,i) in carouselInfo" :key = "i">
<image :src="item.imgUrl" @click="preview(i)"></image>
</swiper-item>
</swiper>
<!--轮播图区结束-->
<view class = "goods-info-box">
<view class = "goods-detail-price" v-if="goodsInfo.price!=null">
¥{{goodsInfo.price}}
</view>
<!--商品主体信息区域开始-->
<view class = "goods-info-body">
<view class = "goods-desc">{{goodsInfo.desc}}</view>
<view class = "goods-sc">
<uni-icons type="star" size="18" color="gray"></uni-icons>
<text>收藏</text>
</view>
</view>
<!--商品主体信息区域结束-->
<!-- 运费 -->
<view class="yf">快递:此处没有快递</view>
</view>
<!--商品主体信息区域结束-->
<!--商品详情图片信息区域开始-->
<view class = "goods-detail-img-box">
<view class = "goods-detail-img" v-for="(item,i) in detailInfo" :key="i">
<image :src="item.imgUrl"></image>
</view>
</view>
<!--商品详情图片信息区域结束-->
<!-- 商品导航组件 -->
<view class="goods_nav" :style="{position:'fixed',bottom:'0',left:'0',width:'100%'}">
<!-- fill 控制右侧按钮的样式 -->
<!-- options 左侧按钮的配置项 -->
<!-- buttonGroup 右侧按钮的配置项 -->
<!-- click 左侧按钮的点击事件处理函数 -->
<!-- buttonClick 右侧按钮的点击事件处理函数 -->
<uni-goods-nav :fill="true" :options="options" :buttonGroup="buttonGroup" @click="switchHandler" @buttonClick="buttonClick"/>
</view>
</view>
</template>
<script>
// 从 vuex 中按需导出 mapState 辅助方法
import { mapState,mapMutations,mapGetters} from 'vuex'
export default {
// 监听 total 值的变化,通过第一个形参得到变化后的新值
watch:{
//定义 total 侦听器,指向一个配置对象
total:{
handler(newVal){
//通过数组的 find() 方法,找到购物车按钮的配置对象
const findResult = this.options.find(x => x.text === '购物车')
if (findResult) {
findResult.info = newVal
}
},
// immediate 属性用来声明此侦听器,是否在页面初次加载完毕后立即调用
immediate:true
}
},
computed: {
// 调用 mapState 方法,把 m_cart 模块中的 cart 数组映射到当前页面中,作为计算属性来使用
// ...mapState('模块的名称', ['要映射的数据名称1', '要映射的数据名称2'])
...mapState('m_cart', ['cart']),
// 把 m_cart 模块中名称为 total 的 getter 映射到当前页面中使用
...mapGetters('m_cart',['total'])
},
data() {
return {
//商品详情图片结果
detailInfo:[],
//轮播图结果
carouselInfo:[],
//商品信息对象
goodsInfo:{
goodsId:'',
price:0,
desc:'',
goodsName:'',
goodsImgUrl:''
},
// 左侧按钮组的配置对象
options: [{
icon: 'shop',
text: '店铺'
}, {
icon: 'cart',
text: '购物车',
info: ''
}],
// 右侧按钮组的配置对象
buttonGroup: [{
text: '加入购物车',
backgroundColor: '#ff0000',
color: '#fff'
},
{
text: '立即购买',
backgroundColor: '#ffa200',
color: '#fff'
}
]
};
},
onLoad(option){
//向服务器发送请求 获取商品详情数据
this.getGoodsDetail(option.goods_id)
//向服务器发送请求 获取商品轮播数据
this.getGoodsCarousel(option.goods_id)
//向服务器发送请求 获取商品基础信息
this.getGoodsInfo(option.goods_id)
},
methods:{
// 把 m_cart 模块中的 addToCart 方法映射到当前页面使用
...mapMutations('m_cart', ['addToCart']),
// 定义请求商品详情数据的方法
async getGoodsDetail(goods_id){
const res = await uni.$http.get('/goods_detail/getDetailByLID?lId='+goods_id+'&mark=0')
console.log("res==>",res)
//如果请求状态异常 弹出消息
if(res.data.status !== 200){return uni.$showMsg()}
//否则将请求结果存到商品详情结果数组中
this.detailInfo = res.data.data
console.log("detailInfo==>",this.detailInfo)
},
async getGoodsCarousel(goods_id){
const res = await uni.$http.get('/goods_detail/getDetailByLID?lId='+goods_id+'&mark=1')
console.log("res==>",res)
//如果请求状态异常 弹出消息
if(res.data.status !== 200){return uni.$showMsg()}
//否则将请求结果存到商品详情结果数组中
this.carouselInfo = res.data.data
},
async getGoodsInfo(goods_id){
const res = await uni.$http.get('/goods_list/v1/findById/'+goods_id)
//如果请求状态异常 弹出消息
if(res.data.status !== 200){return uni.$showMsg()}
console.log("resstatus==>",res)
//否则将请求结果存到商品中
this.goodsInfo.goodsId = res.data.data.id
this.goodsInfo.price = res.data.data.price
this.goodsInfo.desc = res.data.data.goodsDesc
this.goodsInfo.goodsName = res.data.data.goodsName
this.goodsInfo.goodsImgUrl = res.data.data.imgUrl
},
//点击轮播图预览图片
preview(i){
//调用uni.previewImage()方法预览图片
uni.previewImage({
// 预览时,默认显示图片的索引
current: i,
// 所有图片 url 地址的数组
urls: this.carouselInfo.map(x => x.imgUrl)
})
},
// 左侧按钮的点击事件处理函数
switchHandler(e) {
console.log("e==>",e)
if (e.content.text === '购物车') {
// 切换到购物车页面
uni.switchTab({
url: '/pages/cart/cart'
})
}
if(e.content.text==='店铺'){
uni.switchTab({
url:'/pages/home/home'
})
}
},
// 底部按钮的点击事件处理函数
buttonClick(e){
//判断是否点击了加入购物车按钮
if(e.content.text === '加入购物车'){
//组织一个商品对象
const goods = {
goods_id:this.goodsInfo.goodsId,
goods_name:this.goodsInfo.goodsName,
goods_price:this.goodsInfo.price,
goods_imgurl:this.goodsInfo.goodsImgUrl,
goods_count:1,
goods_state:true
}
// 3. 通过 this 调用映射过来的 addToCart 方法,把商品信息对象存储到购物车中
this.addToCart(goods)
}
},
},
}
</script>
<style lang="scss">
swiper{
height: 420rpx;
image{
width: 100%;
height: 100%;
}
}
.goods-info-box{
padding: 10px;
padding-right: 0;
background-color: #fff;
.goods-detail-price{
color:#c00000;
font-size: 18px;
margin: 10px 0;
}
.goods-info-body{
display: flex;
justify-content: space-between;
.goods-desc{
font-size: 13px;
padding-right: 10px;
}
.goods-sc{
width: 120px;
font-size: 12px;
display: flex;
flex-direction: column;
align-items: center;
border-left: 1px solid #efefef;
color:gray
}
}
.yf{
margin: 10px 0;
font-size: 12px;
color:gray;
}
}
.goods-detail-img-box{
padding-bottom: 50px;
.goods-detail-img{
height: 240px;
image{
width: 100%;
}
}
}
</style>