一、项目基本结构
(一)tabbar页面
首页 分类
购物车
我的(用户中心)
(二)非tabbar页面
搜索
商品列表
商品详情
微信支付
结构解析:
1. 构建一个小程序,需要先将小程序的页面结构分类、理清。
现大多数小程序页面结构分为tabbar页面和非tabbar页面。tabbar页面作为小程序的主体框架,所以应该先搭建好tabbar页面,非tabbar页面在此框架上运行。
2. 由于存在tabbar页面和非tabbar页面,因此将小程序项目分为主包和分包
- 主包:小程序启动页或tabbar页面以及公共资源
- 分包:非tabbar页面和私有资源
普通分包页面:先下载主包,再跳转至普通分包运行普通分包的页面
独立分包页面:不依赖于主包,可自己独立运行(配置时加上:"independent" : true)
配置pages.json
// 主包----pages:tabbar页面放入"pages": [{"path" : "pages/home/home","style" : {}},{"path" : "pages/cate/cate","style" : {}},{"path" : "pages/cart/cart","style" : {}},{"path" : "pages/my/my","style" : {}}],
--------------------------------------------------
// subPackages(数组):每个分包为一个对象,每个分包的pages(数组)可包含多个非tabbar页面(对象)
// {root: 该分包在项目中的目录name: 分包别名pages: [{path: 非tabbar页面在此分包下的路径style: 非tabbar页面的配置}]independent: true // 与root平级,加上independent:true,则此分包为独立分包}
--------------------------------------------------"subPackages":[{"root":"subpkg","name":"p1","pages":[{"path" : "goods_detail/goods_detail","style" : {}},{"path" : "goods_list/goods_list","style" : {"onReachBottomDistance":150,"enablePullDownRefresh":true,"backgroundColor":"#F8F8F8"}},{"path" : "search/search","style" : {}}]}],
分包预下载:在进入某页面时,可预先下载可会用到的某分包,提高性能
配置pages.json
"preloadRule": { // preloadRule 关键字与pages平级"pages/index": { // pages/index 触发预下载的路径"network": "all", // network 表示在指定网络模式下进行预下载 可选all/wifi"packages": ["package1"] // packages 预下载哪个分包 ["分包root或name"]},"sub1/index": {"packages": ["package2", "package3"]},}
即:当页面为 /pages/index 时,预下载 package1分包;当页面为 /sub1/index 时,预下载 package2和package3分包。
限制:同一个分包中的页面享有共同的预下载大小限额 2M,限额会在工具中打包时校验。
如,页面 A 和 B 都在同一个分包中,A 中预下载总大小 0.5M 的分包,B中最多只能预下载总大小 1.5M 的分包。
3. tabbar徽标的配置
以此项目为例:cart页面需要右上角徽标,且切换至其他tabbar页面时,cart页面的徽标也要显示(当数值为零时移除徽标)
当多个页面都有相同业务逻辑时,封装一个mixins(js),然后引入至各个页面使用
## uni.setTabBarItem( ) 和 uni.setTabBarItem( )的使用
## minxins/tabbar-badge.jsimport {mapGetters} from 'vuex'export default {computed:{...mapGetters('cart',['totalCount']) // 用作设置徽标的数值},watch:{// 当totalCount改变时,再次设置徽标totalCount:{immediate:true,handler(){this.setTabbarBadge()}}},onLoad(){this.setTabbarBadge()},// 再次进入onShow() {this.setTabbarBadge()},methods:{setTabbarBadge(){// 当购物车中商品数量为0时,去除徽标if(this.totalCount==0){uni.removeTabBarBadge({index:2})return}uni.setTabBarBadge({index:2,text:this.totalCount.toString() // 字符串类型!!!!})}}
}
-----------------------------------------
## home.vue cate.vue cart.vue my.vueimport badgeMix from '@/mixins/tabbar-badge.js' // 引入
export default {minxins:[badgeMix] // 使用}
其他注意事项
- 配置manifest.json
"mp-weixin" : {/* 小程序特有相关 */"setting" : {/*其他代码*/"checkSiteMap":false},
},
- 若项目是他人的(接口地址涉及权限的),要他人在开发管理人员添加自己,才能有权限请求接口
- 配置store以及挂载至vue的原型对象上
二、起步
工具:1.HBuilderX:2.7.14.20200618(安装 scss/sass 编译) 2.微信开发者工具:1.05.2105170
新建项目,在项目根目录中新建 .gitignore 忽略文件,并配置如下:
# 忽略 node_modules 目录
/node_modules
/unpackage/dist
在 unpackage 目录下创建一个叫做 .gitkeep 的文件进行占位
三、框架搭建(即搭建tabbar页面)
配置pages.json--tabBar(注意路径前均没有 "/")
四、配置网络请求
在main.js中引入和配置,具体参考:
@escook/request-miniprogram - npm
项目根目录打开终端 ==> npm init -y ==> npm i @escook/request-miniprogram ==> main.js引入
==> 挂载至uni.$http ==> 设置请求根路径(hostip单独设置,从utils/hostip.js引入)
==> 请求拦截器(showLoading)、响应拦截器(hideLoading)、封装弹窗函数
五、使用Git管理项目
1.本地管理、把项目托管至远程仓库
git init ==> git add . ==> git commit -m "初始化项目" ==> git remote add origin SSH
==> git branch -M main ==> git push -u origin main
2.每开始一个页面(分支)的搭建,先创建分支
git checkout -b tabbar(分支名称)
3.完成了该分支
1)本地分支提交:git add . ==> git commit -m "完成了 tabbar 的开发"
2)将本地分支推送至远程仓库:git push -u origin tabbar
3)将本地分支合并至main分支上:git checkout main ==> git merge tabbar
4)删除本地tabbar分支:git branch -d tabbar
六、页面、组件的搭建,以及数据的获取
(一)home.vue搭建
(二)cate.vue搭建
1.使用<scroll-view>:
1)指定滑动方向:scroll-y
2)给定一个宽度或高度:onLoad()时,获取可使用窗口高度
<scroll-view :style="{height:height+'px'}">...</scroll=view>data() {return {height:0}
},
onLoad() {// 获取可使用高度const info=uni.getSystemInfoSync()this.height=info.windowHeight-50 // 动态给高度赋值this.getCatelist()},
首次渲染时,默认一级分类(选中)==>一级分类children(二级分类)
onLoad==>调用获取数据的函数(){// 二级分类数据首次渲染第一个一级分类的childrenthis.cateLevel2 = res.message[0].children
}// 当点击一级分类切换时==>调用点击切换函数(index){this.cateLevel2 = res.message[index].children
}
解决切换一级分类后重置滚动条的位置
<scroll-view :scroll-top="scrollTop"></scroll-view>data() {return {// 滚动条距离顶部的距离scrollTop: 0}
}// 点击切换一级分类的函数(){this.scrollTop = this.scrollTop === 0 ? 0.01 : 0 // scrollTop前后数据要不一致
}
(三)搜索
封装自定义组件my-search、新建分包页面search
1.cate页面中,<scroll-view>高度的调整
my-search组件使用在cate.vue中,由于my-search的高度为50px
因此,scroll-view的高度 = 可使用屏幕高度 - my-search的高度(50)
2.my-search组件实现吸顶效果
<view class="search-box"><my-search @click="gotoSearch"></my-search>
</view>.search-box {// 设置定位效果为“吸顶”position: sticky; // 不用固定定位,固定定位不占位置,脱离文档流// 吸顶的“位置”top: 0;// 提高层级,防止被轮播图覆盖z-index: 999;
}
3.使用<uni-search-bar></uni-search-bar>内置组件
对内置的<uni-search-bar>组件进行调整:
1)修改背景色:使用深度选择器
// 深度选择器:
// 1)原生css: >>> 子组件中的类名
// 2)less: /deep/ 子组件中的类名
// 3)sass: :v-deep 子组件中的类名
// 4)其他格式: ::v-deep 子组件中的类名
// 注意:要加上 !important::v-deep .uni-searchbar {background-color: #c00000 !important;
}
2)实现搜索框自动获取焦点
<uni-search-bar>组件中,实现自动获取焦点的属性show和showSync在data中默认为false,可设置props属性,由父组件传入值,实现搜索框自动获取焦点
## uni-search-bar源代码修改
props: {.....// 修改showInit:{type:Boolean,default:false},showSyncInit:{type:Boolean,default:false}},data() {return {show: this.showInit,showSync: this.showSyncInit,searchVal: ""}},// 组件使用时,在父组件传入值
<uni-search-bar :showInit="true" :showSyncInit="true"></uni-search-bar>// 当传入值非字符型类型时,前面均需要用:绑定
3)搜索框防抖效果
input(e) {// 清除 timer 对应的延时器clearTimeout(this.timer)// 重新启动一个延时器,并把 timerId 赋值给 this.timerthis.timer = setTimeout(() => {// 如果 500 毫秒内,没有触发新的输入事件,则为搜索关键词赋值this.kw = e.valueconsole.log(this.kw)}, 500)
}
4.搜索建议与搜索历史的渲染以及切换
搜索历史的渲染:1)使用内置组件<uni-tag></uni-tag>
2)注意最新历史在第一位(unshift),以及去重问题(splice(index,1))
3)点击图标,清空历史记录(vuex,本地存储的都清空)
两个页面的切换:根据搜索建议列表的长度(length)
数据存储的构思:
- 构思:哪些数据是不随刷新(重新进入小程序)而改变的(存在于本地)?哪些数据是要跨页面、跨组件使用的(存于vuex中)?
- 基本上存储于vuex中的数据,大多数都有update,saveToStorage。。。的需求
search页面的搜索历史记录(history)、cart页面的商品(即已加入购物车的商品cart)、cart页面的地址(address)、my页面的用户信息(userInfo)、鉴定用户是否已登录的token、重定向信息(redirectInfo)等等。。。
将以上符合条件的数据存储于vuex中,并且存储于本地,同时使vuex中的数据和本地保持同步!
流程:请求回来的数据==>dispatch/commit(handler,数据)==>在commit中,state.xxx=数据
==>调用本地存储handler()==>在本地存储handler(state)中,拿state.xxx存储于本地
==>state.xxx=从本地取回数据==>getters:对state.xxx进行进一步改造调整
## 当组件或页面需要数据时,从vuex的state中拿
## 不要将getters里的数据存储于本地,因为其来源于state,要将源头存储于本地(保持数据更新同步),而不是改造后的数据
(四)商品列表
1.新建分包页面my-goods_list
1)页面跳转所携带过来的参数,在onLoad(options)函数的options中
onLoad(options) {// 将页面参数转存到 this.queryObj 对象中this.queryObj.query = options.query || ''this.queryObj.cid = options.cid || ''
}
2)请求数据前,先整理请求参数(具体参考请求文档)
data() {return {// 请求参数对象queryObj: {// 查询关键词query: '',// 商品分类Idcid: '',// 页码值pagenum: 1,// 每页显示多少条数据pagesize: 10}}
}
2.在分包页面my-goods_list中,将商品 item 项封装为自定义组件my-goods
因为:商品列表中的每个商品的结构与购物车中要展示的商品的结构类似,可以复用
使用:<my-goods :goods="goods"></my-goods> 传入的是每个商品项
3.使用过滤器处理价格
// 在 my-goods 组件中,和 data 节点平级,声明 filters 过滤器节点如下:filters: {// 把数字处理为带两位小数点的数字tofixed(num) {return Number(num).toFixed(2)}
}// 在渲染商品价格的时候,通过管道符 | 调用过滤器:<view class="goods-price">¥{{goods.goods_price | tofixed}}</view>
4.上拉加载更多、下拉刷新
1)上拉加载更多(可配置文件调整onReachBottomDistance,默认50)
- 节流阀
- 判断数据是否加载完毕(当前页码值 * 每页展示数据条数 >= 总数据条数)
// 触底的事件
onReachBottom() {// 让页码值自增 +1this.queryObj.pagenum += 1// 重新获取列表数据this.getGoodsList()
}// 改造 methods 中的 getGoodsList 函数,当列表数据请求成功之后,进行新旧数据的拼接处理:
this.goodsList = [...this.goodsList, ...res.message.goods]
2)下拉刷新(配置文件开启下拉刷新enablePullDownRefresh)
- 重点:重置数据
// 下拉刷新的事件
onPullDownRefresh() {// 1. 重置关键数据this.queryObj.pagenum = 1this.total = 0this.isloading = falsethis.goodsList = []// 2. 重新发起请求this.getGoodsList(() => uni.stopPullDownRefresh()) // 完成后,关闭下拉
}
(五)商品详情
新建分包页面goods_detail
1.轮播图预览效果
preview(i) {// 调用 uni.previewImage() 方法预览图片uni.previewImage({// 预览时,默认显示图片的索引current: i,// 所有图片 url 地址的数组urls: this.goods_info.pics.map(x => x.pics_big)})
}
2.渲染商品详情信息
1)rich-text
<rich-text :nodes="goods_info.goods_introduce"></rich-text>
2)解决图片空白间隙问题
// 因为行内块元素(这里的图片)默认是与文字基线对齐的
// 解决:1.vertical-align: top;vertical-align: middle;2.display: block;// 使用字符串的 replace() 方法,为 img 标签添加行内的 style 样式,从而解决图片底部空白间隙的问题
res.message.goods_introduce = res.message.goods_introduce.replace(/<img /g, '<img style="display:block;" ')
3)解决.web格式图片在ios设备上无法正常显示问题
// 使用字符串的 replace() 方法,将 webp 的后缀名替换为 jpg 的后缀名
res.message.goods_introduce = res.message.goods_introduce.replace(/<img /g, '<img style="display:block;" ').replace(/webp/g, 'jpg')
4)解决商品价格闪烁问题
导致问题的原因:在商品详情数据请求回来之前,data 中 goods_info 的值为 {},因此初次渲染页面时,会导致 商品价格、商品名称 等闪烁的问题。
解决方案:判断 goods_info.goods_name 属性的值是否存在,从而使用 v-if 指令控制页面的显示与隐藏
3.商品详情底部导航
使用内置组件:uni-gods-nav
(六)加入购物车
1.在vuex中存储购物车的相关数据,cart.js
state: () => ({// 购物车的数组,用来存储购物车中每个商品的信息对象// 每个商品的信息对象,都包含如下 6 个属性:// { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }cart: [],}),
- 将商品加入购物车的实现思路关键:遍历cart数组,在其中先find( )即将加入购物车的商品,若存在,则该商品数量++;若不存在,则push( )入cart数组
- goods_id:商品唯一id
- goods_state:加入购物车的商品,默认为true(勾选状态)
2.在 getters 节点下定义一个 total ,用来统计购物车中商品的种类数量,同时通过 wtach 侦听器,监听 total 的变化,动态为 cart 徽标赋值
(七)购物车页面
1.<radio></radio>
1)为my-goods组件封装radio勾选状态,使用内置组件<radio></radio>,父组件传入值,v-if/v-else来调整显示与隐藏,同时 : checked="goods.goods_state" 绑定商品的勾选状态
2)为<radio></radio>绑定自定义事件@onClick,emit出去(goods_state),在父组件中处理业务逻辑
## 封装的自定义组件,一般不自己处理业务逻辑,emit出去,让父组件处理,封装的组件一般是公共组件
2.<uni-number-box></uni-number-box>
1)动态渲染商品数量的值
<!-- 商品数量 -->
<uni-number-box :min="1" :value="goods.goods_count"></uni-number-box>
2)props 属性,来控制当前组件中是否显示 NumberBox 组件
3)@change="numChange(value)",emit(goods_id,value)出去
- 解决numberbox数据不合法问题
3.uni-swiper-action/uni-swiper-item
实现滑动删除商品效果:
1)配置 options
2)滑动事件:uni-swiper-item 绑定@click时间
3)删除商品:filter( ),筛选出不是要删除的商品
4.封装获取地址和展示地址的自定义组件my-address
1)点击按钮,获取地址,使用uni.chooseAddress( )
2)获取地址按钮、展示地址的页面的切换
5.封装结算区组件my-settle
1)全选勾选框
<label class="radio">// 为 radio 组件动态绑定 checked 属性的值<radio color="#C00000" :checked="isFullCheck" /><text>全选</text></label>
全选框的勾选状态:some( )、every( )。。。
2)全选影响每个复选框;每个复选框影响全选
3)购物车为空、购物车不为空的两个页面结构的切换
(八)登录
封装自定义组件my-login和my-userInfo
1.点击结算按钮进行条件判断
1)是否勾选了要结算的商品 2)是否填写了地址 3)是否已经登录
情况一:1、2不满足,3满足。则提醒用户勾选商品和填写地址
情况二:1、2满足,3不满足。则提醒用户登录,并在3秒后自动跳转至登录页
情况三:1、2、3均满足。则整理参数,发起微信支付
// 去结算
goCheck(){// 是否勾选了商品if(!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')// 是否选择了地址if(!this.addr) return uni.$showMsg('请选择收货地址!')// 是否登录了(token)// if(!this.token) return uni.$showMsg('请先登录!')// 重定向if(!this.token) return this.delayNavigate()// 以上条件均符合,则进行支付this.payOrder()
},
2.my-login与my-userInfo的切换:是否有token
3.实现登录
按钮@click事件==>【getUserInfo】获取userInfo和请求参数 uni.getUserProfile( {desc:"login"} )
==>【getToken】获取code uni.login( ) ==>请求参数+code(整理参数queryObj)
==> 发请求获取token uni.$http.post(url,queryObj) ==> 得到token ==> 将token存储于vuex中
my-login与my-userInfo依据token的有无进行切换
4.重定向 & 微信支付
1)重定向
data() {return {second:3,timer:null};
},methods:
// 倒计时提示
showTip(second){uni.showToast({title:`请登录后再结算! ${second} 秒后自动跳转到登录页`,icon:'none',mask:true})
},
// 延时跳转到my页面进行登录
delayNavigate(){// 每次开始时,重置secondthis.second=3this.showTip(this.second)// 定时器this.timer=setInterval(()=>{this.second--if(this.second<=0){// 倒计时为零时,1)清除定时器 2)跳转至my页面 3)return后续代码不再进行clearInterval(this.timer)uni.switchTab({url:'/pages/my/my',success: () => {this.$store.commit('userInfo/updateRedirectInfo',{openType:'switchTab',from:'/pages/cart/cart'})}})// 终止后续代码的运行(当秒数为 0 时,不再展示 toast 提示消息)return}this.showTip(this.second)},1000)
}
2)实现微信支付
发请求:创建订单 ==> 得到订单编号 ==> 发请求:订单预支付==> 得到预支付对象(payInfo)
==>发起微信支付 uni.requestPayment( payInfo )(具体参考请求文档)
==>完成(提示用户)
async payOrder(){// 具体关键字段,参考请求文档// 1.创建订单(订单金额、收货地址、订单中包含的商品信息)--返回 订单编号// 1.1整理请求参数let orderInfo={// order_price:0.001, // 开发时,可用此总价格order_price:this.checkedGoodsAmount, // 订单总价格consignee_addr:this.addr,goods:this.cart.filter(item=>item.goods_state).map(item=>({goods_id:item.goods_id,goods_number:item.goods_count,goods_price:item.goods_price}))}// 1.2发请求let {data:res1}=await uni.$http.post('/api/public/v1/my/orders/create',orderInfo)if(res1.meta.status!=200) return uni.$showMsg('创建订单失败!')// 1.3得到订单编号let order_number=res1.message.order_number// 2.订单预支付(订单编号)--返回 订单预支付的参数对象// 2.1发请求let {data:res2}=await uni.$http.post('/api/public/v1/my/orders/req_unifiedorder',{order_number})if(res2.meta.status!=200) return uni.$showMsg('预付订单生成失败!')// 2.2得到订单预支付对象let payInfo=res2.message.pay// 3.发起微信支付(订单预支付对象)--监听 success fail complete回调函数// 3.1 调用 uni.requestPayment() 发起微信支付let [err,succ]=await uni.requestPayment(payInfo) // 将payInfo传入// 3.2未完成支付if(err) return uni.$showMsg('订单未支付!')// 3.3完成了支付,进一步查询支付结果--查询订单支付状态let {data:res3}=await uni.$http.post('/api/public/v1/my/orders/chkOrder',{order_number})// 3.4检测到订单未支付if(res3.meta.status!=200) return uni.$showMsg('订单未支付!')// 3.5检测到订单支付完成uni.showToast({title:'支付完成!',icon:'success'})
},