【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"></i>
</span>
<span class="left-icon side-bar" v-if="sideBar" @click="showSideBar">
<i title="主菜单" class="iconfont"></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"></i>景区介绍</router-link>
<router-link :to="{name: 'service', params: {type: 3}}"><i class="iconfont"></i>景区公告</router-link>
<router-link :to="{name: 'service', params: {type: 15}}"><i class="iconfont"></i>景区服务</router-link>
<router-link :to="{name: 'service', params: {type: 13}}"><i class="iconfont"></i>预订门票</router-link>
<router-link :to="{name: 'service', params: {type: 14}}"><i class="iconfont"></i>特色购物</router-link>
<router-link to="/travelBox"><i class="iconfont"></i>旅行百宝箱</router-link>
<router-link :to="{name: 'dropBox', params: {url: vrUrl, title: vrTitle}}"><i class="iconfont"></i>虚拟旅游</router-link>
<router-link :to="{name: 'service', params: {type: 6}}"><i class="iconfont"></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;
width: 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%;
width: 50px;
transform: translateY(-50%);
i {
color: $nav-color;
}
}
.side-bar {
left: 0;
width: 10%;
background: $nav-color;
i {
color: #fff;
}
}
nav {
position: fixed;
left: 0;
right: 0;
top: 40px;
bottom: 50px;
z-index: 20;
width: 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-width: 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 {
width: $header-height;
}
nav {
top: $header-height;
bottom: $header-height;
width: 300px;
a {
height: $header-height;
line-height: $header-height;
border-bottom: 3px solid #e6e6e6;
}
}
}
}
</style>
userCount.vue不知道表达什么意思?
//userCount.vue
//src\components\userCount.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;
width: 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-width: 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"></i><span>景区服务</span>
</router-link>
</li>
<li>
<router-link to="/home" :class=" pathName == navUrl[1] ? 'active' : ''">
<i class="iconfont"></i><span>主页</span>
</router-link>
</li>
<li>
<router-link :to="{name: 'service', params: {type: 6}}" :class=" pathName == navUrl[2] ? 'active' : ''">
<i class="iconfont"></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;
width: 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;
width: 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;
width: 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-width: 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;
width: 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 {
width: 100%;
height: 100%;
}
}
.right-side {
position: fixed;
right: 4%;
bottom: 70px;
z-index: 10;
width: $right-side-size;
a {
display: inline-block;
width: $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-width: 1000px) {
$right-side-size: 150px;
.header-logo {
height: 100px;
}
.banner {
.swiper-container {
top: 100px;
bottom: 100px;
}
.right-side {
right: 4%;
bottom: 50%;
width: $right-side-size;
transform: translate3d(0, 50%, 0);
a {
width: $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;
width: 100%;
padding: 2%;
margin-bottom: 10px;
overflow: hidden;
box-sizing: border-box;
background: #fff;
a {
display:inline-block;
}
.list-image {
float: left;
width: 30vw;
height: 30vw;
margin-right: 3vw;
box-sizing: border-box;
img {
width: 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;
width: 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-width: 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;
width: 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>
//src\page\service.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>
//src\page\travelBox.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"></i>
<span>旅游线路</span>
</router-link>
<router-link :to="{name: 'externalMap'}">
<i class="iconfont"></i>
<span>外部交通</span>
</router-link>
<router-link :to="{name: 'listDetail', params: {id: 4, type: 21, identifier: 0}}">
<i class="iconfont"></i>
<span>景区地图</span>
</router-link>
<router-link :to="{name: 'service', params: {type: 7}}">
<i class="iconfont"></i>
<span>周边景点</span>
</router-link>
<router-link :to="{name: 'service', params: {type: 6}}">
<i class="iconfont"></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;
width: 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-width: 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"> 音频播放</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 {
width: 100%;
color: #fff;
padding: 1% 4%;
text-align: right;
background: #000;
opacity: .4;
box-sizing: border-box;
i {
display: inline-block;
width: 100px;
height: 30px;
line-height: 30px;
}
}
.detail-body {
padding: 10px 10px 40px 10px ;
section {
width:100%;
img {
width:100%;
}
}
}
footer {
position: absolute;
right: 0;
bottom: 0;
width: 100%;
height: 40px;
background: #f6f6f6;
text-align: right;
a {
display: inline-block;
width: 80px;
height: 40px;
line-height: 40px;
padding: 2px;
color: #FFF;
text-align: center;
background: #e60012;
box-sizing: border-box;
}
}
}
@media screen and (min-width: 1000px) {
.detail {
padding-top: 100px;
.audio-play {
i {
font-size: 32px;
width: 200px;
height: 50px;
line-height: 50px;
}
}
footer {
height: 100px;
}
}
}
</style>
这个组件中引用了review组件
<template>
<div>
<div class="reviews" v-show="isShow">
<ul>
<li><i class="iconfont"></i>{{ visitCount }}</li>
<li @click.once="upVote" :class="{active: isActive }"><i class="iconfont"></i>{{ goodCount }}</li>
<li @click="showReviewBox"><i class="iconfont"></i>写评论</li>
<li @click="showCommentBox"><i class="iconfont"></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;
width: 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;
width: 13%;
margin-right: 0;
}
}
}
/* 选中样式 */
.active {
color: #e60012;
pointer-events: none;
}
}
/* 评论框基本样式 */
.reviews-box {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 200;
width: 100%;
height: 100px;
padding: 2% 8% 5%;
background: #F6F6F6;
box-sizing: border-box;
.header {
width: 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;
width: 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);
width: 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;
width: 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;
width: 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-width: 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>
//src\page\externalMap.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 {
width: 100%;
height: 400px;
}
@media screen and (min-width: 1000px) {
.travel-box {
margin-top: 100px;
}
#allmap {
height: 600px;
}
}
</style>
后记:我挺喜欢这个项目的代码的,哈哈哈,因为都看得懂,还能够猜到作者的意图。
作者:jser_dimple
-------------------------------------------
个性签名:一个人在年轻的时候浪费自己的才华与天赋是一件非常可惜的事情
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
万水千山总是情,打赏5毛买辣条行不行,所以如果你心情还比较高兴,也是可以扫码打赏博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!
微信
支付宝