1、点击商品跳转到详情页
当我们点击GoodListItem中的每一个item,就跳转到对应的页面。我们首先要做的就是监听GoodsListItem的点击
在GoodsListItem组件中
<div class="goods-item" @click="itemClick">
在methods中跳转到对应的详情页
itemClick(){
//跳转到详情页
}
我们可以先给详情页配置一个路由
新建datail文件夹,新建Detail.vue,然后在router的index.js里面配置与路由相关的信息
itemClick(){
//跳转到详情页
this.$router.push('/detail')
}
但是我们在跳转详情页的时候还需要传递一些参数,需要把点击商品的id拿到,然后根据这个id去请求更加详细的数据。这个id也就是下面对应商品的iid
我们用动态路由的方式传递这个iid参数
itemClick(){
//跳转到详情页
// this.$router.push('/detail')
this.$router.push('/detail/'+this.goodsItem.iid)
}
然后在我们的Detail.vue中就要获取这个iid,根据这个iid去请求数据
data(){
return{
iid:null
}
}
created() {
//保存传入的id
this.iid=this.$route.params.iid
}
新建childComps,新建DetailNavBar.vue,将我们详情页的导航栏相关内容都写在此处
<template>
<div>
<nav-bar>
<div slot="left" @click="backClick" class="back">
<img src="~assets/img/common/back.svg" alt="" >
</div>
<div slot="center" class="title">
<div v-for="(item,index) in titles" class="title-item"
:class="{active:index===currentIndex}" @click="titleClick(index)"
>{{item}}</div>
</div>
</nav-bar>
</div>
</template>
<script>
import navBar from '@/components/common/navbar/NavBar';
export default {
name: 'DetailNavBar',
components:{
navBar
},
data(){
return {
titles:['商品','参数','评论','推荐'],
currentIndex:0
}
},
methods:{
titleClick(index){
this.currentIndex=index;
this.$emit('titleClick',index)
},
backClick(){
this.$router.go(-1)
//go(-1)相当于back
}
}
};
</script>
<style scoped>
.title {
display: flex;
font-size: 13px;
}
.title-item {
flex: 1;
}
.active {
color: var(--color-high-text)
}
.back img {
margin-top: 12px;
}
</style>
将该组件导入到Deatail组件中去
根据iid去请求数据,在network里新建detail.js
import { request } from "./request";
export function getDetail(iid) {
return request({
url: "/detail",
params: {
iid,
},
});
}
然后将这个getDetail方法导入到Detail.vue中,我们在created中打印请求到达数据
created() {
//保存传入的id
this.iid = this.$route.params.iid
//根据拿到的iid请求详细数据
getDetail(this.iid).then(res => {
console.log(res);
})
},
我们要拿到轮播图的数据,在data()中保存一个 topImages
data() {
return {
iid: null,
topImages:[]
}
},
getDetail(this.iid).then(res=>{
console.log(res);
this.topImages=res.result.itemInfo.topImages
}
得到下面结果:
新建DetailSwiper.vue,将轮播图相关的存到里面
先要接收轮播图的数据
props:{
topImages:{
type:Array,
default(){
return []
}
}
}
然后是在Detail.vue导入DetailSwiper.vue,注册,引用,并传值
<detail-swiper :top-images="topImages"></detail-swiper>
DetailSwiper.vue如下:
<template>
<swiper class="detail-swiper">
<swiper-item v-for="item in topImages">
<img :src="item" alt="">
</swiper-item>
</swiper>
</template>
<script>
import {Swiper, SwiperItem} from 'components/common/swiper'
export default {
name: 'DetailSwiper',
components:{
Swiper,
SwiperItem
},
props:{
topImages:{
type:Array,
default(){
return []
}
}
}
};
</script>
<style scoped>
.detail-swiper {
height: 300px;
overflow: hidden;
}
</style>
结果如下:
我们会发现一个问题:不管点击哪一个item,轮播图里面的内容没有发生变化
之所以造成这样的原因是因为我们的数据没有发生变化,也就是我们每次进入详情页,它里面的内容都keep-alive了,但是我们希望它每次进入都要重新获取iid,重新请求新的数据,所以我们在App.vue里面要将Detail排除在外
<div id="app">
<keep-alive exclude="Detail">
<router-view></router-view>
</keep-alive>
<main-tab-bar></main-tab-bar>
</div>
结果如下:
有点轮播图只显示一张图片,是因为它就只有一张图片显示。
这部分数据仍然在通过iid拿到的数据里面
因为需要拿到的数据很多,所以我们需要用一个对象把这些东西做一个整合。然后组件面向这一个对象就可以了。
在我们的detail.js中
//导出一个class
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.vue中,在data()里面定义一个空的goods对象
goods:{}
然后在我们的
created() {
getDetail(this.iid).then(res=>{
...
// 获取商品信息
this.goods=new Goods(data.itemInfo,data.columns,data.shopInfo.services)
拿到的都是我们想要的数据
我们在DetailBaseInfo.vue中将这些数据进行展示
<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">
<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>
然后在Detail.vue中将该组件进行导入,注册,并传值
<detail-base-info :goods="goods"></detail-base-info>
其结果如下:
重点是:对数据的整合。
在detail.js中也对这些数据进行一个整合
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中创建商铺信息,在这之前,在data里面新建一个shop信息
shop:{},
然后在created里面的getDetail函数里面,创建店铺信息对象
this.shop=new Shop(data.shopInfo)
可以看到我们已经拿到了shop对象了,下面我们就可以封装独立的组件了。新建DetailShopInfo.vue
<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>
将该组件在Detail.vue中注册,导入,传值
<detail-shop-info :shop="shop"></detail-shop-info>
结果:
商铺信息就可以正常展示了。
在详情页下面的首页导航栏就可以不用显示了。但是为什么它依然在我们的详情页展示呢?因为下面的这个tab-control脱离了标准流,它会覆盖到我们的标准流的上方。为了能让它盖上我们对样式进行调整。
我们给到detail组件一个id值
#detail {
position: relative;
z-index: 9999;
background-color: #fff;
/*height: 100vh;*/
}
但是我们又发现我们的头部导航栏没了,因为我们没有对原生的js做一个定位,这个时候我们直接用scroll局部滚动,头部导航栏就不用再动了。
首先再Detail.vue中导入scroll,注册,使用
import Scroll from '@/components/common/scroll/Scroll';
<scroll class="content" >
<detail-swiper :top-images="topImages"></detail-swiper>
<detail-base-info :goods="goods"></detail-base-info>
<detail-shop-info :shop="shop"></detail-shop-info>
</scroll>
用scroll将滚动区域包裹,并且设置高度
.content {
height: calc(100% - 44px);
}
此时这个100%是相对父元素而言的,所以要给父元素一个高度
#detail {
position: relative;
z-index: 9999;
background-color: #fff;
height: 100vh;
}
此时会发现头部导航栏并没有固定,所以我们还是为其添加样式
.detail-nav {
position: relative;
z-index: 9;
background-color: #fff;
}
最终的结果:
商品详情的数据都存放在哪呢?可以看出在detailImage的list里面
我们直接在Detail.vue中的data()里面新建一个数据
detailInfo:{},
在created()里面的 getDetail函数里面保存商品的详情数据
this.detailInfo=data.detailInfo
然后再封装一个独立的组件叫做DetailGoodsInfo.vue
<template>
<div v-if="Object.keys(detailInfo).length !== 0" class="goods-info">
<div class="info-desc clear-fix">
<div class="start">
</div>
<div class="desc">{{detailInfo.desc}}</div>
<div class="end"></div>
</div>
<div class="info-key">{{detailInfo.detailImage[0].key}}</div>
<div class="info-list">
<img v-for="(item, index) in detailInfo.detailImage[0].list" :key="index" :src="item" @load="imgLoad" alt="">
</div>
</div>
</template>
<script>
export default {
name: "DetailGoodsInfo",
props: {
detailInfo: {
type: Object
}
},
data() {
return {
counter: 0,
imagesLength: 0
}
},
methods: {
imgLoad() {
// 判断, 所有的图片都加载完了, 那么进行一次回调就可以了.
if (++this.counter === this.imagesLength) {
this.$emit('imageLoad');
}
}
},
//watch用来监听某个属性的变化
watch: {
detailInfo() {
// 获取图片的个数
this.imagesLength = this.detailInfo.detailImage[0].list.length
}
}
}
</script>
<style scoped>
.goods-info {
padding: 20px 0;
border-bottom: 5px solid #f2f5f8;
}
.info-desc {
padding: 0 15px;
}
.info-desc .start, .info-desc .end {
width: 90px;
height: 1px;
background-color: #a3a3a5;
position: relative;
}
.info-desc .start {
float: left;
}
.info-desc .end {
float: right;
}
.info-desc .start::before, .info-desc .end::after {
content: '';
position: absolute;
width: 5px;
height: 5px;
background-color: #333;
bottom: 0;
}
.info-desc .end::after {
right: 0;
}
.info-desc .desc {
padding: 15px 0;
font-size: 14px;
}
.info-key {
margin: 10px 0 10px 15px;
color: #333;
font-size: 15px;
}
.info-list img {
width: 100%;
}
</style>
将该组件导入到Detail.vue中,注册,并传值
<detail-goods-info :detail-info="detailInfo"></detail-goods-info>
结果如下:
我们会发现详情页滚不动的情况,原因是它在最早的时候better-scroll计算可滚动区域的时候只计算一部分高度,后来我们又加载了很多图片,它并没有把我们的图片算在内。
所以在我们的DetailGoodsInfo.vue里面对图片的加载进行了监听
methods: {
imgLoad() {
// 判断, 所有的图片都加载完了, 那么进行一次回调就可以了.
if (++this.counter === this.imagesLength) {
this.$emit('imageLoad');
}
}
},
上面的判断条件的意思是当加载之后的counter和我们图片的长度是相等的,也就是图片加载完了,就向外面发送一次imageLoad事件
然后在我们的Detail.vue中
<detail-goods-info :detail-info="detailInfo" @imageLoad="imageLoad"></detail-goods-info>
在方法里面
imageLoad(){
this.$refs.scroll.scroll.refresh()
},
在detail.js对参数信息做一个整合
export class GoodsParam {
constructor(info, rule) {
// 注: images可能没有值(某些商品有值, 某些没有值)
this.image = info.images ? info.images[0] : "";
this.infos = info.set;
this.sizes = rule.tables;
}
}
然后导入Detail.vue中.
在data()里面定义一个
paramInfo: {},
在到created里面获取参数信息
this. paramInfo=new GoodsParam(data.itemParams.info,data.itemParams.rule)
然后新建DetailParamInfo.vue
<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.vue里,注册,传值
<detail-param-info :param-info="paramInfo"></detail-param-info>
最后的结果如下: