WX小程序学习笔记(Shopping Cart)

一、目标说明

本章目标将实现微信小程序中的购物车功能。如下图:


20250114191355.png

20250114192060.png

二、渲染页底部的导航区域

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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,976评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,249评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,449评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,433评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,460评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,132评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,721评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,641评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,180评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,267评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,408评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,076评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,767评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,255评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,386评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,764评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,413评论 2 358

推荐阅读更多精彩内容