目录
项目总效果展示:
一、新建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.微信支付的流程
第一步:在项目目录中新建.gitignore文件,然后在里面写入要忽略的文件
/node_modules
/unpackage/dist
第二步:为了让git能够跟踪unpackage文件,需要在该文件目录下新建一个.gitkeep文件
第三步:打开git Bash终端初始化一个git仓库
git init
再将文件添加到暂存区
git add .
然后进行第一次提交项目
git commit -m "init project"
首先新建一个仓库
按照下面的指示在git Bash上面操作即可
!!!这里标错了一个地方 第四条那不是新建分支,而是修改现分支的名字,我们新建仓库默认的分支名字是master
提交成功刷新后就可以看到我们的项目仓库已经有了刚提交的文件了
在后面我们只需要每次提交代码后只需要 git push -u origin 分支名 命令就可以提交代码至github了,但是一定要注意分支的不同喔。
git checkout -b tabBar
可以使用此命令查看所有的分支
git branch -v
同理分别创建cate cart my 页面
第一步:
将需要用到的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页面删除
效果就出来了
打开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分支
之后的新建分支与提交代码将不再展示
因为小程序不支持axios,而且原生的wx.request API的效果不能支持拦截器等功能,所以我们使用的是第三方包 在项目目录的终端中初始化一个npm包管理器,然后下载该包
npm init -y
npm i @escook/request-miniprogram
//main.js文件中
//导入网络请求的包
import {$http} from '@escook/request-miniprogram'
uni.$http = $http
$http.beforeRequest = function(options){
wx.showLoading({
title:"数据加载中"
})
}
$http.afterRequest = function(){
uni.hideLoading()
}
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分包
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.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.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 属性
的值:
按照之前的来就行
实现后的效果图
- 使用scroll-view实现滑动效果
- 使用uni.getSystemInfoSync()获取到设备信息,达到自适应的效果
- 进行样式的美化,其中使用伪元素实现红色指示边线
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
xxx
多复制一些节点,演示纵向滚动效果...
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
多复制一些节点,演示纵向滚动效果
- 使用v-for循环渲染结构
- 添加点击事件来切换active类名动态绑定
{{item.cat_name}}
点击事件
methods: {
// 选中项改变的事件处理函数
activeChanged(i) {
this.active = i
}
}
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.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.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.1 为三级分类的 Item 项绑定点击事件处理函数如下:
{{item3.cat_name}}
7.2 定义事件处理函数如下:
// 点击三级分类项跳转到商品列表页面
gotoGoodsList(item3) {
uni.navigateTo({
url: '/subpkg/goods_list/goods_list?cid=' + item3.cat_id
})
}
实现效果图:
在项目目录中新建一个components文件夹,然后新建自定义组件
在分类页面的 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
}
为了增强组件的通用性,我们允许使用者自定义搜索组件的 背景颜色
和 圆角尺寸
。
通过 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.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.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.1渲染搜索页面的基本结构
定义如下的 UI 结构:
实现搜索框的吸顶效果:
.search-box {
position: sticky;
top: 0;
z-index: 999;
}
定义如下的 input 事件处理函数:
methods: {
input(e) {
// e 是最新的搜索内容
console.log(e)
}
}
5.2 实现自动获取焦点
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.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 解决关键字前后顺序的问题
data 中的 historyList
不做任何修改,依然使用 push 进行末尾追加
定义一个计算属性 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
})
}
效果图:
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 || ''
}
在 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
}
}
在页面中,通过 v-for
指令,循环渲染出商品的 UI 结构:
{{goods.goods_name}}
¥{{goods.goods_price}}
为了防止某些商品的图片不存在,需要在 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.1首先在目录中新建对应的组件
4.2 将 goods_list
页面中,关于商品 item 项相关的 UI 结构、样式、data 数据,封装到 my-goods
组件中:
{{goods.goods_name}}
¥{{goods.goods_price}}
4.3 在 goods_list
组件中,循环渲染 my-goods
组件即可:
在 my-goods
组件中,和 data
节点平级,声明 filters
过滤器节点如下:
filters: {
// 把数字处理为带两位小数点的数字
tofixed(num) {
return Number(num).toFixed(2)
}
}
在渲染商品价格的时候,通过管道符 |
调用过滤器:
¥{{goods.goods_price | tofixed}}
打开项目根目录中的 pages.json
配置文件,为 subPackages
分包中的 goods_list
页面配置上拉触底的距离:
在 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
}
在 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()
}
在 pages.json
配置文件中,为当前的 goods_list
页面单独开启下拉刷新效果:
监听页面的 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
}
将循环时的 block
组件修改为 view
组件,并绑定 click
点击事件处理函数:
在 methods
节点中,定义 gotoDetail
事件处理函数:
// 点击跳转到商品详情页面
gotoDetail(item) {
uni.navigateTo({
url: '/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id
})
}
实现效果图:
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.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.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.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(/
导致问题的原因:在商品详情数据请求回来之前,data 中 goods_info
的值为 {}
,因此初次渲染页面时,会导致 商品价格、商品名称
等闪烁的问题。
解决方案:判断 goods_info.goods_name
属性的值是否存在,从而使用 v-if
指令控制页面的显示与隐藏:
基于 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%;
}
根据 e.content.text
的值来判断跳转页面
// 左侧按钮的点击事件处理函数
onClick(e) {
if (e.content.text === '购物车') {
// 切换到购物车页面
uni.switchTab({
url: '/pages/cart/cart'
})
}
}
效果图
1.1在项目根目录中创建 store
文件夹,专门用来存放 vuex 相关的模块
1.2在 store
目录上鼠标右键,选择 新建 -> js文件
,新建 store.js
文件:
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.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.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.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.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.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') || '[]')
}),
使用普通函数的形式定义的 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.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.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.vue
,cate.vue
,cart.vue
,my.vue
这 4 个 tabBar 页面的源代码,分别导入 @/mixins/tabbar-badge.js
模块并进行使用:
// 导入自己封装的 mixin 模块
import badgeMix from '@/mixins/tabbar-badge.js'
export default {
// 将 badgeMix 混入到当前的页面中进行使用
mixins: [badgeMix],
// 省略其它代码...
}
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.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.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.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.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.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.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.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.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.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)
}
}
收货人: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.1 在 data 中定义收货地址的信息对象:
export default {
data() {
return {
// 收货地址
address: {},
}
},
}
3.2 使用 v-if
和 v-else
实现按需展示:
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.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.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: {},
}
目的:为了提高代码的复用性,可以把收货的详细地址抽离为 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.1为 class 类名为 address-info-box
的盒子绑定 click
事件处理函数如下:
my-settle
组件的基本结构和样式:
结算组件
在 cart.vue
页面中使用自定义的 my-settle
组件,并美化页面样式,防止页面底部被覆盖:
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.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.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.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.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}}
问题说明:当修改购物车中商品的数量之后,tabBar 上的数字徽标不会自动更新。
解决方案:改造
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 的值必须是字符串,不能是数字
})
},
},
}
改造 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;
}
}
说明:用户点击了结算按钮之后,需要先后判断是否勾选了要结算的商品、是否选择了收货地址、是否登录
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
},
},
}
先分别新建二个组件,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
页面中,实现登录组件和用户信息组件的按需展示:
登录后尽享更多权益
为登录的 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)
}
}
在 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)
},
},
}
需求说明:当获取到了微信用户的基本信息之后,还需要进一步调用登录相关的接口,从而换取登录成功之后的 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('登录成功')
}
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)
}
}
xxx
在 my-userinfo
组件中,通过 mapState
辅助函数,将需要的成员映射到当前组件中使用:
// 按需导入 mapState 辅助函数
import { mapState } from 'vuex'
export default {
computed: {
// 将 m_user 模块中的 userinfo 映射到当前页面中使用
...mapState('m_user', ['userinfo']),
},
data() {
return {}
},
}
将用户的头像和昵称渲染到页面中:
{{userinfo.nickName}}
8
收藏的店铺
14
收藏的商品
18
关注的商品
84
足迹
我的订单
待付款
待收货
退款/退货
全部订单
收货地址
联系客服
退出登录
为第三个面板区域中的 退出登录
项绑定 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']),
},
}
需求描述:在购物车页面,当用户点击 “结算” 按钮时,如果用户没有登录,则 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)
}
打开项目根目录下的 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,
}
}
}
1.创建订单
- 请求创建订单的 API 接口:把(订单金额、收货地址、订单中包含的商品信息)发送到服务器
服务器响应的结果:订单编号
2.订单预支付
- 请求订单预支付的 API 接口:把(订单编号)发送到服务器
服务器响应的结果:订单预支付的参数对象,里面包含了订单支付相关的必要参数
3.发起微信支付
- 调用
uni.requestPayment()
这个 API,发起微信支付;把步骤 2 得到的 “订单预支付对象” 作为参数传递给uni.requestPayment()
方法- 监听
uni.requestPayment()
这个 API 的success
,fail
,complete
回调函数
改造 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