小程序实战项目

271 阅读6分钟

优购项目主要参考小米商城和苏宁易购

安装说明:
git clone git@github.com:7badao/yougoushoping.git
运行
npm run dev/start
技术栈

mpvue+es6+eslint等,有首页,分类,商品,登录,支付,我的信息等页面,功能完善的比较全面

  • 如果觉得对您有帮助,您可以点右上角给个 star 支持一下,十分感谢!如果有问题,也欢迎提交 issue 一起探讨!

  • 项目截图:可以私信我,这里就不一一上传了,github里也有,如果您有更好的建议或者想法可以一起讨论

  • 项目问题

  • 对promise进行了封装
    // 设置基准地址
    const BASE_URL = 'https://www.uinav.com'
    function request (options) {
      // 判断是否需要加载
      if (!options.noLoading) {
        wx.showToast({
          title: '加载中...', // 提示的内容,
          mask: true
        })
      }
      return new Promise((resolve, reject) => {
        wx.request({
          url: BASE_URL + options.url,
          data: options.data,
          success: res => {
            resolve(res.data.message)
          },
          fail: (err) => {
            console.log(err)
          },
          complete () {
              // 关闭提示
            if (!options.noLoading) {
              wx.hideToast()
            }
          }
        })
      })
    }
    export default request
    
    
    使用promise
    getCategories () {
          this.$request({
            url: '/api/public/v1/categories'
          }).then(data => {
            // console.log(data)
            this.goriesList = data
          })
        }
    

封装头部搜索组件

<template>
  <!-- 头部搜索框 -->
  <div class="header">
    <div class="inputBox">
      <icon type="search" size="20"></icon>
      <span>搜索</span>
    </div>
  </div>
</template>

<script>
export default {

}
</script>

<style lang="less" scoped>
.header {
  height: 100rpx;
  padding: 0 16rpx;
  display: flex;
  align-items: center;
  background-color: #eb4450;
  .inputBox {
    display: flex;
    flex: 1;
    justify-content: center;
    align-items: center;
    overflow: hidden;
    height: 60rpx;
    color: #bbb;
    background-color: #fff;
    font-size: 34rpx;
    icon {
      margin-right: 16rpx;
    }
  }
}
</style>
使用头部组件
// 导入组件
import searchLink from '@/components/searchLink'
//在components中定义 在页面以标签的形式使用
  components: {
    searchLink
  }
<searchLink/>
完成多级导航列表

页面分析
  • 点击分类切换不同的商品列表
  • 点击商品跳转到商品所在的页面
  • 点击搜索跳转到搜索页面
静态页面
  • 将头部封装成组件,需要注意的是,要给组件加scoped,不然导入时会影响样式
接口数据
  • 接口:/api/public/v1/categories
商品页面

在这个页面使用到了下拉刷新,上拉加载更多,并且对此作出优化
  • 遇到的坑

  • 下拉刷新的时候,请求数据没发回来,用户可以用可以一直刷新,这样对服务器会造成不小的压力

    • 这时设置了一个标识位,用来判断当前是否是在发请求,如果就不再发送请求
          // 如果是不在请求或者数据请求完毕直接return
          if (this.isRequest || this.isGoodsList) {
            return
          }
          // 请求中
          this.isRequest = true
    
    • 不管promise什么状态都执行,所以在finally中设置为false
    finally(() => {
            // 不管promise的状态 都会执行
            this.isRequest = false
          })
    
  • 上拉加载更多时,即使数据请求完成或则到达最后一页,只要用户上拉依旧也会发送一个请求,同时如果你直接对数据进行赋值还会发生,后面页数会覆盖前面页数的情况,当你在第二页往上滑,第一页的数据需要重新请求回来

    • 同样设置一个标志位,只要当数据请求完毕或者到达最后一页就不再发送请求,并且展示一个提示,提示数据加载完毕
    • 解决后面数据覆盖前面数据的问题使用扩展运算符
     // 下拉出现问题 后面数据覆盖前面 所以不能直接赋值
            // this.goodsList = data.goods
            this.goodsList = [...this.goodsList, ...data.goods]
            // 判断数组的长度是否等于返回的total
            if (this.goodsList.length === data.total) {
              this.isGoodsList = true // 数据请求完毕
            }
    
  • 对方法进行优化封装 reload方法,因为上面两个问题解决,用到的方法及其类似

    reload () {
          // 设置请求为第一页
          this.pagenum = 1
          this.isRequest = false
          this.isGoodsList = false
          // 将数组里面的数据清空
          this.goodsList = []
          this.getSearch()
        },
    
    • 切换页面时数据依旧保留
    • 将数据数组清空,并将搜索关键字重置设为输入的关键字
下拉时头部固定问题
  • 如果设置为固定定位,那么下拉加载的时候,效果会给覆盖直接看不到,所以想来想去还是设置一个标志位来判断,如果是在下拉属性的时候,定位为static,其他时候都是固定定位
 <!-- 头部搜索框 -->
    <div class="topHeader" :style="{position:isFixed?'fixed':'static'}">
        // 这一步是因为样式的问题
        <!-- 商品展示区域 -->
    <div class="bigShowBox" :style="{marginTop:isFixed?'220rpx':'0'}">
    // 当页面滚动时
    onPageScroll () {
    this.isFixed = true
  }
  • 总结:这一个页面难点还是有的,还是有不少的坑
搜索页面

封装搜索组件,多个页面都需要用到
  • 如果输入框没有内容隐藏清空按钮,如果有内容显示
  • 这也是v-show的一个应用场景
  • 那么问题来了那些时候只能用v-if不能用v-show呢?
    • 在上面多级导航的时候,因为有些数据是不全的,此时有些数据有标题没数据,此时会有个报错,告诉我这个列表没数据,因为它也是需要频繁切换的,如果我用v-show来的话,它依旧报错,此时就biubiu是v-if
<template>
  <div class="header">
    <input type="text" v-model.trim="keyWord" @confirm="confirmHeader" />
    <icon class="search-icon" type="search" size="16"></icon>
    <!-- 点击清空按钮清空输入框的所有内容 -->
    <!-- 有内容则显示清空,这v-show频繁切换 -->
    <icon
      class="clear-icon"
      type="clear"
      size="16"
      color="#ccc"
      v-show="keyWord"
      @click="keyWord=''"
    ></icon>
  </div>
</template>
<script>
export default {
  // 接受传过来的值
  props: ['query'],
  data () {
    return {
      keyWord: this.query
    }
  },
  methods: {
    // 点击搜索触发的事件
    confirmHeader () {
      this.$emit('confirm', this.keyWord)
    }
  },
  // 使用侦听器 侦听query的变换 解决页面切换后内容没有清空
  watch: {
    query (newValue) {
      this.keyWord = newValue
    }
  }
}
</script>
<style lang="less" scoped>
.header {
  height: 120rpx;
  background-color: #eee;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  input {
    background-color: #fff;
    height: 60rpx;
    width: 720rpx;
    padding-left: 74rpx;
    box-sizing: border-box;
  }
  .search-icon {
    position: absolute;
    left: 44rpx;
  }

  .clear-icon {
    position: absolute;
    right: 44rpx;
    z-index: 100;
  }
}
</style>
搜索页面
<template>
  <div>
    <searchBar @confirm="toSearchList" />
    <div class="history-search">
      <div class="title">
        <span class="title">历史搜索</span>
        <icon type="clear" size="18" @click="clearList"></icon>
      </div>
      <ul>
        <li v-for="item in keywordList" :key="item" @click="toSearchList(item)">{{item}}</li>
      </ul>
    </div>
  </div>
</template>

<script>
// 导入搜索框
import searchBar from '@/components/searchBar'
const KEY_WORD = 'keyword'
export default {
  data () {
    return {
      // 取出本地数据
      keywordList: []
    }
  },
  components: {
    searchBar
  },
  onShow () {
      // 初始时有可能为空
    this.keywordList = wx.getStorageSync(KEY_WORD) || []
  },
  methods: {
    // 按照关键字跳转
    toSearchList (data) {
      // 遍历数组 重复的不添加
      let newKeyWord = this.keywordList.filter(v => {
        return v !== data
      })
      // 在数组的头部添加
      newKeyWord.unshift(data)
      // 取出本地数据
      wx.setStorageSync(KEY_WORD, newKeyWord)
      // 点击搜索历史跳转到关键字页面
      wx.navigateTo({ url: '/pages/searchList/main?query=' + data })
    },
    // 点击清空按钮清空所有数据
    clearList () {
      wx.showModal({
        title: '提示', // 提示的标题,
        content: '你确定要清空历史搜索记录吗?', // 提示的内容,
        showCancel: true, // 是否显示取消按钮,
        cancelText: '取消', // 取消按钮的文字,默认为取消,最多 4 个字符,
        cancelColor: '#000000', // 取消按钮的文字颜色,
        confirmText: '确定', // 确定按钮的文字,默认为取消,最多 4 个字符,
        confirmColor: '#3CC51F', // 确定按钮的文字颜色,
        success: res => {
          if (res.confirm) {
            wx.removeStorageSync({ key: KEY_WORD })
            // 清空列表内容
            this.keywordList = []
          }
        }
      })
    }
  }
}
</script>

<style lang="less">
.search {
  background-color: #eee;
  padding: 30rpx 15rpx;
  display: flex;
  justify-content: space-between;
  font-size: 28rpx;
  position: relative;
  icon {
    position: absolute;
    top: 50rpx;
    left: 38rpx;
  }
  input {
    height: 60rpx;
    flex: 1;
    background-color: #fff;
    padding-left: 70rpx;
    box-sizing: border-box;
    border-radius: 4rpx;
  }
  button {
    width: 160rpx;
    height: 60rpx;
    line-height: 60rpx;
    border-radius: 8rpx;
    font-size: 28rpx;
    border: 1rpx solid #bfbfbf;
    background-color: #eee;
    margin-left: 20rpx;
  }
}

.history-search {
  color: #333;
  font-size: 28rpx;
  padding: 30rpx 30rpx 30rpx 15rpx;
  .title {
    display: flex;
    justify-content: space-between;
  }

  ul {
    display: flex;
    flex-wrap: wrap;
    margin-top: 30rpx;
    li {
      height: 64rpx;
      line-height: 64rpx;
      padding: 0 26rpx;
      background-color: #ddd;
      margin: 0 30rpx 16rpx 0;
    }
  }
}
</style>

加入购物车逻辑

  • 点击购物车跳转到购物车页面

    • 点击购物车跳转到购物车页面

    • 注意是tabBar跳转不能使用wx.navigateTo而是要使用wx.switchTab

  • 点击加入购物车,将商品添加到购物车列表,并保存到本地

    • 这里有一个优化,我附上原始代码与优化后的代码
    • 原始代码
    // 取出本地数据,第一次有可能为空(没做判断mpvue也没报错)
    let cart = wx.getStorageSync('cart')||[]
    // 得到点击商品的Id
    let goodsId = this.goodsList.goods_id
    // 查询数据
    let item = cart.find(v=>{
    	return goodsId===goodsId
    })
    // 如果不存在数据
    if(!item){
        // 添加数据
    	cart.push({
            goods_id=:oodsId,
            num:1,
            // 选中状态
            checked:true
        })
    }else{
        // 不是第一次添加
    	item.num++
    }
     // 存入本地数据
          wx.setStorageSync('cart', cart)
    
    • 改造后的代码,这里我没把数组改掉出现了个bug
    // 取出本地数据(注意这边改成对象),如果存入你还是数组,那么他会存入当前点击商品id+1
    let cart = wx.getStorageSync('cart')||{}
    // 得到点击商品的Id
    let goodsId = this.goodsList.goods_id
    cart[goodsId] = {
            // 存在数据,数据++,不存在存入
            num: cart[goodsId] ? ++cart[goodsId].num : 1,
            checked: true
        }
     // 存入本地数据
          wx.setStorageSync('cart', cart)
    
    • 再次改造,简洁很多
    cart[goodsId] = {
        // 数据是否存在,存在+1,不存在则存入
    	num:cart[goodsId]?++cart[gooddsId].num:1,
        checked:true
    }
    

购物车页面

  • 数据渲染步骤

    • 接口:/api/public/v1/goods/goodslist?goods_ids=
    • 取出本地的数据:let cart = wx.getStorageSync('cart')
    • get请求,拼接路径'/api/public/v1/goods/goodslist?goods_ids='+Object.keys('cart')
    • 数据融合,融合代码见下文
    • 数据赋值:this.goodsList = data
  • 为什么要数据融合了?

    • 如果不数据融合,在渲染商品数量的时候你得从cart中取数据---cart[item.goods_id].num,这样在做按钮的显示,点击取反的时候数据看其里就很乱,如果融合了就可以直接使用item
    // 将购物车的数据与goodsList的数据融合
    
    ​        data.forEach(v => {
    
    ​          v.num = cart[v.goods_id].num
    
    ​          v.checked = cart[v.goods_id].checked
    
    ​        })
    
  • 商品按钮逻辑,这一步我一开始是使用forEach遍历,之后觉得直接使用forof会比较好,但是又想了一下,用every是最好的

  • every---对数组中每一项给定函数,如果数组中每一项都是true的话则返回true

  • some---对数组中每一项给点函数,只要数组中有一项是true则返回true

    • 商品按钮一个没选中,全选就不选中
    • 全选选中,上方按钮全部选中 使用计算属性
  • forEach遍历

isAll(){
    let isAllBtn = true
    this.goodsList.forEach(v=>{
        // 判断按钮的状态
        if(!v.checked){
			isAllBtn = false
        }
    })
    return isAllBtn
}
  • for of遍历
let isAllBtn = true
for(let item of this.goodsList){
	if(!item.checked){
		isAllBtn = false
        break
    }
}
return isAllBtn
  • every
isAll:{
    get(){
        this.goodsList.every(v=>{
            return v.checked
        })
    },
    set(){
           this.goodsList.every(v=>{
            v.checked = statues
        })
       }
}

总数量的显示

  • 这里我尝试了两种做法

    • 第一种foreach
    computed:{
        countNum() {
    		let sum = 0
            this.goodsList.forEach(v=>{
    			sum += v.num
            })
            return sum
        }
    }
    
    • 第二种reduce
    computed:{
        countNum() {
        return this.goodsList.reduce((sum,v)=>{
           	// 选中显示num没选中是0
    		return sum+(v.checked?v.num:0)
        })
        }
    }
    
    • reduce用法
    arr.reduce((上一次计算的值,当前遍历的元素)=>{
        return 上一次计算的值与遍历元素的运算
    },初始值)
    
    • 计算商品总价

computed:{
	totalPrice() {
        return this.goodsList((sum,v)=>{
            return sum + (v.checked?v.num*v.goods_price:0)
        },0)
    }
}
将商品数据保存回本地
onHide() {
	let cart = {}
    this.goodsList.forEach(v=>{
        // 当前的商品状态
        cart[v.goods_id] = {
			num:v.num,
            checked:v.checked
        }
    })
    // 将数据保存回本地去
    wx.setStorageSync('cart',cart)
}
点击结算按钮逻辑
  • 判断用户是否登录,如果未登录跳转到登录

  • 判断是否选中商品,未选中提示用户

    countPrice () {
          // 判断是否未选中商品
          if (!this.countNum) {
            wx.showToast({
              title: '请选择商品',
              icon: 'none'
            })
            return
          }
          // 判断用户是否登录
          // 取出token
          let token = wx.getStorageSync('token')
          if (!token) {
            wx.navigateTo({ url: '/pages/login/main' })
            return
          }
          wx.navigateTo({ url: '/pages/pay/main' })
        }