项目过程笔记

一、 框架扩展

 mobx-miniprogram

1. mobx-miniprogram介绍

mobx-miniprogram是针对微信小程序开发的一个简单、高效、轻量级状态管理库,它基于MobX状态管理框架实现。以下是关于mobx-miniprogram的详细介绍:

  1. 目的与功能
    • 解决复杂项目中组件间数据同步的问题。
    • 提供状态管理功能,使得开发者可以方便地在小程序中全局共享状态,并自动更新视图组件。
    • 使得当状态改变时,所有关联组件都会自动更新相对应的数据,从而提升小程序的开发效率。
  2. 优势
    • 简化复杂项目中组件间数据同步的问题,提高开发效率。
    • 使得状态管理更加简单、高效和轻量级。
    • 当状态改变时,能够自动更新关联组件,保持数据的实时性和一致性。
  3. 适用场景
    • 适用于业务逻辑复杂、需要在多个页面间进行状态同步的微信小程序项目。
    • 尤其适用于需要实时更新数据、保持数据一致性的场景。
  4. 与其他全局数据共享方案的比较
    • 相比于其他全局数据共享方案(如Vuex、Redux等),mobx-miniprogram更加简单、轻量级,并且与微信小程序的开发环境更加兼容。

目前已经学习了 6 种小程序页面、组件间的数据通信方案,分别是:

  1. 数据绑定:properties
  2. 获取组件实例:this.selectComponent()
  3. 事件绑定:this.triggerEvent()
  4. 获取应用实例:getApp()
  5. 页面间通信:EventChannel
  6. 事件总线:pubsub-js
2. 创建 Store 对象

如果需要创建 Store 对象需要使用 mobx-miniprogram ,因此需要先熟悉 mobx-miniprogram 三个核心概念:

  1. observable:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。
  2. action:用于修改状态(state)的方法,需要使用 action 函数显式的声明创建。
  3. computed:根据已有状态(state)生成的新值。计算属性是一个方法,在方法前面必须加上 get 修饰符

下面是一个简单的例子,展示了如何在 mobx-miniprogram 中创建一个 Store 对象:

首先,你需要安装 mobx-miniprogram 和 mobx-miniprogram-bindings。你可以通过 npm 或 yarn 来安装它们:

npm install mobx-miniprogram mobx-miniprogram-bindings --save  
# 或者  
yarn add mobx-miniprogram mobx-miniprogram-bindings

创建一个 Store 类,使用 mobx-miniprogram 的装饰器(如果你使用 TypeScript)或手动方式(如果你使用 JavaScript)来定义 observable 状态。

TypeScript 示例(使用装饰器):

import { observable, action } from 'mobx-miniprogram';  
  
class Store {  
  @observable public count = 0;  
  
  @action  
  public increment() {  
    this.count++;  
  }  
  
  @action  
  public decrement() {  
    this.count--;  
  }  
}  
  
export default new Store();

JavaScript 示例(不使用装饰器):

import { observable, action, makeAutoObservable } from 'mobx-miniprogram';  
  
class Store {  
  constructor() {  
    makeAutoObservable(this); // 自动将类的属性和方法转换为 observable 和 action  
    this.count = 0;  
  }  
  
  increment() {  
    this.count++;  
  }  
  
  decrement() {  
    this.count--;  
  }  
}  
  
export default new Store();

在你的小程序页面或组件中,使用 mobx-miniprogram-bindings 将 Store 中的状态或方法绑定到视图层。

TypeScript 或 JavaScript 示例(在小程序页面或组件中使用):

// 引入 mobx-miniprogram-bindings  
import { createStoreBindings } from 'mobx-miniprogram-bindings';  
import store from './store'; // 引入上面创建的 Store 实例  
  
Page({  
  // ...  
  onLoad: function () {  
    this.storeBindings = createStoreBindings(this, {  
      store,  
      fields: ['count'], // 需要绑定的字段  
      actions: ['increment', 'decrement'], // 需要绑定的方法  
    });  
  },  
  onUnload: function () {  
    this.storeBindings.destroyStoreBindings(); // 页面卸载时销毁绑定  
  },  
  // ...  
  methods: {  
    // 可以在这里调用通过 bindings 绑定的方法  
    handleIncrement() {  
      this.increment();  
    },  
    handleDecrement() {  
      this.decrement();  
    },  
  },  
  // ...  
});

这样,你就成功地在 mobx-miniprogram 中创建了一个 Store 对象,并在小程序页面或组件中使用了它。现在你可以通过 this.count 来访问状态,并通过 this.increment()this.decrement() 来修改状态了。

3. 在组件中使用数据

如果需要 Page 或者Component中对共享的数据进行读取、更新操作,需要使用 mobx-miniprogram-bindings

mobx-miniprogram-bindings 的作用就是将 Store 和 页面或组件进行绑定关联

如果需要在组件中使用状态,需要 mobx-miniprogram-bindings 库中导入 ComponentWithStore 方法

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

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

  1. store: 指定要绑定的 Store 对象
  2. fields: 指定需要绑定的 data 字段
  3. actions: 指定需要映射的 actions 方法

在使用 mobx-miniprogram-bindings 时,我们需要将小程序页面的 Page 构造函数或组件的 Component 构造函数替换为 ComponentWithStore,以将 MobX 的 Store 绑定到页面或组件中。

以下是一个使用 ComponentWithStore 的小程序组件示例:

// 引入 mobx-miniprogram-bindings 和你的 Store  
import { ComponentWithStore } from 'mobx-miniprogram-bindings';  
import store from './store';  
  
ComponentWithStore({  
  storeBindings: {  
    store,  
    fields: ['count'], // 需要绑定的字段  
    actions: ['increment', 'decrement'], // 需要映射的 actions 方法  
  },  
  
  // 组件的其他配置项  
  properties: {},  
  
  data: {  
    // 这里不需要再手动声明与 fields 中相同的字段,因为它们会自动从 Store 中同步  
  },  
  
  lifetimes: {  
    attached() {  
      // 组件加载时可能执行的逻辑  
    },  
    detached() {  
      // 组件卸载时销毁绑定  
      this.storeBindings.destroyStoreBindings();  
    },  
  },  
  
  methods: {  
    // 可以在这里调用通过 bindings 绑定的方法  
    handleIncrement() {  
      this.increment();  
    },  
    handleDecrement() {  
      this.decrement();  
    },  
  
    // 其他自定义方法  
  },  
  
  // ... 其他配置项  
});

在上面的示例中,fields 数组指定了从 Store 中需要同步到组件 data 的字段。actions 数组则指定了需要在组件中映射的 Store 方法。

在组件的模板中,你可以直接使用 {{count}} 来显示 count 字段的值,因为 mobx-miniprogram-bindings 会自动为你处理数据同步。同样,你可以直接调用 this.increment()this.decrement() 来触发 Store 中的动作。

4. 在页面中使用数据

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. 新建 behavior 文件,从 mobx-miniprogram-bindings 库中导入 BehaviorWithStore 方法
  2. 在 BehaviorWithStore 方法中配置 storeBindings 配置项从 Store 中映射数据和方法
  3. 在 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'],
  }
})
// index.js

import { indexBehavior } from './behavior'

Page({
  behaviors: [indexBehavior]
    
  // 其他配置项
})
6. fields、actions 对象写法

fieldsactions 有两种写法:数组 或者 对象。

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

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

    • 例如 { a: 'numA', b: 'numB' }
  2. 函数形式:指定 data 中每个字段的计算方法

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

如果 actions 写成对象方式,只有两种写法:

  1. 映射形式:指定模板中调用的哪些方法来源于 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'
    }
  }
})
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>

miniprogram-computed

该工具库提供了两个功能:

  1. 计算属性 computed
  2. 监听器 watch
1. 计算属性 computed

知识点:

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

在使用时:需要将 Component 方法替换成 ComponentWithComputed 方法 ,原本组件配置项也需要写到该方法中在替换以后,就可以新增 computed 以及 watch 配置项。安装 miniprogram-computed, 在安装以后,需要点击 构建 npm,进行

npm install miniprogram-computed

计算属性 computed 的使用:

// component.js

// 引入 miniprogram-computed
import { ComponentWithComputed } from 'miniprogram-computed'

ComponentWithComputed({
  data: {
    a: 1,
    b: 1
  },
  
  computed: {
    total(data) {
      // 注意: 
      // computed 函数中不能访问 this ,只有 data 对象可供访问
      // 这个函数的返回值会被设置到 this.data.sum 字段中
        
      // 计算属性具有缓存,计算属性使用多次,但是计算属性方法只会执行一次
      console.log('~~~~~')
        
      return data.a + data.b
    }
  }
})
2. 监听器 watch

在使用时:需要将 Component 方法替换成 ComponentWithComputed 方法 ,原本组件配置项也需要写到该方法中在替换以后,就可以新增 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
      })
    }
  }
})

拓展:Mobx 与 Computed 结合使用

我们演示使用兼容写法:

  1. 如果使用了 ComponentWithStore 方法构建组件,计算属性写法使用旧版 API

import { ComponentWithComputed } from 'miniprogram-computed'

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

ComponentWithStore({
  behaviors: [computedBehavior],

  data: {
    a: 1,
    b: 1,
    sum: 2
  },
    
  watch: {
    'a, b': function (a, b) {
      this.setData({
        total: a + b
      })
    }
  },
    
  computed: {
    total(data) {
      // 注意: computed 函数中不能访问 this ,只有 data 对象可供访问
      // 这个函数的返回值会被设置到 this.data.sum 字段中
      return data.a + data.b + data.sum // data.c 为自定义 behavior 数据段
    }
  },
    
  // 实现组件和 Store 的关联
  storeBindings: {
    store: numStore,

    // fields 和 actions 有两种写法:数组写法 和 对象写法

    // 数组写法
    fields: ['numA', 'numB', 'sum'],
    actions: ['update']
  }
})

  2.使用了 ComponentWithComputed 方法构建组件,Mobx写法使用旧版 API

import { ComponentWithComputed } from 'miniprogram-computed'

// 导入 storeBindingsBehavior 方法实现组件和 Store 的关联
import { storeBindingsBehavior } from "mobx-miniprogram-bindings"
// 导入 Store 
import { numStore } from '../../stores/numstore'


ComponentWithComputed({
  behaviors: [storeBindingsBehavior],

  data: {
    a: 1,
    b: 1,
    sum: 2
  },
  watch: {
    'a, b': function (a, b) {
      this.setData({
        total: a + b
      })
    }
  },
  computed: {
    total(data) {
      // 注意: computed 函数中不能访问 this ,只有 data 对象可供访问
      // 这个函数的返回值会被设置到 this.data.sum 字段中
      return data.a + data.b + data.sum // data.c 为自定义 behavior 数据段
    }
  },
    
  // 实现组件和 Store 的关联
  storeBindings: {
    store: numStore,

    // fields 和 actions 有两种写法:数组写法 和 对象写法

    // 数组写法
    fields: ['numA', 'numB', 'sum'],
    actions: ['update']
  }
})

二、 用户管理

1. 用户登录-实现小程序登录功能

根据接口文档封装接口 API 函数,当点击授权登录按钮的时候调用 API 函数,在获取到 token 以后,将 token 存储到本地,然后跳转到登录之前的页面。

实现步骤:

  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}`)
}
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: '授权失败,请稍后再试~~~' })
        }
      }
    })
  }
})

2. 用户登录-token 存储到 Store

首先我们先安装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

// 导入 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. 用户信息-用户信息存储到 Store

获取用户信息的接口需要使用 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

落地代码:

/**
 * @description 获取用户信息
 * @returns Promise
 */
export const reqUserInfo = () => {
  return http.get(`/mall-api/weixin/getuserInfo`)
}
// 导入 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)
    }
  }
})

4. 用户信息-使用数据渲染用户信息

实现步骤:

  1. 在个人中心页面导入ComponentWithStore 方法构建页面
  2. 配置 storeBindings 让组件和 Store 建立关联
  3. 渲染页面

落地代码:

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

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

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

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

让页面和 Store 数据建立关联,可以使用 mobx-miniprogram-bindings 提供的 BehaviorWithStore 方法

实现步骤:

  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']
  }
})
import { userBehavior } from './behavior'

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

  // 其他代码略...
})
<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>

6. 更新用户信息-获取头像临时路径

  1. 将 button 组件 open-type 的值设置为 chooseAvatar
  2. 当用户选择需要使用的头像之后,可以通过 bindchooseavatar 事件回调获取到头像信息的临时路径

实现步骤:

  1. 给 button 按钮绑定 open-type 属性,值为 chooseAvatar
  2. 用户点击了头像后,在 bindchooseavatar 事件回调获取到头像信息的临时路径

落地代码:

<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>
// 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
+     })
+   },

  // 略....
})

7. 更新用户信息-头像上传到服务器

思路分析:

通过 bindchooseavatar 事件回调获取到头像信息的临时路径。

当临时文件超过一定的容量的时候,小程序就会将临时文件清理掉,也就是说临时文件可能会随时失效,为了解决这个问题,我们需要将获取到头像信息的临时路径上传到自己的服务器。如果需要将本地资源上传到服务器,需要使用到小程序提供的 API 方法: wx.uploadFile ,语法如下:

wx.uploadFile({
  url: '开发者服务器地址',
  filePath: '要上传文件资源的路径 (本地路径)',
  name: '文件对应的 key',
  header: 'HTTP 请求 Header',
  // 接口调用成功的回调函数
  success: (res) => {},
  // 接口调用失败的回调函数
  fail: (err) => {}
})

实现步骤:

  1. 在获取到用户的临时头像路径以后,调用 wx.uploadFile() 方法,同时设置好基本的参数,
  2. 在上传成功后,获取到服务器返回的永久地址
  3. 将地址赋值给 data 中的数据

落地代码:

// 获取用户头像信息
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'
      })
    }
  })

}

8. 更新用户信息-更新用户昵称

  1. 使用微信昵称
  2. 用户输入最新的昵称

当用户提交表单时,我们将最新的昵称,同步到 userInfo 的 nickname字段中

当用户点击了确定以后,我们将新的的用户信息赋值给 data 中的 userInfo 字段

当用户点击保存时,更新用户信息

实现步骤:

  1. 给 form 表单绑定 bindsubmit 事件,用来获取输入框最新的值
  2. 给 input 组件绑定 type 属性,属性值为 nickname,获取微信昵称
  3. 给 input 组件绑定 bindinput 事件,获取用户输入最新的昵称
  4. 将 formType 设置为 submit 当用户点击确定后,触发 form 表单的 bindsubmit 事件
  5. 在 form 表单的 bindsubmit 事件中进行赋值
  6. 给 form 表单的取消按钮绑定事件,取消弹框

落地代码:

<van-dialog
  custom-style="position: relative"
  use-slot
  title="修改昵称"
  show="{{ isShowPopup }}"
  showConfirmButton="{{ false }}"
  showCancelButton="{{ false }}"
  transition="fade"
>
+   <form bindsubmit="getNewName">
    <!-- type 设置为 nickname 是为了获取微信昵称 -->
    <input
      class="input-name"
+       type="nickname"
+       bindinput="getNewName"
      name="nickname"
      value="{{ userInfo.nickname }}"
    />
    <view class="dialog-content">
+       <button class="cancel" bindtap="cancelForm">取消</button>
+       <!-- 将 formType 设置为 submit 当用户点击确定后,触发 form 表单的 bindsubmit 事件 -->
+       <button class="confirm" type="primary" formType="submit">确定</button>
    </view>
  </form>
</van-dialog>
import { reqUpdateUserInfo, reqUserInfo } from '../../../../api/user'
import { createStoreBindings } from 'mobx-miniprogram-bindings'
import store from '../../../../stores/index'

Page({
  // 页面的初始数据
  data: {
    avatarUrl: '/static/images/avatar.png',
    isShowPopup: false,
    userInfo: {
      nickname: '',
      headimgurl: ''
    }
  },

  // 生命周期函数--监听页面加载
  onLoad(options) {
    createStoreBindings(this, {
      store,
      fields: ['userInfo'],
      actions: ['setUserInfo']
    })
  },

  getAvatar(e) {
    // coding...
  },

  // 更新用户信息
  async updateUserInfo() {
    // coding...
  },

  // 显示修改昵称弹框
  onUpdateNickName() {
    this.setData({
      isShowPopup: true
    })
  },

  // 获取最新的用户昵称
  getNewName(e) {
    // 获取用户输入的最新的昵称
    const { nickname } = e.detail.value

    this.setData({
      'userInfo.nickname': nickname,
      isShowPopup: false
    })
  },

  // 取消更新用户昵称
  cancelForm() {
    this.setData({
      isShowPopup: false
    })
  }
})

三、 地址管理

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

实现步骤:

  1. 在新增收货地址页面 data 中声明所需要的字段
  2. 定义收货地址所需要的全部接口 API 函数

落地代码:

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

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. 收集省市区数据

思路分析

省市区的结构使用了小程序本身自带的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>

实现步骤

  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
    })
  }
    
  // coding...
})

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.  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('')
      })
    }
  })
}

5. async-validator 基本使用

使用步骤:

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

落地代码:

  1. 安装 async-validator

npm i async-validator

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

  2. 在 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('验证通过')
        })
      }
    })

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

思路分析:

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

  1. 收货人不能为空,且不能输入特殊字符
  2. 手机号不能为空,且输入的手机号必须合法
  3. 省市区不能为空
  4. 详细地址不能为空

正则:

// 验证收货人,是否只包含大小写字母、数字和中文字符
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}$'

实现步骤:

  1. 创建 validateForm 方法,使用 async-validator 对表单进行验证
  2. 在新增收货地址之前,调用 validateForm 方法,如果验证成功执行新增守护地址的逻辑

落地代码:

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


  // coding...
})

7. 实现新增收货地址

思路分析:

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

实现步骤:

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

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

落地代码:

// 新增或修改地址
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: '新增收货地址成功' })
      }
    })
  }
    
}

8. 实现更新收货地址

实现步骤:

  1. 在从收货地址列表页面跳转到更新页面的时候,需要携带 id
  2. 在 onLoad 中判断是否存在 id,如果存在 id,在获取数据进行回显

落地代码:

<!-- 编辑、删除按钮 -->
<van-icon bindtap="toEdit" data-id="{{ item.id }}" name="edit" size="22px" color="#999" />
// 去编辑页面
toEdit(event) {
  // 需要编辑的收货地址
  const { id } = event.target.dataset

  wx.navigateTo({
    url: `/modules/settingModule/pages/address/add/index?id=${id}`
  })
}
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...
})

9. 实现删除收货地址

实现步骤:

  1. 给删除按钮绑定点击事件 delAddress,同时通过 data-id 传递需要删除的商品 id
  2. 在 delAddress 事件处理程序后面,调用 API 函数 reqDelAddress,并传递 id
  3. 在删除收货地址成功以后,给用户提示

落地代码

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

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

  await reqDelAddress(id)
  this.getAddressList()
}

优化:SwipeCell 自动收起删除滑块

目前我们已经实现了滑块删除收货地址的功能,

但是我们会发现点击页面空白区域或者点击其他收货地址时,删除的滑块不会自动收起。

如果想实现点击空白区域自动收起滑块功能,需要在 点击空白区域 以及 其他收货地址时,获取要收起的滑块实例。

调用对应滑块的实例方法 close 即可。

实现思路:

  1. 给滑块绑定 id
  2. 在打开滑块时,获取当前滑块的实例,然后将实例存储到 data 的数组中。
  3. 给页面最外层的 view 同时给滑块区域绑定点击事件,在事件处理函数中对数据遍历,每一项调用 close 方法关掉滑块
  4. 将关掉的逻辑抽取成 behavior 文件,方便在其他文件中进行复用。

落地代码:

export const swipeCellBehavior = Behavior({
  data: {
    swipeCelQueue: [] // 实例存储队列
  },

  methods: {
    // 打开滑块时,将实例存储到队列中
    onSwipeCellOpen(event) {
      let instance = this.selectComponent(`#${event.target.id}`)
      this.data.swipeCelQueue.push(instance)
    },

    // 点击其他滑块时,关掉开启的滑块
    onSwipeCellClick() {
      this.onSwipeCellCommonClick()
    },

    // 点击页面空白区域时,关掉开启的滑块
    onSwipeCellPageTap() {
      this.onSwipeCellCommonClick()
    },

    // 关掉滑块的统一方法
    onSwipeCellCommonClick() {
      // 循环关闭开启的滑块
      this.data.swipeCelQueue.forEach(function (instance) {
        instance.close()
      })
        
      // 将滑块进行清空
      this.data.swipeCelQueue = []
    }
  }
})
<view class="container address-list" bindtap="onSwipeCellPageTap">

  <van-swipe-cell
    right-width="{{ 65 }}"
+     data-id="{{ item.id }}"
+     bind:open="onSwipeCellOpen"
+     bind:click="onSwipeCellClick"
  >
  
    <!-- 代码略... -->
  
  </van-swipe-cell>

</view>

四、 商品管理

1. 配置商品管理分包

实现步骤:

  1. 在 modules 目录下创建 goodModule 文件夹,用来存放商品管理分包
  2. 在 app.json 的 subpackages 进行商品管理分包配置
  3. 在 app.json 的 preloadRule 进行商品管理分包配置

落地代码:

{
  "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 函数

落地代码:

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

落地代码:

+ 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()
  }
})
<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>

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

实现步骤:

  1. list.js 文件中声明 onReachBottom 事件处理函数,监听用户的上拉行为
  2. 在 onReachBottom 函数中加 page 进行加 1 的操作,同时发送请求获取下一页数据
  3. 在 getGoodsList 函数中,实现参数的合并

落地代码:

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. 商品列表-节流阀进行列表节流

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

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

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

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

落地代码:

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

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

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

  "navigationBarTitleText": "商品列表",
  "enablePullDownRefresh": true,
  "backgroundColor": "#f7f4f8",
  "backgroundTextStyle": "dark"
}
// 监听页面的下拉刷新
onPullDownRefresh() {
  // 将数据进行重置
  this.setData({
    goodsList: [],
    total: 0,
    isFinish: false,
    requestData: { ...this.data.requestData, page: 1 }
  })

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

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

实现步骤:

  1. 给展示大图的 image 组件绑定点击事件,同时通过自定义属性的方式,传递当前需要显示的图片http 链接
  2. 同时商品详情的数组数据传递给 urls 数组即可

落地代码:

<!-- 商品大图 -->
<view class="banner-img">
  <image
     class="img"
     src="{{ goodsInfo.imageUrl }}"
     bindtap="previewImg"
  />
</view>
// 预览商品图片
previewImg() {
  // 调用预览图片的 API
  wx.previewImage({
    urls: this.data.goodsInfo.detailList
  })
}

优化:配置 @ 路径别名优化访问路径

在对小程序进行分包时,如果访问小程序根目录下的文件,那么访问的路径就会很长。

在 Vue 中,可以使用 @ 符号指向源码目录,简化路径,小程序也给提供了配置的方式。

在小程序中可以在 app.json 中使用 resolveAlias 配置项用来自定义模块路径的映射规则。

{
  "resolveAlias": {
    "@/*": "/*"
  }
}

五、购物车

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

思路分析:

为了方便后续进行购物车模块的开发,我们在这一节将购物车所有的接口封装成接口 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}`)
}

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

产品需求

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

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

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

结构分析

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

<!-- 商品的底部商品导航 -->
<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 属性,来区分是进行的某种操作

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 对象建立关联。

落地代码:

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

export const userBehavior = BehaviorWithStore({
  storeBindings: {
    store: userStore,
    fields: ['token']
  }
})
import { reqGoodsInfo } from '@/api/goods'
import { reqAddCart } from '@/api/cart'
+ import { userBehavior } from '@/behaviors/userBehavior'

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

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

实现步骤:

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

落地代码:

<van-stepper
  value="{{ count }}"
+  integer
+  min="1"
+  max="200"
  bind:change="onChangeGoodsCount"
/>
// 监听是否更改了购买数量
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. 不存在,不执行任何逻辑,

落地代码

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 对象

落地代码:

+ 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. 导入封装好的获取列表数据的 API 函数
  2. 在 onShow 钩子中,根据产品的需求,处理页面的提示
  3. 在获取到数据以后,使用后端返回的数据对页面进行渲染

落地代码:

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

<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. 在 computed 配置项,新增 totalPrice 函数用来计算商品价格总和

落地代码:

<!-- 底部工具栏 -->
<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>
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...
})

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

实现步骤:

  1. 导入封装的接口 API 函数,同时导入处理删除自动关闭效果的 behaviors 并进行注册
  2. 在点击删除以后,调用 API 函数,在删除购物车商品成功以后,给用户提示

落地代码:

+  <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>
// 导入接口 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. 配置分包并跳转到结算页面

思路分析:

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

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

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

落地代码:

"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"]
+   }
}
// 跳转到订单结算页面
toOrder() {
  if (this.data.totalPrice === 0) {
    wx.toast({
      title: '请选择需要购买的商品'
    })

    return
  }

  // 跳转到订单的结算页面
  wx.navigateTo({
    url: '/modules/orderPayModule/pages/order/detail/detail'
  })
}
<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 函数

落地代码:

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. 然后根据数据并渲染结构

落地代码:

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

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

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

+   // 页面展示时触发的钩子函数
+   onShow() {
+     this.getAddress()
+   }
})
<!--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. 商品结算-获取订单详情数据

实现步骤:

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

落地代码:

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/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>

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

实现步骤:

  1. 在页面打开的时候,onShow 中接受传递的参数,并赋值给 data 中的状态
  2. 在 getOrderInfo 函数中,判断立即购买商品的 id 是否存在,如果存在调用立即购买的接口
  3. 获取数据后,然后根据数据并渲染结构即可

落地代码:

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

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

实现步骤:

  1. 在商品结算页面导入封装好的格式化时间的方法 formatTime
  2. 调用 formatTime ,传入需要格式化的时间戳

落地代码:

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...
}

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

思路分析:

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

  1. 收货地址不能为空
  2. 订购人姓名不能为空,且不能输入特殊字符
  3. 订购人手机号不能为空,且输入的手机号必须合法
  4. 送达日期不能为空

落地代码:

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

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

落地代码:

// 获取预付单信息、支付参数
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' })
  }
}

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

落地代码:

// 获取预付单信息、支付参数
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'
    })
  }
},

七、订单列表

1. 封装订单列表接口 API

思路分析:

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

落地代码:

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

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

落地代码:

// 导入封装的接口 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()
   }

})
<!--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 向服务器要数据

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

落地代码:

// 导入封装的接口 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()
 }
)
``
## 04. 判断数据是否加载完毕
*思路分析:**如何判断数据是否加载完成 ❓可以使用后端返回的 `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()

``
## 05. 节流阀进行列表节流在用户网速很慢的情况下,如果用户在距离底部来回的进行多次滑动,可能会发送一些无意义的请求、造成请求浪费的情况,因此需要给上拉加载添加节流功能。我们使用节流阀来给订单列表添加节流功能。在 `data` 中定义节流阀状态 `isLoading`,默认值是 `false`。在请求发送之前,将 `isLoading` 设置为 `true`,表示请求正在发送。在请求结束以后,将 `isLoading` 设置为 `false`,表示请求已经完成。在 `onReachBottom` 事件监听函数中,对 `isLoading` 进行判断,如果数据正在请求中,不请求下一页的数据。
*落地代码:**
➡️ modules/orderPayModule/pages/order/list/list.js`
``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()
  }
})

八、代码优化

1. 优化-分包调整

思路分析:

  1. 将 [设置页面] 配置到 [设置模块分包],在访问个人中心页面时,提前预下载 [设置模块分包]

  2. 进入订单结算页面时,提前预下载 [设置模块分包]

落地代码:

{
    "subPackages": [
    {
      "root": "modules/settingModule",
      "name": "settingModule",
      "pages": [
+         "pages/settings/settings",
        "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/my/my": {
      "network": "all",
      "packages": ["settingModule"]
    },
+     "modules/orderPayModule/pages/order/detail/detail": {
+       "network": "all",
+       "packages": ["settingModule"]
+     },
    "pages/category/category": {
      "network": "all",
      "packages": ["goodModule"]
    },
    "pages/cart/cart": {
      "network": "all",
      "packages": ["orderPayModule"]
    }
  },
}

2. 分享功能

  1. 配置页面
    • 在小程序的 app.json 或具体页面的 json 配置文件中,确保页面的 navigationStyle 或其他相关属性不会干扰分享功能的展示。
    • 如果页面需要自定义分享内容,可以在页面的 js 文件中使用 onShareAppMessage 生命周期函数。
  2. 编写 onShareAppMessage 函数
    • 在需要分享功能的页面 js 文件中,添加 onShareAppMessage 函数。
    • 这个函数返回一个对象,包含 title(标题)、path(页面路径,用于分享后跳转)、imageUrl(分享图片链接,可选)和 success(分享成功的回调函数,可选)等属性。

示例代码:

Page({  
  // ... 其他页面逻辑 ...  
  
  onShareAppMessage: function () {  
    return {  
      title: '分享给朋友',  
      path: '/pages/shareTarget/shareTarget',  
      imageUrl: '/resources/share_image.png', // 这里需要替换成你的图片链接  
      success: function (res) {  
        // 用户点击分享后执行的回调函数  
        console.log('分享成功', res);  
      }  
    }  
  }  
})

注意

  • imageUrl 字段是可选的,但如果你希望分享时附带一张图片,则需要提供有效的图片链接。
  • 如果不同的页面需要不同的分享内容,每个页面都需要有自己的 onShareAppMessage 函数。
  • 在分享到微信外部(如好友、朋友圈)时,图片和描述可能会受到微信平台策略的限制,需要确保内容符合微信的规定。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值