2021版小程序开发5——小程序项目开发实践(2)-完

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中,类似配置上拉加载更多,配置enablePullDownRefreshbackgroundColor,为当前页面单独开启下拉刷新效果;
  • 在页面中,和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中,通过optionsbuttonGroup两个数组,来声明商品导航组件的按钮配置对象;
  • 使用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()获取
  • 调用登录接口
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()的请求回调successfailcomplete函数;

创建订单:传入总价格、地址和商品列表信息;

订单预支付的参数对象:

  • 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的左下角;

相关推荐
我不当帕鲁谁当帕鲁14 分钟前
arcgis for js实现平移立体效果
前端·javascript·arcgis
录大大i42 分钟前
HTML之CSS定位、浮动、盒子模型
前端·css·html
P7进阶路1 小时前
Ajax:重塑Web交互体验的人性化探索
前端·javascript·ajax
一路向北North1 小时前
apache-poi导出excel数据
apache·excel
bin91532 小时前
DeepSeek 助力 Vue 开发:打造丝滑的步骤条
前端·javascript·vue.js
ZeZeZe2 小时前
数据结构之栈与队列
前端·javascript
求索772 小时前
CSS 画水滴效果 - 意想不到的简单!
前端·css
炉火旁打滚2 小时前
无限循环滚动不定宽横幅
前端·javascript
抱走白菜2 小时前
JS高级:手写一个Promise
前端·面试·ecmascript 6
专注API从业者2 小时前
反向海淘独立站未来发展趋势:机遇与挑战并存,如何抢占先机?
大数据·开发语言·前端·数据仓库