Vue2.5去哪儿网移动端项目

本文详细介绍了使用Vue2.5开发移动端项目的步骤,包括搭建项目、配置路由、移动端适配、项目托管、解决点击延迟问题、首页及轮播图制作、数据动态渲染、图标开发、城市页和详情页的开发,以及Vuex数据共享等关键环节,旨在提供一个完整的Vue移动端开发流程指南。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、搭建项目

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-阿里巴巴矢量图标库

  • 到官网下载想要的图标
  • 将iconfont.css放到assets目录下
  • 与iconfont.css平级,创建iconfont文件夹,将iconfont.ttf,iconfont.woff,iconfont.woff2放进去
  • 到iconfont.css文件中修改路径

  • 到main.js中引入iconfont.css文件

  • 引用图标
<span class="iconfont">&#xe601;</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后面,否则不生效

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值