相关预处理及详情页路由思路

  1. 在项目views文件夹下新建一个Detail文件夹,并新建一个childComps子文件夹用来存放子组件,在Detail文件夹中新建Detail.vue文件

  2. 路由配置

    添加detail相关路由,这里和之前添加的路由路径不太一样,因为我们要看每个商品的详情页面,所以我们需要传递具体商品的id号(该项目为iid)以便导航到该商品的详情页面,所以我们为其配置动态路由,如下代码所示

    1
    2
    3
    4
    {
    path: '/detail/:iid',
    component: Detail
    }
  3. 商品点击事件以及Detail.vue页面配置

    为每个商品注册点击事件,以便跳转到该商品的详情界面,来到GoodsListItem文件,为最外围的div注册点击事件(因为点击该商品无论哪个位置都可以导航到商品详情页)<div class="goods-list-item" @click="itemClick">

    接着,实现点击跳转路由,这里,我们通过父组件传来的goodsItem属性拿到每个商品的iid,然后进行路由跳转

    1
    2
    3
    itemClick() {
    this.$router.push('/detail/'+this.goodsItem.iid)
    }

    然后就是,Detaili组件了,初始化完组件后,我们为其定义一个iid数据用来存储当前的商品iid,在组件创建时即保存该id

    1
    2
    3
    4
    5
    6
    7
    8
       data() {
    return {
    iid: null
    },
    created() {
    //保存传入的id
    this.iid = this.$route.params.iid
    },

    导航栏

从上到下解决页面布局,首先就是导航栏的设置,在childComps文件夹新建一个DetailNavBar文件

  1. 首先导入我们的NavBar组件

    1
    import NavBar from 'components/common/navbar/NavBar'
  2. 然后就是使用以及插槽定义,这里要展示的中间插槽需要多个数据,我们通过遍历定义的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
    5
    data() {
    return {
    titles: ['商品', '参数', '评论', '推荐'],
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <style scoped>
    .title {
    display: flex;
    font-size: 13px;
    }
    .title-item {
    flex: 1;
    }
    </style>
  3. 文字切换颜色样式改变

    这里同之前的思路一样,遍历数据的同时将定义的currentIndex设置为当前点击的索引号,进行动态样式匹配,思路如下

    1. data创建currentIndex数据

      1
      2
      3
      4
      5
      6
      data() {
      return {
      titles: ['商品', '参数', '评论', '推荐'],
      currentIndex: 0
      }
      }
    2. 为for遍历的每个div注册点击事件,每次点击,currentIndex设置为当前点击索引值

    3. 动态绑定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
      5
      methods: {
      itemClick(index) {
      this.currentIndex = index
      }
      }
      1
      2
      3
      .active {
      color: var(--color-high-text)
      }
  4. 返回按钮

    这里很简单啦,就是在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
    3
    backClick() {
    this.$router.back()
    }
    1
    2
    3
    .back img {
    margin-top: 12px;
    }

    商品数据请求以及轮播图展示

前面咱们保存过商品id,这个时候就要用到各个商品的id去请求对应的商品数据

首先,让我们去到network文件夹下,新建一个detail.js文件用于发送相关网络请求

导入之前封装好的request函数,导出相对应的getDeatil函数用于网络数据请求

1
2
3
4
5
6
7
8
9
10
import { request } from './request.js'

export function getDetail(iid) {
return request({
url: '/api/h8/detail',
params: {
iid
}
})
}

来到Deatil.vue这里,导入该模块的商品数据请求函数后,进行网络请求并保存相应数据,我们首先定义一个topImages用于保存轮播图图片数据

1
2
3
4
5
6
data() {
return {
iid: null,
topImages: [],
}
},

在组件创建(created)时,发送网络请求并保存该数据

1
2
3
4
5
6
7
8
9
10
created() {
//保存传入的id
this.iid = this.$route.params.iid
//获取商品数据
getDetail(this.iid).then(res => {
const data = res.result
//获取顶部图片数据
this.topImages = data.itemInfo.topImages
})
},

然后就是轮播图展示了,我们先创建一个DetailSwiper组件,位置应该不用说了(childComps),导入Swiper以及SwiperItem组件,然后就是定义一个props用于接收父组件传下来的轮播图数据,接着就是在轮播图组件中遍历该图片数据并进行展示,代码如下

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
<template>
<swiper class="detail-swiper">
<swiper-item v-for="item in topImages">
<img :src="item">
</swiper-item>
</swiper>
</template>

<script>
import {Swiper, SwiperItem} from 'components/common/swiper'
export default {
name: 'DetailSwipe',
components: {
Swiper,
SwiperItem
},
props:{
topImages: {
type: Array,
default() {
return []
}
}
}
}
</script>

<style scoped>
.detail-swiper {
height: 300px;
overflow: hidden;
}
</style>

然后,就可以欣赏你的轮播图了🤭,但是,等等,球德玛蝶,为什么无论我点击哪个商品,最后出来的总是第一次点击的商品数据轮播图,这不合理啊,确实不合理,但仔细想想就会发现,其实你在之前做过了组件保留状态对不,那么在keep-alive包裹下的组件都会保留之前的状态,那么,怎么避免详情页保留状态呢,你可以到App.vue中将不需要保留的组件按下格式定义属性

1
<keep-alive exclude="Detail">

商品基本信息

来到了对商品基本信息进行展示的环节了,在这里,我们首先要对接口的数据进行分析,分析完后在进行相关的展示

因为接口里面的数据比较混乱,分布在各个对象或者数组中,不要疑惑,因为在真实工作中你就可能会遇到该问题,然后就是解决办法,第一个就是你可以在每次渲染数据时找到各个数据的具体位置,然后渲染,但是难免使得代码看起来有点难阅读,所以这里采用第二个办法——对其进行整合封装

来到detaili.js文件,我们首先定义一个类Goods,接着使用构造函数,使得每次你请求数据时,直接new一个对象并传入指定的数据位置,然后再得到各个更佳细化的数据属性,代码如下(根据查阅接口一次性封装)

1
2
3
4
5
6
7
8
9
10
11
12
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
this.columns = columns
this.services = services
this.realPrice = itemInfo.lowNowPrice
}
}

然后就是在Detail组件中去保存该数据了,记得先引入该类,在获取商品数据后初始化该对象并保存在自定义的goods值中

1
2
3
4
5
6
7
8
9
//获取商品数据
getDetail(this.iid).then(res => {
const data = res.result
//获取顶部图片数据
this.topImages = data.itemInfo.topImages

//获取商品基本信息数据
this.goods = new Goods(data.itemInfo, data.columns, data.shopInfo.services)
})

然后就是创建DetailBaseInfo组件,相关代码如下,其中大多是之前的知识点,这里不再赘述,主要就是有两个需要注意的知识点

  1. 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>
  2. 使用v-if进行数据判断,数据存在与否再决定该组件是否渲染

    1
    2
    3
     <div v-if="Object.keys(goods).length !== 0" class="base-info">
    ...
    </div>

    完整代码

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<template>
<div v-if="Object.keys(goods).length !== 0" class="base-info">
<div class="info-title">{{goods.title}}</div>
<div class="info-price">
<span class="n-price">{{goods.newPrice}}</span>
<span class="o-price">{{goods.oldPrice}}</span>
<span v-if="goods.discount" class="discount">{{goods.discount}}</span>
</div>
<div class="info-other">
<span>{{goods.columns[0]}}</span>
<span>{{goods.columns[1]}}</span>
<span>{{goods.services[goods.services.length-1].name}}</span>
</div>
<div class="info-service">
<!-- 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>
</div>
</div>
</template>

<script>
export default {
name: "DetailBaseInfo",
props: {
goods: {
type: Object,
default() {
return {}
}
}
}
}
</script>

<style scoped>
.base-info {
margin-top: 15px;
padding: 0 8px;
color: #999;
border-bottom: 5px solid #f2f5f8;
}

.info-title {
color: #222
}

.info-price {
margin-top: 10px;
}

.info-price .n-price {
font-size: 24px;
color: var(--color-high-text);
}

.info-price .o-price {
font-size: 13px;
margin-left: 5px;
text-decoration: line-through;
}

.info-price .discount {
font-size: 12px;
padding: 2px 5px;
color: #fff;
background-color: var(--color-high-text);
border-radius: 8px;
margin-left: 5px;

/*让元素上浮一些: 使用相对定位即可*/
position: relative;
top: -8px;
}

.info-other {
margin-top: 15px;
line-height: 30px;
display: flex;
font-size: 13px;
border-bottom: 1px solid rgba(100,100,100,.1);
justify-content: space-between;
}

.info-service {
display: flex;
justify-content: space-between;
line-height: 60px;
}

.info-service-item img {
width: 14px;
height: 14px;
position: relative;
top: 2px;
}

.info-service-item span {
font-size: 13px;
color: #333;
}
</style>

商家(店铺)信息展示

同对请求商品基本信息一致,对请求的数据进行封装

1
2
3
4
5
6
7
8
9
10
export class Shop {
constructor(shopInfo) {
this.logo = shopInfo.shopLogo;
this.name = shopInfo.name;
this.fans = shopInfo.cFans;
this.sells = shopInfo.cSells;
this.score = shopInfo.score;
this.goodsCount = shopInfo.cGoods
}
}

然后就是在Detail.vue引入该类名,并在getDetail(this.iid)后保存该类数据

1
2
//获取商家信息
this.shop = new Shop(data.shopInfo)

接着就是创建对应展示的组件–DetailShopInfo.vue,和之前一样,没啥可说滴,样式挺难调的

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
<template>
<div class="shop-info">
<div class="shop-top">
<img :src="shop.logo">
<span class="title">{{shop.name}}</span>
</div>
<div class="shop-middle">
<div class="shop-middle-item shop-middle-left">
<div class="info-sells">
<div class="sells-count">
{{shop.sells | sellCountFilter}}
</div>
<div class="sells-text">总销量</div>
</div>
<div class="info-goods">
<div class="goods-count">
{{shop.goodsCount}}
</div>
<div class="goods-text">全部宝贝</div>
</div>
</div>
<div class="shop-middle-item shop-middle-right">
<table>
<tr v-for="(item, index) in shop.score" :key="index">
<td>{{item.name}}</td>
<td class="score" :class="{'score-better': item.isBetter}">{{item.score}}</td>
<td class="better" :class="{'better-more': item.isBetter}"><span>{{item.isBetter ? '高':'低'}}</span></td>
</tr>
</table>
</div>
</div>
<div class="shop-bottom">
<div class="enter-shop">进店逛逛</div>
</div>
</div>
</template>

<script>
export default {
name: "DetailShopInfo",
props: {
shop: {
type: Object,
default() {
return {}
}
}
},
filters: {
sellCountFilter: function (value) {
if (value < 10000) return value;
return (value/10000).toFixed(1) + '万'
}
}
}
</script>

<style scoped>
.shop-info {
padding: 25px 8px;
border-bottom: 5px solid #f2f5f8;
}

.shop-top {
line-height: 45px;
/* 让元素垂直中心对齐 */
display: flex;
align-items: center;
}

.shop-top img {
width: 45px;
height: 45px;
border-radius: 50%;
border: 1px solid rgba(0,0,0,.1);
}

.shop-top .title {
margin-left: 10px;
vertical-align: center;
}

.shop-middle {
margin-top: 15px;
display: flex;
align-items: center;
}

.shop-middle-item {
flex: 1;
}

.shop-middle-left {
display: flex;
justify-content: space-evenly;
color: #333;
text-align: center;
border-right: 1px solid rgba(0,0,0,.1);
}

.sells-count, .goods-count {
font-size: 18px;
}

.sells-text, .goods-text {
margin-top: 10px;
font-size: 12px;
}

.shop-middle-right {
font-size: 13px;
color: #333;
}

.shop-middle-right table {
width: 120px;
margin-left: 30px;
}

.shop-middle-right table td {
padding: 5px 0;
}

.shop-middle-right .score {
color: #5ea732;
}

.shop-middle-right .score-better {
color: #f13e3a;
}

.shop-middle-right .better span {
background-color: #5ea732;
color: #fff;
text-align: center;
}

.shop-middle-right .better-more span {
background-color: #f13e3a;
}

.shop-bottom {
text-align: center;
margin-top: 10px;
}

.enter-shop {
display: inline-block;
font-size: 14px;
background-color: #f2f5f8;
width: 150px;
height: 30px;
text-align: center;
line-height: 30px;
border-radius: 10px;
}
</style>

详情页引入该组件并传入对应的数据

1
<detail-shop-info :shop="shop"></detail-shop-info>

好的,到这里你会发现,各个类型的数据展示逻辑都是差不多一致的,我们来总结一下,如何将需要的各个接口数据进行按类型展示

  1. 看接口所要展示的数据是否复杂
    • 复杂:在网络请求函数中封装类,进行数据整合,再在相关页面请求数据后初始化该对象并定义对应的data接收
    • 不复杂:直接在相关页面定义data直接在请求数据后进行接收
  2. 创建对应的子组件:根据业务需求定义样式
  3. 导入该组件并将需要的数据传入(子组件props,父组件:data=”data”)

取消底部tab-bar

详情页是不需要展示底部栏的,如何取消掉这个底部栏呢,这里有两个方法,重点讲第一个方法,推荐使用第二个方法(虽然我还没试过)

  1. css样式

    很简单,就是在Detail.vue设置样式,让它覆盖掉底部栏,简单粗暴

    1
    2
    3
    4
    5
    #detail {
    position: relative;
    z-index: 9;
    background-color: #fff;
    }
  2. router相关设置

引入Scroll

这和在做首页的情况一样,除了顶部导航栏外,其它都要在Scroll的包裹下,所以我们引入Scroll组件,并设置样式,首先,当然就是给高度啦,必须给,给Scroll定义类名,并给予高度,这里使用计算值得出高度

1
2
3
4
5
.wrapper {
/* 腹肌元素需要有高度 */
height: calc(100% - 44px);
}

看到注释了有没有,所以我们还要给其父元素定义高度,

1
2
3
#detail {
height: 100vh;
}

然后就是让导航栏固定在顶部

1
2
3
4
5
.detail-nav {
position: relative;
z-index: 9;
background-color: #fff;
}

这里自己的css有一点遗忘,相对定位与绝对定位的区别,在查阅了相关资料后进行了知识回冲——菜鸟教程

商品详细数据

按照前面的思路,首先对接口数据进行查阅,发现该接口数据相对简单,所以直接在Detail组件中进行存储

1
2
3
4
5
6
//获取商品数据
getDetail(this.iid).then(res => {
const data = res.result
//获取商品详细数据
this.detailInfo = data.detailInfo
})

接着就是创建对应展示的组件–DetailGoodsInfo.vue,和之前一样,没啥可说滴,但这里有一个知识点要提,就是,因为这里图片同样很多,和首页遇到的问题一样,你可能刷到一半就卡住不动了,所以我们要对这些图片数据加载完后进行scroll刷新,但是图片很多,如果每次在监听加载完一次图片就进行刷新,会不会很浪费性能,造成更大的内存占用,答案是肯定的,所以我们就等待全部图片加载完再进行刷新,有同学可能要问了,这里是否可以使用防抖,可以的,但是在网络较慢的情况下可能也会刷新多次,但是思路也可行,这里也主要讲另外一种解决方法

Vue.js 有一个方法 watch,它可以用来监测Vue实例上的数据变动。

利用这个特性,我们动态监听接收到的数据,然后对图片加载进行监听,判断图片是否加载完了,加载完则为其执行一次刷新

思路如下

首先定义两个值,一个存储当前加载的图片数量,一个存储需要加载的图片总数

1
2
3
4
5
6
data() {
return {
counter: 0,
imagesLength: 0
}
},

定义监听函数,保存当前需要加载的图片总数

1
2
3
4
5
6
watch: {
detailInfo() {
// 获取图片的个数
this.imagesLength = this.detailInfo.detailImage[0].list.length
}
}

监听图片加载

1
<img v-for="(item, index) in detailInfo.detailImage[0].list" :key="index" :src="item" @load="imgLoad" alt="">

监听函数具体实现,每次加载完一张图片,counter便自增一后再与总图片数进行匹配,如一样,发送该事件,如不一样,继续自增,直到全部加载完

1
2
3
4
5
6
7
8
methods: {
imgLoad() {
// 判断, 所有的图片都加载完了, 那么进行一次回调就可以了.
if (++this.counter === this.imagesLength) {
this.$emit('imageLoad');
}
}
},

在detail进行刷新

首先绑定该事件

1
<detail-goods-info :detailInfo="detailInfo" @imageLoad="imageLoad"></detail-goods-info>

然后很简单,就是执行一次刷新

1
2
3
4
5
methods: {
imageLoad() {
this.$refs.scroll.refresh()
}
}

OK,finish,但是,这里,如果用户刷新很快的话,会出现短暂的卡顿,原因差不多,就是图片还没加载完,但至少不会全都刷不出,所以在这里,如果追求用户体验的话,不追求性能的话,你可以每次图片加载就刷新一次,如果还是以性能优先,则直接全部加载完再刷新

其它的设置就同前面的一致啦,相关组件导入,数据传递等

参数数据

不再赘述,这里在查看完接口后决定进行数据封装,其他步骤一致

  1. 数据封装

    1
    2
    3
    4
    5
    6
    7
    8
    export class GoodsParam {
    constructor(info, rule) {
    // 注: images可能没有值(某些商品有值, 某些没有值)
    this.image = info.images ? info.images[0] : '';
    this.infos = info.set;
    this.sizes = rule.tables;
    }
    }
  2. 获取商品参数数据

    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)
    })
  3. 创建组件(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>

  4. detail引入组件并传递数据

    1
    <detail-param-info :param-info="paramInfo"></detail-param-info>

    业务难点(题外话)

  5. 代码组织问题

  6. 业务逻辑

    1. 理清逻辑(流程图+解决方案)
    2. 书写代码
    3. 功能实现
  7. bug处理