目录
一.框架扩展
1.mobx-miniprogram
小程序页面、组件间的数据通信方案:
数据绑定:properties
获取组件实例:this.selectComponent()
事件绑定:this.triggerEvent()
获取应用实例:getApp()
页面间通信:EventChannel
事件总线:pubsub-js
所以为了方便进行页面、组件之间数据的传递,小程序官方提供了一个扩展工具库:
mobx-miniprogram
1.1mobx-miniprogram的介绍
mobx-miniprogram 是针对微信小程序开发的一个简单、高效、轻量级状态管理库,它基于Mobx状态管理框架实现。
使用 mobx-miniprogram 定义管理的状态是响应式的,当状态一旦它改变,所有关联组件都会自动更新相对应的数据
通过该扩展工具库,开发者可以很方便地在小程序中全局共享的状态,并自动更新视图组件,从而提升小程序的开发效率
1.2注意事项(mobx-miniprogram的安装)
在使用 mobx-miniprogram 需要安装两个包:mobx-miniprogram 和 mobx-miniprogram-bindings
mobx-miniprogram 的作用:创建 Store 对象,用于存储应用的数据
mobx-miniprogram-bindings 的作用:将状态和组件、页面进行绑定关联,从而在组件和页面中操作数据
1.3创建 Store 对象
熟悉mobx-miniprogram 三个核心概念:
observable:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。
action:用于修改状态(state)的方法,需要使用 action 函数显式的声明创建。
computed:根据已有状态(state)生成的新值。计算属性是一个方法,在方法前面必须加上 get 修饰符
// observable:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。
// action:用于显式的声明创建更新 state 状态的方法
import { observable, action } from 'mobx-miniprogram'
// 使用 observable 创建一个被监测的对象
export const numStore = observable({
// 创建应用状态
numA: 1,
numB: 2,
// 使用 action 更新 numA 以及 numB
update: action(function () {
this.numA+=1
this.numB+=1
}),
// 计算属性,使用 get 修饰符,
get sum() {
return this.numA + this.numB;
}
})
1.4在数组中使用数据
使用步骤:
从 mobx-miniprogram-bindings 库中导入 ComponentWithStore 方法
将 Component 方法替换成 ComponentWithStore 方法
然后配置 storeBindings 从 Store 中映射数据和方法即可
在替换以后,就会新增一个 storeBindings 配置项,配置项常用的属性有以下三个:
store: 指定要绑定的 Store 对象
fields: 指定需要绑定的 data 字段
actions: 指定需要映射的 actions 方法
代码如下:
// components/custom01/custom01.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
ComponentWithStore({
data: {
someData: '...'
},
storeBindings: {
store: numStore,
fields: ['numA', 'numB', 'sum'],
actions: ['update']
}
})
1.5在页面中使用数据
第一种方法:
使用Component 方法用于创建自定义组件。
小程序的页面也可以视为自定义组件,因此页面也可以使用 Component 方法进行构建,从而实现复杂的页面逻辑开发。
如果我们使用了 Component 方法来构建页面,那么页面中如果想使用 Store 中的数据,使用方式和组件的使用方式是一样的
第二种方法:
我们可以使用Component 方法构建页面,然后使用 ComponentWithStore 方法让页面和 Store 建立了关联
如果不想使用 Component 方法构建页面。这时候需要使用 mobx-miniprogram-bindings 提供的 BehaviorWithStore 方法来和 Store 建立关联
小程序的 behavior 方法是一种代码复用的方式,可以将一些通用的逻辑和方法提取出来,然后在多个组件中复用,从而减少代码冗余,提高代码的可维护性。在页面中也可以使用 behaviors 配置项
使用步骤如下:
新建 behavior 文件,从 mobx-miniprogram-bindings 库中导入 BehaviorWithStore 方法
在 BehaviorWithStore 方法中配置 storeBindings 配置项从 Store 中映射数据和方法
在 Page 方法中导入创建的 behavior ,然后配置 behavior 属性,并使用导入的 behavior
// behavior.js
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
export const indexBehavior = BehaviorWithStore({
storeBindings: {
store: numStore,
fields: ['numA', 'numB', 'sum'],
actions: ['update'],
}
})
1.6 fields、actions 对象写法
如果 fields 写成对象方式,又有两种写法:
映射形式:指定 data 中哪些字段来源于 store 以及它们在 store 中对应的名字。
例如 { a: 'numA', b: 'numB' }
函数形式:指定 data 中每个字段的计算方法
例如 { a: () => store.numA, b: () => anotherStore.numB }
如果 actions
写成对象方式,只有一种写法(只有映射形式没有函数形式)
映射形式:指定模板中调用的哪些方法来源于 store
以及它们在 store
中对应的名字。
例如 { buttonTap: 'update' }
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
ComponentWithStore({
data: {
someData: '...'
},
storeBindings: {
store: numStore,
fields: {
// 使用函数方式获取 Store 中的数据
a: () => store.numA,
b: () => store.numB,
// 使用映射形式获取 Store 中的数据,值为数据在 store 中对应的名字
total: 'sub'
},
// 使用映射形式获取 Store 中的 action 名字
actions: {
// key 自定义,为当前组件中调用的方法
// 值为 store 中对应的 action 名字
buttonTap: 'update'
}
}
})
2.miniprogram-computed
小程序框架没有提供计算属性相关的 api ,但是官方为开发者提供了拓展工具库 miniprogram-computed,提供了计算属性computed和监听器watch两个功能
2.1计算属性computed
如果需要在组件中使用计算属性功能,需要 miniprogram-computed
库中导入 ComponentWithComputed
方法
在使用时需要将 Component
方法替换成 ComponentWithComputed
方法 ,原本组件配置项也需要写到该方法中
使用npm install miniprogram-computed命令进行安装
2.2监听器watch
在使用时需要将 Component
方法替换成 ComponentWithComputed
方法 ,原本组件配置项也需要写到该方法中
// 引入 miniprogram-computed
import { ComponentWithComputed } from 'miniprogram-computed'
ComponentWithComputed({
data: {
a: 1,
b: 1
},
computed: {
total(data) {
// 注意:
// computed 函数中不能访问 this ,只有 data 对象可供访问
// 这个函数的返回值会被设置到 this.data.sum 字段中
return data.a + data.b
}
}
watch: {
//key:需要监听的数据
//value:是回调函数,回调函数有一个形参,形参就是最新的、改变后的数据
a:function(a){
console.log(a)
}
// 同时对 a 和 b 进行监听
'a, b': function (a, b) {
this.setData({
total: a + b
})
}
},
})
二.用户管理
1.什么是 token
Token是服务器生成的一串字符串,用作客户端发起请求的一个身份令牌。当第一次登录成功后,服务器生成一个 Token便将此 Token返回给客户端,客户端在接收到 Token以后,会使用某种方式将 Token保存到本地。以后客户端发起请求,只需要在请求头上带上这个 Token ,服务器通过验证 Token 来确认用户的身份,而无需再次带上用户名和密码。
Token具体流程
1. 客户端向服务器发起登录请求,服务端验证用户名与密码
2. 验证成功后,服务端会签发一个 `Token`,并将 `Token` 发送到客户端
3. 客户端收到 `token` 以后,将其存储起来,比如放在 `localStorage` 、`sessionStorage` 中
4. 客户端每次向服务器请求资源的时候需要带着服务端签发的 `Token`,服务端收到请求,然后去验证客户端请求里面带着的 `Token` ,如果验证成功,就向客户端返回请求的数据
<img src="http://8.131.91.46:6677/mina/floor/token交互流程.png" style="zoom:50%; border: 1px solid #ccc" />
2.小程序实现用户登录
实现步骤:
1. 在 `/api/user.js` 文件中根据接口文档,创建登录的 `API` 函数 `login`
2. 给登录按钮绑定点击事件,对应 `login` 回调函数
3. 在 `login` 回调函数中调用 `wx.login()` 方法,获取**临时登录凭证code**
4. 在 `/pages/login/login.js` 中导入封装好的 `API` 函数,传入 **临时登录凭证code** 然后调用
5. 在登录成功以后将 `token` 存储到本地
import http from '../utils/http
/**
* @description 授权登录
* @param {*} code 临时登录凭证code
* @returns Promise
*/
export const reqLogin = (code) => {
return http.get(`/mall-api/weixin/wxLogin/${code}`)
}
/pages/login/login.js
//登录请求
import { reqLogin } from '../../api/user'
import { toast } from '../../utils/extendApi'
Page({
// 点击登录
login() {
// 调用 wx.login 获取用户信息
wx.login({
success: async ({ code }) => {
if (code) {
// 调用接口 API,传入 code 进行登录
const res = await reqLogin(code)
// 登录成功以后将 token 存储到本地
wx.setStorageSync('token', res.data.token)
// 返回之前的页面
wx.navigateBack()
} else {
// 登录失败后给用户进行提示
toast({ title: '授权失败,请稍后再试~~~' })
}
}
})
}
})
3.Token 存储到 Store
实现步骤:
1. 安装`Mobx`两个包,在安装好包以后,对包进行构建,点击 `构建 npm`
2. 在项目的根目录下创建 `store` 文件夹,然后在该文件夹下新建 `userstore.js`
3. 导入核心的`observable ` 、`action` 方法,创建`Store`,同时声明数据和方法
4. 在登录页面,导入`ComponentWithStore` 方法,并配置 `storeBindings` 方法让页面和 `Store` 对象关联
安装依赖,安装完成后构建 npm
```shell
npm i mobx-miniprogram mobx-miniprogram-bindings
/store/index.js
// 导入 observable 函数用于创建可观察对象
// 导入 action 修改 store 中的可观察状态
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'
// 创建 store 对象,存储应用的状态
export const userStore = observable({
// 创建可观察状态 token
token: getStorage('token') || '',
// 对 token 进行修改
setToken: action(function (token) {
this.token = token
})
})
import { userStore } from '../../api/userstore'
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
ComponentWithStore({
storeBindings: {
store: userStore,
fields: ['token'],
actions: ['setToken'] }
methods: {
// 授权登录
login() {
// 使用 wx.login 获取用户的临时登录凭证 code
wx.login({
success: async ({ code }) => {
if (code) {
// 在获取到临时登录凭证 code 以后,需要传递给开发者服务器
const { data } = await reqLogin(code)
// 登录成功以后,需要将服务器响应的自定义登录态存储到本地
setStorage('token', data.token)
// 将数据存储到 store 对象中
this.setToken(data.token)
} else {
toast({ title: '授权失败,请重新授权' })
}
}
})
}
}
})
4.用户信息存储到 Store
实现步骤:
1. 在 `store/userstore.js` 中新增`userInfo`字段,同时创建修改的`action`方法
2. 在 `login.js` 中使用映射 `userInfo` 数据和 `setUserInfo` 方法
3. 在 `/api/user.js` 文件中根据接口文档,创建获取用户信息的 `API` 函数 `reqUserInfo`
4. 在 `/pages/login/login.js` 中导入封装好的获取商品列表的 `API` 函数
5. 创建 `getUserInfo` 方法,在 `getUserInfo` 方法中调用接口 `API` 函数 `reqUserInfo`
6. 在登录成功以后,调用`getUserInfo` 方法获取用户,然后将用户信息存到本地以及 `Store`
/api/user.js
/**
* @description 获取用户信息
* @returns Promise
*/
export const reqUserInfo = () => {
return http.get(`/mall-api/weixin/getuserInfo`)
}
/store/userstore.js
// 导入 observable 函数用于创建可观察对象
// 导入 action 修改 store 中的可观察状态
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'
// 创建 store 对象,存储应用的状态
export const userStore = observable({
// 创建可观察状态 token
// token,登录令牌
token: getStorage('token') || '',
// 用户信息
userInfo: wx.getStorageSync('userInfo') || {},
// 对 token 进行修改
setToken: action(function (token) {
this.token = token
}),
// 设置用户信息
setUserInfo: action(function (userInfo) {
this.userInfo = userInfo
})
})
/pages/login/login.js
// pages/login/login.js
// 导入封装通用模块方法
import { toast } from '../../utils/extendApi'
// 导入本地存储 api
import { setStorage } from '../../utils/storage'
// 导入接口 API 函数
import { reqLogin, reqUserInfo } from '../../api/user'
// 导入 ComponentWithStore 方法
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
// 导入 store 对象
import { userStore } from '../../stores/userstore'
// 使用 ComponentWithStore 方法替换 Component 方法构造页面
ComponentWithStore({
// 让页面和 Store 对象建立关联
storeBindings: {
store: userStore,
fields: ['token', 'userInfo'],
actions: ['setToken', 'setUserInfo']
},
methods: {
// 授权登录
login() {
// 使用 wx.login 获取用户的临时登录凭证 code
wx.login({
success: async ({ code }) => {
if (code) {
// 在获取到临时登录凭证 code 以后,需要传递给开发者服务器
const { data } = await reqLogin(code)
// 登录成功以后,需要将服务器响应的自定义登录态存储到本地
setStorage('token', data.token)
// 将自定义登录态 token 存储到 Store 对象
this.setToken(data.token)
// 获取用户信息
this.getUserInfo()
} else {
toast({ title: '授权失败,请重新授权' })
}
}
})
},
// 获取用户信息
async getUserInfo() {
const { data } = await reqUserInfo()
// 将用户信息存储到本地
setStorage('userInfo', data)
// 将用户信息存储到 Store
this.setUserInfo(data)
}
}
})
5.使用数据渲染用户信息
实现步骤:
1. 在个人中心页面导入`ComponentWithStore` 方法构建页面
2. 配置 `storeBindings` 让组件和 `Store` 建立关联
3. 渲染页面
/pages/info/info.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
ComponentWithStore({
storeBindings: {
store: userStore,
fields: ['token', 'userInfo']
}
})
/pages/info/info.wxml
<!--pages/info/info.wxml-->
<view class="container bg">
<!-- 顶部展示图 -->
<view class="top-show">
<image mode="widthFix" class="top-show-img" src="/static/images/banner.jpg"></image>
</view>
<view class="wrap">
<!-- 未登录面板 -->
<view class="user-container section" wx:if="{{ !token }}" bindtap="toLoginPage">
<view class="avatar-container">
<image src="/static/images/avatar.png"></image>
<view class="no-login">
<text class="ellipsis">未登录</text>
<text>点击授权登录</text>
</view>
</view>
</view>
<!-- 登录以后得面包 -->
<view wx:else class="user-container section">
<view class="avatar-container">
<image src="{{ userInfo.headimgurl }}"></image>
<view class="no-login">
<text class="ellipsis">{{ userInfo.nickname }}</text>
</view>
</view>
<view class="setting">
设置
</view>
</view>
<!-- 订单面板 -->
<view class="order section">
<view class="order-title-wrap">
<text class="title">我的订单</text>
<text class="more">查看更多></text>
</view>
<view class="order-content-wrap">
<view class="order-content-item">
<navigator wx:if="{{ !token }}" url="/pages/login/login">
<view class="iconfont icon-dingdan"></view>
<text>商品订单</text>
</navigator>
<navigator wx:else url="/pages/order/list/index">
<view class="iconfont icon-dingdan"></view>
<text>商品订单</text> </navigator>
</view>
<view class="order-content-item">
<view class="iconfont icon-lipinka"></view>
<text>礼品卡订单</text>
</view>
<view class="order-content-item">
<view class="iconfont icon-tuikuan"></view>
<text>退款/售后</text>
</view>
</view>
</view>
<!-- 关于售前售后服务面板 -->
<view class="after-scale section">
<!-- coding... -->
</view>
<!-- 底部面板 -->
<view class="info-footer">
尚硅谷技术支持
</view>
</view>
</view>
6.配置分包以及预下载
实现步骤:
1. 在 `app.json` 新增 `subpackages` 进行分包配置,新增 `preloadRule` 进行分包预下载配置
2. 在 `subpackages` 设置分包的 根目录 `root` 、别名 `name` 、页面路径 `pages`
3. 在 `preloadRule` 设置预下载。
"subpackages": [
{
"root": "modules/settingModule",
"name": "settingModule",
"pages": [
"pages/address/add/index",
"pages/address/list/index",
"pages/profile/profile"
]
}
],
"preloadRule": {
"pages/settings/settings": {
"network": "all",
"packages": ["settingModule"]
}
}
7.更新用户信息-渲染用户信息
实现步骤:
1. 新建 `behavior.js` 文件,从 `mobx-miniprogram-bindings` 库中导入 `BehaviorWithStore` 方法
2. 在 `BehaviorWithStore` 方法中配置 `storeBindings` 配置项从 `Store` 中映射数据和方法
3. 在 `Page` 方法中导入创建的 `behavior` ,然后配置 `behavior` 属性,并使用导入的 `behavior`
// behavior.js
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
// 导入 store 对象
import { userStore } from '../../stores/userstore'
export const userBehavior = BehaviorWithStore({
storeBindings: {
store: userStore,
fields: ['userInfo']
}
})
modules/settingModule/pages/profile/profile.js`
import { userBehavior } from './behavior'
Page({
behaviors: [userBehavior],
// 页面的初始数据
data: {
isShowPopup: false // 控制更新用户昵称的弹框显示与否
},
// 其他代码略...
})
```
modules/settingModules/pages/profile/profile.wxml`
<view class="container">
<view class="setting-list avatar-container">
<text>头像</text>
<view class="avatar">
<button hover-class="none">
<image src="{{ userInfo.headimgurl }}" mode="" />
</button>
</view>
</view>
<view class="setting-list nickname">
<text>昵称</text>
<text>{{ userInfo.nickname }}</text>
</view>
<!-- coding... -->
</view>
8.获取头像临时路径
当用户点击头像时,可以对头像进行更新操作,我们使用通过微信提供的头像昵称填写能力快速完善
<img src="http://8.131.91.46:6677/mina/floor/更新头像.gif" style="zoom:70%; border: 1px solid #ccc" />
实现步骤:
1. 将 [button](https://developers.weixin.qq.com/miniprogram/dev/component/button.html) 组件 `open-type` 的值设置为 `chooseAvatar`
2. 当用户选择需要使用的头像之后,可以通过 `bindchooseavatar` 事件回调获取到头像信息的临时路径
modules/settingModules/pages/profile/profile.wxml
<view class="avatar">
<button
class="avatar-btn"
hover-class="none"
open-type="chooseAvatar"
bindchooseavatar="chooseAvatar"
>
<image src="{{ userInfo.headimgurl || '/assets/images/avatar.png' }}" />
</button>
</view>
modules/settingModules/pages/profile/profile.js`
// pages/profile/profile.js
import { userBehavior } from './behavior'
Page({
// 注册 behavior
behaviors: [userBehavior],
// 页面的初始数据
data: {
isShowPopup: false // 控制更新用户昵称的弹框显示与否
},
// 更新用户头像
chooseAvatar(event) {
// console.log(event)
// 获取头像的临时路径
// 临时路径具有失效时间,需要将临时路径上传到公司的服务器,获取永久的路径
// 在获取永久路径以后,需要使用永久路径更新 headimgurl
// 用户点击 保存按钮,才算真正的更新了头像和昵称
const { avatarUrl } = event.detail
this.setData({
'userInfo.headimgurl': avatarUrl
})
},
})
9.头像上传到服务器
实现步骤:
1. 在获取到用户的临时头像路径以后,调用 `wx.uploadFile()` 方法,同时设置好基本的参数,
2. 在上传成功后,获取到服务器返回的永久地址
3. 将地址赋值给 `data` 中的数据
modules/settingModules/pages/profile/profile.js
// 获取用户头像信息
getAvatar(e) {
// 获取选中的头像
const { avatarUrl } = e.detail
wx.uploadFile({
url: 'https://gmall-prod.atguigu.cn/mall-api/fileUpload',
filePath: avatarUrl,
name: 'file',
header: {
token: wx.getStorageSync('token'),
},
success: (res) => {
// 将获取到的头像赋值给 data 中变量同步给页面结构
const uploadRes = JSON.parse(res.data)
this.setData({
'userInfo.headimgurl': uploadRes.data
})
},
fail(err) {
wx.showToast({
title: '头像更新失败,请稍后再试',
icon: 'none'
})
}
})
}
10.完成头像更新
实现步骤:
1. 在 `/api/user.js` 文件中根据接口文档,创建获取用户信息的 `API` 函数 `reqUpdateUserInfo`
2. 给修改个人资料的保存按钮绑定点击事件,触发 `updateUserInfo` 回调函数
3. 在回调函数中调用接口 `API` 函数 `reqUpdateUserInfo` ,同时传入用户的信息
4. 更新用户信息以后,将用户信息存储到本地同时同步到 `Store`
// pages/profile/profile.js
import { reqUpdateUserInfo, reqUserInfo } from '../../../../api/user'
Page({
// coding...
// 更新用户信息
async updateUserInfo() {
// 调用 API,更新用户信息
await reqUpdateUserInfo(this.data.userInfo)
// 将用户信息存储到本地
wx.setStorageSync('userInfo', this.data.userInfo)
// 将用户信息存储到 Store
this.setUserInfo(this.data.userInfo)
// 给用户提示头像更新成功
wx.showToast({
title: '头像更新成功',
icon: 'none'
})
}
// coding...
}
11.更新用户昵称
实现步骤:
1. 给 `form` 表单绑定 `bindsubmit` 事件,用来获取输入框最新的值
2. 给 `input` 组件绑定 `type` 属性,属性值为 `nickname`,获取微信昵称
3. 给 `input` 组件绑定 `bindinput` 事件,获取用户输入最新的昵称
4. 将 `formType` 设置为 `submit` 当用户点击确定后,触发 `form` 表单的 `bindsubmit` 事件
5. 在 `form` 表单的 `bindsubmit` 事件中进行赋值
6. 给 `form` 表单的取消按钮绑定事件,取消弹框
12.效果图
三、地址管理
1.定义新增参数以及封装接口 API
思路:
点击新建地址按钮,需要跳转到新增地址页面
因为新增和编辑收货地址页面是同一个页面,我们需要在这个页面处理新增和编辑功能,为了做区分处理。
我们在后续做进行编辑的时候传递 `id` 属性,值为 收货地址的 `id` 值。
首先熟悉接口文档:[获取用户信息](https://apifox.com/apidoc/shared-6ed6c5c4-56c4-4619-8e2a-4817aa140e30/api-134640244)
实现步骤:
1. 在新增收货地址页面 `data` 中声明所需要的字段
2. 定义收货地址所需要的全部接口 `API` 函数
代码:
//modules/settingModule/pages/address/add/index
js
Page{{
// 页面的初始数据
data: {
name: '', // 收货人
phone: '', // 手机号
provinceName: '', // 省
provinceCode: '', // 省 编码
cityName: '', // 市
cityCode: '', // 市 编码
districtName: '', // 区
districtCode: '', // 区 编码
address: '', // 详细地址
fullAddress: '', // 完整地址 (省 + 市 + 区 + 详细地址)
isDefault: 0 // 设置默认地址,是否默认地址 → 0:否 1:是
}
}}
/api/address`
js
import http from '../utils/http'
/**
* @description 实现新增收货地址
* @param {*} data
* @returns Promise
*/
export const reqAddAddress = (data) => {
return http.post('/userAddress/save', data)
}
/**
* @description 获取收货地址列表
* @returns Promise
*/
export const reqAddressList = () => {
return http.get('/userAddress/findUserAddress')
}
/**
* @description 获取收货地址详情
* @param {*} id 收货地址id
* @returns Promise
*/
export const reqAddressInfo = (id) => {
return http.get(`/userAddress/${id})
}
/**
* @description 编辑收货地址
* @param {*} data
* @returns Promise
*/
export const reqUpdateAddress = (data) => {
return http.post('/userAddress/update', data)
}
/**
* @description 删除收货地址
* @param {*} id 收货地址 id
* @returns Promise
*/
export const reqDelAddress = (id) => {
return instance.get(`/userAddress/delete/${id})
}
2. 收集省市区数据
实现步骤:
1. 给 `picker` 选择组件添加`change` 事件来监听属性值的改变,获取选中的省市区
2. 将获取到省市区标识和编码赋值给 `data`中的字段
代码:
Page({
// coding...
// 省市区选择
onAddressChange(event) {
const [provinceCode, cityCode, districtCode] = event.detail.code
const [provinceName, cityName, districtName] = event.detail.value
// 存储省市区对应的编码
this.setData({
provinceCode,
provinceName,
cityCode,
cityName,
districtName,
districtCode
})
}
})
3.收集新增地址其他请求参数
实现步骤以及思路:
使用简易双向数据 `model:value` 绑定来收集新增地址表单数据。
在将数据收集以后,需要组织两个数据:
1. 是否是默认地址,0 不设置为默认地址,1 设置为默认地址
2. 拼接完整的收货地址
步骤:1. 使用简易双向数据绑定来收集新增地址表单数据。
2. 给按钮绑定点击事件,在事件处理函数中收集并整理数据
代码:
Page({
// coding
// 获取表单元素的值
saveAddrssForm(event) {
// 解构出省市区以及 是否是默认地址
const { provinceName, cityName, districtName, address, isDefault } = this.data
// 拼接完整的地址
const fullAddress = provinceName + cityName + districtName + address
// 合并接口请求参数
const params = {
...this.data,
fullAddress,
isDefault: isDefault ? 1 : 0
}
console.log(params)
}
})
4.地理定位功能介绍
实现步骤与思路:
1. 在 app.json 中配置 `requiredPrivateInfos` 进行声明启用
2. 在调用 `wx.getLocation()` 时需要在 app.json 配置 `permission `字段,同时使用 `scope.userLocation` 声明**收集用户选择的位置信息**的目的,`wx.chooseLocation()` 接口不需要配置该字段,可以直接进行调用
3. 在配置好以后,调用 `wx.getLocation()` 和 `wx.chooseLocation()` 接口
代码:
json
{
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
],
"permission": {
"scope.userLocation": {
"desc": "获取用户位置信息用于填写收货地址"
}
}
}
**getLocation 使用:**
js
// 地理定位
async onLocation() {
// 获取 纬度 、精度
const { latitude, longitude } = await wx.getLocation()
console.log(location)
}
**chooseLocation 使用:**
js
// 地理定位
async onLocation() {
// 打开地图选择位置,获取 纬度 、精度
const { latitude, longitude } = await wx.chooseLocation()
console.log(res)
}
<img src="http://8.131.91.46:6677/mina/floor/授权地址.jpg" style="zoom:30%; height: 2000px; border: 1px solid #ccc" />
5.拒绝授权后的解决方案
步骤与思路:
在调用 `wx.getLocation()` 获取用地理位置时,如果用户选择拒绝授权,代码会直接抛出错误。
在拒绝授权以后,再次调用 `wx.getLocation()` 时,就不会在弹窗询问用户是否允许授权。
1. `wx.getSetting()`:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限
2. `wx.openSetting()`: 调起客户端小程序设置界面,返回用户设置的操作结果
代码:
// 获取用户地理位置信息
async onLocation() {
// 调用 getSetting 方法获取用户所有的授权信息
// 返回的 authSetting 包含小程序已向小程序申请过的权限已经授权结果(true、false)
const { authSetting } = await wx.getSetting()
console.log(authSetting)
// scope.userLocation 是否已经授权获取地理位置的信息
// 如果之前没有申请过返回 undefined,需要调用 getLocation
// 如果之前同意了授权,返回 true,需要调用 getLocation
// 如果之前拒绝了授权,返回 false,需要用户手动进行授权
// 等于 true,或者不等于 undefined,说明需要进行授权
// const isAuth =
// authSetting['scope.userLocation'] ||
// authSetting['scope.userLocation'] === undefined
// 为了避免冗余的条件判断,使用 !! 把代码进行优化
const isAuth = !!authSetting['scope.userLocation']
if (!isAuth) {
// 弹窗询问用户是否进行授权
const modalRes = await wx.modal({
title: '授权提示',
content: '需要需要您的地理位置信息,请确认授权'
})
// 如果用户点击了取消,说明用户拒绝了授权,给用户提示
if (!modalRes) return wx.toast({ title: '您拒绝了授权' })
// 如果用户点击了确定,调用 wx.openSetting 打开微信客户端小程序授权页面
// 并返回授权以后的结果
const { authSetting } = await wx.openSetting()
// 如果用户没有更新授权信息,提示没有更新授权
if (!authSetting['scope.userLocation'])
return wx.toast({ title: '授权失败!' })
try {
// 如果用户更新授权信息,则调用 getLocation 获取用户地理位置信息
const locationRes = await wx.getLocation()
// 打印地理位置信息
console.log(locationRes)
} catch (err) {
console.log(err)
}
} else {
try {
// 如果是第一次调用 getLocation 或者之前授权过
// 直接调用 getLocation 获取用户信息即可
const locationRes = await wx.getLocation()
console.log(locationRes)
} catch (error) {
wx.toast({ title: '您拒绝授权获取地址位置' })
}
}
}
6.开通腾讯位置服务
步骤与思路:
1. 申请开发者密钥(key):[申请密钥](https://lbs.qq.com/dev/console/application/mine)
2. 开通 webserviceAPI 服务:控制台 → 应用管理→[我的应用](https://lbs.qq.com/dev/console/key/manage) → 添加 key →勾选 WebServiceAPI →保存
3. 下载微信小程序 JavaScriptSDK,微信小程序[JavaScriptSDK v1.1](https://mapapi.qq.com/web/miniprogram/JSSDK/qqmap-wx-jssdk1.1.zip) [JavaScriptSDK v1.2](https://mapapi.qq.com/web/miniprogram/JSSDK/qqmap-wx-jssdk1.2.zip)
4. 安全域名设置
- 在[小程序管理后台](https://mp.weixin.qq.com/wxamp/home/guide) -> 开发 -> 开发管理 -> 开发设置 -> “服务器域名” 中设置 request 合法域名
- 添加 https://apis.map.qq.com
7.LBS 逆地址解析
步骤:
1. 在项目中引入 SDK 核心类
2. 在 `onLoad` 中实例化 API 核心类,同时配置创建的 key
3. 使用实例方法 `reverseGeocoder` 方法进行逆地址解析,将提供的坐标转换为详细的地址位置信息
代码:
1. 引入 SDK 核心类
// var QQMapWX = require('../../libs/qqmap-wx-jssdk.js');
import QQMapWX from '../../../../../libs/qqmap-wx-jssdk.min'
2. 实例化 API 核心类
// 引入SDK核心类,js文件根据自己业务,位置可自行放置
import QQMapWX from '../../../../../libs/qqmap-wx-jssdk.min'
Page({
onLoad: function () {
// 实例化API核心类
this.qqmapsdk = new QQMapWX({
key: '申请的key'
})
}
// coding
}
3. 使用 `reverseGeocoder` 方法进行逆地址解析,将提供的坐标转换为所在位置的文字描述的转换
// LBS 地址逆解析
// 地理定位
async onLocation() {
// 获取 纬度 、精度
// const { latitude, longitude } = await wx.getLocation()
// console.log(location)
// 获取经、纬度、位置名称
let { latitude, longitude, name } = await wx.chooseLocation()
// 使用 reverseGeocoder 方法进行逆地址解析
this.qqmapsdk.reverseGeocoder({
// 传入经、纬度
location: {
latitude,
longitude
},
// 逆地址解析成功后执行
success: (res) => {
// 获取选择的
const { street_number } = res.result.address_component
// province 省 city 市 district 区
const {
province, // 省
city, // 市
district, // 区
adcode, // 行政区划代码
city_code, // 城市代码,由国家码+行政区划代码(提出城市级别)组合而来,总共为9位
nation_code // 国家代码
} = res.result.ad_info
this.setData({
// 省级: 前两位有值,后4位置0,如,河北省: 130000
provinceCode: adcode.replace(adcode.substring(2, 6), '0000'),
provinceName: province,
// 市前面多个国家代码,需要进行截取
cityCode: city_code.slice(nation_code.length),
cityName: city,
// 东莞市、中山市、修州市、嘉关市 因其下无区县级,
districtCode: district && adcode,
districtName: district,
// 详细地址
address: name,
fullAddress: [province, city, district, address].join('')
})
}
})
}
8.async-validator 基本使用
步骤:
1. 安装并在项目中导入 `async-validator`
2. 创建验证规则
3. 创建表单验证实例,将验证规则传递给构造函数,产生实例
4. 调用实例方法 `validate` 对数据进行验证
- 第一个参数:需要验证的数据
- 第二个参数:回调函数,回调函数有两个参数 errors, fields
- errors:如果验证成功,返回 null,验证错误,返回数组
- fields:需要验证的字段,属性值错误数组
代码:
1. 安装 `async-validator`
shell
npm i async-validator
2. 开发者工具,点击构建 `npm`,对 `async-validator` 进行构建
3. 在 js 文件中导入 `async-validator`
// 1️⃣ 引入 async-validator,async-validator 提供了一个构造函数
import Schema from 'async-validator'
Page({
// 2️⃣定义需要验证的数据
data: {
name: '你好'
},
// 验证数据
onValidate() {
// 3️⃣创建表单验证规则
const rules = {
// key 建议和 需要验证的数据字段名字保持一致
name: [
// required 是否是必填项
{ required: true, message: 'name 不能为空' },
// type 数据的类型
// message 如果验证失败,提示的错误内容
{ type: 'string', message: 'name 不是字符串' },
// min 最少位数,max 最大位数
{ min: 2, max: 5, message: '名字最少 2 个字,最多 5 个字' }
// 正则表达式
// { pattern: '', message: '' }
// 自定义验证规则
// { validator: () => {} }
]
}
// 4️⃣创建表单验证实例
// 在创建实例时需要传入验证规则
const validator = new Schema(rules)
// 5️⃣ 调用 validate 实例方法对数据进行验证
// validate 方法接收一个对象作为参数,对象是需要验证的数据
// 注意:validate 方法只会验证和验证规则同名的属性
validator.validate(this.data, (errors, fields) => {
// 如果验证失败,errors 是所有错误的数组
// 如果验证成功,errors 是 null
console.log(errors)
// fields 是需要验证的属性,属性值是数组,数组中包含错误信息
console.log(fields)
if (errors) {
console.log('验证没有通过')
console.log(errors)
return
}
console.log('验证通过')
})
}
})
9.新增收货地址表单验证
步骤与思路:
在点击新增收货地址的时候,我们需要对用户输入的值进行验证。产品需求如下:
1. 收货人不能为空,且不能输入特殊字符
2. 手机号不能为空,且输入的手机号必须合法
3. 省市区不能为空
4. 详细地址不能为空
1. 创建 `validateForm` 方法,使用 `async-validator` 对表单进行验证
2. 在新增收货地址之前,调用 `validateForm` 方法,如果验证成功执行新增守护地址的逻辑
代码:
/modules/settingModule/pages/address/add/index
import Schema from 'async-validator'
Page({
// coding....
// 保存收货地址
async saveAddrssForm() {
// 组织参数 (完整地址、是否设置为默认地址)
const {
provinceName,
cityName,
districtName,
address,
isDefault
} = this.data
// 最终需要发送的请求参数
const params = {
...this.data,
fullAddress: provinceName + cityName + districtName + address,
isDefault: isDefault ? 1 : 0
}
// 调用方法对最终的请求参数进行验证
const { valid } = await this.validateAddress(params)
// 如果验证没有通过,不继续执行后续的逻辑
if (!valid) return
console.log(params)
},
// 验证新增收货地址请求参数
// 形参 params 是需要验证的数据
validateAddress(params) {
// 验证收货人,是否只包含大小写字母、数字和中文字符
const nameRegExp = '^[a-zA-Z\\d\\u4e00-\\u9fa5]+$'
// 验证手机号
const phoneReg = '^1(?:3\\d|4[4-9]|5[0-35-9]|6[67]|7[0-8]|8\\d|9\\d)\\d{8}$'
// 创建验证规则,验证规则是一个对象
// 每一项是一个验证规则,验证规则属性需要和验证的数据进行同名
const rules = {
name: [
{ required: true, message: '请输入收货人姓名' },
{ pattern: nameRegExp, message: '收货人姓名不合法' }
],
phone: [
{ required: true, message: '请输入收货人手机号' },
{ pattern: phoneReg, message: '手机号不合法' }
],
provinceName: { required: true, message: '请选择收货人所在地区' },
address: { required: true, message: '请输入详细地址' }
}
// 创建验证实例,并传入验证规则
const validator = new Schema(rules)
// 调用实例方法对数据进行验证
// 注意:我们希望将验证结果通过 Promsie 的形式返回给函数的调用者
return new Promise((resolve) => {
validator.validate(params, (errors, fields) => {
if (errors) {
// 如果验证失败,需要给用户进行提示
wx.toast({
title: errors[0].message
})
resolve({ valid: false })
} else {
resolve({ valid: true })
}
})
})
},
10.实现新增收货地址
步骤与思路:
在实现了新增收货地址的数据收集、表单验证以后,我们需要实现新增收货地址的功能,将用户的收货地址到服务器。我们直接根据接口文档,封装接口 `API`,然后在表单验证以后,进行收货地址的添加即可。
1. 在对新增收货地址请求参数验证以后,将封装好的新增收货地址的 `API` 函数调用
2. 在新增收货地址成功以后,跳转到收货地址详情页面。
代码:
/pages/address/add/index.js
// 新增或修改地址
async saveAddrssForm(event) {
// 组织参数 (完整地址、是否设置为默认地址)
const {
provinceName,
cityName,
districtName,
address,
isDefault
} = this.data
// 最终需要发送的请求参数
const params = {
...this.data,
fullAddress: provinceName + cityName + districtName + address,
isDefault: isDefault ? 1 : 0
}
// 如果验证没有通过,不进行后续处理
if (!valid) return
// 发送请求,保存收货地址
const res = await reqAddAddress(params)
if (res.code === 200) {
wx.navigateBack({
success() {
wx.toast({ title: '新增收货地址成功' })
}
})
}
}
11.收货地址列表渲染
步骤与思路:
渲染收货地址需要收货地址的数据,需要调用接口获取收货地址数据,使用返回的数据进行结构的渲染。
1. 在 `onShow` 钩子函数中调用`reqAddressList`方法
2. 在获取到数据以后,使用后端返回的数据对页面进行渲染
代码:
/modules/settingModule/pages/address/list/index.js`
// pages/address/list/index.js
import { reqAddressList } from '../../../../../api/address'
Page({
// 页面的初始数据
data: {
addressList: [] // 收货地址列表
},
// 获取收货地址
async getAddressList() {
// 调用 API,获取收货地址
const { data: addressList } = await reqAddressList()
this.setData({
addressList
})
},
// 去编辑页面
toEdit() {
wx.navigateTo({
url: '/modules/settingModule/pages/address/add/index'
})
},
onLoad() {
this.getAddressList()
}
})
`➡️ /modules/settingModule/pages/address/list/index.wxml`
```html
<view class="list-warpper" wx:if="{{ addressList.length }}">
<view wx:for="{{ addressList }}" wx:key="id" class="list-item">
<van-swipe-cell right-width="{{ 65 }}">
<view class="list-item-box">
<view class="info">
<view class="user-info">
<text>{{ item.name }}</text>
<text>{{ item.phone }}</text>
<text wx:if="{{ item.isDefault }}" class="default-tag">默认</text>
</view>
<view class="address-info"> {{ item.fullAddress }} </view>
</view>
<view class="editBtn">
<van-icon bindtap="toEdit" name="edit" size="22px" color="#999" />
</view>
</view>
<!-- <van-icon name="delete" size="22px" color="#999" /> -->
<view slot="right" class="van-swipe-cell__right">
<text>删除</text>
</view>
</van-swipe-cell>
</view>
</view>
12. 实现更新收货地址
步骤与思路:
新增和编辑收货地址页面是同一个页面,我们需要在这个页面处理新增和编辑功能
在收货地址列表页面,点击更新按钮时,需要跳转到新增/更新页面,同时需要将更新这一项的 `id` 传递给新增/更新页面。
在 `onLoad` 中获取 `id`,并且使用 `id ` 区分用户是进行新增还是编辑的操作。
如果存在 `id`,在获取需要更新的收货地址的数据,并进行页面的回显用户的收货地址,并且需要更新导航栏标题
因为我们之前直接是将数据放到 `data` 中的,所以我们直接将数据使用 `setData` 赋值即可
1. 在从收货地址列表页面跳转到更新页面的时候,需要携带 `id`
2. 在 `onLoad` 中判断是否存在 `id`,如果存在 `id`,在获取数据进行回显
代码:
/modules/settingModule/pages/address/list/index.wxml`
<!-- 编辑、删除按钮 -->
<van-icon bindtap="toEdit" data-id="{{ item.id }}" name="edit" size="22px" color="#999" />
/modules/settingModule/pages/address/list/index.js`
// 去编辑页面
toEdit(event) {
// 需要编辑的收货地址
const { id } = event.target.dataset
wx.navigateTo({
url: `/modules/settingModule/pages/address/add/index?id=${id}`
})
}
/modules/settingModule/pages/address/add/index.js`
Page({
// coding...
// 保存收货地址
async saveAddrssForm() {
// 组织参数 (完整地址、是否设置为默认地址)
const {
provinceName,
cityName,
districtName,
address,
isDefault
} = this.data
// 最终需要发送的请求参数
const params = {
...this.data,
fullAddress: provinceName + cityName + districtName + address,
isDefault: isDefault ? 1 : 0
}
// 调用方法对最终的请求参数进行验证
const { valid } = await this.validateAddress(params)
// 如果验证没有通过,不继续执行后续的逻辑
if (!valid) return
// 发送请求,保存收货地址
const res = this.addressId
? await reqUpdateAddress(params)
: await reqAddAddress(params)
if (res.code === 200) {
// 提示用户更新状态
wx.toast({
title: this.addressId ? '编辑收货地址成功' : '新增收货地址成功'
})
// 返回到收货地址列表页面
wx.navigateBack()
}
},
// 回显收货地址的逻辑
showAddressInfo(id) {
// 判断是否存在 id,如果不存在 id,return 不执行后续的逻辑
if (!id) return
// 如果存在 id,将 id 挂载到 this 页面实例上
this.addressId = id
// 动态设置当前页面的标题
wx.setNavigationBarTitle({
title: '更新收货地址'
})
// 调用方法获取收货地址详细信息
const { data } = await reqAddressInfo(this.addressId)
// 将获取的数据进行赋值
this.setData(data)
},
onLoad(options) {
// 对核心类 QQMapWX 进行实例化
this.qqmapwx = new QQMapWX({
// key 要使用自己申请的 key
// 在进行逆解析的时候,如果发现 key 只能使用一次,需要在腾讯位置服务后台配置额度
key: 'S5CBZ-TQXCB-L73UJ-J6VJA-FXS53-JNBY3'
})
// 回显收货地址的逻辑
this.showAddressInfo(options.id)
}
// coding
})
13. 实现删除收货地址
思路与步骤:
点击删除按钮的时候,需要将对应的地址进行删除
当点击删除按钮的时候,调用封装的接口 `API` 函数 ,同时传递需要删除的收货地址 `id` 即可
实现步骤
1. 给删除按钮绑定点击事件 `delAddress`,同时通过 `data-id` 传递需要删除的商品 `id`
2. 在 `delAddress` 事件处理程序后面,调用 `API` 函数 `reqDelAddress`,并传递 `id`
3. 在删除收货地址成功以后,给用户提示
代码:
/modules/settingModule/pages/address/list/index.wxml`
<van-icon
+ bindtap="delAddress"
+ data-id="{{ item.id }}"
name="delete"
size="22px"
color="#999"
/>
```
/modules/settingModule/pages/address/list/index.js`
// 删除收货地址
async delAddress(e) {
const { id } = e.target.dataset
await reqDelAddress(id)
this.getAddressList()
}
四、商品管理
1.配置商品管理分包
思路与步骤:
随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。
因此我们需要将 `商品列表` 和 `商品详情` 功能配置成一个分包,
当用户在访问设置页面时,还预先加载 `商品列表` 和 `商品详情` 所在的分包
实现步骤:
1. 在 `modules` 目录下创建 `goodModule` 文件夹,用来存放商品管理分包
2. 在 `app.json` 的 `subpackages` 进行商品管理分包配置
3. 在 `app.json` 的 `preloadRule` 进行商品管理分包配置
代码:
app.json
```json
{
"subPackages": [
{
"root": "modules/settingModule",
"name": "settingModule",
"pages": [
"pages/address/add/index",
"pages/address/list/index",
"pages/profile/profile"
]
},
{
"root": "modules/goodModule",
"name": "goodModule",
"pages": ["pages/goods/list/list", "pages/goods/detail/detail"]
}
],
"preloadRule": {
"pages/settings/settings": {
"network": "all",
"packages": ["settingModule"]
},
"pages/category/category": {
"network": "all",
"packages": ["goodModule"]
}
}
}
2.封装商品模块接口 API
代码:
api/goods.js`
```js
import http from '../utils/http'
/**
* @description 获取商品列表
* @return Promise
*/
export const reqGoodsList = ({ limit, page, ...reset }) => {
return http.get(`/mall-api/goods/list/${page}/${limit}`, reset)
}
/**
* @description 获取商品详情
* @param {*} goodsId 商品Id
* @returns Promise
*/
export const reqGoodsInfo = (goodsId) => {
return http.get(`/mall-api/goods/${goodsId}`)
}
3.商品列表-准备列表请求参数
实现步骤:
1. 在商品列表的 `data` 字段中,根据接口文档,定义商品列表接口需要使用的字段
2. 在商品列表的 `onLoad` 钩子函数中接收请求的参数,并将请求参数进行合并
代码:
/modules/goodsModule/pages/list/list.js`
```js
Page({
// 页面的初始数据
data: {
goodsList: [], // 商品列表数据
isFinish: false, // 判断数据是否加载完毕
// 接口请求参数
requestData: {
page: 1, // 页码
limit: 10, // 每页请求多少条数据
category1Id: '', // 一级分类 id
category2Id: '' // 二级分类 id
}
},
// 生命周期函数--监听页面加载
onLoad(options) {
// 接收传递的参数
Object.assign(this.data.requestData, options)
}
})
4.商品列表-获取商品列表数据并渲染
思路与步骤:
在准备商品列表的请求参数以后,
在页面调用 `API` 函数获取商品列表的数据,在获取到数据以后,使用后端返回的数据对页面进行渲染。
实现步骤:
1. 在 `/pages/goods/list/list.js` 中导入封装好的获取商品列表的 `API` 函数
2. 页面数据在页面加载的时候进行调用,在 `onLoad` 钩子函数中调用 `reqGoodsList` 方法
3. 在获取到数据以后,使用后端返回的数据对页面进行渲染
代码:
/modules/goodsModules/pages/list/list.js
```js
+ import { reqGoodsList } from '../../../../../api/good'
Page({
// 页面的初始数据
data: {
goodsList: [], // 商品列表数据
+ total: 0, // 数据总条数
isFinish: false, // 判断数据是否加载完毕
// 接口请求参数
requestData: {
page: 1, // 页码
limit: 10, // 每页请求多少条数据
category1Id: '', // 一级分类 id
category2Id: '' // 二级分类 id
}
},
// 获取商品列表的数据
async getGoodsList() {
// 调用 API 获取数据
const { data } = await reqGoodsList(this.data.requestData)
// 将返回的数据赋值给 data 中的变量
this.setData({
goodsList: data.records,
total: data.total
})
},
// 生命周期函数--监听页面加载
onLoad(options) {
// 接收传递的参数
Object.assign(this.data.requestData, options)
// 获取商品列表的数据
this.getGoodsList()
}
})
```
/modules/goodsModule/pages/list/list.wxml`
```html
<view class="container">
<!-- 商品列表功能 -->
<view class="goods-list" wx:if="{{ goodsList.length }}">
<block wx:for="{{ goodsList }}" wx:key="id">
<goods-card goodItem="{{ item }}"></goods-card>
</block>
<!-- 数据是否加载完毕 -->
<view class="finish" hidden="{{ !isFinish }}">数据加载完毕~~~</view>
</view>
<!-- 商品为空的时候展示的结构 -->
<van-empty wx:else description="该分类下暂无商品,去看看其他商品吧~">
<van-button round type="danger" class="bottom-button" bindtap="gotoBack">
查看其他商品
</van-button>
</van-empty>
</view>
5.商品列表-实现上拉加载更多功能
思路与步骤:
当用户从下向上滑动屏幕时,需要加载更多的商品数据。
首先需要在 `.js` 文件中声明 `onReachBottom` 方法监听用户是否进行了上拉
当用户上拉时,需要对 `page` 页码进行加 1,代表要请求下一页的数据
当参数发生改变后,需要重新发送请求,拿最新的 `page` 向服务器发送请求获取数据。
在下一页的商品数据返回以后,将最新的数据和之前的数据进行合并
实现步骤:
1. `list.js` 文件中声明 `onReachBottom` 事件处理函数,监听用户的上拉行为
2. 在 `onReachBottom` 函数中加 `page` 进行加 1 的操作,同时发送请求获取下一页数据
3. 在 `getGoodsList` 函数中,实现参数的合并
代码:
/modules/goodsModule/pages/list/list.js`
```js
import { reqGoodsList } from '../../../api/goods'
Page({
// coding...
// 获取商品列表的数据
async getGoodsList() {
// 调用 API 获取数据
const { data } = await reqGoodsList(this.data.params)
// 将返回的数据赋值给 data 中的变量
this.setData({
goodsList: [...this.data.goodsList, ...data.records],
total: data.total
})
},
// coding...
// 监听页面的上拉操作
onReachBottom() {
let { page } = this.data.requestData
// 页码 + 1
this.setData({
requestData: { ...this.data.requestData, page: page + 1 }
})
// 重新发送请求
this.getGoodsList()
}
})
6.商品列表-判断数据是否加载完毕
步骤:
1. 在数据返回以后,将数据中的 `total` 赋值给 `data` 中的变量 `total`
2. 在 `onReachBottom` 中进行 `total` 和 `goodsList` 进行对比
3. 模板中使用 `total` 和 `goodsList` 进行对比
代码:
/modules/goodsModule/pages/list/list.js`
```js
import { reqGoodsList } from '../../../api/goods'
Page({
// coding...
// 监听页面的上拉操作
onReachBottom() {
+ // 从 data 中解构数据
+ const { total, goodsList, requestData } = this.data
+ let { page } = requestData
+
+ // 判断数据是否加载完毕
+ if (total === goodsList.length) {
+ // 如果相等,数据数据加载完毕
+ // 如果数据加载完毕,需要给用户提示,同时不继续加载下一个数据
+ this.setData({
+ isFinish: true
+ })
+
+ return
+ }
// 页码 + 1
this.setData({
requestData: { ...this.data.requestData, page: (page += 1) }
})
// 重新发送请求
this.getGoodsList()
}
})
7. 商品列表-节流阀进行列表节流
注意事项:
在用户网速很慢的情况下,如果用户在距离底部来回的进行多次滑动,可能会发送一些无意义的请求、造成请求浪费的情况,因此需要给上拉加载添加节流功能。
我们使用节流阀来给商品列表添加节流功能。
在 `data` 中定义节流阀状态 `isLoading`,默认值是 `false`。
在请求发送之前,将 `isLoading` 设置为 `true`,表示请求正在发送。
在请求结束以后,将 `isLoading` 设置为 `false`,表示请求已经完成。
在 `onReachBottom` 事件监听函数中,对 `isLoading` 进行判断,如果数据正在请求中,不请求下一页的数据。
代码:
/modules/goodsModule/pages/list/list.js
```js
import { reqGoodsList } from '../../../../../api/good
Page({
// 页面的初始数据
data: {
goodsList: [], // 商品列表数据
isFinish: false, // 判断数据是否加载完毕
isLoading: false, // 判断数据是否记载完毕
total: 0, // 列表总数据量
// 接口请求参数
requestData: {
page: 1, // 页码
limit: 10, // 每页请求多少条数据
category1Id: '', // 一级分类 id
category2Id: '' // 二级分类 id
}
},
// 获取商品列表的数据
async getGoodsList() {
// 数据真正请求中
this.data.isLoading = true
// 调用 API 获取数据
const { data } = await reqGoodsList(this.data.requestData)
// 数据加载完毕
this.data.isLoading = false
// 将返回的数据赋值给 data 中的变量
this.setData({
goodsList: [...this.data.goodsList, ...data.records],
total: data.total
})
},
// 监听页面的上拉操作
onReachBottom() {
// 从 data 中解构数据
const { total, goodsList, requestData, isLoading } = this.data
let { page } = requestData
// 判断是否加载完毕,如果 isLoading 等于 true
// 说明数据还没有加载完毕,不加载下一页数据
if (isLoading) return
// 判断数据是否加载完毕
// coding...
}
})
8.商品列表-实现下拉刷新功能
步骤:
1. 在`页面.json` 中开启允许下拉,同时可以配置 窗口、loading 样式等
2. 在`页面.js` 中定义 `onPullDownRefresh` 事件监听用户下拉刷新
代码:
/modules/goodsModule/pages/list/list.json
```json
{
"usingComponents": {
"goods-card": "/components/goods-card/goods-card"
},
"navigationBarTitleText": "商品列表",
"enablePullDownRefresh": true,
"backgroundColor": "#f7f4f8",
"backgroundTextStyle": "dark"
}
/modules/goodsModule/pages/list/list.js
```js
// 监听页面的下拉刷新
onPullDownRefresh() {
// 将数据进行重置
this.setData({
goodsList: [],
total: 0,
isFinish: false,
requestData: { ...this.data.requestData, page: 1 }
})
// 重新获取列表数据
this.getGoodsList()
}
9.商品详情-获取并渲染商品详情
步骤与思路:
点击首页轮播图以及点击商品列表商品的时候,需要跳转到商品详情页面
在跳转时将商品的`id` 传递到了商品详情页面,只需要使用 `id` 向后端服务器请求数据,获取对应商品的详情数据
在获取到数据以后,使用后端返回的数据对页面进行渲染。
实现步骤:
1. 在 `/pages/goods/detail/detail.js` 中导入封装好的获取商品列表的 `API` 函数
2. 页面数据在页面加载的时候进行调用,在 `onLoad` 钩子函数中调用 `reqGoodsInfo` 方法
3. 在获取到数据以后,使用后端返回的数据对页面进行渲染
代码:
/modules/goodsModule/pages/detail/detail.js`
```js
import { reqGoodsInfo } from '../../../api/goods'
Page({
// 页面的初始数据
data: {
goodsInfo: {}, // 商品详情
show: false, // 控制加入购物车和立即购买弹框的显示
count: 1, // 商品购买数量,默认是 1
blessing: '' // 祝福语
},
// 获取商品的详情
async getGoodsInfo() {
// 调用接口、传入参数、获取商品详情
const { data: goodsInfo } = await reqGoodsInfo(this.goodsId)
// 将商品详情数据赋值给 data 中的变量
this.setData({
goodsInfo
})
},
// 生命周期函数--监听页面加载
onLoad(options) {
// 将商品 id 挂载到页面实例上
this.goodsId = options.goodsId ? options.goodsId : ''
// 获取商品详情的数据
this.getGoodsInfo()
}
// coding...
})
```
/modules/goodsModule/pages/detail/detail.html`
```html
<view class="container goods-detail">
<!-- 商品大图 -->
<view class="banner-img">
<image class="img" src="{{ goodsInfo.imageUrl }}" />
</view>
<!-- 商品的基本信息 -->
<view class="content">
<view class="price">
<view class="price-num">¥{{ goodsInfo.price }}</view>
<view class="price-origin-num">¥{{ goodsInfo.marketPrice }}</view>
</view>
<view class="title">{{ goodsInfo.name }}</view>
<view class="desc">{{ goodsInfo.material }}</view>
</view>
<!-- 商品的详细信息 -->
<view class="detail">
<image
wx:for="{{ goodsInfo.detailList}}"
wx:key="index"
src="{{ item }}"
class="img"
mode="widthFix"
/>
</view>
<!-- 商品的底部商品导航 -->
<van-goods-action>
<!-- 代码略... -->
</van-goods-action>
<!-- 加入购物车、立即购买弹框 -->
<!-- show 控制弹框的隐藏和展示 -->
<!-- bind:close 点击关闭弹框时触发的回调 -->
<van-action-sheet show="{{ show }}" bind:close="onClose">
<view class="sheet-wrapper">
<view class="goods-item">
<!-- 需要购买的商品图片 -->
<view class="mid">
<image class="img" src="{{ goodsInfo.imageUrl }}" />
</view>
<!-- 商品基本信息 -->
<view class="right">
<!-- 商品名字 -->
<view class="title"> {{ goodsInfo.name }} </view>
<!-- 商品价格 -->
<view class="buy">
<view class="price">
<view class="symbol">¥</view>
<view class="num">{{ goodsInfo.price }}</view>
</view>
<!-- 购买数量弹框 -->
<view class="buy-btn">
<!-- Stepper 步进器,由增加按钮、减少按钮和输入框组成,控制购买数量 -->
<van-stepper value="{{ count }}" bind:change="onChangeGoodsCount" />
</view>
</view>
</view>
</view>
<!-- 祝福语输入框 -->
<view class="time-wraper">
<!-- 代码略... -->
</view>
<!-- 取消、确定弹框 -->
<view class="sheet-footer-btn">
<van-button block type="primary" round> 确定 </van-button>
</view>
</view>
</van-action-sheet>
</view>
10. 商品详情-详情图片预览功能
思路与步骤:
当点击商品的图片时,需要将图片进行全屏预览
如果想实现该功能,需要使用小程序提供的 `API`:`wx.previewImage()`,用来在新页面中全屏预览图片。预览的过程中用户可以进行保存图片、发送给朋友等操作。语法如下:
```js
wx.previewImage({
current: '', // 当前显示图片的 http 链接
urls: [] // 需要预览的图片 http 链接列表
})
```
实现步骤:
1. 给展示大图的 `image` 组件绑定点击事件,同时通过自定义属性的方式,传递当前需要显示的图片http 链接
2. 同时商品详情的数组数据传递给 `urls` 数组即可
代码:
/pages/goods/detail/detail.html`
```html
<!-- 商品大图 -->
<view class="banner-img">
<image
class="img"
src="{{ goodsInfo.imageUrl }}"
bindtap="previewImg"
/>
</view>
```
/pages/goods/detail/detail.js`
```js
// 预览商品图片
previewImg() {
// 调用预览图片的 API
wx.previewImage({
urls: this.data.goodsInfo.detailList
})
}
11.样式图
五、购物车
1.购物车-封装购物车接口 API
思路:
为了方便后续进行购物车模块的开发,我们在这一节将购物车所有的接口封装成接口 API 函数
代码:
```js
import http from '../utils/http'
/**
* @description 获取购物车列表数据
* @returns Promise
*/
export const reqCartList = () => {
return http.get('/mall-api/cart/getCartList')
}
/**
* @description 加入购物车
* @param {*} data
* @returns Promise
*/
export const reqAddCart = (data) => {
return http.get(`/cart/addToCart/${data.goodsId}/${data.count}`, data)
}
/**
* @description 更新商品的选中状态
* @param {*} goodsId 商品 id
* @param {*} isChecked 商品的选中状态
* @returns Promise
*/
export const reqUpdateChecked = (goodsId, isChecked) => {
return http.get(`/cart/checkCart/${goodsId}/${isChecked}`)
}
/**
* @description 全选和全不选
* @param {*} isChecked 商品的选中状态
* @returns Promise
*/
export const reqCheckAllCart = (isChecked) => {
return http.get(`/cart/checkAllCart/${isChecked}`)
}
/**
* @description 删除购物车商品
* @param {*} goodsId 商品 id
* @returns Promise
*/
export const reqDelCart = (goodsId) => {
return http.get(`/cart/delete/${goodsId}`)
}
2.加入购物车-模板分析和渲染
业务介绍:
点击加入购物车和立即购买的时候,展示购物弹框,在弹框中需要用户选择购买数量和祝福语
点击加入购物车和立即购买,触发的是同一个弹框
因此点击弹框中的确定按钮时,我们需要区分当前是加入购物车操作还是立即购买操作。
这时候定义一个状态 `buyNow` 做区分,`buyNow` 等于 1 代表是立即购买,否则是加入购物车
产品需求:
1. 如果点击的是加入购物车,需要将当前商品加入到购物车
2. 如果点击的是立即购买,需要跳转到结算支付页面,立即购买该商品
3. 如果是立即购买,不支持购买多个商品
代码:
点击立即购买和加入购物车的时候,通过 show 属性,控制弹框的隐藏和展示
```html
<!-- 商品的底部商品导航 -->
<van-goods-action>
<!-- coding... -->
<van-goods-action-button text="加入购物车" type="warning" bindtap="handleAddcart" />
<van-goods-action-button text="立即购买" bindtap="handeGotoBuy" />
</van-goods-action>
<!-- 加入购物车、立即购买弹框 -->
<!-- show 控制弹框的隐藏和展示 -->
<!-- bind:close 点击关闭弹框时触发的回调 -->
<van-action-sheet show="{{ show }}" bind:close="onClose">
<view class="sheet-wrapper">
<!-- 代码略... -->
<!-- 购买数量弹框 -->
<view class="buy-btn" wx:if="{{ buyNow === 0 }}">
<!-- Stepper 步进器,由增加按钮、减少按钮和输入框组成,控制购买数量 -->
<van-stepper value="{{ count }}" bind:change="onChangeGoodsCount" />
</view>
<!-- 代码略... -->
</view>
</van-action-sheet>
```
点击立即购买和加入购物车的时候,通过 buyNow 属性,来区分是进行的某种操作
```js
Page({
// 页面的初始数据
data: {
goodsInfo: {}, // 商品详情
show: false, // 加入购物车和立即购买时显示的弹框
count: 1, // 商品购买数量,默认是 1
blessing: '', // 祝福语
buyNow: '' // 是否立即购买
},
// 加入购物车
handleAddcart() {
this.setData({
show: true,
buyNow: 0
})
},
// 立即购买
handeGotoBuy() {
this.setData({
show: true,
buyNow: 1
})
},
3. 加入购物车-关联 Store 对象
思路:
当用户点击加入购物车 或者 立即购买时,需要判断用户是否进行了登录。
我们需要使用 `Token` 进行判断,因此需要让页面和 `Store` 对象建立关联。
这时候可以使用 `BehaviorWithStore` 让页面 和 `Store` 对象建立关联。
代码:
/behaviors/userBehavior.js
```js
// 导入 BehaviorWithStore 让页面和 Store 对象建立关联
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
// 导入用户 Store
import { userStore } from '@/stores/userstore'
export const userBehavior = BehaviorWithStore({
storeBindings: {
store: userStore,
fields: ['token']
}
})
```
/behaviors/userBehavior.js`
```js
import { reqGoodsInfo } from '@/api/goods'
import { reqAddCart } from '@/api/cart'
+ import { userBehavior } from '@/behaviors/userBehavior'
Page({
behaviors: [userBehavior],
})
4.加入购物车和立即购买区分处理
思路与步骤:
击加入购物车以及立即购买以后,需要先判断是否进行了登录,如果用户没有登录过,需要先跳转到登录页面进行登录。
如果点击的是 加入购物车,我们只需要调用 [加入购物车](https://apifox.com/apidoc/shared-6ed6c5c4-56c4-4619-8e2a-4817aa140e30/api-136581098) 接口即可 (需要获取商品的 ID 、购买数量、祝福语)
如果点击的是 立即购买,我们需要携带参数跳转到商品结算页面 (获取商品的 ID 以及 祝福语跳转到结算页面)
实现步骤:
1. 给 `Stepper` 步进器组件,通过`value`设置输入值,同时绑定`change`事件,并将值同步到 `data` 中
2. 根据接口文档,导入封装的购物车的接口 API
3. 点击弹框按钮的时候,判断点击的加入购物车还是立即购买,执行不同的操作
/modules/goodsModule/pages/detail/detail.html
```html
<van-stepper
value="{{ count }}"
+ integer
+ min="1"
+ max="200"
bind:change="onChangeGoodsCount"
/>
```
/modules/goodsModule/pages/detail/detail.js
```js
// 监听是否更改了购买数量
onChangeGoodsCount(event) {
// 将最新的购买数量同步到 data
this.setData({
count: Number(event.detail)
})
},
// 弹框的确定按钮
async handleSubmit() {
// 解构获取数据
const { token, count, blessing, buyNow } = this.data
const goodsId = this.goodsId
// 如果没有 token ,让用户新登录
if (!this.data.token) {
wx.navigateTo({
url: '/pages/login/login'
})
return
}
// 将用户输入的值转成 Number 类型
const count = Number(event.detail)
// 验证购买数量的正则
const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
// 使用正则验证
const res = reg.test(count)
// 如果验证没有通过,直接返回,不执行后续的逻辑
if (!res) return
// 加入购物车
if (buyNow === 0) {
// 加入购物车
const res = await reqAddGood({ goodsId, count, blessing })
if (res.code === 200) {
wx.showToast({
title: '加入购物车成功'
})
this.setData({
show: false
})
}
} else {
// 立即购买
wx.navigateTo({
url: `/pages/order/detail/index?goodsId=${goodsId}&blessing=${blessing}
})
}
}
5. 加入购物车-展示购物车购买数量
思路与步骤:
判断用户是否进行了登录。
如果没有登录过,则不展示购物车商品的数量。
如果用户登录过,则需要展示购物车商品的数量,则获取购物车列表数据,通过累加计算得出商品购买数量
实现步骤:
1. 进入商品详情,调用方法,在方法中判断`token`是否存在
2. 如何存在,则获取购物车列表数据,通过累加计算得出商品购买数量,展示购买的数量
3. 不存在,不执行任何逻辑
代码:
/modules/goodsModule/pages/detail/detail.js
```javascript
Page({
data: {
// coding...
allCount: '' // 购物车商品总数量
},
// 弹框的确定按钮
async handleSubmit() {
// 如果没有 token ,让用户新登录
if (!this.data.token) {
wx.navigateTo({
url: '/pages/login/login'
})
return
}
// 解构获取数据
const { count, blessing, allCount } = this.data
const goodsId = this.goodsId
// 加入购物车
if (this.data.buyNow === 0) {
// 加入购物车
const res = await reqAddCart({ goodsId, count, blessing })
if (res.code === 200) {
wx.toast({
title: '加入购物车成功',
icon: 'success',
mask: false
})
// 购物车购买数量合计
this.getCartCount()
this.setData({
show: false
})
}
} else {
// 立即购买
wx.navigateTo({
url: `/pages/order/detail/detail?goodsId=${goodsId}&blessing=${blessing}
})
}
},
// 计算购买数量
async getCartCount() {
// 如果没有 token ,说明用户是第一次访问小程序,没有进行登录过
if (!this.data.token) return
// 获取购物的商品
const res = await reqCartList()
if (res.data.length !== 0) {
// 购物车商品累加
let allCount = 0
// 获取购物车商品数量
res.data.forEach((item) => {
allCount += item.count
})
// 将购物车购买数量赋值
this.setData({
// 展示的数据要求是字符串
allCount: (allCount > 99 ? '99+' : allCount) + ''
})
}
},
onLoad(options) {
// 接收传递的商品 ID,并且将 商品 ID 挂载到 this 上面
this.goodsId = options.goodsId
// 调用获取商品详情数据的方法
this.getGoodsInfo()
// 计算购买数量
this.getCartCount()
}
// coding...
})
6. 购物车-购物车关联 Store 对象
思路:
当用户进入购物车页面时时,需要判断用户是否进行了登录来控制页面的展示效果
这时候我们就需要使用 `Token` 进行判断,因此需要让页面和 `Store` 对象建立关联。
因为购物车页面采用的 `Component` 方法进行构建
这时候可以使用 `ComponentWithStore` 让页面 和 `Store` 对象建立关联。
代码:
/pages/cart/components/cart.js
```js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
import { reqCartList } from '@/api/cart'
ComponentWithStore({
storeBindings: {
store: userStore,
fields: ['token']
},
// 组件的初始数据
data: {
cartList: [],
emptyDes: '还没有添加商品,快去添加吧'
},
// 组件的方法列表
methods: {
// 处理页面的展示
async showTipList() {
// 将 token 进行解构
const { token } = this.data
console.log(token)
},
onShow() {
this.showTipList()
}
}
})
7. 购物车-获取并渲染购物车列表
思路与步骤:
1. 如果没有进行登录,购物车页面需要展示文案:`您尚未登录,点击登录获取更多权益`
2. 如果用户进行登录,获取购物车列表数据
购物车没有商品,展示文案: `还没有添加商品,快去添加吧~`
购物车列表有数据,需要使用数据对页面进行渲染
实现步骤:
1. 导入封装好的获取列表数据的 `API` 函数
2. 在 `onShow` 钩子中,根据产品的需求,处理页面的提示
3. 在获取到数据以后,使用后端返回的数据对页面进行渲染
代码:
/pages/cart/cart.js`
```js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
import { reqCartList } from '@/api/cart'
ComponentWithStore({
storeBindings: {
store: userStore,
fields: ['token']
},
// 组件的初始数据
data: {
cartList: [],
emptyDes: '还没有添加商品,快去添加吧~'
},
// 组件的方法列表
methods: {
// 获取购物车列表数据 + 处理页面的展示
async showTipGetList() {
// 将 token 进行解构
const { token } = this.data
// 1. 如果没有登录,购物车列表,展示文案:您尚未登录,点击登录获取更多权益
if (!token) {
this.setData({
emptyDes: '您尚未登录,点击登录获取更多权益',
cartList: []
})
return
}
// 获取商品列表数据
const { data: cartList, code } = await reqCartList()
if (code === 200) {
// 2. 如果用户登录,购物车列表为空,展示文案: 还没有添加商品,快去添加吧~
this.setData({
cartList,
emptyDes: cartList === 0 && '还没有添加商品,快去添加吧~'
})
}
},
// 页面展示时触发
onShow() {
this.showTipGetList()
}
}
})
```
`➡️/pages/cart/components/cart.wxml`
```html
<view>
<view
wx:if="{{ token && cartList.length }}"
class="container goods-wrap"
bindtap="onSwipeCellPageTap"
>
<view class="cart-wrap">
<view class="goods-item" wx:for="{{ cartList }}" wx:key="id">
<van-swipe-cell class="goods-swipe" right-width="{{ 65 }}">
<view class="goods-info">
<view class="left">
<van-checkbox
checked-color="#FA4126"
value="{{ item.checked }}"
></van-checkbox>
</view>
<view class="mid">
<image class="img" src="{{ item.imageUrl }}" />
</view>
<view class="right">
<view class="title"> {{ item.name }} </view>
<view class="buy">
<view class="price">
<view class="symbol">¥</view>
<view class="num">{{ item.price }}</view>
</view>
<view class="buy-btn">
<van-stepper value="{{ item.count }}" />
</view>
</view>
</view>
</view>
<view slot="right" class="van-swipe-cell__right">删除</view>
</van-swipe-cell>
</view>
</view>
<!-- 底部工具栏 -->
<van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
<van-checkbox value="{{ true }}" checked-color="#FA4126"> 全选 </van-checkbox>
</van-submit-bar>
</view>
<van-empty wx:else description="{{ emptyDes }}">
<navigator url="/pages/index/index" wx:if="{{ token }}">
<van-button round type="danger" class="bottom-button">去购物</van-button>
</navigator>
<navigator url="/pages/login/login" wx:else>
<van-button round type="danger" class="bottom-button">去登录</van-button>
</navigator>
</van-empty>
</view>
8. 购物车-更新商品的购买状态
思路与步骤:
点击商品的复选框时,更新商品的购买状态。
1. 获取商品最新的购买状态,将最新的状态同步到服务器(需要调用封装的接口 API 函数,0 不购买,1 购买)
2. 在服务器更新状态更新成功以后,将本地的数据一并改变。
实现步骤:
1. 导入封装好的获取列表数据的 `API` 函数
2. 当点击切换切换商品状态的时候,调用 `reqUpdateGoodStatus`,并传参
3. 在更新成功,将本地的数据一并改变。
/pages/cart/cart.wxml
```html
<van-checkbox
checked-color="#FA4126"
value="{{ item.isChecked }}"
bind:change="updateChecked"
data-id="{{ item.goodsId }}"
data-index="{{ index }}"
></van-checkbox>
```
/pages/cart/cart.js
```js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
import { reqCartList, reqUpdateChecked } from '@/api/cart'
Component({
// coding...
// 组件的方法列表
methods: {
// 切换商品的选中状态
async updateChecked(event) {
// 获取最新的选中状态
const { detail } = event
// 获取商品的索引和 id
const { id, index } = event.target.dataset
// 将最新的状态格式化成后端所需要的数据格式
const isChecked = detail ? 1 : 0
// 调用接口,传入参数,更新商品的状态
const res = await reqUpdateChecked(id, isChecked)
// 如果数据更新成功,需要将本地的数据一同改变
if (res.code === 200) {
this.setData({
[`cartList[${index}].isChecked`]: isChecked
})
}
},
// 获取购物车列表数据
async getCartList() {
// coding...
}
}
})
9. 购物车-控制全选按钮的选中状态
思路与步骤:
购物车列表中每个商品的状态 `isCheckd` 都是 1,说明每个商品都需要进行购买。
这时候就需要控制底部工具栏全选按钮的选中效果。
基于购物车列表中已有的数据,产生一个新的数据,来控制全选按钮的选中效果,可以使用 计算属性 来实现。
安装 框架拓展 `computed`
cmd
# 安装并构建 框架拓展 computed
npm i miniprogram-computed
步骤:
1. 在 `cart` 组件中引入 `miniprogram-computed` ,然后再 `behaviors` 中进行注册
2. 新建 `computed` 配置项,新增 `allStatus` 函数用来判断是否是全选
代码:
/pages/cart/cart.js
```js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
import { reqCartList, reqUpdateChecked } from '@/api/cart'
const computedBehavior = require('miniprogram-computed').behavior
ComponentWithStore({
// 注册计算属性
behaviors: [computedBehavior],
computed: {
// 判断是否全选
// computed 函数中不能访问 this ,只有 data 对象可供访问
// 这个函数的返回值会被设置到 this.data.selectAllStatus 字段中
selectAllStatus(data) {
return (
data.cartList.length !== 0 && data.cartList.every((item) => item.isChecked === 1)
)
}
}
// 其他代码略...
})
```
pages/cart/cart.wxml
```html
<!-- 底部工具栏 -->
<van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
<van-checkbox value="{{ selectAllStatus }}" checked-color="#FA4126">
全选
</van-checkbox>
</van-submit-bar>
10. 购物车-实现全选和全不选功能
思路与步骤:
点击全选,控制所有商品的选中与全不选效果
1. 点击全选按钮,获取全选按钮的选中状态(true, false),同时控制所有商品的选中与全不选效果
2. 在获取到全选按钮状态以后,同时需要将状态同步给服务器 (1 是全选,0 是全不选)
3. 在服务器更新成功以后,需要将本地的购物车商品选中状态也进行改变
实现步骤:1. 导入封装好的全选的 `API` 函数
2. 当点击全选和全不选按钮的时候,调用 `reqCheckAll`,并传参
3. 在更新成功,将本地的数据一并改变。
代码:
/pages/cart/cart.wxml
```html
<!-- 底部工具栏 -->
<van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
<van-checkbox
value="{{ selectAllStatus }}"
checked-color="#FA4126"
bind:change="changeAllStatus"
>
全选
</van-checkbox>
</van-submit-bar>
```
/pages/cart/cart.js
```js
ComponentWithStore({
// coding...
methods: {
// coding...
// 全选和全不选功能
async updateAllStatus(event) {
// 获取全选和全不选的状态
const isChecked = event.detail ? 1 : 0
// 调用接口,更新服务器中商品的状态
const res = await reqCheckAllStatus(isChecked)
// 如果更新成功,需要将本地的数据一同改变
if (res.code === 200) {
// 将数据进行拷贝
const newCart = JSON.parse(JSON.stringify(this.data.cartList))
// 将数据进行更改
newCart.forEach((item) => (item.isChecked = isChecked))
// 进行赋值
this.setData({
cartList: newCart
})
}
},
// coding...
}
})
11. 购物车-更新商品购买数量思路分析
思路:
在输入框中输入购买的数量,并**`不是直接将输入的数量同步给服务器,而是需要计算差值`**,服务器端进行处理
注意事项:
更新购买数量 和 加入购物车,使用的是同一个接口,为什么加入购物车没有计算差值,
这是因为在加入购物车以后,服务器对商品购买数量直接进行了累加。
例如:之前购物车添加了某个商品,购买数量是 1 个,商品详情又加入 1 个, 直接累加,在购物车显示购买 2 个
12. 购物车-更新商品的购买数量
思路与步骤:
1. 必须是正整数,最小是`1`,最大是`200`
2. 如果输入的值大于`200`,输入框购买数量需要重置为`200`
3. 输入的值不合法或者小于`1`,还原为之前的购买数量
```js
const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
```
实现步骤:
1. 给输入框绑定监听值是否改变的事件,同时传递商品的 ID `id` 和 商品的购买之前的购买数量 `num`
2. 在事件处理程序中获取到最新的数据,然后进行差值的运算
3. 发送请求即可
代码:
/pages/cart/cart.wxml`
```html
<van-stepper
+ integer
+ min="1"
+ max="200"
value="{{ item.count }}"
+ data-id="{{ item.goodsId }}"
+ data-oldbuynum="{{ item.count }}"
+ data-index="{{ index }}"
+ bindchange="changeBuyNum"
/>
```
`➡️ /pages/cart/cart.js`
```js
// 更新购买的数量
async changeBuyNum(event) {
// 获取最新的购买数量,
// 如果用户输入的值大于 200,购买数量需要重置为 200
// 如果不大于 200,直接返回用户输入的值
let buynum = event.detail > 200 ? 200 : event.detail
// 获取商品的 ID 和 索引
const { id: goodsId, index, oldbuynum } = event.target.dataset
// 验证用户输入的值,是否是 1 ~ 200 直接的正整数
const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
// 对用户输入的值进行验证
const regRes = reg.test(buynum)
// 如果验证没有通过,需要重置为之前的购买数量
if (!regRes) {
this.setData({
[`cartList[${index}].count`]: oldbuynum
})
return
}
// 如果通过,需要计算差值,然后将差值发送给服务器,让服务器进行逻辑处理
const disCount = buynum - oldbuynum
// 如果购买数量没有发生改变,不发送请求
if (disCount === 0) return
// 发送请求:购买的数量 和 差值
const res = await reqAddCart({ goodsId, count: disCount })
// 服务器更新购买数量成功以后,更新本地的数据
if (res.code === 200) {
this.setData({
[`cartList[${index}].count`]: buynum
})
}
}
13. 购物车-更新商品购买数量防抖
思路与步骤:
每次改变购物车购买数量的时候,都会触发 `changeBuyNum` 事件处理程序,这会频繁的向后端发送请求,给服务器造成压力
我们希望用户在输入最终的购买数量,或者停止频繁点击加、减的以后在发送请求,在将购买数量同步到服务器。
这时候就需要使用 **防抖** 来进行代码优化。
`Licia` 是实用 `JavaScript` 工具库,该库目前拥有超过 400 个模块,同时支持浏览器、node 及小程序运行环境。可以极大地提高开发效率。
[licia 官网](https://licia.liriliri.io/)
[licia 中文使用文档](https://github.com/liriliri/licia/blob/HEAD/README_CN.md)
代码:
/pages/cart/cart.js`
```js
// 从 miniprogram-licia 导入防抖函数
import { debounce } from 'miniprogram-licia'
// 更新购买的数量
changeBuyNum: debounce(async function (event) {
// 代码略...
}, 500)
14. 购物车-购物车商品合计
步骤与思路:
在订单提交栏位置,展示要购买商品的总金额。
需要判断购物车中哪些商品被勾选,然后将勾选商品的价格进行累加。
当用户更新了商品的状态,或者更新了商品的购买数量,我们都需要重新计算订单总金额。
我们需要基于购物车列表的数据,产生订单总金额,在这里我们使用依然使用 computed 来实现商品合计的功能
实现步骤:
1. 在 `computed` 配置项,新增 `totalPrice` 函数用来计算商品价格总和
代码:
/pages/cart/cart.wxml
```html
<!-- 底部工具栏 -->
<van-submit-bar
wx:if="{{ cartList.length }}"
price="{{ totalPrice }}"
button-text="去结算"
tip="{{ true }}"
>
<van-checkbox
value="{{ selectAllStatus }}"
checked-color="#FA4126"
bindchange="selectAllStatus"
>
全选
</van-checkbox>
</van-submit-bar>
```
/pages/cart/cart.js
```js
ComponentWithStore({
// coding...
// 定义计算属性
computed: {
// coding...
// 计算商品价格总和
totalPrice(data) {
let totalPrice = 0
data.cartList.forEach((item) => {
// 如果商品的 isChecked 属性等于,说明该商品被选中的
if (item.isChecked === 1) {
totalPrice += item.count * item.price
}
})
return totalPrice
}
},
// coding...
})
15. 购物车-删除购物车中的商品
步骤与思路:
点击删除按钮的时候,需要将对应的购物车商品进行删除
实现步骤:
1. 导入封装的接口 `API` 函数,同时导入处理删除自动关闭效果的 `behaviors` 并进行注册
2. 在点击删除以后,调用 `API` 函数,在删除购物车商品成功以后,给用户提示
代码:
/pages/cart/components/cart.wxml
```html
<view bindtap="onSwipeCellPage">
<!-- 代码略 -->
<van-swipe-cell
class="goods-swipe"
right-width="{{ 65 }}"
id="swipe-cell-{{ item.goodsId }}"
bind:open="swipeCellOpen"
bind:click="onSwipeCellClick"
>
<van-cell-group border="{{ false }}">
<view class="goods-info">
<view class="left">
<van-checkbox
checked-color="#FA4126"
value="{{ item.isChecked }}"
bindchange="updateChecked"
data-id="{{ item.goodsId }}"
data-index="{{ index }}"
></van-checkbox>
</view>
<view class="mid">
<image class="img" src="{{ item.imageUrl }}" />
</view>
<view class="right">
<view class="title"> {{ item.name }} </view>
<view class="buy">
<view class="price">
<view class="symbol">¥</view>
<view class="num">{{ item.price }}</view>
</view>
<view class="buy-btn">
<van-stepper
min="1"
max="200"
integer
value="{{ item.count }}"
data-id="{{ item.goodsId }}"
data-index="{{ index }}"
data-oldbuynum="{{ item.count }}"
bindchange="changeBuyNum"
/>
</view>
</view>
</view>
</view>
</van-cell-group>
<view
slot="right"
class="van-swipe-cell__right"
bindtap="delCartGoods"
data-id="{{ item.goodsId }}"
>
删除
</view>
</van-swipe-cell>
<!-- 代码略 -->
</view>
/pages/cart/components/cart.wxml
```js
// 导入接口 API 函数
import {
reqCartList,
reqUpdateChecked,
reqCheckAllStatus,
reqAddCart,
reqDelCartGoods
} from '@/api/cart'
// 导入让删除滑块自动弹回的 behavior
import { swipeCellBehavior } from '@/behaviors/swipeCell'
ComponentWithStore({
// 注册 behavior
behaviors: [swipeCellBehavior, computedBehavior],
// 组件的方法列表
methods: {
// coding...
// 删除购物车中的商品
async delCartGoods(event) {
// 获取需要删除商品的 id
const { id } = event.currentTarget.dataset
// 询问用户是否删除该商品
const modalRes = await wx.modal({
content: '您确认删除该商品吗 ?'
})
if (modalRes) {
await reqDelCartGoods(id)
this.showTipGetList()
}
},
onHide() {
// 在页面隐藏的时候,需要让删除滑块自动弹回
this.onSwipeCellCommonClick()
}
}
})
六、结算支付
1.配置分包并跳转到结算页面
思路:
随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。
因此我们需要将 `结算支付` 功能配置成一个分包,
当用户在访问设置页面时,还预先加载 `结算支付` 所在的分包
代码:
app.json
```json
"subPackages": [
{
"root": "modules/settingModule",
"name": "settingModule",
"pages": [
"pages/address/add/index",
"pages/address/list/index",
"pages/profile/profile"
]
},
{
"root": "modules/goodModule",
"name": "goodModule",
"pages": ["pages/goods/list/list", "pages/goods/detail/detail"]
},
{
"root": "modules/orderPayModule",
"name": "orderPayModule",
"pages": [
"pages/order/detail/detail",
"pages/order/list/list"
]
}
],
"preloadRule": {
"pages/settings/settings": {
"network": "all",
"packages": ["settingModule"]
},
"pages/category/category": {
"network": "all",
"packages": ["goodModule"]
},
+ "pages/cart/cart": {
+ "network": "all",
+ "packages": ["orderPayModule"]
+ }
}
pages/cart/cart.js
```js
// 跳转到订单结算页面
toOrder() {
if (this.data.totalPrice === 0) {
wx.toast({
title: '请选择需要购买的商品'
})
return
}
// 跳转到订单的结算页面
wx.navigateTo({
url: '/modules/orderPayModule/pages/order/detail/detail'
})
}
pages/cart/cart.wxml
```html
<van-submit-bar
wx:if="{{ cartList.length }}"
price="{{ totalPrice * 100 }}"
button-text="去结算"
tip="{{ true }}"
bindsubmit="toOrder"
>
<van-checkbox
value="{{ selectAllStatus }}"
checked-color="#FA4126"
bindchange="selectAllStatus"
>
全选
</van-checkbox>
</van-submit-bar>
2.封装结算支付的接口 API
思路:
为了方便后续进行结算支付模块的开发,我们在这一节将结算支付所有的接口封装成接口 API 函数
代码:
/api/orderpay.js
```js
import http from '@/utils/http'
/**
* @description 获取订单详情
* @returns Promise
*/
export const reqOrderInfo = () => {
return http.get('/order/trade')
}
/**
* @description 获取订单列表
* @param {*} page 页码
* @param {*} limit 每页展示的条数
* @returns Promise
*/
export const reqOrderList = (page, limit) => {
return http.get(`/order/order/${page}/${limit}`)
}
/**
* @description 获取订单收货地址
* @returns Promise
*/
export const reqOrderAddress = () => {
return http.get('/userAddress/getOrderAddress')
}
/**
* @description 获取立即购买商品的详情信息
* @param { Object } params { goodsId: 商品 Id, blessing:祝福语 }
* @returns Promise
*/
export const reqBuyNowGoods = ({ goodsId, ...data }) => {
return http.get(`/order/buy/${goodsId}`, data)
}
/**
* @description 提交订单
* @returns Promise
*/
export const reqSubmitOrder = () => {
return http.post('/order/submitOrder')
}
/**
* @description 获取微信预支付信息
* @param {*} orderNo 订单 ID
* @returns Promise
*/
export const reqPreBuyInfo = (orderNo) => {
return http.get(`/webChat/createJsapi/${orderNo}`)
}
/**
* @description 微信支付状态查询
* @param {*} orderNo
* @returns Promise
*/
export const reqPayStatus = (orderNo) => {
return http.get(`/webChat/queryPayStatus/${orderNo}`)
}
3.商品结算-获取收货地址
步骤与思路:
进入结算支付页面后,需要获取收货地址信息,在获取到收货地址以后,需要进行判断,
如果没有获取到收货地址,需要展示添加收货地址的结构,
如果获取到了收货地址,需要渲染收货地址。
实现步骤:
1. 在进入结算页面的时候,调用接口 `API` 函数,获取数据
2. 然后根据数据并渲染结构
代码:
/pages/order/detail/index.js`
```js
import { getTradeAddress } from '../../../api/order'
Page({
data: {
// coding...
orderAddress: {} // 收货地址
},
// 获取收货地址
async getAddress() {
const { data: orderAddress } = await reqOrderAddress()
this.setData({
orderAddress
})
},
// 页面展示时触发的钩子函数
onShow() {
this.getAddress()
}
})
```
/pages/order/detail/index.wxml`
```html
<!--pages/order/index.wxml-->
<view class="container order">
<view class="address-card">
<!-- 添加收货地址 -->
<view wx:if="{{ !tradeAddress.id }}" class="add-address" bindtap="toAddress">
<van-icon size="22px" name="add" />
<view>添加收货地址</view>
</view>
<view wx:else class="order-address flex">
<view class="address-content">
<view class="title">{{ tradeAddress.fullAddress }}</view>
<view class="info flex">
<text>{{ tradeAddress.name }}</text>
<text>{{ tradeAddress.phone }}</text>
</view>
</view>
<view class="select-address">
<navigator class="navigator" url="/modules/settingModule/pages/address/list/index">
<van-icon color="#bbb" name="arrow" size="22px" />
</navigator>
</view>
</view>
<view class="top-line"></view>
</view>
<view class="order-info">
<!-- coding... -->
</view>
</view>
4.商品结算-更新收货地址功能
步骤与思路:
当用户需要更改收货地址时,我们需要跳转到收货地址页面,重新选择收货地址
当用户点击了某个地址以后,我们需要将该地址显示到商品结算页面中。
更新收货地址功能,采用 `getApp()` 全局共享数据的方式来实现。
实现步骤:
1. 在 `app.js` 中定义全局共享的数据 `globalData.address`
2. 点击箭头,携带参数跳转到收货地址页面,标识是从订单结算页面进入
3. 在选择收货地址成功以后,将数据存储到 `globalData.address`中,然后返回到订单结算页面。
4. 在订单结算页面判断 `globalData.address` 是否存在收货地址数据,如果存在则渲染
代码:
app.js`
```js
App({
+ // 定义全局共享的数据
+ globalData: {
+ address: {}
+ }
// coding...
})
```
`➡️ /pages/address/list/index.html`
```html
<!-- 每一个收货地址 -->
<view
class="info"
+ bindtap="changeAddress"
+ data-id="{{ item.id }}"
>
<view class="user-info">
<text>{{ item.name }}</text>
<text>{{ item.phone }}</text>
<text wx:if="{{ item.isDefault === 1 }}" class="default-tag">默认</text>
</view>
<view class="address-info"> {{ item.fullAddress }} </view>
</view>
```
`➡️ /pages/address/list/index.js`
```js
// 导入接口 API 函数
import { reqAddressList, reqDelAddress } from '@/api/address'
import { swipeCellBehavior } from '@/behaviors/swipeCell'
+ // 获取全局的应用实例
+ const app = getApp()
Page({
// coding...
// 切换收货地址
changeAddress(event) {
// 判断是否是从订单结算页面进入
if (this.flag !== '1') return
// 获取到点击的收货地址 id
const addressId = event.currentTarget.dataset.id
// 从收货地址列表中获取到获取到点击的收货地址详细信息
const address = this.data.addressList.find((item) => item.id === addressId)
// 如果获取成功,将数据存储到 globalData 中
if (address) {
app.globalData.address = address
wx.navigateBack()
}
},
onLoad(options) {
this.flag = options.flag
}
})
```
`➡️ /pages/order/detail/index.wxml`
```html
<view class="select-address">
<navigator
class="navigator"
url="/modules/settingModule/pages/address/list/index?flag=1"
>
<van-icon color="#bbb" name="arrow" size="22px" />
</navigator>
</view>
```
`➡️ /pages/order/detail/index.js`
```js
// 获取订单页面的收货地址
async getAddress() {
// 如果 globalData 存在收货地址,取出收货地址
if (app.globalData.address.id) {
this.setData({
orderAddress: app.globalData.address
})
// 在赋值以后需要将收货地址清空
app.globalData.address = {}
return
}
// 如果 globalData 中不存在收货地址,获取收货地址渲染即可
const { data: orderAddress } = await reqOrderAddress()
this.setData({
orderAddress
})
},
5.商品结算-获取订单详情数据
步骤:
商品结算页面数据获取收货地址以及商品订单信息
实现步骤:
1. 导入封装的接口 `API` 函数
2. 在进入结算页面的时候,调用接口 `API` 函数,获取数据,然后根据数据并渲染结构即可
代码:
➡️ /pages/order/detail/index.js`
```js
import { reqOrderAddress, reqOrderInfo } from '@/api/orderpay'
Page({
data: {
// coding...
orderAddress: {}, // 收货地址
orderInfo: {}, // 订单商品详情
},
// 获取订单详情
async getOrderInfo() {
const { data: orderInfo } = await reqOrderInfo()
// 判断是否存在祝福语
// 如果需要购买多个商品,挑选第一个填写了祝福语的商品进行赋值
const orderGoods = orderInfo.cartVoList.find((item) => item.blessing !== '')
this.setData({
orderInfo,
blessing: orderGoods && orderGoods.blessing
})
},
// 在页面展示的时候进行触发
onShow() {
// 获取收货地址
this.getAddress()
// 获取订单结算页面的商品信息
this.getOrderInfo()
},
})
```
`➡️ /pages/order/detail/index.wxml`
```html
<!--pages/order/index.wxml-->
<view class="container order">
<view class="address-card">
<!-- 添加收货地址 -->
<!-- coding... -->
</view>
<view class="goods-wraper">
<!-- 商品清单 -->
<view class="goods-list">
<view class="goods-item flex" wx:for="{{ tradeInfo.cartVoList }}" wx:key="goodsId">
<view class="img">
<image src="{{ item.imageUrl }}" />
</view>
<view class="content">
<view class="goods-title">{{ item.name }}</view>
<view class="goods-price">
<view class="price"> ¥ {{ item.price }}</view>
<view>x {{ item.count }}</view>
</view>
</view>
</view>
</view>
</view>
<view class="payment">
<!-- 支付方式 -->
<view class="time-wraper flex">
<image src="/static/images/payment_wxzf.png" />
<view class="title">支付方式</view>
<van-checkbox value="{{true}}"></van-checkbox>
</view>
</view>
<!-- 支付区域 -->
<view class="footer flex">
<view class="left"> ¥ {{ tradeInfo.totalAmount }} </view>
<viwe class="right">结算</viwe>
</view>
<!-- 日期选择弹框 -->
<van-popup show="{{ show }}" round position="bottom" custom-style="height: 50%" bind:close="onClose">
<van-datetime-picker type="date" min-date="{{ minDate }}" model:value="{{ currentDate }}" bind:confirm="onConfirmTimerPicker" bind:cancel="onCancelTimePicker" />
</van-popup>
</view>
6.商品结算-获取立即购买数据
步骤与思路:
当用户从商品详情点击立即购买进入商品结算页面的时候,我们需要在商品结算页面展示立即购买商品的基本信息。
在跳转到商品结算页面的时候,我们已经携带了商品的 `id` 和 `祝福语`。
在结算页面,只需要获取到传递的参数,然后根据传递的参数调用接口即可。
实现步骤:
1. 在页面打开的时候,`onShow` 中接受传递的参数,并赋值给 `data` 中的状态
2. 在 `getOrderInfo` 函数中,判断立即购买商品的 `id` 是否存在,如果存在调用立即购买的接口
3. 获取数据后,然后根据数据并渲染结构即可
代码:
/pages/order/detail/index.js
```js
import {
reqOrderAddress,
reqOrderInfo,
reqBuyNowGoods
} from '@/api/orderpay'
Page({
// 获取订单详情
async getOrderInfo() {
// 从 data 中结构数据
const { goodsId, blessing } = this.data
// 判断是否存在商品 id,
// 如果存在调用立即购买商品详情的接口
// 不存在调用获取订单详情数据接口
const { data: orderInfo } = goodsId
? await reqBuyNowGoods({ goodsId, blessing })
: await reqOrderInfo()
// 判断是否存在祝福语
// 如果需要购买多个商品,挑选第一个填写了祝福语的商品进行赋值
const orderGoods = orderInfo.cartVoList.find((item) => item.blessing !== '')
this.setData({
orderInfo,
orderGoods && orderGoods.blessing
})
}
+ // 接收立即购买传递的参数
+ onLoad (options) {
+ this.setData({
+ ...options
+ })
+ },
// 在页面展示的时候进行触发
onShow() {
// 获取收货地址
this.getAddress()
// 获取订单结算页面的商品信息
this.getOrderInfo()
}
})
7. 商品结算-收集送达时间
思路与步骤:
当选择送达日期的时候,需要选择收货的时间,我们希望获取到的收货的时间格式是:年月日
但是我们使用的是小程序提供的 `vant` 组件,组件返回的时候并不是真正的时分秒,而是时间戳
这时候可以调用小程序项目初始化时,小程序封装的时间格式化工具
实现步骤:
1. 在商品结算页面导入封装好的格式化时间的方法 `formatTime`
2. 调用 `formatTime` ,传入需要格式化的时间戳
代码:
/pages/order/detail/index.js`
```js
import { formatTime } from '../../../utils/formatTime.js'
Page({
// coding...
// 期望送达日期确定按钮
onConfirmTimerPicker(event) {
// 使用 new Date 将时间戳转换成 JS 中的日期对象
const time = formatTime(new Date(event.detail))
// 将转换以后的时间赋值给送到时间
this.setData({
show: false,
deliveryDate: time
})
}
// coding...
}
8.商品结算-表单数据验证
步骤:
使用 `async-validator` 对代码进行验证
1. 收货地址不能为空
2. 订购人姓名不能为空,且不能输入特殊字符
3. 订购人手机号不能为空,且输入的手机号必须合法
4. 送达日期不能为空
代码:
js
import { reqOrderAddress, reqOrderInfo, reqBuyNowGoods } from '@/api/orderpay'
// 导入 async-validator 对参数进行验证
import Schema from 'async-validator'
// 导入格式化时间的方法
import { formatTime } from '@/utils/formatTime'
// 获取应用实例
const app = getApp()
Page({
data: {
buyName: '', // 订购人姓名
buyPhone: '', // 订购人手机号
deliveryDate: '', // 期望送达日期
blessing: '', // 祝福语
show: false, // 期望送达日期弹框
orderAddress: {}, // 收货地址
orderInfo: {}, // 订单商品详情
minDate: new Date().getTime(),
currentDate: new Date().getTime()
},
async submitOrder() {
// 从 data 中结构数据
const {
buyName,
buyPhone,
deliveryDate,
blessing,
orderInfo,
orderAddress
} = this.data
// 组织请求参数
const params = {
buyName,
buyPhone,
deliveryDate,
remarks: blessing,
cartList: orderInfo.cartVoList,
userAddressId: orderAddress.id
}
// 对请求参数进项验证
const { valid } = await this.validatorPerson(params)
// 打印验证结果
console.log(valid)
},
// 对新增收货地址请求参数进行验证
validatorPerson(params) {
// 验证收货人,是否只包含大小写字母、数字和中文字符
const nameRegExp = '^[a-zA-Z\\d\\u4e00-\\u9fa5]+$'
// 验证手机号,是否符合中国大陆手机号码的格式
const phoneReg = '^1(?:3\\d|4[4-9]|5[0-35-9]|6[67]|7[0-8]|8\\d|9\\d)\\d{8}$'
// 创建验证规则
const rules = {
userAddressId: [{ required: true, message: '请选择收货地址' }],
buyName: [
{ required: true, message: '请输入收货人姓名' },
{ pattern: nameRegExp, message: '收货人姓名不合法' }
],
buyPhone: [
{ required: true, message: '请输入收货人手机号' },
{ pattern: phoneReg, message: '收货人手机号不合法' }
],
deliveryDate: { required: true, message: '请选择送达时间' }
}
// 传入验证规则进行实例化
const validator = new Schema(rules)
// 调用实例方法对请求参数进行验证
// 注意:我们希望将验证结果通过 Promise 的形式返回给函数的调用者
return new Promise((resolve) => {
validator.validate(params, (errors) => {
if (errors) {
// 如果验证失败,需要给用户进行提示
wx.toast({ title: errors[0].message })
// 如果属性值是 false,说明验证失败
resolve({ valid: false })
} else {
// 如果属性值是 true,说明验证成功
resolve({ valid: true })
}
})
})
},
// coding....
})
9. 小程序支付-小程序支付流程
步骤:
1. `生成平台订单`:前端调用接口,向后端传递需要购买的商品信息、收货人信息,[后端生成平台订单,返回订单编号]
2. `获取预付单信息`:将订单编号发送给后端后, [后端向微信服务器获取预付单信息,后端会将微信服务器返回的预付单信息进行加密,然后将加密以后的预付单信息返回给前端]
3. `发起微信支付`:前端调用 wx.requestPayment() 发起微信支付
4. `查询支付状态`:调用接口查询支付状态
10.小程序支付-创建平台订单
步骤与思路:
用户在完成选购流程,确认商品信息、订购人、收货人等信息无误后,
用户需要点击提交订单按钮,开始进行下单支付,这时候需要先创建平台订单。
实现步骤:
1. 在提交订单的事件处理函数中调用封装的接口 API 函数
2. 在接口调用成功以后,将服务器响应的订单编码挂载到页面实例上。
代码:
/pages/order/detail/index.js
```js
import {
reqOrderAddress,
reqOrderInfo,
reqBuyNowGoods,
reqSubmitOrder
} from '@/api/orderpay'
Page({
// coding...
// 提交订单
// 处理提交订单
async submitOrder() {
// 需要从 data 中解构数据
const {
buyName,
buyPhone,
deliveryDate,
blessing,
orderAddress,
orderInfo
} = this.data
// 需要根据接口要求组织请求参数
const params = {
buyName,
buyPhone,
cartList: orderInfo.cartVoList,
deliveryDate,
remarks: blessing,
userAddressId: orderAddress.id
}
// 对请求参数进行验证
const { valid } = await this.validatorPerson(params)
// 如果验证失败,直接 return,不执行后续的逻辑处理
if (!valid) return
// 调用接口,创建平台订单
const res = await reqSubmitOrder(params)
// 在平台订单创建成功以后,将订单编号挂载到页面实例上
if (res.code === 200) {
// 将订单编号挂载到页面实例上
this.orderNo = res.data
}
}
11.小程序支付-获取预付单信息
步骤与思路:
将订单编号发送给公司的后端,公司的后端会从数据库找到对应订单的信息。
然后调用微信服务器的 下单接口 进行创建订单,订单创建成功以后,微信服务器会给公司后端返回预付单信息。
公司后端对返回的预付单信息进行加密,返回给小程序客户端。
这一步,咱们需要做的就是:订单编号发送给公司的后端,其他逻辑时后端来完成的。
注意事项:
小程序支付后面的代码,大伙在实现的时候,会出现异常。
这是因为没有小程序的开发权限,以后在实际开发中,只需要参考当前流程进行开发即可
代码:
/pages/order/detail/index.js
```js
Page({
// 处理提交订单
async submitOrder() {
// 需要从 data 中解构数据
const {
buyName,
buyPhone,
deliveryDate,
blessing,
orderAddress,
orderInfo
} = this.data
// 需要根据接口要求组织请求参数
const params = {
buyName,
buyPhone,
cartList: orderInfo.cartVoList,
deliveryDate,
remarks: blessing,
userAddressId: orderAddress.id
}
// 对请求参数进行验证
const { valid } = await this.validatorPerson(params)
// 如果请求参数验证失败,直接 return ,不执行后续的逻辑
if (!valid) return
// 调用接口,创建平台订单
const res = await reqSubmitOrder(params)
if (res.code === 200) {
// 在平台订单创建成功以后,需要将服务器、后端返回的订单编号挂载到页面实例上
this.orderNo = res.data
// 获取预付单信息、支付参数
this.advancePay()
}
},
// 获取预付单信息、支付参数
async advancePay() {
// 调用接口,获取预付单信息、支付参数
const payParams = await reqPrePayInfo(this.orderNo)
if (payParams.code === 200) {
console.log(res.data)
}
},
})
12.小程序支付-发起微信支付
步骤与思路:
小程序客户端在接收支付参数后,调用 `wx.requestPayment()` 发起微信支付,
唤醒支付弹窗,用户开输入支付密码或者进行指纹等操作,微信服务器会进行验证,如果验证成功,就会发起支付。
然后会将支付结果返回给公司后端,也会返回给 `wx.requestPayment()`
并且会微信通知用户支付结果
代码:
/pages/order/detail/index.js
```js
// 获取预付单信息、支付参数
async advancePay() {
try {
const payParams = await reqPrePayInfo(this.orderNo)
if (payParams.code === 200) {
// 进行微信支付
const payInfo = await wx.requestPayment(payParams.data)
console.log(payInfo)
}
}
catch {
wx.toast({ title: '支付遇到问题,请联系客服', icon: 'error' })
}
}
13.小程序支付-支付状态查询
步骤与思路:
通过调用后端接口获取支付状态,如果支付成功,需要给用户提示,同时跳转到订单列表页面。
公司后端开始向微信服务器发送请求,查询支付结果
公司服务器会将微信服务器返回的支付结果,返回到客户端
客户端根据查询结果跳转到订单列表页面
代码:
/pages/order/detail/index.js
```js
// 获取预付单信息、支付参数
async advancePay() {
try {
const payParams = await reqPrePayInfo(this.orderNo)
if (payParams.code === 200) {
// payParams.data 就是获取的支付参数
// 调用 wx.requestPayment 发起微信支付
const payInfo = await wx.requestPayment(payParams.data)
// 获取支付结果
if (payInfo.errMsg === 'requestPayment:ok') {
// 查询订单的支付状态
const payStatus = await reqPayStatus(this.orderNo)
if (payStatus.code === 200) {
wx.redirectTo({
url: '/pages/order/list/index',
success: () => {
wx.toast({
title: '支付成功',
icon: 'success
})
}
})
}
}
}
} catch (error) {
wx.toast({
title: '支付失败,请联系客服',
icon: 'error'
})
}
},
14.样式图
七、订单列表
1.封装订单列表接口API
代码:
api/orderpay.js
```js
/**
* @description 获取订单列表
* @returns Promise
*/
export const reqOrderList = (page, limit) => {
return http.get(`/order/order/${page}/${limit}`)
}
2.获取订单列表数据并渲染
思路与步骤:
当用户从个人中心页面点击进入订单中心的时候,就需要获取到订单中心的数据。
在页面调用 `API` 函数获取订单列表的数据,
在获取到数据以后,使用后端返回的数据对页面进行渲染
代码:
modules/orderPayModule/pages/order/list/list.js`
```js
// 导入封装的接口 API 函数
import { reqOrderList } from '@/api/orderpay'
Page({
// 页面的初始数据
data: {
orderList: [1, 2, 3], // 订单列表
page: 1, // 页码
limit: 10, // 每页展示的条数
total: 0 // 订单列表总条数
},
// 获取订单列表
async getOrderList() {
// 解构获取数据
const { page, limit } = this.data
// 调用接口获取订单列表数据
const res = await reqOrderList(page, limit)
if (res.code === 200) {
this.setData({
orderList: res.data.records,
total: res.data.total
})
}
},
// 生命周期函数--监听页面加载
onLoad() {
this.getOrderList()
}
})
```
`➡️ modules/orderPayModule/pages/order/list/list.wxml`
```html
<!--pages/order/list/index.wxml-->
<view class="order-container container">
<view class="order-list" wx:if="{{ orderList.length > 0 }}">
<view class="order-item" wx:for="{{ orderList }}" wx:key="index">
<view class="order-item-header list-flex">
<view class="orderno">订单号<text class="no">{{ orderList.orderNo }}</text></view>
<view class="order-status {{ item.orderStatus === 1 ? 'order-active' : '' }}">
{{ item.orderStatus === 1 ? '已支付' : '未支付'}}
</view>
</view>
<view
class="goods-item list-flex"
wx:for="{{ item.orderDetailList }}"
wx:key="id"
wx:for-item="goods"
wx:for-index="goodsIndex"
>
<view class="left">
<image src="{{ goods.imageUrl }}" mode="widthFix" class="img" />
</view>
<view class="mid">
<view class="goods-name">{{ goods.name }}</view>
<view class="goods-blessing">{{ goods.blessing }}</view>
</view>
<view class="right">
<view class="goods-price">¥{{ goods.price }}</view>
<view class="goods-count">x{{ goods.count }}</view>
</view>
</view>
<view class="order-item-footer">
<view class="total-amount list-flex">
<text class="text">实付</text>
<text class="price"><text>¥</text>{{ item.totalAmount }}</text>
</view>
</view>
</view>
</view>
<van-empty wx:else description="还没有购买商品,快去购买吧~" />
</view>
3.订单列表上拉加载更多
思路与步骤:
当用户进行了上拉操作时,需要在 `.js` 文件中声明 `onReachBottom` 方法,用来监听页面的上拉触底行为
当用户上拉时,需要对 `page` 参数进行加 1 即可,
当参数发生改变后,需要重新发送请求,拿最新的 `page` 向服务器要数据
在下一页的商品数据返回以后,需要将下一页的数据和之前的数据进行合并
代码:
modules/orderPayModule/pages/order/list/list.js
```js
// 导入封装的接口 API 函数
import { reqOrderList } from '@/api/orderpay'
Page({
// 页面的初始数据
data: {
orderList: [1, 2, 3], // 订单列表
page: 1, // 页码
limit: 10, // 每页展示的条数
total: 0 // 订单列表总条数
},
// 获取订单列表
async getOrderList() {
// 解构获取数据
const { page, limit } = this.data
// 调用接口获取订单列表数据
const res = await reqOrderList(page, limit)
if (res.code === 200) {
this.setData({
orderList: [...this.data.orderList, ...res.data.records],
total: res.data.total
})
}
},
// 页面上拉触底事件的处理函数
onReachBottom() {
// 解构数据
const { page } = this.data
// 更新 page
this.setData({
page: page + 1
})
// 重新发送请求
this.getOrderList()
},
// 生命周期函数--监听页面加载
onLoad() {
this.getOrderList()
}
)
4.判断数据是否加载完毕
步骤与思路:
如何判断数据是否加载完成
可以使用后端返回的 `total` 和 `goodsList` 进行对比,如果 total 大于 `goodsList` ,说明订单中心数据没有加载完,可以继续上拉加载更多。
目前还没有接收 `total`,需要先将后台返回的 total 进行赋值到 data 中,然后使用 `onReachBottom` 中进行判断
代码:
modules/orderPayModule/pages/order/list/list.js
``js
/ 页面上拉触底事件的处理函数
nReachBottom() {
// 解构数据
const { page, total, orderList } = this.data
// 数据总条数 和 订单列表长度进行对比
if (total === orderList.length) {
return wx.toast({ title: '数据加载完毕' })
}
// 更新 page
this.setData({
page: page + 1
})
// 重新发送请求
this.getOrderList()