一、搭建项目
vue2对应vue-router3和vuex3,vue3对应vue-router4和vuex4
vue init webpack v2object
npm install
cnpm i node-sass@4.14.1 sass-loader@7.3.1 --save-dev
页面中使用 <style lang="scss" scoped>
npm run dev
二、配置路由
采用路由懒加载,提高性能
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: () => import('@/views/home/home')
}, {
path: '/city',
name: 'City',
component: () => import('@/views/city/city')
}, {
path: '/detail',
name: 'Detail',
component: () => import('@/views/detail/detail')
}
]
})
三、移动端适配
方案1、
(1)设置meta标签使不能缩放拉伸
<meta name="viewport" content="width=device-width,initial-scale=1.0,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
(2)引入reset.css重置样式表,设置根元素是50px,二倍图高度是86px,写成rem就是0.86rem
(3)引入border.css,利用媒体查询区分二倍屏和三倍屏,判断设备像素比,解决1px边框变粗的问题
// main.js
import './assets/styles/reset.css'
import './assets/styles/border.css'
/*二倍屏*/
@media only screen and (-webkit-min-device-pixel-ratio: 2.0) {
.my-border::after {
transform: scaleY(0.5);
webkit-transform: scaleY(0.5);
}
}
/*三倍屏*/
@media only screen and (-webkit-min-device-pixel-ratio: 3.0) {
.my-border::after {
transform: scaleY(0.33);
webkit-transform: scaleY(0.33);
}
}
方案2、安装插件
使用lib-flexible和postcss-pxtorem
1. lib-flexible 用于动态更新根元素的font-size值,即1rem = 多少px
2. postcss-pxtorem 用于将项目中的px单位自动转rem单位
(1)安装lib-flexible
cnpm i -D lib-flexible
(2)在main.js中引入lib-flexible
import 'lib-flexible'
(3)安装postcss-pxtorem,这里下载5.0.0版本不然会报错
cnpm i -D postcss-pxtorem@5.0.0
(4)配置postcss-pxtorem
在根目录下的.postcssrc.js文件中修改
此文件自动生成,若没有,手动添加
module.exports = {
"plugins": {
"autoprefixer": {},
'postcss-pxtorem': {
rootValue: 37.5, // 75表示750设计稿,37.5表示375设计稿
propList: ['*']
}
}
}
vue2移动端适配_vue2 移动端适配_yarKin.的博客-优快云博客
四、讲项目托管到码云空白仓库
(1)在码云新建空白仓库
(2)在项目文件夹下Git Bash Here
(3)执行以下命令
执行以下命令
git init
git add .
git commit -m 'init project'
git remote add origin git@gitee.com:itxxx/text-project.git
如果git remote add origin报错,那么先删除远程仓库,再重新关联就行
git remote rm origin git@gitee.com:itxxx/text-project.git
git push -u origin "master"
五、安装fastclick解决移动端点击时有300ms延迟的问题
npm install fastclick --save
在main.js中引入fastclick,并使用
import fastclick from 'fastclick'
fastclick.attach(document.body)
六、首页开发
抽离公共样式varibles.scss
$bgColor: #00bcd4;
$darkTextColor: #333;
$headerHeight: .86rem;
@mixin uniline {
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
在需要文本溢出隐藏的地方使用scss函数
@include uniline
在页面局部引入varibles.scss
<style lang="scss" scoped>
@import '@/assets/varibles.scss';
.header {
height: $headerHeight;
display: flex;
background-color: $bgColor;
}
</style>
引入iconfont
- 到官网下载想要的图标
- 将iconfont.css放到assets目录下
- 与iconfont.css平级,创建iconfont文件夹,将iconfont.ttf,iconfont.woff,iconfont.woff2放进去
-
到iconfont.css文件中修改路径
-
到main.js中引入iconfont.css文件
- 引用图标
<span class="iconfont"></span>
七、制作轮播图
下载swiper3
cnpm install swiper@3 vue-awesome-swiper@3 --save-dev
在swiper.vue中
<swiper :options="swiperOption" ref="mySwiper">
<swiper-slide>I'm Slide 1</swiper-slide>
<swiper-slide>I'm Slide 2</swiper-slide>
<swiper-slide>I'm Slide 3</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
</swiper>
引入样式和组件
import { swiper, swiperSlide } from "vue-awesome-swiper";
import "swiper/dist/css/swiper.css";
export default {
name: 'HomeSwiper',
components: {
swiper,
swiperSlide
},
data () {
return {
swiperOption: {
loop: true,
autoplay: {
delay: 3000,
stopOnLastSlide: false,
disableOnInteraction: false
},
pagination: {
el: '.swiper-pagination',
clickable: true
}
}
}
}
}
样式穿透,改变小圆点颜色
.wrapper >>> .swiper-pagination-bullet-active {
background: #fff
}
修改swiper轮播图样式,让轮播图高是宽的33%
.wrapper {
overflow: hidden;
width: 100%;
height: 0;
padding-bottom: 31.25%;
background-color: #eee;
}
img {
width: 100%;
}
八、ajax动态渲染数据
下载axios
cnpm i axios
- 在src目录下新建api文件夹,里面放index.js和request.js
- index.js配置请求路径和mock状态管理
- request.js是ajax二次封装
一、在src目录下配置两个文件夹api和config
二、api负责mock数据的管理和ajax请求的二次封装
api/index.js
import request from "./request";
export default {
getData () {
return request({
url: '/home/getData',
method: 'get',
mock: true
})
},
getCityData () {
return request({
url: '/city/getCityData',
method: 'get',
mock: true
})
},
getDetailData (params) {
return request({
url: '/detail/getDetailData',
method: 'get',
mock: true,
data: params
})
},
}
api/request.js
import axios from 'axios'
import config from '../config'
const NETWORK_ERROR = '网络请求异常,请稍后重试...'
// 创建一个axios实例对象
const service = axios.create({
baseURL: config.baseApi
})
// 请求拦截器
service.interceptors.request.use((req) => {
return req
})
// 响应拦截器
service.interceptors.response.use((res) => {
const { code, data, msg } = res.data
// 根据后端协商而定
if (code == 200) {
return data
} else {
// 网络请求错误
console.log(msg || NETWORK_ERROR);
return Promise.reject(msg || NETWORK_ERROR)
}
})
// 封装的核心函数
function request (options) {
options.method = options.method || 'get'
// toLowerCase()转换成小写
if (options.method.toLowerCase() == 'get') {
options.params = options.data
}
// 对mock的处理,拿到mock总开关
let isMock = config.mock
// 总开关为true,单独设置我们想要的接口为false
if (typeof options.mock !== 'undefined' ) {
isMock = options.mock
}
// 对线上环境处理
if (config.env == 'prod') {
// 不给用到mock接口的机会
service.defaults.baseURL = config.baseApi
} else {
service.defaults.baseURL = isMock ? config.mockApi : config.baseApi
}
return service(options)
}
export default request
三、config是对环境的配置,配置baseApi和mockApi
config/index.js
const env = process.env.NODE_ENV || 'prod'
const EnvConfig = {
development: {
baseApi: '/api',
mockApi: 'https://www.fastmock.site/mock/a930ca4e9d1d4e1a33eba0e7f7489f83/api'
},
test: {
baseApi: '//test.future.com/api',
mockApi: 'https://www.fastmock.site/mock/a930ca4e9d1d4e1a33eba0e7f7489f83/api'
},
prod: {
baseApi: '//future.com/api',
mockApi: 'https://www.fastmock.site/mock/a930ca4e9d1d4e1a33eba0e7f7489f83/api'
}
}
export default {
env,
// mock的总开关
mock: true,
...EnvConfig[env]
}
请求ajax数据
在home.vue中
<template>
<div>
<home-header />
<home-swiper :list="swiperList" />
</div>
</template>
<script>
import HomeHeader from './components/header.vue'
import HomeSwiper from './components/swiper.vue'
export default {
components: {
HomeHeader,
HomeSwiper
},
data () {
return {
swiperList: [],
iconList: [],
recommendList: [],
weekendList: []
}
},
methods: {
async getHomeInfo () {
let res = await this.$api.getData()
this.swiperList = res.swiperList
}
},
mounted () {
this.getHomeInfo()
}
}
</script>
<style lang="scss" scoped></style>
loop: true失效问题
数据是写死的时候,能够loop:true是有效的;
数据是动态获取的loop:true就会失效。
解决办法:
加上v-if="list.length"有效解决
computed: {
isShowSwiper () {
return this.list.length
}
}
九、Icons图标开发
实现两行一行四个图标
<template>
<div class="icons">
<div class="icon" v-for="item in list" :key="item.id">
<div class="icon-img-box">
<img :src="item.imgUrl" alt=""/>
</div>
<p>{{ item.desc }}</p>
</div>
</div>
</template>
<script>
import { swiper, swiperSlide } from "vue-awesome-swiper";
import "swiper/dist/css/swiper.css";
export default {
name: 'HomeIcons',
components: {
swiper,
swiperSlide
},
props: {
list: Array
},
data () {
return {
swiperOption: {}
}
}
}
</script>
<style lang="scss" scoped>
@import '@/assets/varibles.scss';
.icons {
margin-bottom: .1rem;
overflow: hidden;
width: 100%;
height: 0;
padding-bottom: 50%;
.icon {
position: relative;
overflow: hidden;
float: left;
width: 25%;
height: 0;
padding-bottom: 25%;
.icon-img-box {
position: absolute;
box-sizing: border-box;
padding: .1rem;
top: 0;
left: 0;
right: 0;
bottom: .44rem;
img {
display: block;
height: 100%;
margin: auto;
}
}
p {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: .44rem;
text-align: center;
line-height: .44rem;
color: #333;
@include uniline;
}
}
}
</style>
图标分页滑动功能,用轮播图+computed,slider里面是双重for循环
Math.floor() 向下取整
forEach() 遍历数组
这里的index = 8 ,所以 page = 1
page = 0 是第一页,page= 1 是第二页
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
}
}
在模板中,外层循环pages,里层循环page
<div class="icons">
<swiper :options="swiperOption">
<swiper-slide v-for="(page, index) of pages" :key="index">
<div class="icon" v-for="item of page" :key="item.id">
<div class="icon-img-box">
<img :src="item.imgUrl" alt=""/>
</div>
<p>{{ item.desc }}</p>
</div>
</swiper-slide>
</swiper>
</div>
热销推荐和周末去哪儿略
十、城市页开发
search.vue
<template>
<div class="search">
<input class="search-input" v-model="keyword" type="text" placeholder="请输入城市名或拼音">
</div>
</template>
<script>
export default {
name: 'CitySearch',
data () {
return {
keyword: ''
}
}
}
</script>
<style lang="scss" scoped>
@import '@/assets/varibles.scss';
.search {
background-color: $bgColor;
height: .68rem;
text-align: center;
.search-input {
height: .6rem;
border-radius: 2px;
width: 98%;
text-align: center;
}
}
</style>
搜索框功能开发
分析数据结构,cities是一个对象,它里面的item是数组
定义一个空list,和防抖的timer变量
data () {
return {
keyword: '',
list: [],
timer: null
}
},
搜索框输入没有匹配到时,list长度为0,展示没有匹配到数据
computed: {
hasNoData () {
return !this.list.length
}
},
- 当产生搜索框输入事件时
- 添加watch侦听器监听keyword的变化
- 先清空定时器,如果keyword不存在就让list为空,否则遍历cities对象
- 外层for in 遍历对象,里层for Each 遍历数组
- indexOf() 方法,匹配到则返回第一个匹配的索引,没找到则返回下标,区分大小写
- 判断两个,value.spell.indexOf(this.keyword) > -1 判断字母是否能匹配到,或者
-
value.name.indexOf(this.keyword) > -1 判断文字是否能匹配到
-
只要有一个匹配到就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);
}
}
better-csroll的使用
下载better-scroll
cnpm install better-scroll -S
在组件中引入
import BScroll from "better-scroll"
3)页面中使用
- DOM结构要求外层wrapper,里层content
- 初始化BScroll的类时机很重要,否则会造成不能滚动,所以在初始化时可以用$nextTick()包裹一下
- 父元素必须有高度且,有overflow: hidden属性,以上三个条件缺一不可
<template>
<div class="wrapper" ref="wrapper">
<ul class="content">
<li>...</li>
<li>...</li>
...
</ul>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
mounted() {
this.$nextTick(() => {
this.scroll = new Bscroll(this.$refs.wrapper, {})
})
}
}
</script>
<style lang="scss" scoped>
.wrapper {
overflow: hidden;
height: 500px;
}
</style>
如果切换机型才能滚动,设置 observeDOM: true 就可以了
this.scroll = new BScroll(this.$refs.search, {
observeDOM: true
})
城市列表页开发
list.vue注意是双重循环
<div class="area" v-for="(items, key) of cities" :key="key">
<div class="title">{{ key }}</div>
<div class="item-list">
<div class="item border-bottom" v-for="item of items" :key="item.id">{{ item.name }}</div>
</div>
</div>
alphabet组件
定义letters这个计算属性,字母表固定定位
<template>
<div class="alphabet">
<div
class="item"
v-for="item of letters"
:key="item"
:ref="item"
>
{{ item }}
</div>
</div>
</template>
<script>
export default {
name: 'CityAlphabet',
props: {
cities: Object,
hotCities: Array
},
computed: {
letters () {
const letters = []
for (let i in this.cities) {
letters.push(i)
}
return letters
}
}
}
</script>
<style lang="scss" scoped>
@import '@/assets/varibles.scss';
.alphabet {
display: flex;
flex-direction: column;
justify-content: center;
position: fixed;
top: 1.58rem;
right: 0;
bottom: 0;
width: .4rem;
.item {
line-height: .45rem;
text-align: center;
color: $bgColor;
}
}
</style>
实现点击alphabet组件,list组件滚动到相应区域
实现思路
- 兄弟组件传值,可以通过父组件进行
- 设置alphabet组件的$emit,点击字母时触发change事件,并携带文本内容
- 父组件City绑定这个change事件,监听到这个事件后,触发handleLetterChange事件,并接收这个事件的参数,然后把这个参数以单向绑定的方式传递给子组件List
- 子组件List使用props接收letter,然后使用watch方式监听letter,当letter改变时,使用better-scroll提供的方法滚动到标签为lettter的画面
要滚动的DOM元素绑定 :ref=key , this.$refs[this.letter][0]表示拿到所点击的字母所在的DOM位置
父容器list 一定要记得加overflow: hidden; position: absolute; 这两个属性,否则无法滚动
拖动字母表滚动到对应区域
- 在Alphabet组件中循环元素绑定3个事件,在data中定义一个状态标识touchStatus,当开始按住时touchStatus=true,结束按住时=false,
- 然后在计算属性中定义一个letters,用它来把cityes这个对象转换成数组,所以letters是26个字母组成的一个数组,ref绑定item,使$refs成为26个DOM对象的数组,
- 在Move事件中求出A元素到绿色框的距离,还有手指按住时某元素到绿色框的距离,然后就可以取得手指滑动时字母的下标index,
startY是A元素到绿色框的距离
touchY是手指滑动的距离
handleTouchMove (e) {
if (this.touchStatus) {
const startY = this.$refs['A'][0].offsetTop
const touchY = e.touches[0].clientY - 85
const index = Math.floor((touchY - startY) / 20)
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index])
}
}
},
- index等于两者相减后除以字母高度的值,然后向外发出一个change事件,携带这个字母就可以了
优化一下
因为startY在handleTouchMove中每次都会重新计算,而它是一个固定值,所以我们可以这样优化
在data中定义startY,当页面初次渲染时,city父组件中初始化cities的值其实是一个空对象,此时Aphlabet子组件的cities也是一个空对象,也就是Aphlabet子组件中什么内容也不会显示出来,当city父组件通过AJAX获取数据后,cities的值才发生变化,Aphlabet才被渲染出来,当Aphlabet子组件中的数据发生变化时,Aphlabet就会重新渲染,重新渲染之后就会产生Aphlabet列表,这个时候updated生命周期钩子就会被执行,去获取A这个字母的DOM距上端的距离
第一步在data中定义startY,将 startY 放在updated钩子中
第二步我们要做一个函数防抖,限制一下handleTouchMove函数执行频率
十一、Vuex数据共享
1、安装,vue2对应vuex3,vue3对应vuex4
cnpm install vuex@3 -S
2、创建一个store文件夹,新建index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
city: '珠海'
}
})
3、在main.js中引入
4、在组件中使用
{{ this.$store.state.city }}
模块化store
index.js
// 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,
mutations
})
state.js
let defaultCity = '珠海'
try {
if (localStorage.city) {
defaultCity = localStorage.city
}
} catch (error) {
}
export default {
city: defaultCity
}
mutations.js
export default {
changeCity (state, city) {
state.city = city
try {
localStorage.city = city
} catch (error) {
}
}
}
搜索结果列表点击也要添加点击跳转
十二、详情页开发
keep-alive 缓存除了detail的所有组件
切换路由,页面回滚到顶部
头部渐隐渐现效果
- showAbs: true,显示图标,false显示景点详情
- 图标的透明度一开始设置为1,景点详情为0
- 当滑动距离小于40,图标从不透明变透明
- 当滑动距离在40-130,景点详情从透明变不透明
- 当滑动距离大于130,景点详情一直显示
banner制作
设计思路:高度是宽度的55%,子绝父相,banner-info给背景色渐变
background-image linear-gradient(top,rgba(0,0,0,0),rgba(0,0,0,0.8))
公用图片画廊组件
1. 创建一个公用组件 Gallary.vue
使用 vue-awesome-swiper 完成图片滚动
记得添加固定定位,否则组件无法显示
overflow:inherit: 规定应该遵从父元素继承overflow属性的值
点击关闭画廊
在swiper组件加v-if判断
解决swiper的bug
swiper的数据如果是异步动态获取的,那么自动轮播到最后一页之后就停了
只需要在swiper组件上面加上v-if="数据.length > 0"
给画廊绑定点击事件,$emit向外触发Gallary-Close事件
banner接收Gallary-Close
定义showGallary: false ,v-show控制画廊是否展示
在画廊组件最外层div添加@click.stop阻止冒泡,否则点击无法隐藏自身
渐隐渐现过渡效果组件封装
animation.vue
<template>
<transition>
<slot></slot>
</transition>
</template>
<script>
export default {
name: 'FadeAnimation'
}
</script>
<style lang="scss" scoped>
.v-enter, .v-leave-to {
opacity: 0;
}
.v-enter-active, .v-leave-active {
transition: opacity 1s;
}
</style>
递归组件实现详情页列表
background: url(http://detail.png) 0 -.45rem no-repeat;
- 0 距左边的距离
- -.45rem 距顶端的距离
- no-repeat 图片不重复
-
background-size属性要写在background后面,否则不生效