花店小程序·续

目录

一.框架扩展

1.mobx-miniprogram 

1.1mobx-miniprogram的介绍

1.2注意事项(mobx-miniprogram的安装)

1.3创建 Store 对象

1.4在数组中使用数据

1.5在页面中使用数据

1.6 fields、actions 对象写法

2.miniprogram-computed

2.1计算属性computed

2.2监听器watch

二.用户管理

1.什么是 token

2.小程序实现用户登录

3.Token 存储到 Store

4.用户信息存储到 Store

5.使用数据渲染用户信息

6.配置分包以及预下载

7.更新用户信息-渲染用户信息

8.获取头像临时路径

9.头像上传到服务器

10.完成头像更新

11.更新用户昵称

12.效果图

三、地址管理

1.定义新增参数以及封装接口 API

2. 收集省市区数据

3.收集新增地址其他请求参数

4.地理定位功能介绍

5.拒绝授权后的解决方案

6.开通腾讯位置服务

7.LBS 逆地址解析

8.async-validator 基本使用

9.新增收货地址表单验证

10.实现新增收货地址

11.收货地址列表渲染

12. 实现更新收货地址

13. 实现删除收货地址

四、商品管理

1.配置商品管理分包

2.封装商品模块接口 API

3.商品列表-准备列表请求参数

4.商品列表-获取商品列表数据并渲染

5.商品列表-实现上拉加载更多功能

6.商品列表-判断数据是否加载完毕

7. 商品列表-节流阀进行列表节流

8.商品列表-实现下拉刷新功能

9.商品详情-获取并渲染商品详情

10. 商品详情-详情图片预览功能

11.样式图

五、购物车

1.购物车-封装购物车接口 API

2.加入购物车-模板分析和渲染

3. 加入购物车-关联 Store 对象

4.加入购物车和立即购买区分处理

5. 加入购物车-展示购物车购买数量

6. 购物车-购物车关联 Store 对象

7. 购物车-获取并渲染购物车列表

8. 购物车-更新商品的购买状态

9. 购物车-控制全选按钮的选中状态

10. 购物车-实现全选和全不选功能

11. 购物车-更新商品购买数量思路分析

12. 购物车-更新商品的购买数量

13. 购物车-更新商品购买数量防抖

14. 购物车-购物车商品合计

15. 购物车-删除购物车中的商品

六、结算支付

1.配置分包并跳转到结算页面

2.封装结算支付的接口 API

3.商品结算-获取收货地址

4.商品结算-更新收货地址功能

5.商品结算-获取订单详情数据

6.商品结算-获取立即购买数据

7. 商品结算-收集送达时间

8.商品结算-表单数据验证

9. 小程序支付-小程序支付流程

10.小程序支付-创建平台订单

11.小程序支付-获取预付单信息

12.小程序支付-发起微信支付

13.小程序支付-支付状态查询

14.样式图

七、订单列表

1.封装订单列表接口API

2.获取订单列表数据并渲染

3.订单列表上拉加载更多

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


一.框架扩展

1.mobx-miniprogram 

   小程序页面、组件间的数据通信方案:

数据绑定:properties

获取组件实例:this.selectComponent()

事件绑定:this.triggerEvent()

获取应用实例:getApp()

页面间通信:EventChannel

事件总线:pubsub-js

 所以为了方便进行页面、组件之间数据的传递,小程序官方提供了一个扩展工具库: mobx-miniprogram

1.1mobx-miniprogram的介绍

mobx-miniprogram 是针对微信小程序开发的一个简单、高效、轻量级状态管理库,它基于Mobx状态管理框架实现。

使用 mobx-miniprogram 定义管理的状态是响应式的,当状态一旦它改变,所有关联组件都会自动更新相对应的数据

通过该扩展工具库,开发者可以很方便地在小程序中全局共享的状态,并自动更新视图组件,从而提升小程序的开发效率

1.2注意事项(mobx-miniprogram的安装)

在使用 mobx-miniprogram 需要安装两个包:mobx-miniprogram 和 mobx-miniprogram-bindings

mobx-miniprogram 的作用:创建 Store 对象,用于存储应用的数据

mobx-miniprogram-bindings 的作用:将状态和组件、页面进行绑定关联,从而在组件和页面中操作数据

1.3创建 Store 对象

熟悉mobx-miniprogram 三个核心概念:

observable:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。

action:用于修改状态(state)的方法,需要使用 action 函数显式的声明创建。

computed:根据已有状态(state)生成的新值。计算属性是一个方法,在方法前面必须加上 get 修饰符

   // observable:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。
   // action:用于显式的声明创建更新 state 状态的方法
   import { observable, action } from 'mobx-miniprogram'
   
   // 使用 observable 创建一个被监测的对象
   export const numStore = observable({
   
     // 创建应用状态
     numA: 1,
     numB: 2,
       
     // 使用 action 更新 numA 以及 numB
     update: action(function () {
       this.numA+=1
       this.numB+=1
     }),
   
     // 计算属性,使用 get 修饰符,
     get sum() {
       return this.numA + this.numB;
     }
   
   })

1.4在数组中使用数据

使用步骤: 

从 mobx-miniprogram-bindings 库中导入 ComponentWithStore 方法

将 Component 方法替换成 ComponentWithStore 方法

然后配置 storeBindings 从 Store 中映射数据和方法即可

在替换以后,就会新增一个 storeBindings 配置项,配置项常用的属性有以下三个:

store: 指定要绑定的 Store 对象

fields: 指定需要绑定的 data 字段

actions: 指定需要映射的 actions 方法

代码如下:

// components/custom01/custom01.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
 
ComponentWithStore({
  data: {
    someData: '...'
  },
  storeBindings: {
    store: numStore,
    fields: ['numA', 'numB', 'sum'],
    actions: ['update']
  }
})

1.5在页面中使用数据

第一种方法:

使用Component 方法用于创建自定义组件。

小程序的页面也可以视为自定义组件,因此页面也可以使用 Component 方法进行构建,从而实现复杂的页面逻辑开发。

如果我们使用了 Component 方法来构建页面,那么页面中如果想使用 Store 中的数据,使用方式和组件的使用方式是一样的

第二种方法:

我们可以使用Component 方法构建页面,然后使用 ComponentWithStore 方法让页面和 Store 建立了关联

如果不想使用 Component 方法构建页面。这时候需要使用 mobx-miniprogram-bindings 提供的 BehaviorWithStore 方法来和 Store 建立关联

小程序的 behavior 方法是一种代码复用的方式,可以将一些通用的逻辑和方法提取出来,然后在多个组件中复用,从而减少代码冗余,提高代码的可维护性。在页面中也可以使用 behaviors 配置项

使用步骤如下:

新建 behavior 文件,从 mobx-miniprogram-bindings 库中导入 BehaviorWithStore 方法

在 BehaviorWithStore 方法中配置 storeBindings 配置项从 Store 中映射数据和方法

在 Page 方法中导入创建的 behavior ,然后配置 behavior 属性,并使用导入的 behavior

// behavior.js
 
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
 
export const indexBehavior = BehaviorWithStore({
  storeBindings: {
    store: numStore,
    fields: ['numA', 'numB', 'sum'],
    actions: ['update'],
  }
})

1.6 fields、actions 对象写法

如果 fields 写成对象方式,又有两种写法:

映射形式:指定 data 中哪些字段来源于 store 以及它们在 store 中对应的名字。

例如 { a: 'numA', b: 'numB' }

函数形式:指定 data 中每个字段的计算方法

例如 { a: () => store.numA, b: () => anotherStore.numB }

如果 actions 写成对象方式,只有一种写法(只有映射形式没有函数形式)

映射形式:指定模板中调用的哪些方法来源于 store 以及它们在 store 中对应的名字。

例如 { buttonTap: 'update' }

import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
 
ComponentWithStore({
  data: {
    someData: '...'
  },
  storeBindings: {
    store: numStore,
    fields: {
      // 使用函数方式获取 Store 中的数据
      a: () => store.numA,
      b: () => store.numB,
 
      // 使用映射形式获取 Store 中的数据,值为数据在 store 中对应的名字
      total: 'sub'
    },
 
    // 使用映射形式获取 Store 中的 action 名字
    actions: {
      // key 自定义,为当前组件中调用的方法
      // 值为 store 中对应的 action 名字
      buttonTap: 'update'
    }
  }
})

2.miniprogram-computed

小程序框架没有提供计算属性相关的 api ,但是官方为开发者提供了拓展工具库 miniprogram-computed,提供了计算属性computed和监听器watch两个功能

2.1计算属性computed

如果需要在组件中使用计算属性功能,需要 miniprogram-computed 库中导入 ComponentWithComputed 方法

在使用时需要将 Component 方法替换成 ComponentWithComputed 方法 ,原本组件配置项也需要写到该方法中

使用npm install miniprogram-computed命令进行安装

2.2监听器watch

在使用时需要将 Component 方法替换成 ComponentWithComputed 方法 ,原本组件配置项也需要写到该方法中

// 引入 miniprogram-computed
import { ComponentWithComputed } from 'miniprogram-computed'
 
ComponentWithComputed({
    
  data: {
    a: 1,
    b: 1
  },
    
  computed: {
    total(data) {
      // 注意: 
      // computed 函数中不能访问 this ,只有 data 对象可供访问
      // 这个函数的返回值会被设置到 this.data.sum 字段中
      return data.a + data.b
    }
  }
  
  watch: {
 
    //key:需要监听的数据
    //value:是回调函数,回调函数有一个形参,形参就是最新的、改变后的数据
    a:function(a){
        console.log(a)
        }
 
 
    // 同时对 a 和 b 进行监听
    'a, b': function (a, b) {
      this.setData({
        total: a + b
      })
    }
  },
  
})
 

二.用户管理

1.什么是 token

Token是服务器生成的一串字符串,用作客户端发起请求的一个身份令牌。当第一次登录成功后,服务器生成一个 Token便将此 Token返回给客户端,客户端在接收到 Token以后,会使用某种方式将 Token保存到本地。以后客户端发起请求,只需要在请求头上带上这个 Token ,服务器通过验证 Token 来确认用户的身份,而无需再次带上用户名和密码。

Token具体流程

1. 客户端向服务器发起登录请求,服务端验证用户名与密码

2. 验证成功后,服务端会签发一个 `Token`,并将 `Token` 发送到客户端

3. 客户端收到 `token` 以后,将其存储起来,比如放在 `localStorage` 、`sessionStorage` 中

4. 客户端每次向服务器请求资源的时候需要带着服务端签发的 `Token`,服务端收到请求,然后去验证客户端请求里面带着的 `Token` ,如果验证成功,就向客户端返回请求的数据

<img src="http://8.131.91.46:6677/mina/floor/token交互流程.png" style="zoom:50%; border: 1px solid #ccc" />

2.小程序实现用户登录

实现步骤:

1. 在 `/api/user.js`  文件中根据接口文档,创建登录的 `API` 函数 `login`

2. 给登录按钮绑定点击事件,对应 `login` 回调函数

3. 在 `login` 回调函数中调用 `wx.login()` 方法,获取**临时登录凭证code**

4. 在 `/pages/login/login.js` 中导入封装好的 `API` 函数,传入 **临时登录凭证code** 然后调用

5. 在登录成功以后将 `token` 存储到本地


import http from '../utils/http

/**
 * @description 授权登录
 * @param {*} code 临时登录凭证code
 * @returns Promise
 */
export const reqLogin = (code) => {
  return http.get(`/mall-api/weixin/wxLogin/${code}`)
}




 /pages/login/login.js


//登录请求
import { reqLogin } from '../../api/user'
import { toast } from '../../utils/extendApi'

Page({
  // 点击登录
  login() {
    // 调用 wx.login 获取用户信息
    wx.login({
      success: async ({ code }) => {
        if (code) {
          // 调用接口 API,传入 code 进行登录
          const res = await reqLogin(code)

          // 登录成功以后将 token 存储到本地
          wx.setStorageSync('token', res.data.token)

          // 返回之前的页面
          wx.navigateBack()
        } else {
          // 登录失败后给用户进行提示
          toast({ title: '授权失败,请稍后再试~~~' })
        }
      }
    })
  }
})

3.Token 存储到 Store

实现步骤:

1. 安装`Mobx`两个包,在安装好包以后,对包进行构建,点击 `构建 npm`

2. 在项目的根目录下创建 `store` 文件夹,然后在该文件夹下新建 `userstore.js`

3. 导入核心的`observable ` 、`action` 方法,创建`Store`,同时声明数据和方法

4. 在登录页面,导入`ComponentWithStore` 方法,并配置 `storeBindings` 方法让页面和 `Store` 对象关联

安装依赖,安装完成后构建 npm

```shell

npm i mobx-miniprogram mobx-miniprogram-bindings

/store/index.js

// 导入 observable 函数用于创建可观察对象
// 导入 action 修改 store 中的可观察状态
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'

// 创建 store 对象,存储应用的状态
export const userStore = observable({
  // 创建可观察状态 token
  token: getStorage('token') || '',

  // 对 token 进行修改
  setToken: action(function (token) {
    this.token = token
  })
})
 import { userStore } from '../../api/userstore'

import { ComponentWithStore } from 'mobx-miniprogram-bindings'

 ComponentWithStore({
    
   storeBindings: {
    store: userStore,
    fields: ['token'],
    actions: ['setToken']   }

   methods: {
    // 授权登录
    login() {
      // 使用 wx.login 获取用户的临时登录凭证 code
      wx.login({
        success: async ({ code }) => {
          if (code) {
            // 在获取到临时登录凭证 code 以后,需要传递给开发者服务器
            const { data } = await reqLogin(code)

            // 登录成功以后,需要将服务器响应的自定义登录态存储到本地
            setStorage('token', data.token)
              
             // 将数据存储到 store 对象中
             this.setToken(data.token)
          } else {
            toast({ title: '授权失败,请重新授权' })
          }
        }
      })
    }
   }
})

4.用户信息存储到 Store

实现步骤:

1. 在 `store/userstore.js` 中新增`userInfo`字段,同时创建修改的`action`方法

2. 在 `login.js` 中使用映射 `userInfo` 数据和 `setUserInfo` 方法

3. 在 `/api/user.js`  文件中根据接口文档,创建获取用户信息的 `API` 函数 `reqUserInfo`

4. 在 `/pages/login/login.js` 中导入封装好的获取商品列表的 `API` 函数

5. 创建 `getUserInfo` 方法,在 `getUserInfo` 方法中调用接口 `API` 函数 `reqUserInfo`

6. 在登录成功以后,调用`getUserInfo` 方法获取用户,然后将用户信息存到本地以及 `Store`

/api/user.js

/**
 * @description 获取用户信息
 * @returns Promise
 */
export const reqUserInfo = () => {
  return http.get(`/mall-api/weixin/getuserInfo`)
}




/store/userstore.js

// 导入 observable 函数用于创建可观察对象
// 导入 action 修改 store 中的可观察状态
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'

// 创建 store 对象,存储应用的状态
export const userStore = observable({
  // 创建可观察状态 token
  // token,登录令牌
  token: getStorage('token') || '',
   // 用户信息
   userInfo: wx.getStorageSync('userInfo') || {},

  // 对 token 进行修改
  setToken: action(function (token) {
    this.token = token
  }),
    
   // 设置用户信息
   setUserInfo: action(function (userInfo) {
     this.userInfo = userInfo
   })
})





/pages/login/login.js

// pages/login/login.js

// 导入封装通用模块方法
import { toast } from '../../utils/extendApi'
// 导入本地存储 api
import { setStorage } from '../../utils/storage'
// 导入接口 API 函数
 import { reqLogin, reqUserInfo } from '../../api/user'

// 导入 ComponentWithStore 方法
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
// 导入 store 对象
import { userStore } from '../../stores/userstore'

// 使用 ComponentWithStore 方法替换 Component 方法构造页面
ComponentWithStore({
  // 让页面和 Store 对象建立关联
  storeBindings: {
    store: userStore,
     fields: ['token', 'userInfo'],
     actions: ['setToken', 'setUserInfo']
  },

  methods: {
    // 授权登录
    login() {
      // 使用 wx.login 获取用户的临时登录凭证 code
      wx.login({
        success: async ({ code }) => {
          if (code) {
            // 在获取到临时登录凭证 code 以后,需要传递给开发者服务器
            const { data } = await reqLogin(code)

            // 登录成功以后,需要将服务器响应的自定义登录态存储到本地
            setStorage('token', data.token)

            // 将自定义登录态 token 存储到 Store 对象
            this.setToken(data.token)
              
           // 获取用户信息
          this.getUserInfo()
          } else {
            toast({ title: '授权失败,请重新授权' })
          }
        }
      })
    },
        
    // 获取用户信息
    async getUserInfo() {
      const { data } = await reqUserInfo()
      // 将用户信息存储到本地
      setStorage('userInfo', data)
     // 将用户信息存储到 Store
    this.setUserInfo(data)
    }
  }
})

5.使用数据渲染用户信息

实现步骤:

1. 在个人中心页面导入`ComponentWithStore` 方法构建页面

2. 配置 `storeBindings` 让组件和 `Store` 建立关联

3. 渲染页面

/pages/info/info.js

 import { ComponentWithStore } from 'mobx-miniprogram-bindings'

 ComponentWithStore({
    
   storeBindings: {
    store: userStore,
     fields: ['token', 'userInfo']
   }

})





/pages/info/info.wxml

<!--pages/info/info.wxml-->
<view class="container bg">
  <!-- 顶部展示图 -->
  <view class="top-show">
    <image mode="widthFix" class="top-show-img" src="/static/images/banner.jpg"></image>
  </view>
  <view class="wrap">

    <!-- 未登录面板 -->
     <view class="user-container section" wx:if="{{ !token }}" bindtap="toLoginPage">
      <view class="avatar-container">
        <image src="/static/images/avatar.png"></image>
        <view class="no-login">
          <text class="ellipsis">未登录</text>
          <text>点击授权登录</text>
        </view>
      </view>
    </view>

    <!-- 登录以后得面包 -->
     <view wx:else class="user-container section">
       <view class="avatar-container">
         <image src="{{ userInfo.headimgurl }}"></image>
         <view class="no-login">
          <text class="ellipsis">{{ userInfo.nickname }}</text>
         </view>
       </view>
       <view class="setting">
         设置
       </view>
     </view>

    <!-- 订单面板 -->
    <view class="order section">
      <view class="order-title-wrap">
        <text class="title">我的订单</text>
        <text class="more">查看更多></text>
      </view>
      <view class="order-content-wrap">
        <view class="order-content-item">
           <navigator wx:if="{{ !token }}" url="/pages/login/login">
             <view class="iconfont icon-dingdan"></view>
             <text>商品订单</text>
           </navigator>
          <navigator wx:else url="/pages/order/list/index">
            <view class="iconfont icon-dingdan"></view>
            <text>商品订单</text>           </navigator>
        </view>
        <view class="order-content-item">
          <view class="iconfont icon-lipinka"></view>
          <text>礼品卡订单</text>
        </view>
        <view class="order-content-item">
          <view class="iconfont icon-tuikuan"></view>
          <text>退款/售后</text>
        </view>

      </view>
    </view>

    <!-- 关于售前售后服务面板 -->
    <view class="after-scale section">
      <!-- coding... -->
    </view>

    <!-- 底部面板 -->
    <view class="info-footer">
      尚硅谷技术支持
    </view>
  </view>
</view>

6.配置分包以及预下载

实现步骤:

1. 在 `app.json` 新增 `subpackages` 进行分包配置,新增 `preloadRule` 进行分包预下载配置

2. 在 `subpackages` 设置分包的 根目录 `root` 、别名 `name` 、页面路径 `pages`

3. 在 `preloadRule` 设置预下载。

"subpackages": [
  {
    "root": "modules/settingModule",
    "name": "settingModule",
    "pages": [
      "pages/address/add/index",
      "pages/address/list/index",
      "pages/profile/profile"
    ]
  }
],
"preloadRule": {
  "pages/settings/settings": {
    "network": "all",
    "packages": ["settingModule"]
  }
}

7.更新用户信息-渲染用户信息

实现步骤:

1. 新建 `behavior.js` 文件,从 `mobx-miniprogram-bindings` 库中导入 `BehaviorWithStore` 方法

2. 在 `BehaviorWithStore` 方法中配置 `storeBindings` 配置项从 `Store` 中映射数据和方法

3. 在 `Page` 方法中导入创建的 `behavior` ,然后配置 `behavior` 属性,并使用导入的 `behavior`

// behavior.js

import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
// 导入 store 对象
import { userStore } from '../../stores/userstore'

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




 modules/settingModule/pages/profile/profile.js`


import { userBehavior } from './behavior'

Page({
    
  behaviors: [userBehavior],
    
  // 页面的初始数据
  data: {
    isShowPopup: false // 控制更新用户昵称的弹框显示与否
  },

  // 其他代码略...
})

```



 modules/settingModules/pages/profile/profile.wxml`


<view class="container">

  <view class="setting-list avatar-container">
    <text>头像</text>
    <view class="avatar">
      <button hover-class="none">
        <image src="{{ userInfo.headimgurl }}" mode="" />
      </button>
    </view>
  </view>

  <view class="setting-list nickname">
    <text>昵称</text>
    <text>{{ userInfo.nickname }}</text>
  </view>

  
  <!-- coding... -->
</view>

8.获取头像临时路径

当用户点击头像时,可以对头像进行更新操作,我们使用通过微信提供的头像昵称填写能力快速完善

<img src="http://8.131.91.46:6677/mina/floor/更新头像.gif" style="zoom:70%; border: 1px solid #ccc" />

实现步骤:

1. 将 [button](https://developers.weixin.qq.com/miniprogram/dev/component/button.html) 组件 `open-type` 的值设置为 `chooseAvatar`

2. 当用户选择需要使用的头像之后,可以通过 `bindchooseavatar` 事件回调获取到头像信息的临时路径

modules/settingModules/pages/profile/profile.wxml


<view class="avatar">
  <button
    class="avatar-btn"
    hover-class="none"
     open-type="chooseAvatar"
     bindchooseavatar="chooseAvatar"
  >
    <image src="{{ userInfo.headimgurl || '/assets/images/avatar.png' }}" />
  </button>
</view>




  modules/settingModules/pages/profile/profile.js`

// pages/profile/profile.js
import { userBehavior } from './behavior'

Page({
  // 注册 behavior
  behaviors: [userBehavior],

  // 页面的初始数据
  data: {
    isShowPopup: false // 控制更新用户昵称的弹框显示与否
  },

   // 更新用户头像
   chooseAvatar(event) {
     // console.log(event)

     // 获取头像的临时路径
    // 临时路径具有失效时间,需要将临时路径上传到公司的服务器,获取永久的路径
     // 在获取永久路径以后,需要使用永久路径更新 headimgurl
     // 用户点击 保存按钮,才算真正的更新了头像和昵称
     const { avatarUrl } = event.detail
 
     this.setData({
       'userInfo.headimgurl': avatarUrl
     })
   },


})

9.头像上传到服务器

实现步骤:

1. 在获取到用户的临时头像路径以后,调用 `wx.uploadFile()` 方法,同时设置好基本的参数,

2. 在上传成功后,获取到服务器返回的永久地址

3. 将地址赋值给 `data` 中的数据

modules/settingModules/pages/profile/profile.js


// 获取用户头像信息
getAvatar(e) {

  // 获取选中的头像
  const { avatarUrl } = e.detail

  wx.uploadFile({
    url: 'https://gmall-prod.atguigu.cn/mall-api/fileUpload',
    filePath: avatarUrl,
    name: 'file',
    header: {
      token: wx.getStorageSync('token'),
    },
    success: (res) => {
      // 将获取到的头像赋值给 data 中变量同步给页面结构
      const uploadRes = JSON.parse(res.data)
      this.setData({
        'userInfo.headimgurl': uploadRes.data
      })
    },
    fail(err) {
      wx.showToast({
        title: '头像更新失败,请稍后再试',
        icon: 'none'
      })
    }
  })

}

10.完成头像更新

实现步骤:

1. 在 `/api/user.js`  文件中根据接口文档,创建获取用户信息的 `API` 函数 `reqUpdateUserInfo`

2. 给修改个人资料的保存按钮绑定点击事件,触发 `updateUserInfo` 回调函数

3. 在回调函数中调用接口 `API` 函数 `reqUpdateUserInfo` ,同时传入用户的信息

4. 更新用户信息以后,将用户信息存储到本地同时同步到 `Store`

// pages/profile/profile.js
import { reqUpdateUserInfo, reqUserInfo } from '../../../../api/user'

Page({

  // coding...

  // 更新用户信息
  async updateUserInfo() {
    // 调用 API,更新用户信息
    await reqUpdateUserInfo(this.data.userInfo)

    // 将用户信息存储到本地
    wx.setStorageSync('userInfo', this.data.userInfo)

    // 将用户信息存储到 Store
    this.setUserInfo(this.data.userInfo)

    // 给用户提示头像更新成功
    wx.showToast({
      title: '头像更新成功',
      icon: 'none'
    })
  }

 
  // coding...
}

11.更新用户昵称

实现步骤:

1. 给 `form` 表单绑定 `bindsubmit` 事件,用来获取输入框最新的值

2. 给 `input` 组件绑定 `type` 属性,属性值为 `nickname`,获取微信昵称

3. 给 `input` 组件绑定 `bindinput` 事件,获取用户输入最新的昵称

4. 将 `formType` 设置为 `submit` 当用户点击确定后,触发 `form` 表单的 `bindsubmit` 事件

5. 在 `form` 表单的 `bindsubmit` 事件中进行赋值

6. 给 `form` 表单的取消按钮绑定事件,取消弹框

12.效果图

三、地址管理

1.定义新增参数以及封装接口 API

思路:

点击新建地址按钮,需要跳转到新增地址页面

因为新增和编辑收货地址页面是同一个页面,我们需要在这个页面处理新增和编辑功能,为了做区分处理。

我们在后续做进行编辑的时候传递 `id` 属性,值为 收货地址的 `id` 值。


 

首先熟悉接口文档:[获取用户信息](https://apifox.com/apidoc/shared-6ed6c5c4-56c4-4619-8e2a-4817aa140e30/api-134640244)

实现步骤:

1. 在新增收货地址页面 `data` 中声明所需要的字段

2. 定义收货地址所需要的全部接口 `API` 函数

代码:

//modules/settingModule/pages/address/add/index

js
Page{{
  
  // 页面的初始数据
  data: {
	  name: '', // 收货人
      phone: '', // 手机号
      provinceName: '', // 省
      provinceCode: '', // 省 编码
      cityName: '', // 市
      cityCode: '', // 市 编码
      districtName: '', // 区
      districtCode: '', // 区 编码
      address: '',  // 详细地址
      fullAddress: '', // 完整地址 (省 + 市 + 区 + 详细地址)
      isDefault: 0 // 设置默认地址,是否默认地址 → 0:否  1:是
  }
}}





/api/address`

js
import http from '../utils/http'

/**
 * @description 实现新增收货地址
 * @param {*} data
 * @returns Promise
 */
export const reqAddAddress = (data) => {
  return http.post('/userAddress/save', data)
}

/**
 * @description 获取收货地址列表
 * @returns Promise
 */
export const reqAddressList = () => {
  return http.get('/userAddress/findUserAddress')
}

/**
 * @description 获取收货地址详情
 * @param {*} id 收货地址id
 * @returns Promise
 */
export const reqAddressInfo = (id) => {
  return http.get(`/userAddress/${id})
}

/**
 * @description 编辑收货地址
 * @param {*} data
 * @returns Promise
 */
export const reqUpdateAddress = (data) => {
  return http.post('/userAddress/update', data)
}

/**
 * @description 删除收货地址
 * @param {*} id 收货地址 id
 * @returns Promise
 */
export const reqDelAddress = (id) => {
  return instance.get(`/userAddress/delete/${id})
}

2. 收集省市区数据

实现步骤:

1. 给 `picker` 选择组件添加`change` 事件来监听属性值的改变,获取选中的省市区

2. 将获取到省市区标识和编码赋值给 `data`中的字段

代码:


Page({
   
  // coding...
    
  // 省市区选择
  onAddressChange(event) {
    const [provinceCode, cityCode, districtCode] = event.detail.code
    const [provinceName, cityName, districtName] = event.detail.value

    // 存储省市区对应的编码
    this.setData({
      provinceCode,
      provinceName,
      cityCode,
      cityName,
      districtName,
      districtCode
    })
  }
})

3.收集新增地址其他请求参数

实现步骤以及思路:

使用简易双向数据 `model:value` 绑定来收集新增地址表单数据。

在将数据收集以后,需要组织两个数据:

1. 是否是默认地址,0 不设置为默认地址,1 设置为默认地址

2. 拼接完整的收货地址

步骤:

1. 使用简易双向数据绑定来收集新增地址表单数据。

2. 给按钮绑定点击事件,在事件处理函数中收集并整理数据

代码:

Page({
    
  // coding
    
  // 获取表单元素的值
  saveAddrssForm(event) {
      
    // 解构出省市区以及 是否是默认地址
    const { provinceName, cityName, districtName, address, isDefault } = this.data
    
    // 拼接完整的地址
    const fullAddress = provinceName + cityName + districtName + address
      
    // 合并接口请求参数
    const params = {
      ...this.data,
      fullAddress,
      isDefault: isDefault ? 1 : 0
    }


    console.log(params)
  }
})

4.地理定位功能介绍

实现步骤与思路:

1. 在 app.json 中配置 `requiredPrivateInfos` 进行声明启用

2. 在调用 `wx.getLocation()` 时需要在 app.json 配置 `permission `字段,同时使用 `scope.userLocation` 声明**收集用户选择的位置信息**的目的,`wx.chooseLocation()` 接口不需要配置该字段,可以直接进行调用

3. 在配置好以后,调用 `wx.getLocation()` 和 `wx.chooseLocation()` 接口

代码:

json
{
  "requiredPrivateInfos": [
    "getLocation",
    "chooseLocation"
  ],
  "permission": {
    "scope.userLocation": {
      "desc": "获取用户位置信息用于填写收货地址"
    }
  }
}




**getLocation 使用:**



js
// 地理定位
async onLocation() {
  // 获取 纬度 、精度
  const { latitude, longitude } = await wx.getLocation()
  console.log(location)
}




**chooseLocation 使用:**

js
// 地理定位
async onLocation() {
  // 打开地图选择位置,获取 纬度 、精度
  const { latitude, longitude }  = await wx.chooseLocation()
  console.log(res)
}




<img src="http://8.131.91.46:6677/mina/floor/授权地址.jpg" style="zoom:30%; height: 2000px; border: 1px solid #ccc" />

5.拒绝授权后的解决方案

步骤与思路:

在调用 `wx.getLocation()` 获取用地理位置时,如果用户选择拒绝授权,代码会直接抛出错误。

在拒绝授权以后,再次调用 `wx.getLocation()` 时,就不会在弹窗询问用户是否允许授权。

1. `wx.getSetting()`:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限

2. `wx.openSetting()`: 调起客户端小程序设置界面,返回用户设置的操作结果

代码:

// 获取用户地理位置信息
async onLocation() {
  // 调用 getSetting 方法获取用户所有的授权信息
  // 返回的 authSetting 包含小程序已向小程序申请过的权限已经授权结果(true、false)
  const { authSetting } = await wx.getSetting()
  console.log(authSetting)

  // scope.userLocation 是否已经授权获取地理位置的信息
  // 如果之前没有申请过返回 undefined,需要调用 getLocation
  // 如果之前同意了授权,返回 true,需要调用 getLocation
  // 如果之前拒绝了授权,返回 false,需要用户手动进行授权
  // 等于 true,或者不等于 undefined,说明需要进行授权
  // const isAuth =
  //   authSetting['scope.userLocation'] ||
  //   authSetting['scope.userLocation'] === undefined

  // 为了避免冗余的条件判断,使用 !! 把代码进行优化
  const isAuth = !!authSetting['scope.userLocation']

  if (!isAuth) {
    // 弹窗询问用户是否进行授权
    const modalRes = await wx.modal({
      title: '授权提示',
      content: '需要需要您的地理位置信息,请确认授权'
    })

    // 如果用户点击了取消,说明用户拒绝了授权,给用户提示
    if (!modalRes) return wx.toast({ title: '您拒绝了授权' })

    // 如果用户点击了确定,调用 wx.openSetting 打开微信客户端小程序授权页面
    // 并返回授权以后的结果
    const { authSetting } = await wx.openSetting()

    // 如果用户没有更新授权信息,提示没有更新授权
    if (!authSetting['scope.userLocation'])
      return wx.toast({ title: '授权失败!' })

    try {
      // 如果用户更新授权信息,则调用 getLocation 获取用户地理位置信息
      const locationRes = await wx.getLocation()
      // 打印地理位置信息
      console.log(locationRes)
    } catch (err) {
      console.log(err)
    }
  } else {
    try {
      // 如果是第一次调用 getLocation 或者之前授权过
      // 直接调用 getLocation 获取用户信息即可
      const locationRes = await wx.getLocation()
      console.log(locationRes)
    } catch (error) {
      wx.toast({ title: '您拒绝授权获取地址位置' })
    }
  }
}

6.开通腾讯位置服务

步骤与思路:

1. 申请开发者密钥(key):[申请密钥](https://lbs.qq.com/dev/console/application/mine)

2. 开通  webserviceAPI  服务:控制台 → 应用管理→[我的应用](https://lbs.qq.com/dev/console/key/manage) → 添加 key →勾选  WebServiceAPI →保存

3. 下载微信小程序 JavaScriptSDK,微信小程序[JavaScriptSDK v1.1](https://mapapi.qq.com/web/miniprogram/JSSDK/qqmap-wx-jssdk1.1.zip)  [JavaScriptSDK v1.2](https://mapapi.qq.com/web/miniprogram/JSSDK/qqmap-wx-jssdk1.2.zip)

4. 安全域名设置

   - 在[小程序管理后台](https://mp.weixin.qq.com/wxamp/home/guide) -> 开发 -> 开发管理 -> 开发设置 -> “服务器域名” 中设置 request 合法域名

   - 添加 https://apis.map.qq.com

7.LBS 逆地址解析

步骤:

1. 在项目中引入 SDK 核心类

2. 在 `onLoad` 中实例化 API 核心类,同时配置创建的 key

3. 使用实例方法 `reverseGeocoder` 方法进行逆地址解析,将提供的坐标转换为详细的地址位置信息

代码:

1. 引入 SDK 核心类


   // var QQMapWX = require('../../libs/qqmap-wx-jssdk.js');
   import QQMapWX from '../../../../../libs/qqmap-wx-jssdk.min'


   

2. 实例化 API 核心类


   // 引入SDK核心类,js文件根据自己业务,位置可自行放置
   import QQMapWX from '../../../../../libs/qqmap-wx-jssdk.min'
   
   Page({
    
     onLoad: function () {
   
       // 实例化API核心类
       this.qqmapsdk = new QQMapWX({
         key: '申请的key'
       })
    
     }
       
     // coding  
   }

   

3. 使用 `reverseGeocoder` 方法进行逆地址解析,将提供的坐标转换为所在位置的文字描述的转换

   // LBS 地址逆解析
   // 地理定位
   async onLocation() {
     // 获取 纬度 、精度
     // const { latitude, longitude } = await wx.getLocation()
     // console.log(location)
   
     // 获取经、纬度、位置名称
     let { latitude, longitude, name } = await wx.chooseLocation()
   
     // 使用 reverseGeocoder 方法进行逆地址解析
     this.qqmapsdk.reverseGeocoder({
       // 传入经、纬度
       location: {
         latitude,
         longitude
       },
   
       // 逆地址解析成功后执行
       success: (res) => {
         // 获取选择的
         const { street_number } = res.result.address_component
   
         // province 省  city 市  district 区
         const {
           province, // 省
           city, // 市
           district, // 区
           adcode, // 行政区划代码
           city_code, // 城市代码,由国家码+行政区划代码(提出城市级别)组合而来,总共为9位
           nation_code // 国家代码
         } = res.result.ad_info
   
         this.setData({
           // 省级: 前两位有值,后4位置0,如,河北省: 130000
           provinceCode: adcode.replace(adcode.substring(2, 6), '0000'),
           provinceName: province,
   
           // 市前面多个国家代码,需要进行截取
           cityCode: city_code.slice(nation_code.length),
           cityName: city,
   
           // 东莞市、中山市、修州市、嘉关市 因其下无区县级,
           districtCode: district && adcode,
           districtName: district,
   
           // 详细地址
           address: name,
           fullAddress: [province, city, district, address].join('')
         })
       }
     })
   }

8.async-validator 基本使用

步骤:

1. 安装并在项目中导入 `async-validator`

2. 创建验证规则

3. 创建表单验证实例,将验证规则传递给构造函数,产生实例

4. 调用实例方法 `validate` 对数据进行验证

   - 第一个参数:需要验证的数据

   - 第二个参数:回调函数,回调函数有两个参数 errors, fields

     - errors:如果验证成功,返回 null,验证错误,返回数组

     - fields:需要验证的字段,属性值错误数组

代码:

1. 安装 `async-validator`

   shell
   npm i async-validator


   

2. 开发者工具,点击构建 `npm`,对  `async-validator` 进行构建

3. 在 js 文件中导入 `async-validator`


   // 1️⃣ 引入 async-validator,async-validator 提供了一个构造函数
   import Schema from 'async-validator'
   
   Page({
     // 2️⃣定义需要验证的数据
     data: {
       name: '你好'
     },
   
     // 验证数据
     onValidate() {
       // 3️⃣创建表单验证规则
       const rules = {
         // key 建议和 需要验证的数据字段名字保持一致
         name: [
           // required 是否是必填项
           { required: true, message: 'name 不能为空' },
   
           // type 数据的类型
           // message 如果验证失败,提示的错误内容
           { type: 'string', message: 'name 不是字符串' },
   
           // min 最少位数,max 最大位数
           { min: 2, max: 5, message: '名字最少 2 个字,最多 5 个字' }
   
           // 正则表达式
           // { pattern: '', message: '' }
   
           // 自定义验证规则
           // { validator: () => {} }
         ]
       }
   
       // 4️⃣创建表单验证实例
       // 在创建实例时需要传入验证规则
       const validator = new Schema(rules)
   
       // 5️⃣ 调用 validate 实例方法对数据进行验证
       // validate 方法接收一个对象作为参数,对象是需要验证的数据
       // 注意:validate 方法只会验证和验证规则同名的属性
       validator.validate(this.data, (errors, fields) => {
      // 如果验证失败,errors 是所有错误的数组
         // 如果验证成功,errors 是 null
         console.log(errors)
   
         // fields 是需要验证的属性,属性值是数组,数组中包含错误信息
         console.log(fields)
   
         if (errors) {
           console.log('验证没有通过')
           console.log(errors)
           return
         }
   
         console.log('验证通过')
       })
     }
   })
   

9.新增收货地址表单验证

步骤与思路:

在点击新增收货地址的时候,我们需要对用户输入的值进行验证。产品需求如下:
 

1. 收货人不能为空,且不能输入特殊字符

2. 手机号不能为空,且输入的手机号必须合法

3. 省市区不能为空

4. 详细地址不能为空

1. 创建 `validateForm` 方法,使用 `async-validator` 对表单进行验证

2. 在新增收货地址之前,调用 `validateForm` 方法,如果验证成功执行新增守护地址的逻辑

代码:

 /modules/settingModule/pages/address/add/index


import Schema from 'async-validator'

Page({

  // coding....

    // 保存收货地址
  async saveAddrssForm() {
    // 组织参数 (完整地址、是否设置为默认地址)

    const {
      provinceName,
      cityName,
      districtName,
      address,
      isDefault
    } = this.data

    // 最终需要发送的请求参数
    const params = {
      ...this.data,
      fullAddress: provinceName + cityName + districtName + address,
      isDefault: isDefault ? 1 : 0
    }

    // 调用方法对最终的请求参数进行验证
    const { valid } = await this.validateAddress(params)

    // 如果验证没有通过,不继续执行后续的逻辑
    if (!valid) return
    
    console.log(params)
  },

  // 验证新增收货地址请求参数
  // 形参 params 是需要验证的数据
  validateAddress(params) {
    // 验证收货人,是否只包含大小写字母、数字和中文字符
    const nameRegExp = '^[a-zA-Z\\d\\u4e00-\\u9fa5]+$'

    // 验证手机号
    const phoneReg = '^1(?:3\\d|4[4-9]|5[0-35-9]|6[67]|7[0-8]|8\\d|9\\d)\\d{8}$'

    // 创建验证规则,验证规则是一个对象
    // 每一项是一个验证规则,验证规则属性需要和验证的数据进行同名
    const rules = {
      name: [
        { required: true, message: '请输入收货人姓名' },
        { pattern: nameRegExp, message: '收货人姓名不合法' }
      ],
      phone: [
        { required: true, message: '请输入收货人手机号' },
        { pattern: phoneReg, message: '手机号不合法' }
      ],
      provinceName: { required: true, message: '请选择收货人所在地区' },
      address: { required: true, message: '请输入详细地址' }
    }

    // 创建验证实例,并传入验证规则
    const validator = new Schema(rules)

    // 调用实例方法对数据进行验证
    // 注意:我们希望将验证结果通过 Promsie 的形式返回给函数的调用者
    return new Promise((resolve) => {
      validator.validate(params, (errors, fields) => {
        if (errors) {
          // 如果验证失败,需要给用户进行提示
          wx.toast({
            title: errors[0].message
          })

          resolve({ valid: false })
        } else {
          resolve({ valid: true })
        }
      })
    })
  },

10.实现新增收货地址

步骤与思路:

在实现了新增收货地址的数据收集、表单验证以后,我们需要实现新增收货地址的功能,将用户的收货地址到服务器。我们直接根据接口文档,封装接口 `API`,然后在表单验证以后,进行收货地址的添加即可。



 

1. 在对新增收货地址请求参数验证以后,将封装好的新增收货地址的 `API` 函数调用

2. 在新增收货地址成功以后,跳转到收货地址详情页面。

代码:

 /pages/address/add/index.js


// 新增或修改地址
async saveAddrssForm(event) {
  // 组织参数 (完整地址、是否设置为默认地址)
  const {
    provinceName,
    cityName,
    districtName,
    address,
    isDefault
  } = this.data

  // 最终需要发送的请求参数
  const params = {
    ...this.data,
    fullAddress: provinceName + cityName + districtName + address,
    isDefault: isDefault ? 1 : 0
  }
  
  // 如果验证没有通过,不进行后续处理
  if (!valid) return

  // 发送请求,保存收货地址
  const res = await reqAddAddress(params)
  
  if (res.code === 200) {
    wx.navigateBack({
      success() {
        wx.toast({ title: '新增收货地址成功' })
      }
    })
  }
    
}

11.收货地址列表渲染

步骤与思路:

渲染收货地址需要收货地址的数据,需要调用接口获取收货地址数据,使用返回的数据进行结构的渲染。

1. 在 `onShow` 钩子函数中调用`reqAddressList`方法

2. 在获取到数据以后,使用后端返回的数据对页面进行渲染

代码:

/modules/settingModule/pages/address/list/index.js`

// pages/address/list/index.js
import { reqAddressList } from '../../../../../api/address'

Page({
  // 页面的初始数据
  data: {
     addressList: [] // 收货地址列表
  },

   // 获取收货地址
   async getAddressList() {
     // 调用 API,获取收货地址
    const { data: addressList } = await reqAddressList()
    this.setData({
    addressList
   })
  },

  // 去编辑页面
  toEdit() {
    wx.navigateTo({
      url: '/modules/settingModule/pages/address/add/index'
    })
  },

 onLoad() {
   this.getAddressList()
  }
})





`➡️ /modules/settingModule/pages/address/list/index.wxml`

```html
<view class="list-warpper" wx:if="{{ addressList.length }}">
  <view wx:for="{{ addressList }}" wx:key="id" class="list-item">
    <van-swipe-cell right-width="{{ 65 }}">
      <view class="list-item-box">
        <view class="info">
          <view class="user-info">
            <text>{{ item.name }}</text>
          <text>{{ item.phone }}</text>
            <text wx:if="{{ item.isDefault }}" class="default-tag">默认</text>
    </view>

        <view class="address-info"> {{ item.fullAddress }} </view>
        </view>

        <view class="editBtn">
          <van-icon bindtap="toEdit" name="edit" size="22px" color="#999" />
        </view>
      </view>
      <!-- <van-icon name="delete" size="22px" color="#999" /> -->
      <view slot="right" class="van-swipe-cell__right">
        <text>删除</text>
      </view>
    </van-swipe-cell>
  </view>
</view>

12. 实现更新收货地址

步骤与思路:

新增和编辑收货地址页面是同一个页面,我们需要在这个页面处理新增和编辑功能

在收货地址列表页面,点击更新按钮时,需要跳转到新增/更新页面,同时需要将更新这一项的 `id` 传递给新增/更新页面。

在 `onLoad` 中获取 `id`,并且使用 `id ` 区分用户是进行新增还是编辑的操作。

如果存在 `id`,在获取需要更新的收货地址的数据,并进行页面的回显用户的收货地址,并且需要更新导航栏标题

因为我们之前直接是将数据放到 `data` 中的,所以我们直接将数据使用 `setData` 赋值即可

1. 在从收货地址列表页面跳转到更新页面的时候,需要携带 `id`

2. 在 `onLoad` 中判断是否存在 `id`,如果存在 `id`,在获取数据进行回显

代码:

/modules/settingModule/pages/address/list/index.wxml`


<!-- 编辑、删除按钮 -->
<van-icon bindtap="toEdit" data-id="{{ item.id }}" name="edit" size="22px" color="#999" />



/modules/settingModule/pages/address/list/index.js`

// 去编辑页面
toEdit(event) {
  // 需要编辑的收货地址
  const { id } = event.target.dataset

  wx.navigateTo({
    url: `/modules/settingModule/pages/address/add/index?id=${id}`
  })
}



 /modules/settingModule/pages/address/add/index.js`


Page({
   
  // coding...
    
  // 保存收货地址
  async saveAddrssForm() {
    // 组织参数 (完整地址、是否设置为默认地址)
    const {
      provinceName,
      cityName,
      districtName,
      address,
      isDefault
    } = this.data

    // 最终需要发送的请求参数
    const params = {
      ...this.data,
      fullAddress: provinceName + cityName + districtName + address,
      isDefault: isDefault ? 1 : 0
    }

    // 调用方法对最终的请求参数进行验证
    const { valid } = await this.validateAddress(params)

    // 如果验证没有通过,不继续执行后续的逻辑
    if (!valid) return

     // 发送请求,保存收货地址
     const res = this.addressId
       ? await reqUpdateAddress(params)
       : await reqAddAddress(params)

     if (res.code === 200) {
       // 提示用户更新状态
       wx.toast({
         title: this.addressId ? '编辑收货地址成功' : '新增收货地址成功'
       })
 
       // 返回到收货地址列表页面
       wx.navigateBack()
    }
  },
   
   // 回显收货地址的逻辑
   showAddressInfo(id) {
     // 判断是否存在 id,如果不存在 id,return 不执行后续的逻辑
     if (!id) return
 
     // 如果存在 id,将 id 挂载到 this 页面实例上
     this.addressId = id
 
     // 动态设置当前页面的标题
     wx.setNavigationBarTitle({
       title: '更新收货地址'
     })
 
     // 调用方法获取收货地址详细信息
     const { data } = await reqAddressInfo(this.addressId)
     // 将获取的数据进行赋值
     this.setData(data)
   },

  onLoad(options) {
    // 对核心类 QQMapWX 进行实例化
    this.qqmapwx = new QQMapWX({
      // key 要使用自己申请的 key
      // 在进行逆解析的时候,如果发现 key 只能使用一次,需要在腾讯位置服务后台配置额度
      key: 'S5CBZ-TQXCB-L73UJ-J6VJA-FXS53-JNBY3'
    })

     // 回显收货地址的逻辑
     this.showAddressInfo(options.id)
  }
    
  // coding
})

13. 实现删除收货地址

思路与步骤:

点击删除按钮的时候,需要将对应的地址进行删除

当点击删除按钮的时候,调用封装的接口 `API` 函数 ,同时传递需要删除的收货地址 `id` 即可

实现步骤

1. 给删除按钮绑定点击事件 `delAddress`,同时通过 `data-id` 传递需要删除的商品 `id`

2. 在 `delAddress` 事件处理程序后面,调用 `API` 函数 `reqDelAddress`,并传递 `id`

3. 在删除收货地址成功以后,给用户提示

代码:

 /modules/settingModule/pages/address/list/index.wxml`


<van-icon
+   bindtap="delAddress"
+   data-id="{{ item.id }}" 
  name="delete"
  size="22px"
  color="#999"
/>
```



/modules/settingModule/pages/address/list/index.js`


// 删除收货地址
async delAddress(e) {
  const { id } = e.target.dataset

  await reqDelAddress(id)
  this.getAddressList()
}

四、商品管理

1.配置商品管理分包

思路与步骤:

随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。

因此我们需要将 `商品列表` 和 `商品详情` 功能配置成一个分包,

当用户在访问设置页面时,还预先加载 `商品列表` 和 `商品详情` 所在的分包

实现步骤:

1. 在 `modules` 目录下创建 `goodModule` 文件夹,用来存放商品管理分包

2. 在 `app.json` 的 `subpackages` 进行商品管理分包配置

3. 在 `app.json` 的 `preloadRule` 进行商品管理分包配置

代码:

 app.json

```json
{
  "subPackages": [
    {
      "root": "modules/settingModule",
      "name": "settingModule",
      "pages": [
        "pages/address/add/index",
        "pages/address/list/index",
        "pages/profile/profile"
      ]
    },
     {
       "root": "modules/goodModule",
       "name": "goodModule",
       "pages": ["pages/goods/list/list", "pages/goods/detail/detail"]
    }
  ],
  "preloadRule": {
    "pages/settings/settings": {
      "network": "all",
      "packages": ["settingModule"]
    },
     "pages/category/category": {
       "network": "all",
       "packages": ["goodModule"]
     }
  }
}

2.封装商品模块接口 API

代码:

api/goods.js`



```js
import http from '../utils/http'

/**
 * @description 获取商品列表
 * @return Promise
 */
export const reqGoodsList = ({ limit, page, ...reset }) => {
  return http.get(`/mall-api/goods/list/${page}/${limit}`, reset)
}

/**
 * @description 获取商品详情
 * @param {*} goodsId 商品Id
 * @returns Promise
 */
export const reqGoodsInfo = (goodsId) => {
  return http.get(`/mall-api/goods/${goodsId}`)
}

3.商品列表-准备列表请求参数

实现步骤:
 

1. 在商品列表的 `data` 字段中,根据接口文档,定义商品列表接口需要使用的字段

2. 在商品列表的 `onLoad` 钩子函数中接收请求的参数,并将请求参数进行合并

代码:

/modules/goodsModule/pages/list/list.js`

```js
Page({
  // 页面的初始数据
  data: {
    goodsList: [], // 商品列表数据
    isFinish: false, // 判断数据是否加载完毕

     // 接口请求参数
     requestData: {
       page: 1, // 页码
       limit: 10, // 每页请求多少条数据
       category1Id: '', // 一级分类 id
       category2Id: '' // 二级分类 id
     }
  },

     // 生命周期函数--监听页面加载
     onLoad(options) {
       // 接收传递的参数
       Object.assign(this.data.requestData, options)
     }
})

4.商品列表-获取商品列表数据并渲染

思路与步骤:

在准备商品列表的请求参数以后,

在页面调用 `API` 函数获取商品列表的数据,在获取到数据以后,使用后端返回的数据对页面进行渲染。

实现步骤:
 

1. 在 `/pages/goods/list/list.js` 中导入封装好的获取商品列表的 `API` 函数

2. 页面数据在页面加载的时候进行调用,在 `onLoad` 钩子函数中调用  `reqGoodsList` 方法

3. 在获取到数据以后,使用后端返回的数据对页面进行渲染

代码:

/modules/goodsModules/pages/list/list.js

```js
+ import { reqGoodsList } from '../../../../../api/good'

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

   // 获取商品列表的数据
   async getGoodsList() {
     // 调用 API 获取数据
    const { data } = await reqGoodsList(this.data.requestData)
 
    // 将返回的数据赋值给 data 中的变量
     this.setData({
       goodsList: data.records,
       total: data.total
     })
   },

  // 生命周期函数--监听页面加载
  onLoad(options) {
    // 接收传递的参数
    Object.assign(this.data.requestData, options)

   // 获取商品列表的数据
   this.getGoodsList()
  }
})

```



 /modules/goodsModule/pages/list/list.wxml`

```html

<view class="container">
  <!-- 商品列表功能 -->
   <view class="goods-list" wx:if="{{ goodsList.length }}">
     <block wx:for="{{ goodsList }}" wx:key="id">
       <goods-card goodItem="{{ item }}"></goods-card>
     </block>

    <!-- 数据是否加载完毕 -->
    <view class="finish" hidden="{{ !isFinish }}">数据加载完毕~~~</view>
  </view>

   <!-- 商品为空的时候展示的结构 -->
   <van-empty wx:else description="该分类下暂无商品,去看看其他商品吧~">
     <van-button round type="danger" class="bottom-button" bindtap="gotoBack">
       查看其他商品
     </van-button>
  </van-empty>
</view>

5.商品列表-实现上拉加载更多功能

思路与步骤:
 

当用户从下向上滑动屏幕时,需要加载更多的商品数据。
 

首先需要在 `.js` 文件中声明 `onReachBottom` 方法监听用户是否进行了上拉

当用户上拉时,需要对 `page` 页码进行加 1,代表要请求下一页的数据

当参数发生改变后,需要重新发送请求,拿最新的 `page` 向服务器发送请求获取数据。

在下一页的商品数据返回以后,将最新的数据和之前的数据进行合并


 

实现步骤:
 

1. `list.js` 文件中声明 `onReachBottom` 事件处理函数,监听用户的上拉行为

2. 在 `onReachBottom` 函数中加 `page` 进行加 1 的操作,同时发送请求获取下一页数据

3. 在 `getGoodsList` 函数中,实现参数的合并

代码:

 /modules/goodsModule/pages/list/list.js`

```js
import { reqGoodsList } from '../../../api/goods'

Page({

  // coding...

  // 获取商品列表的数据
  async getGoodsList() {
    // 调用 API 获取数据
    const { data } = await reqGoodsList(this.data.params)

    // 将返回的数据赋值给 data 中的变量
    this.setData({
       goodsList: [...this.data.goodsList, ...data.records],
      total: data.total
    })
  },

  // coding...

   // 监听页面的上拉操作
   onReachBottom() {
     let { page } = this.data.requestData
 
     // 页码 + 1
     this.setData({
       requestData: { ...this.data.requestData, page: page + 1 }
     })
 
     // 重新发送请求
     this.getGoodsList()
   }

})

6.商品列表-判断数据是否加载完毕

步骤:

1. 在数据返回以后,将数据中的 `total` 赋值给 `data` 中的变量 `total`

2. 在 `onReachBottom` 中进行 `total` 和 `goodsList` 进行对比

3. 模板中使用 `total` 和 `goodsList` 进行对比

代码:

 /modules/goodsModule/pages/list/list.js`

```js
import { reqGoodsList } from '../../../api/goods'

Page({
    
  // coding...
    
  // 监听页面的上拉操作
  onReachBottom() {
+     // 从 data 中解构数据
+     const { total, goodsList, requestData } = this.data
+     let { page } = requestData
+ 
+     // 判断数据是否加载完毕
+     if (total === goodsList.length) {
+       // 如果相等,数据数据加载完毕
+       // 如果数据加载完毕,需要给用户提示,同时不继续加载下一个数据
+       this.setData({
+         isFinish: true
+       })
+ 
+       return
+     }

    // 页码 + 1
    this.setData({
      requestData: { ...this.data.requestData, page: (page += 1) }
    })

    // 重新发送请求
    this.getGoodsList()
  }
})

7. 商品列表-节流阀进行列表节流

注意事项:

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

我们使用节流阀来给商品列表添加节流功能。

在 `data` 中定义节流阀状态 `isLoading`,默认值是 `false`。

在请求发送之前,将 `isLoading` 设置为 `true`,表示请求正在发送。

在请求结束以后,将 `isLoading` 设置为 `false`,表示请求已经完成。

在 `onReachBottom` 事件监听函数中,对 `isLoading` 进行判断,如果数据正在请求中,不请求下一页的数据。

代码:

/modules/goodsModule/pages/list/list.js

```js
import { reqGoodsList } from '../../../../../api/good

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

  // 获取商品列表的数据
  async getGoodsList() {
     // 数据真正请求中
     this.data.isLoading = true

    // 调用 API 获取数据
    const { data } = await reqGoodsList(this.data.requestData)

     // 数据加载完毕
     this.data.isLoading = false

    // 将返回的数据赋值给 data 中的变量
    this.setData({
      goodsList: [...this.data.goodsList, ...data.records],
      total: data.total
    })
  },

  // 监听页面的上拉操作
  onReachBottom() {
    // 从 data 中解构数据
    const { total, goodsList, requestData, isLoading } = this.data
    let { page } = requestData

    // 判断是否加载完毕,如果 isLoading 等于 true
    // 说明数据还没有加载完毕,不加载下一页数据
    if (isLoading) return

    // 判断数据是否加载完毕
    // coding...
  }
})

8.商品列表-实现下拉刷新功能

步骤:

1. 在`页面.json` 中开启允许下拉,同时可以配置 窗口、loading 样式等

2. 在`页面.js` 中定义 `onPullDownRefresh` 事件监听用户下拉刷新

代码:


/modules/goodsModule/pages/list/list.json

```json
{
  "usingComponents": {
    "goods-card": "/components/goods-card/goods-card"
  },

  "navigationBarTitleText": "商品列表",
  "enablePullDownRefresh": true,
  "backgroundColor": "#f7f4f8",
  "backgroundTextStyle": "dark"
}




 /modules/goodsModule/pages/list/list.js

```js
// 监听页面的下拉刷新
onPullDownRefresh() {
  // 将数据进行重置
  this.setData({
    goodsList: [],
    total: 0,
    isFinish: false,
    requestData: { ...this.data.requestData, page: 1 }
  })

  // 重新获取列表数据
  this.getGoodsList()
}

9.商品详情-获取并渲染商品详情

步骤与思路:

点击首页轮播图以及点击商品列表商品的时候,需要跳转到商品详情页面

在跳转时将商品的`id` 传递到了商品详情页面,只需要使用 `id` 向后端服务器请求数据,获取对应商品的详情数据

在获取到数据以后,使用后端返回的数据对页面进行渲染。


 

实现步骤:

1. 在 `/pages/goods/detail/detail.js` 中导入封装好的获取商品列表的 `API` 函数

2. 页面数据在页面加载的时候进行调用,在 `onLoad` 钩子函数中调用  `reqGoodsInfo` 方法

3. 在获取到数据以后,使用后端返回的数据对页面进行渲染

代码:

/modules/goodsModule/pages/detail/detail.js`

```js
import { reqGoodsInfo } from '../../../api/goods'

Page({

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

  // 获取商品的详情
   async getGoodsInfo() {
     // 调用接口、传入参数、获取商品详情
    const { data: goodsInfo } = await reqGoodsInfo(this.goodsId)
 
     // 将商品详情数据赋值给 data 中的变量
     this.setData({
       goodsInfo
     })
   },

   // 生命周期函数--监听页面加载
   onLoad(options) {
     // 将商品 id 挂载到页面实例上
     this.goodsId = options.goodsId ? options.goodsId : ''
 
     // 获取商品详情的数据
     this.getGoodsInfo()
   }

  // coding...
})
```



 /modules/goodsModule/pages/detail/detail.html`

```html
<view class="container goods-detail">
  <!-- 商品大图 -->
  <view class="banner-img">
     <image class="img" src="{{ goodsInfo.imageUrl }}" />
  </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.material }}</view>
  </view>

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

  <!-- 商品的底部商品导航 -->
  <van-goods-action>
    <!-- 代码略... -->
  </van-goods-action>

  <!-- 加入购物车、立即购买弹框 -->
  <!-- show 控制弹框的隐藏和展示 -->
  <!-- bind:close 点击关闭弹框时触发的回调 -->
  <van-action-sheet show="{{ show }}" bind:close="onClose">
    <view class="sheet-wrapper">
      <view class="goods-item">
        <!-- 需要购买的商品图片 -->
        <view class="mid">
           <image class="img" src="{{ goodsInfo.imageUrl }}" />
        </view>

        <!-- 商品基本信息 -->
        <view class="right">
          <!-- 商品名字 -->
           <view class="title"> {{ goodsInfo.name }} </view>
          <!-- 商品价格 -->
          <view class="buy">
            <view class="price">
              <view class="symbol">¥</view>
               <view class="num">{{ goodsInfo.price }}</view>
            </view>

            <!-- 购买数量弹框 -->
            <view class="buy-btn">
              <!-- Stepper 步进器,由增加按钮、减少按钮和输入框组成,控制购买数量 -->
              <van-stepper value="{{ count }}" bind:change="onChangeGoodsCount" />
            </view>
          </view>
        </view>
      </view>

      <!-- 祝福语输入框 -->
      <view class="time-wraper">
        <!-- 代码略... -->
      </view>

      <!-- 取消、确定弹框 -->
      <view class="sheet-footer-btn">
        <van-button block type="primary" round> 确定 </van-button>
      </view>
    </view>
  </van-action-sheet>
</view>

10. 商品详情-详情图片预览功能

思路与步骤:

当点击商品的图片时,需要将图片进行全屏预览
 

如果想实现该功能,需要使用小程序提供的 `API`:`wx.previewImage()`,用来在新页面中全屏预览图片。预览的过程中用户可以进行保存图片、发送给朋友等操作。语法如下:

```js

wx.previewImage({

  current: '', // 当前显示图片的 http 链接

  urls: [] // 需要预览的图片 http 链接列表

})

```

实现步骤:

1. 给展示大图的 `image` 组件绑定点击事件,同时通过自定义属性的方式,传递当前需要显示的图片http 链接

2. 同时商品详情的数组数据传递给 `urls` 数组即可

代码:

/pages/goods/detail/detail.html`

```html
<!-- 商品大图 -->
<view class="banner-img">
  <image
     class="img"
     src="{{ goodsInfo.imageUrl }}"
     bindtap="previewImg"
  />
</view>
```



 /pages/goods/detail/detail.js`

```js
// 预览商品图片
previewImg() {
  // 调用预览图片的 API
  wx.previewImage({
    urls: this.data.goodsInfo.detailList
  })
}

11.样式图

五、购物车

1.购物车-封装购物车接口 API

思路:

为了方便后续进行购物车模块的开发,我们在这一节将购物车所有的接口封装成接口 API 函数

代码:

```js
import http from '../utils/http'

/**
 * @description 获取购物车列表数据
 * @returns Promise
 */
export const reqCartList = () => {
  return http.get('/mall-api/cart/getCartList')
}

/**
 * @description 加入购物车
 * @param {*} data
 * @returns Promise
 */
export const reqAddCart = (data) => {
  return http.get(`/cart/addToCart/${data.goodsId}/${data.count}`, data)
}

/**
 * @description 更新商品的选中状态
 * @param {*} goodsId 商品 id
 * @param {*} isChecked 商品的选中状态
 * @returns Promise
 */
export const reqUpdateChecked = (goodsId, isChecked) => {
  return http.get(`/cart/checkCart/${goodsId}/${isChecked}`)
}

/**
 * @description 全选和全不选
 * @param {*} isChecked 商品的选中状态
 * @returns Promise
 */
export const reqCheckAllCart = (isChecked) => {
  return http.get(`/cart/checkAllCart/${isChecked}`)
}

/**
 * @description 删除购物车商品
 * @param {*} goodsId 商品 id
 * @returns Promise
 */
export const reqDelCart = (goodsId) => {
  return http.get(`/cart/delete/${goodsId}`)
}

2.加入购物车-模板分析和渲染

业务介绍:

点击加入购物车和立即购买的时候,展示购物弹框,在弹框中需要用户选择购买数量和祝福语

点击加入购物车和立即购买,触发的是同一个弹框

因此点击弹框中的确定按钮时,我们需要区分当前是加入购物车操作还是立即购买操作。

这时候定义一个状态 `buyNow`  做区分,`buyNow`  等于 1 代表是立即购买,否则是加入购物车

产品需求:

1. 如果点击的是加入购物车,需要将当前商品加入到购物车

2. 如果点击的是立即购买,需要跳转到结算支付页面,立即购买该商品

3. 如果是立即购买,不支持购买多个商品

代码:

点击立即购买和加入购物车的时候,通过 show 属性,控制弹框的隐藏和展示

```html
<!-- 商品的底部商品导航 -->
<van-goods-action>
  <!-- coding... -->
   <van-goods-action-button text="加入购物车" type="warning" bindtap="handleAddcart" />
  <van-goods-action-button text="立即购买" bindtap="handeGotoBuy" />
</van-goods-action>

<!-- 加入购物车、立即购买弹框 -->
<!-- show 控制弹框的隐藏和展示 -->
<!-- bind:close 点击关闭弹框时触发的回调 -->
<van-action-sheet show="{{ show }}" bind:close="onClose">
  <view class="sheet-wrapper">
      
    <!-- 代码略... -->
      
    <!-- 购买数量弹框 -->
   <view class="buy-btn" wx:if="{{ buyNow === 0 }}">
      <!-- Stepper 步进器,由增加按钮、减少按钮和输入框组成,控制购买数量 -->
      <van-stepper value="{{ count }}" bind:change="onChangeGoodsCount" />
    </view>
      
    <!-- 代码略... -->
  </view>
</van-action-sheet>
```



点击立即购买和加入购物车的时候,通过 buyNow 属性,来区分是进行的某种操作

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

  // 加入购物车
  handleAddcart() {
    this.setData({
      show: true,
       buyNow: 0
    })
  },

  // 立即购买
  handeGotoBuy() {
    this.setData({
      show: true,
       buyNow: 1
    })
  },

3. 加入购物车-关联 Store 对象

思路:

当用户点击加入购物车 或者 立即购买时,需要判断用户是否进行了登录。

我们需要使用 `Token` 进行判断,因此需要让页面和 `Store` 对象建立关联。

这时候可以使用 `BehaviorWithStore` 让页面 和 `Store` 对象建立关联。

代码:

/behaviors/userBehavior.js

```js
// 导入 BehaviorWithStore 让页面和 Store 对象建立关联
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
// 导入用户 Store
import { userStore } from '@/stores/userstore'

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

```



 /behaviors/userBehavior.js`

```js
import { reqGoodsInfo } from '@/api/goods'
import { reqAddCart } from '@/api/cart'
+ import { userBehavior } from '@/behaviors/userBehavior'

Page({
   behaviors: [userBehavior],

})

4.加入购物车和立即购买区分处理

思路与步骤:

击加入购物车以及立即购买以后,需要先判断是否进行了登录,如果用户没有登录过,需要先跳转到登录页面进行登录。

如果点击的是 加入购物车,我们只需要调用 [加入购物车](https://apifox.com/apidoc/shared-6ed6c5c4-56c4-4619-8e2a-4817aa140e30/api-136581098) 接口即可 (需要获取商品的 ID 、购买数量、祝福语)

如果点击的是 立即购买,我们需要携带参数跳转到商品结算页面 (获取商品的 ID 以及 祝福语跳转到结算页面)

实现步骤:
 

1. 给 `Stepper` 步进器组件,通过`value`设置输入值,同时绑定`change`事件,并将值同步到 `data` 中

2. 根据接口文档,导入封装的购物车的接口 API

3. 点击弹框按钮的时候,判断点击的加入购物车还是立即购买,执行不同的操作

 /modules/goodsModule/pages/detail/detail.html

```html
<van-stepper
  value="{{ count }}"
+  integer
+  min="1"
+  max="200"
  bind:change="onChangeGoodsCount"
/>
```



 /modules/goodsModule/pages/detail/detail.js

```js
// 监听是否更改了购买数量
onChangeGoodsCount(event) {
  // 将最新的购买数量同步到 data
  this.setData({
    count: Number(event.detail)
  })
},

// 弹框的确定按钮
async handleSubmit() {
 // 解构获取数据
 const { token, count, blessing, buyNow } = this.data
 const goodsId = this.goodsId
  
  // 如果没有 token ,让用户新登录
  if (!this.data.token) {
    wx.navigateTo({
      url: '/pages/login/login'
    })
      
    return
  }

  // 将用户输入的值转成 Number 类型
  const count = Number(event.detail)
  // 验证购买数量的正则
  const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
  // 使用正则验证
  const res = reg.test(count)

  // 如果验证没有通过,直接返回,不执行后续的逻辑
  if (!res) return
 

  // 加入购物车
  if (buyNow === 0) {
    // 加入购物车
    const res = await reqAddGood({ goodsId, count, blessing })

    if (res.code === 200) {
        
      wx.showToast({
        title: '加入购物车成功'
      })
      
      this.setData({
        show: false
      })
        
    }
  } else {
    // 立即购买
    wx.navigateTo({
      url: `/pages/order/detail/index?goodsId=${goodsId}&blessing=${blessing}
    })
  }
}

5. 加入购物车-展示购物车购买数量

思路与步骤:

判断用户是否进行了登录。

如果没有登录过,则不展示购物车商品的数量。

如果用户登录过,则需要展示购物车商品的数量,则获取购物车列表数据,通过累加计算得出商品购买数量
 

实现步骤:

1. 进入商品详情,调用方法,在方法中判断`token`是否存在

2. 如何存在,则获取购物车列表数据,通过累加计算得出商品购买数量,展示购买的数量

3. 不存在,不执行任何逻辑

代码:

/modules/goodsModule/pages/detail/detail.js

```javascript
Page({
    
  data: {
    // coding...
     allCount: '' // 购物车商品总数量
 },
   
     // 弹框的确定按钮
 async handleSubmit() {
   // 如果没有 token ,让用户新登录
   if (!this.data.token) {
     wx.navigateTo({
       url: '/pages/login/login'
     })
     return
   }
   // 解构获取数据
   const { count, blessing, allCount } = this.data
   const goodsId = this.goodsId
   // 加入购物车
   if (this.data.buyNow === 0) {
     // 加入购物车
     const res = await reqAddCart({ goodsId, count, blessing })
     if (res.code === 200) {
       wx.toast({
         title: '加入购物车成功',
         icon: 'success',
         mask: false
       })
         // 购物车购买数量合计
         this.getCartCount()
       this.setData({
         show: false
       })
     }
   } else {
     // 立即购买
     wx.navigateTo({
       url: `/pages/order/detail/detail?goodsId=${goodsId}&blessing=${blessing}
     })
   }
 },
 
   // 计算购买数量
   async getCartCount() {
     // 如果没有 token ,说明用户是第一次访问小程序,没有进行登录过
     if (!this.data.token) return
 
     // 获取购物的商品
     const res = await reqCartList()
 
     if (res.data.length !== 0) {
       // 购物车商品累加
       let allCount = 0
 
       // 获取购物车商品数量
       res.data.forEach((item) => {
         allCount += item.count
       })
 
       // 将购物车购买数量赋值
       this.setData({
         // 展示的数据要求是字符串
         allCount: (allCount > 99 ? '99+' : allCount) + ''
       })
     }
   },
 onLoad(options) {
   // 接收传递的商品 ID,并且将 商品 ID 挂载到 this 上面
   this.goodsId = options.goodsId
   // 调用获取商品详情数据的方法
   this.getGoodsInfo()
     // 计算购买数量
     this.getCartCount()
  }
    
  // coding...
})


6. 购物车-购物车关联 Store 对象

思路:

当用户进入购物车页面时时,需要判断用户是否进行了登录来控制页面的展示效果

这时候我们就需要使用 `Token` 进行判断,因此需要让页面和 `Store` 对象建立关联。

因为购物车页面采用的 `Component` 方法进行构建

这时候可以使用 `ComponentWithStore` 让页面 和 `Store` 对象建立关联。

代码:

/pages/cart/components/cart.js

```js
 import { ComponentWithStore } from 'mobx-miniprogram-bindings'
 import { userStore } from '@/stores/userstore'
 import { reqCartList } from '@/api/cart'
 ComponentWithStore({
   storeBindings: {
     store: userStore,
     fields: ['token']
   },
 // 组件的初始数据
 data: {
   cartList: [],
   emptyDes: '还没有添加商品,快去添加吧'
 },
   // 组件的方法列表
   methods: {
     // 处理页面的展示
     async showTipList() {
       // 将 token 进行解构
       const { token } = this.data

 		console.log(token)
     },
   onShow() {
       this.showTipList()
   }
  }
})

7. 购物车-获取并渲染购物车列表

思路与步骤:

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

2. 如果用户进行登录,获取购物车列表数据

   购物车没有商品,展示文案: `还没有添加商品,快去添加吧~`

   购物车列表有数据,需要使用数据对页面进行渲染

实现步骤:

1. 导入封装好的获取列表数据的 `API` 函数

2. 在 `onShow` 钩子中,根据产品的需求,处理页面的提示

3. 在获取到数据以后,使用后端返回的数据对页面进行渲染

代码:

/pages/cart/cart.js`

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

ComponentWithStore({
  storeBindings: {
    store: userStore,
    fields: ['token']
  },

  // 组件的初始数据
  data: {
    cartList: [],
    emptyDes: '还没有添加商品,快去添加吧~'
  },

  // 组件的方法列表
  methods: {
     // 获取购物车列表数据 + 处理页面的展示
     async showTipGetList() {
       // 将 token 进行解构
       const { token } = this.data
 
       // 1. 如果没有登录,购物车列表,展示文案:您尚未登录,点击登录获取更多权益
       if (!token) {
         this.setData({
           emptyDes: '您尚未登录,点击登录获取更多权益',
           cartList: []
         })
 
         return
       }
 
       // 获取商品列表数据
       const { data: cartList, code } = await reqCartList()
 
       if (code === 200) {
         // 2. 如果用户登录,购物车列表为空,展示文案: 还没有添加商品,快去添加吧~
         this.setData({
           cartList,
           emptyDes: cartList === 0 && '还没有添加商品,快去添加吧~'
         })
       }
     },
   // 页面展示时触发
   onShow() {
       this.showTipGetList()
    }
  }
})

```



`➡️/pages/cart/components/cart.wxml`

```html
<view>
  <view
    wx:if="{{ token && cartList.length }}"
    class="container goods-wrap"
    bindtap="onSwipeCellPageTap"
  >
    <view class="cart-wrap">
       <view class="goods-item" wx:for="{{ cartList }}" wx:key="id">
       <van-swipe-cell class="goods-swipe" right-width="{{ 65 }}">
         <view class="goods-info">
           <view class="left">
             <van-checkbox
               checked-color="#FA4126"
                 value="{{ item.checked }}"
             ></van-checkbox>
           </view>
           <view class="mid">
               <image class="img" src="{{ item.imageUrl }}" />
           </view>
           <view class="right">
               <view class="title"> {{ item.name }} </view>
             <view class="buy">
               <view class="price">
                 <view class="symbol">¥</view>
                   <view class="num">{{ item.price }}</view>
               </view>
               <view class="buy-btn">
                   <van-stepper value="{{ item.count }}" />
               </view>
             </view>
           </view>
         </view>
         <view slot="right" class="van-swipe-cell__right">删除</view>
       </van-swipe-cell>
     </view>
   </view>
    <!-- 底部工具栏 -->
    <van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
      <van-checkbox value="{{ true }}" checked-color="#FA4126"> 全选 </van-checkbox>
    </van-submit-bar>
  </view>

  <van-empty wx:else description="{{ emptyDes }}">
     <navigator url="/pages/index/index" wx:if="{{ token }}">
       <van-button round type="danger" class="bottom-button">去购物</van-button>
     </navigator>
 
     <navigator url="/pages/login/login" wx:else>
       <van-button round type="danger" class="bottom-button">去登录</van-button>
     </navigator>
  </van-empty>
</view>

8. 购物车-更新商品的购买状态

思路与步骤:

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

1. 获取商品最新的购买状态,将最新的状态同步到服务器(需要调用封装的接口 API 函数,0 不购买,1 购买)

2. 在服务器更新状态更新成功以后,将本地的数据一并改变。

实现步骤:
 

1. 导入封装好的获取列表数据的 `API` 函数

2. 当点击切换切换商品状态的时候,调用 `reqUpdateGoodStatus`,并传参

3. 在更新成功,将本地的数据一并改变。

/pages/cart/cart.wxml

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



/pages/cart/cart.js

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

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

Component({

  // coding...

  // 组件的方法列表
  methods: {
    // 切换商品的选中状态
    async updateChecked(event) {
      // 获取最新的选中状态
      const { detail } = event
      // 获取商品的索引和 id
      const { id, index } = event.target.dataset
      // 将最新的状态格式化成后端所需要的数据格式
      const isChecked = detail ? 1 : 0

      // 调用接口,传入参数,更新商品的状态
      const res = await reqUpdateChecked(id, isChecked)

      // 如果数据更新成功,需要将本地的数据一同改变
      if (res.code === 200) {
        this.setData({
          [`cartList[${index}].isChecked`]: isChecked
        })
      }
    },

    // 获取购物车列表数据
    async getCartList() {
      // coding...
    }
  }
})

9. 购物车-控制全选按钮的选中状态

思路与步骤:

购物车列表中每个商品的状态 `isCheckd` 都是 1,说明每个商品都需要进行购买。

这时候就需要控制底部工具栏全选按钮的选中效果。

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

安装 框架拓展 `computed`

cmd

# 安装并构建 框架拓展 computed

npm i miniprogram-computed

步骤:

1. 在 `cart` 组件中引入 `miniprogram-computed` ,然后再 `behaviors` 中进行注册

2. 新建 `computed` 配置项,新增 `allStatus` 函数用来判断是否是全选

代码:

/pages/cart/cart.js
```js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
import { reqCartList, reqUpdateChecked } from '@/api/cart'

 const computedBehavior = require('miniprogram-computed').behavior

ComponentWithStore({
    
  // 注册计算属性
  behaviors: [computedBehavior],
  
  computed: {
    // 判断是否全选
    // computed 函数中不能访问 this ,只有 data 对象可供访问
    // 这个函数的返回值会被设置到 this.data.selectAllStatus 字段中
    selectAllStatus(data) {
      return (
        data.cartList.length !== 0 && data.cartList.every((item) => item.isChecked === 1)
      )
    }
  }
 // 其他代码略...
})
```


pages/cart/cart.wxml

```html
<!-- 底部工具栏 -->
<van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
   <van-checkbox value="{{ selectAllStatus }}" checked-color="#FA4126">
    全选
  </van-checkbox>
</van-submit-bar>

10. 购物车-实现全选和全不选功能

思路与步骤:

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

1. 点击全选按钮,获取全选按钮的选中状态(true, false),同时控制所有商品的选中与全不选效果

2. 在获取到全选按钮状态以后,同时需要将状态同步给服务器 (1 是全选,0 是全不选)

3. 在服务器更新成功以后,需要将本地的购物车商品选中状态也进行改变


实现步骤:

1. 导入封装好的全选的 `API` 函数

2. 当点击全选和全不选按钮的时候,调用 `reqCheckAll`,并传参

3. 在更新成功,将本地的数据一并改变。

代码:

 /pages/cart/cart.wxml

```html
<!-- 底部工具栏 -->
<van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
  <van-checkbox
    value="{{ selectAllStatus }}"
    checked-color="#FA4126"
    bind:change="changeAllStatus"
  >
    全选
  </van-checkbox>
</van-submit-bar>
```



/pages/cart/cart.js

```js
ComponentWithStore({
    
  // coding...
    
  methods: {
    // coding...
      
    // 全选和全不选功能
    async updateAllStatus(event) {
      // 获取全选和全不选的状态
      const isChecked = event.detail ? 1 : 0
      // 调用接口,更新服务器中商品的状态
      const res = await reqCheckAllStatus(isChecked)

      // 如果更新成功,需要将本地的数据一同改变
      if (res.code === 200) {
        // 将数据进行拷贝
        const newCart = JSON.parse(JSON.stringify(this.data.cartList))
        // 将数据进行更改
        newCart.forEach((item) => (item.isChecked = isChecked))

        // 进行赋值
        this.setData({
          cartList: newCart
        })
      }
    },
      
    // coding...
  }

})

11. 购物车-更新商品购买数量思路分析

思路:

在输入框中输入购买的数量,并**`不是直接将输入的数量同步给服务器,而是需要计算差值`**,服务器端进行处理

注意事项:

更新购买数量 和 加入购物车,使用的是同一个接口,为什么加入购物车没有计算差值,

 这是因为在加入购物车以后,服务器对商品购买数量直接进行了累加。

例如:之前购物车添加了某个商品,购买数量是 1 个,商品详情又加入 1 个, 直接累加,在购物车显示购买 2 个

12. 购物车-更新商品的购买数量

思路与步骤:

1. 必须是正整数,最小是`1`,最大是`200`

2. 如果输入的值大于`200`,输入框购买数量需要重置为`200`

3. 输入的值不合法或者小于`1`,还原为之前的购买数量

```js

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

```
 

实现步骤:

1.  给输入框绑定监听值是否改变的事件,同时传递商品的 ID `id` 和 商品的购买之前的购买数量 `num`

2.  在事件处理程序中获取到最新的数据,然后进行差值的运算

3.  发送请求即可

代码:

/pages/cart/cart.wxml`

```html
<van-stepper
+   integer
+   min="1"
+   max="200"
  value="{{ item.count }}"
+   data-id="{{ item.goodsId }}"
+   data-oldbuynum="{{ item.count }}"
+   data-index="{{ index }}"
+   bindchange="changeBuyNum"
/>
```



`➡️ /pages/cart/cart.js`

```js
// 更新购买的数量
async changeBuyNum(event) {
  // 获取最新的购买数量,
  // 如果用户输入的值大于 200,购买数量需要重置为 200
  // 如果不大于 200,直接返回用户输入的值
  let buynum = event.detail > 200 ? 200 : event.detail
  // 获取商品的 ID 和 索引
  const { id: goodsId, index, oldbuynum } = event.target.dataset

  // 验证用户输入的值,是否是 1 ~ 200 直接的正整数
  const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/

  // 对用户输入的值进行验证
  const regRes = reg.test(buynum)

  // 如果验证没有通过,需要重置为之前的购买数量
  if (!regRes) {
    this.setData({
      [`cartList[${index}].count`]: oldbuynum
    })

    return
  }

  // 如果通过,需要计算差值,然后将差值发送给服务器,让服务器进行逻辑处理
  const disCount = buynum - oldbuynum

  // 如果购买数量没有发生改变,不发送请求
  if (disCount === 0) return

  // 发送请求:购买的数量 和 差值
  const res = await reqAddCart({ goodsId, count: disCount })

  // 服务器更新购买数量成功以后,更新本地的数据
  if (res.code === 200) {
    this.setData({
      [`cartList[${index}].count`]: buynum
    })
  }
}

13. 购物车-更新商品购买数量防抖

思路与步骤:

每次改变购物车购买数量的时候,都会触发 `changeBuyNum` 事件处理程序,这会频繁的向后端发送请求,给服务器造成压力

我们希望用户在输入最终的购买数量,或者停止频繁点击加、减的以后在发送请求,在将购买数量同步到服务器。

这时候就需要使用 **防抖** 来进行代码优化。

`Licia` 是实用 `JavaScript` 工具库,该库目前拥有超过 400 个模块,同时支持浏览器、node 及小程序运行环境。可以极大地提高开发效率。
 

[licia 官网](https://licia.liriliri.io/)

[licia 中文使用文档](https://github.com/liriliri/licia/blob/HEAD/README_CN.md)

代码:

/pages/cart/cart.js`

```js
// 从 miniprogram-licia 导入防抖函数
import { debounce } from 'miniprogram-licia'

// 更新购买的数量
 changeBuyNum: debounce(async function (event) {
   // 代码略...
 }, 500)

14. 购物车-购物车商品合计

步骤与思路:

在订单提交栏位置,展示要购买商品的总金额。
 

需要判断购物车中哪些商品被勾选,然后将勾选商品的价格进行累加。

当用户更新了商品的状态,或者更新了商品的购买数量,我们都需要重新计算订单总金额。

我们需要基于购物车列表的数据,产生订单总金额,在这里我们使用依然使用 computed 来实现商品合计的功能



实现步骤:


 

1. 在 `computed` 配置项,新增 `totalPrice` 函数用来计算商品价格总和

代码:

 /pages/cart/cart.wxml

```html
<!-- 底部工具栏 -->
<van-submit-bar
  wx:if="{{ cartList.length }}"
  price="{{ totalPrice }}"
  button-text="去结算"
  tip="{{ true }}"
>
  <van-checkbox
    value="{{ selectAllStatus }}"
    checked-color="#FA4126"
    bindchange="selectAllStatus"
  >
    全选
  </van-checkbox>
</van-submit-bar>

```



 /pages/cart/cart.js

```js
ComponentWithStore({
    
  // coding...
    
  // 定义计算属性
  computed: {

    // coding...

    // 计算商品价格总和
    totalPrice(data) {
      let totalPrice = 0

      data.cartList.forEach((item) => {
        // 如果商品的 isChecked 属性等于,说明该商品被选中的
        if (item.isChecked === 1) {
          totalPrice += item.count * item.price
        }
      })

      return totalPrice
    }
  },
    
    
  // coding...
})

15. 购物车-删除购物车中的商品

步骤与思路:

点击删除按钮的时候,需要将对应的购物车商品进行删除
 

实现步骤:

1. 导入封装的接口 `API` 函数,同时导入处理删除自动关闭效果的 `behaviors` 并进行注册

2. 在点击删除以后,调用 `API` 函数,在删除购物车商品成功以后,给用户提示

代码:

 /pages/cart/components/cart.wxml

```html
  <view bindtap="onSwipeCellPage">
   
 <!-- 代码略 -->
   
 <van-swipe-cell
   class="goods-swipe"
   right-width="{{ 65 }}"
     id="swipe-cell-{{ item.goodsId }}"
     bind:open="swipeCellOpen"
     bind:click="onSwipeCellClick"
 >
   <van-cell-group border="{{ false }}">
     <view class="goods-info">
        <view class="left">
          <van-checkbox
            checked-color="#FA4126"
            value="{{ item.isChecked }}"
            bindchange="updateChecked"
            data-id="{{ item.goodsId }}"
            data-index="{{ index }}"
          ></van-checkbox>
        </view>
        <view class="mid">
          <image class="img" src="{{ item.imageUrl }}" />
        </view>
        <view class="right">
          <view class="title"> {{ item.name }} </view>
          <view class="buy">
            <view class="price">
              <view class="symbol">¥</view>
              <view class="num">{{ item.price }}</view>
            </view>
            <view class="buy-btn">
              <van-stepper
                min="1"
                max="200"
                integer
                value="{{ item.count }}"
                data-id="{{ item.goodsId }}"
                data-index="{{ index }}"
                data-oldbuynum="{{ item.count }}"
                bindchange="changeBuyNum"
              />
            </view>
          </view>
        </view>
      </view>
    </van-cell-group>
    <view
      slot="right"
    class="van-swipe-cell__right"
      bindtap="delCartGoods"
      data-id="{{ item.goodsId }}"
    >
      删除
    </view>
  </van-swipe-cell>
    
    
  <!-- 代码略 -->
</view>



/pages/cart/components/cart.wxml

```js
// 导入接口 API 函数
import {
  reqCartList,
  reqUpdateChecked,
  reqCheckAllStatus,
  reqAddCart,
   reqDelCartGoods
} from '@/api/cart'

 // 导入让删除滑块自动弹回的 behavior
 import { swipeCellBehavior } from '@/behaviors/swipeCell'


ComponentWithStore({
  // 注册 behavior
   behaviors: [swipeCellBehavior, computedBehavior],
   
 // 组件的方法列表
 methods: {
   // coding...
   // 删除购物车中的商品
   async delCartGoods(event) {
     // 获取需要删除商品的 id
     const { id } = event.currentTarget.dataset
 
     // 询问用户是否删除该商品
     const modalRes = await wx.modal({
       content: '您确认删除该商品吗 ?'
     })
 
     if (modalRes) {
       await reqDelCartGoods(id)
 
       this.showTipGetList()
     }
   },
     onHide() {
       // 在页面隐藏的时候,需要让删除滑块自动弹回
       this.onSwipeCellCommonClick()
     }
 }
})

六、结算支付

1.配置分包并跳转到结算页面

思路:

随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。

因此我们需要将 `结算支付` 功能配置成一个分包,

当用户在访问设置页面时,还预先加载 `结算支付` 所在的分包

代码:

 app.json

```json
"subPackages": [
  {
    "root": "modules/settingModule",
    "name": "settingModule",
    "pages": [
      "pages/address/add/index",
      "pages/address/list/index",
      "pages/profile/profile"
    ]
  },
  {
    "root": "modules/goodModule",
    "name": "goodModule",
    "pages": ["pages/goods/list/list", "pages/goods/detail/detail"]
  },
   {
    "root": "modules/orderPayModule",
     "name": "orderPayModule",
     "pages": [
      "pages/order/detail/detail",
      "pages/order/list/list"
     ]
   }
],
"preloadRule": {
  "pages/settings/settings": {
    "network": "all",
    "packages": ["settingModule"]
  },
  "pages/category/category": {
    "network": "all",
    "packages": ["goodModule"]
  },
+   "pages/cart/cart": {
+     "network": "all",
+     "packages": ["orderPayModule"]
+   }
}



 pages/cart/cart.js

```js
// 跳转到订单结算页面
toOrder() {
  if (this.data.totalPrice === 0) {
    wx.toast({
      title: '请选择需要购买的商品'
    })

    return
  }

  // 跳转到订单的结算页面
  wx.navigateTo({
    url: '/modules/orderPayModule/pages/order/detail/detail'
  })
}



 pages/cart/cart.wxml

```html
<van-submit-bar
  wx:if="{{ cartList.length }}"
  price="{{ totalPrice * 100 }}"
  button-text="去结算"
  tip="{{ true }}"
  bindsubmit="toOrder"
>
  <van-checkbox
    value="{{ selectAllStatus }}"
    checked-color="#FA4126"
    bindchange="selectAllStatus"
  >
    全选
  </van-checkbox>
</van-submit-bar>

2.封装结算支付的接口 API

思路:

为了方便后续进行结算支付模块的开发,我们在这一节将结算支付所有的接口封装成接口 API 函数

代码:

/api/orderpay.js

```js
import http from '@/utils/http'

/**
 * @description 获取订单详情
 * @returns Promise
 */
export const reqOrderInfo = () => {
  return http.get('/order/trade')
}

/**
 * @description 获取订单列表
 * @param {*} page 页码
 * @param {*} limit 每页展示的条数
 * @returns Promise
 */
export const reqOrderList = (page, limit) => {
  return http.get(`/order/order/${page}/${limit}`)
}

/**
 * @description 获取订单收货地址
 * @returns Promise
 */
export const reqOrderAddress = () => {
  return http.get('/userAddress/getOrderAddress')
}

/**
 * @description 获取立即购买商品的详情信息
 * @param { Object } params { goodsId: 商品 Id,  blessing:祝福语 }
 * @returns Promise
 */
export const reqBuyNowGoods = ({ goodsId, ...data }) => {
  return http.get(`/order/buy/${goodsId}`, data)
}

/**
 * @description 提交订单
 * @returns Promise
 */
export const reqSubmitOrder = () => {
  return http.post('/order/submitOrder')
}

/**
 * @description 获取微信预支付信息
 * @param {*} orderNo 订单 ID
 * @returns Promise
 */
export const reqPreBuyInfo = (orderNo) => {
  return http.get(`/webChat/createJsapi/${orderNo}`)
}

/**
 * @description 微信支付状态查询
 * @param {*} orderNo
 * @returns Promise
 */
export const reqPayStatus = (orderNo) => {
  return http.get(`/webChat/queryPayStatus/${orderNo}`)
}

3.商品结算-获取收货地址

步骤与思路:

进入结算支付页面后,需要获取收货地址信息,在获取到收货地址以后,需要进行判断,

如果没有获取到收货地址,需要展示添加收货地址的结构,

如果获取到了收货地址,需要渲染收货地址。
 

实现步骤:
 

1. 在进入结算页面的时候,调用接口 `API` 函数,获取数据

2. 然后根据数据并渲染结构

代码:

 /pages/order/detail/index.js`

```js
import { getTradeAddress } from '../../../api/order'

Page({
    
  data: {
    // coding...
     orderAddress: {} // 收货地址
  },

   // 获取收货地址
   async getAddress() {
     const { data: orderAddress } = await reqOrderAddress()
 
     this.setData({
       orderAddress
     })
   },

   // 页面展示时触发的钩子函数
   onShow() {
     this.getAddress()
   }
})
```


 /pages/order/detail/index.wxml`

```html
<!--pages/order/index.wxml-->
<view class="container order">
  <view class="address-card">
    <!-- 添加收货地址 -->
    <view wx:if="{{ !tradeAddress.id }}" class="add-address"  bindtap="toAddress">
      <van-icon size="22px" name="add" />
      <view>添加收货地址</view>
    </view>

    <view wx:else class="order-address flex">
      <view class="address-content">
        <view class="title">{{ tradeAddress.fullAddress }}</view>
        <view class="info flex">
          <text>{{ tradeAddress.name }}</text>
          <text>{{ tradeAddress.phone }}</text>
        </view>
      </view>

      <view class="select-address">
        <navigator class="navigator" url="/modules/settingModule/pages/address/list/index">
          <van-icon color="#bbb" name="arrow" size="22px" />
        </navigator>
      </view>
    </view>

    <view class="top-line"></view>
  </view>

  <view class="order-info">
    <!-- coding... -->
  </view>

</view>

4.商品结算-更新收货地址功能

步骤与思路:

当用户需要更改收货地址时,我们需要跳转到收货地址页面,重新选择收货地址

当用户点击了某个地址以后,我们需要将该地址显示到商品结算页面中。
 

更新收货地址功能,采用 `getApp()` 全局共享数据的方式来实现。


 

实现步骤:

1. 在 `app.js` 中定义全局共享的数据 `globalData.address`

2. 点击箭头,携带参数跳转到收货地址页面,标识是从订单结算页面进入

3. 在选择收货地址成功以后,将数据存储到 `globalData.address`中,然后返回到订单结算页面。

4. 在订单结算页面判断 `globalData.address` 是否存在收货地址数据,如果存在则渲染

代码:

 app.js`

```js
App({
    
+   // 定义全局共享的数据
+   globalData: {
+    address: {}
+  }
    
  // coding...
})
```





`➡️ /pages/address/list/index.html`

```html
<!-- 每一个收货地址 -->

<view
  class="info"
+   bindtap="changeAddress"
+   data-id="{{ item.id }}"
>
  <view class="user-info">
    <text>{{ item.name }}</text>
    <text>{{ item.phone }}</text>
    <text wx:if="{{ item.isDefault === 1 }}" class="default-tag">默认</text>
  </view>

  <view class="address-info"> {{ item.fullAddress }} </view>
</view>
```



`➡️ /pages/address/list/index.js`

```js
// 导入接口 API 函数
import { reqAddressList, reqDelAddress } from '@/api/address'
import { swipeCellBehavior } from '@/behaviors/swipeCell'

+ // 获取全局的应用实例
+ const app = getApp()

Page({

  // coding...

   // 切换收货地址
   changeAddress(event) {
     // 判断是否是从订单结算页面进入
     if (this.flag !== '1') return
 
     // 获取到点击的收货地址 id
     const addressId = event.currentTarget.dataset.id
     // 从收货地址列表中获取到获取到点击的收货地址详细信息
     const address = this.data.addressList.find((item) => item.id === addressId)
 
     // 如果获取成功,将数据存储到 globalData 中
     if (address) {
       app.globalData.address = address
       wx.navigateBack()
     }
   },
   onLoad(options) {
     this.flag = options.flag
   }
})

```



`➡️ /pages/order/detail/index.wxml`

```html
<view class="select-address">
  <navigator
    class="navigator"
     url="/modules/settingModule/pages/address/list/index?flag=1"
  >
    <van-icon color="#bbb" name="arrow" size="22px" />
  </navigator>
</view>
```



`➡️ /pages/order/detail/index.js`

```js
  // 获取订单页面的收货地址
  async getAddress() {
      
     // 如果 globalData 存在收货地址,取出收货地址
     if (app.globalData.address.id) {
       this.setData({
         orderAddress: app.globalData.address
       })
 
       // 在赋值以后需要将收货地址清空
       app.globalData.address = {}
 
       return
     }

    // 如果 globalData 中不存在收货地址,获取收货地址渲染即可
    const { data: orderAddress } = await reqOrderAddress()

    this.setData({
      orderAddress
    })
  },

5.商品结算-获取订单详情数据

步骤:

商品结算页面数据获取收货地址以及商品订单信息
 

实现步骤:
 

1. 导入封装的接口 `API` 函数

2. 在进入结算页面的时候,调用接口 `API` 函数,获取数据,然后根据数据并渲染结构即可

代码:

➡️ /pages/order/detail/index.js`

```js
 import { reqOrderAddress, reqOrderInfo } from '@/api/orderpay'

Page({
    
  data: {
    // coding...
    orderAddress: {}, // 收货地址
     orderInfo: {}, // 订单商品详情
 },
    
   // 获取订单详情
   async getOrderInfo() {
     const { data: orderInfo } = await reqOrderInfo()
 
     // 判断是否存在祝福语
     // 如果需要购买多个商品,挑选第一个填写了祝福语的商品进行赋值
     const orderGoods = orderInfo.cartVoList.find((item) => item.blessing !== '')
 
     this.setData({
       orderInfo,
       blessing: orderGoods && orderGoods.blessing
     })
   },

  // 在页面展示的时候进行触发
  onShow() {
    // 获取收货地址
    this.getAddress()

     // 获取订单结算页面的商品信息
     this.getOrderInfo()
  },
})
```



`➡️ /pages/order/detail/index.wxml`

```html
<!--pages/order/index.wxml-->
<view class="container order">
  <view class="address-card">
    <!-- 添加收货地址 -->
    <!-- coding... -->
  </view>

  <view class="goods-wraper">
    <!-- 商品清单 -->
    <view class="goods-list">
       <view class="goods-item flex" wx:for="{{ tradeInfo.cartVoList }}" wx:key="goodsId">
       <view class="img">
           <image src="{{ item.imageUrl }}" />
       </view>
       <view class="content">
           <view class="goods-title">{{ item.name }}</view>
         <view class="goods-price">
             <view class="price"> ¥ {{ item.price }}</view>
             <view>x {{ item.count }}</view>
          </view>
        </view>
      </view>
    </view>
  </view>

  <view class="payment">
    <!-- 支付方式 -->
    <view class="time-wraper flex">
      <image src="/static/images/payment_wxzf.png" />
      <view class="title">支付方式</view>
      <van-checkbox value="{{true}}"></van-checkbox>
    </view>
  </view>

  <!-- 支付区域 -->
  <view class="footer flex">
    <view class="left"> ¥ {{ tradeInfo.totalAmount }} </view>
    <viwe class="right">结算</viwe>
  </view>

  <!-- 日期选择弹框 -->
  <van-popup show="{{ show }}" round position="bottom" custom-style="height: 50%" bind:close="onClose">
    <van-datetime-picker type="date" min-date="{{ minDate }}" model:value="{{ currentDate }}" bind:confirm="onConfirmTimerPicker" bind:cancel="onCancelTimePicker" />
  </van-popup>

</view>

6.商品结算-获取立即购买数据

步骤与思路:

当用户从商品详情点击立即购买进入商品结算页面的时候,我们需要在商品结算页面展示立即购买商品的基本信息。
 

在跳转到商品结算页面的时候,我们已经携带了商品的 `id` 和 `祝福语`。

在结算页面,只需要获取到传递的参数,然后根据传递的参数调用接口即可。


 

实现步骤:
 

1. 在页面打开的时候,`onShow` 中接受传递的参数,并赋值给 `data` 中的状态

2. 在 `getOrderInfo` 函数中,判断立即购买商品的 `id` 是否存在,如果存在调用立即购买的接口

3. 获取数据后,然后根据数据并渲染结构即可

代码:

 /pages/order/detail/index.js

```js
import {
  reqOrderAddress,
  reqOrderInfo,
   reqBuyNowGoods
} from '@/api/orderpay'

Page({

    
  // 获取订单详情
  async getOrderInfo() {
 	  // 从 data 中结构数据      
     const { goodsId, blessing } = this.data

	 // 判断是否存在商品 id,
     // 如果存在调用立即购买商品详情的接口
     // 不存在调用获取订单详情数据接口
    const { data: orderInfo } = goodsId
      ? await reqBuyNowGoods({ goodsId, blessing })
      : await reqOrderInfo()

    // 判断是否存在祝福语
    // 如果需要购买多个商品,挑选第一个填写了祝福语的商品进行赋值
    const orderGoods = orderInfo.cartVoList.find((item) => item.blessing !== '')
      
    this.setData({
      orderInfo,
      orderGoods && orderGoods.blessing
    })
  }
    
 +  // 接收立即购买传递的参数
 +  onLoad (options) {
 +    this.setData({
 +       ...options
 +    })
 +  },

  // 在页面展示的时候进行触发
  onShow() {
    // 获取收货地址
    this.getAddress()

    // 获取订单结算页面的商品信息
    this.getOrderInfo()
  }
})

7. 商品结算-收集送达时间

思路与步骤:

当选择送达日期的时候,需要选择收货的时间,我们希望获取到的收货的时间格式是:年月日

但是我们使用的是小程序提供的 `vant` 组件,组件返回的时候并不是真正的时分秒,而是时间戳

这时候可以调用小程序项目初始化时,小程序封装的时间格式化工具

实现步骤:


 

1. 在商品结算页面导入封装好的格式化时间的方法 `formatTime`

2. 调用  `formatTime` ,传入需要格式化的时间戳

代码:

/pages/order/detail/index.js`

```js
import { formatTime } from '../../../utils/formatTime.js'

Page({
  
  // coding...

  // 期望送达日期确定按钮
  onConfirmTimerPicker(event) {
    // 使用 new Date 将时间戳转换成 JS 中的日期对象
    const time = formatTime(new Date(event.detail))
    
    // 将转换以后的时间赋值给送到时间
    this.setData({
      show: false,
      deliveryDate: time
    })
  }
 
  // coding...
}

8.商品结算-表单数据验证

步骤:

使用 `async-validator` 对代码进行验证


 

1. 收货地址不能为空

2. 订购人姓名不能为空,且不能输入特殊字符

3. 订购人手机号不能为空,且输入的手机号必须合法

4. 送达日期不能为空

代码:

js
import { reqOrderAddress, reqOrderInfo, reqBuyNowGoods } from '@/api/orderpay'
// 导入 async-validator 对参数进行验证
import Schema from 'async-validator'
// 导入格式化时间的方法
import { formatTime } from '@/utils/formatTime'

// 获取应用实例
const app = getApp()

Page({
  data: {
    buyName: '', // 订购人姓名
    buyPhone: '', // 订购人手机号
    deliveryDate: '', // 期望送达日期
    blessing: '', // 祝福语
    show: false, // 期望送达日期弹框
    orderAddress: {}, // 收货地址
    orderInfo: {}, // 订单商品详情
    minDate: new Date().getTime(),
    currentDate: new Date().getTime()
  },

  async submitOrder() {
    // 从 data 中结构数据
    const {
      buyName,
      buyPhone,
      deliveryDate,
      blessing,
      orderInfo,
      orderAddress
    } = this.data

    // 组织请求参数
    const params = {
      buyName,
      buyPhone,
      deliveryDate,
      remarks: blessing,
      cartList: orderInfo.cartVoList,
      userAddressId: orderAddress.id
    }

    // 对请求参数进项验证
    const { valid } = await this.validatorPerson(params)

    // 打印验证结果
    console.log(valid)
  },

  // 对新增收货地址请求参数进行验证
  validatorPerson(params) {
    // 验证收货人,是否只包含大小写字母、数字和中文字符
    const nameRegExp = '^[a-zA-Z\\d\\u4e00-\\u9fa5]+$'

    // 验证手机号,是否符合中国大陆手机号码的格式
    const phoneReg = '^1(?:3\\d|4[4-9]|5[0-35-9]|6[67]|7[0-8]|8\\d|9\\d)\\d{8}$'

    // 创建验证规则
    const rules = {
      userAddressId: [{ required: true, message: '请选择收货地址' }],
      buyName: [
        { required: true, message: '请输入收货人姓名' },
        { pattern: nameRegExp, message: '收货人姓名不合法' }
      ],
      buyPhone: [
        { required: true, message: '请输入收货人手机号' },
        { pattern: phoneReg, message: '收货人手机号不合法' }
      ],
      deliveryDate: { required: true, message: '请选择送达时间' }
    }

    // 传入验证规则进行实例化
    const validator = new Schema(rules)

    // 调用实例方法对请求参数进行验证
    // 注意:我们希望将验证结果通过 Promise 的形式返回给函数的调用者
    return new Promise((resolve) => {
      validator.validate(params, (errors) => {
        if (errors) {
          // 如果验证失败,需要给用户进行提示
          wx.toast({ title: errors[0].message })
          // 如果属性值是 false,说明验证失败
          resolve({ valid: false })
        } else {
          // 如果属性值是 true,说明验证成功
          resolve({ valid: true })
        }
      })
    })
  },
    
  
  // coding....
})

9. 小程序支付-小程序支付流程

步骤:

1. `生成平台订单`:前端调用接口,向后端传递需要购买的商品信息、收货人信息,[后端生成平台订单,返回订单编号]

2. `获取预付单信息`:将订单编号发送给后端后, [后端向微信服务器获取预付单信息,后端会将微信服务器返回的预付单信息进行加密,然后将加密以后的预付单信息返回给前端]

3. `发起微信支付`:前端调用 wx.requestPayment() 发起微信支付

4. `查询支付状态`:调用接口查询支付状态

10.小程序支付-创建平台订单

步骤与思路:

用户在完成选购流程,确认商品信息、订购人、收货人等信息无误后,

用户需要点击提交订单按钮,开始进行下单支付,这时候需要先创建平台订单。
 

实现步骤:
 

1. 在提交订单的事件处理函数中调用封装的接口 API 函数

2. 在接口调用成功以后,将服务器响应的订单编码挂载到页面实例上。

代码:

/pages/order/detail/index.js

```js
import {
  reqOrderAddress,
  reqOrderInfo,
  reqBuyNowGoods,
   reqSubmitOrder
} from '@/api/orderpay'

Page({
   
  // coding...
    
  // 提交订单
    // 处理提交订单
  async submitOrder() {
    // 需要从 data 中解构数据
    const {
      buyName,
      buyPhone,
      deliveryDate,
      blessing,
      orderAddress,
      orderInfo
    } = this.data

    // 需要根据接口要求组织请求参数
    const params = {
      buyName,
      buyPhone,
      cartList: orderInfo.cartVoList,
      deliveryDate,
      remarks: blessing,
      userAddressId: orderAddress.id
    }

    // 对请求参数进行验证
    const { valid } = await this.validatorPerson(params)

    // 如果验证失败,直接 return,不执行后续的逻辑处理
    if (!valid) return

    // 调用接口,创建平台订单
    const res = await reqSubmitOrder(params)

    // 在平台订单创建成功以后,将订单编号挂载到页面实例上
    if (res.code === 200) {
      // 将订单编号挂载到页面实例上
      this.orderNo = res.data
    }
  }

11.小程序支付-获取预付单信息

步骤与思路:

将订单编号发送给公司的后端,公司的后端会从数据库找到对应订单的信息。

然后调用微信服务器的 下单接口 进行创建订单,订单创建成功以后,微信服务器会给公司后端返回预付单信息。

公司后端对返回的预付单信息进行加密,返回给小程序客户端。

这一步,咱们需要做的就是:订单编号发送给公司的后端,其他逻辑时后端来完成的。

注意事项:

​ 小程序支付后面的代码,大伙在实现的时候,会出现异常。

​ 这是因为没有小程序的开发权限,以后在实际开发中,只需要参考当前流程进行开发即可

代码:

/pages/order/detail/index.js

```js
Page({
    // 处理提交订单
  async submitOrder() {
    // 需要从 data 中解构数据
    const {
      buyName,
      buyPhone,
      deliveryDate,
      blessing,
      orderAddress,
      orderInfo
    } = this.data

    // 需要根据接口要求组织请求参数
    const params = {
      buyName,
      buyPhone,
      cartList: orderInfo.cartVoList,
      deliveryDate,
      remarks: blessing,
      userAddressId: orderAddress.id
    }

    // 对请求参数进行验证
    const { valid } = await this.validatorPerson(params)

    // 如果请求参数验证失败,直接 return ,不执行后续的逻辑
    if (!valid) return

    // 调用接口,创建平台订单
    const res = await reqSubmitOrder(params)

    if (res.code === 200) {
      // 在平台订单创建成功以后,需要将服务器、后端返回的订单编号挂载到页面实例上
      this.orderNo = res.data
      
    // 获取预付单信息、支付参数
      this.advancePay()
    }
  },

  // 获取预付单信息、支付参数
  async advancePay() {
     // 调用接口,获取预付单信息、支付参数
     const payParams = await reqPrePayInfo(this.orderNo)
 
     if (payParams.code === 200) {
       console.log(res.data)
     }
   },

})

12.小程序支付-发起微信支付

步骤与思路:

小程序客户端在接收支付参数后,调用 `wx.requestPayment()` 发起微信支付,

唤醒支付弹窗,用户开输入支付密码或者进行指纹等操作,微信服务器会进行验证,如果验证成功,就会发起支付。

然后会将支付结果返回给公司后端,也会返回给 `wx.requestPayment()`

并且会微信通知用户支付结果

代码:

/pages/order/detail/index.js

```js
// 获取预付单信息、支付参数
async advancePay() {
 try {
    const payParams = await reqPrePayInfo(this.orderNo)

    if (payParams.code === 200) {
      // 进行微信支付
      const payInfo = await wx.requestPayment(payParams.data)
      
      console.log(payInfo)
    }
  } 
  catch {
    wx.toast({ title: '支付遇到问题,请联系客服', icon: 'error' })
  }
}

13.小程序支付-支付状态查询

步骤与思路:

通过调用后端接口获取支付状态,如果支付成功,需要给用户提示,同时跳转到订单列表页面。
 

公司后端开始向微信服务器发送请求,查询支付结果

公司服务器会将微信服务器返回的支付结果,返回到客户端

客户端根据查询结果跳转到订单列表页面

代码:

 /pages/order/detail/index.js

```js
// 获取预付单信息、支付参数
async advancePay() {
  try {
    const payParams = await reqPrePayInfo(this.orderNo)

    if (payParams.code === 200) {
      // payParams.data 就是获取的支付参数

      // 调用  wx.requestPayment 发起微信支付
      const payInfo = await wx.requestPayment(payParams.data)
      
      // 获取支付结果
      if (payInfo.errMsg === 'requestPayment:ok') {
        // 查询订单的支付状态
        const payStatus = await reqPayStatus(this.orderNo)

        if (payStatus.code === 200) {
          wx.redirectTo({
            url: '/pages/order/list/index',
            success: () => {
              wx.toast({
                title: '支付成功',
                icon: 'success
              })
            }
          })
        }
      }
      
    }
  } catch (error) {
    wx.toast({
      title: '支付失败,请联系客服',
      icon: 'error'
    })
  }
},

14.样式图

七、订单列表

1.封装订单列表接口API

代码:

api/orderpay.js

```js
/**
 * @description 获取订单列表
 * @returns Promise
 */
export const reqOrderList = (page, limit) => {
  return http.get(`/order/order/${page}/${limit}`)
}

2.获取订单列表数据并渲染

思路与步骤:

当用户从个人中心页面点击进入订单中心的时候,就需要获取到订单中心的数据。

在页面调用 `API` 函数获取订单列表的数据,

在获取到数据以后,使用后端返回的数据对页面进行渲染

代码:

 modules/orderPayModule/pages/order/list/list.js`

```js
 // 导入封装的接口 API 函数
 import { reqOrderList } from '@/api/orderpay'

Page({
  // 页面的初始数据
  data: {
    orderList: [1, 2, 3], // 订单列表
     page: 1, // 页码
     limit: 10, // 每页展示的条数
     total: 0 // 订单列表总条数
 },
   // 获取订单列表
   async getOrderList() {
     // 解构获取数据
     const { page, limit } = this.data
    // 调用接口获取订单列表数据
    const res = await reqOrderList(page, limit)

    if (res.code === 200) {
      this.setData({
        orderList: res.data.records,
        total: res.data.total
      })
    }
  },
  // 生命周期函数--监听页面加载
  onLoad() {
    this.getOrderList()
   }

})

```



`➡️ modules/orderPayModule/pages/order/list/list.wxml`

```html
<!--pages/order/list/index.wxml-->
<view class="order-container container">
  <view class="order-list" wx:if="{{ orderList.length > 0 }}">
    <view class="order-item" wx:for="{{ orderList }}" wx:key="index">
      <view class="order-item-header list-flex">
      <view class="orderno">订单号<text class="no">{{ orderList.orderNo }}</text></view>
        <view class="order-status {{ item.orderStatus === 1 ? 'order-active' : '' }}">
          {{ item.orderStatus === 1 ? '已支付' : '未支付'}}
        </view>
      </view>
     <view
       class="goods-item list-flex"
         wx:for="{{ item.orderDetailList }}"
         wx:key="id"
         wx:for-item="goods"
         wx:for-index="goodsIndex"
     >
       <view class="left">
           <image src="{{ goods.imageUrl }}" mode="widthFix" class="img" />
       </view>
       <view class="mid">
           <view class="goods-name">{{ goods.name }}</view>
           <view class="goods-blessing">{{ goods.blessing }}</view>
       </view>
       <view class="right">
           <view class="goods-price">¥{{ goods.price }}</view>
           <view class="goods-count">x{{ goods.count }}</view>
       </view>
     </view>
     <view class="order-item-footer">
       <view class="total-amount list-flex">
         <text class="text">实付</text>
           <text class="price"><text>¥</text>{{ item.totalAmount }}</text>
        </view>
      </view>
    </view>
  </view>
  <van-empty wx:else description="还没有购买商品,快去购买吧~" />
</view>

3.订单列表上拉加载更多

思路与步骤:

当用户进行了上拉操作时,需要在 `.js` 文件中声明 `onReachBottom` 方法,用来监听页面的上拉触底行为
 

当用户上拉时,需要对 `page` 参数进行加 1 即可,

当参数发生改变后,需要重新发送请求,拿最新的 `page` 向服务器要数据

在下一页的商品数据返回以后,需要将下一页的数据和之前的数据进行合并

代码:

modules/orderPayModule/pages/order/list/list.js

```js
// 导入封装的接口 API 函数
import { reqOrderList } from '@/api/orderpay'

Page({
  // 页面的初始数据
  data: {
    orderList: [1, 2, 3], // 订单列表
    page: 1, // 页码
    limit: 10, // 每页展示的条数
    total: 0 // 订单列表总条数
  },

  // 获取订单列表
  async getOrderList() {
    // 解构获取数据
    const { page, limit } = this.data
    // 调用接口获取订单列表数据
    const res = await reqOrderList(page, limit)

    if (res.code === 200) {
      this.setData({
         orderList: [...this.data.orderList, ...res.data.records],
        total: res.data.total
      })
    }
  },

   // 页面上拉触底事件的处理函数
   onReachBottom() {
     // 解构数据
     const { page } = this.data
 
     // 更新 page
     this.setData({
       page: page + 1
     })
 
     // 重新发送请求
     this.getOrderList()
   },
 // 生命周期函数--监听页面加载
 onLoad() {
   this.getOrderList()
 }
)

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

步骤与思路:

如何判断数据是否加载完成

可以使用后端返回的 `total` 和 `goodsList` 进行对比,如果 total 大于 `goodsList` ,说明订单中心数据没有加载完,可以继续上拉加载更多。

目前还没有接收 `total`,需要先将后台返回的 total 进行赋值到 data 中,然后使用 `onReachBottom` 中进行判断

代码:

modules/orderPayModule/pages/order/list/list.js
``js
/ 页面上拉触底事件的处理函数
nReachBottom() {
   // 解构数据
   const { page, total, orderList } = this.data
 
   // 数据总条数 和 订单列表长度进行对比
   if (total === orderList.length) {
     return wx.toast({ title: '数据加载完毕' })
   }
 // 更新 page
 this.setData({
   page: page + 1
 })
 // 重新发送请求
 this.getOrderList()

  • 24
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值