github地址 点我
该项目所有代码都是自己敲得,不过也是照着b站的coderwhy老师学习的,大体框架、思路和老师的一样,细节不同。
b站视频链接:https://www.bilibili.com/video/av89760569
我是用git将项目push到github上的,虽然中途也踩了很多坑,但在这里就不做过多的叙述了,遇到报错查就是了。
不过开启git后,你的每次修改它都会记录,提醒,这样对我来说太麻烦了,所以我一般会选择关掉。
base.css中对normalize.css进行了引用,到时在App.vue对base.css进行引用即可
在编写项目源码前我们通常需要对项目中会经常引用到的文件夹起别名,这样就可减少引用文件时的代码,也保证代码的可读性。这个基本配置是在node_modules里面的,但是我们在这里的话就在vue.config.js里对基本配置进行补充
.editorconfig文件是对项目里所有的开发者的编程习惯的一种规范,比如默认空格2格还是四格就在这里面。cli3创建的项目将这个文件删除了,可从cli2创建的项目里copy过来
主页上面有个navbar,这个在多个页面都会使用,所以把它对应的组件放在components/common
里,封装组件、在Home中根据需求引用它即可
我们之前在network里面就封装好了request.js,其返回的其实就是一个promise对象,我们只需要调用它传入自己的配置,再在.then后面写就好了
但是在Home中写这些代码会将数据请求这部分代码和Home的其他代码混合,会耦合严重。一个页面会有多次数据请求的情况,全放在Home中代码就会越来越复杂,会变得难以维护。所以为了避免这种情况,想到在network中建个home.js文件,将Home的请求数据这部分代码放在里面,维护Home网络请求的相关代码时就在这个里面统一进行维护。
home.js导出个函数,这个函数里专门进行网络请求。
Home组件在一开始就要请求到网络数据,所以想到在created()里调用函数,并在then方法里将所需数据传到Home的data中进行保存,以便使用
用swiper插件写的,照着api文档写就是了,不过还是将模板放下面。写了个HomeSwiper组件,放在views/home/childCpn
里面
<template>
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="item in cbanners" :key="item.index">
<a :href="item.link">
<img :src="item.image" alt="" width="100%" height="150px">
</a>
</div>
</div>
<div class="swiper-pagination"></div>
</div>
</template>
<script>
import Swiper from 'swiper'
export default {
name: 'HomeSwiper',
//获取Home中的数据
props: {
cbanners: Array,
crecommends: Array
},
mounted() {
const mySwiper = new Swiper ('.swiper-container', {
//这个bug太牛逼了,loop:true明明是循环播放,我写了没用,注释后竟然有用
// loop: true,
//自动播放
autoplay: {
//用户操作每个item后依然自动播放
disableOnInteraction: false,
},
//强制刷新,解决动态引用数据导致的轮播图不正常显示的bug
observer: true,
//分页器,上面的那几个小点点
pagination: {
//绑定
el: '.swiper-pagination',
//可点击切换
clickable :true
}
})
}
}
</script>
<style>
@import "../../../../node_modules/swiper/css/swiper.css";
</style>
写了个HomeRecommend组件,也放在了views/home/childCpn
里面,布局好就行了,等这第一遍写完,写第二遍的时候,好好看看css布局什么的
写了个HomeFeature组件,也放在了views/home/childCpn
里面。与上面的广告不同的是它就是一张图片,但是点击了也是可以跳转页面的。至于广告链接和推荐之间的灰色分割区就是用广告链接的boder-bottom写的
fixed,top:0,left:0,right:0,z-index
对下方展示的商品进行控制,点击不同的种类展示不同的商品
写了个TabControl组件,放在components/content
里面。主要是要完成里面的点击变色操作,还要将其封装起来,还可在另外的页面使用,所以组件里不能有固定数据,需要从组件外部传入数据。
商品是分为三类的(流行、新款、精选),整个商品数据对应的接口是/home/data
而且Home是有上拉加载的功能的,假如点击流行,会将流行的第一页的30个数据请求并展示下来,上拉将第一页数据展示完后,会请求第二页的30个数据并继续展示。
所以要获取数据要知道type(pop,new,sell)
还有 page(1,2,3.....)
,所以要将它们作为query接在url后面获取相应的数据
在network/home.js
里面,封装个获取主页数据的函数。再在created()里面调用函数并在后续的then方法里对数据进行处理,拿到想要的数据res.data.list
。拿到数据了自然要保存数据
goods: {
'pop': {page: 0, list: []},
'new': {page: 0, list: []},
'sell': {page: 0, list: []}
}
再使用push和解构语法向每个对象的list里面存入获取的数据,考虑到保存数据的这个方法会在上拉加载时多次调用,page也不会是写死的,初始化为1,在每一次保存数据后 +1 ,那么下一次调用就会请求第2页的数据了
在components/content/goods
里面添加GoodsList、GoodsListItem两个组件,前者是整个商品显示区,后者是单个的商品
GoodsList要从Home中通过props获取pop的总的商品数据 goods['pop'].list
,GoodsListItem 要从 GoodsList 获取单个商品数据
一个GoodsListItem会有图片goodsItem.show.img
,介绍 goodsItem.title
,价格 goodsItem.price
,收藏图片(网络上找的)assets/img/common/select.js
,收藏数 goodsItem.cfav
再就是漫长的调样式过程啦,GoodsList 用到了 flex布局,介绍用到了使其只显示一行overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
,还用到了em布局等等
tabControl的每一个item都有其index,所以我们只要把被点击的item的index通过 this.$emit('tabClick', index)
穿给Home,在Home中通过switch语法将showType和index一一对应,再将showType替代之前写好的’pop’即可实现点击tabControl切换商品了
现在主页的滚动是根据浏览器的默认滚动的,但是浏览器的默认滚动在移动端的体验是很差的,这时我们就需要用better-scroll插件重构主页。(GitHub上搜better-scroll)
在用第三方框架和插件时,我们都需要封装一个组件,无论是网络请求用的axios、轮播图用的swiper还是现在用的better-scroll,我们在使用之前都需要对其封装,那么在其不在维护时我们就可轻松的对它进行替换。
创建一个Scroll组件,因为会在多个地方使用,所以放在components/common/scroll
中
<template>
<div class="wrapper" ref="wrapper">
<div class="content">
<slot></slot>
</div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
name: 'Scroll',
data() {
return {
scroll: null
}
},
//组件被创建之后
mounted() {
this.scroll = new BScroll(this.$refs.wrapper,{
// 使better-scroll内部的元素还是可被点击的
click: true
})
}
}
</script>
<style scoped>
</style>
此时我们只需要将需要滚动的部分放到
内部即可,但是还是要给这个标签一个高度,为了方便表示,考虑到上方的navbar是44px,下方的tabbar是49px,所以可以这样确定
.wrapper {
position: absolute;
top: 44px;
left: 0;
bottom: 49px;
right: 0;
}
不过我们前面做的tabControl的吸顶效果会失效,会在后面对这一效果进行修复
当我们向下浏览一会儿商品时,会出现一个回到顶部的小按钮,这个小按钮有两个功能,第一就是点击能回到0,0处,第二就是开始是没有的,到一定位置才会出现
创建一个BackTop组件,放在components/common/backtop
里面,它所用的图片放在assets/img/common/backtop.png
里面
其实这个组件内部只是对小按钮的样式做了设定而已,具体的逻辑操作还是由Scroll组件完成的,回到顶部调用的是BScroll对象的scrollTo方法
。根据位置决定小按钮的显示,首先要通过this.scroll.on('scroll',position => {})
监听滚动事件并获得位置(position.y是竖直方向位置,且是负数),再根据位置和v-show决定小按钮的显示。
上拉加载肯定还是要用Scroll组件实现的,我们首先要监听上拉加载事件,监听到已经拉完这一页后再完成后续操作
//监听上拉加载事件
this.scroll.on('pullingUp',() => {
this.$emit('pullUpLoad')
})
监听后,那肯定是要接收到第2页的商品数据,这时就要调用我们之前封装好的方法this.getHomeGoods_methods(this.showtype);
根据传入的showtype决定加载pop、new、sell哪个部分的商品数据
当用户持续翻阅的时候,肯定不会只加载一次,会持续加载,就在获取商品数据的方法最后加入这一行
this.$refs.scroll.scroll.finishPullUp();
由于有些界面不想上拉加载,所以可动态决定
pullUpLoad: this.pullUpLoad //为true时才能进行上拉加载事件
会产生的bug: 当多次上拉加载后,会出现每一次加载的数量不是30和不能滑动的现象。这都是用了better-scroll导致的,better-scroll 的 content 高度一定比 wrapper 高,这样 content 才能在 wrapper 里滚动。那么滚动高度其实就是 wrapper 高度减去 content 高度,在一开始,better-scroll 就会这样计算滚动高度。但是我们这里的 content 是有图片的,图片是异步加载的,所以当网络请求稍慢一点的时候,图片未加载完全,那么算出来的滚动高度和实际不符,就会出现bug了。。。那么其实只需要将better-scroll刷新一次,让它重新计算一遍即可,这个bug在后面解决。
原因在上面已经说过了,就是在异步加载图片时,可滑动高度已经被计算出来了。那么计算的滑动高度和图片加载后的实际滑动高度是不同的,所以会出现滑一部分滑不下去的情况。
解决方法呢就是监听图片的加载,每一次图片加载完成后就重新计算一次滑动高度。。嗯,,,的确有点低效,但是只会这个方法。
监听图片加载可用 @load="imgLoad"
,图片加载完后就会调用 imgLoad 方法。
理一下思路 ,在GoodsListItem里监听了图片加载后,我们要传到Home组件里,再在Home中通过this.$refs.roll.roll.refresh
调用roll组件内roll实例的refresh方法实现重新计算滑动高度
GoodsListItem 和 Home 不是父子组件,用 this.$emit() 一层一层传太麻烦,用vuex又没必要(vuex常用于复杂项目中),所以我们可以用事件总线(EventBus)
main.js中注册全局事件总线
// 在Vue原型上注册事件总线
Vue.prototype.$bus = new Vue()
GoodsListItem 中传出事件 this.$bus.$emit('itemImageLoad')
Home 中接收事件并完成相应操作(一定要在mounted里接收,因为this.$refs.scroll
查找了scroll标签)
this.$bus.$on('itemImageLoad', () => {
this.$refs.scroll.scroll.refresh()
})
上面为了解决bug,使得每加载一个图片就调用一次refresh,这是很影响性能的
所以我们可对其进行防抖处理,在Home的methods中定义防抖函数
它的流程就是,假如设置一个50ms的延迟时间。最开始timer是null,那么if(timer) clearTimeout(timer)
就不会执行,就会执行下面的异步操作。图片加载是很快的,当在执行异步操作时,第二次调用又来了,这次timer有值了,就会执行if(timer) clearTimeout(timer)
取消上一次还在执行的异步操作,再它进行异步操作。。。。。。等到最后的时候,其实就执行了一次,就起到了防抖作用。
// 防抖函数
debounce(func, delay) {
let timer = null
return function(...args) {
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
对原来的refresh函数进行防抖处理
const refresh = this.debounce(this.$refs.scroll.refresh,50)
this.$bus.$on('itemImageLoad', () => {
refresh()
})
当然考虑到在其他地方可能会用到防抖这样的优化性能的函数,我们可将其放在 common/utils/utils.js
中,导出调用即可
我们可发现在加入了better-scroll后,TabControl的吸顶效果已经不起作用了,因为position: stikey
是在浏览器默认滚动上才起效果的
完成吸顶效果呢主要思路肯定是,计算TabControl离顶部的距离(这个可以用元素的offsetTop属性)> 监听滚动位置 > 当滚动位置大于距离时,改变TabControl为fixed,使其固定在顶部
在Home中给tab-control设置ref属性,通过$refs拿到tab-control,但是组件是没有offsetTop属性的,所以需要拿到其内部的div再拿到offsetTop
this.$refs.tabControl.$el.offsetTop
将它放在mounted函数里面拿,但是这样拿到的offsetTop不是准确的,因为TabControl前面有轮播图、广告、推荐,它们里面都有图片,图片在未加载完全之前就拿到offsetTop肯定是错误的。通过分析,轮播图的图片是最能影响offsetTop的,所以要监听轮播图里面的图片加载,加载成功后再拿offsetTop,再将其保存到Home的data中的TabControlOffsetTop
轮播图图片有四张,意味着我们最终会拿到四个一模一样的TabControlOffsetTop,为了更高效,只拿一个数据,我们可在HomeSwiper组件中的图片加载方法中这样做
imageLoad() {
//只传一次
if(!this.isLoad) {
this.$emit('swiperImageLoad')
this.isLoad = !this.isLoad
}
}
现在已经拿到了真实的TabControlOffsetTop,照最开始的想法我们接下来就要利用之前拿到的(-position.y),让其和TabControlOffsetTop作比较从而决定tab-control何时改为fixed。但是我试过,这样做是有问题的,因为我们用了better-scroll,现在假如给tab-control用fixed,那么tab-control就会定在content的最顶部,而content会在wrapper里面不停滚动,那么tab-control就会突然消失。。。
所以我们应该换一种思路,在轮播图前面再用一个tab-control,给它设置relative,并且z-index,使其在 -position.y < TabControlOffsetTop
时不显示;在-position.y > TabControlOffsetTop
时显示,此时因为它relative了,还是占据原来的位置,所以它会一直占据顶部的位置,就达到效果了。
但是要注意,两个tab-control的ref属性的值应该不同,而且因为不知道用户在什么时候回点击,所以要保持两个组件的一致。要在点击事件中加入下面的代码来保证两个组件的点击状态一致。
//使两个tab-control保持一致
this.$refs.tabControl_1.activeIndex = index;
this.$refs.tabControl_2.activeIndex = index;
我们可发现,在主页中滑一段后,切换成分类什么的再切换回来的话,它会自动重新刷新一次,回到最开始未滑动的样子。这是因为切换成其他时,路由改变了,那么Home组件就会销毁,当切换回来时Home组件会被重新创建。
所以我们要使其不被销毁,就要使用keep-alive了,在App.vue中给router-view套一个keep-alive
,那么router-view现在所代表的组件就不会被销毁。
这时我们再进行切换,会发现切换回去后会回到之前的位置。但是切换多了以后会发现还是会回到最初的位置,这就是better-scroll带来的问题了。所以我们还要记录下来切换前的位置,将位置保存下来,切换回来后用scrollTo()
传到切换好的位置就行了。
//活跃状态时(切换回来)
activated() {
this.$refs.scroll.scrollTo(0, this.rememberPositionY, 1)
this.$refs.scroll.refresh()
},
//不活跃状态时(切走)
deactivated() {
//拿到竖直位置并将其保存下来
this.rememberPositionY = this.$refs.scroll.scroll.y
},
不过有两个需要注意的地方:
一是移动时间最好写1ms,写0的话容易处bug,有时会直接跳回顶部,可能是与backTop搞混了
二是切换回来后最好refresh一遍,这个能避免一些bug
在views下面新建文件夹detail,并新建组件Detail,还新建个文件夹childCpn,用于存放detail下自己独立使用的子组件
跳转的话那肯定要用路由,在 route/index.js
中注册detail路由。给GoodsListItem的图片添加点击事件,用this.$router.push()添加路径。
每个商品的详情页数据不同,我们肯定要传入一个id值,并让其在url上动态显示出来,那肯定想到用动态路由 this.$router.push('/detail/'+ this.goodsItem.iid)
,并且route/index.js
中也要改成动态路由 detail/:id
创建DetailNavBar组件,放到 views/detail/childCpn
中。在DetaiNavBar中使用之前封装好的navbar组件,并传入数据,调整样式什么的
要用到的返回的图片在assets/img/common/back.svg
中,点击后要用this.$router.back()
进行页面返回
封装一个detail.js,写到network中。像之前写的home.js一样写,不过要注意的是id对应的是接口上面的iid,这个要注意。
再就在Detail里面,调用detail.js传出的方法,拿到数据。
封装一个DetailSwiper,views/detail/childCpn/DetailSwiper.vue
,像HomeSwiper一样引用swiper插件,引用swiper.css文件,各种配置,从Detail中拿到详情页轮播图的数据res.result.itemInfo.topImages
,用v-for展示即可。图片过大可以给swiper-wrapper设置高度。
不过会有个bug,因为之前给整个路由都keep-alive了,所以created函数只会调用一次,所以会导致每次请求到的数据都一样。可以给keep-alive加个export="Detail"
,使Detail组件不会被缓存
可看出,大致分为四大块,标题区、价格区、销量什么什么、服务区,虽然看着数据不算多,但是其实这些数据很分散,所以我们可以考虑在network/detail.js
中做一层包装,封装一个类出来,在类中将要用的属性搞好即可调用
//商品基本信息
export class Goods {
//需要传入的参数
constructor(itemInfo, columns, services) {
this.title = itemInfo.title //标题
this.desc = itemInfo.desc //和标题差不多
this.newPrice = itemInfo.price //新价格
this.oldPrice = itemInfo.oldPrice //被划去的老价格
this.discount = itemInfo.discountDesc //打折信息:今日特价、7折什么的
this.discountBgColor = itemInfo.discountBgColor
this.columns = columns //销量、收藏在里面
this.services = services //剩下的4个都在里面
this.realPrice = itemInfo.lowNowPrice //真实价格,没有¥符号
}
}
新建一个DetailGoodsInfo组件,放在views/detail/childCpn
中。因为Goods新建实例的话要传入三个参数,那肯定需要在Detail中新建实例,建好后将实例用props传给DetailGoodsInfo,再在DetailGoodsInfo中调整样式显示
但是有需要注意的一点:
<div v-if="Object.keys(goods).length !== 0" class="info">
其实和写商品基本信息差不多,照猫画虎。基本上是一样的步骤,就是里面对数据处理方式和样式不同,比如这里面用了过滤器(filters),可以过滤数据
覆盖掉底部tabbar:给Detail整体进行相对定位、z-index、背景色
使用better-scroll:引入之前封装好的Scroll组件,用scroll套住需要滑动的元素,设置wrapper高度 `height: calc(100vh-44px);
使navbar一直在视窗顶部: 相对定位、z-index、背景色
新建组件DetailGoodsSpecific。
由图可知,大致分为两部分。第一部分是goods-desc,放在res.result.detailInfo.desc
里面;第二部分是goods-image,它又分为两小部分,第一是“穿着效果”这样的小标题,放在res.result.detailInfo.detailImage中每一项的key
,第二是图片,放在res.result.detailInfo.detailImage中每一项的list
我用了v-for的嵌套,再写样式即可。为了解决类似之前在主页出现过的滑动卡顿问题,解决办法一样,监听image的加载,调用scroll的refresh方法
即可。虽然这样会损失一点性能,但是假如用了防抖类似的手段使它只refresh一次的话,还是会卡顿、影响用户的体验,所以就只能损失点儿性能了。
新建DetaiParamInfo组件
可看出样式的话肯定用table表格布局。上面的尺码到衣长部分,在res.data.itemParams.rule.tables
里面,剩余的在res.data.itemParams.info.set
里面,有些在最顶部还有图片,在res.data.itemParams.info.images
里面。可看出数据还是不少且不集中,可以在detail.js中建个类,在Detail中统一引入即可
//商品配置信息
export class GoodsParam {
constructor(info, rule) {
// 注: images可能没有值(某些商品有值, 某些没有值)
this.image = info.images ? info.images[0] : '';
this.infos = info.set; //厚薄到潮流
this.sizes = rule.tables; //尺码到衣长
}
}
新建一个DetailCommentInfo组件
除了标题(用户评价)和点击按钮(更多)是写死的,用户头像、用户昵称、评论的话、时间(时间戳的形式)、商品大致信息、买家秀图片这些都是从后端接口请求下来的
res.result.rate
样式什么的就不再说了,这里最重要的是 时间戳转化为我们需要展示的时间的格式
这个是需要正则表达式的,不需要自己手写转化的函数,在我的common/utils/utils.js
里面
使用方法
//过滤器
filters: {
//将时间戳转为需要显示的日期格式
showDate(value) { //value就是需要过滤的东西
let date = new Date(value*1000); //时间戳单位是s,Date需要传入的是ms
return formatDate(date, 'yyyy-MM-dd')
}
}
推荐区的样式和我们之前在主页写过的GoodsList差不多,所以直接拿过来用就是了。不过也有几点不同要改一下。
第一是推荐区的数据接口和详情页的其他数据不同,要在detail.js中另写一个函数用来获取数据
//推荐部分的所有信息
export function getRecommend() {
return request({
url: '/recommend'
})
}
相应的给GoodsList传入的数组当然也不同,也要改。而且在GoodsListItem内部,Home中图片是在goodsItem.show.img
,而Detail中图片在goodsItem.image
return this.goodsItem.image || this.goodsItem.show.img
并且因为接口不支持,我就不准备给推荐区再做详情页了,所以每个item是不能点击的,那就加个if判断
itemClick() {
//动态路由
if(this.goodsItem.iid) {
this.$router.push('/detail/'+ this.goodsItem.iid)
}
}
在原生中可以用锚点,在better-scroll插件中当然就想到用scrollTo
方法。跳转当然要知道每一部分的位置,那就想到offsetTop
属性,既然要用这个属性肯定要先this.$refs.aaa.$el
获取组件下面的div
a. 获取每一部分的位置
在created()里获取位置,是获取不到的,连组件下面的div都找不到
在mounted()里,能获取到div,但是数据还没获取到
获取到数据的钩子也不行,dom还没渲染完
$nextTick()也不行,图片还未加载完,获取的位置不真实
所以在商品详情页中的图片
加载完后是最好的,不过商品详情页中有很多图片,加载一个获取一次的话肯定影响性能,所以又想到防抖(之前封装在common/utils/utils.js
里面)
created() {
//对获取位置做一层防抖封装
this.getPartsY = debounce(() => {
this.partsY = []
this.partsY.push(0)
this.partsY.push(this.$refs.param.$el.offsetTop)
this.partsY.push(this.$refs.comment.$el.offsetTop)
this.partsY.push(this.$refs.recommend.$el.offsetTop)
console.log(this.partsY);
})
}
goodsImageLoad() {
//真正的获取各部分位置
this.getPartsY()
},
b. 点击跳转
点击跳转就简单了,data中定义一个数组,讲0和后面三部分的位置都传到数组内,再讲scrollTo的第二个参数换成-(this.partsY[index] - 44)
即可,-44是为了对其,应该是navbar的原因
主要思路当然是监听滚动,获得滚动的y方向的值,让其和之前的this.partsY[index] - 44
做对比,所对应的if条件成立时,改变DetailNavBar中activeIndex的值。
但是这样做会很影响性能,每滚动一次就要对activeIndex改变一次值,所以我们可以在detai中定义个数据currentIndex = 0
,代码如下,就可使只改变一次activeIndex的值
//滚动事件
getPosition(position) {
const scrollPosY = -position.y
if(this.currentIndex !== 0 && scrollPosY >= 0 && scrollPosY < (this.partsY[1]-44)) {
this.currentIndex = 0
this.$refs.navbar.activeIndex = 0
}
if(this.currentIndex !== 1 && scrollPosY >= (this.partsY[1]-44) && scrollPosY < (this.partsY[2]-44)) {
this.currentIndex = 1
this.$refs.navbar.activeIndex = 1
}
if(this.currentIndex !== 2 && scrollPosY >= (this.partsY[2]-44) && scrollPosY < (this.partsY[3]-44)) {
this.currentIndex = 2
this.$refs.navbar.activeIndex = 2
}
if(this.currentIndex !== 3 && scrollPosY >= (this.partsY[3]-44)) {
this.currentIndex = 3
this.$refs.navbar.activeIndex = 3
}
}
新建一个组件,DetailBottomBar,这个组件挺好封装的,主要就是调样式。所用到的图片在assets/img/detail/detail_bottom.png
做好后你会发现,当滑动到最底部,DetailBottomBar会把上面的内容给盖住。其实就是因为我们之前算wrapper的高度时是用的 calc(100vh - 44px)
,没有把DetailBottomBar的高度也算进去,所以wrapper将DetailBottomBar的区域也当成了滑动区域,自然就会被盖住,将wrapper高度改成calc(100vh - 44px)
即可
照着主页来就是了
在详情页中,我们有个加入购物车的按钮,点击可将其加入购物车。那么我们就需要拿到该商品需要在购物车界面展示的数据。
//加入购物车
addCart() {
//当前商品需要传给购物车的信息总和
const addGood = {};
addGood.id = this.itemId
addGood.title = this.goods.title
addGood.desc = this.goods.desc
addGood.realPrice = this.goods.realPrice
addGood.image = this.swiperImage[0]
addGood.counter = 1
this.$store.dispatch('addToCart', addGood)
}
这些数据要从一个界面传到另一个界面,所以想到肯定需要用vuex做状态管理
,忘记了vuex可点击 这里 那就要下载vuex,配置index.js文件,再在main.js中挂载
我们需要在state中存入购物车包含商品的一个总的数组,用mutations中的方法向这个数组中添加元素。但是点击加入购物车按钮后有两类结果
,一类是购物车中没有此商品,点击按钮将商品添加进购物车;后一类时购物车按钮中有此商品,点击按钮会将商品的counter+1
根据最好使mutations中的一个方法只做一件事的原则
,我们可以根据上述两类情况定义两个mutations方法,在actions中进行逻辑判断,分情况上传事件到mutations中
具体代码可见src/store/mutations.js
和src/store/actions.js
用之前封装好的navbar即可
封装一个ShopCartList
组件,这个是添加进购物车的商品的展示区。在它里面可引用Scroll
组件管理滚动,再封装一个ShopCartListItem
对应具体的每一个商品。
数据肯定是要先传入ShopCartList
,再传入ShopCartListItem
里的,样式什么的都不说了,源码里面有注释
有几个知识点需要注意。
第一是在详情页添加数个商品到购物车后,在购物车中滑动会发现滑动不了得到情况,这其实跟之前遇到的划不动的情况一样。都是scroll内的内容的高度更新了,但是scroll本身没有意识到。所以我们可以在ShopCartList的activated回调函数中,调用scroll中的refresh函数,就可保证每次当购物车处于活跃状态时,都能重新计算高度
第二是,一般的计算属性computed只能使用它的值、不能改变它的值。
若是需要改变它的值,则就需要用下面的写法。
computed: {
// 计算属性的特殊写法,这样写才能改变计算属性的值,
// 一般写法只能调用值不能改变值
cartList: {
get() {
return this.$store.state.cartList
},
set() {
}
}
},
methods: {
removeItem(index) {
// splice方法删除指定项
this.cartList.splice(index, 1)
}
},
老师说记录点击状态的变量check要放在商品对象里,不能放在组件里,这样能方便监听每个对象的点击状态,那么就要在mutations方法里添加这个属性。
但是添加这个属性了,它更改这个属性又没有通过mutations方法更改属性,这样怎么监测的到呢??
可看出,check这个属性的值的确是改掉了,但是在detools中并未被检测到,这样做是不合理的,所以自然需要通过mutations方法来监测
在mutations中创建一个changeCheck方法,拿到state和index,进行取反操作即可。不过ShopCartListItem里面没有index,所以还要在ShopCartList里将index传给ShopCartListItem
创建一个ShopCartControl组件,放在ShopCartList下面,创建好后别忘了要更改scroll的wrapper的高度。
样式什么的自己写,合计的价格要记得将取到的价格先用parseFloat()化为小数
,然后才能相加,不然会得到NaN,旁边的去结算后面的数字(修改了,将它放到括号里面了)是已选中的商品的数量
全选的按钮分为两个主要逻辑
一是,只有购物车内所有商品都被选中后才可以,全选按钮才被选中。我之前的思维麻烦了,我是想监听每个商品的点击事件,在点击事件判断是否都被选中。其实不需要,只需要弄个计算属性记录全选按钮状态即可,在计算属性里面进行判断。
// 全选按钮的状态
isChecked (){
// 购物车为空时,按钮不选中
if(this.$store.state.cartList.length === 0) {
return false
}
return !(this.$store.state.cartList.filter(item => !item.check).length)
}
我用的是filter高阶函数
,它可以过滤指定内容,比如这里就是将没被选中的商品过滤出来,它们长度为0就返回true,反正返回false
二是,当所有商品都被选中时,点击全选按钮,所有商品都不被选中。若是只有部分商品或无商品被选中,点击全选按钮,所有商品都被选中。我一开始也想复杂了,我是想直接更改isChecked
,但是这样就乱了。所以可以遍历所有商品,对每个商品进行操作
// 决定所有商品的选中与否
selectAll() {
if(this.isChecked) {
for(let item of this.$store.state.cartList) {
item.check = false
}
}else {
for(let item of this.$store.state.cartList) {
item.check = true
}
}
}
toast这个东西在很多地方都会用到,但是它跟我们之前创建的组件不同。比如在我们这里我们点击加入购物车,会在窗口正中间弹出一个toast。
我们若和之前一样使用组件,那么就要各种引用组件,这样很麻烦,何况toast这种东西经常会使用,难道我们一有个地方要弹个东西出来,那就要引入组件?所以我们可以使用引用插件的方式,到时只需一条命令即可使用
我在这里用了别人写的一个轮子,轮子地址
等以后有时间了,学的够深了就自己写一个,哈哈
不过之前我们添加购物车这个点击事件是要dispatch
到actions里面的,actions里面的方法其实可以用promise做一层封装,那么我们就可以在点击事件后面写then方法,在then方法中调用toast
actions.js
export default {
addToCart(context, addGood) {
return new Promise((resolve, reject) => {
let oldCartList = null
for(let item of context.state.cartList) {
if(addGood.id === item.id) {
//将购物车中已有的那个商品赋值给oldCartList
//改变oldCartList也意味着改变购物车中已有的那个商品
oldCartList = item
}
}
//分情况调用对应的mutations方法
if(oldCartList) {
// 商品数量+1
context.commit('addItemCounter', oldCartList)
resolve('商品数量+1')
}else {
// 添加新的商品
context.commit('addToCart', addGood)
resolve('已添加该商品进购物车')
}
})
}
}
点击事件
addCart() {
//当前商品需要传给购物车的信息总和
const addGood = {};
addGood.id = this.itemId
addGood.title = this.goods.title
addGood.desc = this.goods.desc
addGood.realPrice = this.goods.realPrice
addGood.image = this.swiperImage[0]
addGood.counter = 1
//上传到actions中并完成后续操作
this.$store.dispatch('addToCart', addGood).then(res => {
this.$toast(res, 2000)
})
}
我目前将其分成了三大块,顶部的navbar
,左边的CategoryMenu
,右边的CategoryContent
(CategoryContent里面也有好几个部分)
顶部的navbar就不说了,直接拿封装好的,拿来用就是了,fixed,调整样式什么的。。
首先要注意的一点是,因为顶部的navbar fixed脱离文档流了,所以其下面的会被它盖住,所以我们要给整个Category设置一个padding-top: 44px;
好的,现在就要请求CategoryMenu的数据了,跟其他页面一样,需要封装一个category.js
,
import {request} from './request'
export function getCategory() {
return request({
url: '/category'
})
}
因为拿到的数据,多个组件都要用到,所以我是在Category里面拿的。created()
钩子里拿数据,再data中作保存,再用props传给CategoryMenu
再就是写样式了,其实整个Category(除去顶部的navbar)就是两栏布局,左边定宽,右边自适应。我用的是flex布局
,左边百分比定宽,右边flex: 1;
。activeIndex记录处于活跃的item的index,点击时this.activeIndex = index
,再绑定class,动态决定样式即可
而且左边的CategoryMenu是可以独立滑动的,不会影响到右边的内容,所以还是要套一个scroll,不要忘了给scroll的wrapper确定宽度
这一部分的数据是随着左边的CategoryMenu的点击决定的,且拿数据的接口也和左边的CategoryMenu不同
//右侧最顶部的动图链接部分
export function getSubcategory(maitKey) {
return request({
url: '/subcategory',
params: {
maitKey
}
})
}
maitKey是CategoryMenu中每个item都有的且各不相同的,和id差不多,所以我们可以根据maitKey来拿到对应的数据
maitKey是在CategoryMenu中获取的,但是CgContentTop要用到它,因为不是跨界面的数据传递,所以我没用vuex,我用的事件总线(具体用法可见写主页中的解决“偶尔不能滑动的bug”部分)
CategoryMenu中传出
itemClick(item, index) {
// 使用事件总线将maitKey传出
this.$bus.$emit('getMaitKey', item.maitKey)
this.activeIndex = index
}
CgContentTop中引入
// 使用事件总线拿到maitkey
this.$bus.$on('getMaitKey',(maitkey) => {
this.getSubcategory_methods(maitkey)
})
后续的对数据的提取、展示啊,就不细说了,可以看我的源码,源码里都写了详细的注释。布局也是用的flex
布局,也要用scroll支持滑动。
不过有两个部分需要说一下
:
1 首次进入导航时,要默认显示第一个item对应的内容。可以在created()中最先调用一次
created() {
//一开始就拿到第一个item的数据
this.getSubcategory_methods(3627)
// 使用事件总线拿到maitkey
this.$bus.$on('getMaitKey',(maitkey) => {
this.getSubcategory_methods(maitkey)
})
},
2 图片下的文字过多时会撑开item,影响整体布局,可以给p加个max-width
,我设置的值是图片的宽度,这样文字过多时可自动换行,不会影响整体布局
其实就是引用之前写好的tabcontrol就行了,不过要注意要传一些数据和传出一些事件什么的
首先是拿到商品的数据,这些商品的数据又跟上面不是同一个接口,所以需要在category.js中又建个函数
//右侧最底部的商品详情部分
export function getCategoryDetail(miniWallkey, type) {
return request({
url: '/subcategory/detail',
params: {
miniWallkey,
type
}
})
}
这个函数需要传入两个参数,miniWallkey和type,type就三类,就是我们需要传给tabcontrol的那三类,而miniWallkey和我们之前的maitkey一样是要从CategoryMenu中取过来的,所以方法和上面类似,不再多说。
取过来后的分析就是重点了,我以前想的是,tabcontrol传出的index,我可以传入到CategoryContent中,通过index拿到每个type对应的数据。但是这样不行,因为传过来的值不能随着tabcontrol的点击而改变。
所以我们可以在CategoryContent中将每个type的数据都拿到,保存到一个变量中,再由index决定拿变量中哪一类的数据,将这一类的数据传给CgContentDetail中即可。
重要的代码在下面,详细代码可看我的源码
getCategoryDetail_methods(miniWallkey, type) {
getCategoryDetail(miniWallkey, type).then(res => {
//做个判断,防止持续添加,...结构语法
if(this.goods[type].list.length === 0) {
this.goods[type].list.push(...res)
}else {
this.goods[type].list = [],
this.goods[type].list.push(...res)
}
})
}
},
created() {
// 将menu中第一个item对应的数据存进去,保证点击分类页就有商品显示
this.getCategoryDetail_methods(10062603, 'pop')
this.getCategoryDetail_methods(10062603, 'new')
this.getCategoryDetail_methods(10062603, 'sell')
this.$bus.$on('getMiniWallKey',(miniWallKey) => {
//将三类商品的数据都请求下来
this.getCategoryDetail_methods(miniWallKey, 'pop')
this.getCategoryDetail_methods(miniWallKey, 'new')
this.getCategoryDetail_methods(miniWallKey, 'sell')
})
}
就是写了个静态页面而已。。。
为解决这个问题我们需要用到fastclick插件,使用方法只有三步
1.下载 npm install fastclick --save
2.在main.js中引用 import FastClick from 'fastclick'
3.在main.js中使用 FastClick.attach(document.body)
有时我们的项目需要让图片懒加载(要展示这个图片的时候再加载它)
这时我们可以用个插件:vue-lazyload
传送门:vue-lazyout GitHub地址
在项目中我用了大量的px,和一部分的em,用px和em的话其实在不同的屏幕下它的适配性是很差的,会随着屏幕的改变而让我们原本写的样式很奇奇怪怪。所以当我们将项目打包发布时最好用一些插件将css单位统一转化为vw
这样的插件很多,搜都搜得到,不过安装的时候最好是npm install --save-dev
,因为别人用你的项目时是用不到你下载的这个依赖的,和babel(ES6转ES5)是一个道理。
打包后在浏览器运行index.html
这是因为css、js资源都未引用,是css、js文件的路径引用问题
在根目录下的vue.config.js
中添加如下代码
(vue cli2构建的项目可自己Google)
module.exports = {
// 解决打包后资源未引用的问题
publicPath: './',
}
能看见主页,但是点击购物车等页面后直接404了
这是路由的模式问题
在src/router/index.js
中将模式由 history 改为 hash
部分图片未显示,报错问题
这是因为打包后部分图片的src请求协议默认是用的file协议,不是用的http协议,我们需要在src前面加上 http:
例如:
<img :src="'http:' + item" v-for="(item, index) in commentInfo.images" :key="index">
由于改动的很多,附上可能需要改动的地方(每个人具体情况不同。按照你自己的项目报错来)
以下是根据我自己的项目报错来的
轮播图
如果你是用的老师封装的轮播图,那没事,你是自己封装的可能就有问题了
详情页