文章目录
- 黑马优购
-
- 1. 项目介绍
- 2. 初始化项目
-
- 2.1 初始化项目
- 2.2 梳理项目结构
- 2.3 绘制 tabBar
- 2.4 修改导航栏样式
- 3. 首页
-
- 3.1 为异步 API 启用 Promise 功能
- 3.2 轮播图数据渲染
- 3.3 获取首页分类选项数据
- 3.4 渲染分类数据项对应的UI结构
- 3.5 美化分类数据项的UI显示效果
- 3.6 获取楼层相关的数据
- 3.7 渲染楼层UI结构
- 3.8 美化楼层UI结构
- 3.9 点击楼层图片跳转到商品列表页面
- 4. 优化
-
- 4.1 把页面的业务逻辑抽离到单独的 `mixin` 文件中
- 4.2 封装 `baseToast` 函数提示错误消息
- 4.3 封装 `wepy.get` 函数发起get请求
- 4.4 封装 `wepy.post` 函数发起get请求
- 5. 分类页面
-
- 5.1 自定义分类页面的编译模式
- 5.2 获取分类数据列表
- 5.3 下载并安装 `vant` 小程序UI组件库
- 5.4 将 vant 中的徽章组件注册为全局组件
- 5.5 渲染左侧的一级分类列表结构
- 5.6 使用 `scroll-view` 优化左侧分类的滚动效果
- 5.7 动态获取窗口的可用高度
- 5.8 根据一级分类的变化动态切换二级分类数据
- 5.9 渲染二级和三级分类的UI结构
- 5.10 点击三级分类跳转到商品列表页面
- 6. interceptor 拦截器
-
- 6.1 介绍 wepy 中的拦截器
- 6.2 实现数据加载期间的loading效果
- 7. 搜索
-
- 7.1 全局注册搜索组件并渲染到页面中
- 7.2 根据关键字的变化动态获取搜索建议列表数据
- 7.3 解决搜索关键字为空时候的小Bug
- 7.4 通过Cell单元格组件渲染搜索建议列表的UI结构
- 7.5 点击搜索建议项导航到商品详情页
- 7.6 触发搜索事件后导航到商品列表页面
- 7.7 页面加载期间读取搜索关键词列表
- 7.8 将关键词存储到Storage中
- 7.9 定义计算属性来决定是否展示历史搜索区域
- 7.10 绘制历史搜索头部区域的UI结构
- 7.11 渲染历史搜索列表的UI结构
- 8. 商品列表
-
- 8.1 处理请求参数
- 8.2 获取商品列表数据
- 8.3 循环渲染商品列表的UI结构
- 8.4 初步实现上拉加载更多的操作
- 8.5 通过公式判断列表数据是否全部加载完毕
- 8.6 通过 `isover` 控制数据加载完毕后的提示消息
- 8.7 通过 `isloading` 防止重复发起数据请求
- 8.8 下拉刷新
- 8.9 通过 callback 回调函数优化关闭下拉刷新的行为
- 8.10 点击商品列表Item项导航到商品详情页
- 9. 商品详情
-
- 9.1 获取商品详情数据
- 9.2 渲染商品详情页的轮播图
- 9.3 绘制价格名称运费区域
- 9.4 绘制促销已选区域
- 9.5 绘制收货地址区域
- 9.6 注册并使用标签页组件
- 9.7 渲染规格参数面板的UI结构
- 9.8 渲染图文详情面板
- 9.9 实现轮播图预览效果
- 9.10 选择收获地址
- 9.11 通过计算属性动态渲染收货地址
- 9.12 全局注册并使用商品导航组件
- 9.13 客服功能
- 10. 加入购物车
-
- 10.1 为加入购物车按钮绑定单击事件处理函数
- 10.2 在`app.wpy`中定义全局共享的数据和方法
- 10.3 把商品详情直接存储到购物车中
- 10.4 优化商品对象存储到购物车中的过程
- 10.5 防止商品重复添加
- 10.6 持久化存储购物车数据
- 10.7 自定义编译模式并美化空白购物车的页面结构
- 11. 购物车
-
- 11.1 实现空白购物车和非空购物车的按需展示
- 11.2 渲染购物车列表标题
- 11.3 循环渲染基本的商品列表结构
- 11.4 通过插槽自定义渲染商品的价格和数量
- 11.5 美化商品价格和数量区域
- 11.6 数量改变时获取最新的数量值和当前商品的Id
- 11.7 完成商品数量的更新操作
- 11.8 在商品之间渲染分割线
- 11.9 通过thumb插槽渲染复选框和缩略图
- 11.10. 美化复选框和缩略图的样式
- 11.11 监听复选框状态变化的事件
- 11.12 修改商品的选中状态
- 11.13 初步实现滑动删除的UI效果
- 11.14 美化滑动单元格右侧的删除按钮
- 11.15 根据Id从购物车列表中删除对应的商品
- 11.16 渲染提交订单区域的UI结构
- 11.17 通过计算属性动态计算勾选商品的总价格
- 11.18 通过计算属性判断全选的状态
- 11.19 点击全选更新所有商品的选中状态
- 11.20 为TabBar中的购物车添加数字徽章
- 11.21 为商品详情页的购物车图标添加数字徽章
- 11.22 提交订单
- 12. 确认订单
-
- 12.1 修改确认订单页面的标题
- 12.2 渲染选择收货地址区域的UI结构
- 12.3 选择收货地址
- 12.4 在订单页面加载期间读取收货地址
- 12.5 通过计算属性控制收货地址按钮和收货人信息区域的按需显示
- 12.6 渲染并美化收货信息区域
- 12.7 点击联系电话区域重新选择收货地址
- 12.8 渲染订单商品列表
- 12.9 渲染登录后下单的按钮
- 13. 订单支付
-
- 13.1 准备登录相关的参数 - 获取用户信息
- 13.2 准备登录相关的参数 - 获取用户登录凭证
- 13.3 实现登录功能并得到登录成功后的Token值
- 13.4 按需渲染订单支付区域
- 13.5 通过拦截器为header请求头添加Authorization字段
- 13.6 实现下单及支付功能
- 14. 订单列表
-
- 14.1 渲染标签页
- 14.2 获取订单列表数据
- 14.3 通过Panel面板渲染订单信息
- 14.4 把订单Item项的UI结构封装为自定义组件
- 14.5 通过repeat循环创建自定义组件
- 15. 发布小程序
黑马优购
1. 项目介绍
首页、分类、搜索、商品列表、商品详情、购物车、支付
2. 初始化项目
2.1 初始化项目
- 运行
wepy init standard heima_ugo
命令,初始化小程序项目 - 运行
cd heima_ugo
进入项目根目录 - 运行
npm install
安装所有依赖项 - 运行
wepy build --watch
命令,开启 wepy 项目的实时编译功能 - 打开微信开发者工具,加载 wepy 项目并查看效果
- 解决 ESLint 语法报错问题
2.2 梳理项目结构
- 清理并重置
src -> pages -> index.wpy
首页 - 在根目录的
.prettierrc
配置文件内,新增"semi": false
配置,防止每次格式化代码,添加分号的问题 - 清理并重置
src -> app.wpy
中的代码,将style
和script
标签中,不必要的代码删除掉 - 清空
src -> components
和src -> mixins
目录 - 将梳理完毕后的项目,上传至码云
2.3 绘制 tabBar
-
新建
src -> pages -> tabs
文件夹,用来存放所有 tabBar 相关的页面 -
删除
src -> pages -> index.wpy
页面,并在tabs
目录中,新建home.wpy
,cates.wpy
,search.wpy
,cart.wpy
,me.wpy
五个 tabBar 相关的页面 -
将页面路径,记录到
src -> app.wpy
文件的config -> pages
节点中,示例代码如下:pages: ['pages/tabs/home','pages/tabs/cates','pages/tabs/search','pages/tabs/cart','pages/tabs/me' ]
-
新建
src -> assets
目录,并将素材中的icons
文件夹,拷贝到项目src -> assets
目录中 -
在
src -> app.wpy
文件中,新增tabBar
节点,并做如下配置:tabBar: { // 选中的文本颜色selectedColor: '#D81E06',// tabBar 的列表list: [{ // 页面路径pagePath: 'pages/tabs/home',// 显示的文本text: '首页',// 默认图标iconPath: '/assets/icons/home.png',// 选中图标selectedIconPath: '/assets/icons/home-active.png'},{ pagePath: 'pages/tabs/cates',text: '分类',iconPath: '/assets/icons/cates.png',selectedIconPath: '/assets/icons/cates-active.png'},{ pagePath: 'pages/tabs/search',text: '搜索',iconPath: '/assets/icons/search.png',selectedIconPath: '/assets/icons/search-active.png'},{ pagePath: 'pages/tabs/cart',text: '购物车',iconPath: '/assets/icons/cart.png',selectedIconPath: '/assets/icons/cart-active.png'},{ pagePath: 'pages/tabs/me',text: '我的',iconPath: '/assets/icons/my.png',selectedIconPath: '/assets/icons/my-active.png'}] }
2.4 修改导航栏样式
打开 src -> app.wpy
文件,找到 window
节点,并配置如下:
window: {
// 页面背景色backgroundTextStyle: 'dark',// 导航条背景色navigationBarBackgroundColor: '#D81E06',// 导航条标题文本navigationBarTitleText: '黑马优购',// 导航条标题文字颜色navigationBarTextStyle: 'white'
}
3. 首页
3.1 为异步 API 启用 Promise 功能
-
打开
src -> app.wpy
文件 -
找到 constructor() 构造函数
-
在构造函数的最后,新增如下代码:
constructor() { super()this.use('requestfix')// 通过下面这一行代码,可以为异步的API,// 开启Promise功能,这样,异步API调用的结果,返回值是Promise对象this.use('promisify') }
3.2 轮播图数据渲染
-
获取轮播图数据
// 获取轮播图数据的函数 async getSwiperData() { const { data: res } = await wepy.get('/home/swiperdata')if (res.meta.status !== 200) { return wepy.baseToast()}this.swiperList = res.messagethis.$apply() }
-
使用
wepy.showToast()
弹框提示 -
使用
swiper
组件和swiper-item
组件渲染轮播图效果 -
使用
navigator
组件将images
图片包裹起来,从而点击图片实现跳转<!-- 轮播图区域 --> <swiper circular indicator-dots><swiper-item wx:for="{ {swiperList}}" wx:key="index"><navigator url="{ {item.navigator_url}}" open-type="{ {item.open_type}}"><image src="{ {item.image_src}}" /></navigator></swiper-item> </swiper>
-
设置
swiper
组件的高度为350rpx
从而实现轮播图在不同屏幕的自适应swiper {height: 350rpx;navigator,image {height: 100%;width: 750rpx;} }
3.3 获取首页分类选项数据
// 获取首页分类相关的数据项
async getCateItems() {
const {
data: res } = await wepy.get('/home/catitems')if (res.meta.status !== 200) {
return wepy.baseToast()}this.cateItems = res.messagethis.$apply()
}
3.4 渲染分类数据项对应的UI结构
<!-- 分类区域 -->
<view class="cates"><block wx:for="{
{cateItems}}" wx:key="index"><navigator url="/pages/tabs/cates" open-type="{
{item.open_type}}" wx:if="{
{item.navigator_url !== undefined}}" hover-class="none"><image src="{
{item.image_src}}" /></navigator> <image src="{
{item.image_src}}" wx:else/></block>
</view>
3.5 美化分类数据项的UI显示效果
.cates {display: flex;justify-content: space-around;margin: 40rpx 0;image {width: 128rpx;height: 140rpx;}
}
3.6 获取楼层相关的数据
onLoad() {
this.getSwiperData()this.getCateItems()// 在页面加载完成后,自动获取楼层数据this.getFloorData()
}// 获取楼层相关的数据
async getFloorData() {
const {
data: res } = await wepy.get('/home/floordata')if (res.meta.status !== 200) {
return wepy.baseToast()}this.floorData = res.message// 通知页面,data中数据发生了变化,需要强制页面重新渲染一次this.$apply()
}
3.7 渲染楼层UI结构
<!-- 楼层区域 -->
<view class="floor-container"><view class="floor-item" wx:for="{
{floorData}}" wx:key="index"><!-- 楼层的标题 --><image class="floor-item-title" src="{
{item.floor_title.image_src}}"/><!-- 楼层的图片 --><view class="floor-img-box"><image class="floor-item-pic" wx:for="{
{item.product_list}}" wx:key="index" src="{
{item.image_src}}" style="width: {
{
item.image_width}}rpx;" @tap="goGoodsList({
{item.navigator_url}})"/></view></view>
</view>
3.8 美化楼层UI结构
.floor-container {.floor-item {.floor-item-title {height: 50rpx;width: 640rpx;display: block;}.floor-img-box {.floor-item-pic {float: left;height: 190rpx;margin: 8rpx;margin-top: 0;&:nth-child(1) {height: 390rpx;}}}}
}
3.9 点击楼层图片跳转到商品列表页面
methods = {
// 点击楼层中的每一张图片,都要跳转到商品列表页面goGoodsList(url) {
wepy.navigateTo({
url})}
}
4. 优化
4.1 把页面的业务逻辑抽离到单独的 mixin
文件中
为了精简每个小程序页面的代码,可以将 script 中的业务逻辑,抽离到对应的 mixin 文件中,具体步骤:
-
在
src -> mixins
文件夹中,新建与页面路径对应的.js
文件,并初始化基本的代码结构如下:import wepy from 'wepy'// 注意,必须继承自 wepy.mixin export default class extends wepy.mixin { }
-
在对应的页面中,可以导入并使用对应的 mixin,具体代码如下:
<script> import wepy from 'wepy' // 1. 导入外界的 mixin 文件,并接受 // @ 就代表 src 这一层路径 import mix from '@/mixins/tabs/home.js'export default class extends wepy.page { // 2. 把导入的 mix 对象,挂载到 mixins 这个数据中就行mixins = [mix] } </script>
4.2 封装 baseToast
函数提示错误消息
-
为了提高项目的维护性、可用性、扩展性,可以将常用的 js 逻辑,封装到
src -> baseAPI.js
文件中:import wepy from 'wepy'/*** 弹框提示一个无图标的 Toast 消息* @str 要提示的消息内容*/ wepy.baseToast = function(str = '获取数据失败!') { wepy.showToast({ title: str,// 弹框期间不会携带任何图标icon: 'none',duration: 1500}) }
-
在
app.wpy
中导入执行baseAPI.js
文件中的代码:<script>import wepy from 'wepy'import 'wepy-async-function'// 导入并执行 baseAPI.js 中的所有代码import '@/baseAPI.js' </script>
4.3 封装 wepy.get
函数发起get请求
在小程序项目中,需要经常发起数据请求,因此,可以将 wepy.request()
函数封装,在全局挂在 wepy.get()
函数,从而发起 Get 请求,代码如下:
// src/baseAPI.jsimport wepy from 'wepy'// 请求根路径
const baseURL = 'https://www.zhengzhicheng.cn/api/public/v1'/*** 发起 get 请求的 API* @url 请求的地址,为相对路径,必须以 / 开头* @data 请求的参数对象*/
wepy.get = function(url, data = {
}) {
return wepy.request({
url: baseURL + url,method: 'GET',data})
}
4.4 封装 wepy.post
函数发起get请求
在小程序项目中,需要经常发起数据请求,因此,可以将 wepy.request()
函数封装,在全局挂在 wepy.post()
函数,从而发起 Post 请求,代码如下:
// src/baseAPI.jsimport wepy from 'wepy'// 请求根路径
const baseURL = 'https://www.zhengzhicheng.cn/api/public/v1'/*** 发起 post 请求的 API* @url 请求的地址,为相对路径,必须以 / 开头* @data 请求的参数对象*/
wepy.post = function (url, data = {
}) {
return wepy.request({
url: baseURL + url,method: 'POST',data})
}
5. 分类页面
5.1 自定义分类页面的编译模式
- 点击工具栏中,编译模式的下拉菜单,选择新建编译模式
- 填写编译模式的名称
- 选择启动页面的路径
- 确认添加
5.2 获取分类数据列表
async getCateList() {
const {
data: res } = await wepy.get('/categories')if (res.meta.status !== 200) {
return wepy.baseToast()}this.cateList = res.messagethis.secondCate = res.message[0].childrenthis.$apply()
}
5.3 下载并安装 vant
小程序UI组件库
- 访问
vant-weapp
的 Github 主页 https://github.com/youzan/vant-weapp - 点击
Clone or Download
按钮 - 选择
Download ZIP
- 解压下载的
vant-weapp-dev.zip
- 进入解压后的目录,将
lib
目录重命名为vant
- 把重命名为
vant
的目录,复制到src -> assets
目录中
5.4 将 vant 中的徽章组件注册为全局组件
-
打开
app.wpy
文件 -
在
config
节点内,新增usingComponents
节点,具体代码如下:config = { // 引用并注册全局组件usingComponents: { // 徽章组件'van-badge': './assets/vant/badge/index','van-badge-group': './assets/vant/badge-group/index'} }
5.5 渲染左侧的一级分类列表结构
<van-badge-group active="{
{ active }}" bind:change="onChange"><van-badge title="{
{item.cat_name}}" wx:for="{
{cateList}}" wx:key="index" />
</van-badge-group>
5.6 使用 scroll-view
优化左侧分类的滚动效果
<!-- 左侧的滚动视图区域 -->
<scroll-view class="left" scroll-y style="height: 200px;"><van-badge-group active="{
{ active }}" bind:change="onChange"><van-badge title="{
{item.cat_name}}" wx:for="{
{cateList}}" wx:key="index" /></van-badge-group>
</scroll-view>
5.7 动态获取窗口的可用高度
onLoad() {
// 动态获取屏幕可用的高度this.getWindowHeight()this.getCateList()
}// 动态获取屏幕可用的高度
async getWindowHeight() {
const res = await wepy.getSystemInfo()if (res.errMsg === 'getSystemInfo:ok') {
this.wh = res.windowHeightthis.$apply()}
}
5.8 根据一级分类的变化动态切换二级分类数据
methods = {
onChange(e) {
// e.detail 是点击项的索引// console.log(e.detail)this.secondCate = this.cateList[e.detail].children}}
5.9 渲染二级和三级分类的UI结构
<!-- 右侧滚动视图区域 -->
<scroll-view class="right" scroll-y style="height: {
{
wh}}px;"><!-- 循环创建二级分类 --><block wx:for="{
{secondCate}}" wx:key="index"><van-row><van-col span="24" style="text-align:center;"><text class="cate_title" space="ensp">/ {
{item.cat_name}} /</text></van-col></van-row><!-- 三级分类 --><van-row><block wx:for="{
{item.children}}" wx:key="index"><van-col span="8" class="cell" @tap="goGoodsList({
{item.cat_id}})"><image src="{
{item.cat_icon}}" class="thumbImg" /><view class="thumbTitle">{
{item.cat_name}}</view></van-col></block></van-row></block>
</scroll-view>
5.10 点击三级分类跳转到商品列表页面
methods = {
// 点击跳转到商品列表页面,同时把商品分类的 cid 传递过去goGoodsList(cid) {
wepy.navigateTo({
url: '/pages/goods_list?cid=' + cid})}}
6. interceptor 拦截器
6.1 介绍 wepy 中的拦截器
可以使用WePY提供的全局拦截器对原生API的请求进行拦截。
具体方法是配置API的config、fail、success、complete回调函数。参考示例:
import wepy from 'wepy';export default class extends wepy.app {
constructor () {
// this is not allowed before super()super();// 拦截request请求this.intercept('request', {
// 发出请求时的回调函数config (p) {
// 对所有request请求中的OBJECT参数对象统一附加时间戳属性p.timestamp = +new Date();console.log('config request: ', p);// 必须返回OBJECT参数对象,否则无法发送请求到服务端return p;},// 请求成功后的回调函数success (p) {
// 可以在这里对收到的响应数据对象进行加工处理console.log('request success: ', p);// 必须返回响应数据对象,否则后续无法对响应数据进行处理return p;},//请求失败后的回调函数fail (p) {
console.log('request fail: ', p);// 必须返回响应数据对象,否则后续无法对响应数据进行处理return p;},// 请求完成时的回调函数(请求成功或失败都会被执行)complete (p) {
console.log('request complete: ', p);}});}
}
6.2 实现数据加载期间的loading效果
打开 app.wpy
,在 constructor()
构造函数中,通过拦截器实现loading效果,具体代码如下:
constructor() {
super()this.use('requestfix')// 通过这一行代码,可以为异步的API,开启Promise功能,这样,异步API调用的结果,返回值是Promise对象this.use('promisify')// 拦截器this.intercept('request', {
// 发出请求时的回调函数config(p) {
// 显示loading效果wepy.showLoading({
title: '数据加载中...'})// 必须返回OBJECT参数对象,否则无法发送请求到服务端return p},// 请求成功后的回调函数success(p) {
// 必须返回响应数据对象,否则后续无法对响应数据进行处理return p},// 请求失败后的回调函数fail(p) {
// 必须返回响应数据对象,否则后续无法对响应数据进行处理return p},// 请求完成时的回调函数(请求成功或失败都会被执行)complete(p) {
// 隐藏loading效果wepy.hideLoading()}})
}
7. 搜索
7.1 全局注册搜索组件并渲染到页面中
-
在
app.wpy
中的config
节点中,找到usingComponents
并注册搜索组件,代码如下:export default class extends wepy.app { config = { // 引用并注册全局组件usingComponents: { // 商品卡片组件'van-card': './assets/vant/card/index'}}
-
在
search.wpy
中使用刚才注册的组件:<!-- 搜索框区域 --> <van-search value="{ { value }}" placeholder="请输入搜索关键词" show-action bind:change="onChange" bind:search="onSearch" bind:cancel="onCancel" />
7.2 根据关键字的变化动态获取搜索建议列表数据
-
监听搜索框组件的
bind:change="onChange"
事件:// 当搜索关键词发生变化,会触发这个事件处理函数 onChange(e) { // e.detail 是变化过后最新的内容console.log(e.detail)this.getSuggestList(e.detail) }
-
定义
getSuggestList()
函数获取搜索建议列表:// 获取搜索建议列表 async getSuggestList(searchStr) { const { data: res } = await wepy.get('/goods/qsearch', { query: searchStr })if (res.meta.status !== 200) { return wepy.baseToast()}this.suggestList = res.messagethis.$apply() }
7.3 解决搜索关键字为空时候的小Bug
产生 Bug 的原因,是因为用户输入关键词的 length 长度导致的,因此,可以适当修改 onChange
事件处理函数如下:
// 当搜索关键词发生变化,会触发这个事件处理函数
onChange(e) {
// e.detail 是变化过后最新的内容console.log(e.detail)if (e.detail.trim().length <= 0) {
this.suggestList = []return}this.getSuggestList(e.detail)
}
7.4 通过Cell单元格组件渲染搜索建议列表的UI结构
-
全局注册
cell
单元格相关的组件:export default class extends wepy.app { config = { // 引用并注册全局组件usingComponents: { // 单元格组件'van-cell': './assets/vant/cell/index','van-cell-group': './assets/vant/cell-group/index'}}
-
在
search.wpy
页面中渲染搜索建议列表结构:<!-- 搜索的建议列表 --> <van-cell-group><block wx:for="{ {suggestList}}" wx:key="index"><van-cell title="{ {item.goods_name}}" /></block> </van-cell-group>
7.5 点击搜索建议项导航到商品详情页
-
为
vant-cell
组件绑定点击事件处理函数:<van-cell title="{ {item.goods_name}}" @tap="goGoodsDetail({ {item.goods_id}})" />
-
定义事件处理函数,并导航到详情页面:
methods = { // ...// 点击搜索建议项,导航到商品详情页面goGoodsDetail(goods_id) { wepy.navigateTo({ url: '/pages/goods_detail/main?goods_id=' + goods_id})} }
7.6 触发搜索事件后导航到商品列表页面
-
监听
vant-search
组件的bind:search="onSearch"
事件 -
导航到商品详情页面:
// 触发了搜索 onSearch(e) { // e.detail 就是最新的搜索关键字const kw = e.detail.trim()// 如果搜索关键词为空,则阻止跳转if (kw.length <= 0) { return}wepy.navigateTo({ url: '/pages/goods_list?query=' + kw}) }
7.7 页面加载期间读取搜索关键词列表
onLoad() {
// 调用小程序官方提供的 getStorageSync 函数,可以从本地存储中读取数据const kwList = wx.getStorageSync('kw') || []// 将读取的数据挂载到 data 中this.kwList = kwList}
7.8 将关键词存储到Storage中
// 触发了搜索
onSearch(e) {
// e.detail 就是最新的搜索关键字const kw = e.detail.trim()if (kw.length <= 0) {
return}// 把用户填写的搜索关键词,保存到 Storage 中if (this.kwList.indexOf(kw) === -1) {
this.kwList.unshift(kw)}// 数组的 slice 方法,不会修改原数组,而是返回一个新的数组this.kwList = this.kwList.slice(0, 10)wepy.setStorageSync('kw', this.kwList)wepy.navigateTo({
url: '/pages/goods_list?query=' + kw})
}
7.9 定义计算属性来决定是否展示历史搜索区域
// 计算属性
computed = {
// true 展示搜索历史区域// false 展示搜索建议区域isShowHistory() {
if (this.value.length <= 0) {
return true}return false}
}
7.10 绘制历史搜索头部区域的UI结构
-
全局注册
vant-icon
组件:export default class extends wepy.app { config = { // 引用并注册全局组件usingComponents: { // 图标'van-icon': './assets/vant/icon/index'}}
-
定义历史搜索头部区域的UI结构:
<!-- 历史搜索区域 --> <view wx:else><view class="history_title"><text>历史搜索</text><van-icon name="delete" @tap="clearHistory" /></view> </view>
-
定义样式美化对应的UI结构:
.history_title {display: flex;justify-content: space-between;padding: 0 20rpx;text:nth-child(1) {font-size: 26rpx;font-weight: bold;} }
7.11 渲染历史搜索列表的UI结构
-
全局注册
vant-tag
组件:export default class extends wepy.app { config = { // 引用并注册全局组件usingComponents: { // Tag 标签'van-tag': './assets/vant/tag/index'}}
-
通过循环渲染历史搜索列表的UI结构:
<!-- 历史搜索区域 --> <view wx:else><view class="history_title"><text>历史搜索</text><van-icon name="delete" @tap="clearHistory" /></view><view class="history_body"><van-tag size="large" wx:for="{ {kwList}}" wx:key="index" class="tag" @tap="goGoodsList({ {item}})">{ {item}}</van-tag></view> </view>
-
通过样式美化
vant-tag
组件的样式:.tag {> view {margin: 15rpx;} }
8. 商品列表
8.1 处理请求参数
-
在
goods_list.js
中定义data
节点,并定义对应的请求参数:data = { // 查询关键词query: '',// 商品分类的Idcid: '',// 页码值pagenum: 1,// 每页显示多少条数据pagesize: 10 }
-
在
onLoad()
生命周期函数中,处理query
和cid
的值,并发起数据请求:onLoad(options) { this.query = options.query || ''this.cid = options.cid || ''this.getGoodsList() }
8.2 获取商品列表数据
// 获取商品列表数据
async getGoodsList(cb) {
const {
data: res } = await wepy.get('/goods/search', {
query: this.query,cid: this.cid,pagenum: this.pagenum,pagesize: this.pagesize})if (res.meta.status !== 200) {
return wepy.baseToast()}this.goodslist = res.message.goodsthis.total = res.message.totalthis.$apply()
}
8.3 循环渲染商品列表的UI结构
-
全局注册
vant-card
组件:export default class extends wepy.app { config = { // 引用并注册全局组件usingComponents: { // 商品卡片组件'van-card': './assets/vant/card/index'}}
-
循环渲染商品列表对应的UI结构:
<!-- 商品列表区域 --> <block wx:for="{ {goodslist}}" wx:key="index"><van-card num="{ {item.goods_number}}" price="{ {item.goods_price}}" title="{ {item.goods_name}}" thumb="{ { item.goods_small_logo }}" /><!-- 分割线 --><view class="sep_line"></view> </block>
-
美化分割线的样式:
.sep_line {border-top: 1rpx solid #eee; }
8.4 初步实现上拉加载更多的操作
-
在
goods_list.wpy
中配置上拉加载更多的距离:<script> import wepy from 'wepy' import mix from '@/mixins/goods_list.js'export default class extends wepy.page { // 注意:config 节点只能写到页面的JS中,不能抽离到 mixin 中config = { navigationBarTitleText: '商品列表',// 上拉触底的距离,默认是 50pxonReachBottomDistance: 100}mixins = [mix] } </script>
-
在
goods_list.js
中监听上拉触底的事件:// 触底操作 onReachBottom() { console.log('触底了')this.pagenum++this.getGoodsList() }
-
新旧数据拼接合并:
// 获取商品列表数据async getGoodsList(cb) { // ...this.goodslist = [...this.goodslist, ...res.message.goods]// ...}
8.5 通过公式判断列表数据是否全部加载完毕
-
判断数据是否加载完毕的公式为:
当前页码值 * 每页显示的数据条数 >= 总数据条数 pagemun * pagesize >= total
-
优化
onReachBottom()
函数的业务处理逻辑:// 触底操作 onReachBottom() { // 先判断是否有下一页的数据if (this.pagenum * this.pagesize >= this.total) { return}console.log('触底了')this.pagenum++this.getGoodsList() }
8.6 通过 isover
控制数据加载完毕后的提示消息
-
在 data 中定义
isover
布尔值:data = { // ...// 数据是否加载完毕的布尔值,默认为 falseisover: false }
-
当所有数据加载完毕之后,把
isover
的值重置为true
:if (this.pagenum * this.pagesize >= this.total) { this.isover = truereturn }
-
在页面上,渲染数据加载完毕之后的UI结构,并通过
isover
控制其显示与隐藏:<!-- 数据加载完毕后的提示消息 --> <view class="over_line" hidden="{ {!isover}}">-------- 我是有底线的 --------</view>
-
美化
over_line
的样式:.over_line {font-size: 24rpx;text-align: center;height: 60rpx;line-height: 60rpx;color: #ddd; }
8.7 通过 isloading
防止重复发起数据请求
-
在 data 中定义
isloading
布尔值:data = { // ...// 表示当前数据是否正在请求中isloading: false }
-
优化
getGoodsList()
函数:// 获取商品列表数据 async getGoodsList(cb) { // 即将发起请求时,将 isloading 重置为 truethis.isloading = trueconst { data: res } = await wepy.get('/goods/search', { query: this.query,cid: this.cid,pagenum: this.pagenum,pagesize: this.pagesize})if (res.meta.status !== 200) { return wepy.baseToast()}this.goodslist = [...this.goodslist, ...res.message.goods]this.total = res.message.total// 当数据请求完成后,将 isloading 重置为 falsethis.isloading = falsethis.$apply() }
-
优化
onReachBottom()
函数:// 触底操作 onReachBottom() { // 判断当前是否正在请求数据中,// 如果 isloading 值为 true,则 return 从而终止后续操作,防止重复发起数据请求if (this.isloading) { return}// ... }
8.8 下拉刷新
-
在
goods_list.wpy
中启用下拉刷新
和设置下拉刷新窗口的背景色
:export default class extends wepy.page { // 注意:config 节点只能写到页面的JS中,不能抽离到 mixin 中config = { // ...// 开启下拉刷新enablePullDownRefresh: true,// 设置下拉刷新窗口的背景色backgroundColor: '#eee'}mixins = [mix] } </script>
-
在
goods_list.js
中监听下拉刷新的事件处理函数:// 下拉刷新的操作 onPullDownRefresh() { // 初始化必要的字段值this.pagenum = 1this.total = 0this.goodslist = []this.isover = this.isloading = false// 重新发起数据请求this.getGoodsList() }
-
在
getGoodsList()
函数中,当数据请求完成后,手动调用API关闭下拉刷新效果:// 获取商品列表数据async getGoodsList(cb) { // ...this.goodslist = [...this.goodslist, ...res.message.goods]this.total = res.message.totalthis.isloading = falsethis.$apply()// 当数据请求成功后,立即关闭下拉刷新效果wepy.stopPullDownRefresh()}
8.9 通过 callback 回调函数优化关闭下拉刷新的行为
-
优化
onPullDownRefresh
中的代码,在调用getGoodsList()
函数时,把停止下拉刷新的代码,以回调函数的形式传递进去,示例代码如下:// 下拉刷新的操作onPullDownRefresh() { // 初始化必要的字段值this.pagenum = 1this.total = 0this.goodslist = []this.isover = this.isloading = false// 重新发起数据请求this.getGoodsList(() => { // 停止下拉刷新的行为wepy.stopPullDownRefresh()})}
-
优化
stopPullDownRefresh()
中的代码如下:// 获取商品列表数据async getGoodsList(cb) { // ...this.$apply()// 只有当外界传递了 cb 回调函数之后,才调用 cb()cb && cb()}
8.10 点击商品列表Item项导航到商品详情页
-
为商品列表中的每个 Item 项绑定点击事件处理函数:
<van-card @tap="goGoodsDetail({ {item.goods_id}})" />
-
定义事件处理函数,导航到商品详情页面:
methods = { // 点击跳转到商品详情页面goGoodsDetail(goods_id) { wepy.navigateTo({ url: '/pages/goods_detail/main?goods_id=' + goods_id})} }
9. 商品详情
9.1 获取商品详情数据
-
在
data
中定义需要的数据节点:data = { // 商品的Id值goods_id: '',// 商品的详情goodsInfo: { } }
-
在
onLoad
生命周期函数中,转存商品Id,并发起数据请求:onLoad(options) { // 转存商品Idthis.goods_id = options.goods_id// 发起数据请求this.getGoodsInfo() }
-
定义
getGoodsInfo
函数,发起数据请求:// 获取商品详情数据 async getGoodsInfo() { const { data: res } = await wepy.get('/goods/detail', { goods_id: this.goods_id})if (res.meta.status !== 200) { return wepy.baseToast()}this.goodsInfo = res.messagethis.$apply() }
9.2 渲染商品详情页的轮播图
-
绘制UI结构:
<!-- 商品轮播图区域 --> <swiper indicator-dots circular><block wx:for="{ {goodsInfo.pics}}" wx:key="index"><swiper-item><image src="{ {item.pics_big}}"></image></swiper-item></block> </swiper>
-
美化样式:
swiper {height: 750rpx;image {width: 100%;height: 100%;} }
9.3 绘制价格名称运费区域
-
绘制对应的UI结构:
<!-- 商品信息区域 --> <view class="goods_info"><!-- 价格、名称、运费 --><view class="box1"><view class="price">¥{ {goodsInfo.goods_price}}</view><view class="goods_name"><view class="left">{ {goodsInfo.goods_name}}</view><view class="right"><van-icon name="star-o"></van-icon><view>收藏</view></view></view><view class="yunfei">快递:免运费</view></view> </view>
-
通过样式美化价格名称运费区域:
.goods_info {.sep_line {border-bottom: 15rpx solid #efefef;}.box1 {padding: 0 10rpx;.price {font-size: 40rpx;color: red;margin: 20rpx 0;}.goods_name {display: flex;justify-content: space-between;.left {font-size: 26rpx;padding-right: 40rpx;}.right {width: 200rpx;text-align: center;border-left: 1rpx solid #eee;> view {font-size: 20rpx;}}}.yunfei {font-size: 26rpx;color: #666;font-weight: bold;line-height: 80rpx;}} }
9.4 绘制促销已选区域
-
绘制对应的UI结构:
<!-- 促销已选区域 --> <view class="box2"><!-- 促销 --><view><text>促销</text><text>满300元减30元</text></view><!-- 已选 --><view><text>已选</text><text>黑色/S/1件</text></view> </view> <view class="sep_line"></view>
-
通过样式美化促销已选区域:
.box2 {font-size: 24rpx;padding: 0 10rpx;> view {line-height: 80rpx;text:nth-child(1) {margin-right: 20rpx;}text:nth-child(2) {color: #666;}} }
9.5 绘制收货地址区域
-
绘制对应的UI结构:
<!-- 收货地址区域 --> <view class="box3" @tap="chooseAddress"><view><text>送至</text><text>{ {addressStr}}</text></view><van-icon name="arrow"></van-icon> </view> <view class="sep_line"></view>
-
通过样式美化收货地址区域:
.box3 {display: flex;justify-content: space-between;font-size: 24rpx;padding: 25rpx 10rpx 25rpx 10rpx;> view {text:nth-child(1) {margin-right: 20rpx;}text:nth-child(2) {color: #666;}} }
9.6 注册并使用标签页组件
-
在
app.wpy
中注册对应的组件:usingComponents: { // ...// tab 标签页'van-tab': './assets/vant/tab/index','van-tabs': './assets/vant/tabs/index' }
-
通过
标签页组件
,在详情页面上渲染商品详情区域:<!-- 商品详情区域 --> <van-tabs><van-tab title="图文详情">图文详情</van-tab><van-tab title="规格参数" class="tab2">规格参数</van-tab> </van-tabs>
9.7 渲染规格参数面板的UI结构
-
通过 for 循环,渲染对应的UI结构:
<van-tab title="规格参数" class="tab2"><block wx:for="{ {goodsInfo.attrs}}" wx:key="index"><van-row><!-- 参数名 --><van-col span="10">{ {item.attr_name}}</van-col><!-- 参数值 --><van-col span="14">{ {item.attr_value}}</van-col></van-row></block> </van-tab>
-
美化样式:
.tab2 {font-size: 24rpx;.van-row {border-top: 1rpx solid #eee;.van-col {padding: 25rpx 0 25rpx 10rpx;&:nth-child(1) {border-right: 1rpx solid #eee;}}} }.van-tabs {z-index: 0; }.goods_detail_container {padding-bottom: 50px !important; }
9.8 渲染图文详情面板
-
全局注册
wxparse
组件:usingComponents: { // ...// 把 HTML 代码转换为 WXML 代码的插件wxparse: './assets/wxparse/wxparse' }
-
在详情页使用
wxparse
组件:<van-tab title="图文详情"><wxparse data="{ {goodsInfo.goods_introduce}}"></wxparse> </van-tab>
9.9 实现轮播图预览效果
-
为轮播图中的每一张图片,绑定点击事件处理函数:
<image src="{ {item.pics_big}}" @tap="preview({ {item.pics_big}})"></image>
-
在事件处理函数中,调用
wepy.previewImage()
函数,实现轮播图预览效果:methods = { // 点击预览图片preview(current) { wepy.previewImage({ // 所有图片的路径urls: this.goodsInfo.pics.map(x => x.pics_big),// 当前默认看到的图片current: current})} }
9.10 选择收获地址
-
给收货地址对应的
view
组件,绑定点击事件处理函数:<!-- 收货地址区域 --> <view class="box3" @tap="chooseAddress"></view>
-
通过
wepy.chooseAddress()
函数,选择收货地址:// 获取用户的收货地址 async chooseAddress() { const res = await wepy.chooseAddress().catch(err => err)if (res.errMsg !== 'chooseAddress:ok') { return wepy.baseToast('获取收货地址失败!')}this.addressInfo = res// 将选择的收获地址,存储到本地 Storage 中wepy.setStorageSync('address', res)this.$apply() }
9.11 通过计算属性动态渲染收货地址
-
定义计算属性节点:
computed = { addressStr() { if (this.addressInfo === null) { return '请选择收货地址'}const addr = this.addressInfoconst str =addr.provinceName + addr.cityName + addr.countyName + addr.detailInforeturn str} }
-
在页面上渲染对应的收货地址:
<!-- 收货地址区域 --> <view class="box3" @tap="chooseAddress"><view><text>送至</text><text>{ {addressStr}}</text></view><van-icon name="arrow"></van-icon> </view>
9.12 全局注册并使用商品导航组件
-
在
app.wpy
中全局注册商品导航组件
:usingComponents: { // ...// 商品导航区域'van-goods-action': './assets/vant/goods-action/index','van-goods-action-icon': './assets/vant/goods-action-icon/index','van-goods-action-button': './assets/vant/goods-action-button/index' }
-
在详情页中使用
商品导航组件
:<!-- 商品导航区域 --> <van-goods-action><van-goods-action-icon icon="chat-o" text="客服" /><van-goods-action-icon icon="cart-o" text="购物车" url="/pages/tabs/cart" link-type="switchTab" /><van-goods-action-button text="加入购物车" type="warning" /><van-goods-action-button text="立即购买" /> </van-goods-action>
9.13 客服功能
-
为客服按钮,添加
open-type
属性:<van-goods-action-icon icon="chat-o" text="客服" open-type="contact" />
-
登录小程序后台,在
功能 -> 客服
面板中,可以维护客服人员列表,也可以登录网页端客服,为用户提供客服服务。
10. 加入购物车
10.1 为加入购物车按钮绑定单击事件处理函数
-
为
加入购物车
按钮,绑定单击事件处理函数:<van-goods-action-button text="加入购物车" type="warning" bind:click="addToCart" />
-
在
methods
中定义对应的事件处理函数:// 点击按钮,把商品添加到购物车列表中 addToCart() { // 获取到当前商品的所有信息console.log(this.goodsInfo)// 提示用户加入购物车成功wepy.showToast({ title: '已加入购物车',icon: 'success'}) }
10.2 在app.wpy
中定义全局共享的数据和方法
-
在
globalData
中,定义全局的购物车列表:// 专门存储全局共享的数据 // 只需要通过 this.$parent.globalData 就可以拿到这个全局共享的数据对象 globalData = { // 全局的购物车列表cart: [] }
-
和
globalData
平级,定义全局可调用的函数:test() { console.log('ok') }
-
在每个小程序页面中,可通过
this.$parent
访问全局的数据或函数:// 点击按钮,把商品添加到购物车列表中 addToCart() { // 获取到当前商品的所有信息// console.log(this.goodsInfo)console.log(this.$parent.globalData)console.log(this.$parent)// 提示用户加入购物车成功wepy.showToast({ title: '已加入购物车',icon: 'success'}) }
10.3 把商品详情直接存储到购物车中
-
点击加入购物车按钮时候,直接调用全局函数:
// 点击按钮,把商品添加到购物车列表中 addToCart() { // ...this.$parent.addGoodsToCart(this.goodsInfo)// ... }
-
在
app.wpy
中,定义全局函数addGoodsToCart()
如下:// 把商品,添加到购物车列表中 addGoodsToCart(goods) { this.globalData.cart.push(goods) }
10.4 优化商品对象存储到购物车中的过程
-
在将商品添加至购物车期间,不需要用到商品的所有属性,因此可以有选择地将需要的属性,梳理为一个新对象,保存到购物车列表中,具体代码如下:
// 把商品,添加到购物车列表中 addGoodsToCart(goods) { // 梳理出来的商品信息对象const info = { // 商品Idid: goods.goods_id,// 名称name: goods.goods_name,// 图片pic: goods.goods_small_logo,// 价格price: goods.goods_price,// 数量count: 1,// 是否默认被选中isCheck: true}// 将整理出来的商品信息对象,存储到购物车列表中this.globalData.cart.push(info) }
10.5 防止商品重复添加
// 把商品,添加到购物车列表中addGoodsToCart(goods) {
// 先根据 Id 查找购物车列表中,是否已经存储了对应的商品信息// 如果查找的结果,值为 -1,证明要添加的商品不存在于购物车中,可以直接 push// 如果查找的结果,值不为 -1,证明要添加的商品,已存在以购物车中,此时直接更新数量即可!!!const i = this.globalData.cart.findIndex(x => x.id === goods.goods_id)if (i !== -1) {
this.globalData.cart[i].count++return}// ...}
10.6 持久化存储购物车数据
-
在
app.wpy
全局,定义函数saveCartToStorage()
如下:// 将购物车中的商品数据,持久化保存到本地 saveCartToStorage() { wepy.setStorageSync('cart', this.globalData.cart) }
-
凡是对购物车中的数据做了操作,在操作完毕后,已经要调用
saveCartToStorage
函数:// 把商品,添加到购物车列表中 addGoodsToCart(goods) { const i = this.globalData.cart.findIndex(x => x.id === goods.goods_id)if (i !== -1) { this.globalData.cart[i].count++this.saveCartToStorage()return}console.log(goods)// 梳理出来的商品信息对象const info = { // 商品Idid: goods.goods_id,// 名称name: goods.goods_name,// 图片pic: goods.goods_small_logo,// 价格price: goods.goods_price,// 数量count: 1,// 是否默认被选中isCheck: true}// 将整理出来的商品信息对象,存储到购物车列表中this.globalData.cart.push(info)this.saveCartToStorage() }
-
在小程序启动的
onLaunch()
生命周期函数中,加载storage
中的数据,并赋值给购物车列表:onLaunch() { console.log('小程序启动了')this.globalData.cart = wepy.getStorageSync('cart') || [] }
10.7 自定义编译模式并美化空白购物车的页面结构
-
将购物车页面,添加为新的自定义编译模式
-
将
素材
中的cart_empty@2x.png
图片,拷贝到 项目的src/assets/images
目录中 -
打开
cart.wpy
页面,渲染对应的UI结构:<template><view><!-- 空白的购物车 --><view class="empty_cart"><image src="/assets/images/cart_empty@2x.png" /><view>哎呦,购物车是空的噢~</view></view><!-- 非空的购物车 --></view> </template>
-
在
cart.wpy
的style
节点中,美化空白购物车的样式效果:<style lang="less"> .empty_cart {font-size: 26rpx;color: #666;text-align: center;padding-top: 200rpx;image {width: 180rpx;height: 180rpx;} } </style>
11. 购物车
11.1 实现空白购物车和非空购物车的按需展示
-
定义购物车私有数据:
data = { // 购物车商品列表cart: [] }
-
在页面加载期间,转存购物车数据到当前页面中:
onLoad() { this.cart = this.$parent.globalData.cart }
-
定义计算属性:
computed = { // 判断购物车是否为空isEmpty() { if (this.cart.length <= 0) { return true}return false} }
-
按需渲染页面结构:
<!-- 空白的购物车 --> <view class="empty_cart" wx:if="{ {isEmpty}}"></view><!-- 非空的购物车 --> <view class="cart-container" wx:else></view>
11.2 渲染购物车列表标题
-
需要用到
van-cell
单元格组件:<!-- 购物车标题 --> <van-cell title="购物车列表" icon="shop-o" />
11.3 循环渲染基本的商品列表结构
<!-- 购物车商品列表 -->
<block wx:for="{
{cart}}" wx:key="id"><van-card title="{
{item.name}}" thumb="{
{item.pic}}"></van-card>
</block>
11.4 通过插槽自定义渲染商品的价格和数量
-
全局注册
van-stepper
组件:// 引用并注册全局组件 usingComponents: { // Stepper 步进器'van-stepper': './assets/vant/stepper/index' }
-
通过插槽自定义渲染价格与数量:
<van-card title="{ {item.name}}"><!-- 自定义商品的描述区域 --><view slot="desc" class="desc"><!-- 商品的价格 --><text class="price">¥{ {item.price}}</text><!-- 商品的数量 --><van-stepper value="{ {item.count}}" /></view> </van-card>
11.5 美化商品价格和数量区域
.desc {display: flex;justify-content: space-between;align-items: center;position: absolute;bottom: 0;width: 100%;.price {color: red;font-weight: bold;font-size: 12px;}
}
11.6 数量改变时获取最新的数量值和当前商品的Id
-
为
van-stepper
组件绑定事件,并通过自定义属性传参:<van-stepper value="{ {item.count}}" bind:change="countChanged" data-id="{ {item.id}}" />
-
定义事件处理函数:
countChanged(e) { // 获取到变化之后最新的数量值const count = e.detail// 商品的Id值const id = e.target.dataset.id }
11.7 完成商品数量的更新操作
-
在
app.wpy
中定义如下函数:// 更新商品的数量 updateGoodsCount(id, count) { const i = this.globalData.cart.findIndex(x => x.id === id)if (i !== -1) { // 根据索引值,获取到对应的那个商品,// 然后更新数量this.globalData.cart[i].count = count// 把更新过后的购物车数据,立即存储到Storage中this.saveCartToStorage()} }
-
当数量改变时,调用这个全局函数:
methods = { // 监听商品数量变化的事件countChanged(e) { // 获取到变化之后最新的数量值const count = e.detail// 商品的Id值const id = e.target.dataset.idthis.$parent.updateGoodsCount(id, count)} }
11.8 在商品之间渲染分割线
-
为
van-card
组件添加下边框线:.van-card {border-bottom: 1rpx solid #eee; }
11.9 通过thumb插槽渲染复选框和缩略图
-
注册复选框组件:
// 引用并注册全局组件 usingComponents: { // 复选框'van-checkbox': './assets/vant/checkbox/index' }
-
渲染对应的UI结构:
<van-card title="{ {item.name}}"><!-- 自定义渲染缩略图的插槽 --><view slot="thumb" class="thumb"><!-- 复选框 --><van-checkbox value="{ { item.isCheck }}"></van-checkbox><!-- 缩略图 --><image src="{ {item.pic}}" /></view> </van-card>
11.10. 美化复选框和缩略图的样式
.van-card {border-bottom: 1rpx solid #eee;padding-left: 7px !important;
}.thumb {display: flex;align-items: center;width: 118px;image {width: 90px;height: 90px;margin-left: 8px;}
}.van-card__thumb {width: 118px !important;
}
11.11 监听复选框状态变化的事件
-
绑定事件处理函数:
<van-checkbox value="{ { item.isCheck }}" checked-color="#d81e06" bind:change="statusChanged" data-id="{ {item.id}}"></van-checkbox>
-
定义事件处理函数:
methods = { // 当商品前面的复选框,选中状态变化,会触发这个函数statusChanged(e) { // console.log(e)// 当前最新的选中状态const status = e.detail// 当前点击项对应的商品Idconst id = e.target.dataset.id} }
11.12 修改商品的选中状态
-
在
app.wpy
中定义如下函数:// 更新商品的选中状态 updateGoodsStatus(id, status) { const i = this.globalData.cart.findIndex(x => x.id === id)if (i !== -1) { this.globalData.cart[i].isCheck = statusthis.saveCartToStorage()} }
-
调用全局定义的函数:
methods = { // 当商品前面的复选框,选中状态变化,会触发这个函数statusChanged(e) { // console.log(e)// 当前最新的选中状态const status = e.detail// 当前点击项对应的商品Idconst id = e.target.dataset.idthis.$parent.updateGoodsStatus(id, status)} }
11.13 初步实现滑动删除的UI效果
-
注册
van-swipe-cell
组件:// 引用并注册全局组件 usingComponents: { // 滑动单元格组件'van-swipe-cell': './assets/vant/swipe-cell/index' }
-
把商品卡片使用滑动单元格组件包裹起来:
<van-swipe-cell right-width="{ { 65 }}" left-width="{ { 0.1 }}"><van-card title="{ {item.name}}"><!--省略不必要的代码--></van-card><view slot="right" class="close">删除</view> </van-swipe-cell>
11.14 美化滑动单元格右侧的删除按钮
.close {background-color: #ff4444;width: 65px;height: 100%;color: white;font-size: 13px;display: flex;justify-content: center;align-items: center;
}
11.15 根据Id从购物车列表中删除对应的商品
-
为删除按钮绑定事件处理函数:
<view slot="right" class="close" @tap="close({ {item.id}})">删除</view>
-
定义事件处理函数:
// 点击删除对应的商品 close(id) { this.$parent.removeGoodsById(id) }
-
在
app.wpy
中定义removeGoodsById()
函数如下:// 根据Id删除对应的商品 removeGoodsById(id) { const i = this.globalData.cart.findIndex(x => x.id === id)if (i !== -1) { this.globalData.cart.splice(i, 1)this.saveCartToStorage()} }
11.16 渲染提交订单区域的UI结构
-
全局注册
van-submit-bar
组件:// 引用并注册全局组件 usingComponents: { // 提交订单'van-submit-bar': './assets/vant/submit-bar/index' }
-
在页面上绘制提交订单的UI结构:
<!-- 提交订单区域 --> <van-submit-bar price="{ { amount }}" button-text="提交订单" bind:submit="submitOrder" tip="{ { false }}"><!-- 全选/反选 的复选框 --><van-checkbox class="fullCheck" value="{ {isFullChecked}}" checked-color="#d81e06">全选</van-checkbox> </van-submit-bar>
-
美化全选复选框的样式:
.fullCheck { margin-left: 7px; }
11.17 通过计算属性动态计算勾选商品的总价格
// 总价格,单位是 分
amount() {
let total = 0 // 单位是 元this.cart.forEach(x => {
if (x.isCheck) {
total += x.price * x.count}})return total * 100
}
11.18 通过计算属性判断全选的状态
// 是否全选
isFullChecked() {
// 获取所有商品的个数const allCount = this.cart.lengthlet c = 0this.cart.forEach(x => {
if (x.isCheck) {
c++}})return allCount === c
}
11.19 点击全选更新所有商品的选中状态
-
为全选的复选框绑定点击事件:
<!-- 全选/反选 的复选框 --> <van-checkbox class="fullCheck" value="{ {isFullChecked}}" checked-color="#d81e06" bind:change="onFullCheckChanged">全选</van-checkbox>
-
监听全选复选框值改变的事件:
// 监听全选复选框值改变的事件 onFullCheckChanged(e) { this.$parent.updateAllGoodsStatus(e.detail) }
-
在
app.wpy
中定义updateAllGoodsStatus()
函数:// 更新购物车中每件商品的选中状态 updateAllGoodsStatus(status) { this.globalData.cart.forEach(x => { x.isCheck = status})this.saveCartToStorage() }
11.20 为TabBar中的购物车添加数字徽章
-
在
app.wpy
中定义渲染徽章的函数:// 渲染购物车的徽章 async renderCartBadge() { // 计算已勾选的商品数量let c = 0this.globalData.cart.forEach(x => { if (x.isCheck) { c += x.count}})// 调用 API,将已勾选的商品数量渲染到指定的 TabBar 中const res = await wepy.setTabBarBadge({ index: 3,text: c + ''}).catch(err => err)// 设置 tabBar 的徽章失败!if (res.errMsg !== 'setTabBarBadge:ok') { } }
-
在
app.wpy
的onLaunch()
生命周期函数中,调用刚才定义的函数:onLaunch() { console.log('小程序启动了')this.globalData.cart = wepy.getStorageSync('cart') || []this.renderCartBadge() }
-
在
app.wpy
中saveCartToStorage()
的函数中,新增如下代码:// 将购物车中的商品数据,持久化保存到本地 saveCartToStorage() { wepy.setStorageSync('cart', this.globalData.cart)+ this.renderCartBadge() }
11.21 为商品详情页的购物车图标添加数字徽章
-
在
app.wpy
中,定义如下的全局共享数据:globalData = { // 全局的购物车列表cart: [],// 当前购物车中已经勾选的商品数量+ total: 0 }
-
修改
app.wpy
中的renderCartBadge()
函数:// 渲染购物车的徽章 async renderCartBadge() { let c = 0this.globalData.cart.forEach(x => { if (x.isCheck) { c += x.count}})+ // 更新全局的商品数量+ this.globalData.total = cconst res = await wepy.setTabBarBadge({ index: 3,text: c + ''}).catch(err => err)// 设置 tabBar 的徽章失败!if (res.errMsg !== 'setTabBarBadge:ok') { } }
-
在
src/mixins/goods_detail/main.js
中,定义计算属性如下:computed = { // 所有已经勾选的商品的数量total() { return this.$parent.globalData.total} }
-
在
src/pages/goods_detail/main.wpy
中,修改UI结构如下:<!-- 商品导航区域 --> <van-goods-action><!--省略其他代码--><van-goods-action-icon icon="cart-o" text="购物车" url="/pages/tabs/cart" link-type="switchTab" info="{ {total}}" /><!--省略其他代码--> </van-goods-action>
11.22 提交订单
-
为购物车页面的
提交订单区域
绑定bind:submit
事件处理函数:<!-- 提交订单区域 --> <van-submit-bar price="{ { amount }}" button-text="提交订单" bind:submit="submitOrder" tip="{ { false }}"><!--省略其他代码--> </van-submit-bar>
-
在
methods
节点中新增如下处理函数:// 提交订单 submitOrder() { if (this.amount <= 0) { return wepy.baseToast('订单金额不能为空!')}wepy.navigateTo({ url: '/pages/order'}) }
-
在
src/pages
目录中,新建order.wpy
页面文件,并初始化 -
在
src/mixins
目录中,新建order.js
逻辑文件,并初始化 -
将
order.js
导入并挂在到order.wpy
中 -
将新页面的路径,记录到
src/app.wpy
文件的config -> pages
数组中:export default class extends wepy.app { config = { pages: [// 省略其他不必要的代码// 确认订单页面'pages/order']} }
12. 确认订单
12.1 修改确认订单页面的标题
<script>
import wepy from 'wepy'
import mix from '@/mixins/order.js'export default class extends wepy.page {
config = {
// 设置当前页面的标题navigationBarTitleText: '确认订单'}mixins = [mix]
}
</script>
12.2 渲染选择收货地址区域的UI结构
-
绘制UI结构:
<!-- 选择收货地址按钮区域 --> <view class="choose_address_box"><van-button type="info" size="small">+ 选择收货地址</van-button> </view><!-- 分割线 --> <image src="/assets/images/cart_border@2x.png" class="sep_line"></image>
-
美化样式:
.choose_address_box {text-align: center;padding: 60rpx 0; }.sep_line {height: 7px;width: 100%;display: block; }
12.3 选择收货地址
-
为选择收货地址按钮绑定点击事件处理函数:
<van-button type="info" size="small" @tap="chooseAddress">+ 选择收货地址</van-button>
-
在
methods
中定义事件处理函数:// 选择收货地址 async chooseAddress() { const res = await wepy.chooseAddress().catch(err => err)if (res.errMsg !== 'chooseAddress:ok') { return}this.addressInfo = reswepy.setStorageSync('address', res)this.$apply() }
12.4 在订单页面加载期间读取收货地址
onLoad() {
// 读取收货地址this.addressInfo = wepy.getStorageSync('address') || null
}
12.5 通过计算属性控制收货地址按钮和收货人信息区域的按需显示
-
定义计算属性:
computed = { isHaveAddress() { if (this.addressInfo === null) { return false}return true} }
-
按需展示:
<!-- 选择收货地址按钮区域 --> <view class="choose_address_box" wx:if="{ {isHaveAddress === false}}"><!--省略不必要的代码--> </view><!-- 收货人信息区域 --> <view class="address_box" wx:else><!--省略不必要的代码--> </view>
12.6 渲染并美化收货信息区域
-
渲染收货信息区域的UI结构:
<!-- 收货人信息区域 --> <view class="address_box" wx:else><!-- 收货人,联系电话 --><view class="box1"><text>收货人:{ {addressInfo.userName}}</text><view @tap="chooseAddress"><text>联系电话:{ {addressInfo.telNumber}}</text><van-icon name="arrow" /></view></view><!-- 收货地址 --><view class="box2">收货地址:{ {addressStr}}</view> </view>
-
美化样式:
.address_box {font-size: 26rpx;padding: 0 10rpx;.box1 {display: flex;justify-content: space-between;padding: 30rpx 0;}.box2 {padding-bottom: 30rpx;} }
12.7 点击联系电话区域重新选择收货地址
<!-- 收货人,联系电话 -->
<view class="box1"><text>收货人:{
{addressInfo.userName}}</text><view @tap="chooseAddress"><text>联系电话:{
{addressInfo.telNumber}}</text><van-icon name="arrow" /></view>
</view>
12.8 渲染订单商品列表
-
渲染结构:
<!-- 商品列表 --> <view class="goods_list"><block wx:for="{ {cart}}" wx:key="id"><van-card num="{ {item.count}}" price="{ {item.price}}" title="{ {item.name}}" thumb="{ {item.pic}}" /></block> </view>
-
美化样式:
.van-card {border-bottom: 1rpx solid #eee; }
12.9 渲染登录后下单的按钮
-
在
app.wpy
中全局注册按钮组件:// 引用并注册全局组件 usingComponents: { // 按钮组件'van-button': './assets/vant/button/index' }
-
渲染按钮区域
<!-- 登录后下单 --> <van-button type="primary" size="large" class="btnLogin">登录后下单</van-button>
-
美化样式:
.btnLogin {position: fixed;bottom: 0;width: 100%; }.order_container {padding-bottom: 50px; }
13. 订单支付
13.1 准备登录相关的参数 - 获取用户信息
-
为登录按钮设置
open-type
和bindgetuserinfo
属性:<!-- 登录后下单 --> <van-button type="primary" size="large" class="btnLogin" open-type="getUserInfo" bindgetuserinfo="getUserInfo">登录后下单</van-button>
-
定义事件处理函数
getUserInfo
,并通过形参接收用户信息:methods = { // 获取用户信息async getUserInfo(userInfo) { // 判断是否获取用户信息失败if (userInfo.detail.errMsg !== 'getUserInfo:ok') { return wepy.baseToast('获取用户信息失败!')}console.log(userInfo)} }
13.2 准备登录相关的参数 - 获取用户登录凭证
-
在
getUserInfo
处理函数中,新增如下代码:// 获取用户登录的凭证 Codeconst loginRes = await wepy.login()console.log(loginRes)if (loginRes.errMsg !== 'login:ok') { return wepy.baseToast('微信登录失败!')}// 登录的参数const loginParams = { code: loginRes.code,encryptedData: userInfo.detail.encryptedData,iv: userInfo.detail.iv,rawData: userInfo.detail.rawData,signature: userInfo.detail.signature}
13.3 实现登录功能并得到登录成功后的Token值
-
在
getUserInfo
处理函数中,新增如下代码:// 发起登录的请求,换取登录成功之后的 Token 值const { data: res } = await wepy.post('/users/wxlogin', loginParams)console.log(res)if (res.meta.status !== 200) { return wepy.baseToast('微信登录失败!')}// 把登录成功之后的 Token 字符串,保存到 Storage 中wepy.setStorageSync('token', res.message.token)this.islogin = truethis.$apply()
13.4 按需渲染订单支付区域
-
通过
wx:if
和wx:else
按需渲染登录按钮
和订单支付
区域:<!-- 登录后下单 --> <van-button type="primary" size="large" class="btnLogin" open-type="getUserInfo" bindgetuserinfo="getUserInfo" wx:if="{ {islogin === false}}">登录后下单</van-button><!-- 订单支付区域 --> <van-submit-bar price="{ {amount}}" button-text="支付订单" bind:submit="onSubmit" wx:else></van-submit-bar>
13.5 通过拦截器为header请求头添加Authorization字段
-
打开
app.wpy
文件,找到constructor
构造函数中定义的拦截器,从而自定义 header 请求头:constructor() { // ...// 拦截器this.intercept('request', { // 发出请求时的回调函数config(p) { // 显示loading效果wepy.showLoading({ title: '数据加载中...'})// 自定义请求头p.header = { Authorization: wepy.getStorageSync('token')}// console.log(p)// 必须返回OBJECT参数对象,否则无法发送请求到服务端return p},// ...} }
13.6 实现下单及支付功能
-
通过
bind:submit
为支付订单按钮,绑定事件处理函数:<!-- 订单支付区域 --> <van-submit-bar price="{ {amount}}" button-text="支付订单" bind:submit="onSubmit" wx:else></van-submit-bar>
-
定义事件处理函数
onSubmit
,并实现下单及支付功能:// 支付订单 async onSubmit() { if (this.amount <= 0) { return wepy.baseToast('订单金额不能为0!')}if (this.addressStr.length <= 0) { return wepy.baseToast('请选择收货地址!')}// 1. 创建订单const { data: createResult } = await wepy.post('/my/orders/create', { // 订单金额 单位 元order_price: '0.01',consignee_addr: this.addressStr,order_detail: JSON.stringify(this.cart),goods: this.cart.map(x => { return { goods_id: x.id,goods_number: x.count,goods_price: x.price}})})// 创建订单失败if (createResult.meta.status !== 200) { return wepy.baseToast('创建订单失败!')}// 创建订单成功了const orderInfo = createResult.messageconsole.log(orderInfo)// 2. 生成预支付订单const { data: orderResult } = await wepy.post('/my/orders/req_unifiedorder',{ order_number: orderInfo.order_number})// 生成预支付订单失败if (orderResult.meta.status !== 200) { return wepy.baseToast('生成预支付订单失败!')}// 走支付的流程// 3. 调用微信支付的API// console.log(orderResult)const payResult = await wepy.requestPayment(orderResult.message.pay).catch(err => err)// 用户取消了支付if (payResult.errMsg === 'requestPayment:fail cancel') { return wepy.baseToast('您已取消了支付!')}// 用户完成了支付的过程// 4. 检查用户支付的结果const { data: payCheckResult } = await wepy.post('/my/orders/chkOrder', { order_number: orderInfo.order_number})if (payCheckResult.meta.status !== 200) { return wepy.baseToast('订单支付失败!')}// 5. 提示用户支付成功wepy.showToast({ title: '支付成功!'})// 6. 跳转到订单列表页面wepy.navigateTo({ url: '/pages/orderlist'}) }
14. 订单列表
14.1 渲染标签页
-
渲染UI结构:
<van-tabs active="{ { active }}" bind:change="tabChanged"><van-tab title="全部">全部</van-tab><van-tab title="待付款">待付款</van-tab><van-tab title="已付款">已付款</van-tab> </van-tabs>
-
在 data 中定义 active 属性值:
data = { // 默认被激活的标签页的索引active: 0 }
-
在 methods 中定义事件处理函数
tabChanged
:methods = { // 每当切换标签页的时候,都会触发这个函数tabChanged(e) { console.log(e)this.active = e.detail.index} }
14.2 获取订单列表数据
-
在 data 中定义数据列表:
data = { // 默认被激活的标签页的索引active: 0,// 全部 订单列表allOrderList: [],// 待付款 订单列表waitOrderList: [],// 已付款 订单列表finishOrderList: [] }
-
在
onLoad
中获取订单列表数据:onLoad() { this.getOrderList(this.active) }
-
在标签页发生切换时,获取订单列表数据:
// 每当切换标签页的时候,都会触发这个函数 tabChanged(e) { console.log(e)this.active = e.detail.indexthis.getOrderList(this.active) }
-
定义函数
getOrderList
如下:// 获取订单列表 async getOrderList(index) { console.log(index)const { data: res } = await wepy.get('/my/orders/all', { type: index + 1 })if (res.meta.status !== 200) { return wepy.baseToast('获取订单列表失败!')}res.message.orders.forEach(x => (x.order_detail = JSON.parse(x.order_detail)))console.log(res)if (index === 0) { this.allOrderList = res.message.orders} else if (index === 1) { this.waitOrderList = res.message.orders} else if (index === 2) { this.finishOrderList = res.message.orders} else { wepy.baseToast('订单类型错误!')}this.$apply() }
14.3 通过Panel面板渲染订单信息
-
全局注册
Panel
面板组件:usingComponents: { // Panel 面板'van-panel': './assets/vant/panel/index' }
-
循环渲染订单信息面板:
<block wx:for="{ {allOrderList}}" wx:key="index"><view class="sep_line"></view><van-panel title="{ { '订单号:' + item.order_number}}"><block wx:for="{ {item.order_detail}}" wx:key="index"><van-card num="{ {item.count}}" price="{ {item.price}}" title="{ {item.name}}" thumb="{ {item.pic}}" /></block><!-- 商品件数,以及金额 --><van-cell value="共{ {item.total_count}}件商品,订单金额¥{ {item.order_price}}" /></van-panel> </block>
-
美化样式:
.sep_line {border-top: 15rpx solid #eee; }.van-card {border-bottom: 1rpx solid #eee; }
14.4 把订单Item项的UI结构封装为自定义组件
-
在
src/components
目录中,新建组件文件orderItem.wpy
如下:<template><view><view class="sep_line"></view><van-panel title="{ { '订单号:' + order.order_number}}"><block wx:for="{ {order.order_detail}}" wx:key="index"><van-card num="{ {item.count}}" price="{ {item.price}}" title="{ {item.name}}" thumb="{ {item.pic}}" /></block><!-- 商品件数,以及金额 --><van-cell value="共{ {order.total_count}}件商品,订单金额¥{ {order.order_price}}" /></van-panel></view> </template><script> import wepy from 'wepy'export default class extends wepy.component { data = { }// 外界传递过来的数据props = { // 外界把订单数据传递过来order: Object}methods = { } } </script><style lang="less"> .sep_line { border-top: 15rpx solid #eee; } .van-card { border-bottom: 1rpx solid #eee; } </style>
14.5 通过repeat循环创建自定义组件
-
在
orderlist.wpy
中,导入自定义的组件:import orderItem from '@/components/orderItem'
-
注册自定义组件:
// 注册自定义组件 components = { 'order-item': orderItem }
-
通过
WePY
官方提供的辅助组件<repeat>
循环创建自定义组件:<van-tabs active="{ { active }}" bind:change="tabChanged"><van-tab title="全部"><!-- repeat 组件并不是微信官方提供的,而是 WePY 框架提供的 --><repeat for="{ {allOrderList}}" key="index"><order-item :order="item"></order-item></repeat></van-tab><van-tab title="待付款"><repeat for="{ {waitOrderList}}" key="index"><order-item :order="item"></order-item></repeat></van-tab><van-tab title="已付款"><repeat for="{ {finishOrderList}}" key="index"><order-item :order="item"></order-item></repeat></van-tab> </van-tabs>
15. 发布小程序
- 在
微信开发者工具
的工具栏中,点击上传
按钮,填写版本号
和项目备注
,将最新的小程序项目代码,上传为 开发版本 - 登录到自己的
微信小程序后台主页
,点击管理 -> 版本管理
- 点击
开发版本
面板内的提交审核
按钮,填写相关内容后,将最新的开发版本
提交为 审核版本,由腾讯官方进行审核,审核过程需要等待若干天 - 当腾讯官方审核通过之后,可以将审核通过的小程序,提交发布为 线上版本,供所有微信用户进行使用
- 注意:只有 线上版本 才能被普通微信用户正常访问和使用,开发版本 和 审核版本 无法被普通用户访问和使用!