老规矩,先放上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>
原文:https://www.cnblogs.com/smart-girl/p/12875754.html