慕尚花坊 --- 购物车

0. 准备工作

// 将原来的 Page 换位 Component
Component({
    
})

1. 页面构建

pages/cart/cart.json

{
  "usingComponents": {
    "van-card": "@vant/weapp/card/index",
    "van-empty": "@vant/weapp/empty/index",
    "van-button": "@vant/weapp/button/index",
    "van-swipe-cell": "@vant/weapp/swipe-cell/index",
    "van-checkbox": "@vant/weapp/checkbox/index",
    "van-stepper": "@vant/weapp/stepper/index",
    "van-submit-bar": "@vant/weapp/submit-bar/index"
  }
}

pages/cart/cart.wxml

<view>
  <!-- 购物车列表 -->
  <view class="container" wx:if="{{ cartList.length !== 0 }}">
    <view class="goods-item" wx:for="{{ cartList }}" wx:key="goodsId">
      <van-swipe-cell class="goods-swipe" right-width="{{ 65 }}" async-close bind:close="onClose">
        <van-cell-group>
          <view class="goods-info">
            <view class="left">
              <van-checkbox value="{{ checked }}" checked-color="#FA4126" bind:change="onChange" />
            </view>
            <view class="mid">
              <image class="img" src="{{ item.imageUrl }}" mode="" />
            </view>
            <view class="right">
              <view class="title">{{ item.name }}</view>
              <view class="right-bottom">
                <view class="price">¥{{ item.price }}</view>
                <!-- 点击的是加入购物车才显示步进器组件来修改商品数量 -->
                <van-stepper class="stepper" button-size="22px" min="1" integer value="{{ item.count }}" bind:change="onChangeGoodsCount" />
              </view>
            </view>
          </view>

        </van-cell-group>
        <view slot="right">删除</view>
      </van-swipe-cell>
    </view>
  </view>

  <!-- 购物车列表为空时, 展示的结构 -->
  <van-empty wx:else description="{{ emptyDes }}">
    <van-button type="danger" round>去购物</van-button>
  </van-empty>

  <van-submit-bar price="{{ 3050 }}" button-text="去结算" bind:submit="onClickButton" tip="{{ true }}">
    <van-checkbox value="{{ selectAll }}" checked-color="#FA4126" bind:change="onChangeCheckBox"> 全选 </van-checkbox>
  </van-submit-bar>
</view>

pages/cart/cart.scss

.container{
  .goods-item {
    background-color: rgb(240, 240, 240);
    padding: 25rpx;
    width: 690rpx;
    margin-top: 20rpx;
    .goods-swipe {
      .goods-info {
        display: flex;
        height: 280rpx;
        align-items: center;
        .left {
          van-checkbox {
            line-height: 280rpx;
  
          }
        }
  
        .mid {
          .img {
            width: 240rpx;
            height: 280rpx;
          }
        }
  
        .right {
          width: 400rpx;
          .title {
            margin-left: 12rpx;
            font-weight: 1000;
            overflow: hidden; // 超出的文字以省略号代替
            text-overflow: ellipsis;
            white-space: nowrap;
          }
  
          .right-bottom {
            display: flex;
  
            .price {
              margin-top: 160rpx;
              margin-left: 12rpx;
              color: orange;
              font-size: 40rpx;
            }
  
            .stepper {
              margin-top: 160rpx;
              margin-left: 100rpx;
            }
          }
        }
      }
  
    }
  }
}

2. 判断用户是否登录

1. 购物车关联 Store 对象

/pages/cart/cart.js

import {
  ComponentWithStore
} from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userStore'

ComponentWithStore({
  // 让页面和 Store 对象建立关联
  storeBindings: {
    store: userStore,
    fields: ['token']
  },
    
  /**
   * 页面的初始数据
   */
  data: {

  },

  methods: {
    
  }
})

2. 判断用户是否登录

  1. 如果没有登录, 购物车页面需要显示: 您尚未登录, 点击登录获取更多权益

pages/cart/cart.js

import {
  ComponentWithStore
} from 'mobx-miniprogram-bindings'
import {
  userStore
} from '@/stores/userStore'

ComponentWithStore({
  // 让页面和 Store 对象建立关联
  storeBindings: {
    store: userStore,
    fields: ['token'],
  },
  /**
   * 页面的初始数据
   */
  data: {
    cartList: [],
    emptyDes: '还没有添加商品, 快去添加吧~',
  },

  methods: {

    // 展示文案信息, 同时获取购物车列表数据
    showTipAndGetList() {
      const {
        token
      } = this.data
      // 如果用户没登录
      if (!token) {
        this.setData({
          emptyDes: '您尚未登录, 点击登录获取更多权益',
          cartList: []
        })
        return
      }

    },

    onShow() {
      this.showTipAndGetList()
    }
  }
})

3. 购物车列表

  1. 如果用户已经登录, 则获取购物车列表数据
    1. 如果购物车内没有商品, 显示: 还没有添加商品, 快去添加吧~
    2. 购物车列表有商品, 则使用数据对页面进行渲染

pages/cart/cart.wxml

<view class="container">
  <!-- 购物车列表 -->
   <view class="container" wx:if="{{ token && cartList.length !== 0 }}">
    <view class="goods-item" wx:for="{{ cartList }}" wx:key="goodsId">
      <van-swipe-cell class="goods-swipe" right-width="{{ 65 }}" async-close bind:close="onClose">
        <van-cell-group>
          <view class="goods-info">
            <view class="left">
              <van-checkbox value="{{ item.ischecked }}" checked-color="#FA4126" bind:change="onChange" />
            </view>
            <view class="mid">
              <image class="img" src="{{ item.imageUrl }}" mode="" />
            </view>
            <view class="right">
              <view class="title">{{ item.name }}</view>
              <view class="right-bottom">
                <view class="price">¥{{ item.price }}</view>
                <!-- 点击的是加入购物车才显示步进器组件来修改商品数量 -->
                <van-stepper class="stepper" button-size="22px" min="1" integer value="{{ item.count }}" bind:change="onChangeGoodsCount" />
              </view>
            </view>
          </view>

        </van-cell-group>
        <view slot="right">删除</view>
      </van-swipe-cell>
    </view>
  </view>

  <!-- 购物车列表为空时, 展示的结构 -->
  <van-empty wx:else description="{{ emptyDes }}">
    <navigator url="/pages/index/index">
      <van-button wx:if="{{ !token }}" type="danger" round>去登录</van-button>
    </navigator>
    <navigator url="/pages/index/index" open-type="switchTab">
      <van-button wx:else type="danger" round>去购物</van-button>
    </navigator>
  </van-empty>
  
</view>

pages/cart/cart.js

import {
  ComponentWithStore
} from 'mobx-miniprogram-bindings'
import {
  userStore
} from '@/stores/userStore'
import {
  reqCartList
} from '@/api/cart'

ComponentWithStore({
  // 让页面和 Store 对象建立关联
  storeBindings: {
    store: userStore,
    fields: ['token'],
  },
    
  /**
   * 页面的初始数据
   */
  data: {
    cartList: [],
    emptyDes: '还没有添加商品, 快去添加吧~',
  },

  methods: {

    // 展示文案信息, 同时获取购物车列表数据
    async showTipAndGetList() {
      const {
        token
      } = this.data
      if (!token) {
        this.setData({
          emptyDes: '您尚未登录, 点击登录获取更多权益',
          cartList: []
        })
        return
      }

      // 如果用户登录了, 则需要请求购物车列表,
      const {
        code,
        data: cartList
      } = await reqCartList()
      if (code === 200) {
        this.setData({
          cartList,
          emptyDes: cartList.length === 0 ? '还没有添加商品, 快去添加吧~' : emptyDes
        })
      }
    },

    onShow() {
      this.showTipAndGetList()
    }
  }
})

4. 更新商品的选择状态

点击商品的复选框时, 更新商品的购买状态

  1. 获取商品最新的购买状态, 将最新的状态同步到服务器(需要调用封装的接口 API 函数, 0: 不购买;1: 购买)
  2. 在服务器更新商品状态成功后, 将本地的数据改变

pages/cart/cart.wxml

<van-checkbox value="{{ item.isChecked }}" checked-color="#FA4126" bind:change="onChangeCheckBox" data-id="{{ item.goodsId }}" data-index="{{ index }}"/>

pages/cart/cart.js

import {
  ComponentWithStore
} from 'mobx-miniprogram-bindings'
import {
  userStore
} from '@/stores/userStore'
import {
  reqCartList,
  reqUpdateChecked
} from '@/api/cart'

ComponentWithStore({
  data: {
    cartList: [],
    emptyDes: '还没有添加商品, 快去添加吧~',
  },

  methods: {
    async onChangeCheckBox(event) {
      // 获取用户的点击状态
      const {
        detail
      } = event

      // 获取携带的参数
      const {
        id,
        index
      } = event.currentTarget.dataset
      const isChecked = detail ? 1 : 0

      // 方法一: 发送请求, 更新商品的购买状态, 重新发送请求购物车列表页,并替换本地的数据
      const res = await reqUpdateChecked(id, isChecked)
      // if (res.code === 200){
      //   this.showTipGetList()
      // }

      // 方法二: 发送请求, 更新商品的购买状态,找到点击的商品索引, 更新对应索引数据的isChecked
      if (res.code === 200) {
        this.setData({
          [`cartList[${index}].isChecked`]: isChecked
        })
      }
    }
  }
})

5. 计算是否全选 (计算属性)

基于购物车列表中已有的数据, 产生一个新的数据, 来控制全选按钮的选中效果, 就可以使用 计算属性 来实现

由于 ComponentWithComputed 方法 不能与 ComponentWithStore 方法 结合使用, 所以需要使用老版本的写法

pages/cart/cart.wxml

<van-submit-bar price="{{ 3050 }}" button-text="去结算" bind:submit="onClickButton" tip="{{ true }}">
  <van-checkbox value="{{ selectAllStatus }}" checked-color="#FA4126"> 全选 </van-checkbox>
</van-submit-bar>

pages/cart/cart.js

// 1. 导入 computedBehavior
const computedBehavior = require('miniprogram-computed').behavior

ComponentWithStore({
  // 2. 注册 computedBehavior
  behaviors: [computedBehavior],
    
  // 3. 定义计算属性, 计算属性会被挂载到 data 中
  computed:{
    // 判断是否是全选, 控制全选按钮的选中效果
    selectAllStatus(data){
      // computed 函数不能使用 this 来访问 data 中的数据, 需要使用形参 data

      return data.cartList.length !== 0 && data.cartList.every(item => item.isChecked === 1)
    }
  },
})

6. 点击全选按钮

用户点击全选, 控制所有商品的选中和全不选效果

  1. 点击全选按钮, 获取全选按钮的选中状态
  2. 将状态同步给服务区 (1 是全选, 0 是全不选)
  3. 服务器更新成功后, 更新本地数据

pages/cart/cart.wxml

<van-submit-bar price="{{ 3050 }}" button-text="去结算" bind:submit="onClickButton" tip="{{ true }}">
  <!-- 绑定 change 事件 -->
  <van-checkbox value="{{ selectAllStatus }}" checked-color="#FA4126" bind:change="onChangeAllCheckBox"> 全选 </van-checkbox>
</van-submit-bar>

pages/cart/cart.js

import {
  reqCheckAllStatus
} from '@/api/cart'
const computedBehavior = require('miniprogram-computed').behavior

ComponentWithStore({
 
  methods: {
    async onChangeAllCheckBox(event){
      // 全选按钮的选中状态
      const {detail} = event

      const isChecked = detail ? 1 : 0
      // 发送请求, 修改所有商品的选中状态, 并更新本地数据
      const res = await reqCheckAllStatus(isChecked)
      if (res.code === 200){
        // 方式一: 发送请求, 更新商品的购买状态, 重新发送请求购物车列表页,并替换本地的数据
        //  this.showTipGetList()
          
        // 方式二: 发送请求, 更新商品的购买状态,循环每个商品, 修改每个元素的 isChecked 属性
        // 对购物车列表数据进行深拷贝
      	const newCartList = JSON.parse(JSON.stringify(this.data.cartList))
      	newCartList.forEach(item => item.isChecked = isChecked)
      	this.setData({
          cartList: newCartList
      	})
      }
    }
  }
})

7. 更新商品数量

当修改商品数量时, 不是直接将修改后的数量直接传给服务器, 而是计算差值 然后服务器进行算术运算

image-20240730215207044

校验用户输入的商品数量是不是一个正整数

const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/

pages/cart/cart.html

<van-stepper class="stepper" button-size="22px" min="1" max="200" integer value="{{ item.count }}" bind:change="onChangeGoodsCount" data-id="{{ item.goodsId }}" data-index="{{ index }}" data-oldnum="{{ item.count }}"/>

pages/cart/cart.js

import {
  reqAddToCart
} from '@/api/cart'

ComponentWithStore({

  methods: {
    async onChangeGoodsCount(event) {
      // 用户最新输入的商品数量, 如果用户输入的购买数量大于200, 则需要把购买数量重置为200, 小于1,则重置为1
      // 最大购买数量为200, 如果输入的是666, 重置为200, 差值为 200 - 1 = 199, 数据库为 199 + 1 = 200
      const newNum = event.detail > 200 ? 200 : event.detail

      // 获取携带的自定义数据
      const {
        id,
        index,
        oldnum
      } = event.currentTarget.dataset

      // 检验最新购买数量是否在 1 - 200 之间
      const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
      const regRes = reg.test(newNum)
      if (!regRes) {
        // 如果验证未通过, 说明用户输入的数量不合法或者小于1, 需要还原为之前的购买数量, 并提示
        this.setData({
          [`cartList[${index}].count`]: oldnum
        })
        toast({
          title: "商品数量输入不合法!"
        })
        return
      }

      // 如果验证通过, 计算出差值, 将差值发送给服务器
      const disCount = newNum - oldnum
      console.log(disCount);
      // 判断是否发生改变, 如果没有发生改变, 不发送请求
      if (disCount === 0) return


      // 发送请求到服务器
      const res = await reqAddToCart(id, disCount)
      if (res.code === 200) {
        // 方法一: 重新发送请求, 获取本地购物车列表
        // this.showTipAndGetList()

        // 方法二: 找到对应索引的商品,更新购买数量
        this.setData({
          [`cartList[${index}].count`]: newNum,
          // 如果购买数量发生了变化, 需要将当前商品变成选中的状态
          [`cartList[${index}].isChecked`]: 1
        })
      }
    },

  }
})

8. 更新商品数量防抖

1. 官方文档

https://licia.liriliri.io/zh/

2. npm下载

npm i miniprogram-licia 

3. 防抖文档

https://licia.liriliri.io/zh/document.html#debounce

3. 使用

// 1. 导入
import {debounce} from 'miniprogram-licia'



ComponentWithStore({

  methods: {
	// 2. 改成普通函数, 并添加延迟时间	
    onChangeGoodsCount: debounce(async function (event) {
    }, 500),
  }
})

9. 总金额合计

  1. 判断哪些商品被勾选
  2. 将勾选的商品数量 * 商品单价, 然后进行累加
  3. 每次用户更新了商品的勾选状态 或 商品的购买数量, 都需要重新计算总金额
  4. 通过计算属性来实现

pages/cart/cart.html

<!-- 默认是以 分 为单位展示金额, 这里需要将总金额 * 100 -->
<van-submit-bar price="{{ totalPrice * 100 }}" button-text="去结算" bind:submit="onClickButton" tip="{{ true }}">
    <van-checkbox value="{{ selectAllStatus }}" checked-color="#FA4126" bind:change="onChangeAllCheckBox"> 全选 </van-checkbox>
</van-submit-bar>

pages/cart/cart.js

ComponentWithStore({
  computed: {
    // 计算商品总金额
    totalPrice(data){
      let totalPrice = 0

      data.cartList.forEach(item => {
        // 判断商品是否是选中的状态
        if (item.isChecked){
          totalPrice += item.count * item.price
        }
      })
      return totalPrice
    }
  },
})

10. 删除商品

pages/cart/cart.wxml

<view bind:tap="onSwipeCellPage">
  <van-swipe-cell class="goods-swipe" id="swipe-cell-{{ item.goodsId }}" right-width="{{ 65 }}" bind:close="onClose" data-id="{{  item.goodsId }}" bind:open="swipeCellOpen" bind:tap="onSwipeCellClick">
    <view slot="right" class="van-swipe-cell__right" bind:tap="delCartGoods">删除</view>
  </van-swipe-cell>
</view>

pages/cart/cart.js

import {
  reqDelCartGoods
} from '@/api/cart'

import {
  swipeCellBehavior
} from '@/behaviors/swipeCell'

ComponentWithStore({
  behaviors: [swipeCellBehavior],
  methods: {

    async delCartGoods(event){
      const {id} = event.currentTarget.dataset

      // 询问用户是否删除该商品
      const res = await modal({
        content: '您确认删除该商品吗?',
      })
      if (!res) return

      const resReful = await reqDelCartGoods(id)

      if (resReful.code === 200){
        toast({
          title: "删除成功"
        })
	   
        // 重新请求购物车列表数据
        this.showTipAndGetList()
      }
    },
    onShow() {
      this.showTipAndGetList()
    },

    // 关闭滑块
    onHide(){
      this.onSwipeCellCommonClick()
    }
  }
})
posted @ 2024-07-04 15:37  河图s  阅读(31)  评论(0)    收藏  举报