该项目基础来源于coderwhy:https://github.com/coderwhy/HYMall
我完成的项目地址:https://github.com/IronManTonyStark/Mall-Vue.js
原项目有些小bug有修复,有些地方有点改动。
总结了下学习的过程,大概如下:
封装TabBar->TabBar外部的框,中间预留插槽,可插入多个选项钮
封装TabBarItem->TabBar内部的图标和内容:包括激活时的图标,未激活时的图标,图标说明
路由配置->为Home,Category,Cart,Profile四个组件配置路由
响应点击切换设计
r o u t e r 和 router和 router和route的区别:router是VueRouter的一个对象,是一个全局的对象,他包含了所有的路由包含了许多关键的对象和属性。route是一个跳转的路由对象,每一个路由都会有一个route对象,是一个局部的对象。
this.$route.path.indexOf(this.link) !== -1
判断当前所在路由进行图标的切换显示。
this.$router.replace(this.link)
通过replace方法来改变路由。
封装完成后,在content中将Tabbar重新封装成为MainTabBar。
导入axios
npm install --save axios vue-axios
创建axios实例->后续开发某些配置可能和默认实例不一样,创建新的实例,传入属于该实例的配置信息
const instance = originAxios.create({
baseURL: //请求地址,
timeout: //时限ms
});
配置请求和响应拦截
请求拦截的作用和使用
当发送网络请求时, 在页面中添加一个loading组件, 作为动画。
某些请求要求用户必须登录, 判断用户是否有token, 如果没有token跳转到login页面。
对请求的参数进行序列化(看服务器是否需要序列化)。
config.data = qs.stringify(config.data)
使用
instance.interceptors.request.use(config => {
// console.log('来到了request拦截success中');
return config
}, err => {
// console.log('来到了request拦截failure中');
return err
})
响应拦截的作用和使用
响应的成功拦截中,主要是对数据进行过滤。
响应的失败拦截中,可以根据status判断报错的错误码,跳转到不同的错误提示页面。
使用
instance.interceptors.response.use(response => {
// console.log('来到了response拦截success中');
return response.data
}, err => {
console.log('来到了response拦截failure中');
console.log(err);
if (err && err.response) {
switch (err.response.status) {
case 400:
err.message = '请求错误'
break
case 401:
err.message = '未授权的访问'
break
}
}
return err
})
传入对象进行网络请求
instance(option).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
封装请求首页更多数据network->home.js
请求多个数据:将banner数据放在banners变量中,将recommend数据放在recommends变量中
getHomeMultidata().then(res => {
this.banners = res.data[BANNER].list
this.recommends = res.data[RECOMMEND].list
})
请求商品数据:根据传入类型(POP、NEW、SELL)请求当前页码(page)对应的数据,传入到goodlist中,之后将page加1
getHomeData(type, this.goodsList[type].page).then(res => {
const goodsList = res.data.list;
this.goodsList[type].list.push(...goodsList)
this.goodsList[type].page += 1
})
封装Swiper和SwiperItem:可自己封装或使用组件库Mint UI
封装对于首页的childComps->HomeSwiper
<swiper ref="swiper" v-if="banners.length">
<swiper-item v-for="(item, index) in banners" :key="index">
<a :href="item.link">
<img :src="item.image" alt="">
</a>
</swiper-item>
</swiper>
传入banners进行展示
<home-swiper :banners="banners" ref="hSwiper"></home-swiper>
封装childComps->FeatureView
<div class="feature"> <div class="feature-item" v-for="(item, index) in features"> <a :href="item.link"> <img :src="item.image" alt=""> <div>{{item.title}}</div> </a> </div> </div>js
传入recommends数据,进行展示
<feature-view :features="recommends"></feature-view>
展示图片即可。
封装content->TabControl
<div class="tab-control">
<div class="tab-control-item"
:class="{active: currentIndex === index}"
@click="itemClick(index)"
v-for="(item, index) in titles">
<span>{{item}}</span>
</div>
</div>
itemClick: function (index) {
// 1.改变currentIndex
this.currentIndex = index;
// 2.发出事件
this.$emit('itemClick', index)
}
监听点击
//默认currentType = POP
tabClick(index) {
switch (index) {
case 0:
this.currentType = POP
break
case 1:
this.currentType = NEW
break
case 2:
this.currentType = SELL
break
}
}
使用
<tab-control @itemClick="tabClick"
:titles="['流行', '新款', '精选']"
ref="tabControl"></tab-control>
展示商品列表,封装childComps->GoodsList
<grid-view>
<goods-list-item v-for="(item, index) in goodsList" :key="index" :goods="item"></goods-list-item>
</grid-view>
列表中每一个商品,封装childComps->GoodsListItem
使用vue图片懒加载v-lazy
npm install vue-lazyload --save-dev
在main.js中配置
Vue.use(VueLazyload, {
//预加载的高度比例
preLoad: 1,
//加载失败显示图片
error: require('assets/img/common/error.png')
//加载中显示图片
loading: require('assets/img/common/placeholder.png'),
//尝试次数
attempt: 1
})
封装
<div class="goods">
<img v-lazy="getImg" :key="getImg" alt="">
<div class="goods-info">
<p>{{goods.title}}</p>
<span class="price">¥{{goods.price}}</span>
<span class="collect">{{goods.cfav}}</span>
</div>
</div>
//getImg属性
computed: {
getImg() {
return this.goods.img || this.goods.image || this.goods.show.img
}
}
在Home中使用
<goods-list :goods-list="showGoodsList"/>
//showGoodsList属性
computed: {
showGoodsList() {
return this.goodsList[this.currentType].list
}
},
安装better-scroll
npm install better-scroll --save
封装一个独立的组件,用于作为滚动组件:Scroll
组件内代码的封装:
1.创建BetterScroll对象,并且传入DOM和选项(probeType、click、pullUpLoad)
if (!this.$refs.wrapper) return
this.scroll = new BScroll(this.$refs.wrapper, {
//监听滚动位置
//0,1都是不侦测实时位置
//2:只要在滚动过程中侦测,手指离开后的惯性滚动中不侦测
//3: 只要是滚都,都侦测
probeType: this.probeType,
//better-scroll 默认会阻止浏览器的原生 click 事件。当设置为 true,better-scroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性 _constructed,值为 true。
click: true,
//这个配置用于做下拉刷新功能,默认为 false。当设置为 true 或者是一个 Object 的时候,可以开启下拉刷新,
pullUpLoad: this.pullUpLoad
})
2.监听scroll事件,该事件会返回一个position
this.scroll.on('scroll', pos => {
this.$emit('scroll', pos)
})
3.监听pullingUp事件,监听到该事件进行上拉加载更多
this.scroll.on('pullingUp', () => {
console.log('上拉加载');
this.$emit('pullingUp')
})
4.封装刷新的方法:this.scroll.refresh()
refresh() {
this.scroll && this.scroll.refresh && this.scroll.refresh()
},
5.封装滚动的方法:this.scroll.scrollTo(x, y, time)
this.scroll && this.scroll.scrollTo && this.scroll.scrollTo(x, y, time)
6.封装完成刷新的方法:this.scroll.finishedPullUp
this.scroll && this.scroll.finishPullUp && this.scroll.finishPullUp()
通过Scroll监听上拉加载更多。
//触发时机:在一次上拉加载的动作后,这个时机一般用来去后端请求数据。
@pullingUp="loadMore"
在Home中加载更多的数据。
loadMore() {
this.getHomeProducts(this.currentType)
},
请求数据完成后,调动finishedPullUp
this.$refs.scroll.finishPullUp()
封装BackTop组件
<div class="back-top" @click="topClick">
<slot></slot>
</div>
methods: {
topClick: function () {
this.$emit('backTop');
}
}
定义一个常量,用于决定在什么数值下显示BackTop组件
this.showBackTop = position.y < -BACKTOP_DISTANCE
监听滚动,决定BackTop的显示和隐藏
v-show="showBackTop"
监听BackTop的点击,点击时,调用scrollTo返回顶部
backTop() {
this.$refs.scroll.scrollTo(0, 0, 300)
},
重新添加一个tabControl组件(需要设置定位,否则会被盖住)
在updated钩子中获取tabControl的offsetTop
updated() {
this.$nextTick(() => {
this.tabOffsetTop = this.$refs.tabControl.$el.offsetTop
})
},
判断是否滚动超过了offsetTop来决定是否显示新添加的tabControl
this.isTabFixed = position.y < -this.tabOffsetTop
Better-Scroll在决定有多少区域可以滚动时, 是根据scrollerHeight属性决定
如何解决这个问题了?
监听每一张图片是否加载完成, 只要有一张图片加载完成了, 执行一次refresh()
Vue监听图片加载完成:@load=‘方法’
调用scroll的refresh()
mounted() {
// 1.图片加载完成的事件监听
const refresh = debounce(this.$refs.scroll.refresh, 50)
this.$bus.$on('itemImageLoad', () => {
refresh()
})
},
如何将GoodsListItem.vue中的事件传入到Home.vue中
对于refresh非常频繁的问题, 进行防抖操作
debounce(func, delay) {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
},
为路由器设施keep-alive属性
离开时
deactivated() {
this.$refs.hSwiper.stopTimer()
this.saveY = this.$refs.scroll.getScrollY()
},
进入时
activated() {
this.$refs.hSwiper.startTimer()
this.$refs.scroll.scrollTo(0, this.saveY, 0)
this.$refs.scroll.refresh()
},
创建views->detail组件,并配置路由
监听GoodListItem点击
goToDetail () {
// 1.获取iid
let iid = this.goods.iid;
// 2.跳转到详情页面
this.$router.push({path: '/detail', query: {iid}})
}
DetailNavBar向Detail发送点击事件
itemClick: function (index) {
this.$emit('itemClick', index)
},
创建数组themeTops来获取每个标题的offsetTop
获取offsetTop,在updated()中进行调用(小bug:图片加载问题会导致获取错误的位置,有时点击两次才能到正确位置)
_getOffsetTops() {
this.themeTops = []
this.themeTops.push(this.$refs.base.$el.offsetTop)
this.themeTops.push(this.$refs.param.$el.offsetTop)
this.themeTops.push(this.$refs.comment.$el.offsetTop)
this.themeTops.push(this.$refs.recommend.$el.offsetTop)
},
监听点击事件
titleClick(index) {
//console.log(this.themeTops[index])
this.$refs.scroll.scrollTo(0, -this.themeTops[index], 100)
},
监听滚动事件
为themeTops最后添加一个很大的值,用于和最后一个主题的top进行比较
this.themeTops.push(Number.MAX_VALUE)
根据滚动位置来确定currentIndex
_listenScrollTheme(position) {
let length = this.themeTops.length;
for (let i = 0; i < length; i++) {
let iPos = this.themeTops[i];
if (position >= iPos && position < this.themeTops[i+1]) {
if (this.currentIndex !== i) {
this.currentIndex = i;
}
break;
}
}
},
导入content -> BackTop
与Home类似进行使用
因为在Home和Detail有大量重复的关于BackTop的代码,于是把他们提取出来放到mixin.js中
export const backTopMixin = {
data() {
return {
showBackTop: false
}
},
components: {
BackTop
},
methods: {
backTop: function () {
this.$refs.scroll.scrollTo(0, 0, 300);
}
}
}
在DetailBottomBar中向父组件Detail发出点击事件
addToCart() {
this.$emit('addToCart')
}
创建对象,获取加入购物车的商品所需要的信息
const obj = {}
obj.iid = this.iid;
obj.imgURL = this.topImages[0]
obj.title = this.goods.title
obj.desc = this.goods.desc;
obj.newPrice = this.goods.nowPrice;
在Store的state属性中创建cartList数组来保存加入购物车的商品
在actions中实现addCart方法
为什么不在mutations中实现?
mutations唯一的目的就是修改state中的状态,里面的方法完成的事件比较单一一点,该方法有判断条件,放到actions中较好
addCart(context, info) {
//console.log(info);
// 1.查看是否添加过
let oldInfo = context.state.cartList.find(item => item.iid === info.iid)
// 2.+1或者新添加
if (oldInfo) {
//oldInfo.count += 1
context.commit('addCounter', oldInfo)
} else {
info.count = 1
context.commit('addToCart', info)
}
}
mutations中的方法
addCounter(state, oldInfo) {
oldInfo.count ++;
},
addToCart(state, info) {
state.cartList.push(info)
}
在Detail中将商品对象添加到Store中
this.$store.dispatch('addCart', obj)
导入commen->NavBar使用
<nav-bar class="nav-bar">
<div slot="center">购物车({{count}})</div>
</nav-bar>
从getters中获取cartList和cartCount
cartList(state) {
return state.cartList
},
cartCount(state, getters) {
return getters.cartList.length
}
在Cart中使用mapGetters将getters中的方法变为计算属性直接使用
导入
import { mapGetters } from 'vuex'
使用
computed: {
...mapGetters({
count: 'cartCount',
})
}
在CartList中使用mapGetters获取cartList数据
封装展示每件商品的组件GoodListItem,将CartList中的数据进行展示
封装勾选按钮checkButton,在CartlistItem中使用
在actions中为商品添加Checked属性,默认为false(未选中)
info.checked = false
在CheckButton中接受checked来确定商品是否被选中,用watch来检测变化(也可用计算属性)
props: {
value: {
type: Boolean,
default: true
}
},
data() {
return {
checked: this.value
}
},
watch: {
value(newValue) {
this.checked = newValue;
}
}
发出点击事件
selectItem: function () {
this.$emit('checkBtnClick')
}
在CartListItem中使用
<CheckButton @checkBtnClick="checkedChange" :value="itemInfo.checked"/>
checkedChange() {
this.itemInfo.checked = !this.itemInfo.checked;
}
添加滚动效果
滚动的内容必须用
<scroll class="cart-list" ref="scroll">
<div>
<cart-list-item v-for="item in list" :key="item.iid" :item-info="item"></cart-list-item>
</div>
</scroll>
在activated中添加refersh()函数,解决滚动不了的问题
activated() {
this.$refs.scroll.refresh()
}
导入CheckButton封装全选按钮
监听点击事件
计算属性isSelectAll与value绑定判断是否有未选中的按钮
isSelectAll() {
return this.$store.getters.cartList.find(item => item.checked === false) === undefined;
}
点击事件
checkBtnClick: function () {
// 1.判断是否有未选中的按钮
let isSelectAll = this.$store.getters.cartList.find(item => !item.checked);
// 2.有未选中的内容, 则全部选中
if (isSelectAll) {
this.$store.state.cartList.forEach(item => {
item.checked = true;
});
} else {
this.$store.state.cartList.forEach(item => {
item.checked = false;
});
}
}
计算总价(过滤filter和累积reduce)
totalPrice() {
const cartList = this.$store.getters.cartList;
return cartList.filter(item => {
return item.checked
}).reduce((preValue, item) => {
return preValue + item.count * item.newPrice
}, 0).toFixed(2)
},
总数
$store.getters.cartCount
封装请求分类页数据network->category.js
在Category中保存请求的数据
getCategory
_getCategory() {
getCategory().then(res => {
// 1.获取分类数据
this.categories = res.data.category.list
// 2.初始化每个类别的子数据
for (let i = 0; i < this.categories.length; i++) {
this.categoryData[i] = {
subcategories: {},
categoryDetail: {
'pop': [],
'new': [],
'sell': []
}
}
}
// 3.请求第一个分类的数据
this._getSubcategories(0)
})
},
getSubcategories
_getSubcategories(index) {
this.currentIndex = index;
const mailKey = this.categories[index].maitKey;
getSubcategory(mailKey).then(res => {
this.categoryData[index].subcategories = res.data
this.categoryData = {...this.categoryData}
this._getCategoryDetail(POP)
this._getCategoryDetail(SELL)
this._getCategoryDetail(NEW)
})
},
getCategoryDetail
_getCategoryDetail(type) {
// 1.获取请求的miniWallkey
const miniWallkey = this.categories[this.currentIndex].miniWallkey;
// 2.发送请求,传入miniWallkey和type
getCategoryDetail(miniWallkey, type).then(res => {
// 3.将获取的数据保存下来
this.categoryData[this.currentIndex].categoryDetail[type] = res
this.categoryData = {...this.categoryData}
})
},
<div class="menu-list-item"
:class="{active: index===currentIndex}"
v-for="(item, index) in categories"
:key="index"
@click="itemClick(index)">
{{item.title}}
</div>
itemClick(index) {
this.currentIndex = index
this.$emit('selectItem', index)
}
selectItem(index) {
this._getSubcategories(index)
}
将TabControl混合封装到mixin.js后引入
export const tabControlMixin = {
components: {
TabControl
},
data: function () {
return {
currentType: POP
}
},
methods: {
tabClick(index) {
switch (index) {
case 0:
this.currentType = POP
break
case 1:
this.currentType = NEW
break
case 2:
this.currentType = SELL
break
}
console.log(this.currentType);
}
}
}
import {tabControlMixin} from "@/common/mixin";
mixins: [tabControlMixin],
使用TabControl
<tab-control :titles="['综合', '新品', '销量']"
@itemClick="tabClick"/>
所用到的图标都封装到content->Icon中
在App中注册使用Icom和SvgIcon
将用户信息进行排布,预留相应的插槽(如头像,名字,手机号等)
<list-view :list-data="orderList" class="order-list"></list-view>
<list-view :list-data="serviceList" class="service-list"></list-view>