首页 > 其他 > 详细

【美好生活】 haha-edu

时间:2020-05-12 19:23:11      阅读:54      评论:0      收藏:0      [点我收藏+]

老规矩,先放上github地址:https://github.com/hanxyan/haha-edu
接下来我们看看效果图
技术分享图片
接下来我们一起看代码吧
main.js就是我们的入口

import Vue from ‘vue‘
import App from ‘./App.vue‘
import router from ‘./router‘
import store from ‘./store‘
import { Lazyload } from ‘vant‘;
import ‘./assets/iconfont/iconfont.js‘
import ‘./assets/iconfont/icon.css‘

Vue.config.productionTip = false
Vue.use(Lazyload);

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount(‘#app‘)

它里面引用了iconfont.js还有icon.css,以及懒加载
接下来我们看app.vue

<template>
  <div id="app" :style="{height: ‘100%‘}">
    <div class="main" :style="{height: ‘100%‘}">
      <transition name="router-fade" mode="out-in">
        <keep-alive>
          <router-view v-if="$route.meta.keepAlive"></router-view>
        </keep-alive>
      </transition>
      <transition name="router-fade" mode="out-in">
        <!-- <keep-alive> -->
          <router-view v-if="!$route.meta.keepAlive"></router-view>
        <!-- </keep-alive> -->
      </transition>
      <!-- <router-view></router-view> -->
    </div>
    <footer-nav :show="showFootTab"></footer-nav>
  </div>
</template>

<script>
import footerNav from ‘@/components/footer-nav.vue‘

export default {
  name: ‘app‘,
  components: {
    footerNav
  },
  data(){
    return {
      showFootTab:true,
    }
  },
  watch: {
    ‘$route‘: function() {
      this.routeChange()
    }
  },
  created () {
    this.routeChange()
  },
  mounted () {

  },
  methods: {
    routeChange () {
      let path = this.$route.path
      console.log(‘path111111111‘,path)
      if (path === ‘/‘ || path === ‘/index‘ || path === ‘/myCourse‘|| path === ‘/message‘|| path === ‘/me‘) {
        this.showFootTab = true
      } else {
        this.showFootTab = false
      
      }
    }
  }
}
</script>

<style lang="scss">
#app {
  font-family: ‘Avenir‘, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: $color-regular-text;
  //margin-top: 60px;

  .router-fade-enter-active, .router-fade-leave-active {
    transition: opacity .3s;
  }

  .router-fade-enter, .router-fade-leave-active {
    opacity: 0;
  }

  .main{
    margin-bottom:160px;
  }
}
</style>

app.vue中会监听路由的变化,并以此控制会不会显示下面的footer组件

//foter-nav.vue
<template>
	<van-tabbar 
	v-model="active" 
	v-show="show" 
	:active-color="themePriamryColor"
	class="foot-bar"
	route>
		<van-tabbar-item 
		v-for="(item,index) in tabs"
		:name="item.routeName"
		:to="item.routeName"
		class="tab-item"
		:key="index">
			<template v-slot:icon>
				<span class="icon-wrap">
					<svg class="uficon" aria-hidden="true">
						<use :xlink:href="item.icon"></use>
					</svg>
				</span>
			</template>
			<span class="tab-item-text">{{item.text}}</span>
		</van-tabbar-item>
	</van-tabbar>
</template>

<script>
	import { Tabbar, TabbarItem } from ‘vant‘;

	const THEME_PRIMARY=‘#3498db‘
	const REGULAR_TEXT_COLOR=‘#333‘
	const COMPONENT_NAME = ‘footer-nav‘

	export default {
		name: COMPONENT_NAME,
		components: {
			[Tabbar.name]: Tabbar,
			[TabbarItem.name]: TabbarItem
		},
		mixins:[],
		props:{
			show: {
				type: Boolean,
				default: true
			}
		},
		data(){
			return {
				active:‘index‘,
				themePriamryColor:THEME_PRIMARY,//主题色
				regularTextColor:REGULAR_TEXT_COLOR,//常规字体颜色
				tabs:[{
					icon:‘#uficon-shouye1‘,
					text:‘首页‘,
					routeName:‘/index‘
				},{
					icon:‘#uficon-xuexizhongxin‘,
					text:‘学习‘,
					routeName:‘/myCourse‘
				},{
					icon:‘#uficon-duanxinjilu‘,
					text:‘消息‘,
					routeName:‘/message‘
				},{
					icon:‘#uficon-wode‘,
					text:‘我的‘,
					routeName:‘/me‘
				}]
			}
		},
		computed: {

		},
		watch: {},
		created () {},
		mounted () {},
		methods: {}
	}
</script>

<style lang="scss" scoped>
	.foot-bar{
		height:140px;
		font-size:40px;
		
		.tab-item{
			font-size:1em;

			.icon-wrap{
				.uficon{
					width:60px;
					height:60px;
					font-size:60px;
					line-height:1;
				}
			}

			&-text{
				font-size:.75em;
			}
		}
		
	}
</style>

footer.vue中挺有意思的,把主题色还有常规字体颜色都抽出来了,然后就是组件的引用方法
接下来我们看router.js

//router.js
// 首页
// import App from ‘../App‘
const HOME = () => import(‘@/views/Home.vue‘);
const ABOUT = () => import(/* webpackChunkName: "about" */‘@/views/About.vue‘);
const LOGIN_CAPTCHA=() => import(‘@/views/login/login-captcha.vue‘);
const INDEX=() => import(‘@/views/index/index.vue‘);
const USER_INDEX=() => import(‘@/views/user/index.vue‘);
const MESSAGE=() => import(‘@/views/message/message.vue‘);
const MY_COURSE_INDEX=() => import(‘@/views/my-course/index.vue‘);
const COURSE_DETAIL=() => import(‘@/views/course-detail/index.vue‘);
const COURSE_LIST=() => import(‘@/views/course-list/index.vue‘);


export const getMenuList=()=>{
	// let menu=[];
	let filterMenu=[
		//地址为空时跳转home页面
        {
            path: ‘/‘,
            redirect: ‘/index‘
        },
        {
			path:‘/index‘,
			name:‘index‘,
			component: INDEX,
			children:null
		},
		{
			path:‘/me‘,
			name:‘me‘,
			component: USER_INDEX,
			children:null
		},
		{
			path:‘/myCourse‘,
			name:‘myCourse‘,
			component: MY_COURSE_INDEX,
			children:null
		},
		{
			path:‘/message‘,
			name:‘message‘,
			component: MESSAGE,
			children:null
		},
        {
			path:‘/login‘,
			name:‘login‘,
			component: LOGIN_CAPTCHA,
			meta:{keepAlive:true},
			children:null
		},
		{
			path:‘/courseList‘,
			name:‘courseList‘,
			component: COURSE_LIST,
			children:null
		},
		{
			path:‘/courseDetail‘,
			name:‘courseDetail‘,
			meta:{keepAlive:false},
			component: COURSE_DETAIL,
			children:null
		},
		{
			path:‘/home‘,
			name:‘home‘,
			component: HOME,
			children:null
		},
		{
			path:‘/about‘,
			name:‘about‘,
			component: ABOUT,
			children:null
		}
	];

	// let menu=[{
	// 	path: ‘/‘,
	// 	component: App, //顶层路由,对应index.html
	// 	children:filterMenu
	// }]
	return filterMenu;
}

export default getMenuList();

接下来我们看index页面
技术分享图片

//index.vue
<template>
	<div>
		<section class="layout-header" :style="headerStyle">
			<section class="top-search">
				<van-row type="flex" justify="space-between">
					<!-- <van-col span="6" class="location-wrap">
						<span>
							<van-icon name="location-o" size="1em" />
							南宁
						</span>
					</van-col> -->
					<van-col span="24">
						<form action="/">
							<van-search 
							v-model="searchKV"
							background="none"
							placeholder="搜索课程"
							shape=‘round‘>
								<template v-slot:left-icon>
									<svg class="uficon" aria-hidden="true">
										<use xlink:href="#uficon-search"></use>
									</svg>
								</template>
							</van-search>
						</form>
					</van-col>
				</van-row>
			</section>
			<section class="swipe-wrap">
				<van-swipe 
				indicator-color="white"
				:autoplay="3000"
				class="swipe-body"
				ref="swipe"
				@change="swipeChangeEvt">
					<van-swipe-item 
					v-for="(img, index) in swipeImages" 
					:key="index"
					:style="{background:‘url(‘+img.src+‘) no-repeat‘,backgroundPosition:‘center top‘,backgroundSize: ‘cover‘}"
					class="swipe-item"
					@click="swipeItemClickEvt">
						<img 
						v-show="false"
						:src="img.src" />
					</van-swipe-item>
				</van-swipe>
			</section>
		</section>
		<section class="layout-content">
			<section class="catalog-menu">
				<van-grid
				:column-num="3"
				:border="false">
					<van-grid-item 
					v-for="(item,index) in navInfo"
					:key="index"
					:to="item.to">
						<template v-slot:icon>
							<div :class="[‘icon-wrap‘,‘icon-wrap-‘+(index+1)]">
								<svg class="uficon" aria-hidden="true">
									<use :xlink:href="item.icon"></use>
								</svg>
							</div>
						</template>
						<template v-slot:text>
							<span class="item-text">{{item.text}}</span>
						</template>
					</van-grid-item>
				</van-grid>
			</section>
			<section class="public-course-part">
				<van-cell
				class="section-header"
				size="large"
				:border="false"
				is-link 
				to="courseList">
					<template v-slot:title>
						<h3 class="section-title">公开课</h3>
					</template>
				</van-cell>
				<div ref="publicWrapper" class="horizon-scroll-wrapper pc-scroll-wrapper">
					<div class="horizon-scroll-content pc-scroll-content">
						<base-media-card
						v-for="(item,index) in publicCourse"
						:key="index"
						class="pc-scroll-item"
						media-class="pubilc-media"
						content-class="pubilc-info-wrap"
						:img="item.cover"
						@click="courseClickEvt(item)">
							{{item.title}}
						</base-media-card>
					</div>
				</div>
			</section>
			<section class="parent-course-part">
				<van-cell
				class="section-header"
				size="large"
				:border="false"
				is-link 
				to="courseList">
					<template v-slot:title>
						<h3 class="section-title">家长课堂</h3>
					</template>
				</van-cell>
				<div ref="parentWrapper" class="horizon-scroll-wrapper parent-scroll-wrapper">
					<div class="horizon-scroll-content parent-scroll-content">
						<base-media-card
						v-for="(item,index) in parentCourse"
						:key="index"
						class="horizon-scroll-item parent-scroll-item"
						media-class="parent-media"
						content-class="parent-info-wrap"
						:img="item.cover"
						@click="courseClickEvt(item)">
							{{item.title}}
						</base-media-card>
					</div>
				</div>
			</section>
			<section class="course-list">
				<van-cell
				class="section-header"
				size="large"
				:border="false">
					<template v-slot:title>
						<h3 class="section-title">精品课程</h3>
					</template>
				</van-cell>
				<van-tabs 
				v-model="activeCourseTabs"
				:color="themePriamryColor"
				:title-active-color="themePriamryColor"
				:title-inactive-color="regularTextColor"
				class="course-tabs"
				swipeable>
					<van-tab 
					name="hot">
						<template slot="title">
							<span class="tab-title">热门课程</span>
						</template>
						<div class="padding-30">
							<course-card
							title="小学五年级英语培训班"
							course-no="12345678906"
							time="周日 10:00-12:00"
							location="西校园2号楼3楼"
							duration="2019.09.07-2019.09.28"
							teacher-name="黄明明"
							profile-src="https://cn.vuejs.org/images/logo.png"
							:rate-value="rateTestVal"
							prize="¥666"
							class="course-card"
							@rate-change="rateTestEvt"
							@click="swipeItemClickEvt">
								<template v-slot:title-icon>
									<span class="span-icon-one qiu mgr-20">秋</span>
								</template>
								<template v-slot:status>
									<van-button plain type="primary" size="small">增开</van-button>
								</template>
							</course-card>
							<course-card
							title="小学五年级英语培训班"
							course-no="12345678906"
							time="周日 10:00-12:00"
							location="西校园2号楼3楼"
							duration="2019.09.07-2019.09.28"
							status-text="剩余3人"
							teacher-name="曾小贤"
							:rate-value="4"
							prize="¥888"
							@click="swipeItemClickEvt">

							</course-card>
						</div>
						
					</van-tab>
					<van-tab 
					name="new">
						<template slot="title">
							<span class="tab-title">新课推荐</span>
						</template>
						<div class="padding-30">
							<course-card
							v-for="n in 6"
							:key="n"
							class="course-card"
							title="小学五年级英语培训班"
							course-no="12345678906"
							time="周日 10:00-12:00"
							location="西校园2号楼3楼"
							duration="2019.09.07-2019.09.28"
							status-text="剩余3人"
							teacher-name="曾小贤"
							:rate-value="4"
							prize="¥888"
							@click="swipeItemClickEvt">

							</course-card>
						</div>
					</van-tab>
				</van-tabs>
			</section>
		</section>
		<section class="layout-footer"></section>
	</div>
</template>

<script>
	import { Row, Col, Icon, Search, Swipe, SwipeItem, Grid, GridItem, Image, Tab, Tabs, Button, Cell } from ‘vant‘;
	import courseCard from ‘@/components/course-card.vue‘;
	import baseMediaCard from ‘@/components/base-media-card.vue‘;
	import BScroll from ‘better-scroll‘;
	import commonMixin from ‘@/libs/mixins/common.js‘;
	import { THEME_PRIMARY, THEME_LIGHT_BLUE, REGULAR_TEXT_COLOR } from ‘@/libs/constant.js‘;
	// import NAV_ONE from ‘@/assets/images/index_menu_01.png‘;
	// import NAV_TWO from ‘@/assets/images/index_menu_02.png‘;
	// import NAV_THREE from ‘@/assets/images/index_menu_03.png‘

	const COMPONENT_NAME = ‘index‘

	export default {
		name: COMPONENT_NAME,
		components: {
			[Row.name]: Row,
			[Col.name]: Col,
			[Icon.name]: Icon,
			[Search.name]: Search,
			[Swipe.name]: Swipe,
			[SwipeItem.name]: SwipeItem,
			[Grid.name]: Grid,
			[GridItem.name]: GridItem,
			[Image.name]: Image,
			[Tab.name]: Tab,
			[Tabs.name]: Tabs,
			[Button.name]: Button,
			[Cell.name]: Cell,
			courseCard,
			baseMediaCard
		},
		mixins:[commonMixin],
		props:{},
		data(){
			return {
				themePriamryColor:THEME_PRIMARY,//主题色
				themeLightColor:THEME_LIGHT_BLUE,//浅主题色
				regularTextColor:REGULAR_TEXT_COLOR,//常规字体颜色
				searchKV:‘‘,//顶部搜索框关键字
				headerBg:‘‘,//顶部模块背景图
				swipeImages:[
				{src:"https://cdn.pixabay.com/photo/2016/08/09/08/46/education-1580143__480.jpg"},
				{src:"https://image.shutterstock.com/image-photo/ad-advertisement-marketing-commercial-concept-260nw-309964772.jpg"},
				{src:"https://image.shutterstock.com/image-photo/top-view-shot-group-creative-260nw-665740429.jpg"},
				{src:"https://cdn.pixabay.com/photo/2018/09/17/11/30/media-3683580__480.jpg"},
				{src:"https://cdn.pixabay.com/photo/2017/02/20/14/18/business-2082639__480.jpg"}
				],//轮播图
				navInfo:[{
					icon:‘#uficon-notebook‘,
					text:‘课程辅导‘,
					to:‘login‘
				},{
					icon:‘#uficon-teach‘,
					text:‘儿童辅导‘,
					to:‘courseList‘
				},{
					icon:‘#uficon-yishu‘,
					text:‘兴趣爱好‘,
					to:‘courseList‘
				}],
				publicCourse:[{
					cover:‘https://image.shutterstock.com/image-photo/top-view-shot-group-creative-260nw-665740429.jpg‘,
					title:‘《计算机网络原理 第七版》‘,
					to:‘‘
				},{
					cover:‘https://cdn.pixabay.com/photo/2015/02/18/10/48/social-media-640543__480.png‘,
					title:‘初一科学‘,
					to:‘‘
				},{
					cover:‘https://cdn.pixabay.com/photo/2015/02/18/10/48/social-media-640543__480.png‘,
					title:‘英语直播班‘,
					to:‘‘
				},{
					cover:‘https://cdn.pixabay.com/photo/2015/02/18/10/48/social-media-640543__480.png‘,
					title:‘爱上语文‘,
					to:‘‘
				},{
					cover:‘https://image.shutterstock.com/image-photo/view-male-face-through-hole-260nw-1034889298.jpg‘,
					title:‘编程小课程‘,
					to:‘‘
				}],//公开课
				parentCourse:[{
					cover:‘https://cdn.pixabay.com/photo/2015/02/18/10/48/social-media-640543__480.png‘,
					title:‘新学期必备锦囊请接收‘,
					to:‘‘
				},{
					cover:‘https://cdn.pixabay.com/photo/2015/02/18/10/48/social-media-640543__480.png‘,
					title:‘家长必了解的小升初常识‘,
					to:‘‘
				},{
					cover:‘https://cdn.pixabay.com/photo/2015/02/18/10/48/social-media-640543__480.png‘,
					title:‘教孩子如何应对自然灾害‘,
					to:‘‘
				},{
					cover:‘https://cdn.pixabay.com/photo/2015/02/18/10/48/social-media-640543__480.png‘,
					title:‘家长看护好孩子永远是第一重要功课‘,
					to:‘‘
				},{
					cover:‘https://cdn.pixabay.com/photo/2015/02/18/10/48/social-media-640543__480.png‘,
					title:‘孩子沉迷电脑怎么办?‘,
					to:‘‘
				}],//家长课堂
				activeCourseTabs:‘hot‘,//课程模块中选中的tab
				rateTestVal:3
			}
		},
		computed: {
			// 页面顶部模块样式
			headerStyle(){
				if(this.headerBg!==‘‘){
					return {
						background:`url(${this.headerBg}) no-repeat`
						// transition: ‘background .5s ease-in‘
					}
				}else{
					return {}
				}
			}
		},
		watch: {

		},
		created () {},
		mounted () {
			this.headerBg=this.swipeImages[0].src;
			this.$nextTick(()=>{
				this.publicScroll = this.initScroll(‘publicWrapper‘)
				this.parentScroll=this.initScroll(‘parentWrapper‘)
			})
		},
		beforeDestroy() {
			this.destroy(‘publicScroll‘)
			this.destroy(‘parentScroll‘)
		},
		methods: {
			swipeItemClickEvt(){
				this.routerJumpEvtMixin({name:‘courseDetail‘,query:{id:‘课程详情‘}})
			},
			courseClickEvt({title}){
				this.routerJumpEvtMixin({name:‘courseDetail‘,query:{id:title}})
			},
			rateTestEvt(val){
				this.rateTestVal=val;
			},
			swipeChangeEvt(index){
				// if(index===0){
					// this.$refs.swipe.swipeTo(0,{immediate:true});
				// }
				this.headerBg=this.swipeImages[index].src;
				console.log(‘this.headerBg....2222‘,this.headerBg)
			},
			initPublicScroll(){
				this.publicScroll = this.initScroll(‘publicWrapper‘)

			},
			destroy(scrollerName){
				this[scrollerName]&&this[scrollerName].destroy()
				this[scrollerName]=null
			},
			initScroll(wrap_dom,outer_options={},dir=‘horizontal‘){
				if(!this.$refs[wrap_dom]){
					return null;
				}

				const defaultOptions={
					observeDOM: true,
					click: true,
					probeType: 1,
					scrollbar: false,
					pullDownRefresh: false,
					pullUpLoad: false
				}

				let options=Object.assign({}, defaultOptions,{
					scrollY: dir === ‘vertical‘,
					scrollX: dir === ‘horizontal‘,
				},outer_options)

				let scroller = new BScroll(this.$refs[wrap_dom], options)

				return scroller;
			}
		}
	}
</script>

<style lang="scss">
	
</style>
<style lang="scss" scoped>
	.span-icon-one{
		display:inline-flex;
		height:50px;
		min-width:50px;
		border-radius:6px;
		justify-content:center;
		align-items:center;
		font-size:.620em;
		padding:2px;
	}

	.horizon-scroll-wrapper{
		position:relative;
		overflow:hidden;

		.horizon-scroll-content {
			white-space:nowrap;
			display: inline-block;
			padding: 0 35px 35px;
			
			.horizon-scroll-item{
				display:inline-block;
			}
		}
	}

	.layout-header{
		padding:30px;
		background-color:orange;

		.top-search{
			.location-wrap{
				display:flex;
				justify-content:center;
				align-items:center;

				span {
					display:inline-flex;
					align-items:center;

					i {
						margin-right:10px;
					}
				}
			}
		}

		.swipe-wrap{
			.swipe-body{
				height:375px;
				border-radius:15px;
				z-index:20;

				.swipe-item{
					background-color:blue;
					&:nth-child(odd){
						background-color:pink;
					}
				}
			}
		}
	}

	.layout-content{
		position:relative;
		top:-180px;

		&::before{
			content:"";
			display:block;
			height:200px;
			background-color:$color-basic-white;
			border-top-left-radius:180px 30px;
			border-top-right-radius:180px 30px;
		}

		.catalog-menu {
			font-size:40px;
			.icon-wrap {
				width:2em;
				height:2em;
				border-radius:50%;
				color:$color-basic-white;
				background-image:linear-gradient( 45deg, #B2FEFA, #0ED2F7, rgba(25,118,210,0) ), url(../../assets/images/bg-gradient.svg);
				background-size:cover;
				display:flex;
				justify-content:center;
				align-items:center;
				font-size:2em;
				margin-bottom:30px;
			}

			.icon-wrap-2{
				background-image:linear-gradient( 45deg, #24C6DC, #4776E6, rgba(25,118,210,0) ), url(../../assets/images/bg-gradient.svg);
			}

			.icon-wrap-3{
				background-image:linear-gradient( 45deg, #00c6ff, #0072ff, rgba(25,118,210,0) ), url(../../assets/images/bg-gradient.svg);
			}

			.item-text{
				font-size:.75em;
				font-weight:bold;
				color:$color-regular-text;
			}
		}

		.section-header{
			text-align:left;
			padding:30px 35px;

			.section-title{
				font-size:40px;
				font-weight:bold;
				margin:0;
			}
		}

		.public-course-part{
			&::before{
				content:"";
				display:block;
				height:20px;
				background-color:$color-border-three;
			}

			font-size:36px;

			.pc-scroll-wrapper{
				position:relative;
				overflow:hidden;
				
				.pc-scroll-content{
					white-space:nowrap;
					display: inline-block;
					padding: 0 35px 35px;

					.pc-scroll-item{
						display:inline-block;
						width:580px;
						//height:300px;
						overflow:hidden;

						&:not(:last-child){
							margin-right:20px;
						}

						::v-deep .bmc-card-body{
							padding:0;
						}

						::v-deep .pubilc-media{
							height:354px;
						}

						::v-deep .pubilc-info-wrap{
							padding:30px 15px;
							overflow-x: hidden;
							text-overflow:ellipsis;
							white-space: nowrap;
							font-size:1em;
							font-weight:bold;
							//color:$color-heavy-text;
						}
					}
				}
			}

		}

		.parent-course-part{
			&::before{
				content:"";
				display:block;
				height:20px;
				background-color:$color-border-three;
			}

			font-size:36px;

			.parent-scroll-wrapper{
				
				.parent-scroll-content{

					.parent-scroll-item{
						width:430px;
						overflow:hidden;

						&:not(:last-child){
							margin-right:20px;
						}

						::v-deep .bmc-card-body{
							padding:0;
						}

						::v-deep .parent-media{
							height:220px;
							border-top-right-radius:15px;
							border-top-left-radius:15px;
						}

						::v-deep .parent-info-wrap{
							padding:30px 15px;
							overflow-x: hidden;
							text-overflow:ellipsis;
							white-space: nowrap;
							font-size:1em;
							font-weight:bold;
							//color:$color-heavy-text;
						}
					}
				}
			}

		}

		.course-list{
			&::before{
				content:"";
				display:block;
				height:20px;
				background-color:$color-border-three;
			}

			.course-tabs{
				.tab-title{
					font-weight:bold;
				}

				.course-card{
					margin-bottom:40px;

					.qiu{
						border:3px solid $color-light-orange;
						color:$color-light-orange;
						background-color:transparent;
					}
				}
			}		
			
		}
	}
</style>


这个里面用了van这个UI组件,确实内容模块清晰很多
接下来我们看里面对应的组件
第一个是course-card组件

<template>
	<div @click="handleCardClick">
		<base-card card-class="card">
			<div class="cc-header">
				<slot name="title-icon">
					<van-icon class="title-icon" :name="titleIcon?titleIcon:‘notes-o‘" />
				</slot>
				<slot name="title">
					<h3>{{title}}</h3>
				</slot>
			</div>
			<div class="cc-content">
				<slot>
					<div v-if="courseNo" class="row">
						<van-icon class="item-icon" name="label" />
						<span>{{courseNo}}</span>
					</div>
					<div v-if="time" class="row">
						<van-icon class="item-icon" name="bell" />
						<span>{{time}}</span>
					</div>
					<div v-if="location" class="row">
						<van-icon class="item-icon" name="location" />
						<span>{{location}}</span>
					</div>
					<div class="flex-btw-center row">
						<span class="duration">{{duration}}</span>
						<slot name="status">
							<span :class="statusClasses">
								{{statusText}}
							</span>
						</slot>
					</div>
				</slot>
			</div>
			<div v-if="showTeacher||prize" class="cc-footer flex-btw-center">
				<div v-if="showTeacher" class="flex-start-center teacher-info">
					<div class="profile-pic">
						<slot name="profile-pic">
							<van-image 
							class="profile-pic"
							round
							:src="profileSrc?profileSrc:teacherProfilePic"
							/>
						</slot>
					</div>
					<div class="pofile-info">
						<div class="pofile-info-name">{{teacherName}}</div>
						<div class="rate-wrap flex-btw-center" v-if="rateValue">
							<van-rate 
							:value="innerRate" 
							size="1.2em"
							color="#36d7b7"
							@change="rateChangeEvt"
							/>
							<span class="rate-val">{{Number(innerRate).toFixed(1)}}</span>
						</div>
					</div>
					<span class="decorate-line" v-show="locationFoot||classroom">|</span>
					<div class="flex-col-stre-start foot-address" v-if="locationFoot||classroom">
						<div v-if="locationFoot" class="flex-start-center">
							<van-icon class="item-icon" name="location" />
							<span>{{location}}</span>
						</div>
						<div v-if="classroom" class="flex-start-center">
							<van-icon class="item-icon" name="more" />
							<span>{{classroom}}</span>
						</div>
					</div>
				</div>
				<div class="prize" v-if="prize">
					{{prize}}
				</div>
			</div>
		</base-card>
	</div>
</template>

<script>
	import baseCard from ‘@/components/base-card.vue‘;
	import { Icon, Cell, Button, Image, Rate } from ‘vant‘;
	import TEACHER_PROFILE_PIC from ‘@/assets/images/teacher.png‘;

	const COMPONENT_NAME = ‘course-card‘
	const EVENT_RATE_CHANGE=‘rate-change‘
	const EVENT_CLICK = ‘click‘

	export default {
		name: COMPONENT_NAME,
		components: {
			baseCard,
			[Icon.name]: Icon,
			[Cell.name]: Cell,
			[Button.name]: Button,
			[Image.name]: Image,
			[Rate.name]: Rate
		},
		props:{
			titleIcon:String,
			title:String,
			courseNo:String,
			time:String,
			location:String,
			locationFoot:{
				type:Boolean,
				default:false
			},
			duration:{
				type:String,
				default:‘‘
			},
			statusText:{
				type:String,
				default:‘‘
			},
			statusClass:{},
			profileSrc:{
				type:String,
				default:‘‘
			},
			rateValue:{
				type:Number,
				default:0
			},
			showTeacher:{
				type:Boolean,
				default:true
			},
			teacherName:{
				type:String,
				default:‘‘
			},
			classroom:{
				type:String,
				default:‘‘
			},
			prize:{
				type:String,
				default:‘‘
			}
		},
		data(){
			return {
				teacherProfilePic:TEACHER_PROFILE_PIC,
				innerRate:this.rateValue
			}
		},
		computed: {
			statusClasses(){
				return [`status`,this.statusClass]
			}
		},
		watch:{
			rateValue(val){
				if (val !== this.innerRate) {
					this.innerRate=val;
				}
			}
		},
		methods:{
			rateChangeEvt(cur){
				this.$emit(EVENT_RATE_CHANGE,cur);
			},
			handleCardClick(){
				this.$emit(EVENT_CLICK);
			}
		}
	}
</script>

<style lang="scss">
	.card{
		padding:10px;
		box-shadow: 4px 3px 7px 2px rgba(0, 0, 0, 0.04);
		border-radius: 20px;
		border:2px solid $color-border-two;
	}

	.cc-header{
		display:flex;
		align-items: center;
		font-size:50px;

		.title-icon{
			font-size:50px;
			margin-right:20px;
		}

		h3{
			font-size:44px;
			margin:29px 0;
			//line-height: 44px;
			overflow: hidden;
			text-overflow: ellipsis;
			white-space: nowrap;
		}
	}

	.cc-content{
		text-align:left;
		padding-bottom:15px;

		.row{
			padding:17px 0;
			font-size:40px;

			.item-icon{
				color:$color-light-text;
				font-size:42px;
				margin-right:29px;
			}

			.duration{
				color:$color-light-text;
			}

			.status{
				font-size:34px;
			}

		}
	}

	.cc-footer{
		border-top:2px solid $color-border-two;
		padding: 25px 0;
		font-size:36px;

		.teacher-info {
			.profile-pic{
				width:82px;
				height:82px;
				margin-right:20px;
			}

			.pofile-info{
				text-align:left;
				font-size:1em;
			}

			&-name {
				margin-bottom:10px;
			}

			.rate-wrap{
				.rate-val{
					margin-left:20px;
					color:$color-light-text;
				}
			}
		}

		.decorate-line{
			margin-left:20px;
			color:$color-border-two;
		}

		.foot-address{
			text-align:left;
			margin-left:20px;

			.item-icon{
				color:$color-light-text;
				font-size:42px;
				margin-right:20px;
			}
		}

		.prize{
			color:#f13232;
			font-weight:bolder;
			font-size:1.1em;
		}
		
	}
</style>

按照样式封装的
还有base-card组件

<template>
	<div :class="cardClasses">
		<slot name="header">
			<div class="ss-card-header" v-if="header">
				{{ header }}
			</div>
		</slot>
		<div :class="contentClasses" :style="bodyStyle">
			<slot></slot>
		</div>
	</div>
</template>

<script>
	const COMPONENT_NAME = ‘base-card‘

	export default {
		name:COMPONENT_NAME,
		props: {
			header: {},
			bodyStyle: {},
			bodyClass:{},
			cardClass:{}
		},
		data(){
			return {

			}
		},
		computed: {
			contentClasses(){
				return [`ss-card-content`,this.bodyClass]
			},
			cardClasses(){
				return [`ss-card`,this.cardClass]
			}
		}
	}
</script>

<style lang="scss">
	.ss-card{
		.ss-card-content{
			padding: 20px;
		}
	}
</style>

接下来的是base-media-card.vue

<template>
	<div @click="handleCardClick">
		<base-card body-class="bmc-card-body">
			<div :class="mediaClasses" :style="mediaStyle">
				<slot name="media"></slot>
			</div>
			<div :class="contentClasses">
				<slot></slot>
			</div>
		</base-card>
	</div>
</template>

<script>
	import baseCard from ‘@/components/base-card.vue‘;

	const COMPONENT_NAME = ‘base-media-card‘
	const EVENT_CLICK = ‘click‘

	export default {
		name: COMPONENT_NAME,
		components: {
			baseCard
		},
		props: {
			img: String,
			mediaClass:{},
			contentClass:{}
		},
		data() {
			return {

			}
		},
		computed: {
			mediaClasses(){
				return [`bmc-media`,this.mediaClass]
			},
			mediaStyle(){
				let style={};

				if (this.img) {
					style.background = `url("${this.img}") center center / cover no-repeat`
				}

				return style;
			},
			contentClasses(){
				return [`bmc-content`,this.contentClass]
			}
		},
		watch:{},
		methods:{
			handleCardClick(){
				this.$emit(EVENT_CLICK);
			}
		}
	}
</script>

<style lang="scss">
	.bmc-card-body{
		display:flex;
		flex-direction:column;
	}
	.bmc-media{
		overflow:hidden;
	}

	.bmc-content{
		
	}
</style>

common.js中定义了路由的跳转

export default {
	methods:{
		routerJumpEvtMixin(obj={}){
			this.$router.push(obj);
		},
		routerGoMixin(n=-1){
			this.$router.go(n);
		}
	}
}

接下来看myCourse页面
技术分享图片

//my-course.vue
<template>
	<div>
		<section class="layout-header">
			
		</section>
		<section class="layout-show">
			<section class="nav-tab-part">
				<div class="nav-tab-wrap">
					<van-grid
					:border="false"
					class="nav-tab">
						<van-grid-item
						v-for="(item,index) in navInfo"
						:key="index">
							<div class="nav-tab-item flex-col-stre-start">
								<div :class="[‘icon-wrap‘,‘icon-wrap-‘+(index+1)]">
									<svg class="uficon" aria-hidden="true">
										<use :xlink:href="item.icon"></use>
									</svg>
								</div>
								<span class="item-text">{{item.text}}</span>
							</div>
						</van-grid-item>
					</van-grid>
				</div>
			</section>
		</section>
		<section class="layout-content">
			<section class="course-display-part">
				<van-cell
				class="section-header"
				size="large"
				:border="false"
				is-link 
				to="me">
					<template v-slot:title>
						<h3 class="section-title">我的课程</h3>
					</template>
				</van-cell>
				<div ref="courseDisWrapper" class="horizon-scroll-wrapper cd-scroll-wrapper">
					<div class="horizon-scroll-content cd-scroll-content">
						<base-media-card
						v-for="(item,index) in parentCourse"
						:key="index"
						class="horizon-scroll-item cd-scroll-item"
						media-class="cd-media"
						content-class="cd-info-wrap"
						media-style="‘background-clip‘:‘content-box‘"
						>
							<template v-slot:media>
								<div class="cd-media-img" :style="{background:‘url(‘+item.cover+‘) center center / cover no-repeat‘,height:‘100%‘}">
									
								</div>
							</template>
							{{item.title}}
						</base-media-card>
					</div>
				</div>
			</section>
			<section class="course-calendar-part">
				<van-cell
				class="section-header"
				size="large"
				:border="false">
					<template v-slot:title>
						<span :class="[‘section-title‘,{active:showYMPicker}]" @click="showYMPicker=true">{{yearMonthTitle}}</span>
					</template>
					<template v-slot:right-icon>
						<span class="icon-round" @click="backToTodayEvt">
							今
						</span>
					</template>
				</van-cell>
				<van-popup
				v-model="showYMPicker"
				position="bottom">
					<van-datetime-picker 
					:value="selectedDate"
					ref="yearMonthPicker"
					type="year-month"
					:formatter="formatter"
					@confirm="ympConfirmEvt"
					@cancel="ympCancelEvt"
					/>
				</van-popup>
				<div ref="calendarWrapper" class="horizon-scroll-wrapper calendar-scroll-wrapper">
					<div 
					class="horizon-scroll-content calendar-scroll-content"
					:style="{transform:‘translateX(-‘+activeDate*11.111+‘vw) translateY(0px) translateZ(0px)‘}">
						<div 
						v-for="(item,index) in curMonDaysInfo"
						:key="index"
						:class="[‘horizon-scroll-item‘,‘calendar-item-wrap‘,{‘selected‘:selectedDate.format(‘YYYY-MM-DD‘)===item.date}]"
						@click="dateClickEvt(item.date)">
							<div class="calendar-item">
								<span class="weekday">{{item.weekday}}</span>
								<span class="dayno">{{item.no}}</span>
								<span class="course-tag">{{item.classNum}}</span>
							</div>
						</div>
					</div>
				</div>
				<div class="cal-course-panel">
					<van-cell 
					:title="`以下是您9月2日(周一)的课程`" 
					title-class="cal-course-tip"
					size="large" 
					/>
					<div class="course-card-container">
						<course-card
						title="小学五年级英语培训班"
						time="周日 10:00-12:00"
						location="西校园2号楼3楼"
						teacher-name="黄明泉"
						profile-src="https://cn.vuejs.org/images/logo.png"
						:location-foot="true"
						classroom="教室分配中"
						class="course-card">
							<template v-slot:title-icon>
								<span class="span-icon-one qiu mgr-20">秋</span>
							</template>
							<div class="flex-btw-center">
								<span class="time">周一15:50-17:50</span>
								<span class="periods">第3课/共10课</span>
							</div>
						</course-card>
						<course-card
						title="小学五年级数学辅导班"
						time="周日 8:00-10:00"
						location="西校园2号楼3楼"
						teacher-name="黄明明"
						profile-src="https://cn.vuejs.org/images/logo.png"
						:location-foot="true"
						classroom="教室分配中"
						class="course-card">
							<template v-slot:title-icon>
								<span class="span-icon-one qiu mgr-20">秋</span>
							</template>
							<div class="flex-btw-center">
								<span class="time">周一15:50-17:50</span>
								<span class="periods">第3课/共10课</span>
							</div>
						</course-card>
					</div>
				</div>
			</section>
		</section>
		<section class="layout-footer"></section>
	</div>
</template>

<script>
	import { Grid, GridItem, Cell, Popup, DatetimePicker } from ‘vant‘;
	import moment from ‘moment‘;
	moment.locale(‘zh-cn‘);
	// import BScroll from ‘better-scroll‘;
	import bsMixin from ‘@/libs/mixins/better-scroll.js‘;
	import baseMediaCard from ‘@/components/base-media-card.vue‘;
	import courseCard from ‘@/components/course-card.vue‘;
	import COURSE_FILL_IMG from ‘@/assets/images/course.png‘

	const COMPONENT_NAME = ‘my-course‘

	export default {
		name: COMPONENT_NAME,
		components: {
			[Grid.name]: Grid,
			[GridItem.name]: GridItem,
			[Cell.name]: Cell,
			[Popup.name]: Popup,
			[DatetimePicker.name]: DatetimePicker,
			baseMediaCard,
			courseCard
		},
		mixins:[bsMixin],
		props:{},
		data(){
			return {
				activeDate:new Date().getDate(),
				currentDate:moment(),//当天
				selectedDate:moment(),
				showYMPicker:false,//是否显示年月日期选择弹框
				navInfo:[{
					icon:‘#uficon-notebook‘,
					text:‘课程表‘,
					to:‘login‘
				},{
					icon:‘#uficon-teach‘,
					text:‘在线课堂‘,
					to:‘me‘
				},{
					icon:‘#uficon-yishu‘,
					text:‘本地课程‘,
					to:‘message‘
				},{
					icon:‘#uficon-yishu‘,
					text:‘常见问题‘,
					to:‘message‘
				}],
				parentCourse:[{
					cover:COURSE_FILL_IMG,
					title:‘新学期必备锦囊请接收‘,
					to:‘‘
				},{
					cover:COURSE_FILL_IMG,
					title:‘初一数学‘,
					to:‘‘
				},{
					cover:COURSE_FILL_IMG,
					title:‘初一语文‘,
					to:‘‘
				},{
					cover:COURSE_FILL_IMG,
					title:‘初一英语‘,
					to:‘‘
				}]//
			}
		},
		computed: {
			// 当前月份或选中月份的信息
			curMonDaysInfo(){
				let dateVal=this.selectedDate.format(‘YYYY-MM-DD‘);
				return this.monthDayInfo(dateVal);
			},
			// 当月天数
			monthDaysNum(){
				return this.selectedDate.daysInMonth(); // 获取当月天数
			},
			// 显示的年月
			yearMonthTitle(){
				return this.selectedDate.format("YYYY年MM月");
			}
		},
		watch: {},
		created () {},
		mounted () {
			this.calendarScroll=this.initScrollMixin(‘calendarWrapper‘);
			this.cosDisScoll=this.initScrollMixin(‘courseDisWrapper‘);
		},
		beforeDestroy() {
			this.destroyScrollMixin(‘calendarScroll‘);
			this.destroyScrollMixin(‘cosDisScoll‘);
		},
		methods: {
			backToTodayEvt(){
				this.selectedDate=this.currentDate;
			},
			dateClickEvt(cal_item){
				this.selectedDate=moment(cal_item);
				console.log(‘.............真的日期真的日期‘,this.selectedDate)
			},
			ympCancelEvt(){
				// 关闭日期选择
				this.showYMPicker=false;
			},
			// 年月选择确定按钮点击事件
			ympConfirmEvt(value){
				// let that=this;
				// 设置日期为选中年月的1号
				this.selectedDate=moment(value);
				console.log(‘this.selectedDate222222‘,this.selectedDate)
				// 关闭日期选择
				this.showYMPicker=false;
			},
			monthDayInfo(date){
				const currentDate=moment(date);
				// const currentWeekday = moment(date).date(1).weekday(); // 获取当月1日为星期几
				const currentMonthDays = moment(date).daysInMonth(); // 获取当月天数
				let daysInfoArray=[];
				for(let i=1;i<=currentMonthDays;i++){
					let weekdaysInfo=[‘周一‘,‘周二‘,‘周三‘,‘周四‘,‘周五‘,‘周六‘,‘周日‘]
					let day={
						date:currentDate.date(i).format(‘YYYY-MM-DD‘),
						weekday:weekdaysInfo[currentDate.date(i).weekday()],
						no:i,
						classNum:‘2节课‘
					}
					daysInfoArray.push(day);
				}
				return daysInfoArray;
			},
			getWeekDay(dateNo){
				let d=new Date(2019,8,dateNo);
				let day=d.getDay();
				let out_val;
				switch(day){
					case 0 :out_val=`日`;break;
					case 1 :out_val=`一`;break;
					case 2 :out_val=`二`;break;
					case 3 :out_val=`三`;break;
					case 4 :out_val=`四`;break;
					case 5 :out_val=`五`;break;
					case 6 :out_val=`六`;break;
					default:out_val=‘‘;
				}
				return out_val;
			},
			formatter(type, value){
				if (type === ‘year‘) {
					return `${value}年`;
				}else if (type === ‘month‘) {
					return `${value}月`
				}

				return value;
			}
		}
	}
</script>

<style lang="scss" scoped>
	$normal-distance:30px;
	$distance-20:20px;

	.span-icon-one{
		display:inline-flex;
		height:50px;
		min-width:50px;
		border-radius:6px;
		justify-content:center;
		align-items:center;
		font-size:.620em;
		padding:2px;
	}

	.horizon-scroll-wrapper{
		position:relative;
		overflow:hidden;

		.horizon-scroll-content {
			white-space:nowrap;
			display: inline-block;
			padding: 0 35px 35px;
			
			.horizon-scroll-item{
				display:inline-block;
			}
		}
	}

	.layout-header{
		height:240px;
		background-image:linear-gradient( 45deg, $color-light-blue,$color-dark-blue, rgba(25,118,210,0) ), url(../../assets/images/bg-gradient.svg);
		background-size:cover;		
	}

	.layout-show{
		position:relative;
		font-size:40px;

		.nav-tab-part{
			position:absolute;
			top:-150px;
			width:100%;
			padding:0 $normal-distance;
			box-sizing:border-box;
			z-index:5;

			.nav-tab-wrap{
				box-shadow: 4px 3px 7px 2px rgba(0, 0, 0, 0.04);
				border-radius: 20px;
				border:2px solid $color-border-two;
				padding:$normal-distance;
				background-color:$color-basic-white;

				.nav-tab{
					.nav-tab-item{
						.icon-wrap{
							font-size:2em;
							margin-bottom:$distance-20;
							color:$color-dark-blue;
						}

						.item-text{
							font-size:.9em;
						}
					}
				}
			}
		}
	}

	.layout-content{	
		margin-top:200px;	
		.section-header{
			text-align:left;
			padding:30px 35px;

			.section-title{
				font-size:40px;
				font-weight:bold;
				margin:0;
			}
		}

		.course-display-part{
			//&::before{
				//content:"";
				//display:block;
				//height:20px;
				//background-color:$color-border-three;
			//}

			font-size:36px;

			.cd-scroll-wrapper{
				
				.cd-scroll-content{

					.cd-scroll-item{
						width:870px;
						overflow:hidden;

						&:not(:last-child){
							margin-right:30px;
						}

						::v-deep .bmc-card-body{
							padding:0;
						}

						::v-deep .cd-media{
							height:490px;
							padding:30px;
							border:5px solid $color-border-three;
							background-clip:content-box;
						}

						::v-deep .cd-info-wrap{
							padding:30px 15px;
							overflow-x: hidden;
							text-overflow:ellipsis;
							white-space: nowrap;
							font-size:1em;
							font-weight:bold;
							//color:$color-heavy-text;
						}
					}
				}
			}

		}

		.course-calendar-part{
			font-size:40px;
			.section-header{
				.section-title{
					&::after{
						content:‘>‘;
						margin-left:15px;
						display:inline-block;
					}
				}

				.active{
					&::after{
						//transform:rotate(90deg);
						content:"_(|3」∠)_";
					}
				}

				.icon-round{
					display:inline-flex;
					justify-content:center;
					align-items:center;
					font-size:35px;
					width:55px;
					height:55px;
					border-radius:50%;
					border:1px solid $color-dark-blue;
					color:$color-dark-blue;
				}
			}

			.calendar-scroll-wrapper{
				.calendar-scroll-content{
					.calendar-item-wrap{
						width:100px;
						min-height:100px;
						padding:20px;
						border:1px solid $color-dark-blue;
						border-radius:10px;

						&:not(:last-child){
							margin-right:20px;
						}

						.calendar-item{
							display:flex;
							flex-direction:column;
							justify-content:center;
							height:100%;
							//border:1px solid $color-dark-blue;
							//border-radius:10px;
							.weekday,.dayno{
								font-weight:bold;
							}
							.weekday{
								font-size:.75em;
								margin-bottom:10px;
							}

							.dayno{
								font-size:1em;
							}

							.course-tag{
								font-size:.5em;
								color:$color-light-text;
							}
						}
					}

					.selected{background-color:rgba(52,152,219,.3)}
				}
			}

			.cal-course-panel{
				font-size:40px;
				.cal-course-tip{
					text-align:left;
					font-size:30px;
					color:$color-light-text;
				}

				.course-card-container{
					padding:$normal-distance;
					.course-card{
						margin-bottom:40px;

						.qiu{
							border:3px solid $color-light-orange;
							color:$color-light-orange;
							background-color:transparent;
						}

						.time{
							font-size:1.5em;
							font-weight:bolder;
						}

						.periods{
							font-size:.75em;
							color:$color-light-text;
						}
					}
				}
			}
			
		}
	}
</style>

接下来我们看message.js页面
技术分享图片

<template>
	<div>
		<section class="layout-content">
			<van-tabs 
			v-model="activeTab" 
			sticky 
			class="tabs" 
			swipeable
			:color="themePriamryColor"
			:title-active-color="themePriamryColor"
			:title-inactive-color="regularTextColor">
				<van-tab>
					<template v-slot:title>
						<div class="tab-title">
							系统通知
						</div>
					</template>
					<template v-if="login">
						<template v-if="sysList.length>0">
							<base-msg-card
							v-for="(item,index) in sysList"
							:key="index"
							:title="item.title"
							class="msg-card">
								{{item.content}}
							</base-msg-card>
						</template>
						<template v-else>
							<base-msg-card
							
							v-for="n in 5"
							:key="n"
							title="系统公告"
							class="msg-card">
								哈哈教育,为6-18岁孩子提供小初高全学科课程辅导。课程历经十余年教学沉淀,陪伴千万孩子成长,是您值得信任的好伙伴。
							</base-msg-card>
						</template>
						
					</template>
					<template v-else>
						<div class="default-wrap">
							<div class="defalut-header">
								<van-image
								width="10em"
								height="10em"
								fit="cover"
								:src="defaultNoMsg"
								/>
							</div>
							<div class="default-content">
								暂无消息,登录查看
							</div>
							<div class="default-footer">
								<van-button 
								round 
								class="login-btn"
								:color="themePriamryColor"
								to="/login">立即登录</van-button>
							</div>
						</div>
					</template>
				</van-tab>
				<van-tab>
					<template v-slot:title>
						<div class="tab-title">
							课程提醒
						</div>
					</template>
					<template v-if="login">
						<base-msg-card
						v-for="n in 3"
						:key="n"
						title="课程提醒"
						class="msg-card">
							您订购的数学辅导短训班第一节课将于明天(2019年9月11号)早上9点在西校区紫荆花1号教室开课,请准备好需要的书本资料等按时参加哦。
						</base-msg-card>
					</template>
					<template v-else>
						<div class="default-wrap">
							<div class="defalut-header">
								<van-image
								width="10em"
								height="10em"
								fit="cover"
								:src="defaultNoMsg"
								/>
							</div>
							<div class="default-content">
								暂无消息,登录查看
							</div>
							<div class="default-footer">
								<van-button 
								round 
								class="login-btn"
								:color="themePriamryColor"
								to="/login">立即登录</van-button>
							</div>
						</div>
					</template>
				</van-tab>
				<van-tab>
					<template v-slot:title>
						<div class="tab-title">
							在线客服
						</div>
					</template>

				</van-tab>
			</van-tabs>
		</section>
	</div>
</template>

<script>
	import { Tab, Tabs, Image, Button, PullRefresh } from ‘vant‘;
	import baseMsgCard from ‘@/components/base-message-card.vue‘;
	import { mapState } from ‘vuex‘
	import { THEME_PRIMARY, THEME_LIGHT_BLUE, REGULAR_TEXT_COLOR, DEFAULT_NO_MSG } from ‘@/libs/constant.js‘;
	import { systemMsgList as fetchSystemMsgList } from ‘@/api/base‘

	const COMPONENT_NAME = ‘message-index‘

	export default {
		name: COMPONENT_NAME,
		components: {
			[Tab.name]: Tab,
			[Tabs.name]: Tabs,
			[Image.name]: Image,
			[Button.name]: Button,
			[PullRefresh.name]: PullRefresh,
			baseMsgCard
		},
		mixins:[],
		props:{},
		data(){
			return {
				activeTab:0,
				themePriamryColor:THEME_PRIMARY,//主题色
				themeLightColor:THEME_LIGHT_BLUE,//浅主题色
				regularTextColor:REGULAR_TEXT_COLOR,//常规字体颜色
				defaultNoMsg:DEFAULT_NO_MSG,//消息缺省填充图片
				sysList:[]
			}
		},
		computed: {
			...mapState([
				‘userInfo‘,
				‘login‘
			]),
		},
		watch: {
			userInfo: function (value) {
				if (value && value.user_id && !this.sysList) {
					this.intialSystemMsg();
				}
			}
		},
		created () {},
		mounted () {
			this.intialSystemMsg();
		},
		beforeDestroy() {},
		methods: {
			// 获取初始系统通知信息
			async intialSystemMsg(){
				if(!this.login||!this.userInfo||!this.userInfo.user_id){
					return;
				}

				await fetchSystemMsgList(this.userInfo.user_id,1,this.userInfo.user_id.charAt(this.userInfo.user_id.length-1)).then((res)=>{
					this.sysList=res.data.rows;
				})
			}
		}
	}
</script>

<style lang="scss" scoped>
	$normal-distance-30:30px;
	.layout-content{
		font-size:40px;

		.tabs{
			padding:$normal-distance-30;

			::v-deep .van-tabs__wrap{
				$tab-bar-height:120px;
				height:$tab-bar-height;

				.van-tab{
					line-height:$tab-bar-height;
				}
			}

			.tab-title{
				font-size:40px;
			}

			.default-wrap{
				margin-top:200px;
				.defalut-header{
					@include flex-cet-cet;
				}
				.default-content{
					color:$color-light-text;
				}
				.default-footer{
					padding:$normal-distance-30;
					.login-btn{
						padding:0 40px;
						font-size:$font-size-base;
						line-height:2.8em;
						height:2.8em;
					}
				}
			}

			.msg-card{
				box-shadow: 4px 3px 7px 2px rgba(0, 0, 0, 0.04);
				border-radius: 20px;
				border:2px solid $color-border-two;
				margin-bottom:$normal-distance-30;

				&:first-child{
					margin-top:$normal-distance-30;
				}
			}
		}
	}
</style>

接下来是me页面
技术分享图片

<template>
	<div>
		<section class="layout-header">
			<section class="top-bar">
				{{login?userInfo.name:"未登录用户"}}
				<div class="tool-box">
					<van-icon name="user-o" size="1em" class="top-icon" />
					<van-icon name="setting-o" size="1em" class="top-icon" />
				</div>
			</section>
			<section class="profile-card">
				<div class="profile-img-wrap">
					<van-image 
					class="profile-img"
					round
					fit="cover"
					:src="login?userInfo.profileImg:profileStaticPic">

					</van-image>
				</div>
				<div class="profile-info">
					<span class="coin-num">{{login?userInfo.balanceCoin:"0"}}</span>
				</div>
			</section>
			<section class="nav-part">
				<van-grid
				class="nav-box"
				:border="false">
					<van-grid-item 
					v-for="(item,index) in navInfo"
					:key="index">
						<div class="flex-start-center nav-item">
							<div :class="[‘icon-wrap‘,‘icon-wrap-‘+(index+1)]">
								<i v-show="item.showDot" class="dot"></i>
								<svg class="uficon" aria-hidden="true">
									<use :xlink:href="item.icon"></use>
								</svg>
							</div>
							<span class="item-text">{{item.text}}</span>
						</div>
					</van-grid-item>
				</van-grid>
			</section>
			<section class="conduct-part">
				<van-cell 
				v-for="(item,index) in cellMenu"
				:key="index"
				center
				is-link
				:title="item.title"
				class="conduct-cell">
					<template v-slot:icon>
						<span class="left-icon-wrap">
							<svg class="uficon" aria-hidden="true">
								<use :xlink:href="item.leftIcon"></use>
							</svg>
						</span>
					</template>
				</van-cell>
			</section>
		</section>
		<section class="layout-content"></section>
		<section class="layout-footer"></section>
	</div>
</template>

<script>
	import { Row, Col, Icon, Search, Swipe, SwipeItem, Grid, GridItem, Image, Tab, Tabs, Button, Cell } from ‘vant‘;
	import USER_PIC from ‘@/assets/images/user_profile.png‘;
	import { mapState, mapMutations, mapGetters } from ‘vuex‘

	const COMPONENT_NAME = ‘user-index‘

	export default {
		name: COMPONENT_NAME,
		components: {
			[Row.name]: Row,
			[Col.name]: Col,
			[Icon.name]: Icon,
			[Search.name]: Search,
			[Swipe.name]: Swipe,
			[SwipeItem.name]: SwipeItem,
			[Grid.name]: Grid,
			[GridItem.name]: GridItem,
			[Image.name]: Image,
			[Tab.name]: Tab,
			[Tabs.name]: Tabs,
			[Button.name]: Button,
			[Cell.name]: Cell
		},
		mixins:[],
		props:{},
		data(){
			return {
				profileStaticPic:USER_PIC,
				// navInfo:[{
				// 	icon:‘#uficon-notebook‘,
				// 	text:‘购课袋‘,
				// 	showDot:false
				// },{
				// 	icon:‘#uficon-ziyuan‘,
				// 	text:‘待付款‘,
				// 	showDot:true
				// },{
				// 	icon:‘#uficon-ziyuan1‘,
				// 	text:‘已付款‘,
				// 	showDot:false
				// },{
				// 	icon:‘#uficon-ziyuan2‘,
				// 	text:‘已退款‘,
				// 	showDot:false
				// }],
				cellMenu:[{
					leftIcon:‘#uficon-yue‘,
					title:‘我的余额‘,
					to:‘‘
				},{
					leftIcon:‘#uficon-youhuiquan‘,
					title:‘优惠券‘,
					to:‘‘
				},{
					leftIcon:‘#uficon-knowledge‘,
					title:‘我的课程‘,
					to:‘‘
				},{
					leftIcon:‘#uficon-admin‘,
					title:‘客服中心‘,
					to:‘‘
				}]
			}
		},
		computed:{
			...mapState([
				‘userInfo‘,
				‘login‘
			]),
			...mapGetters([
				‘toPayList‘,
				‘paidList‘,
				‘refundList‘
			]),
			navInfo(){
				return[{
					icon:‘#uficon-notebook‘,
					text:‘购课袋‘,
					showDot:false
				},{
					icon:‘#uficon-ziyuan‘,
					text:‘待付款‘,
					showDot:this.toPayList.length>0
				},{
					icon:‘#uficon-ziyuan1‘,
					text:‘已付款‘,
					showDot:false
				},{
					icon:‘#uficon-ziyuan2‘,
					text:‘已退款‘,
					showDot:false
				}]
			}
		},
		methods:{
			...mapMutations([
				
			])
		}
	}
</script>

<style lang="scss" scoped>
	.layout-header{
		font-size:42px;

		.top-bar{
			$vertical-dis-1:35px;

			position:relative;
			padding:$vertical-dis-1 30px;
			background-color:$color-dark-blue;
			color:$color-basic-white;

			.tool-box{
				display:inline-flex;
				justify-content:space-between;
				align-items:center;
				width:150px;
				height:100%;
				position:absolute;
				top:0;
				right:40px;
			}
		}

		.profile-card{
			display:flex;
			align-items:center;
			padding:0 30px;

			.profile-img-wrap{
				margin-right:30px;
				position:relative;
				top:-80px;
				left:0;

				.profile-img{
					width:260px;
					height:260px;
					border:20px solid $color-basic-white;
					background-color:$color-basic-white;
				}
			}

			.profile-info{
				flex:1;
				display:flex;

				.coin-num{
					display:inline-block;
					font-size:1.5em;
					font-weight:bold;
					margin-right:30px;
					color:$color-light-orange;

					&::after{
						content:"金币";
						color:$color-light-text;
						font-size:.6em;
						font-weight:normal;
						margin-left:20px;
					}
				}
			}
		}

		.nav-part{
			font-size:40px;

			&::before{
				content:"";
				display:block;
				height:20px;
				background-color:$color-border-three;
			}

			.nav-box{
				//padding:30px 0;

				.nav-item{
					padding:30px 0;
					position:relative;

					.icon-wrap{
						font-size:1.5em;
						margin-right:20px;
						position:relative;

						.dot{
							position:absolute;
							top:0;
							right:0;
							display:inline-block;
							width:20px;
							height:20px;
							border-radius:50%;
							background-color:red;
						}
					}

					.item-text{
						font-size:1em;
						color:$color-regular-text;
					}
				}

				::v-deep .van-grid-item:not(:last-child) .nav-item{
					&::after{
						display:inline-block;
						content:"";
						width:3px;
						height:100%;
						position: relative;
						left:30px;
						background-color:$color-border-three;
					}					
				}
			}
		}

		.conduct-part{
			font-size:40px;
			text-align:left;

			&::before{
				content:"";
				display:block;
				height:20px;
				background-color:$color-border-three;
			}

			.conduct-cell{
				font-size:1.1em;
				padding:40px 50px;
				color:$color-regular-text;

				.left-icon-wrap {
					margin-right:20px;
					font-size:1.2em;
				}
			}
		}
	}
</style>

接下来看Login页面
技术分享图片

<template>
	<div>
		<div class="layout-header">
			
		</div>
		<div class="layout-content">
			<van-row type="flex" justify="center">
				<van-col span="5" />
				<van-col span="14">
					<img 
					:src="logoImg" 
					style="width:100%;height:auto!important;" 
					/>
				</van-col>
				<van-col span="5" />
			</van-row>
			<form class="login-form">
				<van-cell-group 
				class="cell-group" 
				:border="false">
					<van-field 
					v-model="formItem.account" 
					placeholder="请输入手机号"
					type="tel"
					class="cell-field"
					clearable
					:error-message="validatorObj.errMsg.account"
					:disabled="telFieldDisabled"
					@input="telInputEvt">
						<template v-slot:left-icon>
							<svg class="uficon" aria-hidden="true">
								<use xlink:href="#uficon-yonghu"></use>
							</svg>
						</template>
					</van-field>
					<van-field 
					v-show="!loginByPws"
					v-model="formItem.captcha" 
					center
					:error-message="validatorObj.errMsg.captcha"
					placeholder="请输入验证码"
					class="cell-field">
						<template v-slot:left-icon>
							<svg class="uficon" aria-hidden="true">
								<use xlink:href="#uficon-pws"></use>
							</svg>
						</template>
						<van-button 
						v-show="!computedTime"
						slot="button" 
						size="small" 
						round
						:color="themePriamryColor"
						:disabled="codeBtnIsDisabled"
						class="code-btn"
						@click="getCaptchaMsg">
						获取验证码
						</van-button>
						<van-button 
						v-show="computedTime"
						slot="button" 
						size="small" 
						round
						:color="themePriamryColor"
						disabled
						class="code-btn">
						剩余{{computedTime}}s
						</van-button>
					</van-field>
					<van-field 
					v-model="formItem.password" 
					v-show="loginByPws"
					center
					clearable
					:error-message="validatorObj.errMsg.password"
					placeholder="请输入登录密码"
					class="cell-field"
					:right-icon="pwsRightIcon"
					:type="pwsShowType"
					@click-right-icon="togglePws">
						<template v-slot:left-icon>
							<svg class="uficon" aria-hidden="true">
								<use xlink:href="#uficon-pws"></use>
							</svg>
						</template>
					</van-field>
				</van-cell-group>				
			</form>
			<section>
				<van-button 
				round
				block
				:style="loginBtnStyle"
				:disabled="loginBtnDisabled"
				@click="loginBtnClickEvt">{{loginByPws?‘账号登录‘:‘快捷登录‘}}</van-button>
			</section>
			<section>
				<van-row>
					<van-col span="8">
						<van-button 
						v-show="loginByPws"
						type="default" 
						class="switch-way"
						tag="a"
						to="/index">忘记密码</van-button>
					</van-col>
					<van-col offset="8" span="8">
						<van-button 
						type="default" 
						class="switch-way"
						tag="a"
						:disabled="switchWayBtnDisabled"
						@click="changeLoginWay">{{loginByPws?‘验证码登录‘:‘密码登录‘}}</van-button>
					</van-col>
				</van-row>
			</section>
		</div>
		<div class="layout-footer"></div>
		<van-loading v-show="showLoading" type="spinner" :color="themePriamryColor" />
	</div>
</template>

<script>
	import { mapMutations } from ‘vuex‘
	import logoImg from ‘@/assets/images/logo_login.png‘
	import { Image, Row, Col, Field, CellGroup, Button, Toast, Loading } from ‘vant‘;
	// import { PHONE_NUMBER_REG, LOGIN_VERIFY_CODE_REG, EMPTY_REG } from ‘@/libs/regexp‘
	import { PHONE_NUMBER_REG, LOGIN_VERIFY_CODE_REG } from ‘@/libs/regexp‘
	import { accountLogin, captchaLogin } from ‘@/api/base‘
	import commonMixin from ‘@/libs/mixins/common.js‘;
	import { THEME_PRIMARY, THEME_LIGHT_BLUE } from ‘@/libs/constant.js‘;

	const COMPONENT_NAME = ‘login-captcha‘

	export default {
		name: COMPONENT_NAME,
		components: {
			[Image.name]: Image,
			[Row.name]: Row,
			[Col.name]: Col,
			[Field.name]: Field,
			[CellGroup.name]: CellGroup,
			[Button.name]: Button,
			[Toast.name]: Toast,
			[Loading.name]: Loading
		},
		mixins:[commonMixin],
		props:{},
		data(){
			return{
				logoImg,//logo图片
				themePriamryColor:THEME_PRIMARY,//主题色
				themeLightColor:THEME_LIGHT_BLUE,//浅主题色
				loginByPws:false,//是否为密码登录
				pwsRightIcon:‘closed-eye‘,//密码登录右图标
				pwsShowType:‘password‘,//是否显示明文密码
				computedTime:0,//校验码倒计时
				codeBtnIsDisabled:true,//校验码按钮是否为禁用状态
				telFieldDisabled:false,//手机号码输入框是否为禁用状态
				switchWayBtnDisabled:false,//登录方式切换按钮是否为禁用状态
				userInfo: null, //获取到的用户信息
				showLoading:false,//显示正在加载中
				loginBtnDisabled:false,//禁用登录按钮
				formItem:{
					account:‘‘,
					captcha:‘‘,
					password:‘‘
				},//表单项
				validatorObj:{
					errMsg:{
						account:‘‘,
						captcha:‘‘,
						password:‘‘
					}
				}//表单校验
			}
		},
		computed: {
			// 登录按钮的样式
			loginBtnStyle(){
				return {
					background: `linear-gradient( to right,${this.themeLightColor}, ${this.themePriamryColor})`,
					color:‘#ffffff‘
				}
			},
			//判断手机号码
			isValidTel(){
				return PHONE_NUMBER_REG.test(this.formItem.account);
			}
		},
		watch: {
			loginByPws(n,o){
				if(n==!o){
					for(let k in this.formItem){
						this.formItem[k]=‘‘
					}
					this.codeBtnIsDisabled=true;
				}
			}
		},
		created () {},
		mounted () {},
		methods: {
			...mapMutations([
				‘RECORD_USERINFO‘
			]),
			async loginBtnClickEvt(){
				for(let k in this.validatorObj.errMsg){
					this.validatorObj.errMsg[k]=""
				}
				this.showLoading=true;
				this.loginBtnDisabled=true;
				// 根据loginByPws判断登录方式
				if(this.loginByPws){
					// 判断电话号码是否有效
					if(!this.isValidTel){
						this.validatorObj.errMsg.account=!this.formItem.account?"请输入手机号":"手机号码格式不正确";
						return;
					}else if(!this.formItem.password){
						this.validatorObj.errMsg.password="请输入登录密码";
						return;
					}else{
						// 登录
						await accountLogin(this.formItem.account, this.formItem.password).then((res)=>{
							this.userInfo=res.data.data;
						}).catch(err=>{
							this.userInfo={
								user_id:this.formItem.account,
								name:‘小明‘,
								profileImg:"http://img4.imgtn.bdimg.com/it/u=2476436593,434837001&fm=26&gp=0.jpg",
								balanceCoin:36,
								shoppingList:[{
									name:"接口错误辅导班",
									status:1
								}],
								errInfo:err
							}
						});
					}
				}else{
					// 判断电话号码是否有效
					if(!this.isValidTel){
						this.validatorObj.errMsg.account=!this.formItem.account?"请输入手机号":"手机号码格式不正确";
						return;
					}else if(!LOGIN_VERIFY_CODE_REG.test(this.formItem.captcha)){
						this.validatorObj.errMsg.captcha="请输入正确的6位数校验码";
						return;
					}else{
						// 登录
						await captchaLogin(this.formItem.account, this.formItem.captcha).then((res)=>{
							this.userInfo=res.data.data;
						}).catch(err=>{
							this.userInfo={
								user_id:this.formItem.account,
								name:‘明明‘,
								profileImg:"http://img4.imgtn.bdimg.com/it/u=2476436593,434837001&fm=26&gp=0.jpg",
								balanceCoin:36,
								shoppingList:[{
									name:"接口错误辅导班",
									status:1
								}],
								errInfo:err
							}
						});
					}
				}
				this.showLoading=false;
				if(this.userInfo.user_id){
					this.RECORD_USERINFO(this.userInfo);
					this.routerJumpEvtMixin({name:‘me‘})
				}else{
					Toast.fail(‘登录失败‘);
				}
				this.loginBtnDisabled=false;
			},
			// 改变登录方式
			changeLoginWay(){
				this.loginByPws=!this.loginByPws;
			},
			// 切换密码的显示状态
			togglePws(){
				let that=this;
				if(that.pwsShowType===‘password‘){
					this.pwsRightIcon=‘eye-o‘;
					this.pwsShowType=‘text‘
				}else{
					this.pwsRightIcon=‘closed-eye‘;
					this.pwsShowType=‘password‘
				}
			},
			// 手机号码输入事件
			telInputEvt(val){
				if(/^[1][0-9]{10}$/.test(val)){
					this.codeBtnIsDisabled=false;
				}else{
					this.codeBtnIsDisabled=true;
				}
			},
			// 获取短信验证码
			async getCaptchaMsg(){
				// 手机号是否有效
				if(this.isValidTel){
					this.computedTime = 45;
					this.captchaTimer=setInterval(()=>{
						this.computedTime--;
						if(this.computedTime===0){
							clearInterval(this.captchaTimer)
							// 恢复获取校验码按钮、手机号码输入框、登录方式切换按钮
							this.codeBtnIsDisabled=false;
							this.telFieldDisabled=false;
							this.switchWayBtnDisabled=false;
						}
					},1000);

					// 禁用获取校验码按钮、手机号码输入框、登录方式切换按钮
					this.codeBtnIsDisabled=true;
					this.telFieldDisabled=true;
					this.switchWayBtnDisabled=true;
				}else{
					Toast(‘请输入正确的手机号码‘);
				}
			}
		}
	}
</script>

<style lang="scss" scoped>
	.layout-content{
		margin-top:300px;
		padding:0 90px;

		.login-form{

			.cell-group{
				margin:110px 0;

				.code-btn{
					padding:0 40px;
				}

				.cell-field{
					border-bottom:4px solid $color-border-three;
					padding-top:20px;
					padding-bottom:20px;
					font-size:40px;

					.uficon{
						width:1.5em;
						height:1.5em;
					}

					&:not(:first-child){
						margin-top:30px;
					}
				}
			}

		}

		.switch-way{
			color:$color-light-text;
			border:none;

			&:active::before{opacity:0}
		}
	}
</style>

【美好生活】 haha-edu

原文:https://www.cnblogs.com/smart-girl/p/12875754.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!