黑马微信小程序项目·黑马优购

目录

项目总效果展示:

一、新建uni-app项目

 项目目录结构

 运行项目至微信开发者工具上

使用git管理项目

 将项目托管至github上管理

二、实现tabBar效果

1.新建tabBar分支

2.新建tabBar页面

3.配置tabBar效果

4.修改导航条样式效果

 5.提交tabBar代码

三、实现首页效果

1.配置网络请求

2.挂载$http,配置请求拦截器和响应拦截器

3.完成轮播图

4.完成分类导航区域

5.楼层结构

6.提交home首页的代码

四、实现分类

1.实现基本结构

2.获取分类数据

3.渲染一级分类列表

4.渲染二级分类列表

5.渲染三级分类结构

6.切换一级分类后重置滚动条的位置

7.点击三级分类跳转到商品列表页面

 五、搜索功能

1.新建自定义组件

 2.通过自定义属性增强组件的通用性

3. 为自定义组件封装 click 事件

 4.实现首页搜索组件的吸顶效果

5.搜索建议的实现 

 6.搜索历史

 六、商品列表

1.定义请求参数对象

 2.获取商品列表数据

3. 渲染商品列表结构 

 4. 把商品 item 项封装为自定义组件

 5.使用过滤器处理价格

 6.上拉加载更多

7.通过节流阀防止发起额外的请求 

8.下拉刷新

 9.点击商品 item 项跳转到详情页面

七、商品详情

1.获取商品详情数据

2.渲染商品详情页轮播图区域

3.渲染商品信息区域 

 4.渲染商品详情信息

5.解决商品价格闪烁的问题 

 6.渲染商品导航区域的 UI 结构

7.点击跳转到购物车页面 

八、加入购物车 

1.配置vuex

 2.创建购物车的 store 模块

 3. 在商品详情页中使用 Store 中的数据

4.实现加入购物车的功能 

 5. 动态统计购物车中商品的总数量

 6.持久化存储购物车中的商品

7.优化商品详情页的 total 侦听器 

8.动态为 tabBar 页面设置数字徽标 

9.将设置 tabBar 徽标的代码抽离为 mixins 

九、购物车页面 

商品列表区域

1.渲染购物车商品列表的标题区域

 2.渲染商品列表区域的基本结构

 3.为 my-goods 组件封装 radio 勾选状态

 4.为 my-goods 组件封装 radio-change 事件

5.修改购物车中商品的勾选状态 

 6.为 my-goods 组件封装 NumberBox

 7. 为 my-goods 组件封装 num-change 事件

8.修改购物车中商品的数量

9.渲染滑动删除的 UI 效果 

 10.实现滑动删除的功能

收货地址区域 

1.创建收货地址的组件

2.渲染收货地址组件的基本结构:

3.实现收货地址区域的按需展示

4.实现选择收货地址的功能 

 5.将 address 信息存储到 vuex 中

6.将 Store 中的 address 持久化存储到本地 

7.将 addstr 抽离为 getters 

 8.重新选择收货地址

结算区域 

1.先新建一个结算的组件

2.初始化 my-settle 组件的基本结构和样式:

 3.渲染结算区域的结构和样式

4. 动态渲染已勾选商品的总数量

5. 动态渲染全选按钮的选中状态 

6.实现商品的全选/反选功能

7 动态渲染已勾选商品的总价格

 8.动态计算购物车徽标的数值

9.渲染购物车为空时的页面结构 

十、登录与支付 

1.点击结算按钮进行条件判断

 2.登录

2.1实现登录和用户信息组件的按需展示

 2.2 实现登录组件的基本布局

2.3 点击登录按钮获取微信用户的基本信息

 2.4 将用户的基本信息存储到 vuex

2.5 登录获取 Token 字符串 

2.6将 Token 存储到 vuex 

在 store/user.js 模块的 mutations 节点中,声明如下的两个方法:

3.用户信息 

3.1 实现用户头像昵称区域的基本布局

3.2渲染用户的头像和昵称 

 3.3 渲染面板信息区域

效果图

3.4实现退出登录的功能

4.三秒后自动跳转到登录页面 

 5.微信支付

1.在请求头中添加 Token 身份认证的字段

2.微信支付的流程


项目总效果展示:

黑马微信小程序项目·黑马优购_第1张图片黑马微信小程序项目·黑马优购_第2张图片

 黑马微信小程序项目·黑马优购_第3张图片

 黑马微信小程序项目·黑马优购_第4张图片

 黑马微信小程序项目·黑马优购_第5张图片

 黑马微信小程序项目·黑马优购_第6张图片

一、新建uni-app项目

黑马微信小程序项目·黑马优购_第7张图片

 项目目录结构

黑马微信小程序项目·黑马优购_第8张图片

 运行项目至微信开发者工具上

黑马微信小程序项目·黑马优购_第9张图片

 黑马微信小程序项目·黑马优购_第10张图片

 黑马微信小程序项目·黑马优购_第11张图片

黑马微信小程序项目·黑马优购_第12张图片

使用git管理项目

第一步:在项目目录中新建.gitignore文件,然后在里面写入要忽略的文件

/node_modules
/unpackage/dist

第二步:为了让git能够跟踪unpackage文件,需要在该文件目录下新建一个.gitkeep文件

第三步:打开git Bash终端初始化一个git仓库

git init 

再将文件添加到暂存区

git add .

然后进行第一次提交项目

git commit -m "init project"

 将项目托管至github上管理

首先新建一个仓库

黑马微信小程序项目·黑马优购_第13张图片

 按照下面的指示在git Bash上面操作即可

黑马微信小程序项目·黑马优购_第14张图片

!!!这里标错了一个地方 第四条那不是新建分支,而是修改现分支的名字,我们新建仓库默认的分支名字是master

 提交成功刷新后就可以看到我们的项目仓库已经有了刚提交的文件了

黑马微信小程序项目·黑马优购_第15张图片

 在后面我们只需要每次提交代码后只需要 git push -u origin 分支名 命令就可以提交代码至github了,但是一定要注意分支的不同喔。

二、实现tabBar效果

1.新建tabBar分支

git checkout -b tabBar

可以使用此命令查看所有的分支

git branch -v

2.新建tabBar页面

黑马微信小程序项目·黑马优购_第16张图片

 同理分别创建cate cart my 页面

3.配置tabBar效果

第一步:

将需要用到的tabBar的icon图标替换掉原来的static文件

第二步:

在page.json文件中配置tabBar

"tabBar": {
		"selectedColor": "#C00000",
		"list": [
			{
				"pagePath": "pages/home/home",
				"text": "首页",
				"selectedIconPath": "static/tab_icons/home-active.png",
				"iconPath": "static/tab_icons/home.png"
			},
			{
				"pagePath": "pages/cate/cate",
				"text": "分类",
				"selectedIconPath": "static/tab_icons/cate-active.png",
				"iconPath": "static/tab_icons/cate.png"
			},
			{
				"pagePath": "pages/cart/cart",
				"text": "购物车",
				"selectedIconPath": "static/tab_icons/cart-active.png",
				"iconPath": "static/tab_icons/cart.png"
			},
			{
				"pagePath": "pages/my/my",
				"text": "我的",
				"selectedIconPath": "static/tab_icons/my-active.png",
				"iconPath": "static/tab_icons/my.png"
			}
		]
	}

第三步:

将原来的index页面删除

黑马微信小程序项目·黑马优购_第17张图片

 效果就出来了

黑马微信小程序项目·黑马优购_第18张图片

4.修改导航条样式效果

黑马微信小程序项目·黑马优购_第19张图片

 5.提交tabBar代码

打开git Bash输入下面的命令

git add . 
git commit -m "完成tabBar效果" //提交本地
git push -u origin tabBar  //提交分支tabBar至远程仓库
git checkout main //切换分支到main
git merge tabBar //合并tabBar至main
git push origin main //提交合并后main至远程仓库
git branch -d tabBar //删除本地的tabBar分支

之后的新建分支与提交代码将不再展示

三、实现首页效果

1.配置网络请求

因为小程序不支持axios,而且原生的wx.request API的效果不能支持拦截器等功能,所以我们使用的是第三方包 在项目目录的终端中初始化一个npm包管理器,然后下载该包

npm init -y
npm i @escook/request-miniprogram

2.挂载$http,配置请求拦截器和响应拦截器

//main.js文件中
//导入网络请求的包
import	{$http} from '@escook/request-miniprogram'
uni.$http = $http

$http.beforeRequest = function(options){
	wx.showLoading({
		title:"数据加载中"
	})
}

$http.afterRequest = function(){
	uni.hideLoading()
}

3.完成轮播图

3.1 获取轮播图数据

先在main.js文件中配置请求路径

//请求根路径
$http.baseUrl = 'https://api-hmugo-web.itheima.net'

在home组件中请求数据

	data() {
			return {
				swiperList:[]
			};
		},
		onLoad() {
			//调用方法获取轮播图数据
			this.getSwiperList()
		},
		methods:{
			async getSwiperList(){
			const {data:res} = await uni.$http.get('/api/public/v1/home/swiperdata')
			if(res.meta.status !== 200){  //请求失败给一个弹窗
				return uni.showToast({
						title:'数据请求失败',
						duration:1500,
						icon:'none'
					})
			}
			this.swiperList = res.message
			}
		}

3.2 渲染轮播图


			
				 
				
				
			
		

//scss样式

swiper{
	height: 330rpx;
	.swiper-item,image{
		width: 100%;
		height: 100%;
	}
}

3.3 配置分包

在pages.json中添加如下配置后保存

"subPackages": [
		{
			"root": "subpkg",
			"pages": []
		}
	],

然后在subpkg目录下新建页面后选择subpkg分包

黑马微信小程序项目·黑马优购_第20张图片

 3.4 实现点击轮播图跳转至详情页并传递goods_id参数

修改ul结构

	
		
			
				 
				
				
			
		

3.5 封装 uni.$showMsg() 方法

在 main.js 中,为 uni 对象挂载自定义的 $showMsg() 方法:

// 封装的展示消息提示的方法
uni.$showMsg = function (title = '数据加载失败!', duration = 1500) {
  uni.showToast({
    title,
    duration,
    icon: 'none',
  })
}

今后,在需要提示消息的时候,直接调用 uni.$showMsg() 方法即可:

async getSwiperList() {
   const { data: res } = await uni.$http.get('/api/public/v1/home/swiperdata')
   if (res.meta.status !== 200) return uni.$showMsg()
   this.swiperList = res.message
}

4.完成分类导航区域

4.1 获取分类数据

export default {
  data() {
    return {
      // 1. 分类导航的数据列表
      navList: [],
    }
  },
  onLoad() {
    // 2. 在 onLoad 中调用获取数据的方法
    this.getNavList()
  },
  methods: {
    // 3. 在 methods 中定义获取数据的方法
    async getNavList() {
      const { data: res } = await uni.$http.get('/api/public/v1/home/catitems')
      if (res.meta.status !== 200) return uni.$showMsg()
      this.navList = res.message
    },
  },
}

4.2 渲染nav结构


		
		   
		   
		

scss

.nav-list {
  display: flex;
  justify-content: space-around;
  margin: 15px 0;

  .nav-img {
    width: 128rpx;
    height: 140rpx;
  }
}

点击事件函数

// nav-item 项被点击时候的事件处理函数
navClickHandler(item) {
  // 判断点击的是哪个 nav
  if (item.name === '分类') {
    uni.switchTab({
      url: '/pages/cate/cate'
    })
  }
}

5.楼层结构

5.1 获取数据

export default {
  data() {
    return {
      // 1. 楼层的数据列表
      floorList: [],
    }
  },
  onLoad() {
    // 2. 在 onLoad 中调用获取楼层数据的方法
    this.getFloorList()
  },
  methods: {
    // 3. 定义获取楼层列表数据的方法
    async getFloorList() {
      const { data: res } = await uni.$http.get('/api/public/v1/home/floordata')
      if (res.meta.status !== 200) return uni.$showMsg()
      this.floorList = res.message
    },
  },
}

5.2 渲染结构


		
			
			
				
				
				
				
				  
				  
				    
				  
				  
				  
				    
				      
				    
				  
				
			
		

scss

	.floor-title {
		height: 60rpx;
		width: 100%;
		display: flex;
	}
	.right-img-box {
	  display: flex;
	  flex-wrap: wrap;
	  justify-content: space-around;
	}
	
	.floor-img-box {
	  display: flex;
	  padding-left: 10rpx;
	}

5.3 实现图片点击跳转商品列表页面

在 subpkg 分包中,新建 goods_list 页面

楼层数据请求成功之后,通过双层 forEach 循环,处理 URL 地址:

// 获取楼层列表数据
async getFloorList() {
  const { data: res } = await uni.$http.get('/api/public/v1/home/floordata')
  if (res.meta.status !== 200) return uni.$showMsg()

  // 通过双层 forEach 循环,处理 URL 地址
  res.message.forEach(floor => {
    floor.product_list.forEach(prod => {
      prod.url = '/subpkg/goods_list/goods_list?' + prod.navigator_url.split('?')[1]
    })
  })

  this.floorList = res.message
}

把图片外层的 view 组件,改造为 navigator 组件,并动态绑定 url 属性 的值:



  
  
    
  
  
  
    
      
    
  

6.提交home首页的代码

按照之前的来就行

实现后的效果图

黑马微信小程序项目·黑马优购_第21张图片

四、实现分类

1.实现基本结构

  • 使用scroll-view实现滑动效果
  • 使用uni.getSystemInfoSync()获取到设备信息,达到自适应的效果
  • 进行样式的美化,其中使用伪元素实现红色指示边线





2.获取分类数据

3.渲染一级分类列表

  • 使用v-for循环渲染结构
  • 添加点击事件来切换active类名动态绑定

		
			
				
					{{item.cat_name}}
				
			

点击事件

methods: {
  // 选中项改变的事件处理函数
  activeChanged(i) {
    this.active = i
  }
}

4.渲染二级分类列表

4.1 在 data 中定义二级分类列表的数据节点

data() {
  return {
    // 二级分类列表
    cateLevel2: []
  }
}

 4.2 修改 getCateList 方法,在请求到数据之后,为二级分类列表数据赋值:

async getCateList() {
  const { data: res } = await uni.$http.get('/api/public/v1/categories')
  if (res.meta.status !== 200) return uni.$showMsg()
  this.cateList = res.message
  // 为二级分类赋值
  this.cateLevel2 = res.message[0].children
}

4.3 修改 activeChanged 方法,在一级分类选中项改变之后,为二级分类列表数据重新赋值:

activeChanged(i) {
  this.active = i
  // 为二级分类列表重新赋值
  this.cateLevel2 = this.cateList[i].children
}

4.4 循环渲染右侧二级分类列表的 UI 结构:



  
    / {{item2.cat_name}} /
  

4.5 美化二级分类的标题样式:

.cate-lv2-title {
  font-size: 12px;
  font-weight: bold;
  text-align: center;
  padding: 15px 0;
}

5.渲染三级分类结构

5.1 在二级分类的  组件中,循环渲染三级分类的列表结构:



  
    / {{item2.cat_name}} /
    
    
      
      
        
        
        
        {{item3.cat_name}}
      
    
  

5.2 美化三级分类的样式:

.cate-lv3-list {
  display: flex;
  flex-wrap: wrap;

  .cate-lv3-item {
    width: 33.33%;
    margin-bottom: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;

    image {
      width: 60px;
      height: 60px;
    }

    text {
      font-size: 12px;
    }
  }
}

6.切换一级分类后重置滚动条的位置

6.1 在 data 中定义 滚动条距离顶部的距离

data() {
  return {
    // 滚动条距离顶部的距离
    scrollTop: 0
  }
}

6.2 动态为右侧的  组件绑定 scroll-top 属性的值:


6. 3 切换一级分类时,动态设置 scrollTop 的值:注意这里切换时不能切换成和原来一样的值

// 选中项改变的事件处理函数
activeChanged(i) {
  this.active = i
  this.cateLevel2 = this.cateList[i].children

  // 让 scrollTop 的值在 0 与 1 之间切换
  this.scrollTop = this.scrollTop === 0 ? 1 : 0

  // 可以简化为如下的代码:
  // this.scrollTop = this.scrollTop ? 0 : 1
}

7.点击三级分类跳转到商品列表页面

7.1 为三级分类的 Item 项绑定点击事件处理函数如下:


  
  {{item3.cat_name}}

7.2 定义事件处理函数如下:

// 点击三级分类项跳转到商品列表页面
gotoGoodsList(item3) {
  uni.navigateTo({
    url: '/subpkg/goods_list/goods_list?cid=' + item3.cat_id
  })
}

实现效果图:

黑马微信小程序项目·黑马优购_第22张图片

 五、搜索功能

1.新建自定义组件

在项目目录中新建一个components文件夹,然后新建自定义组件

黑马微信小程序项目·黑马优购_第23张图片

 在分类页面的 UI 结构中,直接以标签的形式使用 my-search 自定义组件:


定义 my-search 组件的 UI 结构如下:

美化自定义 search 组件的样式:

.my-search-container {
  background-color: #c00000;
  height: 50px;
  padding: 0 10px;
  display: flex;
  align-items: center;
}

.my-search-box {
  height: 36px;
  background-color: #ffffff;
  border-radius: 15px;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;

  .placeholder {
    font-size: 15px;
    margin-left: 5px;
  }
}

由于自定义的 my-search 组件高度为 50px,因此,需要重新计算分类页面窗口的可用高度:

onLoad() {
  const sysInfo = uni.getSystemInfoSync()
  // 可用高度 = 屏幕高度 - navigationBar高度 - tabBar高度 - 自定义的search组件高度
  this.wh = sysInfo.windowHeight - 50
}

 2.通过自定义属性增强组件的通用性

为了增强组件的通用性,我们允许使用者自定义搜索组件的 背景颜色 和 圆角尺寸

通过 props 定义 bgcolor 和 radius 两个属性,并指定值类型和属性默认值:

props: {
  // 背景颜色
  bgcolor: {
    type: String,
    default: '#C00000'
  },
  // 圆角尺寸
  radius: {
    type: Number,
    // 单位是 px
    default: 18
  }
}

通过属性绑定的形式,为 .my-search-container 盒子和 .my-search-box 盒子动态绑定 style 属性:


  
    
    搜索
  

移除对应 scss 样式中的 背景颜色 和 圆角尺寸

.my-search-container {
  // 移除背景颜色,改由 props 属性控制
  // background-color: #C00000;
  height: 50px;
  padding: 0 10px;
  display: flex;
  align-items: center;
}

.my-search-box {
  height: 36px;
  background-color: #ffffff;
  // 移除圆角尺寸,改由 props 属性控制
  // border-radius: 15px;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;

  .placeholder {
    font-size: 15px;
    margin-left: 5px;
  }
}

这样在使用组件时就可以传递参数

3. 为自定义组件封装 click 事件

3.1 在 my-search 自定义组件内部,给类名为 .my-search-box 的 view 绑定 click 事件处理函数:


  
  搜索

3.2 在 my-search 自定义组件的 methods 节点中,声明事件处理函数如下:

methods: {
  // 点击了模拟的 input 输入框
  searchBoxHandler() {
    // 触发外界通过 @click 绑定的 click 事件处理函数
    this.$emit('click')
  }
}

3.3 在分类页面中使用 my-search 自定义组件时,即可通过 @click 为其绑定点击事件处理函数: 

 3.4 同时在分类页面中,定义 gotoSearch 事件处理函数如下:

methods: {
   // 跳转到分包中的搜索页面
   gotoSearch() {
     uni.navigateTo({
       url: '/subpkg/search/search'
     })
   }
}

 4.实现首页搜索组件的吸顶效果

4.1 在 home 首页定义如下的 UI 结构:



  

4.2 在 home 首页定义如下的事件处理函数:

gotoSearch() {
  uni.navigateTo({
    url: '/subpkg/search/search'
  })
}

 4.3 通过如下的样式实现吸顶的效果:

.search-box {
  // 设置定位效果为“吸顶”
  position: sticky;
  // 吸顶的“位置”
  top: 0;
  // 提高层级,防止被轮播图覆盖
  z-index: 999;
}

5.搜索建议的实现 

5.1渲染搜索页面的基本结构

定义如下的 UI 结构:


  
  

实现搜索框的吸顶效果:

.search-box {
  position: sticky;
  top: 0;
  z-index: 999;
}

定义如下的 input 事件处理函数:

methods: {
  input(e) {
    // e 是最新的搜索内容
    console.log(e)
  }
}

 5.2 实现自动获取焦点

黑马微信小程序项目·黑马优购_第24张图片

 5.3 实现防抖处理

在 data 中定义防抖的延时器 timerId 如下:

data() {
  return {
    // 延时器的 timerId
    timer: null,
    // 搜索关键词
    kw: ''
  }
}

修改 input 事件处理函数如下:使用定时器

input(e) {
  // 清除 timer 对应的延时器
  clearTimeout(this.timer)
  // 重新启动一个延时器,并把 timerId 赋值给 this.timer
  this.timer = setTimeout(() => {
    // 如果 500 毫秒内,没有触发新的输入事件,则为搜索关键词赋值
    this.kw = e.value
    console.log(this.kw)
  }, 500)
}

5.4 根据关键词查询搜索建议列表

在 data 中定义如下的数据节点,用来存放搜索建议的列表数据:

data() {
  return {
    // 搜索结果列表
    searchResults: []
  }
}

在防抖的 setTimeout 中,调用 getSearchList 方法获取搜索建议列表:

this.timer = setTimeout(() => {
  this.kw = e.value
  // 根据关键词,查询搜索建议列表
  this.getSearchList()
}, 500)

在 methods 中定义 getSearchList 方法如下: 

// 根据搜索关键词,搜索商品建议列表
async getSearchList() {
  // 判断关键词是否为空
  if (this.kw === '') {
    this.searchResults = []
    return
  }
  // 发起请求,获取搜索建议列表
  const { data: res } = await uni.$http.get('/api/public/v1/goods/qsearch', { query: this.kw })
  if (res.meta.status !== 200) return uni.$showMsg()
  this.searchResults = res.message
}

 5.5 渲染搜索建议列表

定义如下的 UI 结构:



  
    {{item.goods_name}}
    
  

 美化搜索建议列表:

.sugg-list {
  padding: 0 5px;

  .sugg-item {
    font-size: 12px;
    padding: 13px 0;
    border-bottom: 1px solid #efefef;
    display: flex;
    align-items: center;
    justify-content: space-between;

    .goods-name {
      // 文字不允许换行(单行文本)
      white-space: nowrap;
      // 溢出部分隐藏
      overflow: hidden;
      // 文本溢出后,使用 ... 代替
      text-overflow: ellipsis;
      margin-right: 3px;
    }
  }
}

点击搜索建议的 Item 项,跳转到商品详情页面:

gotoDetail(goods_id) {
  uni.navigateTo({
    // 指定详情页面的 URL 地址,并传递 goods_id 参数
    url: '/subpkg/goods_detail/goods_detail?goods_id=' + goods_id
  })
}

 6.搜索历史

6.1 渲染搜索历史记录的基本结构

在 data 中定义搜索历史的假数据

data() {
  return {
    // 搜索关键词的历史记录
    historyList: ['a', 'app', 'apple']
  }
}

渲染搜索历史区域的 UI 结构:



  
  
    搜索历史
    
  
  
  
    
  

美化搜索历史区域的样式:

.history-box {
  padding: 0 5px;

  .history-title {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 40px;
    font-size: 13px;
    border-bottom: 1px solid #efefef;
  }

  .history-list {
    display: flex;
    flex-wrap: wrap;

    .uni-tag {
      margin-top: 5px;
      margin-right: 5px;
    }
  }
}

实现搜索建议和搜索历史的按需展示



  




  

 将搜索关键词存入 historyList

methods: {
  // 根据搜索关键词,搜索商品建议列表
  async getSearchList() {
    // 省略其它不必要的代码...

    // 1. 查询到搜索建议之后,调用 saveSearchHistory() 方法保存搜索关键词
    this.saveSearchHistory()
  },
  // 2. 保存搜索关键词的方法
  saveSearchHistory() {
    // 2.1 直接把搜索关键词 push 到 historyList 数组中
    this.historyList.push(this.kw)
  }
}

6.2  解决关键字前后顺序的问题

  1. data 中的 historyList 不做任何修改,依然使用 push 进行末尾追加

  2. 定义一个计算属性 historys,将 historyList 数组 reverse 反转之后,就是此计算属性的值:

computed: {
  historys() {
    // 注意:由于数组是引用类型,所以不要直接基于原数组调用 reverse 方法,以免修改原数组中元素的顺序
    // 而是应该新建一个内存无关的数组,再进行 reverse 反转
    return [...this.historyList].reverse()
  }
}

页面中渲染搜索关键词的时候,不再使用 data 中的 historyList,而是使用计算属性 historys: 


  

6.3 解决关键词重复的问题

修改 saveSearchHistory 方法如下:

// 保存搜索关键词为历史记录
saveSearchHistory() {
  // this.historyList.push(this.kw)

  // 1. 将 Array 数组转化为 Set 对象
  const set = new Set(this.historyList)
  // 2. 调用 Set 对象的 delete 方法,移除对应的元素
  set.delete(this.kw)
  // 3. 调用 Set 对象的 add 方法,向 Set 中添加元素
  set.add(this.kw)
  // 4. 将 Set 对象转化为 Array 数组
  this.historyList = Array.from(set)
}

 6.4 将搜索历史记录持久化存储到本地

修改 saveSearchHistory 方法如下:

// 保存搜索关键词为历史记录
saveSearchHistory() {
  const set = new Set(this.historyList)
  set.delete(this.kw)
  set.add(this.kw)
  this.historyList = Array.from(set)
  // 调用 uni.setStorageSync(key, value) 将搜索历史记录持久化存储到本地
  uni.setStorageSync('kw', JSON.stringify(this.historyList))
}

 在 onLoad 生命周期函数中,加载本地存储的搜索历史记录:

onLoad() {
  this.historyList = JSON.parse(uni.getStorageSync('kw') || '[]')
}

6.5 清空搜索历史记录

为清空的图标按钮绑定 click 事件: 

在 methods 中定义 cleanHistory 处理函数:

// 清空搜索历史记录
cleanHistory() {
  // 清空 data 中保存的搜索历史
  this.historyList = []
  // 清空本地存储中记录的搜索历史
  uni.setStorageSync('kw', '[]')
}

 6.6 点击搜索历史跳转到商品列表页面

为搜索历史的 Item 项绑定 click 事件处理函数:

 在 methods 中定义 gotoGoodsList 处理函数:

// 点击跳转到商品列表页面
gotoGoodsList(kw) {
  uni.navigateTo({
    url: '/subpkg/goods_list/goods_list?query=' + kw
  })
}

 效果图:

黑马微信小程序项目·黑马优购_第25张图片

 六、商品列表

1.定义请求参数对象

data() {
  return {
    // 请求参数对象
    queryObj: {
      // 查询关键词
      query: '',
      // 商品分类Id
      cid: '',
      // 页码值
      pagenum: 1,
      // 每页显示多少条数据
      pagesize: 10
    }
  }
}

将页面跳转时携带的参数,转存到 queryObj 对象中:

onLoad(options) {
  // 将页面参数转存到 this.queryObj 对象中
  this.queryObj.query = options.query || ''
  this.queryObj.cid = options.cid || ''
}

 2.获取商品列表数据

在 data 中新增如下的数据节点:

data() {
  return {
    // 商品列表的数据
    goodsList: [],
    // 总数量,用来实现分页
    total: 0
  }
}

在 onLoad 生命周期函数中,调用 getGoodsList 方法获取商品列表数据:

onLoad(options) {
  // 调用获取商品列表数据的方法
  this.getGoodsList()
}

 在 methods 节点中,声明 getGoodsList 方法如下:

methods: {
  // 获取商品列表数据的方法
  async getGoodsList() {
    // 发起请求
    const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
    if (res.meta.status !== 200) return uni.$showMsg()
    // 为数据赋值
    this.goodsList = res.message.goods
    this.total = res.message.total
  }
}

3. 渲染商品列表结构 

在页面中,通过 v-for 指令,循环渲染出商品的 UI 结构:

为了防止某些商品的图片不存在,需要在 data 中定义一个默认的图片:

data() {
  return {
    // 默认的空图片
    defaultPic: 'https://img3.doubanio.com/f/movie/8dd0c794499fe925ae2ae89ee30cd225750457b4/pics/movie/celebrity-default-medium.png'
  }
}

 并在页面渲染时按需使用:

美化商品列表的 UI 结构:

.goods-item {
  display: flex;
  padding: 10px 5px;
  border-bottom: 1px solid #f0f0f0;

  .goods-item-left {
    margin-right: 5px;

    .goods-pic {
      width: 100px;
      height: 100px;
      display: block;
    }
  }

  .goods-item-right {
    display: flex;
    flex-direction: column;
    justify-content: space-between;

    .goods-name {
      font-size: 13px;
    }

    .goods-price {
      font-size: 16px;
      color: #c00000;
    }
  }
}

 4. 把商品 item 项封装为自定义组件

4.1首先在目录中新建对应的组件

4.2 将 goods_list 页面中,关于商品 item 项相关的 UI 结构、样式、data 数据,封装到 my-goods 组件中:





4.3 在 goods_list 组件中,循环渲染 my-goods 组件即可:


  
    
    
  

 5.使用过滤器处理价格

在 my-goods 组件中,和 data 节点平级,声明 filters 过滤器节点如下:

filters: {
  // 把数字处理为带两位小数点的数字
  tofixed(num) {
    return Number(num).toFixed(2)
  }
}

在渲染商品价格的时候,通过管道符 | 调用过滤器:


¥{{goods.goods_price | tofixed}}

 6.上拉加载更多

打开项目根目录中的 pages.json 配置文件,为 subPackages 分包中的 goods_list 页面配置上拉触底的距离:

黑马微信小程序项目·黑马优购_第26张图片

 在 goods_list 页面中,和 methods 节点平级,声明 onReachBottom 事件处理函数,用来监听页面的上拉触底行为:

// 触底的事件
onReachBottom() {
  // 让页码值自增 +1
  this.queryObj.pagenum += 1
  // 重新获取列表数据
  this.getGoodsList()
}

 改造 methods 中的 getGoodsList 函数,当列表数据请求成功之后,进行新旧数据的拼接处理:

// 获取商品列表数据的方法
async getGoodsList() {
  // 发起请求
  const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
  if (res.meta.status !== 200) return uni.$showMsg()

  // 为数据赋值:通过展开运算符的形式,进行新旧数据的拼接
  this.goodsList = [...this.goodsList, ...res.message.goods]
  this.total = res.message.total
}

7.通过节流阀防止发起额外的请求 

在 data 中定义 isloading 节流阀如下:

data() {
  return {
    // 是否正在请求数据
    isloading: false
  }
}

修改 getGoodsList 方法,在请求数据前后,分别打开和关闭节流阀:

// 获取商品列表数据的方法
async getGoodsList() {
  // ** 打开节流阀
  this.isloading = true
  // 发起请求
  const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
  // ** 关闭节流阀
  this.isloading = false

  // 省略其它代码...
}

 在 onReachBottom 触底事件处理函数中,根据节流阀的状态,来决定是否发起请求:

// 触底的事件
onReachBottom() {
  // 判断是否正在请求其它数据,如果是,则不发起额外的请求
  if (this.isloading) return

  this.queryObj.pagenum += 1
  this.getGoodsList()
}

判断数据是否加载完毕 

修改 onReachBottom 事件处理函数如下:

// 触底的事件
onReachBottom() {
  // 判断是否还有下一页数据
  if (this.queryObj.pagenum * this.queryObj.pagesize >= this.total) return uni.$showMsg('数据加载完毕!')

  // 判断是否正在请求其它数据,如果是,则不发起额外的请求
  if (this.isloading) return

  this.queryObj.pagenum += 1
  this.getGoodsList()
}

8.下拉刷新

在 pages.json 配置文件中,为当前的 goods_list 页面单独开启下拉刷新效果:

黑马微信小程序项目·黑马优购_第27张图片

 监听页面的 onPullDownRefresh 事件处理函数:

// 下拉刷新的事件
onPullDownRefresh() {
  // 1. 重置关键数据
  this.queryObj.pagenum = 1
  this.total = 0
  this.isloading = false
  this.goodsList = []

  // 2. 重新发起请求
  this.getGoodsList(() => uni.stopPullDownRefresh())
}

修改 getGoodsList 函数,接收 cb 回调函数并按需进行调用:

// 获取商品列表数据的方法
async getGoodsList(cb) {
  this.isloading = true
  const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
  this.isloading = false
  // 只要数据请求完毕,就立即按需调用 cb 回调函数
  cb && cb()

  if (res.meta.status !== 200) return uni.$showMsg()
  this.goodsList = [...this.goodsList, ...res.message.goods]
  this.total = res.message.total
}

 9.点击商品 item 项跳转到详情页面

将循环时的 block 组件修改为 view 组件,并绑定 click 点击事件处理函数:


  
    
    
  

在 methods 节点中,定义 gotoDetail 事件处理函数:

// 点击跳转到商品详情页面
gotoDetail(item) {
  uni.navigateTo({
    url: '/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id
  })
}

实现效果图:

黑马微信小程序项目·黑马优购_第28张图片

 

七、商品详情

1.获取商品详情数据

1.1 在 data 中定义商品详情的数据节点:

data() {
  return {
    // 商品详情对象
    goods_info: {}
  }
}

1.2 在 onLoad 中获取商品的 Id,并调用请求商品详情的方法:

onLoad(options) {
  // 获取商品 Id
  const goods_id = options.goods_id
  // 调用请求商品详情数据的方法
  this.getGoodsDetail(goods_id)
}

 1.3 在 methods 中声明 getGoodsDetail 方法:

methods: {
  // 定义请求商品详情数据的方法
  async getGoodsDetail(goods_id) {
    const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
    if (res.meta.status !== 200) return uni.$showMsg()
    // 为 data 中的数据赋值
    this.goods_info = res.message
  }
}

2.渲染商品详情页轮播图区域

2.1 使用 v-for 指令,循环渲染如下的轮播图 UI 结构:



  
    
  

 2.2 美化轮播图的样式:

swiper {
  height: 750rpx;

  image {
    width: 100%;
    height: 100%;
  }
}

2.3 实现轮播图预览效果

为轮播图中的 image 图片绑定 click 事件处理函数:


  
  

 在 methods 中定义 preview 事件处理函数:

// 实现轮播图的预览效果
preview(i) {
  // 调用 uni.previewImage() 方法预览图片
  uni.previewImage({
    // 预览时,默认显示图片的索引
    current: i,
    // 所有图片 url 地址的数组
    urls: this.goods_info.pics.map(x => x.pics_big)
  })
}

3.渲染商品信息区域 

3.1 定义商品信息区域的 UI 结构如下:



  
  ¥{{goods_info.goods_price}}
  
  
    
    {{goods_info.goods_name}}
    
    
      
      收藏
    
  
  
  快递:免运费

3.2 美化商品信息区域的样式:

// 商品信息区域的样式
.goods-info-box {
  padding: 10px;
  padding-right: 0;

  .price {
    color: #c00000;
    font-size: 18px;
    margin: 10px 0;
  }

  .goods-info-body {
    display: flex;
    justify-content: space-between;

    .goods-name {
      font-size: 13px;
      padding-right: 10px;
    }
    // 收藏区域
    .favi {
      width: 120px;
      font-size: 12px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      border-left: 1px solid #efefef;
      color: gray;
    }
  }

  // 运费
  .yf {
    margin: 10px 0;
    font-size: 12px;
    color: gray;
  }
}

 4.渲染商品详情信息

4.1 在页面结构中,使用 rich-text 组件,将带有 HTML 标签的内容,渲染为小程序的页面结构:


4.2 修改 getGoodsDetail 方法,从而解决图片底部 空白间隙 的问题:

// 定义请求商品详情数据的方法
async getGoodsDetail(goods_id) {
  const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
  if (res.meta.status !== 200) return uni.$showMsg()

  // 使用字符串的 replace() 方法,为 img 标签添加行内的 style 样式,从而解决图片底部空白间隙的问题
  res.message.goods_introduce = res.message.goods_introduce.replace(/

 4.3 解决 .webp 格式图片在 ios 设备上无法正常显示的问题:

// 定义请求商品详情数据的方法
async getGoodsDetail(goods_id) {
  const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
  if (res.meta.status !== 200) return uni.$showMsg()

  // 使用字符串的 replace() 方法,将 webp 的后缀名替换为 jpg 的后缀名
  res.message.goods_introduce = res.message.goods_introduce.replace(/

5.解决商品价格闪烁的问题 

  1. 导致问题的原因:在商品详情数据请求回来之前,data 中 goods_info 的值为 {},因此初次渲染页面时,会导致 商品价格、商品名称 等闪烁的问题。

  2. 解决方案:判断 goods_info.goods_name 属性的值是否存在,从而使用 v-if 指令控制页面的显示与隐藏:

 6.渲染商品导航区域的 UI 结构

基于 uni-ui 提供的 GoodsNav 组件来实现商品导航区域的效果

6.1 在 data 中,通过 options 和 buttonGroup 两个数组,来声明商品导航组件的按钮配置对象:

data() {
  return {
    // 商品详情对象
    goods_info: {},
    // 左侧按钮组的配置对象
    options: [{
      icon: 'shop',
      text: '店铺'
    }, {
      icon: 'cart',
      text: '购物车',
      info: 2
    }],
    // 右侧按钮组的配置对象
    buttonGroup: [{
        text: '加入购物车',
        backgroundColor: '#ff0000',
        color: '#fff'
      },
      {
        text: '立即购买',
        backgroundColor: '#ffa200',
        color: '#fff'
      }
    ]
  }
}

6.2 在页面中使用 uni-goods-nav 商品导航组件:



  
  
  
  
  
  

 6.3 美化商品导航组件,使之固定在页面最底部:

.goods-detail-container {
  // 给页面外层的容器,添加 50px 的内padding,
  // 防止页面内容被底部的商品导航组件遮盖
  padding-bottom: 50px;
}

.goods_nav {
  // 为商品导航组件添加固定定位
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
}

7.点击跳转到购物车页面 

根据 e.content.text 的值来判断跳转页面

// 左侧按钮的点击事件处理函数
onClick(e) {
  if (e.content.text === '购物车') {
    // 切换到购物车页面
    uni.switchTab({
      url: '/pages/cart/cart'
    })
  }
}

效果图

黑马微信小程序项目·黑马优购_第29张图片

 

八、加入购物车 

1.配置vuex

1.1在项目根目录中创建 store 文件夹,专门用来存放 vuex 相关的模块

1.2在 store 目录上鼠标右键,选择 新建 -> js文件,新建 store.js 文件:

黑马微信小程序项目·黑马优购_第30张图片

 1.3 在 store.js 中按照如下 4 个步骤初始化 Store 的实例对象

// 1. 导入 Vue 和 Vuex
import Vue from 'vue'
import Vuex from 'vuex'

// 2. 将 Vuex 安装为 Vue 的插件
Vue.use(Vuex)

// 3. 创建 Store 的实例对象
const store = new Vuex.Store({
  // TODO:挂载 store 模块
  modules: {},
})

// 4. 向外共享 Store 的实例对象
export default store

1.4 在 main.js 中导入 store 实例对象并挂载到 Vue 的实例上:

// 1. 导入 store 的实例对象
import store from './store/store.js'

// 省略其它代码...

const app = new Vue({
  ...App,
  // 2. 将 store 挂载到 Vue 实例上
  store,
})
app.$mount()

 2.创建购物车的 store 模块

2.1 在 store 目录上鼠标右键,选择 新建 -> js文件,创建购物车的 store 模块,命名为 cart.js

2.2 在 cart.js 中,初始化如下的 vuex 模块:

export default {
  // 为当前模块开启命名空间
  namespaced: true,

  // 模块的 state 数据
  state: () => ({
    // 购物车的数组,用来存储购物车中每个商品的信息对象
    // 每个商品的信息对象,都包含如下 6 个属性:
    // { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
    cart: [],
  }),

  // 模块的 mutations 方法
  mutations: {},

  // 模块的 getters 属性
  getters: {},
}

2.3 在 store/store.js 模块中,导入并挂载购物车的 vuex 模块,示例代码如下:

import Vue from 'vue'
import Vuex from 'vuex'
// 1. 导入购物车的 vuex 模块
import moduleCart from './cart.js'

Vue.use(Vuex)

const store = new Vuex.Store({
  // TODO:挂载 store 模块
  modules: {
    // 2. 挂载购物车的 vuex 模块,模块内成员的访问路径被调整为 m_cart,例如:
    //    购物车模块中 cart 数组的访问路径是 m_cart/cart
    m_cart: moduleCart,
  },
})

export default store

 3. 在商品详情页中使用 Store 中的数据

3.1 在 goods_detail.vue 页面中,修改  标签中的代码如下:

// 从 vuex 中按需导出 mapState 辅助方法
import { mapState } from 'vuex'

export default {
  computed: {
    // 调用 mapState 方法,把 m_cart 模块中的 cart 数组映射到当前页面中,作为计算属性来使用
    // ...mapState('模块的名称', ['要映射的数据名称1', '要映射的数据名称2'])
    ...mapState('m_cart', ['cart']),
  },
  // 省略其它代码...
}

3.2 在页面渲染时,可以直接使用映射过来的数据,例如: 


快递:免运费 -- {{cart.length}}

4.实现加入购物车的功能 

 4.1 在 store 目录下的 cart.js 模块中,封装一个将商品信息加入购物车的 mutations 方法,命名为 addToCart。示例代码如下:

export default {
  // 为当前模块开启命名空间
  namespaced: true,

  // 模块的 state 数据
  state: () => ({
    // 购物车的数组,用来存储购物车中每个商品的信息对象
    // 每个商品的信息对象,都包含如下 6 个属性:
    // { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
    cart: [],
  }),

  // 模块的 mutations 方法
  mutations: {
    addToCart(state, goods) {
      // 根据提交的商品的Id,查询购物车中是否存在这件商品
      // 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
      const findResult = state.cart.find((x) => x.goods_id === goods.goods_id)

      if (!findResult) {
        // 如果购物车中没有这件商品,则直接 push
        state.cart.push(goods)
      } else {
        // 如果购物车中有这件商品,则只更新数量即可
        findResult.goods_count++
      }
    },
  },

  // 模块的 getters 属性
  getters: {},
}

4.2 在商品详情页面中,通过 mapMutations 这个辅助方法,把 vuex 中 m_cart 模块下的 addToCart 方法映射到当前页面:

// 按需导入 mapMutations 这个辅助方法
import { mapMutations } from 'vuex'

export default {
  methods: {
    // 把 m_cart 模块中的 addToCart 方法映射到当前页面使用
    ...mapMutations('m_cart', ['addToCart']),
  },
}

4.3 为商品导航组件 uni-goods-nav 绑定 @buttonClick="buttonClick" 事件处理函数:

// 右侧按钮的点击事件处理函数
buttonClick(e) {
   // 1. 判断是否点击了 加入购物车 按钮
   if (e.content.text === '加入购物车') {

      // 2. 组织一个商品的信息对象
      const goods = {
         goods_id: this.goods_info.goods_id,       // 商品的Id
         goods_name: this.goods_info.goods_name,   // 商品的名称
         goods_price: this.goods_info.goods_price, // 商品的价格
         goods_count: 1,                           // 商品的数量
         goods_small_logo: this.goods_info.goods_small_logo, // 商品的图片
         goods_state: true                         // 商品的勾选状态
      }

      // 3. 通过 this 调用映射过来的 addToCart 方法,把商品信息对象存储到购物车中
      this.addToCart(goods)

   }
}

 5. 动态统计购物车中商品的总数量

5.1 在 cart.js 模块中,在 getters 节点下定义一个 total 方法,用来统计购物车中商品的总数量:

// 模块的 getters 属性
getters: {
   // 统计购物车中商品的总数量
   total(state) {
      let c = 0
      // 循环统计商品的数量,累加到变量 c 中
      state.cart.forEach(goods => c += goods.goods_count)
      return c
   }
}

 5.2 在商品详情页面的 script 标签中,按需导入 mapGetters 方法并进行使用:

// 按需导入 mapGetters 这个辅助方法
import { mapGetters } from 'vuex'

export default {
  computed: {
    // 把 m_cart 模块中名称为 total 的 getter 映射到当前页面中使用
    ...mapGetters('m_cart', ['total']),
  },
}

5.3 通过 watch 侦听器,监听计算属性 total 值的变化,从而动态为购物车按钮的徽标赋值

export default {
  watch: {
    // 1. 监听 total 值的变化,通过第一个形参得到变化后的新值
    total(newVal) {
      // 2. 通过数组的 find() 方法,找到购物车按钮的配置对象
      const findResult = this.options.find((x) => x.text === '购物车')

      if (findResult) {
        // 3. 动态为购物车按钮的 info 属性赋值
        findResult.info = newVal
      }
    },
  },
}

 6.持久化存储购物车中的商品

6.1 在 cart.js 模块中,声明一个叫做 saveToStorage 的 mutations 方法,此方法负责将购物车中的数据持久化存储到本地:

// 将购物车中的数据持久化存储到本地
saveToStorage(state) {
   uni.setStorageSync('cart', JSON.stringify(state.cart))
}

6.2 修改 mutations 节点中的 addToCart 方法,在处理完商品信息后,调用步骤 1 中定义的 saveToStorage 方法:

addToCart(state, goods) {
   // 根据提交的商品的Id,查询购物车中是否存在这件商品
   // 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
   const findResult = state.cart.find(x => x.goods_id === goods.goods_id)

   if (!findResult) {
      // 如果购物车中没有这件商品,则直接 push
      state.cart.push(goods)
   } else {
      // 如果购物车中有这件商品,则只更新数量即可
      findResult.goods_count++
   }

   // 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
   this.commit('m_cart/saveToStorage')
}

 6.3 修改 cart.js 模块中的 state 函数,读取本地存储的购物车数据,对 cart 数组进行初始化:

// 模块的 state 数据
state: () => ({
   // 购物车的数组,用来存储购物车中每个商品的信息对象
   // 每个商品的信息对象,都包含如下 6 个属性:
   // { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
   cart: JSON.parse(uni.getStorageSync('cart') || '[]')
}),

7.优化商品详情页的 total 侦听器 

使用普通函数的形式定义的 watch 侦听器,在页面首次加载后不会被调用。因此导致了商品详情页在首次加载完毕之后,不会将商品的总数量显示到商品导航区域:为了防止这个上述问题,可以使用对象的形式来定义 watch 侦听器

watch: {
   // 定义 total 侦听器,指向一个配置对象
   total: {
      // handler 属性用来定义侦听器的 function 处理函数
      handler(newVal) {
         const findResult = this.options.find(x => x.text === '购物车')
         if (findResult) {
            findResult.info = newVal
         }
      },
      // immediate 属性用来声明此侦听器,是否在页面初次加载完毕后立即调用
      immediate: true
   }
}

8.动态为 tabBar 页面设置数字徽标 

8.1 把 Store 中的 total 映射到 cart.vue 中使用:

// 按需导入 mapGetters 这个辅助方法
import { mapGetters } from 'vuex'

export default {
  data() {
    return {}
  },
  computed: {
    // 将 m_cart 模块中的 total 映射为当前页面的计算属性
    ...mapGetters('m_cart', ['total']),
  },
}

8.2 在页面刚显示出来的时候,立即调用 setBadge 方法,为 tabBar 设置数字徽标:

onShow() {
   // 在页面刚展示的时候,设置数字徽标
   this.setBadge()
}

 8.3 在 methods 节点中,声明 setBadge 方法如下,通过 uni.setTabBarBadge() 为 tabBar 设置数字徽标:

methods: {
   setBadge() {
      // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
      uni.setTabBarBadge({
         index: 2, // 索引
         text: this.total + '' // 注意:text 的值必须是字符串,不能是数字
      })
   }
}

9.将设置 tabBar 徽标的代码抽离为 mixins 

9.1 在项目根目录中新建 mixins 文件夹,并在 mixins 文件夹之下新建 tabbar-badge.js 文件,用来把设置 tabBar 徽标的代码封装为一个 mixin 文件:

import { mapGetters } from 'vuex'

// 导出一个 mixin 对象
export default {
  computed: {
    ...mapGetters('m_cart', ['total']),
  },
  onShow() {
    // 在页面刚展示的时候,设置数字徽标
    this.setBadge()
  },
  methods: {
    setBadge() {
      // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
      uni.setTabBarBadge({
        index: 2,
        text: this.total + '', // 注意:text 的值必须是字符串,不能是数字
      })
    },
  },
}

9.2 修改 home.vuecate.vuecart.vuemy.vue 这 4 个 tabBar 页面的源代码,分别导入 @/mixins/tabbar-badge.js 模块并进行使用: 

// 导入自己封装的 mixin 模块
import badgeMix from '@/mixins/tabbar-badge.js'

export default {
  // 将 badgeMix 混入到当前的页面中进行使用
  mixins: [badgeMix],
  // 省略其它代码...
}

九、购物车页面 

商品列表区域

1.渲染购物车商品列表的标题区域

1.1 定义如下的 UI 结构:



  
  
  
  购物车

1.2 美化样式:

.cart-title {
  height: 40px;
  display: flex;
  align-items: center;
  font-size: 14px;
  padding-left: 5px;
  border-bottom: 1px solid #efefef;
  .cart-title-text {
    margin-left: 10px;
  }
}

 2.渲染商品列表区域的基本结构

2.1 通过 mapState 辅助函数,将 Store 中的 cart 数组映射到当前页面中使用:

import badgeMix from '@/mixins/tabbar-badge.js'
// 按需导入 mapState 这个辅助函数
import { mapState } from 'vuex'

export default {
  mixins: [badgeMix],
  computed: {
    // 将 m_cart 模块中的 cart 数组映射到当前页面中使用
    ...mapState('m_cart', ['cart']),
  },
  data() {
    return {}
  },
}

2.2 在 UI 结构中,通过 v-for 指令循环渲染自定义的 my-goods 组件:



  

 3.为 my-goods 组件封装 radio 勾选状态

3.1 打开 my-goods.vue 组件的源代码,为商品的左侧图片区域添加 radio 组件:



  
  

3.2 给类名为 goods-item-left 的 view 组件添加样式,实现 radio 组件和 image 组件的左右布局:

.goods-item-left {
  margin-right: 5px;
  display: flex;
  justify-content: space-between;
  align-items: center;

  .goods-pic {
    width: 100px;
    height: 100px;
    display: block;
  }
}

 3.3 封装名称为 showRadio 的 props 属性,来控制当前组件中是否显示 radio 组件:

export default {
  // 定义 props 属性,用来接收外界传递到当前组件的数据
  props: {
    // 商品的信息对象
    goods: {
      type: Object,
      default: {},
    },
    // 是否展示图片左侧的 radio
    showRadio: {
      type: Boolean,
      // 如果外界没有指定 show-radio 属性的值,则默认不展示 radio 组件
      default: false,
    },
  },
}

3.4 使用 v-if 指令控制 radio 组件的按需展示: 



  
  
  

3.5 在 cart.vue 页面中的商品列表区域,指定 :show-radio="true" 属性,从而显示 radio 组件: 



  

3.6 修改 my-goods.vue 组件,动态为 radio 绑定选中状态:



  
  
  

 4.为 my-goods 组件封装 radio-change 事件

4.1 当用户点击 radio 组件,希望修改当前商品的勾选状态,此时用户可以为 my-goods 组件绑定 @radio-change 事件,从而获取当前商品的 goods_id 和 goods_state



  
  

 4.2 定义 radioChangeHandler 事件处理函数如下:

methods: {
  // 商品的勾选状态发生了变化
  radioChangeHandler(e) {
    console.log(e) // 输出得到的数据 -> {goods_id: 395, goods_state: false}
  }
}

4.3 在 my-goods.vue 组件中,为 radio 组件绑定 @click 事件处理函数如下:



  
  

 4.4 在 my-goods.vue 组件的 methods 节点中,定义 radioClickHandler 事件处理函数:

methods: {
  // radio 组件的点击事件处理函数
  radioClickHandler() {
    // 通过 this.$emit() 触发外界通过 @ 绑定的 radio-change 事件,
    // 同时把商品的 Id 和 勾选状态 作为参数传递给 radio-change 事件处理函数
    this.$emit('radio-change', {
      // 商品的 Id
      goods_id: this.goods.goods_id,
      // 商品最新的勾选状态
      goods_state: !this.goods.goods_state
    })
  }
}

5.修改购物车中商品的勾选状态 

5.1 在 store/cart.js 模块中,声明如下的 mutations 方法,用来修改对应商品的勾选状态:

// 更新购物车中商品的勾选状态
updateGoodsState(state, goods) {
  // 根据 goods_id 查询购物车中对应商品的信息对象
  const findResult = state.cart.find(x => x.goods_id === goods.goods_id)

  // 有对应的商品信息对象
  if (findResult) {
    // 更新对应商品的勾选状态
    findResult.goods_state = goods.goods_state
    // 持久化存储到本地
    this.commit('m_cart/saveToStorage')
  }
}

5.2 在 cart.vue 页面中,导入 mapMutations 这个辅助函数,从而将需要的 mutations 方法映射到当前页面中使用:

import badgeMix from '@/mixins/tabbar-badge.js'
import { mapState, mapMutations } from 'vuex'

export default {
  mixins: [badgeMix],
  computed: {
    ...mapState('m_cart', ['cart']),
  },
  data() {
    return {}
  },
  methods: {
    ...mapMutations('m_cart', ['updateGoodsState']),
    // 商品的勾选状态发生了变化
    radioChangeHandler(e) {
      this.updateGoodsState(e)
    },
  },
}

 6.为 my-goods 组件封装 NumberBox

6.1修改 my-goods.vue 组件的源代码,在类名为 goods-info-box 的 view 组件内部渲染 NumberBox 组件的基本结构


  
  ¥{{goods.goods_price | tofixed}}
  
  

6.2 美化页面的结构:

.goods-item-right {
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: space-between;

  .goods-name {
    font-size: 13px;
  }

  .goods-info-box {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .goods-price {
    font-size: 16px;
    color: #c00000;
  }
}

 6.3在 my-goods.vue 组件中,动态为 NumberBox 组件绑定商品的数量值


  
  ¥{{goods.goods_price | tofixed}}
  
  

6.4 在 my-goods.vue 组件中,封装名称为 showNum 的 props 属性,来控制当前组件中是否显示 NumberBox 组件:

export default {
  // 定义 props 属性,用来接收外界传递到当前组件的数据
  props: {
    // 商品的信息对象
    goods: {
      type: Object,
      defaul: {},
    },
    // 是否展示图片左侧的 radio
    showRadio: {
      type: Boolean,
      // 如果外界没有指定 show-radio 属性的值,则默认不展示 radio 组件
      default: false,
    },
    // 是否展示价格右侧的 NumberBox 组件
    showNum: {
      type: Boolean,
      default: false,
    },
  },
}

 6.5在 my-goods.vue 组件中,使用 v-if 指令控制 NumberBox 组件的按需展示:


  
  ¥{{goods.goods_price | tofixed}}
  
  

6.6 在 cart.vue 页面中的商品列表区域,指定 :show-num="true" 属性,从而显示 NumberBox 组件:



  

 7. 为 my-goods 组件封装 num-change 事件

7.1当用户修改了 NumberBox 的值以后,希望将最新的商品数量更新到购物车中,此时用户可以为 my-goods 组件绑定 @num-change 事件,从而获取当前商品的 goods_id 和 goods_count:



  

7.2 定义 numberChangeHandler 事件处理函数如下:

// 商品的数量发生了变化
numberChangeHandler(e) {
  console.log(e)
}

 7.3 在 my-goods.vue 组件中,为 uni-number-box 组件绑定 @change 事件处理函数如下:


  
  ¥{{goods.goods_price | tofixed}}
  
  

7.4 在 my-goods.vue 组件的 methods 节点中,定义 numChangeHandler 事件处理函数:

methods: {
  // NumberBox 组件的 change 事件处理函数
  numChangeHandler(val) {
    // 通过 this.$emit() 触发外界通过 @ 绑定的 num-change 事件
    this.$emit('num-change', {
      // 商品的 Id
      goods_id: this.goods.goods_id,
      // 商品的最新数量
      goods_count: +val
    })
  }
}

官方在版本1.1.2中已经对组件进行了优化,不合法校验已经自带了,所以就不需要进行合法性校验了

8.修改购物车中商品的数量

8.1在 store/cart.js 模块中,声明如下的 mutations 方法,用来修改对应商品的数量:

// 更新购物车中商品的数量
updateGoodsCount(state, goods) {
  // 根据 goods_id 查询购物车中对应商品的信息对象
  const findResult = state.cart.find(x => x.goods_id === goods.goods_id)

  if(findResult) {
    // 更新对应商品的数量
    findResult.goods_count = goods.goods_count
    // 持久化存储到本地
    this.commit('m_cart/saveToStorage')
  }
}

8.2在 cart.vue 页面中,通过 mapMutations 这个辅助函数,将需要的 mutations 方法映射到当前页面中使用:

import badgeMix from '@/mixins/tabbar-badge.js'
import { mapState, mapMutations } from 'vuex'

export default {
  mixins: [badgeMix],
  computed: {
    ...mapState('m_cart', ['cart']),
  },
  data() {
    return {}
  },
  methods: {
    ...mapMutations('m_cart', ['updateGoodsState', 'updateGoodsCount']),
    // 商品的勾选状态发生了变化
    radioChangeHandler(e) {
      this.updateGoodsState(e)
    },
    // 商品的数量发生了变化
    numberChangeHandler(e) {
      this.updateGoodsCount(e)
    },
  },
}

9.渲染滑动删除的 UI 效果 

9.1 改造 cart.vue 页面的 UI 结构,将商品列表区域的结构修改如下(可以使用 uSwipeAction 代码块快速生成基本的 UI 结构):




  
    
    
      
    
  

9.2在 data 节点中声明 options 数组,用来定义操作按钮的配置信息: 

data() {
  return {
    options: [{
      text: '删除', // 显示的文本内容
      style: {
        backgroundColor: '#C00000' // 按钮的背景颜色
      }
    }]
  }
}

9.3在 methods 中声明 uni-swipe-action-item 组件的 @click 事件处理函数:

// 点击了滑动操作按钮
swipeActionClickHandler(goods) {
  console.log(goods)
}

 10.实现滑动删除的功能

10.1在 store/cart.js 模块的 mutations 节点中声明如下的方法,从而根据商品的 Id 从购物车中移除对应的商品:

// 根据 Id 从购物车中删除对应的商品信息
removeGoodsById(state, goods_id) {
  // 调用数组的 filter 方法进行过滤
  state.cart = state.cart.filter(x => x.goods_id !== goods_id)
  // 持久化存储到本地
  this.commit('m_cart/saveToStorage')
}

10.2在 cart.vue 页面中,使用 mapMutations 辅助函数,把需要的方法映射到当前页面中使用:

methods: {
  ...mapMutations('m_cart', ['updateGoodsState', 'updateGoodsCount', 'removeGoodsById']),
  // 商品的勾选状态发生了变化
  radioChangeHandler(e) {
    this.updateGoodsState(e)
  },
  // 商品的数量发生了变化
  numberChangeHandler(e) {
    this.updateGoodsCount(e)
  },
  // 点击了滑动操作按钮
  swipeActionClickHandler(goods) {
    this.removeGoodsById(goods.goods_id)
  }
}

收货地址区域 

1.创建收货地址的组件

2.渲染收货地址组件的基本结构:



  
  
    
  

  
  
    
      
        收货人:escook
      
      
        电话:138XXXX5555
        
      
    
    
      收货地址:
      河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx 
    
  

  
  

// 底部边框线的样式
.address-border {
  display: block;
  width: 100%;
  height: 5px;
}

// 选择收货地址的盒子
.address-choose-box {
  height: 90px;
  display: flex;
  align-items: center;
  justify-content: center;
}

// 渲染收货信息的盒子
.address-info-box {
  font-size: 12px;
  height: 90px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 5px;

  // 第一行
  .row1 {
    display: flex;
    justify-content: space-between;

    .row1-right {
      display: flex;
      align-items: center;

      .phone {
        margin-right: 5px;
      }
    }
  }

  // 第二行
  .row2 {
    display: flex;
    align-items: center;
    margin-top: 10px;

    .row2-left {
      white-space: nowrap;
    }
  }
}

3.实现收货地址区域的按需展示

3.1 在 data 中定义收货地址的信息对象: 

export default {
  data() {
    return {
      // 收货地址
      address: {},
    }
  },
}

3.2 使用 v-if 和 v-else 实现按需展示: 



  




  

4.实现选择收货地址的功能 

4.1 为 请选择收货地址+ 的 button 按钮绑定点击事件处理函数:



  

4.2 定义 chooseAddress 事件处理函数,调用小程序提供的 chooseAddress() API 实现选择收货地址的功能:

methods: {
  // 选择收货地址
  async chooseAddress() {
    // 1. 调用小程序提供的 chooseAddress() 方法,即可使用选择收货地址的功能
    //    返回值是一个数组:第 1 项为错误对象;第 2 项为成功之后的收货地址对象
    const [err, succ] = await uni.chooseAddress().catch(err => err)

    // 2. 用户成功的选择了收货地址
    if (err === null && succ.errMsg === 'chooseAddress:ok') {
      // 为 data 里面的收货地址对象赋值
      this.address = succ
    }
  }
}

4.3 定义收货详细地址的计算属性:

computed: {
  // 收货详细地址的计算属性
  addstr() {
    if (!this.address.provinceName) return ''

    // 拼接 省,市,区,详细地址 的字符串并返回给用户
    return this.address.provinceName + this.address.cityName + this.address.countyName + this.address.detailInfo
  }
}

 4.4渲染收货地址区域的数据:



  
    
      收货人:{{address.userName}}
    
    
      电话:{{address.telNumber}}
      
    
  
  
    收货地址:
    {{addstr}}
  

 5.将 address 信息存储到 vuex 中

5.1 在 store 目录中,创建用户相关的 vuex 模块,命名为 user.js

export default {
  // 开启命名空间
  namespaced: true,

  // state 数据
  state: () => ({
    // 收货地址
    address: {},
  }),

  // 方法
  mutations: {
    // 更新收货地址
    updateAddress(state, address) {
      state.address = address
    },
  },

  // 数据包装器
  getters: {},
}

5.2 在 store/store.js 模块中,导入并挂载 user.js 模块:

// 1. 导入 Vue 和 Vuex
import Vue from 'vue'
import Vuex from 'vuex'
// 导入购物车的 vuex 模块
import moduleCart from './cart.js'
// 导入用户的 vuex 模块
import moduleUser from './user.js'

// 2. 将 Vuex 安装为 Vue 的插件
Vue.use(Vuex)

// 3. 创建 Store 的实例对象
const store = new Vuex.Store({
  // TODO:挂载 store 模块
  modules: {
    // 挂载购物车的 vuex 模块,模块内成员的访问路径被调整为 m_cart,例如:
    // 购物车模块中 cart 数组的访问路径是 m_cart/cart
    m_cart: moduleCart,
    // 挂载用户的 vuex 模块,访问路径为 m_user
    m_user: moduleUser,
  },
})

// 4. 向外共享 Store 的实例对象
export default store

 5.3 改造 address.vue 组件中的代码,使用 vuex 提供的 address 计算属性 替代 data 中定义的本地 address 对象

// 1. 按需导入 mapState 和 mapMutations 这两个辅助函数
import { mapState, mapMutations } from 'vuex'

export default {
  data() {
    return {
      // 2.1 注释掉下面的 address 对象,使用 2.2 中的代码替代之
      // address: {}
    }
  },
  methods: {
    // 3.1 把 m_user 模块中的 updateAddress 函数映射到当前组件
    ...mapMutations('m_user', ['updateAddress']),
    // 选择收货地址
    async chooseAddress() {
      const [err, succ] = await uni.chooseAddress().catch((err) => err)

      // 用户成功的选择了收货地址
      if (err === null && succ.errMsg === 'chooseAddress:ok') {
        // 3.2 把下面这行代码注释掉,使用 3.3 中的代码替代之
        // this.address = succ

        // 3.3 调用 Store 中提供的 updateAddress 方法,将 address 保存到 Store 里面
        this.updateAddress(succ)
      }
    },
  },
  computed: {
    // 2.2 把 m_user 模块中的 address 对象映射当前组件中使用,代替 data 中 address 对象
    ...mapState('m_user', ['address']),
    // 收货详细地址的计算属性
    addstr() {
      if (!this.address.provinceName) return ''

      // 拼接 省,市,区,详细地址 的字符串并返回给用户
      return this.address.provinceName + this.address.cityName + this.address.countyName + this.address.detailInfo
    },
  },
}

6.将 Store 中的 address 持久化存储到本地 

6.1将 Store 中的 address 持久化存储到本地

export default {
  // 开启命名空间
  namespaced: true,

  // state 数据
  state: () => ({
    // 3. 读取本地的收货地址数据,初始化 address 对象
    address: JSON.parse(uni.getStorageSync('address') || '{}'),
  }),

  // 方法
  mutations: {
    // 更新收货地址
    updateAddress(state, address) {
      state.address = address

      // 2. 通过 this.commit() 方法,调用 m_user 模块下的 saveAddressToStorage 方法将 address 对象持久化存储到本地
      this.commit('m_user/saveAddressToStorage')
    },
    // 1. 定义将 address 持久化存储到本地 mutations 方法
    saveAddressToStorage(state) {
      uni.setStorageSync('address', JSON.stringify(state.address))
    },
  },

  // 数据包装器
  getters: {},
}

7.将 addstr 抽离为 getters 

目的:为了提高代码的复用性,可以把收货的详细地址抽离为 getters,方便在多个页面和组件之间实现复用。

7.1 剪切 my-address.vue 组件中的 addstr 计算属性的代码,粘贴到 user.js 模块中,作为一个 getters 节点:

// 数据包装器
getters: {
  // 收货详细地址的计算属性
  addstr(state) {
    if (!state.address.provinceName) return ''

    // 拼接 省,市,区,详细地址 的字符串并返回给用户
    return state.address.provinceName + state.address.cityName + state.address.countyName + state.address.detailInfo
  }
}

7.2 改造 my-address.vue 组件中的代码,通过 mapGetters 辅助函数,将 m_user 模块中的 addstr 映射到当前组件中使用:

// 按需导入 mapGetters 辅助函数
import { mapState, mapMutations, mapGetters } from 'vuex'

export default {
  // 省略其它代码
  computed: {
    ...mapState('m_user', ['address']),
    // 将 m_user 模块中的 addstr 映射到当前组件中使用
    ...mapGetters('m_user', ['addstr']),
  },
}

 8.重新选择收货地址

8.1为 class 类名为 address-info-box 的盒子绑定 click 事件处理函数如下:



  

结算区域 

1.先新建一个结算的组件

2.初始化 my-settle 组件的基本结构和样式:





在 cart.vue 页面中使用自定义的 my-settle 组件,并美化页面样式,防止页面底部被覆盖:



 3.渲染结算区域的结构和样式

3.1 定义如下的 UI 结构:



  
  

  
  
    合计:¥1234.00
  

  
  结算(0)

3.2 美化样式:

.my-settle-container {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 50px;
  // 将背景色从 cyan 改为 white
  background-color: white;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-left: 5px;
  font-size: 14px;

  .radio {
    display: flex;
    align-items: center;
  }

  .amount {
    color: #c00000;
  }

  .btn-settle {
    height: 50px;
    min-width: 100px;
    background-color: #c00000;
    color: white;
    line-height: 50px;
    text-align: center;
    padding: 0 10px;
  }
}

4. 动态渲染已勾选商品的总数量

4.1 在 store/cart.js 模块中,定义一个名称为 checkedCount 的 getters,用来统计已勾选商品的总数量:

// 勾选的商品的总数量
checkedCount(state) {
  // 先使用 filter 方法,从购物车中过滤器已勾选的商品
  // 再使用 reduce 方法,将已勾选的商品总数量进行累加
  // reduce() 的返回值就是已勾选的商品的总数量
  return state.cart.filter(x => x.goods_state).reduce((total, item) => total += item.goods_count, 0)
}

4.2 在 my-settle 组件中,通过 mapGetters 辅助函数,将需要的 getters 映射到当前组件中使用:

import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters('m_cart', ['checkedCount']),
  },
  data() {
    return {}
  },
}

 4.3 将 checkedCount 的值渲染到页面中:


结算({{checkedCount}})

5. 动态渲染全选按钮的选中状态 

5.1 使用 mapGetters 辅助函数,将商品的总数量映射到当前组件中使用,并定义一个叫做 isFullCheck 的计算属性:

import { mapGetters } from 'vuex'

export default {
  computed: {
    // 1. 将 total 映射到当前组件中
    ...mapGetters('m_cart', ['checkedCount', 'total']),
    // 2. 是否全选
    isFullCheck() {
      return this.total === this.checkedCount
    },
  },
  data() {
    return {}
  },
}

5.2 为 radio 组件动态绑定 checked 属性的值:


 5.3 改进total

    total(state) {
            // let c = 0
            // state.cart.forEach(x => c += x.goods_count)
            // return c
            return state.cart.reduce((total,item)=>total += item.goods_count,0)
        },

6.实现商品的全选/反选功能

6.1在 store/cart.js 模块中,定义一个叫做 updateAllGoodsState 的 mutations 方法,用来修改所有商品的勾选状态:

// 更新所有商品的勾选状态
updateAllGoodsState(state, newState) {
  // 循环更新购物车中每件商品的勾选状态
  state.cart.forEach(x => x.goods_state = newState)
  // 持久化存储到本地
  this.commit('m_cart/saveToStorage')
}

6.2在 my-settle 组件中,通过 mapMutations 辅助函数,将需要的 mutations 方法映射到当前组件中使用:

// 1. 按需导入 mapMutations 辅助函数
import { mapGetters, mapMutations } from 'vuex'

export default {
  // 省略其它代码
  methods: {
    // 2. 使用 mapMutations 辅助函数,把 m_cart 模块提供的 updateAllGoodsState 方法映射到当前组件中使用
    ...mapMutations('m_cart', ['updateAllGoodsState']),
  },
}

 6.3为 UI 中的 label 组件绑定 click 事件处理函数:


6.4在 my-settle 组件的 methods 节点中,声明 changeAllState 事件处理函数:

methods: {
  ...mapMutations('m_cart', ['updateAllGoodsState']),
  // label 的点击事件处理函数
  changeAllState() {
    // 修改购物车中所有商品的选中状态
    // !this.isFullCheck 表示:当前全选按钮的状态取反之后,就是最新的勾选状态
    this.updateAllGoodsState(!this.isFullCheck)
  }
}

7 动态渲染已勾选商品的总价格

7.1 在 store/cart.js 模块中,定义一个叫做 checkedGoodsAmount 的 getters,用来统计已勾选商品的总价格:

// 已勾选的商品的总价
checkedGoodsAmount(state) {
  // 先使用 filter 方法,从购物车中过滤器已勾选的商品
  // 再使用 reduce 方法,将已勾选的商品数量 * 单价之后,进行累加
  // reduce() 的返回值就是已勾选的商品的总价
  // 最后调用 toFixed(2) 方法,保留两位小数
  return state.cart.filter(x => x.goods_state)
                   .reduce((total, item) => total += item.goods_count * item.goods_price, 0)
                   .toFixed(2)
}

 7.2在 my-settle 组件中,使用 mapGetters 辅助函数,把需要的 checkedGoodsAmount 映射到当前组件中使用:

...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount'])

7.3在组件的 UI 结构中,渲染已勾选的商品的总价:



  合计:¥{{checkedGoodsAmount}}

 8.动态计算购物车徽标的数值

  1. 问题说明:当修改购物车中商品的数量之后,tabBar 上的数字徽标不会自动更新。

  2. 解决方案:改造 mixins/tabbar-badge.js 中的代码,使用 watch 侦听器,监听 total 总数量的变化,从而动态为 tabBar 的徽标赋值:

import { mapGetters } from 'vuex'

// 导出一个 mixin 对象
export default {
  computed: {
    ...mapGetters('m_cart', ['total']),
  },
  watch: {
    // 监听 total 值的变化
    total() {
      // 调用 methods 中的 setBadge 方法,重新为 tabBar 的数字徽章赋值
      this.setBadge()
    },
  },
  onShow() {
    // 在页面刚展示的时候,设置数字徽标
    this.setBadge()
  },
  methods: {
    setBadge() {
      // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
      uni.setTabBarBadge({
        index: 2,
        text: this.total + '', // 注意:text 的值必须是字符串,不能是数字
      })
    },
  },
}

9.渲染购物车为空时的页面结构 

改造 cart.vue 页面的 UI 结构,使用 v-if 和 v-else 控制购物车区域空白购物车区域的按需展示:

	
		 
		
			
			
				
				
				
				购物车
			
			
			
				
					
						
					
				
			
			
		

		
		
			
			空空如也~
		
	

样式

.empty-cart {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 150px;

  .empty-img {
    width: 90px;
    height: 90px;
  }

  .tip-text {
    font-size: 12px;
    color: gray;
    margin-top: 15px;
  }
}

黑马微信小程序项目·黑马优购_第31张图片

 

十、登录与支付 

1.点击结算按钮进行条件判断

说明:用户点击了结算按钮之后,需要先后判断是否勾选了要结算的商品是否选择了收货地址是否登录

1.1 在 my-settle 组件中,为结算按钮绑定点击事件处理函数:


结算({{checkedCount}})

 1.2 在 my-settle 组件的 methods 节点中声明 settlement 事件处理函数如下:

// 点击了结算按钮
settlement() {
  // 1. 先判断是否勾选了要结算的商品
  if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')

  // 2. 再判断用户是否选择了收货地址
  if (!this.addstr) return uni.$showMsg('请选择收货地址!')

  // 3. 最后判断用户是否登录了
  if (!this.token) return uni.$showMsg('请先登录!')
}

1.3 在 my-settle 组件中,使用 mapGetters 辅助函数,从 m_user 模块中将 addstr 映射到当前组件中使用:

export default {
  computed: {
    ...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount']),
    // addstr 是详细的收货地址
    ...mapGetters('m_user', ['addstr']),
    isFullCheck() {
      return this.total === this.checkedCount
    },
  },
}

1.4在 store/user.js 模块的 state 节点中,声明 token 字符串:

export default {
  // 开启命名空间
  namespaced: true,

  // state 数据
  state: () => ({
    // 收货地址
    address: JSON.parse(uni.getStorageSync('address') || '{}'),
    // 登录成功之后的 token 字符串
    token: '',
  }),

  // 省略其它代码
}

 1.5在 my-settle 组件中,使用 mapState 辅助函数,从 m_user 模块中将 token 映射到当前组件中使用:

// 按需从 vuex 中导入 mapState 辅助函数
import { mapGetters, mapMutations, mapState } from 'vuex'

export default {
  computed: {
    ...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount']),
    ...mapGetters('m_user', ['addstr']),
    // token 是用户登录成功之后的 token 字符串
    ...mapState('m_user', ['token']),
    isFullCheck() {
      return this.total === this.checkedCount
    },
  },
}

 2.登录

2.1实现登录和用户信息组件的按需展示

先分别新建二个组件,my-userinfo 和 my-login

在 my.vue 页面中,通过 mapState 辅助函数,导入需要的 token 字符串:

import badgeMix from '@/mixins/tabbar-badge.js'
// 1. 从 vuex 中按需导入 mapState 辅助函数
import { mapState } from 'vuex'

export default {
  mixins: [badgeMix],
  computed: {
    // 2. 从 m_user 模块中导入需要的 token 字符串
    ...mapState('m_user', ['token']),
  },
  data() {
    return {}
  },
}

在 my.vue 页面中,实现登录组件用户信息组件的按需展示:

 2.2 实现登录组件的基本布局





2.3 点击登录按钮获取微信用户的基本信息

为登录的 button 按钮绑定 open-type="getUserInfo" 属性,表示点击按钮时,希望获取用户的基本信息:



在 methods 节点中声明 getUserInfo 事件处理函数如下:

methods: {
  // 获取微信用户的基本信息
  getUserInfo(e) {
    // 判断是否获取用户信息成功
    if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')

    // 获取用户信息成功, e.detail.userInfo 就是用户的基本信息
    console.log(e.detail.userInfo)
  }
}

 2.4 将用户的基本信息存储到 vuex

在 store/user.js 模块的 state 节点中,声明 userinfo 的信息对象如下:

// state 数据
state: () => ({
  // 收货地址
  // address: {}
  address: JSON.parse(uni.getStorageSync('address') || '{}'),
  // 登录成功之后的 token 字符串
  token: '',
  // 用户的基本信息
  userinfo: JSON.parse(uni.getStorageSync('userinfo') || '{}')
}),

在 store/user.js 模块的 mutations 节点中,声明如下的两个方法:

// 方法
mutations: {
  // 省略其它代码...

  // 更新用户的基本信息
  updateUserInfo(state, userinfo) {
    state.userinfo = userinfo
    // 通过 this.commit() 方法,调用 m_user 模块下的 saveUserInfoToStorage 方法,将 userinfo 对象持久化存储到本地
    this.commit('m_user/saveUserInfoToStorage')
  },

  // 将 userinfo 持久化存储到本地
  saveUserInfoToStorage(state) {
    uni.setStorageSync('userinfo', JSON.stringify(state.userinfo))
  }
}

 使用 mapMutations 辅助函数,将需要的方法映射到 my-login 组件中使用:

// 1. 按需导入 mapMutations 辅助函数
import { mapMutations } from 'vuex'

export default {
  data() {
    return {}
  },
  methods: {
    // 2. 调用 mapMutations 辅助方法,把 m_user 模块中的 updateUserInfo 映射到当前组件中使用
    ...mapMutations('m_user', ['updateUserInfo']),
    // 获取微信用户的基本信息
    getUserInfo(e) {
      // 判断是否获取用户信息成功
      if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')
      // 获取用户信息成功, e.detail.userInfo 就是用户的基本信息
      // console.log(e.detail.userInfo)

      // 3. 将用户的基本信息存储到 vuex 中
      this.updateUserInfo(e.detail.userInfo)
    },
  },
}

2.5 登录获取 Token 字符串 

需求说明:当获取到了微信用户的基本信息之后,还需要进一步调用登录相关的接口,从而换取登录成功之后的 Token 字符串

在 getUserInfo 方法中,预调用 this.getToken() 方法,同时把获取到的用户信息传递进去:

// 获取微信用户的基本信息
getUserInfo(e) {
  // 判断是否获取用户信息成功
  if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')

  // 将用户的基本信息存储到 vuex 中
  this.updateUserInfo(e.detail.userInfo)

  // 获取登录成功后的 Token 字符串
  this.getToken(e.detail)
}

 在 methods 中定义 getToken 方法,调用登录相关的 API,实现登录的功能

这里的token因为我们没有权限,所以只能自己模拟一个

"Bearer 	eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWOiOjEyLCJpYXQi0OjE1MjUGNDIyMjMsImV4cCI6NTUyNTO40DvYyN30.g-4GtEQNPwT_Xs8Pq7Lrco_9nlHQQsBi0KZerkO-0-o"
// 调用登录接口,换取永久的 token
async getToken(info) {
  // 调用微信登录接口
  const [err, res] = await uni.login().catch(err => err)
  // 判断是否 uni.login() 调用失败
  if (err || res.errMsg !== 'login:ok') return uni.$showError('登录失败!')

  // 准备参数对象
  const query = {
    code: res.code,
    encryptedData: info.encryptedData,
    iv: info.iv,
    rawData: info.rawData,
    signature: info.signature
  }

  // 换取 token
  const { data: loginResult } = await uni.$http.post('/api/public/v1/users/wxlogin', query)
  if (loginResult.meta.status !== 200) return uni.$showMsg('登录失败!')
  uni.$showMsg('登录成功')
  
}

2.6将 Token 存储到 vuex 

在 store/user.js 模块的 mutations 节点中,声明如下的两个方法:

mutations: {
  // 省略其它代码...

  // 更新 token 字符串
  updateToken(state, token) {
    state.token = token
    // 通过 this.commit() 方法,调用 m_user 模块下的 saveTokenToStorage 方法,将 token 字符串持久化存储到本地
    this.commit('m_user/saveTokenToStorage')
  },

  // 将 token 字符串持久化存储到本地
  saveTokenToStorage(state) {
    uni.setStorageSync('token', state.token)
  }
}

修改 store/user.js 模块的 state 节点如下: 

// state 数据
state: () => ({
  // 收货地址
  address: JSON.parse(uni.getStorageSync('address') || '{}'),
  // 登录成功之后的 token 字符串
  token: uni.getStorageSync('token') || '',
  // 用户的基本信息
  userinfo: JSON.parse(uni.getStorageSync('userinfo') || '{}')
}),

在 my-login 组件中,把 vuex 中的 updateToken 方法映射到当前组件中使用: 

methods: {
  // 1. 使用 mapMutations 辅助方法,把 m_user 模块中的 updateToken 方法映射到当前组件中使用
  ...mapMutations('m_user', ['updateUserInfo', 'updateToken'])

  // 省略其它代码...

  // 调用登录接口,换取永久的 token
  async getToken(info) {
    // 调用微信登录接口
    const [err, res] = await uni.login().catch(err => err)
    // 判断是否 uni.login() 调用失败
    if (err || res.errMsg !== 'login:ok') return uni.$showError('登录失败!')

    // 准备参数对象
    const query = {
      code: res.code,
      encryptedData: info.encryptedData,
      iv: info.iv,
      rawData: info.rawData,
      signature: info.signature
    }
	const token ="Bearer 	eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWOiOjEyLCJpYXQi0OjE1MjUGNDIyMjMsImV4cCI6NTUyNTO40DvYyN30.g-4GtEQNPwT_Xs8Pq7Lrco_9nlHQQsBi0KZerkO-0-o"

    // 换取 token
    const { data: loginResult } = await uni.$http.post('/api/public/v1/users/wxlogin', query)
    //if (loginResult.meta.status !== 200) return uni.$showMsg('登录失败!')

    // 2. 更新 vuex 中的 token
    //this.updateToken(loginResult.message.token)
    this.updateToken(token)
  }
}

3.用户信息 

3.1 实现用户头像昵称区域的基本布局



3.2渲染用户的头像和昵称 

在 my-userinfo 组件中,通过 mapState 辅助函数,将需要的成员映射到当前组件中使用:

// 按需导入 mapState 辅助函数
import { mapState } from 'vuex'

export default {
  computed: {
    // 将 m_user 模块中的 userinfo 映射到当前页面中使用
    ...mapState('m_user', ['userinfo']),
  },
  data() {
    return {}
  },
}

将用户的头像和昵称渲染到页面中:



  
  {{userinfo.nickName}}

 3.3 渲染面板信息区域

		
		
			
			
				
				
					
					
						8
						收藏的店铺
					
					
						14
						收藏的商品
					
					
						18
						关注的商品
					
					
						84
						足迹
					
				
			

			
			
			
				
				我的订单
				
				
					
					
						
						待付款
					
					
						
						待收货
					
					
						
						退款/退货
					
					
						
						全部订单
					
				
			
			
			
			
				
					收货地址
					
				
				
					联系客服
					
				
				
					退出登录
					
				
			
		

效果图

黑马微信小程序项目·黑马优购_第32张图片

3.4实现退出登录的功能

为第三个面板区域中的 退出登录 项绑定 click 点击事件处理函数


  退出登录
  

在 my-userinfo 组件的 methods 节点中定义 logout 事件处理函数:

// 退出登录
async logout() {
  // 询问用户是否退出登录
  const [err, succ] = await uni.showModal({
    title: '提示',
    content: '确认退出登录吗?'
  }).catch(err => err)

  if (succ && succ.confirm) {
     // 用户确认了退出登录的操作
     // 需要清空 vuex 中的 userinfo、token 和 address
     this.updateUserInfo({})
     this.updateToken('')
     this.updateAddress({})
  }
}

 使用 mapMutations 辅助方法,将需要用到的 mutations 方法映射到当前组件中:

// 按需导入辅助函数
import { mapState, mapMutations } from 'vuex'

export default {
  methods: {
    ...mapMutations('m_user', ['updateUserInfo', 'updateToken', 'updateAddress']),
  },
}

4.三秒后自动跳转到登录页面 

需求描述:在购物车页面,当用户点击 “结算” 按钮时,如果用户没有登录,则 3 秒后自动跳转到登录页面

在 my-settle 组件的 methods 节点中,声明一个叫做 showTips 的方法,专门用来展示倒计时的提示消息:

// 展示倒计时的提示消息
showTips(n) {
  // 调用 uni.showToast() 方法,展示提示消息
  uni.showToast({
    // 不展示任何图标
    icon: 'none',
    // 提示的消息
    title: '请登录后再结算!' + n + ' 秒后自动跳转到登录页',
    // 为页面添加透明遮罩,防止点击穿透
    mask: true,
    // 1.5 秒后自动消失
    duration: 1500
  })
}

在 data 节点中声明倒计时的秒数:

data() {
  return {
    // 倒计时的秒数
    seconds: 3
  }
}

 改造 结算 按钮的 click 事件处理函数,如果用户没有登录,则预调用一个叫做 delayNavigate 的方法,进行倒计时的导航跳转:

// 点击了结算按钮
settlement() {
  // 1. 先判断是否勾选了要结算的商品
  if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')

  // 2. 再判断用户是否选择了收货地址
  if (!this.addstr) return uni.$showMsg('请选择收货地址!')

  // 3. 最后判断用户是否登录了,如果没有登录,则调用 delayNavigate() 进行倒计时的导航跳转
  // if (!this.token) return uni.$showMsg('请先登录!')
  if (!this.token) return this.delayNavigate()
},

 定义 delayNavigate 方法,初步实现倒计时的提示功能

// 延迟导航到 my 页面
delayNavigate() {
  // 1. 展示提示消息,此时 seconds 的值等于 3
  this.showTips(this.seconds)

  // 2. 创建定时器,每隔 1 秒执行一次
  setInterval(() => {
    // 2.1 先让秒数自减 1
    this.seconds--
    // 2.2 再根据最新的秒数,进行消息提示
    this.showTips(this.seconds)
  }, 1000)
},

上述代码的问题:定时器不会自动停止,此时秒数会出现等于 0 或小于 0 的情况!

在 data 节点中声明定时器的 Id 如下: 

data() {
  return {
    // 倒计时的秒数
    seconds: 3,
    // 定时器的 Id
    timer: null
  }
}

改造 delayNavigate 方法如下:

// 延迟导航到 my 页面
delayNavigate() {
  this.showTips(this.seconds)

  // 1. 将定时器的 Id 存储到 timer 中
  this.timer = setInterval(() => {
    this.seconds--

    // 2. 判断秒数是否 <= 0
    if (this.seconds <= 0) {
      // 2.1 清除定时器
      clearInterval(this.timer)

      // 2.2 跳转到 my 页面
      uni.switchTab({
        url: '/pages/my/my'
      })

      // 2.3 终止后续代码的运行(当秒数为 0 时,不再展示 toast 提示消息)
      return
    }

    this.showTips(this.seconds)
  }, 1000)
},

 上述代码的问题:seconds 秒数不会被重置,导致第 2 次,3 次,n 次 的倒计时跳转功能无法正常工作

进一步改造 delayNavigate 方法,在执行此方法时,立即将 seconds 秒数重置为 3 即可: 

// 延迟导航到 my 页面
delayNavigate() {
  // 把 data 中的秒数重置成 3 秒
  this.seconds = 3
  this.showTips(this.seconds)

  this.timer = setInterval(() => {
    this.seconds--

    if (this.seconds <= 0) {
      clearInterval(this.timer)
      uni.switchTab({
        url: '/pages/my/my'
      })
      return
    }

    this.showTips(this.seconds)
  }, 1000)
}

 5.微信支付

1.在请求头中添加 Token 身份认证的字段

打开项目根目录下的 main.js,改造 $http.beforeRequest 请求拦截器中的代码如下:

// 请求开始之前做一些事情
$http.beforeRequest = function(options) {
  uni.showLoading({
    title: '数据加载中...',
  })

  // 判断请求的是否为有权限的 API 接口
  if (options.url.indexOf('/my/') !== -1) {
    // 为请求头添加身份认证字段
    options.header = {
      // 字段的值可以直接从 vuex 中进行获取
      Authorization: store.state.m_user.token,
    }
  }
}

2.微信支付的流程

1.创建订单

  • 请求创建订单的 API 接口:把(订单金额、收货地址、订单中包含的商品信息)发送到服务器
  • 服务器响应的结果:订单编号

2.订单预支付

  • 请求订单预支付的 API 接口:把(订单编号)发送到服务器
  • 服务器响应的结果:订单预支付的参数对象,里面包含了订单支付相关的必要参数

3.发起微信支付

  • 调用 uni.requestPayment() 这个 API,发起微信支付;把步骤 2 得到的 “订单预支付对象” 作为参数传递给 uni.requestPayment() 方法
  • 监听 uni.requestPayment() 这个 API 的 successfailcomplete 回调函数

 改造 my-settle 组件中的 settlement 方法,当前三个判断条件通过之后,调用实现微信支付的方法:

// 点击了结算按钮
settlement() {
  // 1. 先判断是否勾选了要结算的商品
  if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')

  // 2. 再判断用户是否选择了收货地址
  if (!this.addstr) return uni.$showMsg('请选择收货地址!')

  // 3. 最后判断用户是否登录了
  // if (!this.token) return uni.$showMsg('请先登录!')
  if (!this.token) return this.delayNavigate()

  // 4. 实现微信支付功能
  this.payOrder()
},

payOrder支付函数 

// 微信支付
async payOrder() {
  // 1. 创建订单
  // 1.1 组织订单的信息对象
  const orderInfo = {
    // 开发期间,注释掉真实的订单价格,
    // order_price: this.checkedGoodsAmount,
    // 写死订单总价为 1 分钱
    order_price: 0.01,
    consignee_addr: this.addstr,
    goods: this.cart.filter(x => x.goods_state).map(x => ({ goods_id: x.goods_id, goods_number: x.goods_count, goods_price: x.goods_price }))
  }
  // 1.2 发起请求创建订单
  const { data: res } = await uni.$http.post('/api/public/v1/my/orders/create', orderInfo)
  if (res.meta.status !== 200) return uni.$showMsg('创建订单失败!')
  // 1.3 得到服务器响应的“订单编号”
  const orderNumber = res.message.order_number

   // 2. 订单预支付
   // 2.1 发起请求获取订单的支付信息
   const { data: res2 } = await uni.$http.post('/api/public/v1/my/orders/req_unifiedorder', { order_number: orderNumber })
   // 2.2 预付订单生成失败
   if (res2.meta.status !== 200) return uni.$showError('预付订单生成失败!')
   // 2.3 得到订单支付相关的必要参数
   const payInfo = res2.message.pay

   // 3. 发起微信支付
   // 3.1 调用 uni.requestPayment() 发起微信支付
   const [err, succ] = await uni.requestPayment(payInfo)
   // 3.2 未完成支付
   if (err) return uni.$showMsg('订单未支付!')
   // 3.3 完成了支付,进一步查询支付的结果
   const { data: res3 } = await uni.$http.post('/api/public/v1/my/orders/chkOrder', { order_number: orderNumber })
   // 3.4 检测到订单未支付
   if (res3.meta.status !== 200) return uni.$showMsg('订单未支付!')
   // 3.5 检测到订单支付完成
   uni.showToast({
     title: '支付完成!',
     icon: 'success'
   })
 }

 到这里,整个项目也算是结束了,需要我的项目代码可以使用git克隆到本地

在一个文件夹中输入这行命令即可

git clone https://github.com/jiangjunjie666/uni-shop.git

你可能感兴趣的:(微信小程序,前端,html,微信小程序,小程序,前端,javascript)