当前位置:网站首页>uni-app黑马优购项目学习记录(下)
uni-app黑马优购项目学习记录(下)
2022-04-23 17:25:00 【海底烧烤店ai】
写在前边
这篇博文是在黑马程序员uniapp-黑马优购项目文档的基础上进行书写的,原文在这里:uniapp-黑马优购,视频教程。
文章资料和接口文档:链接:https://pan.baidu.com/s/1dkJu8aaJEjnLEfERL7j1Nw
提取码:b3ub
如你所见,本人现在正在学习黑马的uniapp,为了让自己能够方便的复习所学知识,我将自己的理解以及一些在学习过程中遇到的问题和解决方法与黑马原有的文档进行结合,书写了这篇博文,如有侵权,联系必删!
查看此篇博客时请先查看:uni-app黑马优购项目学习记录(上)
uni-app黑马优购项目学习记录(下)
- 写在前边
- 8. 加入购物车
- 9. 购物车页面
-
- 9.1 商品列表区域
-
- 9.1.1 渲染购物车商品列表的标题区域
- 9.1.2 渲染商品列表区域的基本结构
- 9.1.3 为 my-goods 组件封装 radio 勾选状态
- 9.1.4 为 my-goods 组件封装 radio-change 事件
- 9.1.5 修改购物车中商品的勾选状态
- 9.1.6 为 my-goods 组件封装 NumberBox
- 9.1.7 为 my-goods 组件封装 num-change 事件
- 9.1.8 解决 NumberBox 数据不合法的问题
- 9.1.9 完善 NumberBox 的 inputValue 侦听器
- 9.1.10 修改购物车中商品的数量
- 9.1.11 渲染滑动删除的 UI 效果
- 9.1.12 实现滑动删除的功能
- 9.2 收货地址区域
- 9.3 结算区域
- 9.4 分支的合并与提交
- 10. 登录与支付
- 11. 发布
8. 加入购物车

8.0 创建 cart 分支
运行如下的命令,基于 master 分支在本地创建 cart 子分支,用来开发购物车相关的功能:
git checkout -b cart
8.1 配置 vuex
-
在项目根目录中创建
store文件夹,专门用来存放vuex相关的模块 -
在
store目录上鼠标右键,选择新建 -> js文件,新建store.js文件:
-
在
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 -
在
main.js中导入store实例对象并挂载到 Vue 的实例上:// 1. 导入 store 的实例对象 import store from './store/store.js' // 省略其它代码... const app = new Vue({ ...App, // 2. 将 store 挂载到 Vue 实例上 store, }) app.$mount()
8.2 创建购物车的 store 模块
-
在
store目录上鼠标右键,选择新建 -> js文件,创建购物车的store模块,命名为cart.js: -
在
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: { }, } -
在
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
8.3 在商品详情页中使用 Store 中的数据
-
在
goods_detail.vue页面中,修改<script></script>标签中的代码如下:// 从 vuex 中按需导出 mapState 辅助方法 import { mapState } from 'vuex' export default { computed: { // 调用 mapState 方法,把 m_cart 模块中的 cart 数组映射到当前页面中,作为计算属性来使用 // ...mapState('模块的名称', ['要映射的数据名称1', '要映射的数据名称2']) ...mapState('m_cart', ['cart']), }, // 省略其它代码... }注意:今后无论映射
mutations方法,还是getters属性,还是state中的数据,都需要指定模块的名称,才能进行映射。 -
在页面渲染时,可以直接使用映射过来的数据,例如:
<!-- 运费 --> <view class="yf">快递:免运费 -- { {cart.length}}</view>
8.4 实现加入购物车的功能
-
在
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: { }, } -
在商品详情页面中,通过
mapMutations这个辅助方法,把vuex中m_cart模块下的addToCart方法映射到当前页面:// 按需导入 mapMutations 这个辅助方法 import { mapMutations } from 'vuex' export default { methods: { // 把 m_cart 模块中的 addToCart 方法映射到当前页面使用 ...mapMutations('m_cart', ['addToCart']), }, } -
为商品导航组件
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) } }
8.5 动态统计购物车中商品的总数量
-
在
cart.js模块中,在getters节点下定义一个total方法,用来统计购物车中商品的总数量:// 模块的 getters 属性 getters: { // 统计购物车中商品的总数量 total(state) { let c = 0 // 循环统计商品的数量,累加到变量 c 中 state.cart.forEach(goods => c += goods.goods_count) return c } } -
在商品详情页面的
script标签中,按需导入mapGetters方法并进行使用:// 按需导入 mapGetters 这个辅助方法 import { mapGetters } from 'vuex' export default { computed: { // 把 m_cart 模块中名称为 total 的 getter 映射到当前页面中使用 ...mapGetters('m_cart', ['total']), }, } -
通过
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 } }, }, }
8.6 持久化存储购物车中的商品
-
在
cart.js模块中,声明一个叫做saveToStorage的mutations方法,此方法负责将购物车中的数据持久化存储到本地:// 将购物车中的数据持久化存储到本地 saveToStorage(state) { uni.setStorageSync('cart', JSON.stringify(state.cart)) } -
修改
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') } -
修改
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') || '[]') }),
8.7 优化商品详情页的 total 侦听器
-
使用普通函数的形式定义的
watch侦听器,在页面首次加载后不会被调用。因此导致了商品详情页在首次加载完毕之后,不会将商品的总数量显示到商品导航区域:watch: { // 页面首次加载完毕后,不会调用这个侦听器 total(newVal) { const findResult = this.options.find(x => x.text === '购物车') if (findResult) { findResult.info = newVal } } } -
为了防止这个上述问题,可以使用对象的形式来定义
watch侦听器(详细文档请参考Vue官方的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.8 动态为 tabBar 页面设置数字徽标
需求描述:从商品详情页面导航到购物车页面之后,需要为
tabBar中的购物车动态设置数字徽标。
-
把
Store中的total映射到cart.vue中使用:// 按需导入 mapGetters 这个辅助方法 import { mapGetters } from 'vuex' export default { data() { return { } }, computed: { // 将 m_cart 模块中的 total 映射为当前页面的计算属性 ...mapGetters('m_cart', ['total']), }, } -
在页面刚显示出来的时候,立即调用
setBadge方法,为tabBar设置数字徽标:onShow() { // 在页面刚展示的时候,设置数字徽标 this.setBadge() } -
在
methods节点中,声明setBadge方法如下,通过uni.setTabBarBadge()为tabBar设置数字徽标:methods: { setBadge() { // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标 uni.setTabBarBadge({ index: 2, // 索引 text: this.total + '' // 注意:text 的值必须是字符串,不能是数字 }) } }
8.9 将设置 tabBar 徽标的代码抽离为 mixins
注意:除了要在
cart.vue页面中设置购物车的数字徽标,还需要在其它 3 个 tabBar 页面中,为购物车设置数字徽标。
此时可以使用
Vue提供的mixins特性,提高代码的可维护性。
-
在项目根目录中新建
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 的值必须是字符串,不能是数字 }) }, }, } -
修改
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], // 省略其它代码... }
9. 购物车页面

9.1 商品列表区域
9.1.1 渲染购物车商品列表的标题区域
-
定义如下的 UI 结构:
<!-- 购物车商品列表的标题区域 --> <view class="cart-title"> <!-- 左侧的图标 --> <uni-icons type="shop" size="18"></uni-icons> <!-- 描述文本 --> <text class="cart-title-text">购物车</text> </view> -
美化样式:
.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; } }
9.1.2 渲染商品列表区域的基本结构
-
通过
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 { } }, } -
在 UI 结构中,通过
v-for指令循环渲染自定义的my-goods组件:<!-- 商品列表区域 --> <block v-for="(goods, i) in cart" :key="i"> <my-goods :goods="goods"></my-goods> </block>
9.1.3 为 my-goods 组件封装 radio 勾选状态
-
打开
my-goods.vue组件的源代码,为商品的左侧图片区域添加radio组件:<!-- 商品左侧图片区域 --> <view class="goods-item-left"> <radio checked color="#C00000"></radio> <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image> </view> -
给类名为
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; } } -
封装名称为
showRadio的props属性,来控制当前组件中是否显示radio组件:export default { // 定义 props 属性,用来接收外界传递到当前组件的数据 props: { // 商品的信息对象 goods: { type: Object, default: { }, }, // 是否展示图片左侧的 radio showRadio: { type: Boolean, // 如果外界没有指定 show-radio 属性的值,则默认不展示 radio 组件 default: false, }, }, } -
使用
v-if指令控制radio组件的按需展示:<!-- 商品左侧图片区域 --> <view class="goods-item-left"> <!-- 使用 v-if 指令控制 radio 组件的显示与隐藏 --> <radio checked color="#C00000" v-if="showRadio"></radio> <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image> </view> -
在
cart.vue页面中的商品列表区域,指定:show-radio="true"属性,从而显示radio组件:<!-- 商品列表区域 --> <block v-for="(goods, i) in cart" :key="i"> <my-goods :goods="goods" :show-radio="true"></my-goods> </block> -
修改
my-goods.vue组件,动态为radio绑定选中状态:<!-- 商品左侧图片区域 --> <view class="goods-item-left"> <!-- 存储在购物车中的商品,包含 goods_state 属性,表示商品的勾选状态 --> <radio :checked="goods.goods_state" color="#C00000" v-if="showRadio"></radio> <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image> </view>
9.1.4 为 my-goods 组件封装 radio-change 事件
-
当用户点击
radio组件,希望修改当前商品的勾选状态,此时用户可以为my-goods组件绑定@radio-change事件,从而获取当前商品的goods_id和goods_state:<!-- 商品列表区域 --> <block v-for="(goods, i) in cart" :key="i"> <!-- my-good内定义的props属性为showRadio驼峰命名法时,在使用时需要写成show-radio形式 --> <!-- 在 radioChangeHandler 事件处理函数中,通过事件对象 e,得到商品的 goods_id 和 goods_state --> <my-goods :goods="goods" :show-radio="true" @radio-change="radioChangeHandler"></my-goods> </block>定义
radioChangeHandler事件处理函数如下:methods: { // 商品的勾选状态发生了变化 radioChangeHandler(e) { console.log(e) // 输出得到的数据 -> {goods_id: 395, goods_state: false} } } -
在
my-goods.vue组件中,为radio组件绑定@click事件处理函数如下:<!-- 商品左侧图片区域 --> <view class="goods-item-left"> <radio :checked="goods.goods_state" color="#C00000" v-if="showRadio" @click="radioClickHandler"></radio> <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image> </view> -
在
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 }) } }
9.1.5 修改购物车中商品的勾选状态
-
在
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') } } -
在
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) }, }, }
9.1.6 为 my-goods 组件封装 NumberBox
注意:
NumberBox组件是uni-ui提供的
-
修改
my-goods.vue组件的源代码,在类名为goods-info-box的view组件内部渲染NumberBox组件的基本结构:<view class="goods-info-box"> <!-- 商品价格 --> <view class="goods-price">¥{ {goods.goods_price | tofixed}}</view> <!-- 商品数量 --> <uni-number-box :min="1"></uni-number-box> </view> -
美化页面的结构:
.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; } } -
在
my-goods.vue组件中,动态为NumberBox组件绑定商品的数量值:<view class="goods-info-box"> <!-- 商品价格 --> <view class="goods-price">¥{ {goods.goods_price | tofixed}}</view> <!-- 商品数量 --> <uni-number-box :min="1" :value="goods.goods_count"></uni-number-box> </view> -
在
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, }, }, } -
在
my-goods.vue组件中,使用v-if指令控制NumberBox组件的按需展示:<view class="goods-info-box"> <!-- 商品价格 --> <view class="goods-price">¥{ {goods.goods_price | tofixed}}</view> <!-- 商品数量 --> <uni-number-box :min="1" :value="goods.goods_count" @change="numChangeHandler" v-if="showNum"></uni-number-box> </view> -
在
cart.vue页面中的商品列表区域,指定:show-num="true"属性,从而显示NumberBox组件:<!-- 商品列表区域 --> <block v-for="(goods, i) in cart" :key="i"> <my-goods :goods="goods" :show-radio="true" :show-num="true" @radio-change="radioChangeHandler"></my-goods> </block>
9.1.7 为 my-goods 组件封装 num-change 事件
-
当用户修改了
NumberBox的值以后,希望将最新的商品数量更新到购物车中,此时用户可以为my-goods组件绑定@num-change事件,从而获取当前商品的goods_id和goods_count:<!-- 商品列表区域 --> <block v-for="(goods, i) in cart" :key="i"> <my-goods :goods="goods" :show-radio="true" :show-num="true" @radio-change="radioChangeHandler" @num-change="numberChangeHandler"></my-goods> </block>定义
numberChangeHandler事件处理函数如下:// 商品的数量发生了变化 numberChangeHandler(e) { console.log(e) } -
在
my-goods.vue组件中,为uni-number-box组件绑定@change事件处理函数如下:<view class="goods-info-box"> <!-- 商品价格 --> <view class="goods-price">¥{ {goods.goods_price | tofixed}}</view> <!-- 商品数量 --> <uni-number-box :min="1" :value="goods.goods_count" @change="numChangeHandler"></uni-number-box> </view> -
在
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 }) } }
9.1.8 解决 NumberBox 数据不合法的问题
问题说明:当用户在
NumberBox中输入字母等非法字符之后,会导致NumberBox数据紊乱的问题
-
打开项目根目录中
components/uni-number-box/uni-number-box.vue组件,修改methods节点中的_onBlur函数如下:_onBlur(event) { // 官方的代码没有进行数值转换,用户输入的 value 值可能是非法字符: // let value = event.detail.value; // 将用户输入的内容转化为整数 let value = parseInt(event.detail.value); if (!value) { // 如果转化之后的结果为 NaN,则给定默认值为 1 this.inputValue = 1; return; } // 省略其它代码... } -
修改完毕之后,用户输入小数会被转化为整数,用户输入非法字符会被替换为默认值 1
9.1.9 完善 NumberBox 的 inputValue 侦听器
问题说明:在用户每次输入内容之后,都会触发
inputValue侦听器,从而调用this.$emit("change", newVal)方法。这种做法可能会把不合法的内容传递出去!
-
打开项目根目录中
components/uni-number-box/uni-number-box.vue组件,修改watch节点中的inputValue侦听器如下:inputValue(newVal, oldVal) { // 官方提供的 if 判断条件,在用户每次输入内容时,都会调用 this.$emit("change", newVal) // if (+newVal !== +oldVal) { // 新旧内容不同 && 新值内容合法 && 新值中不包含小数点 if (+newVal !== +oldVal && Number(newVal) && String(newVal).indexOf('.') === -1) { this.$emit("change", newVal); } } -
修改完毕之后,
NumberBox组件只会把合法的、且不包含小数点的新值传递出去
9.1.10 修改购物车中商品的数量
-
在
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') } } -
在
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.11 渲染滑动删除的 UI 效果
滑动删除需要用到
uni-ui的uni-swipe-action组件和uni-swipe-action-item。详细的官方文档请参考SwipeAction 滑动操作。
-
改造
cart.vue页面的 UI 结构,将商品列表区域的结构修改如下(可以使用uSwipeAction代码块快速生成基本的 UI 结构):<!-- 商品列表区域 --> <!-- uni-swipe-action 是最外层包裹性质的容器 --> <uni-swipe-action> <block v-for="(goods, i) in cart" :key="i"> <!-- uni-swipe-action-item 可以为其子节点提供滑动操作的效果。需要通过right-options 属性来指定从右向左滑操作按钮的配置信息 --> <uni-swipe-action-item :right-options="options" @click="swipeActionClickHandler(goods)"> <my-goods :goods="goods" :show-radio="true" :show-num="true" @radio-change="radioChangeHandler" @num-change="numberChangeHandler"></my-goods> </uni-swipe-action-item> </block> </uni-swipe-action> -
在
data节点中声明options数组,用来定义操作按钮的配置信息:data() { return { options: [{ text: '删除', // 显示的文本内容 style: { backgroundColor: '#C00000' // 按钮的背景颜色 } }] } } -
在
methods中声明uni-swipe-action-item组件的@click事件处理函数:// 点击了滑动操作按钮 swipeActionClickHandler(goods) { console.log(goods) } -
美化
my-goods.vue组件的样式:.goods-item { // 让 goods-item 项占满整个屏幕的宽度 width: 750rpx; // 设置盒模型为 border-box box-sizing: border-box; display: flex; padding: 10px 5px; border-bottom: 1px solid #f0f0f0; }
9.1.12 实现滑动删除的功能
-
在
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') } -
在
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) } }
9.2 收货地址区域
9.2.1 创建收货地址组件
-
在
components目录上鼠标右键,选择新建组件,并填写组件相关的信息。 -
在
cart.vue引用<template> <view> <!-- 收获地址 --> <my-address></my-address> <!--..--> <view> <template> -
渲染收货地址组件的基本结构:
<view> <!-- 选择收货地址的盒子 --> <view class="address-choose-box"> <button type="primary" size="mini" class="btnChooseAddress">请选择收货地址+</button> </view> <!-- 渲染收货信息的盒子 --> <view class="address-info-box"> <view class="row1"> <view class="row1-left"> <view class="username">收货人:<text>escook</text></view> </view> <view class="row1-right"> <view class="phone">电话:<text>138XXXX5555</text></view> <uni-icons type="arrowright" size="16"></uni-icons> </view> </view> <view class="row2"> <view class="row2-left">收货地址:</view> <view class="row2-right">河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx </view> </view> </view> <!-- 底部的边框线 --> <image src="/static/[email protected]" class="address-border"></image> </view> -
美化收货地址组件的样式:
// 底部边框线的样式 .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; } } }
9.2.2 实现收货地址区域的按需展示
-
在
data中定义收货地址的信息对象:export default { data() { return { // 收货地址 address: { }, } }, } -
使用
v-if和v-else实现按需展示:<!-- 选择收货地址的盒子 --> <view class="address-choose-box" v-if="JSON.stringify(address) === '{}'"> <button type="primary" size="mini" class="btnChooseAddress">请选择收货地址+</button> </view> <!-- 渲染收货信息的盒子 --> <view class="address-info-box" v-else> <!-- 省略其它代码 --> </view>
9.2.3 实现选择收货地址的功能
-
为
请选择收货地址+的button按钮绑定点击事件处理函数:<!-- 选择收货地址的盒子 --> <view class="address-choose-box" v-if="JSON.stringify(address) === '{}'"> <button type="primary" size="mini" class="btnChooseAddress" @click="chooseAddress">请选择收货地址+</button> </view> -
定义
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 } } } -
定义收货详细地址的计算属性:
computed: { // 收货详细地址的计算属性 addstr() { if (!this.address.provinceName) return '' // 拼接 省,市,区,详细地址 的字符串并返回给用户 return this.address.provinceName + this.address.cityName + this.address.countyName + this.address.detailInfo } } -
渲染收货地址区域的数据:
<!-- 渲染收货信息的盒子 --> <view class="address-info-box" v-else> <view class="row1"> <view class="row1-left"> <view class="username">收货人:<text>{ {address.userName}}</text></view> </view> <view class="row1-right"> <view class="phone">电话:<text>{ {address.telNumber}}</text></view> <uni-icons type="arrowright" size="16"></uni-icons> </view> </view> <view class="row2"> <view class="row2-left">收货地址:</view> <view class="row2-right">{ {addstr}}</view> </view> </view>
9.2.4 将 address 信息存储到 vuex 中
-
在
store目录中,创建用户相关的vuex模块,命名为user.js:export default { // 开启命名空间 namespaced: true, // state 数据 state: () => ({ // 收货地址 address: { }, }), // 方法 mutations: { // 更新收货地址 updateAddress(state, address) { state.address = address }, }, // 数据包装器 getters: { }, } -
在
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 -
改造
my-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 }, }, }
9.2.5 将 Store 中的 address 持久化存储到本地
-
修改
store/user.js模块中的代码如下: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: { }, }
9.2.6 将 addstr 抽离为 getters
目的:为了提高代码的复用性,可以把收货的详细地址抽离为
getters,方便在多个页面和组件之间实现复用。
-
剪切
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 } } -
改造
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']), }, }
9.2.7 重新选择收货地址
-
为
class类名为address-info-box的盒子绑定click事件处理函数如下:<!-- 渲染收货信息的盒子 --> <view class="address-info-box" v-else @click="chooseAddress"> <!-- 省略其它代码 --> </view>
9.2.8 解决收货地址授权失败的问题

如果在选择收货地址的时候,用户点击了取消授权,则需要进行特殊的处理,否则用户将无法再次选择收货地址!
注意:新版本的微信小程序不会再弹出授权提示框,默认是已授权状态,所以做以下处理主要是适配低版本小程序
-
改造
chooseAddress方法如下:// 选择收货地址 async chooseAddress() { // 1. 调用小程序提供的 chooseAddress() 方法,即可使用选择收货地址的功能 // 返回值是一个数组:第1项为错误对象;第2项为成功之后的收货地址对象 const [err, succ] = await uni.chooseAddress().catch(err => err) // 2. 用户成功的选择了收货地址 if (succ && succ.errMsg === 'chooseAddress:ok') { // 更新 vuex 中的收货地址 this.updateAddress(succ) } // 3. 用户没有授权 if (err && err.errMsg === 'chooseAddress:fail auth deny') { this.reAuth() // 调用 this.reAuth() 方法,向用户重新发起授权申请 } } -
在
methods节点中声明reAuth方法如下:// 调用此方法,重新发起收货地址的授权 async reAuth() { // 3.1 提示用户对地址进行授权 const [err2, confirmResult] = await uni.showModal({ content: '检测到您没打开地址权限,是否去设置打开?', confirmText: "确认", cancelText: "取消", }) // 3.2 如果弹框异常,则直接退出 if (err2) return // 3.3 如果用户点击了 “取消” 按钮,则提示用户 “您取消了地址授权!” if (confirmResult.cancel) return uni.$showMsg('您取消了地址授权!') // 3.4 如果用户点击了 “确认” 按钮,则调用 uni.openSetting() 方法进入授权页面,让用户重新进行授权 if (confirmResult.confirm) return uni.openSetting({ // 3.4.1 授权结束,需要对授权的结果做进一步判断 success: (settingResult) => { // 3.4.2 地址授权的值等于 true,提示用户 “授权成功” if (settingResult.authSetting['scope.address']) return uni.$showMsg('授权成功!请选择地址') // 3.4.3 地址授权的值等于 false,提示用户 “您取消了地址授权” if (!settingResult.authSetting['scope.address']) return uni.$showMsg('您取消了地址授权!') } }) }
9.2.9 解决 iPhone 真机上无法重新授权的问题
问题说明:在 iPhone 设备上,当用户取消授权之后,再次点击选择收货地址按钮的时候,无法弹出授权的提示框!
-
导致问题的原因 - 用户取消授权后,再次点击 “选择收货地址” 按钮的时候:
在模拟器和安卓真机上,错误消息
err.errMsg的值为chooseAddress:fail auth deny
在 iPhone 真机上,错误消息err.errMsg的值为chooseAddress:fail authorize no response -
解决问题的方案 - 修改
chooseAddress方法中的代码,进一步完善用户没有授权时的if判断条件即可:async chooseAddress() { // 1. 调用小程序提供的 chooseAddress() 方法,即可使用选择收货地址的功能 // 返回值是一个数组:第1项为错误对象;第2项为成功之后的收货地址对象 const [err, succ] = await uni.chooseAddress().catch(err => err) // 2. 用户成功的选择了收货地址 if (succ && succ.errMsg === 'chooseAddress:ok') { this.updateAddress(succ) } // 3. 用户没有授权 if (err && (err.errMsg === 'chooseAddress:fail auth deny' || err.errMsg === 'chooseAddress:fail authorize no response')) { this.reAuth() } }
9.3 结算区域
9.3.1 把结算区域封装为组件
-
在
components目录中,新建my-settle结算组件 -
初始化
my-settle组件的基本结构和样式:<template> <!-- 最外层的容器 --> <view class="my-settle-container"> 结算组件 </view> </template> <script> export default { data() { return { } }, } </script> <style lang="scss"> .my-settle-container { /* 底部固定定位 */ position: fixed; bottom: 0; left: 0; /* 设置宽高和背景色 */ width: 100%; height: 50px; background-color: cyan; } </style> -
在
cart.vue页面中使用自定义的my-settle组件,并美化页面样式,防止页面底部被覆盖:<template> <view class="cart-container"> <!-- 使用自定义的 address 组件 --> <!-- 购物车商品列表的标题区域 --> <!-- 商品列表区域 --> <!-- 结算区域 --> <my-settle></my-settle> </view> </template> <style lang="scss"> .cart-container { padding-bottom: 50px; } </style>
9.3.2 渲染结算区域的结构和样式
-
定义如下的 UI 结构:
<!-- 最外层的容器 --> <view class="my-settle-container"> <!-- 全选区域 --> <label class="radio"> <radio color="#C00000" :checked="true" /><text>全选</text> </label> <!-- 合计区域 --> <view class="amount-box"> 合计:<text class="amount">¥1234.00</text> </view> <!-- 结算按钮 --> <view class="btn-settle">结算(0)</view> </view> -
美化样式:
.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; } }
9.3.3 动态渲染已勾选商品的总数量
-
在
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) }reduce()方法:array.reduce((pre, cur, index, arr)=>{ ... }, init);pre: 必需。初始值, 或者计算结束后的返回值。
cur: 必需。当前元素。
index: 可选。当前元素的索引。
arr: 可选。当前元素所属的数组对象。
init: 可选。传递给函数的初始值,相当于pre的初始值。 -
在
my-settle组件中,通过mapGetters辅助函数,将需要的getters映射到当前组件中使用:import { mapGetters } from 'vuex' export default { computed: { ...mapGetters('m_cart', ['checkedCount']), }, data() { return { } }, } -
将
checkedCount的值渲染到页面中:<!-- 结算按钮 --> <view class="btn-settle">结算({ {checkedCount}})</view>
9.3.4 动态渲染全选按钮的选中状态
-
使用
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 { } }, } -
为
radio组件动态绑定checked属性的值:<!-- 全选区域 --> <label class="radio"> <radio color="#C00000" :checked="isFullCheck" /><text>全选</text> </label>
9.3.5 实现商品的全选/反选功能
-
在
store/cart.js模块中,定义一个叫做updateAllGoodsState的mutations方法,用来修改所有商品的勾选状态:// 更新所有商品的勾选状态 updateAllGoodsState(state, newState) { // 循环更新购物车中每件商品的勾选状态 state.cart.forEach(x => x.goods_state = newState) // 持久化存储到本地 this.commit('m_cart/saveToStorage') } -
在
my-settle组件中,通过mapMutations辅助函数,将需要的mutations方法映射到当前组件中使用:// 1. 按需导入 mapMutations 辅助函数 import { mapGetters, mapMutations } from 'vuex' export default { // 省略其它代码 methods: { // 2. 使用 mapMutations 辅助函数,把 m_cart 模块提供的 updateAllGoodsState 方法映射到当前组件中使用 ...mapMutations('m_cart', ['updateAllGoodsState']), }, } -
为 UI 中的
label组件绑定click事件处理函数:<!-- 全选区域 --> <label class="radio" @click="changeAllState"> <radio color="#C00000" :checked="isFullCheck" /><text>全选</text> </label> -
在
my-settle组件的methods节点中,声明changeAllState事件处理函数:methods: { ...mapMutations('m_cart', ['updateAllGoodsState']), // label 的点击事件处理函数 changeAllState() { // 修改购物车中所有商品的选中状态 // !this.isFullCheck 表示:当前全选按钮的状态取反之后,就是最新的勾选状态 this.updateAllGoodsState(!this.isFullCheck) } }
9.3.6 动态渲染已勾选商品的总价格
-
在
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) } -
在
my-settle组件中,使用mapGetters辅助函数,把需要的checkedGoodsAmount映射到当前组件中使用:...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount']) -
在组件的 UI 结构中,渲染已勾选的商品的总价:
<!-- 合计区域 --> <view class="amount-box"> 合计:<text class="amount">¥{ {checkedGoodsAmount}}</text> </view>
9.3.7 动态计算购物车徽标的数值
-
问题说明:当修改购物车中商品的数量之后,
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 的值必须是字符串,不能是数字 }) }, }, }
9.3.8 渲染购物车为空时的页面结构
改造 cart.vue 页面的 UI 结构,使用 v-if 和 v-else 控制购物车区域和空白购物车区域的按需展示:
<template>
<view class="cart-container" v-if="cart.length !== 0">
<!-- 使用自定义的 address 组件 -->
<!-- 购物车商品列表的标题区域 -->
<!-- 商品列表区域 -->
<!-- 结算区域 -->
</view>
<!-- 空白购物车区域 -->
<view class="empty-cart" v-else>
<text class="tip-text">空空如也~</text>
</view>
</template>
美化空白购物车区域的样式:
.empty-cart {
display: flex;
justify-content: center;
align-items: center;
padding-top: 150px;
.empty-img {
width: 90px;
height: 90px;
}
.tip-text {
font-size: 12px;
color: gray;
margin-top: 15px;
}
}
9.4 分支的合并与提交
-
将
cart分支进行本地提交:git add . git commit -m "完成了购物车的开发" -
将本地的
cart分支推送到码云:git push -u origin cart -
将本地
cart分支中的代码合并到master分支:git checkout master git merge cart git push -
删除本地的
cart分支:git branch -d cart
10. 登录与支付

注意:因接口问题,支付功能无法实现,就不贴出相应接口的信息了,想了解的可以在文章顶部网盘里获取
10.0 创建 settle 分支
运行如下的命令,基于 master 分支在本地创建 settle 子分支,用来开发登录与支付相关的功能:
git checkout -b settle
10.1 点击结算按钮进行条件判断
说明:用户点击了结算按钮之后,需要先后判断是否勾选了要结算的商品、是否选择了收货地址、是否登录。
-
在
my-settle组件中,为结算按钮绑定点击事件处理函数:<!-- 结算按钮 --> <view class="btn-settle" @click="settlement">结算({ {checkedCount}})</view> -
在
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('请先登录!') } -
在
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 }, }, } -
在
store/user.js模块的state节点中,声明token字符串:export default { // 开启命名空间 namespaced: true, // state 数据 state: () => ({ // 收货地址 address: JSON.parse(uni.getStorageSync('address') || '{}'), // 登录成功之后的 token 字符串 token: '', }), // 省略其它代码 } -
在
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 }, }, }
10.2 登录
10.2.1 实现登录和用户信息组件的按需展示
-
在
components目录中新建登录组件:
-
在
components目录中新建用户信息组件(my-userinfo): -
在
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页面中,实现登录组件和用户信息组件的按需展示:<template> <view> <!-- 用户未登录时,显示登录组件 --> <my-login v-if="!token"></my-login> <!-- 用户登录后,显示用户信息组件 --> <my-userinfo v-else></my-userinfo> </view> </template>
10.2.2 实现登录组件的基本布局
-
为
my-login组件定义如下的 UI 结构:<template> <view class="login-container"> <!-- 提示登录的图标 --> <uni-icons type="contact-filled" size="100" color="#AFAFAF"></uni-icons> <!-- 登录按钮 --> <button type="primary" class="btn-login">一键登录</button> <!-- 登录提示 --> <view class="tips-text">登录后尽享更多权益</view> </view> </template> -
美化登录组件的样式:
.login-container { // 登录盒子的样式 height: 750rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background-color: #f8f8f8; position: relative; overflow: hidden; // 绘制登录盒子底部的半椭圆造型 &::after { content: ' '; display: block; position: absolute; width: 100%; height: 40px; left: 0; bottom: 0; background-color: white; border-radius: 100%; transform: translateY(50%); } // 登录按钮的样式 .btn-login { width: 90%; border-radius: 100px; margin: 15px 0; background-color: #c00000; } // 按钮下方提示消息的样式 .tips-text { font-size: 12px; color: gray; } }
10.2.3 点击登录按钮获取微信用户的基本信息
需求描述:需要获取微信用户的头像、昵称等基本信息。
-
为登录的
button按钮绑定getUserInfo方法,表示点击按钮时,希望获取用户的基本信息:<!-- 登录按钮 --> <!-- 可以从 @getuserinfo 事件处理函数的形参中,获取到用户的基本信息 --> <button type="primary" class="btn-login" @click="getUserInfo">一键登录</button> -
在
methods节点中声明getUserInfo事件处理函数如下:methods: { // 获取微信用户的基本信息 getUserInfo() { const [err, res] = await uni.getUserProfile({ desc: '用于会员登录' }).catch(err => err) // 判断是否获取用户信息成功 if (err?.errMsg === 'getUserProfile:fail auth deny') return uni.$showMsg('您取消了登录授权!') // 获取用户信息成功,res.userInfo就是用户的基本信息 console.log(res.userInfo) } }Promise.catch()错误捕获机制
- 这里使用[err, res]对返回的信息进行解构,注意err, res解构的顺序问题
- getUserProfile的返回信息会被赋值给res,catch捕捉的错误会被赋值给err
- [err, res]结构赋值里err,res的顺序不能变if (err?.errMsg === 'getUserProfile:fail auth deny')
等价于
if(err&&err.Msg&&err.errMsg === 'getUserProfile:fail auth deny')
10.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() { const [err, res] = await uni.getUserProfile({ desc: '用于会员登录' }).catch(err => err) // 判断是否获取用户信息成功 if (err?.errMsg === 'getUserProfile:fail auth deny') return uni.$showMsg('您取消了登录授权!') // 获取用户信息成功,res.userInfo就是用户的基本信息 //console.log(res.userInfo) // 3. 将用户的基本信息存储到 vuex 中 this.updateUserInfo(res.userInfo) }, }, }
10.2.5 登录获取 Token 字符串
需求说明:当获取到了微信用户的基本信息之后,还需要进一步调用登录相关的接口,从而换取登录成功之后的 Token 字符串。
- 在
getUserInfo方法中,预调用this.getToken()方法,同时把获取到的用户信息传递进去:
// 获取微信用户的基本信息
getUserInfo() {
const [err, res] = await uni.getUserProfile({
desc: '用于会员登录'
}).catch(err => err)
if (err?.errMsg === 'getUserProfile:fail auth deny') return uni.$showMsg('您取消了登录授权!')
this.updateUserInfo(res.userInfo)
// 获取登录成功后的 Token 字符串
this.getToken(res)
},
-
在
methods中定义getToken方法,调用登录相关的API,实现登录的功能:// 调用登录接口,换取永久的 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('登录成功')
10.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 } // 换取 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) } }因为获取token的接口无法使用了,为了做测试,可以将
this.updateToken(loginResult.message.token)写成this.updateToken('xunitokentokentokentoken')用一个假的虚拟token代替,并把换取token的请求和判断登录失败的两行代码注释掉
10.3 用户信息
10.3.1 实现用户头像昵称区域的基本布局
-
在
my-userinfo组件中,定义如下的 UI 结构:<template> <view class="my-userinfo-container"> <!-- 头像昵称区域 --> <view class="top-box"> <image src="" class="avatar"></image> <view class="nickname">xxx</view> </view> </view> </template> -
美化当前组件的样式:
.my-userinfo-container { height: 100%; // 为整个组件的结构添加浅灰色的背景 background-color: #f4f4f4; .top-box { height: 400rpx; background-color: #c00000; display: flex; flex-direction: column; align-items: center; justify-content: center; .avatar { display: block; width: 90px; height: 90px; border-radius: 45px; border: 2px solid white; box-shadow: 0 1px 5px black; } .nickname { color: white; font-weight: bold; font-size: 16px; margin-top: 10px; } } } -
在
my.vue页面中,为最外层包裹性质的view容器,添加class="my-container"的类名,并美化样式如下:page, .my-container { height: 100%; }
10.3.2 渲染用户的头像和昵称
-
在
my-userinfo组件中,通过mapState辅助函数,将需要的成员映射到当前组件中使用:// 按需导入 mapState 辅助函数 import { mapState } from 'vuex' export default { computed: { // 将 m_user 模块中的 userinfo 映射到当前页面中使用 ...mapState('m_user', ['userinfo']), }, data() { return { } }, } -
将用户的头像和昵称渲染到页面中:
<!-- 头像昵称区域 --> <view class="top-box"> <image :src="userinfo.avatarUrl" class="avatar"></image> <view class="nickname">{ {userinfo.nickName}}</view> </view>
10.3.3 渲染第一个面板区域
-
在
my-userinfo组件中,定义如下的UI结构:<!-- 面板的列表区域 --> <view class="panel-list"> <!-- 第一个面板 --> <view class="panel"> <!-- panel 的主体区域 --> <view class="panel-body"> <!-- panel 的 item 项 --> <view class="panel-item"> <text>8</text> <text>收藏的店铺</text> </view> <view class="panel-item"> <text>14</text> <text>收藏的商品</text> </view> <view class="panel-item"> <text>18</text> <text>关注的商品</text> </view> <view class="panel-item"> <text>84</text> <text>足迹</text> </view> </view> </view> <!-- 第二个面板 --> <!-- 第三个面板 --> </view> -
美化第一个面板的样式:
.panel-list { padding: 0 10px; position: relative; top: -10px; .panel { background-color: white; border-radius: 3px; margin-bottom: 8px; .panel-body { display: flex; justify-content: space-around; .panel-item { display: flex; flex-direction: column; align-items: center; justify-content: space-around; font-size: 13px; padding: 10px 0; } } } }
10.3.4 渲染第二个面板区域
-
定义第二个面板区域的 UI 结构:
<!-- 第二个面板 --> <view class="panel"> <!-- 面板的标题 --> <view class="panel-title">我的订单</view> <!-- 面板的主体 --> <view class="panel-body"> <!-- 面板主体中的 item 项 --> <view class="panel-item"> <image src="/static/my-icons/icon1.png" class="icon"></image> <text>待付款</text> </view> <view class="panel-item"> <image src="/static/my-icons/icon2.png" class="icon"></image> <text>待收货</text> </view> <view class="panel-item"> <image src="/static/my-icons/icon3.png" class="icon"></image> <text>退款/退货</text> </view> <view class="panel-item"> <image src="/static/my-icons/icon4.png" class="icon"></image> <text>全部订单</text> </view> </view> </view> -
对之前的
SCSS样式进行改造,从而美化第二个面板的样式:.panel-list { padding: 0 10px; position: relative; top: -10px; .panel { background-color: white; border-radius: 3px; margin-bottom: 8px; .panel-title { line-height: 45px; padding-left: 10px; font-size: 15px; border-bottom: 1px solid #f4f4f4; } .panel-body { display: flex; justify-content: space-around; .panel-item { display: flex; flex-direction: column; align-items: center; justify-content: space-around; font-size: 13px; padding: 10px 0; .icon { width: 35px; height: 35px; } } } } }
10.3.5 渲染第三个面板区域
-
定义第三个面板区域的 UI 结构:
<!-- 第三个面板 --> <view class="panel"> <view class="panel-list-item"> <text>收货地址</text> <uni-icons type="arrowright" size="15"></uni-icons> </view> <view class="panel-list-item"> <text>联系客服</text> <uni-icons type="arrowright" size="15"></uni-icons> </view> <view class="panel-list-item"> <text>退出登录</text> <uni-icons type="arrowright" size="15"></uni-icons> </view> </view> -
美化第三个面板区域的样式:
.panel-list-item { height: 45px; display: flex; justify-content: space-between; align-items: center; font-size: 15px; padding: 0 10px; }
10.3.6 实现退出登录的功能
-
为第三个面板区域中的
退出登录项绑定click点击事件处理函数:<view class="panel-list-item" @click="logout"> <text>退出登录</text> <uni-icons type="arrowright" size="15"></uni-icons> </view> -
在
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']), }, }
10.4 三秒后自动跳转
10.4.1 三秒后自动跳转到登录页面
需求描述:在购物车页面,当用户点击 “结算” 按钮时,如果用户没有登录,则 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) }
10.4.2 登录成功之后再返回之前的页面
核心实现思路:在自动跳转到登录页面成功之后,把返回页面的信息存储到 vuex 中,从而方便登录成功之后,根据返回页面的信息重新跳转回去。
返回页面的信息对象,主要包含 { openType, from } 两个属性,其中
openType表示以哪种方式导航回之前的页面;from表示之前页面的 url 地址;
-
在
store/user.js模块的state节点中,声明一个叫做redirectInfo的对象如下:// state 数据 state: () => ({ // 收货地址 address: JSON.parse(uni.getStorageSync('address') || '{}'), // 登录成功之后的 token 字符串 token: uni.getStorageSync('token') || '', // 用户的基本信息 userinfo: JSON.parse(uni.getStorageSync('userinfo') || '{}'), // 重定向的 object 对象 { openType, from } redirectInfo: null }), -
在
store/user.js模块的mutations节点中,声明一个叫做updateRedirectInfo的方法:mutations: { // 更新重定向的信息对象 updateRedirectInfo(state, info) { state.redirectInfo = info } } -
在
my-settle组件中,通过mapMutations辅助方法,把m_user模块中的updateRedirectInfo方法映射到当前页面中使用:methods: { // 把 m_user 模块中的 updateRedirectInfo 方法映射到当前页面中使用 ...mapMutations('m_user', ['updateRedirectInfo']), } -
改造
my-settle组件methods节点中的delayNavigate方法,当成功跳转到my 页面之后,将重定向的信息对象存储到vuex中:// 延迟导航到 my 页面 delayNavigate() { // 把 data 中的秒数重置成 3 秒 this.seconds = 3 this.showTips(this.seconds) this.timer = setInterval(() => { this.seconds-- if (this.seconds <= 0) { // 清除定时器 clearInterval(this.timer) // 跳转到 my 页面 uni.switchTab({ url: '/pages/my/my', // 页面跳转成功之后的回调函数 success: () => { // 调用 vuex 的 updateRedirectInfo 方法,把跳转信息存储到 Store 中 this.updateRedirectInfo({ // 跳转的方式 openType: 'switchTab', // 从哪个页面跳转过去的 from: '/pages/cart/cart' }) } }) return } this.showTips(this.seconds) }, 1000) } -
在
my-login组件中,通过mapState和mapMutations辅助方法,将vuex中需要的数据和方法,映射到当前页面中使用:// 按需导入辅助函数 import { mapMutations, mapState } from 'vuex' export default { computed: { // 调用 mapState 辅助方法,把 m_user 模块中的数据映射到当前用组件中使用 ...mapState('m_user', ['redirectInfo']), }, methods: { // 调用 mapMutations 辅助方法,把 m_user 模块中的方法映射到当前组件中使用 ...mapMutations('m_user', ['updateUserInfo', 'updateToken', 'updateRedirectInfo']), }, } -
改造
my-login组件中的getToken方法,当登录成功之后,预调用this.navigateBack()方法返回登录之前的页面:// 调用登录接口,换取永久的 token async getToken(info) { // 省略其它代码... // 判断 vuex 中的 redirectInfo 是否为 null // 如果不为 null,则登录成功之后,需要重新导航到对应的页面 this.navigateBack() } -
在
my-login组件中,声明navigateBack方法如下:// 返回登录之前的页面 navigateBack() { // redirectInfo 不为 null,并且导航方式为 switchTab if (this.redirectInfo && this.redirectInfo.openType === 'switchTab') { // 调用小程序提供的 uni.switchTab() API 进行页面的导航 uni.switchTab({ // 要导航到的页面地址 url: this.redirectInfo.from, // 导航成功之后,把 vuex 中的 redirectInfo 对象重置为 null complete: () => { this.updateRedirectInfo(null) } }) } }
10.5 微信支付
10.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, } } }
10.5.2 微信支付的流程
-
创建订单
– 请求创建订单的 API 接口:把(订单金额、收货地址、订单中包含的商品信息)发送到服务器
– 服务器响应的结果:订单编号
-
订单预支付
– 请求订单预支付的 API 接口:把(订单编号)发送到服务器
– 服务器响应的结果:订单预支付的参数对象,里面包含了订单支付相关的必要参数
-
发起微信支付
– 调用
uni.requestPayment()这个 API,发起微信支付;把步骤 2 得到的 “订单预支付对象” 作为参数传递给uni.requestPayment()方法– 监听
uni.requestPayment()这个 API 的success,fail,complete回调函数
10.5.3 创建订单
-
改造
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() }, -
在
my-settle组件中定义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. 订单预支付 // 3. 发起微信支付 }
10.5.4 订单预支付
-
改造
my-settle组件的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. 发起微信支付 }
10.5.5 发起微信支付
-
改造
my-settle组件的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' }) }
10.6 分支的合并与提交
-
将
settle分支进行本地提交:git add . git commit -m "完成了登录和支付功能的开发" -
将本地的
settle分支推送到码云:git push -u origin settle -
将本地
settle分支中的代码合并到master分支:git checkout master git merge settle git push -
删除本地的
settle分支:git branch -d settle
11. 发布
11.1 为什么要发布
-
小程序只有发布之后,才能被用户搜索并使用
-
开发期间的小程序为了便于调试,含有
sourcemap相关的文件,并且代码没有被压缩,因此体积较大,不适合直接当作线上版本进行发布 -
通过执行 “小程序发布”,能够优化小程序的体积,提高小程序的运行性能
11.2 发布小程序的流程
-
点击
HBuilderX菜单栏上的发行->小程序-微信(仅适用于uni-app): -
在弹出框中填写要发布的
小程序的名称和AppId之后,点击发行按钮: -
在
HBuilderX的控制台中查看小程序发布编译的进度 -
发布编译完成之后,会自动打开一个新的微信开发者工具界面,此时,点击工具栏上的上传按钮

-
填写版本号和项目备注之后,点击上传按钮
-
上传完成之后,会出现如下的提示消息,直接点击确定按钮即可:

-
通过微信开发者工具上传的代码,默认处于版本管理的开发版本列表中,如图所示:

上述网站在微信公共平台
-
将
开发版本提交审核-> 再将审核通过的版本发布上线,即可实现小程序的发布和上线
11.3 发布为 Android App 的流程
-
点击
HBuilderX状态栏左侧的未登录按钮,弹出登录的对话框:

-
在弹出的登录对话框中,填写账号和密码之后,点击登录即可:
没登陆的点击注册,登录后显示没验证的按提示进行验证即可

-
打开项目根目录中的
manifest.json配置文件,在基础配置面板中,获取uni-app 应用标识,并填写应用名称:
-
切换到 App 图标配置面板,点击浏览按钮,选择合适的图片之后,再点击自动生成所有图标并替换即可:

-
点击菜单栏上的 发行 -> 原生 App-云打包:

-
勾选打包配置如下:

-
在控制台中查看打包的进度信息
-
点击链接下载 apk 的安装包,并安装到 Android 手机中查看打包的效果。
注意:由于开发期间没有进行多端适配,所以有些功能在 App 中无法正常运行,例如:选择收货地址、微信登录、微信支付
版权声明
本文为[海底烧烤店ai]所创,转载请带上原文链接,感谢
https://blog.csdn.net/m0_51969330/article/details/124319434
边栏推荐
- Generation of barcode and QR code
- JS, entries(), keys(), values(), some(), object Assign() traversal array usage
- Variable length parameter__ VA_ ARGS__ Macro definitions for and logging
- Using quartz under. Net core -- a simple trigger of [7] operation and trigger
- Promise (II)
- Handwritten event publish subscribe framework
- Compare the performance of query based on the number of paging data that meet the query conditions
- Low code development platform sorting
- Shell-sort命令的使用
- Entity Framework core captures database changes
猜你喜欢

Detailed explanation of C webpai route
![[WPF binding 3] listview basic binding and data template binding](/img/2e/fbdb4175297bb4964a8ccfd0b909ae.png)
[WPF binding 3] listview basic binding and data template binding

基于51单片机红外无线通讯仿真

01-初识sketch-sketch优势

快时钟同步慢时钟域下的异步控制信号slow clk to fast clk

Document operation II (5000 word summary)

Clickhouse table engine

ASP. NET CORE3. 1. Solution to login failure after identity registers users

Further study of data visualization

stm32入门开发板选野火还是正点原子呢?
随机推荐
stm32入门开发板选野火还是正点原子呢?
Baidu Map Case - Zoom component, map scale component
1-1 NodeJS
Baidu Map 3D rotation and tilt angle adjustment
ClickHouse-SQL 操作
El cascade and El select click elsewhere to make the drop-down box disappear
Clickhouse SQL operation
Redis docker installation
Further optimize Baidu map data visualization
Lock lock
Model problems of stock in and stock out and inventory system
[difference between Oracle and MySQL]
Metaprogramming, proxy and reflection
1-5 nodejs commonjs specification
ASP. NET CORE3. 1. Solution to login failure after identity registers users
Basic case of Baidu map
Go language RPC communication
Node template engine (EJS, art template)
Detailed explanation of Milvus 2.0 quality assurance system
If you start from zero according to the frame