优购项目主要参考小米商城和苏宁易购
安装说明:
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' }) }