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

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

猜你喜欢

转载自blog.csdn.net/u014361280/article/details/125474369