5.详情页上
相关预处理及详情页路由思路
在项目views文件夹下新建一个Detail文件夹,并新建一个childComps子文件夹用来存放子组件,在Detail文件夹中新建Detail.vue文件
路由配置
添加detail相关路由,这里和之前添加的路由路径不太一样,因为我们要看每个商品的详情页面,所以我们需要传递具体商品的id号(该项目为iid)以便导航到该商品的详情页面,所以我们为其配置动态路由,如下代码所示
1
2
3
4{
path: '/detail/:iid',
component: Detail
}商品点击事件以及Detail.vue页面配置
为每个商品注册点击事件,以便跳转到该商品的详情界面,来到GoodsListItem文件,为最外围的div注册点击事件(因为点击该商品无论哪个位置都可以导航到商品详情页)
<div class="goods-list-item" @click="itemClick">
接着,实现点击跳转路由,这里,我们通过父组件传来的goodsItem属性拿到每个商品的iid,然后进行路由跳转
1
2
3itemClick() {
this.$router.push('/detail/'+this.goodsItem.iid)
}然后就是,Detaili组件了,初始化完组件后,我们为其定义一个iid数据用来存储当前的商品iid,在组件创建时即保存该id
1
2
3
4
5
6
7
8data() {
return {
iid: null
},
created() {
//保存传入的id
this.iid = this.$route.params.iid
},导航栏
从上到下解决页面布局,首先就是导航栏的设置,在childComps文件夹新建一个DetailNavBar文件
首先导入我们的NavBar组件
1
import NavBar from 'components/common/navbar/NavBar'
然后就是使用以及插槽定义,这里要展示的中间插槽需要多个数据,我们通过遍历定义的titles数据来渲染该插槽,并设置相关样式,这里使用flex布局
1
2
3
4
5
6
7
8<nav-bar>
<div slot="center" class="title">
<div class="title-item"
v-for="(item,index) in titles">
{{item}}
</div>
</div>
</nav-bar>1
2
3
4
5data() {
return {
titles: ['商品', '参数', '评论', '推荐'],
}
}1
2
3
4
5
6
7
8
9<style scoped>
.title {
display: flex;
font-size: 13px;
}
.title-item {
flex: 1;
}
</style>文字切换颜色样式改变
这里同之前的思路一样,遍历数据的同时将定义的currentIndex设置为当前点击的索引号,进行动态样式匹配,思路如下
data创建currentIndex数据
1
2
3
4
5
6data() {
return {
titles: ['商品', '参数', '评论', '推荐'],
currentIndex: 0
}
}为for遍历的每个div注册点击事件,每次点击,currentIndex设置为当前点击索引值
动态绑定class,判断当前index是否和currentIndex一致,一致,则展示active样式
1
2
3
4
5
6
7<div slot="center" class="title">
<div class="title-item"
v-for="(item,index) in titles"
@click="itemClick(index)"
:class="{active: index === currentIndex}">
{{item}}
</div>1
2
3
4
5methods: {
itemClick(index) {
this.currentIndex = index
}
}1
2
3.active {
color: var(--color-high-text)
}
返回按钮
这里很简单啦,就是在left插槽定义一个img,然后设置相关样式,同时绑定点击事件,每次点击跳转到上一个路由
$router.back()
具体代码如下1
2
3<div slot="left" class="back" @click="backClick">
<img src="~assets/img/common/back.svg" alt="">
</div>1
2
3backClick() {
this.$router.back()
}1
2
3.back img {
margin-top: 12px;
}商品数据请求以及轮播图展示
前面咱们保存过商品id,这个时候就要用到各个商品的id去请求对应的商品数据
首先,让我们去到network文件夹下,新建一个detail.js文件用于发送相关网络请求
导入之前封装好的request函数,导出相对应的getDeatil函数用于网络数据请求
1 | import { request } from './request.js' |
来到Deatil.vue这里,导入该模块的商品数据请求函数后,进行网络请求并保存相应数据,我们首先定义一个topImages用于保存轮播图图片数据
1 | data() { |
在组件创建(created)时,发送网络请求并保存该数据
1 | created() { |
然后就是轮播图展示了,我们先创建一个DetailSwiper组件,位置应该不用说了(childComps),导入Swiper以及SwiperItem组件,然后就是定义一个props用于接收父组件传下来的轮播图数据,接着就是在轮播图组件中遍历该图片数据并进行展示,代码如下
1 | <template> |
然后,就可以欣赏你的轮播图了🤭,但是,等等,球德玛蝶,为什么无论我点击哪个商品,最后出来的总是第一次点击的商品数据轮播图,这不合理啊,确实不合理,但仔细想想就会发现,其实你在之前做过了组件保留状态对不,那么在keep-alive包裹下的组件都会保留之前的状态,那么,怎么避免详情页保留状态呢,你可以到App.vue中将不需要保留的组件按下格式定义属性
1 | <keep-alive exclude="Detail"> |
商品基本信息
来到了对商品基本信息进行展示的环节了,在这里,我们首先要对接口的数据进行分析,分析完后在进行相关的展示
因为接口里面的数据比较混乱,分布在各个对象或者数组中,不要疑惑,因为在真实工作中你就可能会遇到该问题,然后就是解决办法,第一个就是你可以在每次渲染数据时找到各个数据的具体位置,然后渲染,但是难免使得代码看起来有点难阅读,所以这里采用第二个办法——对其进行整合封装
来到detaili.js文件,我们首先定义一个类Goods,接着使用构造函数,使得每次你请求数据时,直接new一个对象并传入指定的数据位置,然后再得到各个更佳细化的数据属性,代码如下(根据查阅接口一次性封装)
1 | export class Goods { |
然后就是在Detail组件中去保存该数据了,记得先引入该类,在获取商品数据后初始化该对象并保存在自定义的goods值中
1 | //获取商品数据 |
然后就是创建DetailBaseInfo组件,相关代码如下,其中大多是之前的知识点,这里不再赘述,主要就是有两个需要注意的知识点
v-for
可以对数字进行遍历,然后再去取到相应的数据1
2
3
4
5<!-- v-for也可以遍历数字,通过遍历数字获得需要的商品数据 -->
<span class="info-service-item" v-for="index in goods.services.length-1" :key="index">
<img :src="goods.services[index-1].icon">
<span>{{goods.services[index-1].name}}</span>
</span>使用v-if进行数据判断,数据存在与否再决定该组件是否渲染
1
2
3<div v-if="Object.keys(goods).length !== 0" class="base-info">
...
</div>完整代码
1 | <template> |
商家(店铺)信息展示
同对请求商品基本信息一致,对请求的数据进行封装
1 | export class Shop { |
然后就是在Detail.vue引入该类名,并在getDetail(this.iid)后保存该类数据
1 | //获取商家信息 |
接着就是创建对应展示的组件–DetailShopInfo.vue,和之前一样,没啥可说滴,样式挺难调的
1 | <template> |
详情页引入该组件并传入对应的数据
1 | <detail-shop-info :shop="shop"></detail-shop-info> |
好的,到这里你会发现,各个类型的数据展示逻辑都是差不多一致的,我们来总结一下,如何将需要的各个接口数据进行按类型展示
- 看接口所要展示的数据是否复杂
- 复杂:在网络请求函数中封装类,进行数据整合,再在相关页面请求数据后初始化该对象并定义对应的data接收
- 不复杂:直接在相关页面定义data直接在请求数据后进行接收
- 创建对应的子组件:根据业务需求定义样式
- 导入该组件并将需要的数据传入(子组件props,父组件:data=”data”)
取消底部tab-bar
详情页是不需要展示底部栏的,如何取消掉这个底部栏呢,这里有两个方法,重点讲第一个方法,推荐使用第二个方法(虽然我还没试过)
css样式
很简单,就是在Detail.vue设置样式,让它覆盖掉底部栏,简单粗暴
1
2
3
4
5#detail {
position: relative;
z-index: 9;
background-color: #fff;
}
引入Scroll
这和在做首页的情况一样,除了顶部导航栏外,其它都要在Scroll的包裹下,所以我们引入Scroll组件,并设置样式,首先,当然就是给高度啦,必须给,给Scroll定义类名,并给予高度,这里使用计算值得出高度
1 | .wrapper { |
看到注释了有没有,所以我们还要给其父元素定义高度,
1 | #detail { |
然后就是让导航栏固定在顶部
1 | .detail-nav { |
这里自己的css有一点遗忘,相对定位与绝对定位的区别,在查阅了相关资料后进行了知识回冲——菜鸟教程
商品详细数据
按照前面的思路,首先对接口数据进行查阅,发现该接口数据相对简单,所以直接在Detail组件中进行存储
1 | //获取商品数据 |
接着就是创建对应展示的组件–DetailGoodsInfo.vue,和之前一样,没啥可说滴,但这里有一个知识点要提,就是,因为这里图片同样很多,和首页遇到的问题一样,你可能刷到一半就卡住不动了,所以我们要对这些图片数据加载完后进行scroll刷新,但是图片很多,如果每次在监听加载完一次图片就进行刷新,会不会很浪费性能,造成更大的内存占用,答案是肯定的,所以我们就等待全部图片加载完再进行刷新,有同学可能要问了,这里是否可以使用防抖,可以的,但是在网络较慢的情况下可能也会刷新多次,但是思路也可行,这里也主要讲另外一种解决方法
Vue.js 有一个方法 watch,它可以用来监测Vue实例上的数据变动。
利用这个特性,我们动态监听接收到的数据,然后对图片加载进行监听,判断图片是否加载完了,加载完则为其执行一次刷新
思路如下
首先定义两个值,一个存储当前加载的图片数量,一个存储需要加载的图片总数
1 | data() { |
定义监听函数,保存当前需要加载的图片总数
1 | watch: { |
监听图片加载
1 | <img v-for="(item, index) in detailInfo.detailImage[0].list" :key="index" :src="item" @load="imgLoad" alt=""> |
监听函数具体实现,每次加载完一张图片,counter便自增一后再与总图片数进行匹配,如一样,发送该事件,如不一样,继续自增,直到全部加载完
1 | methods: { |
在detail进行刷新
首先绑定该事件
1 | <detail-goods-info :detailInfo="detailInfo" @imageLoad="imageLoad"></detail-goods-info> |
然后很简单,就是执行一次刷新
1 | methods: { |
OK,finish,但是,这里,如果用户刷新很快的话,会出现短暂的卡顿,原因差不多,就是图片还没加载完,但至少不会全都刷不出,所以在这里,如果追求用户体验的话,不追求性能的话,你可以每次图片加载就刷新一次,如果还是以性能优先,则直接全部加载完再刷新
其它的设置就同前面的一致啦,相关组件导入,数据传递等
参数数据
不再赘述,这里在查看完接口后决定进行数据封装,其他步骤一致
数据封装
1
2
3
4
5
6
7
8export class GoodsParam {
constructor(info, rule) {
// 注: images可能没有值(某些商品有值, 某些没有值)
this.image = info.images ? info.images[0] : '';
this.infos = info.set;
this.sizes = rule.tables;
}
}获取商品参数数据
1
2
3
4
5
6//获取商品数据
getDetail(this.iid).then(res => {
const data = res.result
//获取商品参数数据
this.paramInfo = new GoodsParam(data.itemParams.info, data.itemParams.rule)
})创建组件(DetailParamIfo)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72<template>
<div class="param-info" v-if="Object.keys(paramInfo).length !== 0">
<table v-for="(table, index) in paramInfo.sizes"
class="info-size" :key="index">
<tr v-for="(tr, indey) in table" :key="indey">
<td v-for="(td, indez) in tr" :key="indez">{{td}}</td>
</tr>
</table>
<table class="info-param">
<tr v-for="(info, index) in paramInfo.infos">
<td class="info-param-key">{{info.key}}</td>
<td class="param-value">{{info.value}}</td>
</tr>
</table>
<div class="info-img" v-if="paramInfo.image.length !== 0">
<img :src="paramInfo.image" alt="">
</div>
</div>
</template>
<script>
export default {
name: "DetailParamInfo",
props: {
paramInfo: {
type: Object,
default() {
return {}
}
}
}
}
</script>
<style scoped>
.param-info {
padding: 20px 15px;
font-size: 14px;
border-bottom: 5px solid #f2f5f8;
}
.param-info table {
width: 100%;
border-collapse: collapse;
}
.param-info table tr {
height: 42px;
}
.param-info table tr td {
border-bottom: 1px solid rgba(100,100,100,.1);
}
.info-param-key {
/*当value的数据量比较大的时候, 会挤到key,所以给一个固定的宽度*/
width: 95px;
}
.info-param {
border-top: 1px solid rgba(0,0,0,.1);
}
.param-value {
color: #eb4868
}
.info-img img {
width: 100%;
}
</style>detail引入组件并传递数据
1
<detail-param-info :param-info="paramInfo"></detail-param-info>
业务难点(题外话)
代码组织问题
业务逻辑
- 理清逻辑(流程图+解决方案)
- 书写代码
- 功能实现
bug处理