Vue 项目笔记

2020 年 06 月 04 日 • 阅读数: 191

Vue2.0 项目笔记

环境搭建

node.js 的安装

node.js 是JavaScript的运行依赖

安装完成之后在命令行输入 node -v 来查看当前版本(本项目使用版本为8.9.3)

npm 安装

npm 是一个包管理工具

当安装好 node.js 后,就自动安装好了,同样通过 npm -v 来查看版本号(本项使用目的版本为5.5.1)

npm 的下载速度较慢,我们可以选择安装淘宝的 npm ,通过以下命令安装

npm install -g cnpm --registry=https://registry.npm.taobao.org

Vue-cli 安装

Vue-cli 是Vue项目的一个脚手架工具

安装好后通过 vue -v 来查看版本号(本项目使用的版本为3.0.0)

git 的安装

git 是一个版本控制工具,同时本项目使用 码云 作为线上的代码仓库

安装完成之后通过 git --version 查看版本(本项目使用的版本2.18.0)

创建项目

创建线上仓库

注册 码云

创建密钥对

在设置里的SSH公钥中添加一个公钥

你可以按如下命令来生成密钥对:

ssh-keygen -t rsa -C "1769570627@qq.com"  

# 三次回车即可生成 ssh key

查看你的公钥,并把他添加到码云(Gitee.com

cat ~/.ssh/id_rsa.pub

创建一个私有仓库

这里建议开源协议选择MIT

克隆项目到本地

创建完项目后,点击右边的 “克隆/下载” ,选择SSH协议,复制链接地址

在Git Bash中输入以下命令

git clone git@gitee.com:luoyang_C/Travel.git

# 后面部分替换成刚才复制的链接

创建Vue项目

在刚才克隆到本地的项目的上级目录中创建项目

使用 Vue-cli 来创建项目,在命令行输入

vue init webpack Travel

注意这里的项目名称和仓库同名,它会提示是否往文件夹继续添加,选yes

通过 cd Travel 切换到项目目录下,通过 npm run dev 启动项目

如果在浏览器中可以访问到页面则项目的初始化完成

可以使用以下命令同步本地代码到线上仓库

git add .
get commit -m 'project initialized'
get push

初始化项目

添加meta属性

因为本项目是一个手机端的项目,所以在 index.html 添加一个 meta 属性

<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">

修改默认样式

src 目录下的 assets 目录下新建 styles 目录,用于存放样式

将一些基础样式放在里面然后再 main.js 中引用它们

以下分别代表:

  • 默认样式
  • 手机端一像素边框
  • 图标样式(图标样式的制作方式很多,推荐 iconfont )
import 'styles/reset.css'
import 'styles/border.css'
import 'styles/iconfont.css'

基础库的安装

为了解决手机端点击延迟的问题,这里引入 fastclick

npm install fastclick --save

然后在 main.js 中引入它

import fastClick from 'fastclick'

fastClick.attach(document.body)

stylus的安装

stylus是一个样式的第三方库,比css写起来更加方便

npm install stylus --sava
npm install stylus-loader --save

目录结构的修改

如下图中的项目结构,pages目录用来存放单个的页面组件,home是主页目录,在该目录下继续进行拆分,将拆分的首页组件放在components里面,这里拆分成了4个部分,然后在 Home.vue 文件中将各个组件整合成一个页面,其他页面的目录结构以同样的方式创建

src
..assets               # 样式目录
..pages                # 页面目录
....home               # 主页目录
......components       # 主页中的组件目录
........Header.vue     # 顶部组件
........Icons.vue      # 图标组件
........Recommend.vue  # 热门推荐组件
........Swiper.vue     # 轮播图组件
......Home.vue         # 主页组件
..router               # 路由目录
..App.vue              # 主程序
..main.js              # 程序入口

至此项目的初始化基本完成,可以同步一下代码到线上仓库

首页的开发

上面我们已经介绍了首页分为4个部分,然后在 Home.vue 中进行整合,最终在 App.vue 中进行显示

首页路由的配置

在router目录下的 index.js 文件中配置路由

配置方式:

  • 引入 vuevue-router
  • 引入 Home 页面组件
  • vue-router 注册到 vue
  • 创建 router 实例,并暴露给外部,共外界使用
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/pages/home/Home'

Vue.use(Router)

export default new Router({
  routes: [{
      path: '/',
      name: 'home',
      component: Home
    }]
})

在程序的入口文件 main.js 中,在创建Vue实例的时候,传入 router 实例

import Vue from 'vue'
import App from './App'
import router from './router'

new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

App.vue 的编写

目前App.vue就只是一个路由的出口

<template>
  <div id="app">
    <keep-alive>
      <router-view/>
    </keep-alive>
  </div>
</template>

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

Header的开发

Header由三部分组成:

  • 左侧一个返回图标,这里使用了 iconfont 的图标样式
  • 中间一个搜索框(功能暂未开发,只是做了样式)
  • 右侧一个城市选择按钮,点击进入城市选择页面,使用 <router-link to="#"> 做跳转(以后添加)
<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>
      <span>输入城市/景点/游玩主题</span>
    </div>
    <router-link to="/city">
      <div class="header-right">
        <span v-text="重庆"></span>
        <span class="iconfont arrow-icon">&#xe64a;</span>
      </div>
    </router-link>
  </div>
</template>

轮播图的开发

这里需要使用第三方库 vue-awesome-swiper 来实现轮播图

安装成功后在 main.js 中引入并使用它

import VueAwesomeSwiper from 'vue-awesome-swiper'

import 'swiper/dist/css/swiper.css'

Vue.use(VueAwesomeSwiper)

在页面上就可以直接通过以下方式创建一个轮播图

注意:可以看出数据是从父组件传递过来的,这里做了一个循环将数据渲染到页面

关于下面的 data 中的 swiperOption 对象的解释

  • pagination: '.swiper-pagination' 指明 '.swiper-pagination' 这个元素是一个滚动的标识
  • loop: true 指明是否循环

注意:这个对象不是必须的,但不定义这个对象,swiper标签中的 :options="swiperOption" 需要删除

最下面还有一个计算属性,showSwiper 它返回一个bool值,如果 swiperList 不为0,才显示轮播图

<template>
  <div class="wrapper">
    <swiper :options="swiperOption" v-if="showSwiper">
      <swiper-slide v-for="item of swiperList" :key="item.id">
        <img class="swiper-img" :src="item.imageUrl">
      </swiper-slide>
      <div class="swiper-pagination"  slot="pagination"></div>
    </swiper>
  </div>
</template>

<script>
export default {
  name: 'HomeSwiper',
  props: {
    swiperList: Array
  },
  data () {
    return {
      swiperOption: {
        pagination: '.swiper-pagination',
        loop: true
      }
    }
  },
  computed: {
    showSwiper () {
      return this.swiperList.length
    }
  }
}
</script>

图标组件的开发

图标组件也使用了 swiper

大部分实现和轮播图的实现类似,只不过需要注意的是,我们规定每一页显示8个图标,超出的图标才显示到下一页,所以这里添加一个计算属性

<script>
export default {
  name: 'HomeIcons',
  props: {
    iconList: Array
  },
  data () {
    return {
      swiperOption: {
        pagination: '.swiper-pagination'
      }
    }
  },
  computed: {
    pages () {
      const pages = []
      this.iconList.forEach((item, index) => {
        const page = Math.floor(index / 8)
        if (!pages[page]) {
          pages[page] = []
        }
        pages[page].push(item)
      })
      return pages
    }
  }
}
</script>

热门推荐的开发

热门推荐的逻辑很简单,就是一个列表然后循环的将数据填充进去,可以看到数据依然是从父组件传递过来的

<template>
  <div>
    <div class="title">热门推荐</div>
    <ul>
      <li class="item border-bottom" v-for="item of recommendList" :key="item.id">
        <img class="item-img" :src="item.imgUrl">
        <div class="item-info">
          <p class="item-title" v-text="item.title"></p>
          <p class="item-desc" v-text="item.desc"></p>
          <button class="item-button">查看详情</button>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'HomeRecommend',
  props: {
    recommendList: Array
  }
}
</script>

数据的模拟

我们的数据本来是应该通过请求后端的API进行获取,但目前并没有后端的系统

所以我们可以通过mock数据来实现数据的模拟

在现目的根目录下有一个static文件夹,我们可以在下面新建一个mock文件夹,将数据放在mock文件夹下

static
..mock
....city.json
....detail.json
....index.json

在Vue项目中static目录下的文件是可以直接访问的,通过 http://localhost:8080/static/mock/city.json

但是我们最好不要直接请求这个url去或取数据,因为如果这么写了在代码上线的时候,所有的ajax请求的url都要修改为对应的API地址,我们可以通过以下配置,将 static/mock 这个路径映射为 api

在config目录下的index.json文件中,修改 proxyTable 中的内容

这样我们就可以通过 http://localhost:8080/api/city.json 获取到数据了

proxyTable: {
      '/api': {
        target: 'http://127.0.0.1:8080',
        pathRewrite: {
          '^/api': '/static/mock'
        }
      }
    },

数据的获取

在Home.vue中编写我们的ajax请求的逻辑代码

我们需要发送ajax请求,就需要安装 axios 这个第三方库

安装完成后在Home.vue中引入并使用它

然后通过数据绑定的方式传递给子组件

<template>
  <div>
    <home-header></home-header>
    <home-swiper :swiperList="swiperList"></home-swiper>
    <home-icons :iconList="iconList"></home-icons>
    <home-recommend :recommendList="recommendList"></home-recommend>
  </div>
</template>

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

export default {
  name: 'Home',
  components: {
    HomeSwiper,
    HomeHeader,
    HomeIcons,
    HomeRecommend
  },
  data () {
    return {
      city: '',
      lastCity: '',
      swiperList: [],
      iconList: [],
      recommendList: []
    }
  },
  methods: {
    getHomeInfo () {
      axios.get('api/index.json?city=' + this.city).then(this.getHomeInfoSuccess)
    },
    getHomeInfoSuccess (res) {
      res = res.data
      if (res.ret && res.data) {
        const data = res.data
        this.city = data.city
        this.swiperList = data.swiperList
        this.iconList = data.iconList
        this.recommendList = data.recommendList
      }
    }
  },
  mounted () {
    this.lastCity = this.city
    this.getHomeInfo()
  },
  // keepalive的生命周期函数
  activated () {
    if (this.lastCity !== this.city) {
      this.lastCity = this.city
      this.getHomeInfo()
    }
  }
}
</script>

注意:activated这个生命周期函数,用于判断我们当前城市有没有被修改,如果修改则重新发送请求

这个生命周期函数是keepalive的生命周期函数,keepalive可以然组件的内容缓存,这样就没必要每次切换组件都重新发送请求,所以,这里加上判断,使得我们只有在修改了城市数据后才重新发送请求

城市页的开发

城市页的目录结构

city               # 主页目录
..components       # 主页中的组件目录
....Alphabet.vue   # 右侧字母导航组件
....Header.vue     # 顶部组件
....List.vue       # 城市列表组件
....Search.vue     # 搜索组件
..City.vue         # 城市主页组件

路由配置

在路由中引入city组件

import City from '@/pages/city/City'

routers 中添加一个路由对象

{
      path: '/city',
      name: 'city',
      component: City
    }

记得在首页的Header组件中添加路由入口,也就是写一条 <router-link to="/city">

CityHeader的开发

顶部只需要一个标题和返回首页按钮,所以非常简单,就不过多解释

<template>
  <div class="header">
    <div class="header-left">
      <router-link to="/">
        <div class="iconfont back-icon">&#xe624;</div>
      </router-link>
    </div>
    <div class="header-title">
      <span>城市选择</span>
    </div>
  </div>
</template>

搜索组件的开发

搜索组件需要一个输入框和一个浮层,当输入内容的时候才显示浮层,清空输入的时候,隐藏浮层

浮层中是匹配到的城市列表,当点击一个城市后,返回到首页,没有匹配的数据时,给用户一个提示

实现方式:

  • v-model 给输入框做一个双向数据绑定一个 keyword 的数据
  • 监听 keyword 如果有数据改变则进行匹配,并把匹配的数据保存到 list 中,如果数据为空,则清空 list
  • 在浮层中循环 list 中的数据,并给它们绑定一个点击事件 @click="handleCityClick(item.name)"
  • methods 中编写回调函数,这个函数的作用是修改vuex的数据(vuex在后面介绍),并返回首页
  • 还需要一个计算属性,计数 list 的值,判断是否给用户提示没有匹配数据
  • 最后还需要引入一个第三方库 better-scroll 用于页面的滚动(具体使用方法后面介绍)
<template>
  <div>
    <div class="search">
      <input class="search-input" type="text" placeholder="输入城市名或拼音" v-model="keyword"/>
    </div>
    <div class="search-content" v-show="keyword" ref="search">
      <ul>
        <li class="search-item border-bottom"
            v-for="item of list" :key="item.id"
            v-text="item.name" @click="handleCityClick(item.name)"></li>
        <li class="search-item border-bottom" v-show="hasNoData">没有找到匹配数据</li>
      </ul>
    </div>
  </div>
</template>

<script>
import Bscroll from 'better-scroll'
export default {
  name: 'CitySearch',
  props: {
    cities: Object
  },
  data () {
    return {
      keyword: '',
      timer: null,
      list: []
    }
  },
  computed: {
    hasNoData () {
      return !this.list.length
    }
  },
  methods: {
    handleCityClick (city) {
      this.$store.commit('changeCity', city)
      this.$router.push('/')
    }
  },
  watch: {
    keyword () {
      if (this.timer) {
        clearTimeout(this.timer)
      }
      if (!this.keyword) {
        this.list = []
        return
      }
      this.timer = setTimeout(() => {
        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
      }, 100)
    }
  },
  mounted () {
    this.scroll = new Bscroll(this.$refs.search)
  }
}
</script>

城市列表组件的开发

城市列表组件包含三个部分:当前城市,热门城市和所有城市(按字母顺序排序)

整个组件可以滚动,而Header和搜索部分保持不动,当点击了某个城市之后跳转到首页

实现方式:

  • 当前城市的数据通过vuex获取(vuex在后面介绍)
  • 热门城市的数据和所有城市的数据通过父元素获取
  • 使用第三方库 better-scroll 实现页面的滚动
  • 循环所有数据,为每个数据绑定一个点击事件,点击事件的实现方法和搜索中的相同

Bscroll 的使用方法:

  • 引入对象 import Bscroll from 'better-scroll'
  • 在生命周期函数中创建实例 this.scroll = new Bscroll(this.$refs.wrapper) 传入一个页面元素对象
  • 元素标签需要具有 ref 属性,然后才能通过 this.$refs.wrapper 获取到页面元素对象
  • 在页面上传入Bscroll 实例的元素下需要再包裹一层元素,这层元素即为滚动区域
<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" v-text="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 hotCities" :key="item.id"
               @click="handleCityClick(item.name)">
            <div class="button" v-text="item.name"></div>
          </div>
        </div>
      </div>
      <div class="area" v-for="(item, key) of cities" :key="key" ref="key">
        <div class="title border-topbottom" v-text="key"></div>
        <div class="button-list">
          <div class="button-wrapper"
               v-for="city of item" :key="city.id"
               @click="handleCityClick(city.name)">
            <div class="button" v-text="city.name"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import Bscroll from 'better-scroll'
import { mapState } from 'vuex'
export default {
  name: 'CityList',
  props: {
    hotCities: Array,
    cities: Object,
    letter: String
  },
  computed: {
    ...mapState({
      currentCity: 'city'
    })
  },
  methods: {
    handleCityClick (city) {
      this.$store.dispatch('changeCity', city)
      this.$router.push('/')
    }
  },
  watch: {
    // 绑定一个监听事件一旦letter的值改变则触发函数
    letter () {
      if (this.letter) {
        // 获取到letter对应的元素,并滚动到对应的元素
        const element = this.$refs[this.letter][0]
        this.scroll.scrollToElement(element)
      }
    }
  },
  mounted () {
    this.scroll = new Bscroll(this.$refs.wrapper)
  }
}
</script>

注意 letter 数据,这个数据是需要和其他组件联动的数据(后面介绍)

右侧字母导航组件的开发

右侧字母导航栏的页面结构很简单,就是将所有的字母以列表的方式放在右侧固定不动

但它需要实现一些功能:点击某个字母,城市列表页跳转到相应位置,可以滚动,手指再上面滚动的时候,城市列表页跟着切换,这两个功能的实现都需要和其他组件进行联动,所以相对比较复杂

点击切换的实现方式:

  • 从父组件获取全部的城市列表,这个列表包含了所有的城市对应的首字母,将它们循环输出到屏幕上
  • 给每一列元素绑定一个 ref 属性,便于以后获取当前元素对象
  • 添加一个点击事件,在回调函数中向外触发一个 change 事件,并将当前点击元素的内容传递出去
  • 在父组件中定义一个 letter 数据,并且监听子组件,一旦监听到 change 事件,就修改 letter 的值
  • letter 传递到城市列表组件,然后给他一个监听,一旦发送数据改变,则通过 letter 的值滚动页面

滚动切换的实现方式:

  • 为每个列表元素绑定三个事件监听:@touchstart@touchmove@touchend
  • 在生命周期函数 updated 中获取到字母A距离顶部的位置
  • 设置一个状态码 touchStatus 默认为false,当监听到 @touchstart 的时候将它置为true
  • 当手指移动的时候,触发 @touchmove 事件,在回调函数中获取到触控点距离顶部的高度
  • 然后和 startY 相减,差值在处以一个字母的高度,在向下取整,获得字母对应的索引值
  • 通过计算属性将这个索引转换为对应的字母,然后向外触发 change 事件,将字母作为参数传出
  • 之后逻辑和上面点击切换的相同
<template>
  <ul class="list">
    <li
      class="item"
      v-for="item of letters" :key="item"
      v-text="item"
      :ref="item"
      @click="handleLetterClick"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
    ></li>
  </ul>
</template>

<script>
export default {
  name: 'CityAlphabet',
  props: {
    cities: Object
  },
  data () {
    return {
      touchStatus: false,
      startY: 0,
      timer: null
    }
  },
  updated () {
    this.startY = this.$refs['A'][0].offsetTop
  },
  computed: {
    letters () {
      const letters = []
      for (let i in this.cities) {
        letters.push(i)
      }
      return letters
    }
  },
  methods: {
    handleLetterClick (e) {
      // 向外触发一个事件
      this.$emit('change', e.target.innerText)
    },
    handleTouchStart (e) {
      this.touchStatus = true
    },
    handleTouchMove (e) {
      if (this.touchStatus) {
        if (this.timer) {
          clearTimeout(this.timer)
        }
        this.timer = setTimeout(() => {
          // 获取到触控点距离顶部的高度
          const touchY = e.touches[0].clientY - 79
          // 两个的差值在处以一个字母的高度,在向下取整,获得字母对应的索引值
          const index = Math.floor((touchY - this.startY) / 20)
          if (index >= 0 && index < this.letters.length) {
            this.$emit('change', this.letters[index])
          }
        }, 16)
      }
    },
    handleTouchEnd (e) {
      this.touchStatus = false
    }
  }
}
</script>

城市主页面的实现

letter 的值的含义在上面已经详细介绍,citieshotCities 的值都是通过Ajax获取到的

<template>
  <div>
    <city-header></city-header>
    <city-search :cities="cities"></city-search>
    <city-list :cities="cities" :hotCities="hotCities" :letter="letter"></city-list>
    <!--监听change事件,绑定一个回调函数-->
    <city-alphabet :cities="cities" @change="handleLetterClick"></city-alphabet>
  </div>
</template>

<script>
import axios from 'axios'
import CityHeader from './components/Header'
import CitySearch from './components/Search'
import CityList from './components/List'
import CityAlphabet from './components/Alphabet'

export default {
  name: 'City',
  components: {
    CityHeader,
    CitySearch,
    CityList,
    CityAlphabet
  },
  data () {
    return {
      cities: {},
      hotCities: [],
      letter: ''
    }
  },
  methods: {
    getCityInfo () {
      axios.get('/api/city.json').then(this.handleGetCityInfoSuccess)
    },
    handleGetCityInfoSuccess (res) {
      res = res.data
      if (res.ret && res.data) {
        const data = res.data
        this.cities = data.cities
        this.hotCities = data.hotCities
      }
    },
    // 修改letter的值,传递给list组件
    handleLetterClick (letter) {
      this.letter = letter
    }
  },
  mounted () {
    this.getCityInfo()
  }
}
</script>

至此城市页的开发基本完成,但还有一个地方没有解释

vuex实现数据共享

vuex可以简单理解为一个数据仓库

它由三个部分组成Actions、Mutations、State它们的关系如下图所示

1534555154935

我们在项目根目录(src文件夹)下新建一个store文件夹,在里面创建3个核心组成对应的JS文件,再创建一个 index.js 作为整个vuex的根组件

state.js 文件如下,这个文件中有一个 city 数据,这个数据是我们希望全局共享的

let defaultCity = '上海'
try {
  if (localStorage.city) {
    defaultCity = localStorage.city
  }
} catch (e) {}

export default {
  city: defaultCity
}

actions.js 文件如下,它定义了一个方法,这个方法是向 mutations 提交一个请求

export default {
  changeCity (ctx, city) {
    ctx.commit('changeCity', city)
  }
}

mutations.js 文件如下,它是最终更改数据的地方

export default {
  changeCity (state, city) {
    state.city = city
    try {
      localStorage.city = city
    } catch (e) {}
  }
}

index.js 就是对前面的文件的整合

import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import actions from './actions'
import mutations from './mutations'

Vue.use(Vuex)

export default new Vuex.Store({
  state: state,
  actions: actions,
  mutations: mutations
})

vuex的使用

既然我们已经定义了vuex,那么就来看看如何使用吧。其实前面已经提到过了

如城市列表页里,我们需要先引入vuex,然后通过一个计算属性,将数据做一个映射,通过该映射,现在的currentCity 就相当于我们再vuex中定义的 city

然后在下面的methods中,我们在回调函数中发起了对 city 的修改请求,通过 $store.dispatch 可以调用 actions.js 中的方法,例如这里调用了 changeCity 这个方法,这个方法并不会真正的改变数据,在 action 中,我们需要向 mutation 提交修改数据的请求才可以

<script>
import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState({
      currentCity: 'city'
    })
  },
  methods: {
    handleCityClick (city) {
      this.$store.dispatch('changeCity', city)
      this.$router.push('/')
    }
  },
}
</script>

当然我们也可以直接向 mutations.js 提交数据,我们可以通过 $store.commit 调用 mutations.js 中的函数

  methods: {
    handleCityClick (city) {
      this.$store.commit('changeCity', city)
      this.$router.push('/')
    }
  },

既然我们已经使用了vuex,那么首页的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>
      <span>输入城市/景点/游玩主题</span>
    </div>
    <router-link to="/city">
      <div class="header-right">
        <span v-text="this.city"></span>
        <span class="iconfont arrow-icon">&#xe64a;</span>
      </div>
    </router-link>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  name: 'HomeHeader',
  computed: {
    ...mapState(['city'])
  }
}
</script>

详情页的开发

详情页的目录结构

detail
..components
....Banner.vue
....Header.vue
....List.vue
..Detail.vue

路由配置

在路由中引入详情页组件

import Detail from '@/pages/detail/Detail'

添加一个route

{
      path: '/detail/:id',
      name: 'detail',
      component: Detail
    }

注意:这里使用了动态路由配置,通过id传值

画廊的开发

我们希望实现的功能是,当点击图片的时候,可以显示图片的画廊区域,再次点击退出画廊

<template>
  <div>
    <div class="banner" @click="handleBannerClick">
      <img class="banner-img" :src="bannerImg"/>
      <div class="banner-info">
        <div class="banner-title" v-text="sightName"></div>
        <div class="banner-number">
          <span class="banner-icon iconfont">&#xe692;</span>
          <span>29</span>
        </div>
      </div>
    </div>
    <fade-animation>
      <common-gallary :imgs="gallaryImgs" v-show="showGallary" @close="handleBannerClick">
      </common-gallary>
    </fade-animation>
  </div>
</template>

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

点击弹出图层的组件,没啥说的vue基本的动画效果

<template>
  <transition>
    <slot></slot>
  </transition>
</template>

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

<style lang='stylus' scoped>
  .v-enter, .v-leave-to
    opacity 0
  .v-enter-active, .v-leave-active
    transition opacity .5s
</style>

画廊组件,和前面的轮播图使用方式类似

<template>
  <div class="container" @click="handleGallaryClick">
    <div class="wrapper">
      <swiper :options="swiperOptions">
        <swiper-slide v-for="(item, index) of imgs" :key="index">
          <img class="gallary-img" :src="item">
        </swiper-slide>
        <div class="swiper-pagination"  slot="pagination"></div>
      </swiper>
    </div>
  </div>
</template>

<script>
export default {
  name: 'CommonGallary',
  props: {
    imgs: Array
  },
  data () {
    return {
      swiperOptions: {
        pagination: '.swiper-pagination',
        paginationType: 'fraction',
        observeParents: true,
        observer: true
      }
    }
  },
  methods: {
    handleGallaryClick () {
      this.$emit('close')
    }
  }
}
</script>

Header组件的开发

这个Header组件有一个动画的过程,一开始只有一个返回按钮,当我们将页面往下拉的时候,到达一定高度,Header才会显示出来

实现方式:

  • 对全局事件进行监听,获取当前页面滚动的距离
  • 当距离大于60px的时候修改opacity的值,这将实现一个渐隐渐现的效果
  • 最后对应全局事件需要在页面切出去的时候解除绑定
<template>
  <div>
    <router-link to="/" tag="div" class="header-abs" v-show="showAbs">
      <div class="iconfont back-icon">&#xe624;</div>
    </router-link>
    <div class="header-fixed" v-show="!showAbs" :style="opacityStyle">
      <div class="header-left">
        <router-link to="/">
          <div class="iconfont back-icon">&#xe624;</div>
        </router-link>
      </div>
      <div class="header-title">景点详情</div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'DetailHeader',
  data () {
    return {
      showAbs: true,
      opacityStyle: {
        opacity: 0
      }
    }
  },
  methods: {
    handleScroll () {
      const top = document.documentElement.scrollTop
      if (top > 60) {
        let opacity = top / 140
        opacity = opacity > 1 ? 1 : opacity
        this.opacityStyle = {opacity}
        this.showAbs = false
      } else {
        this.showAbs = true
      }
    }
  },
  activated () {
    window.addEventListener('scroll', this.handleScroll)
  },
  // 记得对全局事件解绑
  deactivated () {
    window.removeEventListener('scroll', this.handleScroll)
  }
}
</script>

List组件的开发

List组件中使用了递归组件,能否使用递归组件,这是根据需求和数据来决定的(老实说,这里我觉得并不需要)

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

<script>
export default {
  name: 'DetailList',
  props: {
    list: Array
  }
}
</script>

详情主页面的开发

这个主页面和其他主页面的功能并无二致,都是通过ajax获取数据,然后将数据分发到各自下面的子组件中

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

<script>
import axios from 'axios'
import DetailBanner from './components/Banner'
import DetailHeader from './components/Header'
import DetailList from './components/List'
export default {
  name: 'Detail',
  components: {
    DetailBanner,
    DetailHeader,
    DetailList
  },
  data () {
    return {
      categoryList: [],
      sightName: '',
      bannerImg: '',
      gallaryImgs: []
    }
  },
  methods: {
    getDetailInfo () {
      axios.get('/api/detail.json', {
        params: {
          id: this.$route.params.id
        }
      }).then(this.handleGetDetailSuccess)
    },
    handleGetDetailSuccess (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.categoryList = data.categoryList
      }
    }
  },
  mounted () {
    this.getDetailInfo()
  }
}
</script>

最终

还有一个小细节,就是路由的配置

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
    }
  ],
  scrollBehavior (to, from, savedPosition) {
    return {x: 0, y: 0}
  }
})

可以看到,我们在路由的最后加了一段代码,这段代码的含义是,当我们做路由跳转的时候,每次都需要将页面定位到最顶部,如果不加,你可以自己看看效果

标签: Vue前端项目笔记
添加评论
评论列表
没有更多内容