【vue】vue-znly

老规矩,放下博主的项目地址:https://github.com/wohaiwo/vue-znly
我一直在想给那些开源者取什么名字比较好,怎样才对得起他们开源项目的精神,后来想想,还是叫博主吧。有的人用语言表达技术,有的人用代码表达技术。
接下来我们还是来看项目效果吧

我们可以看到这个项目内容还是挺多的,里面缺少一些内容,但是不影响我们研究这个项目
我们看index.html可以发现,这个项目用到了vue和jquery结合做的。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, height=device-height, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0">
    <meta name="keywords" content="今世缘 景区">
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-touch-fullscreen" content="yes">
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <title>今世缘景区欢迎您</title>
    <link rel="shortcut icon" href="/static/logo/favicon.ico" type="image/x-icon" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
    <meta name="format-detection"content="telephone=no, email=no" />
    <meta http-equiv="refresh" content="2000;url=http://www.baidu.com" />
  </head>
  <body>
    <div id="app">
        <router-view></router-view>
    </div>
    <script type="text/javascript" src="http://api.map.baidu.com/api?v=1.4"></script>
    <script type="text/javascript" src="../static/lib/js/jquery-3.2.1.slim.min.js"></script>
  </body>
</html>

main.js中引入入口文件App.vue

//main.js
// 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 VueRouter from 'vue-router';
import routes from './router/router.js';
import axios from 'axios';
// 解决30秒延迟问题
import FastClick from 'FastClick';

Vue.use(VueRouter);		// 加载vue-router插件
Vue.prototype.$http = axios;

FastClick.attach(document.body);


// 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({ 
	mode: 'hash',
	routes
});

// 创建和挂载根实例	
var vm =  new Vue({
	router,
	components: { App }
}).$mount('#app')
//app.vue中还加入了动画
<template>
	<div>
		<transition name="router-fade" mode="out-in">
    		<router-view></router-view>
    	</transition>
    </div> 
</template>

<script> 
	import './static/lib/css/main.css'
	import './static/lib/css/reset.css'
  	export default {
  	}

</script>

<style lang="scss">
	.router-fade-enter-active, .router-fade-leave-active {
	  	transition: opacity .3s;
	}
	.router-fade-enter, .router-fade-leave-active {
	  	opacity: 0;
	}

	.router-slid-enter-active, .router-slid-leave-active {
        transition: all .4s;
    }
    .router-slid-enter, .router-slid-leave-to {
        transform: translate3d(-100px, 0, 0);
        opacity: 0;
    }
</style>

router中也是使用懒加载的模式,可以看到首先加载定向的是我们的home组件在app.vue中渲染出来

//router.js
import App from '../App.vue';

// Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
const home = resolve => require(['../page/home.vue'], resolve);		// 首页
const introduction = resolve => require(['../page/scenicIntroduction.vue'], resolve);
const listDetail = resolve => require(['../components/listDetail.vue'], resolve);
const travelBox = resolve => require(['../page/travelBox.vue'], resolve);
const externalMap = resolve => require(['../page/externalMap.vue'], resolve);
const service = resolve => require(['../page/service.vue'], resolve);
const dropBox = resolve => require(['../components/dropBox.vue'], resolve);

// 定义路由
const routes = [
	{
		path: '/',
		component: App,
		children: [
			{
				path: '/',
				redirect: { name: 'home' }
			},
			{
				path: '/home',
				name: 'home',
				component: home
			}, {
				path: '/scenic/introduction', 
				name: 'introduction', 
				component: introduction
			}, {
				path: '/scenic/detail/:id/:type/:identifier',
				name: 'listDetail', 
				component: listDetail
			}, {
				path: '/travelBox',
				name: 'travelBox', 
				component: travelBox
			}, {
				path: '/externalMap',
				name: 'externalMap',
				component: externalMap
			}, {
				path: '/service/:type',
				name: 'service',
				component: service
			}, {
				path: '/dropBox/:url/:title',
				name: 'dropBox',
				component: dropBox
			}
		]
	}
]


export default routes;

我们来看home组件,里面的代码挺有意思的,
先看header组件

//header.vue
<template>
	<header>
		<slot name="logo"></slot>
		<span class="left-icon" v-if="goBack" @click="$router.go(-1)">
			<i title="返回" class="iconfont">&#xe679;</i>
        </span>
		<span class="left-icon side-bar" v-if="sideBar" @click="showSideBar">
			<i title="主菜单" class="iconfont">&#xe602;</i>
        </span>
        <span class="title-text" v-if="headTitle">{{headTitle}}</span>
        <transition name="slide-fade">
        <nav v-show="isShowSideBar">
	    	<router-link to="/scenic/introduction"><i class="iconfont">&#xe641;</i>景区介绍</router-link>
			<router-link :to="{name: 'service', params: {type: 3}}"><i class="iconfont">&#xe64c;</i>景区公告</router-link>
			<router-link :to="{name: 'service', params: {type: 15}}"><i class="iconfont">&#xe69b;</i>景区服务</router-link>
		    <router-link :to="{name: 'service', params: {type: 13}}"><i class="iconfont">&#xe6b2;</i>预订门票</router-link>
	        <router-link :to="{name: 'service', params: {type: 14}}"><i class="iconfont">&#xe6af;</i>特色购物</router-link>
	    	<router-link to="/travelBox"><i class="iconfont">&#xe603;</i>旅行百宝箱</router-link>
    		<router-link :to="{name: 'dropBox', params: {url: vrUrl, title: vrTitle}}"><i class="iconfont">&#xe73d;</i>虚拟旅游</router-link>
    		 <router-link :to="{name: 'service', params: {type: 6}}"><i class="iconfont">&#xe7f1;</i>餐饮住宿</router-link>
        </nav>
        </transition>
	</header>
</template>
<script>
	export default {
		data() {
			return {
			}
		},
		props: ['goBack', 'headTitle', 'sideBar', 'isShowSideBar', 'vrUrl', 'vrTitle'],
		methods: {
			// 子组件通过emit向父组件传递事件的函数名
			showSideBar() {
				this.$emit('breadcrumb');
			}
		}
	}
</script>
<style scoped lang="scss">
	$nav-color: #e60012;
	header {
		position: fixed;
		left: 0;
		top: 0;
		z-index: 100;
		 100%;
		height: 40px;
		line-height: 40px;
		color: $nav-color;
		background: #fff ;
		text-align: center;
		border-bottom: 2px solid #ededed;
		box-sizing: border-box;
		span {
			font-size: 18px;
			font-weight: bold;	
		}		
		.left-icon {
			position: absolute;
			left: 0;
			top: 50%;
			 50px;
			transform: translateY(-50%);
			i {
				color: $nav-color;
			}
		}
		.side-bar {
			left: 0;
			 10%;
			background: $nav-color;
			i {
				color: #fff;
			}
		}
		nav {
			position: fixed;
			left: 0;
			right: 0;
			top: 40px;
			bottom: 50px;
			z-index: 20;
			 140px;
			background: #fff;
			a {
				display: block;
				height: 50px;
				line-height: 50px;
				text-align: left;
				padding-left: 8%;
				box-sizing: border-box;
				color: #000;
				&:not(:last-child) {
					border-bottom: 1px solid #e6e6e6;
				}
				i {
					color: $nav-color;
					margin-right: 20px;
				}
			}
		}
	}
	.slide-fade-enter-active, .slide-fade-leave-active  {
		transition: all 0.3s ease-in;
	}
	.slide-fade-enter, .slide-fade-leave-to{
		opacity: 0;
		transform: translate3d(-150px, 0, 0);
	}
	// 适配一体机样式
	@media screen and  (min- 1000px) {
		$header-height: 100px;
	 	i {
	 		font-size: 36px;
	 	}
	  	header {
			font-size: 32px;
		  	height: $header-height;
		  	line-height: $header-height;
		  	border-bottom: 4px solid #ededed;
			span {
				font-size: 45px;
				font-weight: bold;	
			}
			.left-icon {
				 $header-height;
			}
		  	nav {
				top: $header-height;
				bottom: $header-height;
				 300px;
		  		a {
					height: $header-height;
					line-height: $header-height;
					border-bottom: 3px solid #e6e6e6;
				}
		  	}
	  }

	}
</style>

userCount.vue不知道表达什么意思?

//userCount.vue
//srccomponentsuserCount.vue
 <template>
	<div>
		<p>{{ msg }}</p>
		 
	</div>
</template>

<script>
	export default {
		data() {
			return {
				msg: ''
			}
		},
		created() {
			let url = '/JSY_H5/h5/statistics';
			this.$http.get(url).then((response) => {
				this.$data.msg = response.data.data.msg;
			}, (response) => {
				console.log('oops, data is error');
			});
		}
	}
</script>

<style scoped lang="scss">
	div {
		position: relative;
		 100%;
		height:  20px;
		padding: 0 4%;
		margin-top: 40px;
		font-size: 14px;
		color: #ddd;
		background: rgba(0, 0, 0, .4);
		overflow: hidden;
		z-index: 40;
		user-select: none;
		box-sizing: border-box;
		p {
			left: 100%;
			position: absolute;
			z-index: 40;
			white-space: nowrap;
			animation-delay: 1s;
			animation-name: slide;
			animation-duration: 45s;
			animation-iteration-count: infinite;
		}
	}
	@media screen and (min- 1000px) {
		div{
			height:  60px;
			margin-top: 100px;
			font-size: 32px;
			p {
				line-height: 60px;
			}
		}
	}
	@keyframes slide {
		0% { left: 100%; }
		100% { left: -120%; }
	}
</style>
//footer.vue
<template>
	<footer>
	    <ul>
	    	<li>
		    	<router-link :to="{name: 'service', params: {type: 15}}" :class=" pathName == navUrl[0] ? 'active' : ''">
		    		<i class="iconfont">&#xe69b;</i><span>景区服务</span>
		    	</router-link>
	    	</li>
	    	<li>
	    		<router-link to="/home" :class=" pathName == navUrl[1] ? 'active' : ''">
	    			<i class="iconfont">&#xe6b8;</i><span>主页</span>
	    		</router-link>
	    	</li>
	    	<li>
	    		<router-link :to="{name: 'service', params: {type: 6}}" :class=" pathName == navUrl[2] ? 'active' : ''">
	    			<i class="iconfont">&#xe7f1;</i><span>餐饮住宿</span>
	    		</router-link>
	    	</li>
	    </ul>
	</footer>
</template>

<script>
	export default {
		data() {
			return {
				isShow: false,
				navUrl : [0, 1, 2]
			}
		},
		props: ['pathName'],
		created() {
		},
		methods: {
			showNav(state) {
				this.$data.isShow = state ? false : true;
			}
		}
	}
</script>

<style scoped lang="scss">
	footer {
		display: block;
		 100%;
		height: 50px;
		position: fixed;
		left: 0;
		bottom: 0;
		z-index: 100;
		color: #000;
		background: #fff;			
		ul {
			height: 100%;
			overflow: hidden;
			li {
				display: inline-block;
				position: relative;
				float: left;
				 33.33%;
				height: 100%;
				box-sizing: border-box;
				.iconfont {
					display: block;
					margin-top: 4px;
					font-size: 20px;
				}
			}
		}
		// 菜单栏选中点击样式
		.active {
			i, span {
				color: #e60012;
			}
		}
		a {
			display: block;
			 100%;
			height: 100%;
			font-size: 14px;
			color: #5D656B;
			text-align: center;
		}
	}

	.slide-fade-enter-active {
	  transition: all .3s ease;
	}
	.slide-fade-leave-active {
	  transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
	}
	.slide-fade-enter, .slide-fade-leave-to {
	  transform: translate3d(0, 100%, 0);
	  opacity: 0;
	}

	@media screen and  (min- 1000px) {
		footer {
			height: 100px;
			ul {
				li {
					.iconfont {
						font-size: 48px;
					}
				}
			}
			a {
				font-size: 24px;
			}
		}
	}
</style>

home.vue引入了这几个组件

//home.vue
<template>
    <div id="main">
        <v-header sideBar="true" :isShowSideBar="isShowSideBar" :vrUrl="vRinfo.jumpUrl" :vrTitle="vRinfo.title" @breadcrumb="showSideBar">
            <span class="header-logo" slot="logo"><img class="logo" :src="logoImgUrl" alt="logo-title"></span>
        </v-header>
        <user-count></user-count>
        <!-- 首页滚动banner -->
        <div class="banner">
            <div class="swiper-container" @click="closeSideBar">
                <div class="swiper-wrapper">
                    <!-- 从后端取数据进行渲染的 -->
                    <div class="swiper-slide" v-for="item in imageDataArr">
                       <img :src="item.INFO_IMAGE_URL" :alt="item.INFO_TITLE">
                    </div>
                </div>
                <!-- 如果需要分页器 -->
                <div class="swiper-pagination swiper-pagination-white"></div>
            </div>
            <nav class="right-side">
                <router-link :to="{name: 'service', params: {type: 13}}"><span>预订</span><span>门票</span></router-link>
                <router-link v-if="isApp" :to="{name: 'dropBox', params: {url: vRinfo.jumpUrl, title: vRinfo.title}}"><span>虚拟</span><span>旅游</span></router-link>
                <a v-if="!isApp" target="_blank" :href = "vRinfo.jumpUrl"><span>虚拟</span><span>旅游</span></a>
                <a @click="showSideBar"><span>更多</span><span>功能</span></a>
            </nav>
        </div>
        <v-footer :pathName="1"></v-footer>
    </div>
</template>

<script>
import Vue from 'vue';
import vHeader from '../components/header'
import userCount from '../components/userCount'
import vFooter from '../components/footer.vue'
import '../static/lib/js/swiper.min.js'
import '../static/lib/css/swiper.min.css'

export default {
    data() {
        return {
            isApp: false,           // 是否是园区一体机
            isShowSideBar: false,
            imageDataArr: [],       // 首页轮播图
            vRinfo: {               // 虚拟旅游
                title: '',
                jumpUrl: ''
            },
            logoImgUrl: ''
        }
    },
    components: {
        vHeader, userCount,  vFooter
    },
    created() {

    },
    mounted() {
        let isApp = window.localStorage ? localStorage.getItem('isApp') : Cookie.read('isApp');
        // 浏览器本地存储是否是一体机
        // 判断本地缓存里面是否已经存在isApp
        if(isApp == 'true') {
            this.logoImgUrl = '../static/logo/logo-red-pc.png';
            this.isApp = true;
        } else {
            // 判断是否是第一次进来首页,如果是,则获取params的参数
            this.isApp = this.$route.query && this.$route.query.app;
            if(this.isApp == 'true') {
                // 保存到全局变量中
                if(window.localStorage) {
                    localStorage.setItem('isApp', this.isApp);
                } else {
                    Cookie.wirte('isApp', this.isApp);
                }
                this.logoImgUrl = '../static/logo/logo-red-pc.png';
            } else {
                this.logoImgUrl = '../static/logo/logo-red-h5.png';
            }
        }
        this.initPage();
        this.getVRTravel();
    },
    methods: {
        initPage() {
              let url = `/JSY_H5/h5/queryServiceList?type=1`;
            this.$http.get(url).then((response) => {
                this.imageDataArr = response.data.rows;
                // vue.nextTick在页面初始挂载就要渲染好轮播
                Vue.nextTick(function() {
                    new Swiper('.swiper-container', {
                        autoplay: 10000, 
                        pagination: '.swiper-pagination',
                        loop: true
                    });
                });
            }, (response) => {
                console.log('oops, data is not found');                
            });
        },
        getVRTravel() {
            let url = '/JSY_H5/h5/queryServiceList?type=16';
            this.$http.get(url).then((response) => {
                // 遍历数据,改变数据结构,套用同一天模板listTpl
                this.$data.vRinfo['title'] = response.data.rows[0]['INFO_TITLE'];
                this.$data.vRinfo['jumpUrl'] = response.data.rows[0]['JUMP_URL'];
            });
        },  
        showSideBar() {
            this.isShowSideBar = !this.isShowSideBar;
            console.log("this.isShowBar",this.isShowSideBar)
        },
        closeSideBar() {
            this.isShowSideBar = false;
        }
    }
}
</script>

<style scoped lang="scss">
    #main {
        font-family: "Microsoft Yahei", 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
    }
    .header-logo {
        display: inline-block;
         100%;
        height: 40px;
        box-sizing: border-box;
        .logo {
            height: 100%;
        }
    }
    .banner {
        $right-side-size: 50px;
        .swiper-container {
            position: fixed;
            left: 0;
            right: 0;
            top: 40px;
            bottom: 50px;
            z-index: 10;
            overflow: hidden;
            .swiper-slide img {
                 100%;
                height: 100%;
            }
        }
        .right-side {
            position: fixed;
            right: 4%;
            bottom: 70px;
            z-index: 10;
             $right-side-size;
            a {
                display: inline-block;
                 $right-side-size;
                height: $right-side-size;
                color: #fff;
                background: #e60012;
                border: 2px solid #fff;
                padding: 5px;
                border-radius: 50%;
                box-shadow: 0 0 10px 0 rgba(0, 0, 0, .5);
                box-sizing: border-box;
                span {
                    display: block;
                    font-size: 14px;
                    height: 18px;
                    line-height: 18px;
                }
                &:not(:last-child) {
                    margin-bottom: 10px;
                }
            }
        }
    }

    // 适配一体机样式
    @media screen and  (min- 1000px) {
        $right-side-size: 150px;
        .header-logo {
            height: 100px;
        }
        .banner {
            .swiper-container {
                top: 100px;
                bottom: 100px;
            }
             .right-side {
                right: 4%;
                bottom: 50%;
                 $right-side-size;
                transform: translate3d(0, 50%, 0);
                a {
                     $right-side-size;
                    height: $right-side-size;
                    padding: 12px;
                    border: 5px solid #fff;
                    span {
                        font-size: 45px;
                        height: 55px;
                        line-height: 55px;
                    }
                }
            }
        }

    }
</style>

在景区页面中引入了listTpl.vue

//listTpl.vue
<template>
	<div>
		<div class="list-tpl" >
			<ul v-if="!isType" class="list-item">
				<li v-for="item in items">
			    	<router-link :to="{name: 'listDetail', params: {id: item.id, type: type, identifier: identifier}}">
						<div class="list-image">
							<img :src="item.imageUrl">
						</div>
						<aside >
							<h3>{{ item.title }}</h3>
							<article>{{ item.description }}</article>
						</aside>
	                </router-link>
				</li>
			</ul>

			<ul v-if="isType" class="list-item">
				<li v-for="item in items">
					<div class="list-image">
						<img :src="item.imageUrl">
					</div>
					<aside >
						<h3>{{ item.title }}</h3>
						<article v-html= "item.description"></article>
						<a class="jump-url" v-if="!isApp" :href="item.jumpUrl" target="_blank">去预订</a>
						<a class="jump-url" v-if="isApp" @click="showQRCode(item.qrCode)">去预订</a>
					</aside>
				</li>
			</ul>
	    </div>
	    <div v-if="isShowQrBox" id="qrcode" @click="closeQrcodeBox">
	    	<div class="mask"></div>
	    	<div id="qrcode-content"></div>
	    </div>
	</div>
</template>

<script>
	import Vue from 'vue';
	import '../static/lib/js/jquery.qrcode.min.js';
	export default {
		data() {
			return {
				isApp: false,				// 判断是否使用不同的遍历模块, true => 调出二维码模板
				isType: false, 				// 当type = 13 || 14时,显示去预定的模板
				isShowQrBox: false
			}
		},
		props: ['identifier', 'items', 'type'],
		created() {
			// 在listTpl页面中 只在预订门票和特色购物里面调出而二维码
	        if(this.type == 13 || this.type == 14){
	        	this.isType = true;
	        	// 判断是否是园区一体机
	        	let isApp = window.localStorage ? localStorage.getItem('isApp') : Cookie.read('isApp');
				if(isApp == 'true') {
					this.isApp = true;
				}
	        }
		},
		methods: {
			showQRCode(url) {
				this.isShowQrBox = true;
				jQuery('#qrcode #qrcode-content').empty();
				Vue.nextTick(function() {
					jQuery('#qrcode #qrcode-content').qrcode(url);
				});
			},
			// 关闭二维码框
			closeQrcodeBox() {
				this.isShowQrBox = false;
			}
		}
	}
</script>
<style scoped lang="scss">
	.list-tpl {
		margin-top: 45px;
		.list-item {
			margin: 10px auto;
			list-style: none;
			height: 100vh;
			overflow: auto;
			background: #EDEDED;	
			li {
				display: inline-block;
				 100%;
				padding: 2%;
				margin-bottom: 10px;
				overflow: hidden;
				box-sizing: border-box;
				background: #fff;
				a {
					display:inline-block;
				}
				.list-image {
					float: left;
					 30vw;
					height: 30vw;
					margin-right: 3vw;
					box-sizing: border-box;
					img {
						 100%;
						height: 100%;	
					}
				}
				aside {
					position: relative;
					min-height: 30vw;
					font-size: 14px;
					text-align: left;
					overflow: hidden;
					text-overflow: ellipsis;
					box-sizing: border-box;
					h3 {
						color: #CD1940;
						padding: 2px 0 5px 0;
						font-size: 16px;
						font-weight: bold;
					}
					article {
						font-size: 14px;
						color: #000;
						line-height: 1.4;
						text-align: justify;
					}
			        .jump-url {
				        position: absolute;
				        right: 0;
				        bottom: 0;
			             60px;
			            height: 30px;
			            line-height: 30px;
			            text-align: center;
			            color: #FFF;
			            background: #e60012;
			            box-sizing: border-box;
			        }
				}
			}
		}
	}
	#qrcode {
		position: relative;
		.mask {
			position: fixed;
			display: block;
			left: 0;
			right: 0;
			top: 0;
			bottom: 0;
			background: rgba(0, 0, 0, 0.4);
			z-index: 10;
		}
		#qrcode-content {
			position: fixed;
			left: 50%;
			top: 50%;
			padding: 15px;
			text-align: center;
			background: #fff;
			z-index: 100;
			transform: translate3d(-50%, -50%, 0);
			&:after {
				content: '扫一扫上面的二维码图案';
				display: block;
				padding-top: 10px;
			}
		}
	}

	@media screen and (min- 1000px) {
		.list-tpl {
			margin-top: 100px;
			.list-item {
				li {
					aside {
						h3 {
							font-size: 38px;
						}
						article {
							font-size: 32px;
						}
						.jump-url {
					        position: absolute;
					        right: 0;
					        bottom: 0;
				             120px;
				            height: 60px;
				            line-height: 60px;
				            text-align: center;
				            color: #FFF;
				            font-size: 24px;
				            background: #e60012;
				            box-sizing: border-box;
				        }
					}
				}
			}
		}
	}
</style>

/#/?app=true

我们其实可以猜测这个组件里面主要是展示景区相关图片,并且还有二维码扫码的功能,用jQuery做的
完整的景区介绍的代码

<template>
    <div>
        <v-header goBack="true" headTitle="景区介绍"></v-header>
        <list-tpl :items="scenicInfo" :type="type"  identifier="1"></list-tpl>
        <loading :show="done"></loading>
    </div>
</template>

<script>
    import vHeader from '../components/header.vue';
    import listTpl from '../components/listTpl.vue';
    import loading from '../components/loading.vue'; 
      
    export default {
      data() {
        return {
            done: false,
            type: '0',      // 这个类型应该是字符串,需要跟路由匹配到
            scenicInfo: []
        }
      },
      components: {
        vHeader, listTpl, loading
      },
      mounted() {
        // 页面初始化时加载数据
        this.initPage();
      },
      methods: {
        initPage() {
            this.done = true;
            let url = '/JSY_H5/h5/querySSSList';
            this.$http.get(url).then((response) => {
                // 遍历数据,改变数据结构,套用同一套模板listTpl
                response.data.rows.forEach((item, index) => {
                    let tmp = {};
                    tmp['description'] = item['SS_DESCRIPTION'];
                    tmp['id'] = item['SS_NO'];
                    tmp['imageUrl'] = item['SS_IMAGE_URL'];
                    tmp['title'] = item['SS_TITLE'];
                    this.$data.scenicInfo.push(tmp);
                });
                this.$data.done = false;
            }, (response) => {
                this.$data.done = false;
            });
        }
      }
    }
  </script>
//loading.vue
<template>
  <transition>
    <svg class="spinner" :class="{ show: show }" v-show="show" width="68px" height="68px" viewBox="0 0 44 44">
      <circle class="path" fill="none" stroke-width="4" stroke-linecap="round" cx="22" cy="22" r="20"></circle>
    </svg>
  </transition>
</template>

<script>
  export default {
    props: ['show']
  }
</script>

<style lang="scss">
  $offset: 126;
  $duration: 1.4s;
  .spinner {
    position: fixed;
    z-index: 999;
    transition: opacity .15s ease;
    animation: rotator $duration linear infinite;
    animation-play-state: paused;
    right: 50%;
    top: 20%;
    margin-right: -34px;
    &.show {
      animation-play-state: running
    }

    &.v-enter, &.v-leave-active {
      opacity: 0;
    }

    &.v-enter-active, &.v-leave {
      opacity: 1;
    }
  }

  @keyframes rotator {
    0% {
      transform: scale(0.5) rotate(0deg);
    }
    100% {
      transform: scale(0.5) rotate(270deg);
    }
  }

  .spinner .path {
    stroke: #42b983;
    stroke-dasharray: $offset;
    stroke-dashoffset: 0;
    transform-origin: center;
    animation: dash $duration ease-in-out infinite;
  }

  @keyframes dash {
    0% {
      stroke-dashoffset: $offset;
    }
    50% {
      stroke-dashoffset: ($offset/2) transform rotate(135deg);
    }
    100% {
      stroke-dashoffset: $offset transform rotate(450deg);
    }
  }

</style>

//srcpageservice.vue
<template>
    <div>
        <v-header goBack="true" :headTitle="headTitle"></v-header>
        <list-tpl :items="serviceInfo" :type="type"  identifier="2"></list-tpl>
        <v-footer :pathName="index" v-show=" type == 6 || type == 15"></v-footer>
        <loading :show="done"></loading>
    </div>
</template>

<script>
    import vHeader from '../components/header.vue';
    import listTpl from '../components/listTpl.vue';
    import loading from '../components/loading.vue';
    import vFooter from '../components/footer.vue';
    export default {
        data() {
            return {
                type: null,
                done: false,
                index: 0,          // 动态显示footer导航栏显示位置
                serviceInfo: []
            }
        },
        computed: {
            headTitle: function() {
                let type = `${this.type}`;
                switch(type) {
                    case '3':
                        type = '景区公告';
                        break;
                    case '6':
                        type = '餐饮住宿';
                        break;
                    case '7':
                        type = '周边景点';
                        break;
                    case '13':
                        type = '预订门票';
                        break;
                    case '14':
                        type = '特色购物';
                        break;
                    case '15':
                        type = '景区服务';
                        break;
                }
                return type;
            },
        },
        components: {
            vHeader, listTpl, loading, vFooter
        },
        created() {
      	    this.type = this.$route.params.type;
       },
        mounted() {
            // 页面初始化时加载数据
            this.initPage();
        },
        // 只在当前路由改变,但是该组件被复用时调用
        // to 表示 route即将要进去的路由
        // from 表示 route正要离开的路由
        beforeRouteUpdate(to, from, next) {
            this.type = to.params.type;
            next(this.initPage());          
        },
        methods: {
            initPage() {
                // 判断footer底部导航栏的显示位置
                if(this.type == 6) {
                    this.index = 2;  
                } else if (this.type == 15) {
                    this.index = 0;
                }
                this.$data.serviceInfo = [];   // 初始化数据,防止footer底部导航栏切换数据没有清空
                this.done = true;
                let url = `/JSY_H5/h5/queryServiceList?type=${this.type}`;
                this.$http.get(url).then((response) => {
                	// 遍历数据,改变数据结构,套用同一天模板listTpl
                    response.data.rows.forEach((item, index) => {
                    	let tmp = {};
                    	tmp['description'] = item['INFO_DESCRIPTION'];
                    	tmp['id'] = item['INFO_NO'];
                    	tmp['imageUrl'] = item['INFO_IMAGE_URL'];
                    	tmp['title'] = item['INFO_TITLE'];
                        tmp['qrCode'] = item['QR_CORE_URL'];
                        tmp['jumpUrl'] = item['JUMP_URL'];
                    	this.$data.serviceInfo.push(tmp);
                    });
                    this.$data.done = false;
                }, (response) => {
                    this.$data.done = false;
                });
            }
        }
    }
  </script>

//srcpage	ravelBox.vue
<template>
	<div>
        <v-header goBack="true" headTitle="旅行百宝箱"></v-header>
        <div class="travel-box">
			<section class="item-box">
				<router-link :to="{name: 'listDetail', params: {id: 5, type: 20, identifier: 0}}">
					<i class="iconfont">&#xe656;</i>
					<span>旅游线路</span>
				</router-link>
				<router-link :to="{name: 'externalMap'}">
					<i class="iconfont">&#xe621;</i>
					<span>外部交通</span>
				</router-link>
				<router-link :to="{name: 'listDetail', params: {id: 4, type: 21, identifier: 0}}">
					<i class="iconfont">&#xe638;</i>
					<span>景区地图</span>
				</router-link>
				<router-link :to="{name: 'service', params: {type: 7}}">
					<i class="iconfont">&#xe600;</i>
					<span>周边景点</span>
				</router-link>
				<router-link :to="{name: 'service', params: {type: 6}}">
					<i class="iconfont">&#xe7f1;</i>
					<span>餐饮,住宿</span>
				</router-link>
			</section>			
        </div>
	 </div>
</template>

	<script>
		import vHeader from '../components/header'
		export default {
			data() {
				return {
				}
			},
			components: {
				vHeader
			},
			mounted() {

			}
		}
	</script>

	<style scoped lang="scss">
		.travel-box {
			margin-top: 42px;
				height: 100vh;
				background: #F5F5F5;
				a {
					display: inline-block;
					 32%;
					height: 100px;
					margin: 0 2% 2% 0;
					color: #5D656B;
					background: #fff;
					text-align: center;
					box-sizing: border-box;
					&:nth-child(3n + 0) {
						margin-right: 0;
					}
					i {
						display: block;
						font-size: 48px;
						line-height: 60px;
						margin-top: 10px;
					}
					span {
						font-size: 16px;
					}
				}
		}
		@media screen and (min- 1000px) {
			.travel-box {
				margin-top: 100px;
				a {
					height: 200px;
					i {
						font-size: 64px;
						line-height: 100px;
					}
					span {
						font-size: 24px;
					}
				}
			}
		}
</style>
//listDetail
<template>
    <div class="detail">
        <v-header goBack="true" :headTitle="listDetail.title"></v-header>
        <div class="audio-play" v-if="this.identifier == 1 && listDetail.audio">
            <i v-on:click="playAudio" class="iconfont">&#xe66b;&nbsp;音频播放</i>
            <audio  id="audio" :src="listDetail.audio" loop="true">
                你的浏览器不支持 <code>audio</code> 音频播放功能.
            </audio>
        </div>
        <div class="detail-body" v-show="isShow">
            <section v-html="listDetail.content"></section>
            <review :id="detailId" :qrCodeUrl="qrCodeUrl" v-if="needReview"></review>
        </div>
        <loading :show="done"></loading>
    </div>
</template>

<script>
    import loading from '../components/loading.vue';
    import vHeader from '../components/header.vue';
    import review from '../components/review.vue';
    export default {
        data() {
          return {
            done: false,
            isShow: false,     // 只有当数据加载完成之后才能够实现出来
            needReview: false,  //是否需要显示评论(只有景点才需要,其他的地方都是不需要的,默认关闭)
            detailId: '',
            type: '',          // 判断当前的模块信息
            identifier: '',    // 标识符 景点介绍模块为1 旅行百宝箱模块为0
            shopUrl: '',
            qrCodeUrl: '',      // 二维码的生成地址
            listDetail: {      // 详情列表信息
                title: '',
                content: '',
                audio: null
            }
          }
        },
        components: {
            loading, vHeader, review
        },
        created() {
            this.detailId = this.$route.params.id;
            this.identifier =  this.$route.params.identifier || 0;
            this.type = this.$route.params.type;
        },
        mounted() {
           this.initPage();
        },
        methods: {
            initPage() {
                let listDetailUrl = '';
                this.$data.done = true;
                if(this.identifier == 1) {  
                    listDetailUrl = `/JSY_H5/h5/querySSSOne?id=${this.detailId}`;   // 景点介绍调用的接口
                } else if(this.identifier == 2) {
                    listDetailUrl = `/JSY_H5/h5/queryServiceOne?id=${this.detailId}`;   // service.vue下面过来调用接口
                } else {
                    listDetailUrl = `/JSY_H5/h5/queryServiceList?type=${this.detailId}`;     // 旅游线路,景区地图调用的接口
                }

                this.$http.get(listDetailUrl).then((response) => {
                    this.done = false;
                    this.isShow = true;
                    let data = response.data.rows;
                    // 景点介绍
                    if(this.identifier == 1) {
                        this.listDetail.title = data[0].SS_TITLE;
                        this.listDetail.content = data[0].SS_CONTENT;
                        this.listDetail.audio = data[0].SS_VIDEO_URL;
                        this.qrCodeUrl = data[0].QR_CORE_URL;
                        this.needReview = true;
                    } else {
                        // 资讯
                        this.listDetail.title = data[0].INFO_TITLE;
                        this.listDetail.content = data[0].INFO_CONTENT;
                        this.needReview = false;
                    }
                    // 由于后台传过来是一段字符串 需要使用正则来适配一体机文字大小
                    let isApp = window.localStorage ? localStorage.getItem('isApp') : Cookie.read('isApp');
                    if(isApp == 'true') {
                        this.listDetail.content = this.listDetail.content.replace(/font-size:s*d+px;/g, 'font-size: 32px;');
                    }
                }, (response) => {
                    console.log('opps Is Error: ' + response);
                    this.done = false;
                })
            },
            playAudio() {
                let audio = document.getElementById('audio');
                var isPlaying = audio.currentTime > 0 && !audio.paused && !audio.ended 
                    && audio.readyState > 2;

                if (!isPlaying) {
                  audio.load(); 
                  audio.play();
                }
            }
        }
    }
</script>

<style lang="scss">
  .detail {
    padding-top: 40px;  // 移除头部header的高度
    .audio-play {
         100%;
        color: #fff;
        padding: 1% 4%;
        text-align: right;
        background: #000;
        opacity: .4;
        box-sizing: border-box;
        i {
            display: inline-block;
             100px;
            height: 30px;
            line-height: 30px;
        }
    }
    .detail-body {
        padding: 10px 10px 40px 10px ;
        section {
            100%;
            img {
                100%;
            }
        }
    }
    footer {
        position: absolute;
        right: 0;
        bottom: 0;
         100%;
        height: 40px;
        background: #f6f6f6;
        text-align: right;
        a {
            display: inline-block;
             80px;
            height: 40px;
            line-height: 40px;
            padding: 2px;
            color: #FFF;
            text-align: center;
            background: #e60012;
            box-sizing: border-box;
        }
    }
  }
  @media screen and (min- 1000px) {
    .detail {
        padding-top: 100px;
        .audio-play {
            i {
                font-size: 32px;
                 200px;
                height: 50px;
                line-height: 50px;
            }
        }
        footer {
            height: 100px;
        }
    }
}
</style>

这个组件中引用了review组件

<template>
	<div>
		<div class="reviews" v-show="isShow">
			<ul>
				<li><i class="iconfont">&#xe73d;</i>{{ visitCount }}</li>
				<li @click.once="upVote" :class="{active: isActive }"><i class="iconfont">&#xe644;</i>{{ goodCount }}</li>
				<li @click="showReviewBox"><i class="iconfont">&#xe761;</i>写评论</li>
				<li @click="showCommentBox"><i class="iconfont">&#xe649;</i>{{ reviewCount }}</li>
			</ul>
		</div>
		<transition name="slide-fade-down">
			<div v-show="isShowReviewBox" class="reviews-box">
				<div class="header">
					<span @click="closeReviewBox">取消</span>
					<span>评论</span>
					<span @click="addReview" :class="isSend">发送</span>
				</div>
				<div class="body">
					<textarea v-model="reviewContent" autofocus maxlength="120" required></textarea>
				</div>
			</div>
		</transition>
		<transition name="slide-fade-right">
		<div v-show="isShowCommentBox" class="comment-box">
			<div @click.stop.prevent="closeCommentBox" class="mask"></div>
			<div class="comment-main">
				<section v-for="(item, index) in reviewData">
					<div class="reviews-author"><span>游客</span></div>
					<div class="reviews-body">
						<p class="reviews-content">{{ item.SSR_CONTENT }}</p>
						<p>{{ item.ENTRY_DATE_TIME  | time}}</p>
					</div>
				</section>
			</div>
		</div>
		</transition>
	    <div v-if="isShowQrBox" id="qrcode" @click="closeQrcodeBox">
	    	<div class="mask"></div>
	    	<div id="qrcode-content"></div>
	    </div>
	    <div v-if="isShowTipBox" class="tip-box">
	    	您的评论已经提交,请等待审核通过...
	    </div>
	</div>
</template>

<script>
	import Vue from 'vue';
	import '../static/lib/js/jquery.qrcode.min.js'
	export default {
		data() {
			return {
				isShow: true,				
				SS_NO: this.id,				// 当前的景点id
				isActive: false,
				reviewData: [],   			// 评论数
				visitCount: 0,    			// 访问数
				goodCount: 0,	  			// 点赞数
				reviewCount: 0,   			// 评论数
				reviewContent: '', 			// 评论内容
				isShowReviewBox: false,		// 是否显示评论框
				isShowCommentBox: false,     // 是否显示评论列表
				isShowQrBox: false,          // 是否显示二维码
				isShowTipBox: false			  // 是否显示评论成功提示框
			}
		},
		props: ['id', 'qrCodeUrl'],
		mounted() {
			this.initPage();
		},
		computed: {
			isSend: function () {
				return {
					active:	!!this.$data.reviewContent.length
				}   
			}
		},
		filters: {
			// 格式化时间
			time: function(date) {
				if(!date) return '';
			    var date = new Date(date);
			    var Y = date.getFullYear() + '-';
			    var M = (date.getMonth()+1 < 10 ? '0'+(date.getMonth()+1) : date.getMonth()+1) + '-';
			    var D = (date.getDate() < 10 ? '0' + (date.getDate()) : date.getDate()) + ' ';
			    	return Y + M + D;
			}
		},
		methods: {
			// 评分
			showRate(rate) {
				if(!rate) rate = 5;
				return "★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate);
			},
			// 判断是否显示评论界面
			showReviewBox() {
				let isApp = window.localStorage ? localStorage.getItem('isApp') : Cookie.read('isApp');
				if(isApp == 'true') {			// 如果当前是一体机访问 则无法添加评论 调出二维码
					let url = this.qrCodeUrl;
					this.$data.isShowReviewBox = false;
					this.isShowQrBox = true;
					jQuery('#qrcode #qrcode-content').empty();
					Vue.nextTick(function() {
						jQuery('#qrcode #qrcode-content').qrcode(url);			// 使用ES6来进行字符串转义
					});
				} else {
					this.$data.isShowReviewBox = !this.$data.isShowReviewBox;
				}
			},
			showCommentBox() {
				if(this.reviewCount == 0) return false;     // 如果当前的评论数为0 则不显示评论列表
				this.$data.isShowCommentBox = !this.$data.isShowCommentBox;
			},
			// 关闭评论界面
			closeReviewBox() {
				this.$data.isShowReviewBox = false;
			},
			closeCommentBox() {
				this.$data.isShowCommentBox = false;
			},
			// 添加评论
			addReview() {
				let url = `/JSY_H5/h5/saveSSR`;
				this.$http.post(url, {
					SS_NO: this.$data.SS_NO,
					SSR_CONTENT: this.$data.reviewContent
				}).then( (response) => {
					this.closeReviewBox();
					this.reviewContent = '';
					this.isShowTipBox = true;
					// 需要使用箭头函数来邦定this的值
					setTimeout(() => {
						this.isShowTipBox = false;
					}, 1000);
				}, (response) => {
					console.log('opps Is Error: ' + response);
				})
			},
			initPage() {
				let url = `/JSY_H5/h5/querySSRList?id=${this.$data.SS_NO}`;
				this.$http.get(url).then((response) => {
					this.$data.reviewData = response.data.rows;
				}, (response) => {
					console.log('opps Is Error: ' + response);
				});
				this.getUserVisit();	// 获取评论接口中 访问量和点赞数
			},
			// 获取当前景点的页面访问量点赞数以及评论数
			getUserVisit() {
				let url = `/JSY_H5/h5/addInteractive?id=${this.$data.SS_NO}`;  // 游客访问量
				this.$http.get(url).then((response) => {
					this.$data.goodCount = response.data.GOODED_COUNT;
					this.$data.visitCount = response.data.LOOKED_COUNT;
					this.$data.isActive = response.data.IS_GOODED;
					this.$data.reviewCount = response.data.REVIEW_COUNT;
				}, (response) => {
					console.log('opps Is Error: ' + response);
				});
			},
			// 添加点赞
			upVote() {
				let url = `/JSY_H5/h5/addInteractive?id=${this.$data.SS_NO}&ACTION="good"`;  // 当前景点-点赞数
				this.$http.get(url).then((response) => {
					this.$data.goodCount = response.data.GOODED_COUNT;
					this.$data.isActive = true;
				}, (response) => {
					console.log('opps Is Error: ' + response);
				});
			},
			// 关闭二维码框
			closeQrcodeBox() {
				this.isShowQrBox = false;
			}
		}
	}	
</script>


<style scoped lang="scss">
	/* 底部详情操作框 */
	.reviews {
		position: fixed;
		bottom: 0;
		left: 0;
		right: 0;
		height: 60px;
		z-index: 100;
		background: #F6F6F6;
		ul  {
			padding: 0 5%;
			li {
				display: inline-block;
				 25%;
				height: 30px;
				line-height: 30px;
				margin: 15px 4% 0 0;
				text-align: center;
				border-radius: 10%;
				background: #fff;
				box-sizing: border-box;
				i {
					margin-right: 5px;
				}
				&:last-child {
					color: #e60012;
					 13%;
					margin-right: 0;
				}
			}
		}
		/* 选中样式 */
		.active {
			color: #e60012;
			pointer-events: none;
		}
	}
	/* 评论框基本样式 */
	.reviews-box {
		position: fixed;
		left: 0;
		right: 0;
		bottom: 0;
		z-index: 200;
		 100%;
		height: 100px;
		padding: 2% 8% 5%; 
		background: #F6F6F6;
		box-sizing: border-box;
		.header {
			 100%;
			padding-bottom: 5px;
			text-align: center;
			span {
				color: #000;
				&:first-child {
					float: left;
				}
				&:last-child {
					float: right;
					color: #333;
					pointer-events: none;
					&.active {
						pointer-events: auto;
						color: #e60012;
					}
				}
			}
		}
		/* 用户编辑框 */
		.body {
			textarea {
				min-height: 40px;
				 100%;
				padding: 2%;
				font-size: 14px;
				border-radius: 5%;
				border: 2px solid #F6F6F6;
				background: #fff;
				box-sizing: border-box;
				resize: none;
				box-shadow: none;
			}
		}
	}
	/* 动画效果 */
	.slide-fade-down-enter-active, .slide-fade-down-leave-active  {
		transition: all 1s ease-in;
	}
	.slide-fade-down-enter, .slide-fade-down-leave-to{
		transform: translate3d(0, 100px, 0);
	}

	.slide-fade-right-enter-active, .slide-fade-right-leave-active  {
		transition: all 1s ease-in;
	}
	.slide-fade-right-enter, .slide-fade-right-leave-to{
		transform: translate3d(100%, 0, 0);
	}
	.tip-box {
		position: absolute;
		left: 50%;
		top: 50%;
		transform: translate3d(-50%, -50%, 0);
		 200px;
		height: 100px;
		font-size: 18px;
		text-align: justify;
		padding: 10px 20px;
		background: #fff;
		box-shadow: 1px 1px 10px rgba(0, 0, 0, .5);
		box-sizing: border-box;
	}
	/* 右侧评论列表 */
	.comment-box {
		position: fixed;
		top: 40px;
		bottom: 50px;
		right: 0;
		 80%;
		overflow-y: auto;
		background: #F6F6F6;
		.mask {
			position: fixed;
			display: block;
			left: 0;
			right: 0;
			top: 0;
			bottom: 0;
			background: rgba(0, 0, 0, .5);
			z-index: 100;
		}
		.comment-main {
			position: relative;
			z-index: 100;
			section {
				min-height: 80px;
				padding: 2%;
				background: #fff;
				overflow: hidden;
				text-align: left;
				font-size: 14px;
				border-bottom: 2px solid #F6F6F6;
				box-sizing: border-box;
				.reviews-author {
					float: left;
					 25%;
					height: 80px;
					padding-left: 4%;
					margin-right: 2%;
					color: #333;
					box-sizing: border-box;
				}
				.reviews-body {
					min-height: 80px;
					overflow: hidden;
					.reviews-content {
						min-height: 40px;
					}
					p {
						&:first-child span {
							color: #e60012;
						}
						&:last-child {
							font-size: 12px;
						}
					}
				}
			}
		}
	}
	@media screen and  (min- 1000px) {
		.comment-box {
			top: 100px;
			bottom: 60px;
			.comment-main {
				section {
					font-size: 32px;
					.reviews-body {
						p {
							&:last-child {
								font-size: 24px;
							}
						}
					}
				}
			}
		}
	}
	#qrcode {
		position: relative;
		.mask {
			position: fixed;
			display: block;
			left: 0;
			right: 0;
			top: 0;
			bottom: 0;
			background: rgba(0, 0, 0, 0.4);
			z-index: 10;
		}
		#qrcode-content {
			position: fixed;
			left: 50%;
			top: 50%;
			padding: 15px;
			text-align: center;
			background: #fff;
			z-index: 100;
			transform: translate3d(-50%, -50%, 0);
			&:after {
				content: '扫一扫上面的二维码图案';
				display: block;
				padding-top: 10px;

			}
		}
	}
</style>
//srcpageexternalMap.vue
<template>
	<div>
        <v-header goBack="true" headTitle="外部地图"></v-header>
        <div class="travel-box">
			<div id="allmap"></div>
        </div>

    </div>
</template>

<script>
	import vHeader from '../components/header'
	export default {
		data() {
			return {
			}
		},
		components: {
			vHeader
		},
		mounted() {

			//百度地图API功能
			var map = new BMap.Map("allmap");    // 创建Map实例
			map.centerAndZoom(new BMap.Point(119.199201,34.019519), 18);  // 初始化地图,设置中心点坐标和地图级别
			map.addControl(new BMap.MapTypeControl());   //添加地图类型控件
			map.setCurrentCity("淮安国缘宾馆");          // 设置地图显示的城市 此项是必须设置的
			map.enableScrollWheelZoom(true);     //开启鼠标滚轮缩放
			// 编写自定义函数,创建标注
			function addMarker(point){
			  var marker = new BMap.Marker(point);
			  map.addOverlay(marker);
			}
			// 向指定地图里面添加标注
			addMarker(new BMap.Point(119.199201,34.019519));
		}
	}
</script>

<style scoped lang="scss">
	.travel-box {
		margin-top: 60px;
	}
	#allmap {
		 100%;
		height: 400px;
	}
	@media screen and (min- 1000px) {
		.travel-box {
			margin-top: 100px;
		}
		#allmap {
			height: 600px;
		}
	}
</style>

后记:我挺喜欢这个项目的代码的,哈哈哈,因为都看得懂,还能够猜到作者的意图。

原文地址:https://www.cnblogs.com/smart-girl/p/11166096.html