1. 登录功能
本节课完成登录页面、登录接口以及退出登录。
1.1. 新建路由/login。
在router文件中添加路径为/login的路由,引入视图,注意是与路径“/”为兄弟关系。
//src/router/index.js
{
path:'/login',
name:'login',
component:()=>import('../view/login/login.vue')
}
在view下新建login文件夹,新建login.vue,模板页面。
//src/views/login/login.vue
<template>
<div>登录页面</div>
</template>
<script>
export default{
name:'login',
data(){
return{}
}
}
</script>
1.2. login页面
(1)页面有账号和密码两个表单域,引入表单el-form,添加属性,添加model属性“form”,states-icon表示表单校验图标,rules表单校验,新建校验名为“rules”,新建ref属性为“form”,以此能拿return里的form内容。label-width为名称的宽度。在return中定义rules{}。rules为一个对象,里面有两个数组对象,一个是username,一个是password。每个对象包含三个属性。第一个对象是required,必填,属性为true,校验不通过信息message,触发方式trigger为blur(布尔类型)。对二哥对象为长度限制,min为最小长度。
return{
form:{},
rules:{
username:[
{
required:true,message:"请输入用户名",trigger:"blur"
},
{
min:3,
message:"用户名长度不得小于3位",
trigger:"blur"
},
],
password:[
{
required:true,message:"请输入密码",trigger:"blur"
}
]
}
}
(2)开始填写卡片里的内容,新建h3标签,表单域用el-form-item标签,label为表单域名称,label-width为名称的宽度,prop属性绑定rules(?有疑问,为什么prop估计可以接受return里到的数据),el-input绑定v-model很重要,绑定form下的form.username,password同理。现在form为空,所以不好理解。加上登录按钮。
<template>
<el-form
:model="form"
status-icon
:rules="rules"
ref="form"
label-width="100px"
class="login-container">
<h3 class="login-title">系统登录</h3>
<el-form-item
label="用户名"
label-width="80px"
prop="username"
class="username">
<el-input
type="input"
v-model="form.username"
autocomplete="off"
placeholder="请输入账号">
</el-input>
</el-form-item>
<el-form-item
label="密码"
label-width="80px"
prop="password"
>
<el-input
type="password"
v-model="form.password"
autocomplete="off"
placeholder="请输入密码">
</el-input>
</el-form-item>
<el-form-item class="login_submit">
<el-button type="primary" @click="login" class="login_submit">登录</el-button>
</el-form-item>
</el-form>
</template>
(3)更改样式
<style lang="less" scoped>
.login-container{
border-width: 15px;
background-clip: padding-box;
margin: 180px auto;
width:350px;
padding: 35px 35px 15px 35px;
background-color: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.login-title{
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
.login_submit{
margin: 10px auto 0px auto;
}
</style>
(4)编写login()方法,token直接用Mock.random()方法生成随机数,调用setToken并传入token。这里登录先写死。
methods:{
login(){
const token = Mock.random.guid()
this.$store.commit('setToken',token)
this.$router.push({ name:'home' })
}
}
(5)src/views/login/logon.vue完整代码
//src/views/login/logon.vue
<template>
<el-form
:model="form"
status-icon
:rules="rules"
ref="form"
label-width="100px"
class="login-container">
<h3 class="login-title">系统登录</h3>
<el-form-item
label="用户名"
label-width="80px"
prop="username"
class="username">
<el-input
type="input"
v-model="form.username"
autocomplete="off"
placeholder="请输入账号">
</el-input>
</el-form-item>
<el-form-item
label="密码"
label-width="80px"
prop="password">
<el-input
type="password"
v-model="form.password"
autocomplete="off"
placeholder="请输入密码">
</el-input>
</el-form-item>
<el-form-item class="login_submit">
<el-button type="primary" @click="login" class="login_submit">登录</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: 'login',
data() {
return {
form: {},
rules: {
username: [
{
required: true,
message: "请输入用户名",
trigger: "blur"
},
{
min: 3,
message: "用户名长度不得小于3位",
trigger: "blur"
},
],
password: [
{
required: true,
message: "请输入密码",
trigger: "blur"
}
]
}
}
}
}
</script>
<style lang="less" scoped>
.login-container {
border-width: 15px;
background-clip: padding-box;
margin: 180px auto;
width: 350px;
padding: 35px 35px 15px 35px;
background-color: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.login-title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
.login_submit {
margin: 10px auto 0px auto;
}
</style>
1.3. 登录权限凭证token
登录需要用到token,用户输入账户密码,前端调用接口将数据传送给后端,后端根据数据库校验,校验成功后给出一个登录凭证,也就是“token”。
(1)安装依赖第三方库js-cookie,cnpm i js-cookie@3.0.5,
(2)在store中新建user.js,引入cookie,向外暴露,定义一个state,定义token为空,mutations编几个方法用来改变token,setToken()接收两个参数state、val,val为val赋值给token。调用cookie的set方法,接收两个参数,第一个是cookie的名称,第二个是传入的值val,清除cookie方法clearToken()方法,方法同理,将token置空,cookie的remove方法将token清空(?),获取token方法getToken()方法,获取token调用cookie的get方法,如果当前缓存中有token,我们直接cookie.get(token名称)获取,如果没有,要从state中获取,双竖线表示容错。
import Cookie from 'js-cookie'
export default{
state:{
token:'',
},
mutations:{
setToken(state,val){
state.token=val
Cookie.set('token',val)
},
clearToken(){
state.token = ''
Cookie.remove('token')
},
getToken(state){
// state.token = state.token || Cookie.get('token')
state.token = Cookie.get('token') || state.token
}
}
}
(3)注意需要在store中引入user.js,并且向外暴露。
import tab from './tab'
import user from './user'
Vue.use(Vuex)
export default new Vuex.Store({
modules:{
tab,
user
}
})
引入user.js
1.4. 路由导航守卫
更改main.js,路由进行跳转的时候,我们可以通过导航守卫监听,router.beforeEach()方法可以监听,接收一个箭头函数,接收三个参to,from,next。commit调用getToken(commit用法?),这样做是为了防止页面刷新后丢失了token信息,这里需要一个判断,如果token不存在并且当前页不是/login,那么我们直接返回/login页,to.name表示当前页。如果是其他情况,那么正常跳转即可。
//src/mian.js
router.beforeEach((to,from,next)=>{
store.commit('getToken')
const token = store.state.user.token
if(!token && to.name !=='login'){
next({ name:'login' })
}else{
next()
}
})
1.5. login登录事件
我们给系统添加一个登最凭还叫"token”,这个tokn在登录的时候通过接口请求将用户名和密码传给后端,后端再数报库中匹登成功后返回一个凭证,前端将token缓存起来,再调用接口时传给后端验证就建立了登录权限校验。
(1)在src/api/mockServeData文件夹下新建permission.js。
// src/api/mockServeData/permission.js
import Mock from 'mockjs'
export default {
getMenu: config => {
const { username, password } = JSON.parse(config.body)
// 先判断用户是否存在
// 判断账号和密码是否对应
//menuList用于后面做权限分配,也就是用户可以展示的菜单
if (username === 'admin' && password === 'admin') {
return {
code: 200,
data: {
menuList: [
{
path: '/home',
name: 'home',
label: '首页',
icon: 'house',
url: 'Home'
},
{
path: '/mall',
name: 'mall',
label: '商品管理',
icon: 'video-play',
url: 'Mall'
},
{
path: '/user',
name: 'user',
label: '用户管理',
icon: 'user',
url: 'User'
},
{
path: 'other',
label: '其他',
icon: 'location',
children: [
{
path: '/page1',
name: 'page1',
label: '页面1',
icon: 'setting',
url: 'Page1'
},
{
path: '/page2',
name: 'page2',
label: '页面2',
icon: 'setting',
url: 'Page2'
}
]
}
],
token: Mock.Random.guid(),
message: '获取成功'
}
}
} else if (username === 'xiaoxiao' && password === 'xiaoxiao') {
return {
code: 200,
data: {
menuList: [
{
path: '/home',
name: 'home',
label: '首页',
icon: 'house',
url: 'Home'
},
{
path: '/user',
name: 'user',
label: '用户管理',
icon: 'user',
url: 'User'
}
],
token: Mock.Random.guid(),
message: '获取成功'
}
}
} else {
return {
code: -999,
data: {
message: '密码错误'
}
}
}
}
}
(2)引用permission.js。
//mock.js
import Mock from 'mockjs'
import homeApi from './mockServeData/home.js'
import userApi from './mockServeData/user.js'
import permissionApi from './mockServeData/permission'
Mock.mock('/home/getData',homeApi.getStatisticalData)
Mock.mock(/user\/add/,'post',userApi.createUser)
Mock.mock(/user\/edit/,'post',userApi.updateUser)
Mock.mock(/user\/getUser/,'get',userApi.getUserList)
Mock.mock(/user\/del/,'post',userApi.deleteUser)
Mock.mock(/permission\/getMenu/,'post',permissionApi.getMenu)
(3)接口对外暴露。
// src/api/data.js
// 这里引入的是文件,不是依赖
import axios from './axios'
/**
* 接口对外暴露
* @param param
* @returns {*}
*/
export const getMenu = (param) => {
return axios.request({
url:'/permission/getMenu',
method:'post',
data:param
})
}
/**
* 接口对外暴露
*/
export const getData = () =>{
return axios.request({
url:'/home/getData'
})
}
/**
* 接口对外暴露
*/
export const getUser = (params) => {
return axios.request({
url:'/user/getUser',
method:'get',
params
})
}
(4)在src/views/login/logon.vue调用
import {getMenu} from '@/api/data'
编写login()方法,调用getMenu并传入token。
methods:{
login(){
getMenu(this.form).then(res=>{
if (res.code===2000){
//成功
}else {
//异常通过element ui方法
this.$message.warning(res.data.message)
}
})
// const token = Mock.random.guid()
// this.$store.commit('setToken',token)
// this.$router.push({ name:'home' })
}
}
(5)数据通讯
import Cookie from 'js-cookie'
export default {
state: {
isCollapse: false,
//默认菜单面包屑
tabsList: [{
id: 0,
path: "/home",
name: "home",
label: "首页",
icon: "s-home",
url: "Home/Home",
}],
//当前菜单面包屑
currentMenu: null,
menu: []
},
//更改 Vuex 的store中的状态的唯一方法是提交mutations
mutations: {
collapseMenu(state) {
state.isCollapse = !state.isCollapse
},
selectMenu(state, val) {
if (val.name !== 'home') {
state.currentMenu = val
const result = state.tabsList
.findIndex(item => item.name === val.name)
if (result === -1) {//不存在
state.tabsList.push(val)
}
} else {
state.currentMenu = null
}
},
closeTag(state, val) {
const result = state.tabsList.findIndex(item => item.name === val.name)
state.tabsList.splice(result, 1)
},
setMenu(state, val) {
state.menu = val
},
clearMenu(state) {
state.menu = []
Cookie.remove('menu')
},
addMenu(state, router) {
if (!Cookie.get('menu')) {
return
}
const menu = JSON.parse(Cookie.get('menu'))
state.menu = menu
const menuArray = []
menuArray.forEach(item => {
if (item.children) {
item.children = item.children.map(item2 => {
item2.component=()=>import(`../views/${item2.url}`)
return item2
})
menuArray.push(...item.children)
}else {
item.component=()=>import(`../views/${item.url}`)
menuArray.push(item)
}
})
//路由动态添加
menuArray.forEach(item=>{
router.addRoutes('Main',item)
})
}
}
}
(6)整理数据
methods:{
login(){
getMenu(this.form).then(res=>{
if (res.code===2000){
//成功
//清除路由
this.$store.commit('clearMenu')
//设置路由
this.$store.commit('setMenu',res.data.menu)
//设置Token
this.$store.commit('setToken',res.data.token)
//动态添加路由
this.$store.commit('addMenu',this.$store)
//页面跳转
this.$router.push({ name:'home' })
}else {
//异常通过element ui方法
this.$message.warning(res.data.message)
}
})
// const token = Mock.random.guid()
// this.$store.commit('setToken',token)
// this.$router.push({ name:'home' })
}
}
(7)CommonAside
<template>
<el-menu background-color="#545c64" text-color="#fff"
active-text-color="#ffd04b" default-active="/home"
class="el-menu-vertical-demo"
@open="handleOpen" @close="handleClose"
:collapse="isCollapse">
<!-- <h3>通用后台管理系统</h3> -->
<h3>{{isCollapse ? '后台' : '通用后台管理系统'}}</h3>
<!-- 一级菜单 -->
<el-menu-item v-for="item in noChildren"
@click="clickMenu(item)" default-active=""
:index="item.path+''" :key="item.path">
<i :class="'el-icon-'+item.icon"></i>
<span slot="title">{{item.label}}</span>
</el-menu-item>
<!-- 二级菜单 -->
<el-submenu v-for="item in hasChildren"
:index="item.path+''" :key="item.path">
<template slot="title">
<i :class="'el-icon'+item.icon"></i>
<span slot="title">{{item.label}}</span>
</template>
<!-- 这里subItem和subIndex并没有实际意义,只是用来指代item.children的多个数组,
两者甚至可以互换,只新定义一个也可以的,:index后面有没有.path都可以运行成功 -->
<el-menu-item-group v-for="(subItem,subIndex) in item.children"
:key="subItem.path+''">
<el-menu-item @click="clickMenu(subItem)" :index="subIndex.toString()">
{{subItem.label}}
</el-menu-item>
</el-menu-item-group>
</el-submenu>
<!-- <el-menu-item index="3" disabled>-->
<!-- <i class="el-icon-document"></i>-->
<!-- <span slot="title">导航三</span>-->
<!-- </el-menu-item>-->
</el-menu>
</template>
<script>
export default {
data() {
return {
// isCollapse: false,
menu: [
// {
// id: 0,
// path: "/home",
// name: "home",
// label: "首页",
// icon: "s-home",
// url: "Home/Home",
// },
// {
// id: 1,
// path: "/mail",
// name: "mail",
// label: "商品管理",
// icon: "video-play",
// url: "MailManage/MailManage",
// },
// {
// id: 2,
// path: "/user",
// name: "user",
// label: "用户管理",
// icon: "user",
// url: "UserManage/UserManage",
// },
// {
// label: "其他",
// icon: "location",
// children: [
// {
// id: 3,
// path: "/page1",
// name: "pageOne",
// label: "页面1",
// icon: "setting",
// url: "Other/PageOne",
// },
// {
// id: 4,
// path: "/page2",
// name: "pageTwo",
// label: "页面2",
// icon: "setting",
// url: "Other/PageTwo",
// }
// ],
// },
]
};
},
created() {
//初始化数据
this.initData();
},
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
/**
* 一级、二级菜单点击事件
* @param item
*/
clickMenu(item) {
//全局引用router跳转
this.$router.push({
// name: item.name //名字路由指引
path:item.path//路径路由指引
}).catch(e => {
})
this.$store.commit('selectMenu',item)
},
/**
* 初始化数据
*/
initData() {
//全局引用router跳转
this.$router.push({
name: "home" //名字路由指引
// path:item.path//路径路由指引
}).catch(e => {
})
}
},
//计算属性
computed: {
isCollapse() {
return this.$store.state.tab.isCollapse
},
// //没有子菜单
// noChildren() {
// //filter过滤没有子菜单的列表数据
// return this.menu.filter(item => !item.children)
// },
// //有子菜单
// hasChildren() {
// //filter过滤有子菜单的列表数据
// return this.menu.filter(item => item.children)
// },
//没有子菜单
noChildren() {
//filter过滤没有子菜单的列表数据
return this.asyncMenu.filter(item => !item.children)
},
//有子菜单
hasChildren() {
//filter过滤有子菜单的列表数据
return this.asyncMenu.filter(item => item.children)
},
//添加
asyncMenu(){
this.$store.state.tab.menu
}
}
}
</script>
//components/CommonAside.vue的style标签
<style lang="less" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
// 这里h3是less的语法规范,需要引入,scoped表是只在该组件中使用样式,
//less语法中,h3标签不用加点,class才要。
.el-menu {
height: 100%;
border: none;
h3 {
color: rgb(255, 255, 255);
text-align: center;
line-height: 48px;
}
}
</style>
(8)注释数据
import Vue from 'vue';
import VueRouter from 'vue-router';
import Main from '../views/Main.vue';
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Main',
component: Main,//全局引用
children:[
// {
// path:'/home',
// name:'home',
// component:()=>import('../views/home/index')
// },
// {
// path:'/user',
// name:'user',
// component:()=>import('../views/user/index')
// },
// {
// path:'/mail',
// name:'mail',
// component:()=>import('../views/mail/index')
// },
// {
// path:'/page1',
// name:'pageOne',
// component:()=>import('../views/other/pageOne.vue')
// },
// {
// path:'/page2',
// name:'pageTwo',
// component:()=>import('../views/other/pageTwo.vue')
// }
]
},
{
path:'/login',
name:'login',
component:()=>import('../views/login/login.vue')
}
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router;
(9)刷新白屏处理
new Vue({
store,
router,
render: h => h(App),
created() {
//动态添加路由菜单动态设置
store.commit('addMenu', this.$router)
}
}).$mount('#app')
(10)登录状态,展示首页
router.beforeEach((to, from, next) => {
console.log("w=token==")
store.commit('getToken')
const token = store.state.user.token
console.log("w=token==", token)
if (!token && to.name !== 'login') {
next({name: 'login'})
} else if (token && to.name === 'login') {
//登录状态,展示首页
next({name: 'home'})
} else {
next()
}
})
(11)退出登录
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item @click.native="logout()">退出</el- dropdown-item>
</el-dropdown-menu>
/**
* 退出登录
*/
logout(){
this.$store.commit('clearToken')
this.$store.commit('clearMenu')
//通过路径页面跳转
this.$router.push('/login')
}