Vue.js

Vue.js第8课-项目实战-旅游网站详情页面开发(part01

2019-06-02  本文已影响34人  e20a12f8855d

一、详情页动态路由及 banner 布局

这一章来看一下详情页的动态路由及 banner 布局。首选新建一个分支 detail-banner 并且切换到这个分支进行开发。

首先打开 home/recommend.vue,我们把循环项改为 router-link 标签的形式,然后通过 to 给元素加路由地址,为了让每个页面的详情页地址不一样,这里我们使用动态路由,就是在 to 前面加一个冒号,地址后面跟一个 item.id 这样一个参数,这样每个详情页的地址就是动态的了,例:

home/recommend.vue

<router-link class="rl_li border-bottom" v-for="item of list" :key="item.id" :to="'/detail/' + item.id">
    <div class="a">
        <div class="pic">
        <img :src="item.imgUrl" alt class="img">
        </div>
        <div class="info">
        <div class="tit">{{item.infoTit}}</div>
        <div class="txt">{{item.infoTxt}}</div>
        <div class="money">
            <b class="b">¥</b>
            <i class="i">{{item.infoMoney}}</i>起
        </div>
        </div>
    </div>
</router-link>

这个时候打开页面,点击城市,看地址栏 url 的变化,不同城市的详情页地址也不同。

补充,router-link 会自动把标签渲染为 a 标签,如果不想让他渲染为 a 标签,就给他加一个 tag 属性,里边写上标签名,例如:tag="li",意思是让这个 router-link 渲染为 li 标签。

现在我们还没有进行路由的配置,接下来我们添加一个路由。首先在 pages 下新建一个 detail 目录用来存放详情页的组件,然后在 detail 目录下新建一个 Detail.vue 组件,里面编写一些内容,例:

Detail.vue

<template>
    <div>
        detail
    </div>
</template>

<script>
export default {
    name : "detail"
}
</script>

<style lang="stylus" scoped>

</style>

接下来就可以去 router 中添加这个页面的路由信息了,打开 router/index.js,首先通过 import 引入 Detail.vue 这个组件,然后给他配置路由信息,这个时候我们知道了 detail 这个路径后面还要跟一个参数,所以在路径后要跟一个动态路由,在 Vue 中,通过 /路径/:id 的形式我们就写了一个动态路由,他的意思是,前面的路径必须是 /detail/,后面要带一个参数,这个参数要放在 id 这个变量里面,之后我们在给列表项设置路由 vue-router 的时候,:to 里的值就是一个地址加列表项的 id 值,因为使用了路由,所以记得给 to 前加冒号,例如:

<router-link :to="'/detail/' + item.id"></router-link>

后面会详细讲解这一块。先来看一下路由配置这一块:

router/index.js

import Vue from "vue";
import Router from "vue-router";
import Home from "@/pages/home/Home";
import City from "@/pages/city/City";
import Detail from "@/pages/detail/Detail";

Vue.use(Router);

export default new Router({
    routes: [
        {
            path: "/",
            name: "Home",
            component: Home
        },{
            path: "/city",
            name: "City",
            component: City
        },
        {
            path: "/detail/:id",
            name: "Detail",
            component: Detail
        }
    ]
});

我发现上面设置动态路由这里,冒号后边带的参数可以是其他名字的,他并不和 router-link 标签中我们使用的列表项的 id 对应,但是如果冒号后边不跟值的话,页面就会出问题,详情页是空白,所以,冒号后边跟的这个参数,名字可以随意起,但必须要有,我的理解是只要让路由知道路径后是要跟一个参数的,这样才能实现动态路由的功能。

这个时候,打开页面,点击城市,url 后面就会跟一个该城市对应的 id。下面我们先来完成详情页面的布局与样式。

先来完成一下 banner 部分,在 detail 目录下新建一个 components 目录,然后在 components 目录中新建一个 banner.vue 组件。下面是我已经编辑好的 banner 部分的布局与样式:

detail/banner.vue

<template>
<div class="banner-con">
    <div class="banner-pic">
        <img
        src="//img1.qunarzz.com/sight/p0/1707/e1/e13a1e819bc59b79a3.img.jpg_600x330_29b1824b.jpg"
        alt
      >
    </div>
        <div class="banner-txt">
            <div class="bt-picnum">
                <span class="iconfont">&#xe685;</span>3
            </div>
            <div class="bt-tit">涠洲岛船票</div>
        </div>
    </div>
</template>

<script>
export default {
    name: "DetailBanner"
};
</script>

<style lang="stylus" scoped>
.banner-con {
    position: relative;
    color: #fff;
    
    .banner-pic {
        height: 3.52rem;

        img {
            width: 100%;
        }
    }

    .banner-txt {
        position: absolute;
        left: 0.2rem;
        bottom: 0.2rem;

        .bt-picnum {
            background-color: rgba(0, 0, 0, 0.6);
            line-height: 0.4rem;
            text-align: center;
            font-size: 0.24rem;
            border-radius: 0.2rem;

            .iconfont {
                font-size: 0.24rem;
                margin-right: 0.1rem;
            }
        }

        .bt-tit {
            font-size: 0.28rem;
            margin-top: 0.2rem;
        }
    }
}
</style>

完成 banner 部分的布局样式后,还需要在 Detail 中引入并使用 banner.vue 组件。

Detail.vue

<template>
    <div>
        <detail-banner></detail-banner>
    </div>
</template>

<script>
import DetailBanner from "./components/banner";
export default {
    name : "Detail",
    components :{
        DetailBanner
    }
}
</script>

这个时候打开页面,点击“猜你喜欢”部分的城市,会跳转到详情页,样式布局如下:

以上就完成了详情页动态路由及 banner 布局,点击 banner 部分,他是会打开一个轮播的,下一章我们来看一下这个轮播如何实现。

二、公用图片画廊组件拆分

这一章来创建一个公用图片画廊的组件,点击 banner 图,会打开一个图片画廊,图片有轮播滚动的效果,并且在底部会显示一共有几张图,当前是第几张图的效果。

因为在其他地方可能也会用到这个组件,所以我们把他作为一个公共组件来开发,首先创建这个公共组件,在 src 目录下创建 common 目录,然后在里面创建 gallary 目录,并在里面创建 Gallary.vue 文件,将画廊的代码编写到这个组件中。

这个时候我们要去 banner 中引入并使用这个组件,这里我们可以配置一下路径,回忆一下 /api 路径 是怎么配置指向为 /static/mock 路径的。打开 build 目录下的 webpack.base.conf.js,在 alias 中,我们添加一条配置信息,'common': resolve('src/common'),也就是让 common 指向 src 下的 common 目录。

刚才修改了配置信息,所以要重启一下项目服务才能生效。回到 banner.vue 中,可以直接 import CommonGallary from "common/gallary/Gallary" 来引入公共画廊组件,这样就不用再向上一层目录去寻找文件了。然后在组件对象中添加 CommonGallary 这个组件,并在模板中使这个组件。

在 banner.vue 中完成组件的引入与使用后,我们来编写一下 Gallary.vue 这个组件的布局与样式。

Gallary.vue

<template>
<div class="gallery-con">
    <div class="gc-wrapper">
        <swiper :options="swiperOptions" class="swiper">
            <swiper-slide class="slide">
                <img
            class="slide-img"
            src="http://img1.qunarzz.com/sight/p0/1902/84/84696f368bbec10da3.img.jpg_350x240_3a0fefe8.jpg"
            alt
          >
        </swiper-slide>
                <swiper-slide>
                    <img
            class="slide-img"
            src="http://img1.qunarzz.com/sight/p0/1902/f3/f351c5dd27344a30a3.img.jpg_350x240_1136e527.jpg"
            alt
          >
        </swiper-slide>
            <div class="swiper-pagination" slot="pagination"></div>
        </swiper>
    </div>
</div>
</template>

<script>
export default {
    name: "CommonGallary",
    data() {
        return {
            // swiperOptions: {
            //     loop: false,
            //     pagination: '.swiper-pagination',
            //     paginationType : 'fraction',
            // }
            swiperOptions: {
                loop: true,
                pagination: {
                    el: ".swiper-pagination",
                    type: "fraction"
                }
            }
        };
    }
};
</script>

<style lang="stylus" scoped>
.gallery-con {
    display: flex;
    flex-direction: column;
    justify-content: center;
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 9;
    background-color: #000;

    .gc-wrapper {
        .slide-img {
            width: 100%;
        }

        .swiper-pagination {
            color: #fff;
        }
    }
}
</style>

遇到一个关于 swiper 分页器的问题,如果直接把 pagination 和 paginationType 属性写在 swiperOptions 对象中,分页器就显示不出来,必须将分页器的配置参数写在 pagination 对象中:

swiperOptions: {
    loop: true,
    pagination: {
        el: ".swiper-pagination",
        type: "fraction"
    }
}

以上就完成了画廊组件基本的样式布局,但是现在轮播图片是写死的,接下来我们通过获取数据动态的将轮播图片渲染出来,在 props 中定义一组默认数据,模拟通过 props 接收外部传来的数据。

Gallary.vue

<template>
<div class="gallery-con">
    <div class="gc-wrapper">
        <swiper :options="swiperOptions" class="swiper">
            <swiper-slide class="slide" v-for="(item,index) of imgs" :key="index">
                <img
            class="slide-img"
            :src="item"
            alt
          >
            </swiper-slide>
            <div class="swiper-pagination" slot="pagination"></div>
        </swiper>
    </div>
</div>
</template>

<script>
export default {
    name: "CommonGallary",
    props:{
        imgs:{
            type : Array,
            default(){
                return ["http://img1.qunarzz.com/sight/p0/1902/84/84696f368bbec10da3.img.jpg_350x240_3a0fefe8.jpg","http://img1.qunarzz.com/sight/p0/1902/f3/f351c5dd27344a30a3.img.jpg_350x240_1136e527.jpg"]
            }
        }
    },
    data() {
        return {
            swiperOptions: {
                loop: true,
                pagination: {
                    el: ".swiper-pagination",
                    type: "fraction"
                }
            }
        };
    }
};
</script>

但是,公用的组件其实不应该有默认值,将 default 中 return 改为一个空数组,然后打开 banner.vue 组件,我让这个组件给 Gallary.vue 组件传一组数据,将刚才那组图片数据放到 banner.vue 中的 data 中并返回。然后通过属性的形式将 imgs 传给 common-gallary 组件,Gallary.vue 再通过 props 接收这个 imgs 就可以了。

banner.vue

data(){
    return {
        imgs : ["http://img1.qunarzz.com/sight/p0/1902/84/84696f368bbec10da3.img.jpg_350x240_3a0fefe8.jpg","http://img1.qunarzz.com/sight/p0/1902/f3/f351c5dd27344a30a3.img.jpg_350x240_1136e527.jpg"]
    }
}

Gallary.vue

props:{
    imgs:{
        type : Array,
        default(){
            return imgs
        }
    }
},

此时打开页面,轮播依然可以正常渲染出来,没有问题。接着我们做一些逻辑上的控制,在 banner.vue 中让 common-gallary 默认是隐藏的,使用 v-show 中传一个 showGallary 变量,默认为 false,用这个变量来控制画廊的显示与隐藏。这个时候打开页面,可以看到画廊是不显示的。接下来我们要做一件事情,就是点击 banner 区域显示画廊。

打开 banner.vue,给 banner 外层区域绑定一个点击事件,并在 methods 中写这个方法:

banner.vue

<div class="banner-con" @click="hanleBannerClick"></div>

methods:{
    hanleBannerClick(){
        this.showGallary = true
    }
}

但是这个时候打开页面,会看到轮播出现了问题,因为开始 swiper 是隐藏状态,再次打开后,swiper 计算就会有问题,所以这里需要给 swiper 添加两个参数 observeParents(当Swiper的父元素变化时,Swiper更新)和 c(当改变swiper的样式(例如隐藏/显示)或者修改swiper的子元素时,自动初始化swipe),

Gallery.vue

swiperOptions: {
    loop: true,
    pagination: {
        el: ".swiper-pagination",
        type: "fraction"
    },
    observeParents : true,
    observer : true
}

这个时候就没有任何问题了。接下来再实现一个功能,就是点击画廊,可以将他关闭。首先在 Gallary.vue 中给画廊组件绑定一个点击事件 handleGalleryClick,然后在 methods 中通过 emit 向外触发一个 close 事件,然后去 banner.vue 中绑定这个 close 事件,添加一个事件方法例如 GalleryClose,然后在 methods 中编写 GalleryClose 方法,让 showGallary 这个变量变为 flase,这样就实现了点击画廊,画廊关闭的效果。附上 Gallary.vue 和 banner.vue 的代码:

Gallary.vue

<template>
<div class="gallery-con" @click="handleGalleryClick">
    <div class="gc-wrapper">
        <swiper :options="swiperOptions" class="swiper">
            <swiper-slide class="slide" v-for="(item,index) of imgs" :key="index">
                <img
            class="slide-img"
            :src="item"
            alt
          >
            </swiper-slide>
            <div class="swiper-pagination" slot="pagination"></div>
        </swiper>
    </div>
</div>
</template>

<script>
export default {
    name: "CommonGallary",
    props:{
        imgs:{
            type : Array,
            default(){
                return imgs
            }
        }
    },
    data() {
        return {
            swiperOptions: {
                loop: true,
                pagination: {
                    el: ".swiper-pagination",
                    type: "fraction"
                },
                observeParents : true,
                observer : true
            }
        }
    },
    methods:{
        handleGalleryClick(){
            this.$emit("close");
        }
    }
};
</script>

<style lang="stylus" scoped>
.gallery-con {
    display: flex;
    flex-direction: column;
    justify-content: center;
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 9;
    background-color: #000;

    .gc-wrapper {
        .slide-img {
            width: 100%;
        }

        .swiper-pagination {
            color: #fff;
        }
    }
}
</style>

banner.vue

<template>
<div>
    <div class="banner-con" @click="hanleBannerClick">
    <div class="banner-return">
        <span class="iconfont">&#xe624;</span>
    </div>
    <div class="banner-pic">
        <img
        src="//img1.qunarzz.com/sight/p0/1707/e1/e13a1e819bc59b79a3.img.jpg_600x330_29b1824b.jpg"
        alt
      >
    </div>
        <div class="banner-txt">
            <div class="bt-picnum">
                <span class="iconfont">&#xe624;</span>3
            </div>
            <div class="bt-tit">涠洲岛船票</div>
        </div>
    </div>
    <common-gallary :imgs="imgs" v-show="showGallary" @close="GalleryClose"></common-gallary>
</div>

</template>

<script>
import CommonGallary from "common/gallary/Gallary"
export default {
    name: "DetailBanner",
    components : {
        CommonGallary
    },
    data(){
        return {
            showGallary : false,
            imgs : ["http://img1.qunarzz.com/sight/p0/1902/84/84696f368bbec10da3.img.jpg_350x240_3a0fefe8.jpg","http://img1.qunarzz.com/sight/p0/1902/f3/f351c5dd27344a30a3.img.jpg_350x240_1136e527.jpg"]
        }
    },
    methods:{
        hanleBannerClick(){
            this.showGallary = true
        },
        GalleryClose(){
            this.showGallary = false
        }
    }
};
</script>

<style lang="stylus" scoped>
.banner-con {
    position: relative;
    color: #fff;

    .banner-return {
        position: absolute;
        top: 0.2rem;
        left: 0.2rem;
        width: 0.72rem;
        height: 0.72rem;
        background-color: rgba(0, 0, 0, 0.6);
        border-radius: 50%;
        text-align: center;
        line-height: 0.72rem;
    }

    .banner-pic {
        height: 3.52rem;

        img {
            width: 100%;
        }
    }

    .banner-txt {
        position: absolute;
        left: 0.2rem;
        bottom: 0.2rem;

        .bt-picnum {
            background-color: rgba(0, 0, 0, 0.6);
            line-height: 0.4rem;
            text-align: center;
            font-size: 0.24rem;
            border-radius: 0.2rem;

            .iconfont {
                font-size: 0.24rem;
            }
        }

        .bt-tit {
            font-size: 0.28rem;
            margin-top: 0.2rem;
        }
    }
}
</style>

以上我们就完成了公用图片画廊组件拆分与功能的实现,最后记得提交代码并合并分支。


长得好看的都会关注我的 o(≧v≦)o~~

上一篇 下一篇

猜你喜欢

热点阅读