慕尚花坊 --- 分类

0. 配置

1. 数据请求 API

1. 定义接口 API 函数

miniprogram/api/cate.js

import http from '@/utils/http'

/**
 * @description 获取商品分类的数据
 * @returns Promise
 */
export const reqCategoryData = () => {
  return http.get('/index/findCategoryTree')
}

pages/cate/cate.js

import {
  reqCategoryData
} from '@/api/category'
Page({
  data: {
    categoryList: []
  },

  // 获取商品分类数据
  async getCategoryData() {
    const res = await reqCategoryData()
    console.log(res);
    this.setData({
      categoryList: res.data
    })
  },
  onLoad(options) {
    // 获取商品分类数据
    this.getCategoryData()
  },
})

2. 全局样式

1. 一级分类

**需求: **

  1. 当进入分类页面的时候, 第一个一级分类默认是高亮选中的状态
  2. 当点击任意的一级分类以后, 对应的一级分类需要高亮选中, 其余的一级分类取消高亮选中

0. 效果图

1. 页面创建

pages/cate/cate.wxml

<view>
  <view class="category-container">
    <!-- 左侧的滚动视图区域 -->
    <scroll-view class="category-left-view" scroll-y bind:tap="clickOneCategory">
      <view class="left-view-item active"> 鲜花玫瑰 </view>
      <view class="left-view-item active"> 鲜花玫瑰 </view>
      <view class="left-view-item active"> 鲜花玫瑰 </view>
      <view class="left-view-item active"> 鲜花玫瑰 </view>
      <view class="left-view-item active"> 鲜花玫瑰 </view>
    </scroll-view>
  </view>
</view>

pages/cate/cate.scss

.category-container {
  display: flex;

  .category-left-view {
    width: 340rpx;
    padding: 10rpx 5rpx;

    .left-view-item {
      height: 65rpx;
      padding: 16rpx 20rpx;
      line-height: 50rpx;
      border-right: 1rpx solid #efefef;
    }

    .active {
      &::before {
        content: '|';
        background-color: #f3514f;   // 使 | 加粗
        margin-right: 20rpx;
      }
      color: #f3514f;
    }
  }
}

2. 数据交互

1. 根据数据渲染页面

<view>
  <view class="category-container">
    <!-- 左侧的滚动视图区域 -->
    <scroll-view class="category-left-view" scroll-y bind:tap="clickOneCategory">
      <block wx:for="{{ categoryData }}" wx:key="id">
        <view class="left-view-item">{{item.name}} </view>
      </block>
    </scroll-view>
  </view>
</view>

2. 选中效果

1. 先初始化数据 activeIndex, 代表被激活的一级分类, 默认值为0

pages/cate/cate.js

import {
  reqCategoryData
} from '@/api/cate'
Page({
  data: {
    activeIndex: 0,    // 用户点击的一级菜单索引
    categoryData: []
  },

  async getCategoryData() {
    const res = await reqCategoryData()
    this.setData({
      categoryData: res.data
    })
  },

  clickOneCategory(event){
    this.setData({
      activeIndex: event.target.dataset.index
    })
  },

  onLoad(options) {
    this.getCategoryData()
  },

})

2. 使用事件冒泡给左侧菜单栏绑定点击事件, 当点击某个一级分类时, 将对应的索引赋值给 activeIndex

pages/cate/cate.wxml

<view>
  <view class="category-container">
    <!-- 绑定点击事件 -->
    <scroll-view class="category-left-view" scroll-y bind:tap="clickOneCategory">
      <block wx:for="{{ categoryData }}" wx:key="id">
        <!-- 传递当前标签的 index,并对比用户点击的索引是否与当前标签索引一致,如果一致就添加 active 高亮效果 -->
        <view data-index="{{ index }}" class="left-view-item {{ activeIndex === index ? 'active' : '' }}">{{item.name}} </view>
      </block>
    </scroll-view>
  </view>
</view>

pages/cate/cate.js

import {
  reqCategoryData
} from '@/api/cate'
Page({
  data: {
    activeIndex: 0,
    categoryData: []
  },

  async getCategoryData() {
    const res = await reqCategoryData()
    this.setData({
      categoryData: res.data
    })
  },
	
  // 获取用户点击的索引, 并赋值给 activeIndex
  clickOneCategory(event){
    this.setData({
      activeIndex: event.target.dataset.index
    })
  },
  onLoad(options) {
    this.getCategoryData()
  },
})

2. 二级分类

0. 效果图

1. 页面创建

pages/cate/cate.wxml

<view>
  <view class="category-container">
    <!-- 左侧的滚动视图区域 -->
    <scroll-view class="category-left-view" scroll-y bind:tap="clickOneCategory">
    </scroll-view>

    <!-- 右侧的滚动视图区域 -->
    <scroll-view class="category-right-view" scroll-y>
      <!-- 二级分类 -->
      <view class="right--view-container">
        <view class="right-view-item">
          <navigator class="navigator-nav" url="">
            <image class="img" src="../../assets/categorys/cate-1.png" mode="" />
            <text class="text">真情告白</text>
          </navigator>
        </view>
        <view class="right-view-item">
          <navigator class="navigator-nav" url="">
            <image class="img" src="../../assets/categorys/cate-1.png" mode="" />
            <text class="text">真情告白</text>
          </navigator>
        </view>
        <view class="right-view-item">
          <navigator class="navigator-nav" url="">
            <image class="img" src="../../assets/categorys/cate-1.png" mode="" />
            <text class="text">真情告白</text>
          </navigator>
        </view>
        <view class="right-view-item">
          <navigator class="navigator-nav" url="">
            <image class="img" src="../../assets/categorys/cate-1.png" mode="" />
            <text class="text">真情告白</text>
          </navigator>
        </view>
        <view class="right-view-item">
          <navigator class="navigator-nav" url="">
            <image class="img" src="../../assets/categorys/cate-1.png" mode="" />
            <text class="text">真情告白</text>
          </navigator>
        </view>
        
      </view>
    </scroll-view>
  </view>
</view>

pages/cate/cate.scss

.category-container {
  display: flex;

  // 一级菜单栏
  .category-left-view {
  }
  // 二级菜单栏
  .category-right-view {
    .right--view-container {
      display: flex;
      flex-wrap: wrap;

      .right-view-item {
        width: 110rpx;
        margin: 16rpx 35rpx;

        .navigator-nav {
          display: flex;
          flex-direction: column;

          .img {
            width: 110rpx;
            height: 110rpx;
          }

          .text {
            margin-top: 10rpx;
            font-size: 27rpx;
          }
        }
      }
    }
  }
}

2. 数据交互

pages/cate/cate.wxml

<view>
  <view class="category-container">
    <!-- 左侧的滚动视图区域 -->
    <scroll-view class="category-left-view" scroll-y bind:tap="clickOneCategory">
    </scroll-view>

    <!-- 右侧的滚动视图区域 -->
    <scroll-view class="category-right-view" scroll-y>
      <view class="right--view-container">
        <!-- 循环对应索引的 children 对象数组 -->
        <view class="right-view-item" wx:for="{{ categoryData[activeIndex].children }}" wx:key="id">
          <!-- 跳转二级分类的商品列表 -->
          <navigator class="navigator-nav" url="/pages/goods/list/list?category2Id={{ item.id }}">
            <image class="img" src="{{ item.imageUrl }}" mode="" />
            <text class="text">{{item.name}}</text>
          </navigator>
        </view>
      </view>
    </scroll-view>
  </view>
</view>

3. 分包和预下载

app.json

{
  "subPackages": [
    {
      "root": "modules/settingModule",
      "name": "settingModule",
      "pages": [
        "pages/setup/personal-data/personal-data",
        "pages/setup/receive-address/add/add",
        "pages/setup/receive-address/list/list"
      ]
    },
    {
      "root": "modules/goodModule",
      "name": "goodModule",
      "pages": [
        "pages/goods/list/list",
        "pages/goods/detail/detail"
      ]
    }
  ],
  "preloadRule": {
    "pages/setup/setup": {
      "network": "all",
      "packages": [
        "settingModule"
      ]
    },
    "pages/cate/cate": {
      "network": "all",
      "packages": [
        "goodModule"
      ]
    }
  }
}

全局搜索 pages/goods/list/list 和 pages/goods/detail/detail 将其替换为 /modules/goodModule/pages/goods/list/list 和 /modules/goodModule/pages/goods/detail/detail

4. 商品列表

1. 列表展示

api/goods.js

import http from '@/utils/http'

/**
 * @description 分页获取商品列表
 * @param {Object} param { page, limit, category1Id, category2Id}
 * @returns Promise 
 */
export const reqGoodList = ({page, limit, ...data}) => {
  return http.get(`/goods/list/${page}/${limit}`, data)
 }

miniprogram/modules/goodModule/pages/goods/list/list.json

{
  "usingComponents": {
    "goods-card": "../../../../../components/goods-card/goods-card",
    "van-empty": "@vant/weapp/empty/index",
    "van-button": "@vant/weapp/button/index"
  },
  "navigationBarTitleText": "商品列表"
}

miniprogram/modules/goodModule/pages/goods/list/list.wxml

<!-- 商品列表 -->
<view class="goods_container" wx:if="{{ goodsList.length }}">
  <!-- 标题 -->
  <view class="goods_title">{{ title }}</view>

  <!-- 列表区域 -->
  <view class="goods_card_list">
    <block wx:for="{{ goodsList }}" wx:key="id">
      <goods-card goodItem="{{ item }}"></goods-card>
    </block>
  </view>

  <!-- 查看更多 -->
  <view class="goods_more">查看更多</view>
</view>

<van-empty wx:else description="该分类下暂无商品, 去看看其他商品吧~">
  <van-button round type="danger" class="bottom-button" bind:tap="gotoBack">查看其他商品</van-button>
</van-empty>

miniprogram/modules/goodModule/pages/goods/list/list.js

import {
  reqGoodList
} from "@/api/goods"
Page({

  /**
   * 页面的初始数据
   */
  data: {
    goodsList: [], // 商品列表数据
    isFinish: false, // 判断数据是否加载完毕
    total: 0, // 数据总条数
    requestData: { // 商品列表请求数据
      page: 1, // 页码
      limit: 10, // 每页请求的条数
      category1Id: '', // 一级分类id
      category2Id: '' // 二级分类id
    }
  },

  // 获取商品列表
  async getGoodsList() {
    const {
      data
    } = await reqGoodList(this.data.requestData)
    this.setData({
      goodsList: data.records,
      total: data.total
    })
  },
    
  // 返回上一级页面
  gotoBack(){
    wx.navigateBack()
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    // 获取传递过来的参数, 合并对象, 后面对象的属性会覆盖前面对象的相同属性
    Object.assign(this.data.requestData, options)

    // 调用获取商品列表的方法
    this.getGoodsList()
  }
})

2. 上拉加载更多

miniprogram/modules/goodModule/pages/goods/list/list.json

{
  "onReachBottomDistance": 400
}

miniprogram/modules/goodModule/pages/goods/list/list.js

import {
  reqGoodList
} from "@/api/goods"
Page({

  /**
   * 页面的初始数据
   */
  data: {
    goodsList: [], // 商品列表数据
    isFinish: false, // 判断数据是否加载完毕
    total: 0, // 数据总条数
    requestData: { // 商品列表请求数据
      page: 1, // 页码
      limit: 10, // 每页请求的条数
      category1Id: '', // 一级分类id
      category2Id: '' // 二级分类id
    }
  },

  // 获取商品列表
  async getGoodsList() {
    const {
      data
    } = await reqGoodList(this.data.requestData)
    this.setData({
      // 将最新请求到的数据和之前的数据合并
      goodsList: [...this.data.goodsList,...data.records],
      total: data.total
    })
  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {
    console.log("上拉了")
    const { page } = this.data.requestData
    // 页码加 1
    this.setData({
      requestData: {...this.data.requestData,page: page + 1}
    })
    // 重新获取商品列表
    this.getGoodsList()
  }
})

3. 判断数据是否加载完毕

使用 goodsList 数组的长度 和数据总条数进行对比

import {
  reqGoodList
} from "@/api/goods"
Page({

  /**
   * 页面的初始数据
   */
  data: {
    goodsList: [], // 商品列表数据
    isFinish: false, // 判断数据是否加载完毕
    total: 0, // 数据总条数
    requestData: { // 商品列表请求数据
      page: 1, // 页码
      limit: 10, // 每页请求的条数
      category1Id: '', // 一级分类id
      category2Id: '' // 二级分类id
    }
  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {
    const { goodsList, total,requestData} = this.data
    const { page } = requestData

    // 对比 goodsList 和 total 对比
    if (goodsList.length === total){
      this.setData({
        isFinish: true
      })
      return
    }

    this.setData({
      requestData: {
        ...this.data.requestData,
        page: page + 1
      }
    })
    this.getGoodsList()
  },

})

4. 节流

在用户网速很慢的情况下, 如果用户在距离底部来回的进行多次滑动, 可能会发送一些无意义的请求,造成请求浪费的情况, 因此需要给上拉加载添加节流功能

  1. data 中定义节流阀状态 isLoading, 默认值为 false
  2. 在请求发送之前, 将 isLoading 设置为 true, 表示请求正在发送
  3. 在请求结束之后, 将 isLoading 设置为 false, 表示请求已经完成
  4. onReachBottom 事件监听函数中, 对 isLoading 进行判断, 如果数据正在请求中, 不请求下一次页的数据

miniprogram/modules/goodModule/pages/goods/list/list.js

import {
  reqGoodList
} from "@/api/goods"
Page({

  data: {
    goodsList: [], // 商品列表数据
    isFinish: false, // 判断数据是否加载完毕
    isLoading: false, // 判断本次请求是否正在发送
    total: 0, // 数据总条数
    requestData: { // 商品列表请求数据
      page: 1, // 页码
      limit: 10, // 每页请求的条数
      category1Id: '', // 一级分类id
      category2Id: '' // 二级分类id
    }
  },

  async getGoodsList() {
    // 在请求发送之前,将 isLoading 设置为 true, 表示请求正在发送中...
    this.data.isLoading = true
      
    // 发送请求
    const {
      data
    } = await reqGoodList(this.data.requestData)
    this.setData({
      goodsList: [...this.data.goodsList, ...data.records],
      total: data.total,
    })
      
    // 在请求发送之后,将 isLoading 设置为 false, 表示请求发送完毕
    this.data.isLoading = false
    
  },
  gotoBack() {
    wx.navigateBack()
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    Object.assign(this.data.requestData, options)

    this.getGoodsList()
  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {
    const {
      goodsList,
      total,
      requestData,
      isLoading
    } = this.data
    const {
      page
    } = requestData
    
    // 判断请求是否正在发送, 如果正在发送中,则不执行以下请求逻辑,请求下一页数据
    if (isLoading) return

    if (goodsList.length === total) {
      this.setData({
        isFinish: true
      })
      return
    }

    this.setData({
      requestData: {
        ...this.data.requestData,
        page: page + 1
      }
    })
    this.getGoodsList()
  }
})

5. 下拉刷新

下拉刷新时小程序常见的一种刷新方式, 当用户下拉页面时, 页面也会自动刷新,以便用户获取最新的内容

  1. app.json (全局配置)页面.json (当前页面配置) 中配置允许下拉, 同时可以配置 窗口、loading样式等
  2. 页面.js 中定义 onPullDownRefresh 事件监听用户下拉刷新

miniprogram/modules/goodModule/pages/goods/list/list.json

{
  "enablePullDownRefresh": true,
  "backgroundColor": "#efefef",    // 背景色
  "backgroundTextStyle": "dark",   // loading 样式
}

miniprogram/modules/goodModule/pages/goods/list/list.js

import {
  reqGoodList
} from "@/api/goods"
Page({

  data: {
    goodsList: [], // 商品列表数据
    isFinish: false, // 判断数据是否加载完毕
    isLoading: false, // 判断当前请求是否发送中...
    total: 0, // 数据总条数
    requestData: { // 商品列表请求数据
      page: 1, // 页码
      limit: 10, // 每页请求的条数
      category1Id: '', // 一级分类id
      category2Id: '' // 二级分类id
    }
  }

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh() {
    // 将所有数据进行重置
    this.setData({
      goodsList: [],
      total: 0,
      isFinish: false,
      requestData: {
        ...this.data.requestData,
        page: 1
      }
    })
    // 使用最新的参数发送请求
    this.getGoodsList()

    // 手动关闭下拉刷新的窗口
    wx.stopPullDownRefresh()
  }
})

5. 商品详情

1. 页面构建

miniprogram/modules/goodModule/pages/goods/detail/detail.json

{
  "usingComponents": {
    "van-icon": "@vant/weapp/icon/index",
    "van-button": "@vant/weapp/button/index",
    "van-goods-action": "@vant/weapp/goods-action/index",
    "van-goods-action-icon": "@vant/weapp/goods-action-icon/index",
    "van-goods-action-button": "@vant/weapp/goods-action-button/index",
    "van-action-sheet": "@vant/weapp/action-sheet/index",
    "van-stepper": "@vant/weapp/stepper/index",
    "van-field": "@vant/weapp/field/index"
  }
}

miniprogram/modules/goodModule/pages/goods/detail/detail.wxml

<view class="container goods-detail">
  <!-- 商品大图 -->
  <view class="banner-img">
    <image src="{{ goodsInfo.imageUrl }}" mode="" class="img" bind:tap="previewImage" />
  </view>

  <!-- 商品基本信息 -->
  <view class="content">
    <view class="price">
      <view class="price-num">¥{{ goodsInfo.price }}</view>
      <view class="price-origin-num">¥{{ goodsInfo.marketPrice }}</view>
    </view>
    <view class="title">{{ goodsInfo.name }}</view>
    <view class="desc">{{ goodsInfo.floralLanguage }}</view>
  </view>

  <!-- 商品的详细信息 -->
  <view class="detail">
    <image wx:for="{{ goodsInfo.detailList }}" wx:key="index" src="{{ item }}" mode="" mode="widthFix" class="img" />
  </view>



  <!-- 底部导航栏 -->
  <view class="bottom">
    <view class="bottom-items">
      <van-goods-action>
        <navigator class="home" url="/pages/index/index" open-type="switchTab">
          <van-goods-action-icon icon="chat-o" text="首页" />
        </navigator>

        <navigator class="to-cart" url="/pages/cart/cart" open-type="switchTab">
          <van-goods-action-icon icon="cart-o" text="购物车" open-type="switchTab" />
        </navigator>

        <van-goods-action-icon icon="chat-o" text="客服" open-type="contact" />

        <van-goods-action-button text="加入购物车" type="warning" bind:tap="onClickCartButton" />
        <van-goods-action-button text="立即购买" bind:tap="onClickPatButton" />
      </van-goods-action>

    </view>

    <!-- 弹出层 -->
    <van-action-sheet show="{{ show }}" round safe-area-inset-bottom overlay close-on-click-overlay bind:close="onClose">
      <view class="goods-item">
        <!-- 商品图片 -->
        <view class="mid">
          <image class="img" src="{{ goodsInfo.imageUrl }}" mode="" />
        </view>
        <!-- 商品基本信息 -->
        <view class="right">
          <view class="title">{{ goodsInfo.name }}</view>
          <view class="right-bottom">
            <view class="price">¥{{ goodsInfo.price }}</view>
            <van-stepper class="stepper" button-size="22px" min="1" integer value="{{ count }}" bind:change="onChange" />
          </view>
        </view>
      </view>
      <view class="popup-bottom">
        <view class="title"><text>祝福语</text></view>
        <textarea value="" required placeholder="必填, 写上您的祝福语, 给心爱的他 (她) 送上你的祝福 (请勿填写特殊符号或表情符号)" />
      </view>
      <van-button size="large" round type="primary">确定</van-button>
    </van-action-sheet>

  </view>
</view>

miniprogram/modules/goodModule/pages/goods/detail/detail.scss

.container {
  position: relative;
  width: 750rpx;

  .banner-img {
    .img {
      width: 100%;
    }
  }

  .content {
    position: absolute;
    top: 400rpx;
    width: 650rpx;
    padding: 40rpx 30rpx;
    height: 120rpx;
    margin-left: 20rpx;
    background-color: #efefef;

    .price {
      display: flex;
      height: 40rpx;

      .price-num {
        color: orange;
      }

      .price-origin-num {
        font-size: 22rpx;
        line-height: 40rpx;
        margin-left: 12rpx;
        color: #adaaaa;
        font-weight: 300;
        text-decoration: line-through;
      }
    }

    .title {
      margin-top: 12rpx;
      font-weight: 1000;
      overflow: hidden; // 超出的文字以省略号代替
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .desc {
      font-size: 22rpx;
      color: #adaaaa;
      margin-top: 12rpx;
    }
  }

  .detail {
    width: 710rpx;
    padding: 20rpx;
    margin-top: 120rpx;

    .img {
      width: 100%;
    }
  }

  .bottom {
    van-action-sheet {
      height: 650rpx;
      .goods-item {
        padding: 25rpx;
        display: flex;
  
        .mid {
          .img {
            width: 240rpx;
            height: 280rpx;
          }
        }
  
        .right {
          .title {
            margin-left: 12rpx;
            font-weight: 1000;
            overflow: hidden; // 超出的文字以省略号代替
            text-overflow: ellipsis;
            white-space: nowrap;
          }
  
          .right-bottom {
            display: flex;
  
            .price {
              margin-top: 180rpx;
              margin-left: 12rpx;
              color: orange;
              font-size: 40rpx;
            }
  
            .stepper {
              margin-top: 180rpx;
              margin-left: 130rpx;
            }
          }
  
        }
      }
  
      .popup-bottom {
        padding: 20rpx;
        .title{
          font-weight: 600;
        }
        textarea{
          margin-top: 60rpx;
          font-size: 28rpx;
          font-weight: 300;
        }
      }
    }
  }
}


2. 数据交互

api/goods.js

 /**
 * @description 获取商品详情
 * @param {Number} param 商品的id
 * @returns Promise 
 */
export const reqGoodsInfo = (goodId) => {
  return http.get(`/goods/${goodId}`)
 }

miniprogram/modules/goodModule/pages/goods/detail/detail.js

import {
  reqGoodsInfo
} from '@/api/goods'

Page({

  /**
   * 页面的初始数据
   */
  data: {
    goodsInfo: {}, // 商品详情
    show: false, // 加入购物车和立即购买时显示的弹框
    count: 1, // 商品购买数量, 默认为1
    blessing: '' // 祝福语
  },

  // 获取商品详情的数据
  async getGoodsInfo() {
    const {
      data: goodsInfo
    } = await reqGoodsInfo(this.goodsId)
    this.setData({
      goodsInfo
    })
  },

  // 打开弹出框
  openPopup(){
    this.setData({
      show: true
    })
  },
  // 关闭弹出框
  closePopup(){
    this.setData({
      show: false
    })
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    // 接收传递过来的商品 id, 并挂载到this上, 方便多个方法调用
    this.goodsId = options.goodsId

    // 调用获取商品详情数据的方法
    this.getGoodsInfo()
  },
})

3. 详情图片预览

miniprogram/modules/goodModule/pages/goods/detail/detail.wxml

<view class="container goods-detail">
  <!-- 商品大图 -->
  <view class="banner-img">
    <!-- 绑定点击事件 -->
    <image src="{{ goodsInfo.imageUrl }}" mode="" class="img" bind:tap="previewImage" />
  </view>
</view>

miniprogram/modules/goodModule/pages/goods/detail/detail.js

import {
  reqGoodsInfo
} from '@/api/goods'

Page({

  /**
   * 页面的初始数据
   */
  data: {
    goodsInfo: {}, // 商品详情
    show: false, // 加入购物车和立即购买时显示的弹框
    count: 1, // 商品购买数量, 默认为1
    blessing: '' // 祝福语
  },
  
  // 详情图片预览
  previewImage(){
    wx.previewImage({
      urls: this.data.goodsInfo.detailList,   // urls 必须是数组类型数据
    })
  }
})

4. 判断用户是否登录

使用 Token 来判断用户是否登录, Token 是存储在 Store 中的

/behaviors/userBehavior.js

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

export const userBehavior = BehaviorWithStore({
  storeBindings:{
    store: userStore,
    fields: ['token']
  }
})

miniprogram/modules/goodModule/pages/goods/detail/detail.js

import {
  userBehavior
} from '@/behaviors/userBehavior'

Page({
  behaviors: [userBehavior],
})

5. 加入购物车 && 立即购买

  1. 定义 payNow 作为用户点击的是加入购物车还是立即购买, 点击加入购物车, payNow 设置为0, 点击立即购买则设置为 1
  2. 点击 加入购物车, 将当前商品加入到购物车内
  3. 点击 立即购买, 跳转到结算支付页面, 立即购买该商品
  4. 如果是 立即购买, 不支持购买多个商品

miniprogram/modules/goodModule/pages/goods/detail/detail.wxml

<view class="container goods-detail">
  <!-- 商品大图 -->
  <view class="banner-img">
    <image src="{{ goodsInfo.imageUrl }}" mode="" class="img" bind:tap="previewImage" />
  </view>

  <!-- 商品基本信息 -->
  <view class="content">
    <view class="price">
      <view class="price-num">¥{{ goodsInfo.price }}</view>
      <view class="price-origin-num">¥{{ goodsInfo.marketPrice }}</view>
    </view>
    <view class="title">{{ goodsInfo.name }}</view>
    <view class="desc">{{ goodsInfo.floralLanguage }}</view>
  </view>

  <view class="detail">
    <image wx:for="{{ goodsInfo.detailList }}" wx:key="index" src="{{ item }}" mode="" mode="widthFix" class="img" />
  </view>

  <!-- 底部导航栏 -->
  <view class="bottom">
    <view class="bottom-items">
      <van-goods-action>
        <navigator class="home" url="/pages/index/index" open-type="switchTab">
          <van-goods-action-icon icon="chat-o" text="首页" />
        </navigator>

        <navigator class="to-cart" url="/pages/cart/cart" open-type="switchTab">
          <van-goods-action-icon icon="cart-o" text="购物车" open-type="switchTab" />
        </navigator>

        <van-goods-action-icon icon="chat-o" text="客服" open-type="contact" />

        <van-goods-action-button text="加入购物车" type="warning" bind:tap="onClickCartButton" />
        <van-goods-action-button text="立即购买" bind:tap="onClickPayButton" />
      </van-goods-action>

    </view>

    <!-- 弹出层 -->
    <van-action-sheet show="{{ show }}" round safe-area-inset-bottom overlay close-on-click-overlay bind:close="onClose">
      <view class="goods-item">
        <view class="mid">
          <image class="img" src="{{ goodsInfo.imageUrl }}" mode="" />
        </view>
        <view class="right">
          <view class="title">{{ goodsInfo.name }}</view>
          <view class="right-bottom">
            <view class="price">¥{{ goodsInfo.price }}</view>
            <!--  点击的是加入购物车才显示步进器组件来修改商品数量 -->
            <van-stepper wx:if="{{ payNow === 0 }}" class="stepper" button-size="22px" min="1" max="200" integer value="{{ count }}" bind:change="onChangeGoodsCount" />
          </view>
        </view>
      </view>
      <view class="popup-bottom">
        <view class="title"><text>祝福语</text></view>
        <!-- 双向数据绑定祝福语 -->
        <textarea value="" required model:value="{{ blessing }}" placeholder="必填, 写上您的祝福语, 给心爱的他 (她) 送上你的祝福 (请勿填写特殊符号或表情符号)" bindinput="onTextAreaChange" />
      </view>
      <van-button size="large" round type="primary" bind:tap="onSubmit">确定</van-button>
    </van-action-sheet>

  </view>
</view>

miniprogram/modules/goodModule/pages/goods/detail/detail.js

import {
  reqGoodsInfo
} from '@/api/goods'

Page({

  /**
   * 页面的初始数据
   */
  data: {
    goodsInfo: {}, // 商品详情
    show: false, // 加入购物车和立即购买时显示的弹框
    count: 1, // 商品购买数量, 默认为1
    blessing: '', // 祝福语
    payNow: 0 // 用于区分加入购物车还是立即购买 0 加入购物车 1 立即购买
  },

  // 获取商品详情的数据
  async getGoodsInfo() {
    const {
      data: goodsInfo
    } = await reqGoodsInfo(this.goodsId)
    this.setData({
      goodsInfo
    })
  },
   // 图片预览
   previewImage() {
    wx.previewImage({
      urls: this.data.goodsInfo.detailList,
    })
  },

  // 点击加入购物车, 打开弹出框, 修改 payNow 为0
  onClickCartButton() {
    this.setData({
      show: true,
      payNow: 0
    })
  },
  // 点击立即购买, 打开弹出框, 修改 payNow 为1
  onClickPayButton(){
    this.setData({
      show: true,
      payNow: 1
    })
  },
  // 点击遮罩层, 关闭弹出框
  onClose() {
    this.setData({
      show: false,
      payNow: 0
    })
  },

  // 修改商品数量
  onChangeGoodsCount(event) {
    this.setData({
      count: Number(event.detail)
    })
  },

   // 用户点击确定按钮 
  async onSubmit() {
    const {
      token,
      count,
      blessing,
      payNow
    } = this.data

    const goodsId = this.goodsId
	
    // 判断用户是否登录, 没有登录就跳转到登录页面, 并终止以下代码执行
    if (!token) {
      wx.navigateTo({
        url: '/pages/login/login',
      })
      return
    }

    if (payNow === 0) { // 用户点击的是加入购物车
      const res = await reqAddToCart(goodsId, count, blessing)
      if (res.code === 200){
        // 提示用户加入购物车成功
        toast({
          title: '加入购物车成功'
        })

        // 隐藏弹出框
        this.setData({
          show: false
        })
      }
    } else { // 用户点击的是立即购买
      // 跳转到结算页面
      wx.navigateTo({
        url: `/pages/order/detail/detail?goodsId=${goodsId}&blessing=${blessing}`,
      })
    }
  },
    
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    // 接收传递过来的商品 id, 并挂载到this上, 方便多个方法调用
    this.goodsId = options.goodsId

    // 调用获取商品详情数据的方法
    this.getGoodsInfo()
  }
})

6. 购物车图标右上角显示数量

miniprogram/modules/goodModule/pages/goods/detail/detail.wxml

<!-- 使用 info 来展示右上角数量 -->
<van-goods-action-icon icon="cart-o" text="购物车" open-type="switchTab" info="{{ allCount}}" />

miniprogram/modules/goodModule/pages/goods/detail/detail.js

import {
  reqGoodsInfo
} from '@/api/goods'
import {
  userBehavior
} from '@/behaviors/userBehavior'
import {
  toast
} from '@/utils/extendAPI'

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

Page({
  behaviors: [userBehavior],
  /**
   * 页面的初始数据
   */
  data: {
    allCount: 0 // 用户购物车内商品的数量
  },

  // 计算用户购物车商品数量, 并显示在底部的购物车右上角
  async getCartGoodsCount() {

    // 判断用户是否登录
    if (!this.data.token) return

    const res = await reqCartList()
    if (res.code === 200 && res.data.length !== 0) {
      let allCount = 0
      // 累加购物车的每种商品个数
      res.data.forEach((item) => {
        allCount += item.count
      })

      this.setData({
        // info 的属性值要求必须是字符串类型, 而且如果总数量大于99, 显示99+
        allCount: allCount > 99 ? '99+' : String(allCount) 
      })
    }
  },
    
  // 用户点击确定按钮 
  async onSubmit() {
    const {
      token,
      count,
      blessing,
      payNow
    } = this.data

    const goodsId = this.goodsId

    if (!token) {
      wx.navigateTo({
        url: '/pages/login/login',
      })
      return
    }

    if (payNow === 0) { // 用户点击的是加入购物车
      const res = await reqAddToCart(goodsId, count, blessing)
      if (res.code === 200) {
        toast({
          title: '加入购物车成功'
        })

        // 加入购物车成功后, 需要重新计算购物车内商品的购买数量, 来显示购物车右上角的数量
        this.getCartGoodsCount()

        this.setData({
          show: false
        })
      }
    } else { 
      wx.navigateTo({
        url: `/pages/order/detail/detail?goodsId=${goodsId}&blessing=${blessing}`,
      })
    }
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    this.goodsId = options.goodsId

    this.getGoodsInfo()

    // 获取用户的购物车商品数量, 并显示在底部的购物车右上角
    this.getCartGoodsCount()
  }

})
posted @ 2024-07-04 15:34  河图s  阅读(22)  评论(0)    收藏  举报