一、 框架扩展
mobx-miniprogram
1. mobx-miniprogram介绍
mobx-miniprogram是针对微信小程序开发的一个简单、高效、轻量级状态管理库,它基于MobX状态管理框架实现。以下是关于mobx-miniprogram的详细介绍:
- 目的与功能:
- 解决复杂项目中组件间数据同步的问题。
- 提供状态管理功能,使得开发者可以方便地在小程序中全局共享状态,并自动更新视图组件。
- 使得当状态改变时,所有关联组件都会自动更新相对应的数据,从而提升小程序的开发效率。
- 优势:
- 简化复杂项目中组件间数据同步的问题,提高开发效率。
- 使得状态管理更加简单、高效和轻量级。
- 当状态改变时,能够自动更新关联组件,保持数据的实时性和一致性。
- 适用场景:
- 适用于业务逻辑复杂、需要在多个页面间进行状态同步的微信小程序项目。
- 尤其适用于需要实时更新数据、保持数据一致性的场景。
- 与其他全局数据共享方案的比较:
- 相比于其他全局数据共享方案(如Vuex、Redux等),mobx-miniprogram更加简单、轻量级,并且与微信小程序的开发环境更加兼容。
目前已经学习了 6 种小程序页面、组件间的数据通信方案,分别是:
- 数据绑定:
properties
- 获取组件实例:
this.selectComponent()
- 事件绑定:
this.triggerEvent()
- 获取应用实例:
getApp()
- 页面间通信:
EventChannel
- 事件总线:
pubsub-js
2. 创建 Store 对象
如果需要创建 Store 对象需要使用 mobx-miniprogram
,因此需要先熟悉 mobx-miniprogram
三个核心概念:
observable
:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。action
:用于修改状态(state)的方法,需要使用 action 函数显式的声明创建。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
配置项,配置项常用的属性有以下三个:
store
: 指定要绑定的Store
对象fields
: 指定需要绑定的data
字段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
中的数据,使用方式和组件的使用方式是一样的
- 从
mobx-miniprogram-bindings
库中导入ComponentWithStore
方法 - 将
Component
方法替换成ComponentWithStore
方法 - 然后配置
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']
}
})
- 新建
behavior
文件,从mobx-miniprogram-bindings
库中导入BehaviorWithStore
方法 - 在
BehaviorWithStore
方法中配置storeBindings
配置项从Store
中映射数据和方法 - 在
Page
方法中导入创建的behavior
,然后配置behavior
属性,并使用导入的behavior
// behavior.js
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
export const indexBehavior = BehaviorWithStore({
storeBindings: {
store: numStore,
fields: ['numA', 'numB', 'sum'],
actions: ['update'],
}
})
// index.js
import { indexBehavior } from './behavior'
Page({
behaviors: [indexBehavior]
// 其他配置项
})
6. fields、actions 对象写法
fields
、actions
有两种写法:数组 或者 对象。
如果 fields
写成对象方式,有两种写法:
-
映射形式:指定 data 中哪些字段来源于
store
以及它们在store
中对应的名字。- 例如
{ a: 'numA', b: 'numB' }
- 例如
-
函数形式:指定 data 中每个字段的计算方法
- 例如
{ a: () => store.numA, b: () => anotherStore.numB }
- 例如
如果 actions
写成对象方式,只有两种写法:
- 映射形式:指定模板中调用的哪些方法来源于
store
以及它们在store
中对应的名字。- 例如
{ buttonTap: 'update' }
- 例如
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
ComponentWithStore({
data: {
someData: '...'
},
storeBindings: {
store: numStore,
fields: {
// 使用函数方式获取 Store 中的数据
a: () => store.numA,
b: () => store.numB,
// 使用映射形式获取 Store 中的数据,值为数据在 store 中对应的名字
total: 'sub'
},
// 使用映射形式获取 Store 中的 action 名字
actions: {
// key 自定义,为当前组件中调用的方法
// 值为 store 中对应的 action 名字
buttonTap: 'update'
}
}
})
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
该工具库提供了两个功能:
- 计算属性
computed
- 监听器
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 结合使用
我们演示使用兼容写法:
-
如果使用了
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
存储到本地,然后跳转到登录之前的页面。
实现步骤:
- 在
/api/user.js
文件中根据接口文档,创建登录的API
函数login
- 给登录按钮绑定点击事件,对应
login
回调函数 - 在
login
回调函数中调用wx.login()
方法,获取临时登录凭证code - 在
/pages/login/login.js
中导入封装好的API
函数,传入 临时登录凭证code 然后调用 - 在登录成功以后将
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
对象关联
实现步骤:
- 安装
Mobx
两个包,在安装好包以后,对包进行构建,点击构建 npm
- 在项目的根目录下创建
store
文件夹,然后在该文件夹下新建userstore.js
- 导入核心的
observable
、action
方法,创建Store
,同时声明数据和方法 - 在登录页面,导入
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
实现步骤:
-
在
store/userstore.js
中新增userInfo
字段,同时创建修改的action
方法 -
在
login.js
中使用映射userInfo
数据和setUserInfo
方法 -
在
/api/user.js
文件中根据接口文档,创建获取用户信息的API
函数reqUserInfo
-
在
/pages/login/login.js
中导入封装好的获取商品列表的API
函数 -
创建
getUserInfo
方法,在getUserInfo
方法中调用接口API
函数reqUserInfo
-
在登录成功以后,调用
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. 用户信息-使用数据渲染用户信息
实现步骤:
- 在个人中心页面导入
ComponentWithStore
方法构建页面 - 配置
storeBindings
让组件和Store
建立关联 - 渲染页面
落地代码:
+ 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
方法
实现步骤:
- 新建
behavior.js
文件,从mobx-miniprogram-bindings
库中导入BehaviorWithStore
方法 - 在
BehaviorWithStore
方法中配置storeBindings
配置项从Store
中映射数据和方法 - 在
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. 更新用户信息-获取头像临时路径
- 将 button 组件
open-type
的值设置为chooseAvatar
- 当用户选择需要使用的头像之后,可以通过
bindchooseavatar
事件回调获取到头像信息的临时路径
实现步骤:
- 给
button
按钮绑定open-type
属性,值为chooseAvatar
- 用户点击了头像后,在
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) => {}
})
实现步骤:
- 在获取到用户的临时头像路径以后,调用
wx.uploadFile()
方法,同时设置好基本的参数, - 在上传成功后,获取到服务器返回的永久地址
- 将地址赋值给
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. 更新用户信息-更新用户昵称
- 使用微信昵称
- 用户输入最新的昵称
当用户提交表单时,我们将最新的昵称,同步到 userInfo
的 nickname
字段中
当用户点击了确定以后,我们将新的的用户信息赋值给 data
中的 userInfo
字段
当用户点击保存时,更新用户信息
实现步骤:
- 给
form
表单绑定bindsubmit
事件,用来获取输入框最新的值 - 给
input
组件绑定type
属性,属性值为nickname
,获取微信昵称 - 给
input
组件绑定bindinput
事件,获取用户输入最新的昵称 - 将
formType
设置为submit
当用户点击确定后,触发form
表单的bindsubmit
事件 - 在
form
表单的bindsubmit
事件中进行赋值 - 给
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
实现步骤:
- 在新增收货地址页面
data
中声明所需要的字段 - 定义收货地址所需要的全部接口
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>
实现步骤:
- 给
picker
选择组件添加change
事件来监听属性值的改变,获取选中的省市区 - 将获取到省市区标识和编码赋值给
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
绑定来收集新增地址表单数据。
在将数据收集以后,需要组织两个数据:
- 是否是默认地址,0 不设置为默认地址,1 设置为默认地址
- 拼接完整的收货地址
实现步骤:
- 使用简易双向数据绑定来收集新增地址表单数据。
- 给按钮绑定点击事件,在事件处理函数中收集并整理数据
落地代码:
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 逆地址解析
使用步骤:
- 在项目中引入 SDK 核心类
- 在
onLoad
中实例化 API 核心类,同时配置创建的 key - 使用实例方法
reverseGeocoder
方法进行逆地址解析,将提供的坐标转换为详细的地址位置信息
落地代码:
-
引入 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 基本使用
使用步骤:
- 安装并在项目中导入
async-validator
- 创建验证规则
- 创建表单验证实例,将验证规则传递给构造函数,产生实例
- 调用实例方法
validate
对数据进行验证- 第一个参数:需要验证的数据
- 第二个参数:回调函数,回调函数有两个参数 errors, fields
- errors:如果验证成功,返回 null,验证错误,返回数组
- fields:需要验证的字段,属性值错误数组
落地代码:
-
安装
async-validator
npm i async-validator
-
开发者工具,点击构建
npm
,对async-validator
进行构建 -
在 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. 新增收货地址表单验证
思路分析:
在点击新增收货地址的时候,我们需要对用户输入的值进行验证。产品需求如下:
- 收货人不能为空,且不能输入特殊字符
- 手机号不能为空,且输入的手机号必须合法
- 省市区不能为空
- 详细地址不能为空
正则:
// 验证收货人,是否只包含大小写字母、数字和中文字符
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}$'
实现步骤:
- 创建
validateForm
方法,使用async-validator
对表单进行验证 - 在新增收货地址之前,调用
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
,然后在表单验证以后,进行收货地址的添加即可。
实现步骤:
-
在对新增收货地址请求参数验证以后,将封装好的新增收货地址的
API
函数调用 -
在新增收货地址成功以后,跳转到收货地址详情页面。
落地代码:
// 新增或修改地址
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. 实现更新收货地址
实现步骤:
- 在从收货地址列表页面跳转到更新页面的时候,需要携带
id
- 在
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. 实现删除收货地址
实现步骤:
- 给删除按钮绑定点击事件
delAddress
,同时通过data-id
传递需要删除的商品id
- 在
delAddress
事件处理程序后面,调用API
函数reqDelAddress
,并传递id
- 在删除收货地址成功以后,给用户提示
落地代码
<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
即可。
实现思路:
- 给滑块绑定 id
- 在打开滑块时,获取当前滑块的实例,然后将实例存储到 data 的数组中。
- 给页面最外层的 view 同时给滑块区域绑定点击事件,在事件处理函数中对数据遍历,每一项调用
close
方法关掉滑块 - 将关掉的逻辑抽取成 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. 配置商品管理分包
实现步骤:
- 在
modules
目录下创建goodModule
文件夹,用来存放商品管理分包 - 在
app.json
的subpackages
进行商品管理分包配置 - 在
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. 商品列表-获取商品列表数据并渲染
实现步骤:
- 在
/pages/goods/list/list.js
中导入封装好的获取商品列表的API
函数 - 页面数据在页面加载的时候进行调用,在
onLoad
钩子函数中调用reqGoodsList
方法 - 在获取到数据以后,使用后端返回的数据对页面进行渲染
落地代码:
+ 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. 商品列表-实现上拉加载更多功能
实现步骤:
list.js
文件中声明onReachBottom
事件处理函数,监听用户的上拉行为- 在
onReachBottom
函数中加page
进行加 1 的操作,同时发送请求获取下一页数据 - 在
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. 商品详情-详情图片预览功能
实现步骤:
- 给展示大图的
image
组件绑定点击事件,同时通过自定义属性的方式,传递当前需要显示的图片http 链接 - 同时商品详情的数组数据传递给
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. 加入购物车-模板分析和渲
产品需求
-
如果点击的是加入购物车,需要将当前商品加入到购物车
-
如果点击的是立即购买,需要跳转到结算支付页面,立即购买该商品
-
如果是立即购买,不支持购买多个商品
结构分析:
点击立即购买和加入购物车的时候,通过 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. 加入购物车和立即购买区分处理
实现步骤:
- 给
Stepper
步进器组件,通过value
设置输入值,同时绑定change
事件,并将值同步到data
中 - 根据接口文档,导入封装的购物车的接口 API
- 点击弹框按钮的时候,判断点击的加入购物车还是立即购买,执行不同的操作
落地代码:
<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. 加入购物车-展示购物车购买数量
实现步骤:
- 进入商品详情,调用方法,在方法中判断
token
是否存在 - 如何存在,则获取购物车列表数据,通过累加计算得出商品购买数量,展示购买的数量
- 不存在,不执行任何逻辑,
落地代码:
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. 购物车-获取并渲染购物车列表
实现步骤:
- 导入封装好的获取列表数据的
API
函数 - 在
onShow
钩子中,根据产品的需求,处理页面的提示 - 在获取到数据以后,使用后端返回的数据对页面进行渲染
落地代码:
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. 购物车-购物车商品合计
实现步骤:
- 在
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. 购物车-删除购物车中的商品
实现步骤:
- 导入封装的接口
API
函数,同时导入处理删除自动关闭效果的behaviors
并进行注册 - 在点击删除以后,调用
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. 商品结算-获取收货地址
实现步骤:
- 在进入结算页面的时候,调用接口
API
函数,获取数据 - 然后根据数据并渲染结构
落地代码:
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. 商品结算-获取订单详情数据
实现步骤:
- 导入封装的接口
API
函数 - 在进入结算页面的时候,调用接口
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. 商品结算-获取立即购买数据
实现步骤:
- 在页面打开的时候,
onShow
中接受传递的参数,并赋值给data
中的状态 - 在
getOrderInfo
函数中,判断立即购买商品的id
是否存在,如果存在调用立即购买的接口 - 获取数据后,然后根据数据并渲染结构即可
落地代码:
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. 商品结算-收集送达时间
实现步骤:
- 在商品结算页面导入封装好的格式化时间的方法
formatTime
- 调用
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
对代码进行验证
- 收货地址不能为空
- 订购人姓名不能为空,且不能输入特殊字符
- 订购人手机号不能为空,且输入的手机号必须合法
- 送达日期不能为空
落地代码:
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. 优化-分包调整
思路分析:
-
将 [设置页面] 配置到 [设置模块分包],在访问个人中心页面时,提前预下载 [设置模块分包]
-
进入订单结算页面时,提前预下载 [设置模块分包]
落地代码:
{
"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. 分享功能
- 配置页面:
- 在小程序的
app.json
或具体页面的json
配置文件中,确保页面的navigationStyle
或其他相关属性不会干扰分享功能的展示。 - 如果页面需要自定义分享内容,可以在页面的
js
文件中使用onShareAppMessage
生命周期函数。
- 在小程序的
- 编写
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
函数。 - 在分享到微信外部(如好友、朋友圈)时,图片和描述可能会受到微信平台策略的限制,需要确保内容符合微信的规定。