慕尚花坊过程笔记

1.mobx-miniprogram

1.1 mobx-miniprogram 介绍

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

  1. 作用与优势

    • 全局共享状态:使用mobx-miniprogram,开发者可以很方便地在小程序中全局共享状态。
    • 自动更新视图组件:当状态发生变化时,所有关联组件都会自动更新相对应的数据,无需手动操作。
    • 提升开发效率:通过自动更新视图组件,可以显著提升小程序的开发效率。
  2. 核心概念

    • observable:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。
    • action:用于定义改变状态的方法,确保状态改变的可追踪性和可预测性。
  3. 使用方式

    • 安装:需要安装两个包,分别是mobx-miniprogram和mobx-miniprogram-bindings。其中,mobx-miniprogram用于创建Store对象,存储应用的数据;而mobx-miniprogram-bindings则用于将状态和组件、页面进行绑定关联。
    • 创建Store对象:在小程序根目录下新建store.js文件,并使用observable和action等核心概念定义Store对象。
    • 在页面和组件中使用:可以通过behavior绑定或手工绑定的方式,将页面、自定义组件和store进行绑定,从而在页面和组件中操作数据。

1.2 创建 Store 对象

创建 Store 对象是使用 mobx-miniprogram 进行状态管理的第一步。Store 对象用于存储应用的数据,并通过 observable 属性使其响应式,以及通过 action 方法来修改这些数据。以下是创建 Store 对象的基本步骤:

1. 引入 mobx-miniprogram

首先,确保已经安装了 mobx-miniprogram 包。如果尚未安装,可以通过 npm 进行安装:

 

npm install mobx-miniprogram

2. 定义 Store

在小程序项目中创建一个新文件,例如 store.js,并在该文件中定义 Store 对象:

import { observable, action, computed } from 'mobx-miniprogram';

// 创建 Store 对象
const store = observable({
  // 定义响应式状态
  count: 0,

  // 定义修改状态的 action 方法
  increment: action(function () {
    this.count++;
  }),

  decrement: action(function () {
    this.count--;
  }),

  // 定义计算属性
  get doubleCount() {
    return this.count * 2;
  }
});

// 导出 Store 对象
export default store;

3. 使用 Store

创建完 Store 对象后,可以在小程序的页面或组件中使用它。为了实现这一点,需要使用 mobx-miniprogram-bindings 包来绑定 Store 和页面或组件

4. 绑定 Store 到页面或组件

使用 mobx-miniprogram-bindings 中的 createStoreBindings 函数将 Store 中的数据和行为绑定到页面或组件的 data 和方法中:

import { createStoreBindings } from 'mobx-miniprogram-bindings';
import store from './store.js'; // 引入 Store 对象

Page({
  data: {
    // 使用 createStoreBindings 来绑定 Store 中的数据
    count: createStoreBindings(store, ['count']).get()
  },
  onLoad() {
    // 绑定 Store
    this.storeBindings = createStoreBindings(this, {
      store,
      fields: ['count'],
      actions: ['increment', 'decrement']
    });
  },
  onUnload() {
    // 页面卸载时解绑
    if (this.storeBindings) {
      this.storeBindings.destroyStoreBindings();
    }
  },
  increment() {
    this.storeBindings.actions.increment();
  },
  decrement() {
    this.storeBindings.actions.decrement();
  }
});

在这个例子中,incrementdecrement 方法被绑定到页面上,允许通过调用这些方法来修改 Store 中的状态。同时,count 状态被绑定到页面的 data 中,使得它在状态变化时能够自动更新。

 1.3 在组件中使用数据

如果需要 `Page` 或者`Component`中对共享的数据进行读取、更新操作,需要使用 `mobx-miniprogram-bindings``mobx-miniprogram-bindings` 的作用就是将 `Store` 和 页面或组件进行绑定关联
 

如果需要在组件中使用状态,需要 `mobx-miniprogram-bindings` 库中导入 `ComponentWithStore` 方法在使用时:**<font color="red">需要将 `Component` 方法替换成 `ComponentWithStore` 方法 </font>**,原本组件配置项也需要写到该方法中

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

1. `store`: 指定要绑定的 `Store` 对象
2. `fields`: 指定需要绑定的 `data` 字段
3. `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.4 在页面中使用数据-方式1

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

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

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

1. 从 `mobx-miniprogram-bindings` 库中导入 `ComponentWithStore` 方法
2. 将 `Component` 方法替换成 `ComponentWithStore` 方法
3. 然后配置 `storeBindings` 从 `Store` 中映射数据和方法即可

// index/index.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 在页面中使用数据-方式2

使用 `mobx-miniprogram-bindings` 提供的 `BehaviorWithStore` 方法来和 `Store` 建立关联。

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

使用方式如下:

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

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

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

import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'

export const indexBehavior = BehaviorWithStore({
  storeBindings: {
    store: numStore,
    fields: ['numA', 'numB', 'sum'],
    actions: ['update'],
  }
})
// index.js

import { indexBehavior } from './behavior'

Page({
  behaviors: [indexBehavior]
    
  // 其他配置项
})

1.6 fields、actions 对象写法

在mobx-miniprogram-bindings中,fields和actions是用于定义你需要从MobX store中绑定到页面或组件的字段(observable)和动作(actions)的对象。

fields 对象

fields对象应该是一个对象字面量,其属性名对应你想要绑定的observable字段名。这些字段名将作为数据源的键(key)被映射到页面的data对象中。

actions 对象

actions对象也应该是一个对象字面量,其属性名对应你想要绑定的action名。这些动作名将被映射为页面方法,以便你可以在页面中直接调用它们。

1.7 绑定多个 store 以及命名空间

在实际开发中,一个页面或者组件可能会绑定多个 `Store` ,这时候我们可以将 `storeBindings` 改造成数组。数组每一项就是一个个要绑定的 `Store`。

​​​​如果多个 `Store` 中存在相同的数据,显示会出现异常。还可以通过 `namespace` 属性给当前 `Store` 开启命名空间,在开启命名空间以后,访问数据的时候,需要加上 `namespace ` 的名字才可以。

// behavior.js

import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'

export const indexBehavior = BehaviorWithStore({
  storeBindings: [
    {
      namespace: 'numStore',
      store: numStore,
      fields: ['numA', 'numB', 'sum'],
      actions: ['update'],
    }
  ]
})
// index/index.wxml

<view>{{ numStore.numA }} + {{ numStore.numB }} = {{numStore.sum}}</view>

2. miniprogram-computed

2.1 计算属性 `computed`

在 mobx-miniprogram 中,可以使用计算属性(computed)来基于其他状态(observable)定义衍生数据。这些计算属性会根据其所依赖的状态的变化而自动重新计算,从而确保数据始终是最新的。

然而,与 MobX 的传统用法不同,mobx-miniprogram 是为微信小程序设计的,它并没有直接提供 computed 装饰器或函数。但是,可以通过定义 getter 方法来模拟计算属性的行为。

以下是如何在 mobx-miniprogram 的 Store 中定义和使用计算属性的示例:

  1. 定义 Store

在 store.js 文件中,使用 observable 来定义状态,并使用 getter 方法来定义计算属性。

import { observable, action } from 'mobx-miniprogram';  
  
const store = observable({  
  // 响应式数据  
  numA: 1,  
  numB: 2,  
  
  // 计算属性(通过 getter 方法定义)  
  get sum() {  
    return this.numA + this.numB;  
  },  
  
  // 状态修改方法  
  addNumA: action('addNumA', (num) => {  
    this.numA += num;  
  }),  
  // ... 其他属性和方法  
});  
  
export default store;

在这个例子中,sum 是一个计算属性,它基于 numA 和 numB 的值进行计算。

2.2在组件中使用计算属性

在组件中,可以像访问普通数据属性一样访问计算属性。由于已经在 Store 中定义了 getter 方法,所以当 numA 或 numB 发生变化时,sum 的值也会自动更新。

import { observer, inject } from 'mobx-miniprogram-bindings';  
  
const myBehavior = inject('store')(observer);  
  
Component({  
  behaviors: [myBehavior],  
  
  methods: {  
    onTapButton() {  
      console.log(this.data.sum); // 输出 numA 和 numB 的和  
      this.addNumA(1); // 调用 action 更新 numA  
      console.log(this.data.sum); // 输出更新后的 numA 和 numB 的和  
    },  
  },  
});

在上面的例子中,onTapButton 方法首先输出了 sum 的当前值,然后调用了 addNumA 方法来增加 numA 的值。由于 sum 是一个计算属性,它会自动重新计算并反映 numA 的新值。因此,第二次调用 console.log(this.data.sum); 会输出更新后的和。

2.3监听器 `watch`

在使用时:**<font color="red">需要将 `Component` 方法替换成 `ComponentWithComputed` 方法 </font>**,原本组件配置项也需要写到该方法中在替换以后,就可以新增 `computed` 以及 `watch` 配置项。

// 引入 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: {
    // 同时对 a 和 b 进行监听
    'a, b': function (a, b) {
      this.setData({
        total: a + b
      })
    }
  },
  
  methods: {
    updateData() {
      this.setData({
        a: this.data.a + 1,
        b: this.data.b + 1
      })
    }
  }
})

3.用户管理

3.1 用户登录-什么是 token

  1. 定义与功能
    • Token是在微信小程序中进行API接口调用的一种凭据。
    • 它类似于身份认证,允许开发者在获取到Token后,使用该Token进行小程序的接口调用,以实现各种功能。
  2. 获取方式
    • Token的获取通常通过与微信服务器进行交互完成。具体来说,微信小程序登录采用了微信开放平台的登录接口,通过该接口可以实现用户的快速登录,并获取到用户的Token。
    • 在微信小程序中,用户的登录信息会被加密存储在微信的服务器中,而Token则用于后续的接口调用验证。

3.2 用户登录-小程序登录流程介绍

业务介绍:
传统的登录功能,需要用户先注册,注册完成以后,使用注册的账号、密码进行登录。小程序的登录操作则比较简单,小程序可以通过微信提供的登录能力,便捷地获取微信提供的用户身份标识进行登录。免去了注册和输入账号密码的步骤,从而提高了用户体验。

登录流程说明:
1. 用户访问小程序,点击 [登录] ,调用 `wx.login()` 方法获取 **临时登录凭证code** 临时登录凭证 code,就像是一个会过期的临时身份证一样,有效时间仅为 5分钟

2. 使用 `wx.request()` 方法将 **临时登录凭证code** 传递给开发者服务器,方便后续可以换取微信用户身份 id

3. 开发者的后台接收 **临时登录凭证code**,同时在微信公众后台拿到 `AppId` 和 `AppSecret` ,向微信服务器发送请求, 请求参数合法的话,微信服务器会给开发者后台返回 openid(微信用户的唯一标识) 以及 session_key(会话密钥) 等。

  openid 是微信用户的唯一标识,也就是微信用户身份 id,可以用这个 id 来区分不同的微信用户

   session_key 则是微信服务器给开发者服务器颁发的身份凭证,开发者可以用session_key请求微信服务器其他接口来获取一些其他信息

4. 开发者后台在接收到微信服务器返回的数据以后,会执行一些业务逻辑的处理,例如:将用户标识和其他信息进行加密处理,生成自定义登录态,这个登录态可以理解为就是 `Token` ,然后让 `Token` 与 `openid` 和 `session_key` 进行关联

5. 开发者后台处理好逻辑后,会将 自定义登录态 `Token` 返回给微信小程序客户端,客户端收到 `token` 以后,将其存储起来,比如放在 `localStorage` 中。

6. 客户端每次向开发者后台发送请求的时候,需要携带自定义登录态 `Token` ,开发者后台收到请求后,对 `Token` 进行验证识别用户身份,同时拿自定义登录态 `Token` 查询 `openid` 和 `session_key`,从而获取用户请求的数据,进行返回。

3.3 用户登录-实现小程序登录功

实现步骤:

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

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

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

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

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

落地代码:
/api/user.js`

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.4用户登录-token 存储到 Store

思路分析:
`Mobx`允许开发人员在应用程序中统一管理所有组件之间的公共数据。通过使用 `Mobx`,开发人员可以轻松地将 token 存储到全局状态中,并实现在整个应用程序中的共享。并且,存储到`Mobx`中的数据是响应式的,数据发生了变化,使用的地方也会发生变化
首先我们先安装`Mobx`,然后进行实例化,在实例化的时候,创建共享的数据 `Token`,以及对 `Token` 修改的方法
然后使用 `Component` 构造页面,并导入`ComponentWithStore` 方法,并配置 `storeBindings` 方法让页面和 `Store` 对象关联
实现步骤:
1. 安装`Mobx`两个包,在安装好包以后,对包进行构建,点击 `构建 npm`
2. 在项目的根目录下创建 `store` 文件夹,然后在该文件夹下新建 `userstore.js` 
3. 导入核心的`observable ` 、`action` 方法,创建`Store`,同时声明数据和方法
4. 在登录页面,导入`ComponentWithStore` 方法,并配置 `storeBindings` 方法让页面和 `Store` 对象关联
落地代码:
安装依赖,安装完成后构建 npm 
npm i mobx-miniprogram mobx-miniprogram-bindings
/store/index.js`

```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 { reqLogin } from '../../api/user'
 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: '授权失败,请重新授权' })
          }
        }
      })
    }
  }
})

 

3.5用户信息-用户信息存储到 Store

思路分析:

需要调用接口获取用户的信息,在获取到数据以后,需要存储用户信息数据到本地,

用户信息可能会在多个地方使用到,为了方便对用户信息的获取和使用,依然将用户信息存储到`store`

首先在 `store/index.js` 中新增`userInfo`可观测字段,同时创建赋值和删除的`action`方法

获取用户信息的接口需要使用 `token`,所以需要在登录成功以后,调用获取用户信息的接口

登录成功以后,将用户信息存储到本地,然后调用`action`方法,将用户信息存储到 `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

// 导入封装通用模块方法
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)
    }
  }
})

 

3.6用户信息-使用数据渲染用户信息

思路分析:
在获取到数据以后,已经将用户信息数据存储到本地和`Store`,从 `Store` 中取出用户信息数据,并渲染到页面上

个人中心页面展示用于展示个人信息
如果用户没有登录的时候,展示没有登录的头像、提示用户登录的文案信息,不展示设置按钮
如果用户已经登录,展示用户的头像和昵称,并且展示设置按钮,方便用户对收货地址、头像、昵称进行更改

实现步骤:
1. 在个人中心页面导入`ComponentWithStore` 方法构建页面
2. 配置 `storeBindings` 让组件和 `Store` 建立关联
3. 渲染页面
落地代码:
/pages/info/info.js`

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

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

})

3.7分包处理-配置分包以及预下载

思路分析:

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

因此需要将 `更新个人资料` 和 `收货地址` 功能配置成一个分包,

当用户在访问设置页面时,还预先加载 `更新个人资料` 和 `收货地址` 所在的分包
实现步骤:
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"]
  }
}

 4. 收货地址

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

思路分析:

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

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

我们在后续做进行编辑的时候传递 `id` 属性,值为 收货地址的 `id` 值。
实现步骤:
1. 在新增收货地址页面 `data` 中声明所需要的字段
2. 定义收货地址所需要的全部接口 `API` 函数
落地代码:
 modules/settingModule/pages/address/add/index`

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

 

4.2 收集省市区数据

思路分析:
省市区的结构使用了小程序本身自带的`picker` 件,并将组件的 `mode` 属性设置为了 `region`,从而变成省市区选择器

如果想获取省市区的数据,需要给 `picker` 选择组件添加`change` 事件来监听属性值的改变,获取选中的省市区
<!-- 省市县 -->
<view class="item">
  <text class="label">省/市/县 (区)</text>

  <!-- mode:给组件添加 mode 属性设置为了 region,从而变成省市区选择器 -->
  <!-- value:要求是一个数组,表示选中的省市区,默认选中每一列的第一个值 -->
  <!-- bindchange:来监听属性值的改变,也就是获取选中的省市区 -->
  <picker
    mode="region"
    value="{{ [provinceName, cityName, districtName] }}"
    bindchange="onAddressChange"
  >
    <view wx:if="{{ provinceName }}" class="region">
     {{ provinceName + ' ' + cityName + ' ' + districtName }}
    </view>
    <view wx:else class="placeholder">请填写收货人所在城市</view>
  </picker>

  <view class="location" bindtap="onLocation">
    <van-icon name="location-o" color="#777" />
    <text>定位</text>
  </view>
</view>

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

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

在将数据收集以后,需要组织两个数据:
1. 是否是默认地址,0 不设置为默认地址,1 设置为默认地址
2. 拼接完整的收货地址
实现步骤:
1. 使用简易双向数据绑定来收集新增地址表单数据。
3. 给按钮绑定点击事件,在事件处理函数中收集并整理数据
落地代码:
Page({

  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.4 地理定位功能介绍

地理定位介绍:
小程序地理定位是指通过小程序开发平台提供的 `API`,来获取用户的地理位置信息。用户在使用小程序时,可以授权小程序获取自己的地理位置信息
1. `wx.getLocation()` :获取当前的地理位置
2. `wx.chooseLocation()`:打开地图选择位置

申请开通: 暂时只对部分类目的小程序开放,需要先通过类目审核,然后在小程序管理后台,「开发」-「开发管理」-「接口设置」中自助开通该接口权限。

使用方法:

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

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

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

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

4.5 拒绝授权后的解决方案

async onLocation() {

  const { authSetting } = await wx.getSetting()
  console.log(authSetting)

  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 {
      const locationRes = await wx.getLocation()
      // 打印地理位置信息
      console.log(locationRes)
    } catch (err) {
      console.log(err)
    }
  } else {
    try {

      const locationRes = await wx.getLocation()
      console.log(locationRes)
    } catch (error) {
      wx.toast({ title: '您拒绝授权获取地址位置' })
    }
  }
}

4.6 开通腾讯位置服务

1. 申请密钥:[密钥申请](https://lbs.qq.com/dev/console/application/mine),微信扫码进行登录,选择绑定已有账号、或者注册新账号 (需要绑定手机、验证邮箱)

2. 控制台 → 应用管理→[我的应用](https://lbs.qq.com/dev/console/key/manage) → 创建应用 → 添加 key  →  创建完成

   <img src="http://8.131.91.46:6677/mina/floor/创建应用.jpg" style="zoom:60%;" />

   <img src="http://8.131.91.46:6677/mina/floor/添加 key.jpg" style="zoom:76%; border: 1px solid #ccc" />

3. 下载微信小程序 [JavaScriptSDK v1.2](https://mapapi.qq.com/web/miniprogram/JSSDK/qqmap-wx-jssdk1.2.zip),下载将 `.js` 文件放到小程序的 `libs` 目录下

4. 进行安全域名设置,或者点击微信开发者工具中的暂时不校验域名

4.7 LBS 逆地址解析

使用步骤:
1. 在项目中引入 SDK 核心类
2. 在 `onLoad` 中实例化 API 核心类,同时配置创建的 key
3. 使用实例方法 `reverseGeocoder` 方法进行逆地址解析,将提供的坐标转换为详细的地址位置信息
落地代码:
1. 引入 SDK 核心类
2. 实例化 API 核心类
3. 使用 `reverseGeocoder` 方法进行逆地址解析,将提供的坐标转换为所在位置的文字描述的转换

4.8 async-validator 基本使用

知识点:

`async-validator`是一个基于 `JavaScript` 的表单验证库,支持异步验证规则和自定义验证规则

主流的 `UI` 组件库 `Ant-design` 和 `Element`中的表单验证都是基于 `async-validator`

使用 `async-validator` 可以方便地构建表单验证逻辑,使得错误提示信息更加友好和灵活。


使用步骤:

1. 安装并在项目中导入 `async-validator`
2. 创建验证规则
3. 创建表单验证实例,将验证规则传递给构造函数,产生实例
4. 调用实例方法 `validate` 对数据进行验证
   - 第一个参数:需要验证的数据
   - 第二个参数:回调函数,回调函数有两个参数 errors, fields
     - errors:如果验证成功,返回 null,验证错误,返回数组
     - fields:需要验证的字段,属性值错误数组

5.商品管理

5.1 配置商品管理分包

思路分析:

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

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

当用户在访问设置页面时,还预先加载 `商品列表` 和 `商品详情` 所在的分包
落地代码:
{
  "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"]
     }
  }
}

 

5.2 封装商品模块接口 API

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}`)
}

5.3 商品列表-准备列表请求参数

实现步骤:

1. 在商品列表的 `data` 字段中,根据接口文档,定义商品列表接口需要使用的字段
2. 在商品列表的 `onLoad` 钩子函数中接收请求的参数,并将请求参数进行合并
落地代码:
 /modules/goodsModule/pages/list/list.js`

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

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

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

 

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

思路分析:
在准备商品列表的请求参数以后,

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

实现步骤:
1. 在 `/pages/goods/list/list.js` 中导入封装好的获取商品列表的 `API` 函数
2. 页面数据在页面加载的时候进行调用,在 `onLoad` 钩子函数中调用  `reqGoodsList` 方法
3. 在获取到数据以后,使用后端返回的数据对页面进行渲染
 /modules/goodsModules/pages/list/list.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()
  }
})

 

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

思路分析:

当用户从下向上滑动屏幕时,需要加载更多的商品数据。
首先需要在 `.js` 文件中声明 `onReachBottom` 方法监听用户是否进行了上拉

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

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

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

实现步骤:
1. `list.js` 文件中声明 `onReachBottom` 事件处理函数,监听用户的上拉行为
2. 在 `onReachBottom` 函数中加 `page` 进行加 1 的操作,同时发送请求获取下一页数据
3. 在 `getGoodsList` 函数中,实现参数的合并
/modules/goodsModule/pages/list/list.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()
   }

})

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

实现步骤:
1. 在数据返回以后,将数据中的 `total` 赋值给 `data` 中的变量 `total`
2. 在 `onReachBottom` 中进行 `total` 和 `goodsList` 进行对比
3. 模板中使用 `total` 和 `goodsList` 进行对比
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()
  }
})

5.7 商品列表-节流阀进行列表节流

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

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

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

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

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

在 `onReachBottom` 事件监听函数中,对 `isLoading` 进行判断,如果数据正在请求中,不请求下一页的数据
/modules/goodsModule/pages/list/list.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...
  }
})

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

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

小程序中实现上拉加载更多的方式:

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

2. 在`页面.js` 中定义 `onPullDownRefresh` 事件监听用户下拉刷新
/modules/goodsModule/pages/list/list.json`
{
  "usingComponents": {
    "goods-card": "/components/goods-card/goods-card"
  },

  "navigationBarTitleText": "商品列表",
  "enablePullDownRefresh": true,
  "backgroundColor": "#f7f4f8",
  "backgroundTextStyle": "dark"
}
/modules/goodsModule/pages/list/list.js`

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

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

6. 购物车

6.1 购物车-封装购物车接口 API

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}`)
}

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

业务介绍:
点击加入购物车和立即购买的时候,展示购物弹框,在弹框中需要用户选择购买数量和祝福语
点击加入购物车和立即购买,触发的是同一个弹框。
因此点击弹框中的确定按钮时,我们需要区分当前是加入购物车操作还是立即购买操作。
这时候定义一个状态 `buyNow`  做区分,`buyNow`  等于 1 代表是立即购买,否则是加入购物车


产品需求:
1. 如果点击的是加入购物车,需要将当前商品加入到购物车
2. 如果点击的是立即购买,需要跳转到结算支付页面,立即购买该商品
3. 如果是立即购买,不支持购买多个商品

6.3 加入购物车-关联 Store 对象

思路分析:
当用户点击加入购物车 或者 立即购买时,需要判断用户是否进行了登录。
我们需要使用 `Token` 进行判断,因此需要让页面和 `Store` 对象建立关联。
这时候可以使用 `BehaviorWithStore` 让页面 和 `Store` 对象建立关联。
/behaviors/userBehavior.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`
import { reqGoodsInfo } from '@/api/goods'
import { reqAddCart } from '@/api/cart'
+ import { userBehavior } from '@/behaviors/userBehavior'

Page({
   behaviors: [userBehavior],
  
  // 代码略...
})

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

思路分析:
点击加入购物车以及立即购买以后,需要先判断是否进行了登录,如果用户没有登录过,需要先跳转到登录页面进行登录。
如果点击的是 加入购物车,我们只需要调用 [加入购物车](https://apifox.com/apidoc/shared-6ed6c5c4-56c4-4619-8e2a-4817aa140e30/api-136581098) 接口即可 (需要获取商品的 ID 、购买数量、祝福语)
如果点击的是 立即购买,我们需要携带参数跳转到商品结算页面 (获取商品的 ID 以及 祝福语跳转到结算页面)

购买数量的限制有 4 个限制,这 4 个限制直接使用 `Vant` 组件提供的属性进行限制即可:
1. 必须是正整数,最小是`1`,最大是`200`
2. 若输入小于`1`,则重置为`1` 
3. 若输入大于`200`,则重置为`200`
4. 若输入的是其他值,则重置为`1`

实现步骤:
1. 给 `Stepper` 步进器组件,通过`value`设置输入值,同时绑定`change`事件,并将值同步到 `data` 中
2. 根据接口文档,导入封装的购物车的接口 API
3. 点击弹框按钮的时候,判断点击的加入购物车还是立即购买,执行不同的操作
/modules/goodsModule/pages/detail/detail.html`

<van-stepper
  value="{{ count }}"
+  integer
+  min="1"
+  max="200"
  bind:change="onChangeGoodsCount"
/>
/modules/goodsModule/pages/detail/detail.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}`
    })
  }
}

6.5 加入购物车-展示购物车购买数量

思路分析:
判断用户是否进行了登录。
如果没有登录过,则不展示购物车商品的数量。
如果用户登录过,则需要展示购物车商品的数量,则获取购物车列表数据,通过累加计算得出商品购买数量

实现步骤:
1. 进入商品详情,调用方法,在方法中判断`token`是否存在
2. 如何存在,则获取购物车列表数据,通过累加计算得出商品购买数量,展示购买的数量
3. 不存在,不执行任何逻辑,
/modules/goodsModule/pages/detail/detail.js`
Page({
    
  data: {
     allCount: '' 
  },
    
  async handleSubmit() {
    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() {
     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) {
    this.goodsId = options.goodsId
    this.getGoodsInfo()
     this.getCartCount()
  }
    
  // coding...
})

6.6 购物车-购物车关联 Store 对象

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

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

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

这时候可以使用 `ComponentWithStore` 让页面 和 `Store` 对象建立关联。
/pages/cart/components/cart.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.结算支付

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

思路分析:

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

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

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

app.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"]
   }
}

7.2 封装结算支付的接口 API

 /api/orderpay.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}`)
}

7.3 商品结算-获取收货地址

思路分析:
进入结算支付页面后,需要获取收货地址信息,在获取到收货地址以后,需要进行判断,
如果没有获取到收货地址,需要展示添加收货地址的结构,
如果获取到了收货地址,需要渲染收货地址。

实现步骤:
1. 在进入结算页面的时候,调用接口 `API` 函数,获取数据
2. 然后根据数据并渲染结构
/pages/order/detail/index.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`
<!--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>

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

思路分析:
当用户需要更改收货地址时,我们需要跳转到收货地址页面,重新选择收货地址
当用户点击了某个地址以后,我们需要将该地址显示到商品结算页面中。
更新收货地址功能,采用 `getApp()` 全局共享数据的方式来实现。

实现步骤:
在 `app.js` 中定义全局共享的数据 `globalData.address`
点击箭头,携带参数跳转到收货地址页面,标识是从订单结算页面进入
在选择收货地址成功以后,将数据存储到 `globalData.address`中,然后返回到订单结算页面。
在订单结算页面判断 `globalData.address` 是否存在收货地址数据,如果存在则渲染
app.js`
App({
   // 定义全局共享的数据
   globalData: {
    address: {}
  }
})
/pages/address/list/index.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>

 

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

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

实现步骤:
导入封装的接口 `API` 函数
在进入结算页面的时候,调用接口 `API` 函数,获取数据,然后根据数据并渲染结构即可
 /pages/order/detail/index.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`
<!--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>

 

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

思路分析:
当用户从商品详情点击立即购买进入商品结算页面的时候,我们需要在商品结算页面展示立即购买商品的基本信息。
在跳转到商品结算页面的时候,我们已经携带了商品的 `id` 和 `祝福语`。
在结算页面,只需要获取到传递的参数,然后根据传递的参数调用接口即可。

实现步骤:
在页面打开的时候,`onShow` 中接受传递的参数,并赋值给 `data` 中的状态
在 `getOrderInfo` 函数中,判断立即购买商品的 `id` 是否存在,如果存在调用立即购买的接口
获取数据后,然后根据数据并渲染结构即可
/pages/order/detail/index.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()
  }
})

8.订单列表

8.1 封装订单列表接口 `API`

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

8.2 获取订单列表数据并渲染

思路分析:
当用户从个人中心页面点击进入订单中心的时候,就需要获取到订单中心的数据。
在页面调用 `API` 函数获取订单列表的数据,
在获取到数据以后,使用后端返回的数据对页面进行渲染
 modules/orderPayModule/pages/order/list/list.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`
<!--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>

8.3 订单列表上拉加载更多

思路分析:
当用户进行了上拉操作时,需要在 `.js` 文件中声明 `onReachBottom` 方法,用来监听页面的上拉触底行为
当用户上拉时,需要对 `page` 参数进行加 1 即可,
当参数发生改变后,需要重新发送请求,拿最新的 `page` 向服务器要数据 
在下一页的商品数据返回以后,需要将下一页的数据和之前的数据进行合并
modules/orderPayModule/pages/order/list/list.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()
 })

8.4 判断数据是否加载完毕

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

8.5  节流阀

节流阀进行列表节流在用户网速很慢的情况下,如果用户在距离底部来回的进行多次滑动,可能会发送一些无意义的请求、造成请求浪费的情况,因此需要给上拉加载添加节流功能。
使用节流阀来给订单列表添加节流功能。在 `data` 中定义节流阀状态 `isLoading`,默认值是 `false`。在请求发送之前,将 `isLoading` 设置为 `true`,表示请求正在发送。在请求结束以后,将 `isLoading` 设置为 `false`,表示请求已经完成。在 `onReachBottom` 事件监听函数中,对 `isLoading` 进行判断,如果数据正在请求中,不请求下一页的数据。
modules/orderPayModule/pages/order/list/list.js`
/ 导入封装的接口 API 函数
mport { reqOrderList } from '@/api/orderpay'
age({
 // 页面的初始数据
 data: {
   orderList: [1, 2, 3], // 订单列表
   page: 1, // 页码
   limit: 10, // 每页展示的条数
   total: 0, // 订单列表总条数
     isLoading: false // 判断数据是否记载完毕
 },
 // 获取订单列表
 async getOrderList() {
   // 解构获取数据
   const { page, limit } = this.data
     // 数据正在请求中
     this.data.isLoading = true
   // 调用接口获取订单列表数据
   const res = await reqOrderList(page, limit)
     // 数据加载完毕
     this.data.isLoading = false

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

  // 页面上拉触底事件的处理函数
  onReachBottom() {
     // 解构数据
     const { page, total, orderList, isLoading } = this.data
     // 判断是否加载完毕,如果 isLoading 等于 true
     // 说明数据还没有加载完毕,不加载下一页数据
     if (isLoading) return
    // 数据总条数 和 订单列表长度进行对比
    if (total === orderList.length) {
      return wx.toast({ title: '数据加载完毕' })
    }

    // 更新 page
    this.setData({
      page: page + 1
    })

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

  // 生命周期函数--监听页面加载
  onLoad() {
    this.getOrderList()
  }
})

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值