npm install --global vue-cli
vue init webpack ‘文件夹名称’
npm run dev
/main.js 项目的入口文件
import Vue from 'vue'
import App from './App'
import router from './router'
new Vue({
el: '#app',
router,
components: { App },
template: ' '
})
/App.vue 项目默认的渲染文件
<template>
<div id="app">
<keep-alive exclude="Detail">
<router-view/> //显示当前地址所对应的内容
keep-alive>
div>
template>
<script>
export default {
name: 'App'
}
script>
<style>
style>
router/main.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
}
],
})
1./index.html
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> //页面的比例始终是一比一,用户端不可以进行窗口的扩大缩小
<title>Traveltitle>
head>
<body>
<div id="app">div>
body>
html>
import ‘./assets/styles/reset.css’
import ‘./assets/styles/border.css’
import fastclick from ‘fastckick’
fastclick.attach(document.body)
import ‘./assets/styles/iconfont.css’
图标代码
图表代码可以在购物车中的图表上查看
install stylus --save
install stylus-loader --save
设置变量 $bgColor = #00bcd4
import ‘~styles/assets/styles/varibles.styl’
$bgColor
‘styles’ : resolve(‘src/assets/styles’),
import ‘swiper/dist/css/swiper.css’
import VueAwesomeSwiper from ‘vue-awesome-swiper’
Vue.use(VueAwesomeSwiper,‘可以给个参数’)
<template>
<div class="wrapper">
<div class="iconw">
<div class="iconw-img">
<swiper :options="swiperOption" v-if="showSwiper" > //应用轮播插件
<swiper-slide v-for="item of list" :key="item.id">
<img class="swipper-img" :src="item.imgUrl" />
</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div> //应用下面的选择点
</swiper>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HomeSwiper',
props: {
list: Array
},
data () {
return {
swiperOption: {
pagination: '.swiper-pagination',
loop: true //可以从最好一个滚回到第一个
}
}
},
computed: {
showSwiper () {
return this.list.length
}
}
}
</script>
<style lang="stylus" scoped>
.wrapper >>> .swiper-pagination-bullet-active //样式穿透,将设置应用在非当前页面的class样式中,不受scoped的限制
background: #fff
.wrapper
overflow: hidden
height: 0
padding-bottom: 30.9% //将高度设置为宽度的30.9%
background: green
.iconw
position: relative
.iconw-img
position: absolute
top: 0
left: 0
right: 0
.swipper-img
width: 100%
</style>
data () {
return {
swiperOption: {
autoplay: false //不允许自动播放
}
}
},
computed: {
pages () {
const pages = []
this.list.forEach((item, index) => {
const page = Math.floor(index / 8)
if (!pages[page]) {
pages[page] = []
}
pages[page].push(item)
})
return pages
}
}
.icon-desc
position: absolute
left: 0
right: 0
bottom: 0
height: .44rem
line-height: .44rem
text-align: center
color: $darkTextColor
//以下三句话实现隐藏加点
overflow: hidden
white-sapce: nowrap
text-overflow: ellipsis
npm install axios --save
proxyTable: {
'/api': {
target: 'http://localhost:8080',
pathRewrite: {
'^/api': '/static/mock'
}
}
import axios from ‘axios’
mounted () {
this.getHomeInfo()
},
methods: {
getHomeInfo () {
axios.get('/api/index.json?city=' + this.city).then(this.getHomeInfoSucc)
},
getHomeInfoSucc (res) {
res = res.data
if (res.ret && res.data) {
const data = res.data
this.swiperList = data.swiperList
this.iconList = data.iconList
this.recommendList = data.recommendList
this.weekendList = data.weekendList
this.cities = data.cities
}
}
}
props:{
cities:Object
}
- {{item}}
/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
}
],
scrollBehavior (to, from, savedPosition) {
return {x: 0, y: 0} //页面切换后从顶部开始显示内容
}
})
Header中点击城市名,将页面跳转到城市选择页面
<router-link to='/city'>
<div class="header-right">
{{this.city}}
<span class="iconfont arrow-icon">span>
div>
router-link>
搜索框样式
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.search
height: .72rem
padding: 0 .1rem
background: $bgColor
.search-input
box-sizing: border-box
width: 100%
height: .62rem
padding: 0 .1rem
line-height: .62rem
text-align: center
border-radius: .06rem
color: #666
.search-content
z-index: 1
overflow: hidden
position: absolute
top: 1.58rem
left: 0
right: 0
bottom: 0
background: #eee
.search-item
line-height: .62rem
padding-left: .2rem
color: #666
background: #fff
style>
.border-topbottom
&:before
border-color: #ccc
&:after
border-color: #ccc
npm install better-scroll --save
- 引入
import Bscroll from ‘better-scroll’
- 在mounted中实例化
mounted () {
this.scroll = new Bscroll(this.$refs.wrapper)
},字母表的设计(alphabet.vue)
零碎的知识点
- 通过点击获取li标签里面的文本
console.log(e.target.innerText)
- 样式
<style lang="stylus" scoped> @import '~styles/varibles.styl' .list display: flex list-style:none flex-direction: column justify-content: center position: absolute top: 1.58rem right: 0 bottom: 0 width: .4rem .item line-height: .44rem text-align: center color: $bgColor style>
兄弟组件的联动-点击转到对应区域
目的:实现点击一个字母(alphabet.vue),将城市选择(cityList.vue)跳转到字母对应的区域
首先在字母的li上绑定一个事件
- {{key}}
实现这个方法
methods: {
handleLetterClick: function (e) {
this.$emit(‘change’, e.target.innerText)
},主页面中对这个组件进行监听
实现handleLetterChange这个方法
handleLetterChange (letter) {
this.letter = letter
}将letter传递给cityList.vue组件
cityList.vue中进行接收
props: {
hot: Array,
cities: Object,
letter: String
},利用侦听器进行侦听,若letter改变,则显示对应的区域
watch: { letter () { if (this.letter) { const element = this.$refs[this.letter][0] this.scroll.scrollToElement(element) } } }
在li标签中进行显示
<div class="area" v-for="(item,key) of cities" :key="key" :ref="key"> <div class="title border-topbottom">{{key}}div> //key中的值为A,B,C,D... <div class="item-list"> <div class="item border-bottom" v-for="innerItem of item" :key="innerItem.id" > {{innerItem.name}} //为具体的城市名称 div> div> div>
兄弟组件的联动-在字母表上下拖拽转到对应区域
目的:实现在字母表(alphabet.vue)上滑动,将城市选择(cityList.vue)跳转到字母对应的区域
- 在li标签中绑定一个事件
@touchstart.prevent="handleTouchStart"
@touchmove="handleTouchMove" @touchend="handleTouchEnd">{{item}}
- 实现对应的三个方法
props: { cities: Object }, computed: { letters () { const letters = [] for (let i in this.cities) { letters.push(i) } return letters } }, data () { return { touchStatus: false, startY: 0, timer: null } }, updated () { //当页面的数据被更新,页面完成了渲染 this.startY = this.$refs['A'][0].offsetTop //A距离顶部的高度 }, methods: { handleTouchStart () { 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 () { this.touchStatus = false } }
搜索功能的实现
Search.vue接收来自主页面的cities这个参数,并设置一个div,在查询内容不为空的条件下(v-show)显示,展示查询结果。
零散的知识点
- 运算放在computed中
<template> <div> <div class="search"> <input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音"/> div> <div class="search-content" ref="search" v-show="keyword"> <ul> <li class="search-item border-bottom" v-for="item of list" @click="handleCityClick(item.name)" :key="item.id"> {{item.name}}li> <li class="search-item border-bottom" v-show="hasNoData">没有找到匹配数据li> ul> div> div> template> <script> import Bscroll from 'better-scroll' import { mapMutations } from 'vuex' export default { name: 'CitySearch', props: { cities: Object }, mounted () { this.scroll = new Bscroll(this.$refs.search) }, data () { return { keyword: '', list: [], timer: null } }, computed: { hasNoData () { return !this.list.length } }, methods: { handleCityClick (city) { this.changeCitys(city) this.$router.push('/') }, ...mapMutations(['changeCitys']) }, 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) } } } script>
使用Vuex实现数据共享
使用Vuex
- 安装
npm install vuex --save
- 新建src/store/index.js
import Vue from 'vue' import Vuex from 'vuex' import state from './state' import mutations from './mutations' Vue.use(Vuex) export default new Vuex.Store({ state: state, mutations: mutations, getters: { doubleCity (state) { return state.city + ' ' + state.city } } })
- 在main.js中引入
import store from ‘./store’
new Vue({ el: '#app', store: store, router, components: { App }, template: '
' })
- 在Header.vue中进行操作,将显示城市的内容改为
this. s t o r e . s t a t e . c i t y 之 所 以 每 个 页 面 都 可 以 使 用 store.state.city 之所以每个页面都可以使用 store.state.city之所以每个页面都可以使用store,是因为他在主js中引入,之后被派发到每一个页面
- 在城市选择页面List.vue,将当前城市改为
this.$store.state.city
- 实现点击一个城市名,则将首页的城市显示为该城市名
methods: { handleCityClick (city) { this.changeCitys(city) this.$router.push('/') }, ...mapMutations(['changeCitys']) },
- store/mutations.js
export default { changeCitys (state, city) { state.city = city try { localStorage.city = city } catch (e) {} } }
- store/state.js
let defaultCity = '上海' try { if (localStorage.city) { defaultCity = localStorage.city } } catch (e) { } export default { city: defaultCity }
使用localstorage记住用户的选择
优化
- vuex方法写法优化
先引入
import { mapState, mapGetters } from 'vuex'
this.$store.state.city 的写法优化
computed: {
…mapState([‘city’]),
}
使用时
{{this.city}}this.$store.state.city 的写法优化2
computed: {
…mapState({
currentCity: ‘city’
})
使用时
{{this.currentCity}}2.vuex中的getters
类似于computed的作用,可以使用数据计算数据
使用keep-alive优化网页性能
- 在App.vue中添加代码keep-alive
意思是;路由的内容加载一次以后,就将内容放到内存之中,下一次再取这个路由的时候直接从内存中取出,不需要重新渲染
- 发送ajax数据请求的时候带一个参数city
axios.get(’/api/index.json?city=’ + this.city).then(this.getHomeInfoSucc)
引入city这个参数的方法是vuex
- import { mapState } from ‘vuex’
- computed: {
…mapState([‘city’])
},此时由于包含在keep-alive中并不会重新发送请求,可以使用activated,他在页面每次重新显示的时候被加载
activated () { if (this.lastCity !== this.city) { this.lastCity = this.city this.getHomeInfo() //这个方法发送ajax请求 } },
旅游网站详情页面开发
详情页动态路由及banner布局
- 在内容的外部包裹路由跳转
- 在index.js中加入该路由信息(带有参数)
{ path: '/detail/:id', name: 'Detail', component: Detail }
- 实现文字区域背景图片的渐变色
background-image: linear-gradient(top, rgba(0, 0, 0, 0),rgba(0, 0, 0, 0.8))
公用图片画廊组件拆分
detail/banner.vue
<template> <div> <div class="banner" @click="handleBannerClick"> <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">span>{{this.bannerImgs.length}}div> div> div> <fade-animation> <common-gallary :imgs="bannerImgs" v-show="showGallary" @close="handleGallaryClose">common-gallary> fade-animation> div> template> <script> import commonGallary from 'common/gallary/Gallary' import FadeAnimation from 'common/fade/Fade' export default { name: 'DetailBanner', props: { sightName: String, bannerImg: String, bannerImgs: Array }, data () { return { showGallary: false } }, methods: { handleBannerClick () { this.showGallary = true }, handleGallaryClose () { this.showGallary = false } }, components: { commonGallary, FadeAnimation } } script> <style lang="stylus" scoped> .banner position: relative overflow: hidden height: 0 padding-bottom: 51% .banner-img width: 100% .banner-info display: flex position: absolute left: 0 right: 0 bottom: 0 line-height: .6rem color: #fff background-image: linear-gradient(top, rgba(0, 0, 0, 0),rgba(0, 0, 0, 0.8)) .banner-title flex:1 font-size: .32rem padding: 0 .2rem .banner-number margin-top: .14rem padding: 0 .4rem line-height: .32rem height: .32rem border-radius: .2rem background: rgba(0, 0, 0, .8) font-size: .24rem .banner-icon font-size: .24rem style>
common/gallary.vue
<template> <div class="container" @click='handleGallaryClick'> <div class="wrapper"> <swiper :options="swiperOptions"> <swiper-slide v-for="(item,index) in 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: 'CommonGallery', props: { imgs: { type: Array, default () { return ['https://img1.qunarzz.com/vs_ceph_vs_tts/4d217b8b-a26b-4c50-80ef-c37bcbb8005a.jpg', 'http://img1.qunarzz.com/vs_ceph_vs_tts/b60743c9-5a9c-407a-8e4f-a73344f63ffc.jpg_r_500x333x90_5a5cead1.jpg'] } } }, data () { return { swiperOptions: { pagination: '.swiper-pagination', paginationType: 'fraction', observeParents: true, observer: true } } }, methods: { handleGallaryClick () { this.$emit('close') } } } </script> <style lang="stylus" scoped> .container >>> .swiper-container overflow: inherit .container display: flex z-index: 99 position: fixed flex-direction: column justify-content: center left: 0 right: 0 top: 0 bottom: 0 background: #000 .wrapper height: 0 width: 100% padding-bottom: 100% .gallary-img width: 100% .swiper-pagination color: #fff bottom: -1rem </style>
实现Header渐隐渐显效果(起初显示一个返回按钮,下滑的时候按钮消失,显示另一个返回的箭头)
detail/header.vue
<template> <div> <router-link tag="div" to="/" class="header-abs" v-show='showAbs'> <div class="iconfont header-abs-back">div> router-link> <div class="header-fixed" v-show="!showAbs" :style="opacityStyle" > <router-link to="/"> <div class="iconfont header-fixed-back">div> router-link> 景点详情 div> div> template> <script> export default { name: 'DetailHeader', data () { return { showAbs: true, opacityStyle: { opacity: 0 } } }, activated () { window.addEventListener('scroll', this.handleScroll) }, deactivated () { window.removeEventListener('scroll', this.handleScroll) }, 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 } } } } script> <style lang="stylus" scoped> @import '~styles/varibles.styl' .header-abs position: absolute left: .2rem top: .2rem width: .8rem height: .8rem line-height: .8rem text-align: center border-radius: .4rem background: rgba(0,0,0,.8) .header-abs-back color: #fff font-size: .4rem .header-fixed z-index: 2 position: fixed top: 0 left: 0 right: 0 height: $headerHeight line-height: $headerHeight text-align: center color: #fff background: $bgColor font-size: .32rem .header-fixed-back position: absolute top: 0 left: 0 width: .64rem text-align: center font-size: .4rem color: #fff style>
对全局时间的解绑
- 在header.vue中的方法
window.removeEventListener(‘scroll’, this.handleScroll)
该方法是对全局的影响,使用deactivated()钩子进行消除影响
list.vue的设计编写
- 关于递归组件
组件的name值,常用于递归组件的调用
<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> {{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 } } </script> <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: .8rem font-size: .32rem padding: 0 .2rem .item-children padding: 0 .2rem </style>
使用Ajax获取动态数据
将detail/list的数据通过Ajax获取
detail.vue<template> <div> <detail-banner :sightName="sightName" :bannerImg="bannerImg" :bannerImgs='gallaryImg' ></detail-banner> <detail-header></detail-header> <div class="content" > <detail-list :list="list"></detail-list> </div> </div> </template> <script> import detailBanner from './components/Banner' import DetailHeader from './components/Header' import DetailList from './components/List' import axios from 'axios' export default { name: 'Detail', components: { detailBanner, DetailHeader, DetailList }, data () { return { sightName: '', bannerImg: '', gallaryImg: [], 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.gallaryImg = data.gallaryImgs this.list = data.categoryList } } }, mounted () { this.getDetailInfo() } } </script> <style lang="stylus" scoped> .content height: 50rem </style>
组件中的name值有什么用
页面拖拽在多个页面之中相互影响
在router/index.js中添加scrollBahavior
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} } })
在项目中加入基础动画
- 在详情的点击进入图片轮播这里加入基础动画
- common/Fade.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>
优化收尾
Vue项目的接口联调
Vue项目的真机测试
Vue项目打包上线