zl程序教程

您现在的位置是:首页 >  前端

当前栏目

Vue 实例实战之 Vue webpack 仿去哪儿网App页面开发(应用中的几个页面简单实现)

2023-09-11 14:20:50 时间

Vue 实例实战之 Vue webpack 仿去哪儿网App页面开发(应用中的几个页面简单实现)

目录

Vue 实例实战之 Vue webpack 仿去哪儿网App页面开发(应用中的几个页面简单实现)

一、简单介绍

二、环境

三、效果预览

四、项目的页面结构

五、项目主要插件

六、项目实现过程

七、router路由管理, store vuex 状态管理 说明

八、几个性能优化点说明

九、axios 获取服务端数据说明

十、src/common 的共有 vue 

十一、src/assets/styles 存放css 样式、常用的变量样式参数等

 十二、该仿去哪儿网的演示项目源码下载


一、简单介绍

Vue 开发的一些知识整理,方便后期遇到类似的问题,能够及时查阅使用。

本节介绍,Vue 开发的实例实战,模仿开发去哪儿网的几个页面 ,体验 Vue 在实战中应用,欢迎指出,或者你有更好的方法,欢迎留言。

二、环境

1、vue  2.5.2

2、vue-router 3.0.1

3、vuex 3.0.1

三、效果预览

四、项目的页面结构

五、项目主要插件

 

六、项目实现过程

1、环境构建,并且 vue init webpack xxx_工程名,根据提示创建工程

具体环境搭建过程:Web 前端 之 Vue webpack 环境的搭建及工程创建简单整理_仙魁XAN的博客-CSDN博客

2、工程文件目录结构如下

3、安装依赖,如果下面的一些依赖没有安装,可以对应使用 npm install 插件名@版本号 --save 先安装插件包

4、src 开发文件结构说明

5、在 main.js 中引入 reset.css 用作重置浏览器标签的样式表,统一样式,border.css 移动端1像素边框,fastclick 解决移动端click事件延迟300ms和点击穿透问题,vue-awesome-swiper 全局轮播组件设置,babel-polyfill 解决部分手机白屏问题

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
/*
//reset.css是重置浏览器标签的样式表,其作用就是重新定义标签样式,覆盖浏览器的CSS默认属性,也就是指把浏览器提供的默认样式覆盖掉。
//
//在HTML标签在浏览器里有默认的样式,例如 p 标签有上下边距,strong标签有字体加粗样式,em标签有字体倾斜样式。不同浏览器的默认样式之间也会有差别,例如ul默认带有缩进的样式,在IE下,它的缩进是通过margin实现的,而Firefox下,它的缩进是由padding实现的。在切换页面的时候,浏览器的默认样式往往会给我们带来麻烦,影响开发效率。
//
//所以解决的方法就是一开始就将浏览器的默认样式全部去掉,更准确说就是通过重新定义标签样式。“覆盖”浏览器的CSS默认属性。最最简单的说法就是把浏览器提供的默认样式覆盖掉!这就是CSS reset。
*/
import 'styles/reset.css'
import 'styles/iconfont.css'

/*
//该css样式用于解决移动端1像素边框问题。问题分析:有些手机的屏幕分辨率较高,是2-3倍屏幕。css样式中border:1px solid red;在2倍屏下,显示的并不是1个物理像素,而是2个物理像素。为了解决这个问题,引入border.css是非常有必要的。
*/
import 'styles/border.css'

// fastclick 解决移动端click事件延迟300ms和点击穿透问题
import fastClick from 'fastclick'
// 轮播图插件
import VueAwesomeSwiper from 'vue-awesome-swiper'
import 'swiper/dist/css/swiper.css'

import store from './store/index'

// 解决部分手机白屏问题
import "babel-polyfill"

fastClick.attach(document.body)
Vue.config.productionTip = false
// 全局使用轮播图插件
Vue.use(VueAwesomeSwiper)

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

6、App.vue 中的 <router-view/> 用于显示路由切换的界面,<keep-alive exclude="Detail"> 标签,作用是缓存 vue ,执行一次 mounted ,exclude 的 vue 则不做缓存

<template>
  <div id="app">
    <!--缓存数据,执行一次 mounted  ,exclude 不包括-->
    <keep-alive exclude="Detail">
      <router-view/>
    </keep-alive>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
</style>
 

7、pages 是三个路由界面,以及路由界面里面的拆分页面,拆分的目的也是把复杂的界面简单化

8、 Home 界面,包含 5 个拆分页面,Home.vue 主要功能是,从 store 中获取 城市信息,然后axios.get 获取数据,把数据传递给各个子页面,

<template>
    <div>
      <!--:city='city' 数据向子组件传递-->
     <home-header></home-header>
     <home-swiper :list="swiperList"></home-swiper>
      <home-icons :list="iconsList"></home-icons>
      <home-recommend :list="recommendsList"></home-recommend>
      <home-weekend :list="weekendsList"></home-weekend>
    </div>
</template>

<script>
import HomeHeader from './components/Header'
import HomeSwiper from './components/Swiper.vue'
import HomeIcons from './components/Icons.vue'
import HomeRecommend from './components/Recommend.vue'
import HomeWeekend from './components/Weekend.vue'

// 获取网络数据
import axios from 'axios'
import {mapState} from 'vuex'

export default {
  name: 'Home',
  components: {
    HomeHeader,
    HomeSwiper,
    HomeIcons,
    HomeRecommend,
    HomeWeekend
  },
  data (){
  return{
    swiperList:[],
    iconsList:[],
    recommendsList:[],
    weekendsList:[],
    lastCity:'',
  }

  },
computed:{
...mapState(['city'])
},
  methods:{
    getHomeInfo(){
      axios.get('/api/index.json?city='+this.city).then(this.getHomeInfoSucc)
    },
    getHomeInfoSucc(res){
      console.log(res)
      const result = res.data.ret
      if(result && res.data.data){
        const data = res.data.data
        this.swiperList = data.swiperList
        this.iconsList = data.iconsList
        this.recommendsList = data.recommendsList
        this.weekendsList = data.weekendsList

      }
    },
  },

  mounted(){
    this.lastCity = this.city
    this.getHomeInfo()
  },
  activated(){
    if(this.lastCity !== this.city){
      this.lastCity = this.city
      this.getHomeInfo()
    }
  },
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
    * {
        margin: 0px;
        padding: 0px;
    }
</style>
9、Home 界面中的 Header ,包含输入框,显示当前城市,点击城市可以跳转到城市选择界面
<template>
    <div class="header">
      <div class="header-left">
        <div class="iconfont back-icon">&#xe624;</div>
      </div>
      <div class="header-input">
        <span class="iconfont">&#xe632;</span>
        输入城市/景点/游玩主题</div>
      <router-link to="/city">
      <div class="header-right">{{this.city}}
      <span class="iconfont arrow-icon">&#xe64a;</span>
      </div>
      </router-link>
    </div>
</template>

<script>
  // 映射属性
  import {mapState} from 'vuex'

export default {
  name: 'HomeHeader',
//  接收父组件的数据
  props:{
  },
  computed :{
    ...mapState(['city'])
  },
  data: function(){
    return {}
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  @import "~styles/varibles.styl";
  .header{
    display: flex;
    line-height: $headerHeight;
    background: $bgColor;
    color: #ffffff;
  }
  .header .header-left{
    width: 0.64rem;
    float: left;
  }
  .header .header-left .back-icon {
    text-align: center;
    font-size: 0.4rem;
  }
  .header .header-input{
    flex: 1;
    background: #fff;
    border-radius: 0.1rem;
    margin-top: 0.12rem;
    margin-left: 0.2rem;
    padding-left: 0.2rem;
    height: 0.64rem;
    line-height: 0.64rem;
    color: #ccc;
  }
  .header .header-right{
    min-width: 1.04rem;
    padding: 0 .1rem;
    float: right;
    text-align: center;
    color:white;

  }

  .header .header-right .arrow-icon{
    margin-left: -0.04rem;
    font-size: 0.24rem;

  }

</style>
10、Home 界面中的 Swiper 是一个轮播组件,轮播图片
<template>
    <div class="wrapper">
      <swiper :options="swiperOption" v-if="showSwiper">
        <swiper-slide v-for="item of list" :key="item.id">
          <img class="swiper-img" :src="item.url">
        </swiper-slide>
        <div class="swiper-pagination" slot="pagination"></div>
      </swiper>
    </div>
</template>

<script>
export default {
  name: 'HomeSwiper',
  props:{
    list:Array
  },
  data: function () {
    return {
      swiperOption: {
        // 下面的圆点
        pagination:'.swiper-pagination',
        // 循环轮播
        loop:true
      },
    }
  },
  computed:{
    showSwiper(){
      return this.list.length
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  // >>> (scoped 阻挡后 >>>穿透 )
  .wrapper >>> .swiper-pagination-bullet{
    background: #fff;
  }
  .wrapper{
    width: 100%;
    height: 0;
    padding-bottom: 30.45%;
    background: #eee;
  }
    .swiper-img{
      width: 100%;
    }
</style>
 

11、Home 界面中的 Icons ,显示icon图标组

<template>
    <div class="icons">
      <swiper :options="swiperOption">
        <swiper-slide v-for="(page, indexPage) of pages" :key="indexPage">
      <div class="icon" v-for="item of page" :key="item.id">
        <div class="icon-img">
          <img class="icon-img-content" :src="item.imgUrl">
        </div>
        <p class="icon-desc">{{item.desc}}</p>
      </div>
        </swiper-slide>
        </swiper>
    </div>

</template>

<script>
    export default {
        name: 'HomeIcons',
      props:{
        list:Array
      },
        data: function () {
            return {
              swiperOption:{
                autoplay:false
              }
            }
        },

  computed:{
    pages (){
      const pages = []
      this.list.forEach((item, index) =>{
      const page = Math.floor(index/8)
      if(pages[page]==null){
        pages[page]=[]
      }
      pages[page].push(item)
      })
      return pages
      }
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  @import "~styles/varibles.styl";
  @import "~styles/mixins.styl";
  .icons >>> .swiper-wrapper{
    height: 0;
    padding-bottom: 50%;
  }
  .icons{
    margin-top: 0.2rem;
  }
  .icons .icon{
    position: relative;
    overflow: hidden;
    float: left;
    width: 25%;
    padding-bottom: 25%;
  }
  .icons .icon .icon-img{
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: .44rem;
  }
  .icons .icon .icon-img .icon-img-content{
    height: 90%;
    display: block;
    margin: 0 auto;
  }
  .icons .icon .icon-desc{
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: .44rem;
    line-height: .44rem;
    color: $darkTextColor;
    text-align: center;
    font-size: 0.2rem;
    /*文字过多,则 ... 显示*/
    ellipsis()

  }
</style>
12、Home 界面中的 Recommend,热门推荐,点击可以跳转到热门推荐的详情界面
<template>
    <div>
      <div class="title">热门推荐</div>
      <ul class="item-wrapper">
        <!--border-bottom 每个下面有线-->
        <router-link tag="li" class="item border-bottom"
            v-for="item of list"
            :key="item.id"
            :to="/detail/ + item.id"
          >
          <img class="item-img" :src="item.imgUrl">
          <div class="item-info">
            <p class="item-title">{{item.title}}</p>
            <p class="item-desc">{{item.desc}}</p>
            <button class="item-button">查看详情</button>
          </div>
        </router-link>
      </ul>
    </div>
</template>

<script>
    export default {
        name: 'HomeRecommend',
      props:{
        list:Array
      },
        data: function () {
            return {

            }
        }
    }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  @import "~styles/mixins.styl";
  .title{
    margin-top: 0.2rem;
    line-height: 0.8rem;
    background: #eee;
    text-indent: 0.2rem;
  }
  .item-wrapper{
    padding: 0.15rem;
  }
  .item{
    overflow: hidden;
    display: flex;
    height: 1.9rem;
  }
  .item .item-info{
    flex:1;
    padding: 0.1rem;
    /*让 ellipsis() 生效*/
    min-width: 0;
  }
  .item .item-img {
    width: 1.7rem;
    height: 1.7rem;
    padding: 0.1rem;
  }
  .item .item-info .item-title{
    line-height: 0.54rem;
    font-size: .32rem;
    ellipsis()
  }
  .item .item-info .item-desc{
    line-height: 0.4rem;
    color: #ccc;
    ellipsis()
  }
  .item .item-info .item-button{
    line-height: .44rem;
    margin-top: 0.2rem;
    background: #ff9300;
    padding: 0 0.2rem;
    border-radius: 0.06rem;
    color: #fff;
  }
</style>
13、Home 界面中的 Weekend,周末去哪页面
<template>
    <div>
      <div class="title">周末去哪儿</div>
      <ul class="item-wrapper">
        <!--border-bottom 每个下面有线-->
        <li class="item border-bottom" v-for="item of list" :key="item.id">
          <img class="item-img" :src="item.imgUrl">
          <div class="item-info">
            <p class="item-title">{{item.title}}</p>
            <p class="item-desc">{{item.desc}}</p>
          </div>
        </li>
      </ul>
    </div>
</template>

<script>
    export default {
        name: 'HomeWeekend',
      props:{
      list:Array
    },
        data: function () {
            return {}
        }
    }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  @import "~styles/mixins.styl";
  .title{
    line-height: 0.8rem;
    background: #eee;
    text-indent: 0.2rem;
  }
  .item-wrapper{
    padding: 0.15rem;
  }
  .item {
    overflow: hidden;
    height: 0;
    padding-bottom: 47%;
  }
  .item .item-img {
    width: 100%;
  }
  .item .item-info .item-title{
    line-height: 0.54rem;
    font-size: .32rem;
    ellipsis()
  }
  .item .item-info .item-desc{
    line-height: 0.4rem;
    font-size: 0.2rem;
    color: #ccc;
    ellipsis()
  }
</style>
 

14、 City 界面,包含 4 个拆分页面,City.vue 主要功能是,axios.get 获取数据,以及获取 Alphabet传递的数据,对应把数据传递给各个子页面

<template>
  <div>
    <city-header></city-header>
    <city-search :cities="cities"></city-search>
    <city-list :cities="cities" :hot="hotCities" :letter="letter"></city-list>
    <city-alphabet :cities="cities" @change="handleAlphabetEvent"></city-alphabet>
  </div>
</template>

<script>
  import CityHeader from './components/Header.vue'
  import CitySearch from './components/Search.vue'
  import CityList from './components/List.vue'
  import CityAlphabet from './components/Alphabet.vue'
  import axios from 'axios'
  export default {
    name: 'City',
    components:{
      CityHeader,
      CitySearch,
      CityList,
      CityAlphabet,
    },
    data: function () {
      return {
        cities:{},
        hotCities:[],
        letter:''
      }
    },

    methods:{
      getCityInfo(){
        axios.get('/api/city.json').then(this.getCityInfoSucc)
      },

      getCityInfoSucc(res){
        res = res.data
        if(res.ret && res.data){
          const data = res.data
          this.cities = data.cities
          this.hotCities = data.hotCities
          console.log(res)
        }
      },

      handleAlphabetEvent(alpha){
        this.letter = alpha
      }
    },
  mounted(){
    this.getCityInfo()
  }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>

</style>
15、City 界面中的 Header,包含标题,以及返回键,点击回到 Home 界面
<template>
    <div class="header">
      城市选择
      <router-link to="/">
      <div class="iconfont back-icon">&#xe624;</div>
      </router-link>
    </div>
</template>

<script>
export default {
  name: 'CityHeader',
  data: function () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  @import "~styles/varibles.styl";
  .header{
    position: relative;
    overflow: hidden;
    height: $headerHeight;
    line-height: $headerHeight;
    background: $bgColor;
    color: #ffffff;
    text-align: center;
    font-size: 0.4rem;
  }

  .header .back-icon{
    position: absolute;
    top:0;
    left: 0;
    width: 0.64rem;
    text-align: center;
    font-size: 0.4rem;
    color: white;
  }


</style>
16、City 界面中的 Search,包含输入框,以及搜索出城市的列表,和没有匹配数据的提示;其中搜索功能是在watch 中监听输入的变化,进行在城市名字和拼音中是否包含,添加到结果中去,从而实现搜索功能
<template>
  <div>
    <div class="search">
      <input v-model="keyword" class="search-input" placeholder="输入城市">
    </div>
    <div v-show="keyword" class="search-content" ref="search">
      <ul>
        <li class="search-item border-bottom"
            v-for="item of list"
            :ket="item.id"
            @click="handleClickCity(item.name)"
          >
          {{item.name}}</li>
        <li class="search-item border-bottom" v-show="hasNoData">没有匹配数据</li>
      </ul>
    </div>
  </div>
</template>

<script>
  import BScroll from 'better-scroll'
  import {mapActions} from 'vuex'
export default {
  name: 'CitySearch',
  props:{
    cities:Object
  },
  data: function () {
    return {
      keyword:'',
      list:[],
      timer:null
    }
  },
  watch:{
    keyword(){
    console.log('sdd')
      // 控制执行频率,提高性能
      if(this.timer){
        clearTimeout(this.timer)
      }
      this.timer = setTimeout(()=>{

        if(!this.keyword){
          this.list = []
          return
        }

      const result = []
        for(let i in this.cities){
          this.cities[i].forEach((value)=>{
            if(value.spell.indexOf(this.keyword) > -1
              || value.name.indexOf(this.keyword)> -1){

              result.push(value)
            }
          })
        }
      this.list = result
      console.log('this.scroll ', this.scroll)
        setTimeout(()=>{
        this.scroll.refresh()
      },10)
      },100)
    }
  },
  computed:{
    hasNoData(){

      return this.list.length==0
    }
  },
  methods:{
    handleClickCity(city){
      this.changeCity(city)
      this.$router.push('/')
    },
  ...mapActions(['changeCity'])
  },
  mounted(){
    this.scroll = new BScroll(this.$refs.search,{click:true})
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  @import "~styles/varibles.styl";
  .search{
    height: 0.72rem;
    padding: 0 0.1rem;
    background: $bgColor;
  }

  .search .search-input{
    box-sizing: border-box;
    padding: 0 0.1rem;
    width: 100%;
    height: 0.62rem;
    line-height: 0.62rem;
    border-radius: 0.06rem;
    text-align: center;
    color: #777;
  }
  .search-content{
    z-index: 1;
    position: absolute;
    overflow: hidden;
    top:1.58rem;
    bottom: 0;
    right: 0;
    left: 0;
    background: #eee;
  }

  .search-content .search-item{
    line-height: 0.62rem;
    padding-left: 0.2rem;
    background: #fff;
  }


</style>
17、City 界面中的 List,包含当前城市,热门城市,以及以首字母排列的城市列表,点击热门城市,以及城市列表的城市,都会跳转到对应城市的 Home 界面
<template>
    <div class="list" ref="wrapper">
      <div>
        <div class="area">
          <div class="title border-topbottom">当前城市</div>
          <div class="button-list">
            <div class="button-wrapper">
              <div class="button">{{this.currentCity}}</div>
            </div>
          </div>
        </div>
        <div class="area">
          <div class="title border-topbottom">热门城市</div>
          <div class="button-list">
            <div class="button-wrapper"
                 v-for="item of hot"
                 :key="item.id"
                 v-on:click="handleClickCity(item.name)"
              >
              <div class="button">{{item.name}}</div>
            </div>
          </div>
        </div>
        <div class="area" v-for="(items,key) of cities" :key="key" :ref="key">
          <div class="title border-topbottom">{{key}}</div>
          <div class="item-list">
            <div class="item border-bottom"
                 v-for="item of items"
                 :key="item.id"
                 v-on:click="handleClickCity(item.name)"
              >
              {{item.name}}</div>
          </div>
        </div>
      </div>
    </div>
</template>

<script>
  import BScroll from 'better-scroll'
  import {mapState, mapActions} from 'vuex'
export default {
  name: 'CityList',
  props:{
    hot:Array,
    cities:Object,
    letter:String
  },
  data: function () {
    return {
    }
  },
  computed:{
    ...mapState({
    currentCity:'city'
  })
  },
  watch:{
    letter(){
      if(!this.letter.isEmpty){
        // 监听字母点击,跳转
        console.log(this.letter)
        const element = this.$refs[this.letter][0]
        console.log(element)
        this.scroll.scrollToElement(element)
      }
    }
  },

  methods:{
    handleClickCity(city){
      this.changeCity(city)
      this.$router.push('/')
    },
    ...mapActions(['changeCity'])
  },
  mounted(){
    // 城市数据先创建,然后在 scroll 不能可能 scroll 在没有数据的时候构建,从而使得scroll无法滚动
    setTimeout(()=>{
      this.scroll = new BScroll(this.$refs.wrapper,{click:true})
      console.log(' this.scroll ',this.scroll)
    },100)

  },
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  @import "~styles/varibles.styl";

  .list{
    overflow: hidden;
    position: absolute;
    top:1.58rem;
    bottom:0;
    left:0;
    right:0;
  }
  .border-topbottom::before{
    border-color: #ccc;
  }
  .border-topbottom::after{
    border-color: #ccc;
  }
  .border-bottom::before{
    border-color: #ccc;
  }
.title{
  padding-left: 0.2rem;
  background: #eee;
  color: #666;
  line-height: 0.44rem;
  font-size: 0.24rem;
}
  .button-list{
    overflow: hidden;
    padding: 0.1rem 0.6rem 0.1rem 0.1rem;
  }
  .button-list .button-wrapper{
    width: 33.33%;
    float: left;
  }
  .button-list .button-wrapper .button{
    margin: 0.1rem;
    padding: 0.1rem 0;
    border: 0.02rem solid #ccc;
    text-align: center;
    border-radius: 0.06rem;
  }
  .item-list .item{
    line-height: 0.76rem;
    padding-left: 0.2rem;
  }

</style>
18、City 界面中的 Alphabet,包含所有城市列表的首字母组成的列表,其中通过touch的位置计算出当前选择的哪个字母,传递给 City,City 在传递给 List ,显示对应首字母城市列表
<template>
    <div class="alphabet">
      <ul>
        <li class="item"
            v-for="item of letters"
            @click="handleOnClick"
            @touchstart.prevent="handleTouchStart"
            @touchmove="handleTouchMove"
            @touchend="handleTouchEnd"
            :ref="item"
          >{{item}}</li>
      </ul>
    </div>
</template>

<script>
export default {
  name: 'CityAlphabet',
  props:{
    cities:Object
  },
  data: function () {
    return {
      touchStatus:false,
      aStartY:0,
      // 性能优化
      timer:null
    }
  },
  computed:{
    letters(){
      const letters =[]
      for(let i in this.cities){
        letters.push(i)
      }
      return letters
    }
  },
  updated(){
    this.aStartY = this.$refs['A'][0].offsetTop
  },

  methods:{
    handleOnClick(e){
      this.$emit('change', e.target.innerText)
    },
    handleTouchStart(){
      this.touchStatus =true
    },
    handleTouchMove(e){
      if(this.touchStatus){
        if(this.timer){
          clearTimeout(this.timer)
        }
        // 控制执行频率,从而提升性能
        this.timer = setTimeout(()=>{
          // 75 是上面城市 和 输入城市元素的 Y 总和
          const touchY = e.touches[0].clientY - 75
          // 20 是 字母 元素的 Y 值
          const index = Math.floor((touchY - this.aStartY) / 20)
          if(index >= 0 && index < this.letters.length){
            this.$emit('change',this.letters[index])
          }
        }, 16)

      }
    },
    handleTouchEnd(){
      this.touchStatus =false
    },

  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  @import "~styles/varibles.styl";

.alphabet{
  position: absolute;
  top: 1.58rem;
  right: 0;
  bottom: 0;
  width: 0.4rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
  .alphabet .item{
    color: $bgColor;
    text-align: center;
    line-height: 0.4rem;
  }

</style>

19、 Detail界面,包含 3 个拆分页面,Detail.vue 主要功能是,axios.get 获取数据,对应把数据传递给各个子页面

<template>
  <div class="detail">
    <detail-banner
      :sightName="sightName"
      :bannerImg="bannerImg"
      :gallaryImgs="gallaryImgs"
      ></detail-banner>
    <detail-header></detail-header>
    <div class="content">
      <detail-list :list="list"></detail-list>
    </div>
  </div>
</template>

<script>
  import DetailBanner from './components/Banner.vue'
  import DetailHeader from './components/Header.vue'
  import DetailList from './components/List.vue'
  import axios from 'axios'
  export default {
    name: 'Detail',
    components:{
      DetailBanner,
      DetailHeader,
      DetailList,
    },
    data: function () {
      return {
        sightName:'',
        bannerImg:'',
        gallaryImgs:[],
        list: []
      }
    },
    methods:{
      getDetailInfo() {
//        axios.get('/api/detail.json?id='+this.$route.params.id)
        axios.get('/api/detail.json',{
          params:{
            id: this.$route.params.id
          }
        }).then(this.handleGetDataSucc)

      },
       handleGetDataSucc(res){
        res = res.data
        if(res.ret && res.data){
          const data = res.data
          this.sightName = data.sightName
          this.bannerImg = data.bannerImg
          this.gallaryImgs = data.gallaryImgs
          this.list = data.categoryList
        }
      }
    },

    mounted(){
      this.getDetailInfo()
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  .content{
    height: 20rem;
  }
</style>

20、Detail 界面中的 Banner ,包含一个图片展示,以及一个图片集轮播组件(默认隐藏);其中点击图片,就会显示图片集轮播

<template>
  <div>
    <div class="banner" @click="onClickBanner">
      <img class="banner-img" :src="bannerImg"/>
      <div class="banner-info">
        <div class="banner-title">{{this.sightName}}</div>
        <div class="banner-number"><span class="iconfont banner-icon">&#xe692;</span>78</div>
      </div>
    </div>
    <fade-animation>
      <common-gallary
        :imgs="gallaryImgs"
        @close="handleGallaryClose"
        v-show="isShowGallary"
        ></common-gallary>
    </fade-animation>
  </div>
</template>

<script>
  import CommonGallary from 'common/gallary/Gallary'
  import FadeAnimation from 'common/fade/FadeAnimation'
  export default {
    name: 'DetailBanner',
    props:{
      sightName:String,
      bannerImg:String,
      gallaryImgs:Array,
    },
    components:{
      CommonGallary,
      FadeAnimation,
    },
    data: function () {
      return {
        isShowGallary: false,
      }
    },
    methods:{
      onClickBanner(){
        this.isShowGallary = true
        },
      handleGallaryClose(){
        this.isShowGallary = false
      },
    },
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  .banner{
    position: relative;
    overflow: hidden;
    height: 0;
    padding-bottom: 50%;
    background: green;
  }

  .banner .banner-img{
    width: 100%;
  }

  .banner .banner-info{
    display: flex;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    line-height: 0.6rem;
    color: #fff;
    background-image: linear-gradient(top,rgba(0,0,0,0),rgba(0,0,0,0.8));
   }

  .banner .banner-info .banner-title{
    flex: 1;
    font-size: 0.32rem;
    padding: 0 0.2rem;
  }

  .banner .banner-info .banner-number{
    height: 0.32rem;
    line-height: 0.32rem;
    padding: 0 0.4rem;
    margin-top: 0.14rem;
    border-radius: 0.2rem;
    background: rgba(0,0,0,0.8);
    font-size: 0.24rem;
  }

  .banner .banner-info .banner-number .banner-icon{
    font-size: 0.24rem;
    padding: 0.1rem;
  }
</style>

21、Detail 界面中的 Header,包含绝对位置的返回按钮,以及一个固定位置的返回按钮标题;根据当前页面的滚动情况,动态切换不同位置的按钮显隐

<template>
  <div class="header">
    <router-link tag="div" to='/' class="header-abs" v-show="showAbs">
      <div class="iconfont back-abs-icon">&#xe624;</div>
    </router-link>
    <router-link tag="div" to='/' class="header-fixed" v-show="!showAbs"
      :style="styleOpacity"
      >
      <div class="iconfont back-fixed-icon">&#xe624;</div>
      景点详情
    </router-link>
  </div>
</template>

<script>
  export default {
    name: 'DetailHeader',
    data: function () {
      return {
        showAbs:true,
        styleOpacity:{
          opacity:0
        }
      }
    },

    methods:{
      handleScroll()
      {
        console.log('handleScroll')
        const top = document.documentElement.scrollTop
        console.warn('top ', top)
        if (top > 60) {
          this.showAbs = false
          const opacity = top/140 > 1? 1: top/140
          this.styleOpacity = {
            opacity,
          }
        } else {
          this.showAbs = true
          this.styleOpacity = {
            opacity:0,
          }
        }
      },
  },
  mounted(){
    window.addEventListener('scroll',this.handleScroll)
  },
  unmounted(){
    window.removeEventListener('scroll',this.handleScroll)
  },
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
  @import "~styles/varibles.styl";

  .header-abs{
  position: absolute;
  left:0.2rem;
  top:0.2rem;
  width: 0.8rem;
  height: 0.8rem;
  line-height: 0.8rem;
  text-align: center;
  border-radius: 0.4rem;
  background: rgba(0,0,0,0.8);
}
  .header-abs .back-abs-icon{
    color: #fff;
    font-size: 0.4rem;

  }

.header-fixed{
  position: fixed;
  z-index: 2;
  top:0;
  left:0;
  right:0;
  overflow: hidden;
  height: $headerHeight;
  line-height: $headerHeight;
  background: $bgColor;
  color: #ffffff;
  text-align: center;
  font-size: 0.4rem;
}

.header-fixed .back-fixed-icon{
  position: absolute;
  top:0;
  left: 0;
  width: 0.64rem;
  text-align: center;
  font-size: 0.4rem;
  color: white;
}
</style>

22、Detail 界面中的 List,一个简单的列表信息展示

<template>
  <div class="list">
    <div class="item" v-for="(item,id) of list" :key="id">
      <div class="item-title border-bottom">
        <span class="item-title-icon"></span>
        {{item.title}}</div>
      <div v-if="item.children" class="item-children">
        <detail-list :list="item.children"></detail-list>
      </div>
    </div>

  </div>
</template>

<script>
  export default {
    name: 'DetailList',
    props:{
      list:Array
    },
    data: function () {
      return {
        msg: 'Welcome to Your Vue.js App'
      }
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>

  .item-title-icon{
    position: relative;
    left: .06rem;
    top: .06rem;
    display: inline-block;
    width: .36rem;
    height: .36rem;
    background: url(http://s.qunarzz.com/piao/image/touch/sight/detail.png) 0 -.45rem no-repeat;
    margin-right: .1rem;
    background-size: .4rem 3rem;
  }
  .item-title{
    line-height: 0.8rem;
    font-size: 0.32rem;
    padding: 0 0.2rem;
  }
  .item-children{
    padding: 0 0.4rem;
  }
</style>

七、router路由管理, store vuex 状态管理 说明

1、router路由管理,三个路由 path(/ 、/city、/detail/id),并且添加路由地址切换回 scroll 都置于顶部处理

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
      // 异步加载组件,避免大量的代码堆积到 app.js 中,()=> import( '@/pages/city/City') 可以拆分代码到其他的projectX.js中
      component: ()=> import( '@/pages/home/Home')
    },
    {
      path: '/city',
      name: 'City',
      //component: City
      component: ()=>import('@/pages/city/City')
    },
    {
      //动态路由
      path: '/detail/:id',
      name: 'Detail',
      //component: Detail
      component: ()=>import('@/pages/detail/Detail')
    },
  ],

  //添加路由切换的时候,scroll ,都回到顶部处理
  scrollBehavior(to, from, savedPosition) {
    // 回到顶部
    return { x: 0, y:0 }
  },


})

2、store 添加对某个全局参数的管理

state.js

/**
 * Created by 12722 on 2022/6/13.
 */
let defaultCity = '桂林'
try{
  if (localStorage.city){
    defaultCity = localStorage.city
  }
}
catch(e){}

export default{
  city:defaultCity
}



action.js
/**
 * Created by 12722 on 2022/6/13.
 */
export default {
  changeCity(context,city){
    context.commit('changeCity',city)
  }
}



mutations.js
/**
 * Created by 12722 on 2022/6/13.
 */
export default {
  changeCity(state, city){
    state.city = city
    try{ // 本地化保存
      localStorage.city = city}
    catch(e){}

  }
}

八、几个性能优化点说明

1、App.vue 中的 <keep-alive exclude="Detail"></keep-alive>

keep-alive 会缓存一些数据,在界面切换后,也不销毁,例如mounted函数中 axios.get数据,从而避免 axios.get 频繁访问

2、setTimeout 函数的使用,例如在 City 界面中 Search.vue 进行输入搜索时,可以控制搜索频率,间接提高性能;以及类似在 City 界面中 Alphabet.vue 进行 Touch 选中 字母的时候,也可控制搜索频率,间接提高性能

 

九、axios 获取服务端数据说明

1、例如 Home.vue 中 mounted 使用 axios.get 获取服务端数据时

使用 axios.get('/api/index.json?city='+this.city).then(this.getHomeInfoSucc) 中的

/api/index.json?city='+this.city 在浏览器中获取不到数据,为什么 axios.get 可以获得呢

 

2、其实,在 config/index.js 做代理配置,当遇到 /api 就会进行对应转换,从而使得 axios.get 获取到对应的数据

 

3、axios.get 获取的数据 static/mock 文件夹下的数据

十、src/common 的共有 vue 

1、Gallary.vue 全屏大图滑动轮播图片

只要引入Gallary.vue,对应的添加传输图片列表数据,并且添加对应关闭事件,就可以轻易实现全屏大图滑动轮播图片

参考引用 detail/components/Banner.vue :

 

2、FadeAnimation.vue 渐隐渐现动画效果

只要引入FadeAnimation.vue 包裹需要渐隐渐现的元素即可

 参考引用 detail/components/Banner.vue :

十一、src/assets/styles 存放css 样式、常用的变量样式参数等

 1、mixins.styl 功能是:字显示超出范围,则用三个点... 表示,例如 ‘字太多了...’

 

 十二、该房去哪儿网的演示项目源码下载

仅供学习参考使用:

代码运行(最好的运行端口为 8080):

1、npm install

2、npm run start