2021版小程序开发5------小程序项目开发实践(2)
学习笔记 2025
使用uni-app开发一个电商项目继续;
过滤器的使用
js
filters: {
toFixed(num){
return Number(num).toFixed(2)
}
}
xml
<!-- 通过管道符 | 使用过滤器 -->
<view> {{ item.price | toFixed }}</view>
上拉加载更多 和 下拉刷新
上拉加载更多:
- 在pages.json配置文件中,为页面(如subPackages分包中的goods_list页面)配置上拉触底的距离;
- 在页面中,和methods节点平级,声明
onReachBottom
事件处理函数,以监听页面上拉触底事件; - 通过节流阀 避免一次性发送多个请求
下拉刷新:
- 在pages.json中,类似配置上拉加载更多,配置
enablePullDownRefresh
和backgroundColor
,为当前页面单独开启下拉刷新效果; - 在页面中,和methods节点平级,声明
onPullDownRefresh
事件处理函数,以监听页面下拉刷新事件; - 通过回调函数主动关闭下拉刷新;
json
"subPackages": [
{
"root": "subpkg",
"pages": [
{
"pach": "goods_detail/goods_detail",
"style": {}
},
{
"path": "goods_list/goods/list",
"style": {
"onReachBottomDistance": 150, // px
"enablePullDownRefresh": true,
"backgroundColor": "#F8F8F8"
}
}
]
}
]
js
onReachBottom() {
if (this.isloading) return
this.queryObj.pagenum += 1
this.getGoodsList()
}
onPullDownRefresh(){
// 重置必要数据
// 重新发送请求,并传入一个回调函数,主动停止下拉刷新
this.getGoodsList(()=> uni.stopPullDownRefresh())
}
...
getGoodsList(callBack){
this.isloading = true // 通过节流阀 避免一次性发送多个请求
// ...request
// 将接口响应数据 拼接到之前数据后
this.goodsList = [...this.goodsList, ...res.message.goods]
//
this.isloading = false
callBack && callBack()
}
block元素包裹
模拟一个块元素,通常用作v-for包裹;
大图预览
js
preview(i){
uni.previewImage({
current: i, // 图片索引
urls: this.goods_pics.map(x=>x.pic_url)
})
}
html文本渲染
可以使用rich-text
富文本组件的nodes属性,可以在小程序环境中渲染出html字符串;
为了避免图文中图片下边白条,可以给所有img添加行内样式:display: block
js
this.goods_intro = this.goods_intro.replace(/<img /g, '<img style="display: block;"')
xml
<rich-text :nodes="goods_intro"></rich-text>
商品详情底部的商品导航区域
uni-ui提供了GoodsNav
组件,实现该效果;
- 在data中,通过
options
和buttonGroup
两个数组,来声明商品导航组件的按钮配置对象; - 使用
uni-goods-nav
组件; - 使组件固定在页面最底部
xml
<view class="goods-nav">
<uni-goods-nav :fill="true" :options="options" :buttonGroup="buttonGroup" @click="onClick" @buttonClick="buttonClick"></uni-goods-nav>
</view>
js
options: [{
icon: 'shop',
text: '店铺'
},{
icon: 'cart',
text: '购物车',
info: 2
}],
buttonGroup: [
{
text: "加入购物车",
backgroundColor: '#FF0000',
color: "#FFF"
},
{
text: "立即购买",
backgroundColor: '#FFA200',
color: "#FFF"
}
]
css
.goods-nav {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
}
uni-app配置Vuex实现全局数据共享
- 根目录创建store文件夹,专门存放vuex相关模块;
- 新建store.js,初始化Store实例对象:
- 导入Vue和Vuex
- 将Vuex安装为Vue插件
- 创建Store实例对象
- 向外共享Store实例对象
- 在main.js中导出store实例对象,并挂载到Vue的实例上;
js
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import moduleCart from "./cart.js"
Vue.use(Vuex)
const store = new Vuex.Store({
// 挂载store模块
modules:{
// 调整访问路径 为 m_cart/carts,即m_cart模块的carts数组
m_cart: moduleCart,
}
})
export default store
js
// main.js
import store from './store/store.js'
const app = new Vue({
...App,
store,//
})
app.$mount()
创建必要的store模块
继续创建 cart.js
js
// cart.js
export default {
// 为当前模块开启命名空间
namespaced: true,
// 模块的state数据
state: () => ({
// 购物车 商品对象数组
carts: [],
})
// mutations 方法:专门修改数据
mutations: {
addToCart(state, goods){
const findRes = start.carts.find((x)=>{
x.id === goods.id
})
if (!findRes){
state.carts.push(goods)
}else{
findRes.count++
}
}
},
// getters属性:相当于数据的包装器
getters: {
cart_num(state){
let c = 0
state.carts.forEach(goods => c += goods.count)
return c
}
},
}
在页面使用Store中的数据
js
import { mapState, mapMutations, mapGetters } from "vuex"
computed: {
// 将m_cart模块的carts数组 映射到当前页面
...mapState("m_cart", ['carts']),
...mapGetters("m_cart", ['cart_num']),
},
watch: {
// cart_num(newVal){
// const findRes = this.options.find((x)=> x.text === '购物车')
// if(findRes) {
// // 监听数据变化 修改购物车按钮徽标
// findRes.info = newVal
// }
// },
// 优化后的监听器 支持在页面初次加载的时候立即执行
cart_num: {
handler(newVal){
// ...
},
immediate: true
}
}
methods: {
...mapMutations("m_cart", ['addToCart'])
//
todo(){
this.addToCart(goods_item)
}
}
持久化存储购物车的数据
cart.js的mutations声明saveToStorage
方法:
js
saveToStorage(state){
uni.setStorageSync("carts", JSON.stringfy(state.carts))
}
页面上的调用:
js
// 任何修改carts的地方
this.commit("m_cart/saveToStorage")
cart.js的state中 carts从Storage中初始化:
js
state: () => ({
// 购物车 商品对象数组
carts: JSON.parse(uni.getStorageSync('carts') || '[]'),
})
动态为tabBar上的购物车添加数字徽标
- 把Store中的 cart_num 映射到 cart.vue中使用
- 在
onShow(){}
中自定义方法this.setBadge()
- 添加监听vuex中商品总数值,显式的调用
this.setBadge()
js
setBadge(){
uni.setTabBarBadge({
index: 2,
text: this.cart_num + '' // 转成字符串进行显示
})
}
进一步的还需要在其他tabBar页面也这样添加(这样在tabBar的任何一个页面,都可以触发购物车数字徽标的显示),因此可以将这些代码封装为一个mixin;
- 在根目录新建mixins文件夹,再新建
tabbar-badge.js
文件; - 修改需要混入的页面代码;
js
// tabbar-badge.js
import {mapGetters} from "vuex"
export default {
computed: {
...mapGetters('m_cart', ["cart_num"])
},
watch: {
cart_num() {
this.setBadge()
}
},
onShow(){
this.setBadge()
},
methods:{
setBadge(){
uni.setTabBarBadge({
index: 2,
text: this.cart_num + '' // 转成字符串进行显示
})
}
}
}
js
// 页面使用
import badgeMix from "@/minins/tabbar-badge.js"
export default {
mixins: [badgeMix]
}
购物车页面
从Vuex中取出 carts数据列表,进行渲染;
js
import { mapState } from "vuex"
computed: {
...mapState("m_cart", ['carts']),
},
商品选中单选框
xml
<radio cheched color="#C00000"></radio>
组件内单选状态的改变,可以通过组件的自定义回调函数传递到使用组件的页面;
在store/cart.js中定义一个mutations方法,实现以下逻辑:
- 在购物车商品列表中找到当前商品;
- 修改其商品状态(引用对象,已生效);
- 持久化存储到本地(
this.$commit("m_cart/saveToStorage")
);
在页面相应的回调函数中,调用该mutations方法即可;
flex-item设置flex:1
表示占满剩余区域;
解决数字输入框NumberBox数据输入不合法导致的问题
修改uni-number-box.vue组件的代码,在_onBlur回调中:
js
_onBlur(event){
// let value = event.detail.value
let value = parseInt(event.detail.value) // 转int
if (!value){
this.inputValue = 1 // 如果解析出NaN 则强制赋为1
return
}
}
继续修改watch节点的inputValue监听器:
js
inputValue(newVal, oldVal){
// 值发生变化(新旧值不等) 且 为数值 且 不为小数点 才向外传递
if (+newVal !== +oldVal && Number(newVal) && String(newVal).indexOf(".") === -1){
this.$emit("change", newVal)
}
}
数组删除元素
使用数组的filter方法,返回过滤后的列表,可便捷实现删除;
文本不换行
white-space: nowrap;
判断对象是否为空
v-if="JSON.stringfy(address) === '{}'"
收货地址的选择
- 调用小程序提供的chooseAddress方法
js
methods: {
async chooseAddress() {
const [err, succ] = await uni.chooseAddress().catch(err => err)
if (err == null && succ.errMsg == 'chooseAddress:ok'){
this.address = succ
}
}
}
当前已选择的收货地址,也需要保存到 vuex中进行持久化;
- 在store中新建一个user.js,用来实现用户选择的收货地址持久化的过程;
- 之后当前页面的address也不在组件的data中进行存储,而是直接使用vuex中的数据;
js
// store.user.js
export default {
namespaced: true,
state: () => ({
address: JSON.parse(uni.getStorageSync('address' || '{}')),
}),
mutations: {
updateAddress(state, address) {
state.address = address
this.commit('m_user/saveAddressToStorage')
},
saveAddressToStorage(state){
uni.setStorageSync('address', JSON.stringfy(state.address))
}
},
getters: {
addstr(state){
if (!state.address.provinceName) return ''
return state.address.provinceName + state.address.cityName + state.address.countryName
}
},
}
收货地址授权失败的问题
获取用户收货地址授权的时候,点击取消授权,需要进行特殊处理,否则将无法再次提示授权;
js
methods: {
async chooseAddress() {
const [err, succ] = await uni.chooseAddress().catch(err => err)
if (err == null && succ.errMsg == 'chooseAddress:ok'){
// 更新 Vuex中的地址
this.updateAddress(succ)
}
if (err && (err.errMsg === 'chooseAddress:fail auth deny' || err.errMsg === 'chooseAddress:fail authorize no response')){
// 重新发起授权(可在小程序IDE中 清空缓存的授权信息 进行测试)
this.reAuth()
}
},
async reAuth(){
// 提示给用户重新授权
const [err2, confirmResult] = await uni.showModel({
cotent:"检测到您没打开地址权限,是否去设置打开?",
confirmText: "确认",
cancelText: "取消"
})
if (err2) return
if (confirmResult.cancel) return uni.$showMsg("您取消了地址授权")
if (confirmResult.confirm) {
// 打开授权设置面板
return uni.openSetting({
// 设置成功的回调
success: (settingResult) => {
if (settingResult.authSetting["scope.address"]) return uni.$showMsg("授权成功")
if (!settingResult.authSetting["scope.address"]) return uni.$showMsg("授权取消")
}
})
}
}
}
iphone设备的问题:
- iPhone真机上,err.errMsg的值为
chooseAddress:fail authorize no response
自定义结算组件
css
/* 固定定位 */
.my-settle-container {
position : fixed;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
backgroundColor: #c00000;
}
被遮蔽的组件,可以加一个下
padding-bottom:50px;
;
vuex中所有选中状态商品的数量总数:
js
// store.cart.js 定义一个 checkedCount的getters
checkedCount(state){
return state.carts.filter(x=>x.goods_state).reduce((total, item) => total += item.goods_count, 0)
}
结算组件中的全选/反选操作:
xml
<!-- 可以看到 radio组件仅做显示 按钮操作是加在其父节点上的 -->
<label class="radio" @click="changeAllState">
<radio color="#C00000" :checked="isFullCheck" /><text>全选</text>
</label>
js
// isFullCheck 是有 选中商品数 与 所有商品数 判等 的计算属性
methods: {
...mapMutations('m_cart', ['updateAllGoodsState']),
changeAllState(){
this.updateAllGoodsState(!this.isFullCheck)
}
}
登录
点击结算要检验
- 是否勾选了要结算的商品
- 是否选择了收货地址
- 是否登录
js
// 登录判断
if (!thid.token ) return uni.$showMsg("请前往登录")
token也是存在vuex state中的;
登录/我的
登录:
- 准备登录参数
- encryptedData:用户信息(加密),通过
getUserInfo()
获取 - iv:加密算法的初始向量,通过
getUserInfo()
获取 - rawData:用户信息json字符串,通过
getUserInfo()
获取,JSON.stringfy转字符串 - signature:sha1签名,通过
getUserInfo()
获取 - code:用户登录凭证,通过
wx.login()
获取
- encryptedData:用户信息(加密),通过
- 调用登录接口
xml
<!-- 按钮 指定 open-type 和getuserinfo -->
<button type="primary" class="btn-login" open-type="getUserInfo" @getuserinfo="getUserInfo">登录</button>
js
getUserInfo(e){
if (e.detail.errMsg === "getUserInfo:fail auth deny"){
return uni.$showMsg("登录授权取消")
}
// e.detail中包含了 登录所需参数
// e.detail.userInfo需要存储到Vuex中
// this.updateUserInfo(e.detail.huseInfo)
// 进一步调用
this.getToken(e.detail)
},
getToken(info){
// 调用微信登录
const [err, res] = await uni.login().catch(err=>err)
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 { data: loginResult } = await uni.$http.post("/suburl", query)
if (loginResult.meta.status !== 200 )return uni.$showMsg("登录失败")
uni.$showMsg("登录成功")
// loginResult.message.token需要存储到Vuex中
// this.updateToken(loginResult.message.token)
// 页面使用token是否存在 来切换组件的展示
}
通过尾元素实现弧顶
css
.login-container {
height: 750rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
&::after {
content: "";
display: block;
width: 100%;
height: 40px;
backgroundColor: #c00000;
position: absolute;
bottom: 0;
left: 0;
border-radius: 100%;
transform: translateY(50%);
}
}
页面全屏
css
/* 使用根节点page标签 */
page, my-container {
height: 100%;
}
组件重叠
只需要使用相对定位,然后在需要重叠的方向 设置负值即可;
css
.panel-list {
padding: 0 10px;
position: relative;
top: -10px;
}
退出登录
js
async logout(){
const [err, succ] = await uni.showModel({
title: "提示",
content: "确认退出登录吗?"
}).catch(err => err)
if (succ && succ.confirm){
// 清空vuex中的userinfo token 和 address
this.updateUserInfo({})
this.updateToken("")
this.updateAddress({})
}
}
3s倒计时提示后自动跳转到登录页
js
data(){
return {
seconds: 3,
timer: null
}
},
// ...
showTips(n){
uni.showToast({
icon: 'none',
title: "请登录后再结算!" + n + "秒后自动跳转",
mask: true,
duration: 1500
})
}
delayNavigate(){
this.seconds = 3 // 每次触发 都从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)
}
从购物车页面 触发的登录,在登录成功后还需要自动跳转会购物车页面(可以把参数存储到Vuex中来实现);
支付
添加身份认证请求头字段
只有登录后才允许调用支付相关接口,因此需要为有权限的接口添加身份认证请求头字段;
js
import store from "@/store/store.js"
$http.beforeRequest = function(options){
uni.showLoading({
title: '数据加载中...'
})
// 包含/my/的url被认为是 有权限的接口
if (options.url.indexOf('/my/' !== -1)){
// 提阿难捱身份认证字段
options.header = {
Authorization: store.state.m_user.token,
}
}
}
微信支付流程
- 创建订单,返回订单编号
- 订单预支付,使用订单编号,获取订单支付的相关信息(订单预支付的参数对象)
- 发起微信支付:
- 调用
uni.requestPayment()
,发起微信支付,传入"订单预支付的参数对象"; - 监听
uni.requestPayment()
的请求回调success
、fail
和complete
函数;
- 调用
创建订单:传入总价格、地址和商品列表信息;
订单预支付的参数对象:
- nonceStr
- package
- paySign
- signType
- timeStamp
js
const [err, succ] = await uni.requestPayment(payInfo)
if (err) return uni.$showMsg("订单未支付!")
const { data: res3 } = await uni.$http.post("/查询支付的回调结果", { order_number: orderNumber })
if (res3.meta.status !== 200) return uni.$showMsg("订单未支付!")
uni.showToast({
title: "支付完成",
icon: "success"
})
发布
一般步骤:
- HBuilderX 菜单栏点击"发行","小程序-微信"
- 弹出框填写发布的小程序名称,和AppId;点击"发行"
- 编译ing
- 编译完成后,会打开微信开发者工具,地那几工具栏上的"上传"按钮;
- 继续填写:版本号 和 项目备注,点击上传;
- 上传成功后会提示"上传代码完成",并提示编译后的代码大小;
- 登录微信小程序管理后台,在版本管理,"开发版本"列表中,点击"提交审核",审核通过后就是"审核版本",再点击"发布"按钮就成为了"线上版本";
发布体验版小程序:
- 1.DCloudID 重新获取;(无需每次都获取;如果DCloud账号配置正常的话,还需要重新在manifest.json中生成自己的APPID;git上无需提交该修改;)
- 2.前往验证账号;
- 3.点击发行-微信小程序-"提交审核"按钮旁的下拉选择设置为体验版即可;
HBuilderX登录DCloud账号是在IDE的左下角;