HR-saas中台管理项目-基于vue-element-admin-登录模块

登录模块

线上地址

本章节,我们将在现有模板的基础上,完成如图的登录模块的功能

image-20200811014205189

设置固定的本地访问端口和网站名称

目标: 设置统一的本地访问端口和网站title

在正式开发业务之前,先将项目的本地端口网站名称进行一下调整

本地服务端口: 在**vue.config.js**中进行设置

vue.config.js 就是vue项目相关的编译,配置,打包,启动服务相关的配置文件,它的核心在于webpack,但是又不同于webpack,相当于改良版的webpack, 文档地址

如图,是开发环境服务端口的位置

image-20200710162221402

我们看到上面的 **process.env.port**实际上是一个nodejs服务下的环境变量,process表示在该项目的环境下,该变量在哪里设置呢?

在项目下, 我们发现了**.env.development.env.production**两个文件

development => 开发环境

production => 生产环境

当我们运行npm run dev进行开发调试的时候,此时会加载执行**.env.development**文件内容

当我们运行npm run build:prod进行生产环境打包的时候,会加载执行**.env.production**文件内容

所以,如果想要设置开发环境的接口,直接在**.env.development**中写入对于port变量的赋值即可

# 设置端口号
port = 8888

本节注意:修改服务的配置文件,想要生效的话,必须要重新启动服务,值‘8888’后面不能留有空格

网站名称

网站名称实际在configureWebpack选项中的name选项,通过阅读代码,我们会发现name实际上来源于src目录下

**settings.js**文件

所以,我们可以将网站名称改成"人力资源管理平台"

image-20200710164040042

提交代码

本节注意:修改服务的配置文件,想要生效的话,必须要重新启动服务,值‘8888’后面不能留有空格

本节任务:完成网站的开发服务端口设置和网站名称设置

登录页面的基础布局

**目标**完成登录页面的基础布局

页面效果

image-20200710164517167

首先要实现以上的页面效果, 我们可以直接将当前的登录页面进行相应的改造

设置头部背景

<!-- 放置标题图片 @是设置的别名-->
<div class="title-container">
        <h3 class="title">
          <img src="@/assets/common/login-logo.png" alt="">
        </h3>
 </div>

本节注意@是我们在vue.config.js中设置的一个路径别名,指定src根目录,这样可以很方便的寻找文件

设置背景图片

/* reset element-ui css */
.login-container {
    
    
  background-image: url('~@/assets/common/login.jpg'); // 设置背景图片
  background-position: center; // 将图片位置设置为充满整个屏幕
}

本节注意: 如需要在样式表中使用**@别名的时候,需要在@前面加上一个~**符号,否则不识别

设置手机号和密码的字体颜色

$light_gray: #68b0fe;  // 将输入框颜色改成蓝色

设置输入表单整体背景色

  .el-form-item {
    
    
    border: 1px solid rgba(255, 255, 255, 0.1);
    background: rgba(255, 255, 255, 0.7); // 输入登录表单的背景色
    border-radius: 5px;
    color: #454545;
  }

设置错误信息的颜色

 .el-form-item__error {
    
    
    color: #fff
  }

设置登录按钮的样式

需要给el-button 增加一个loginBtn的class样式

.loginBtn {
    
    
  background: #407ffe;
  height: 64px;
  line-height: 32px;
  font-size: 24px;
}

修改显示的提示文本和登录文本

   <div class="tips">
        <span style="margin-right:20px;">账号: 13800000002</span>
        <span> 密码: 123456</span>
   </div>

经过以上的改动, 我们得到了一个比较完善的登录页面

image-20200711002545085

解释:我们该项目的侧重点在于更多的介绍Vue中台项目所应用的技术,所以对于一些过于细节的样式或者布局,我们采用直接粘贴或者拷贝成型代码的方式,同学们可以通过课下多多练习

提交代码

本节注意@是我们在vue.config.js中设置的一个路径别名,指定src根目录,这样可以很方便的寻找文件

本节注意: 如需要在样式表中使用**@别名的时候,需要在@前面加上一个~**符号,否则不识别

本节任务: 完成登录首页的基本布局

登录表单的校验

**目标**对登录表单进行规则校验

基础模板已经有了基础校验的代码,所以我们这一章节更多的是修正和完善

el-form表单校验的先决条件

image-20200830212537835

手机号和密码的校验

字段名对应

为什么要对应? 因为基础模板采用的是**username的字段,但是实际接口中采用的是mobile的字段,为了更方便的写代码,所以我们将username改成mobile**

这里除了字段名,还有我们的规则校验名称,以及prop名称。

英文提示变成中文

基础模板中都是placeHolder占位符是英文,要变成中文

登录按钮文字同样需要换成中文

校验手机号和校验密码

基础模板中,已经做了校验,我们针对代码进行一些优化

新规则:手机号必填,并且进行格式校验,密码必填,长度6-16位之间

 data() {
    
    
    // 自定义校验函数
    const validateMobile = function(rule, value, callback) {
    
    
      // 校验value
      // if (validMobile(value)) {
    
    
      //   // 如果通过 直接执行callback
      //   callback()
      // } else {
    
    
      //   callback(new Error('手机号格式不正确'))
      // }
      validMobile(value) ? callback() : callback(new Error('手机号格式不正确'))
    }

    return {
    
    
      loginForm: {
    
    
        mobile: '13800000002',
        password: '123456'
      },
      loginRules: {
    
    
        mobile: [{
    
     required: true, trigger: 'blur', message: '手机号不能为空' }, {
    
    
          validator: validateMobile, trigger: 'blur'
        }],
        password: [{
    
     required: true, trigger: 'blur', message: '密码不能为空' }, {
    
    
          min: 6, max: 16, message: '密码的长度在6-16位之间 ', trigger: 'blur'
        }]
      },
      loading: false,
      passwordType: 'password',
      redirect: undefined
    }
  },

我们在**utils/validate.js**方法中增加了一个校验手机号的方法

/**
 * 校验手机号
 * **/
export function validMobile(str) {
    
    
  return /^1[3-9]\d{
    
    9}$/.test(str) // 校验手机号
}

**utils/validate.js**是一个专门存放校验工具方法的文件

关于修饰符

关于修饰符

在该页面中,我们发现了事件的几个修饰符 @keyup.enter.native @click.native.prevent

@keyup.**enter**属于按键修饰符,如果我们想监听在按回车键的时候触发,可以如下编写

<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">

@keyup.enter.native 表示监听组件的原生事件,比如 keyup就是于input的原生事件,这里写native表示keyup是一个原生事件

提交代码

本节任务:实现登录用户的手机号和密码校验

Vue-Cli配置跨域代理

目标: 通过配置vue-cli的代理解决跨域访问的问题

为什么会出现跨域?

当下,最流行的就是**前后分离项目,也就是前端项目后端接口并不在一个域名之下,那么前端项目访问后端接口必然存在跨域**的行为.

image-20200826110754199

怎么解决这种跨域 ?

请注意,我们所遇到的这种跨域是位于开发环境的,真正部署上线时的跨域是生产环境

解决开发环境的跨域问题

开发环境的跨域

开发环境的跨域,也就是在**vue-cli脚手架环境下开发启动服务时,我们访问接口所遇到的跨域问题,vue-cli为我们在本地开启了一个服务,可以通过这个服务帮我们代理请求**,解决跨域问题

这就是vue-cli配置webpack的反向代理

image-20200811022013103

采用vue-cli的代理配置

vue-cli的配置文件即**vue.config.js**,这里有我们需要的 代理选项

module.exports = {
    
    
  devServer: {
    
    
   // 代理配置
    proxy: {
    
    
        // 这里的api 表示如果我们的请求地址有/api的时候,就出触发代理机制
        // localhost:8888/api/abc  => 代理给另一个服务器
        // 本地的前端  =》 本地的后端  =》 代理我们向另一个服务器发请求 (行得通)
        // 本地的前端  =》 另外一个服务器发请求 (跨域 行不通)
        '/api': {
    
    
        target: 'www.baidu.com', // 我们要代理的地址
        changeOrigin: true, // 是否跨域 需要设置此值为true 才可以让本地服务代理我们发出请求
         // 路径重写
        pathRewrite: {
    
    
            // 重新路由  localhost:8888/api/login  => www.baidu.com/api/login
            '^/api': '' // 假设我们想把 localhost:8888/api/login 变成www.baidu.com/login 就需要这么做 
        }
      },
    }
  }
}

以上就是我们在vue-cli项目中配置的代理设置

接下来,我们在代码中将要代理的后端地址变成 后端接口地址

 // 代理跨域的配置
    proxy: {
    
    
      // 当我们的本地的请求 有/api的时候,就会代理我们的请求地址向另外一个服务器发出请求
      '/api': {
    
    
        target: 'http://ihrm-java.itheima.net/', // 跨域请求的地址
        changeOrigin: true // 只有这个值为true的情况下 才表示开启跨域
      }
    }

本节注意:我们并没有进行**pathRewrite,因为后端接口就是ihrm-java.itheima.net/api**这种格式,所以不需要重写

**vue.config.js**的改动如果要生效,需要进行重启服务

同时,还需要注意的是,我们同时需要注释掉 mock的加载,因为mock-server会导致代理服务的异常

// before: require('./mock/mock-server.js'),  // 注释mock-server加载

生产环境的跨域

生产环境表示我们已经开发完成项目,将项目部署到了服务器上,这时已经没有了vue-cli脚手架的**辅助了,我们只是把打包好的html+js+css交付运维人员,放到Nginx服务器而已,所以此时需要借助Nginx**的反向代理来进行

server{
    
    
    # 监听9099端口
    listen 9099;
    # 本地的域名是localhost
    server_name localhost;
    #凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://baidu.com
    location ^~ /api {
    
    
        proxy_pass http://baidu.com;
    }    
}

注意:这里的操作一般由运维人员完成,需要前端进行操作,这里我们进行一下简单了解

更多正向代理和反向代理知识,请阅读这篇文章Nginx反向代理

提交代码

本节注意:我们并没有进行**pathRewrite,因为后端接口就是ihrm-java.itheima.net/api**这种格式,所以不需要重写

本节任务: 配置vue-cli的反向代理,实现后端接口的跨域访问

封装单独的登录接口

目标 在单独请求模块中,单独封装登录接口

完成登录模块之后,我们需要对登录接口进行封装

首先,查阅接口文档中的登录接口

基础模板已经有了原来的登录代码,我们只需要进行简单的改造即可

export function login(data) {
    
    
  // 返回一个axios对象 => promise  // 返回了一个promise对象
  return request({
    
    
    url: '/sys/login', // 因为所有的接口都要跨域 表示所有的接口要带 /api
    method: 'post',
    data
  })
}

如图

image-20200812002834804

提交代码

本节任务:封装单独的登录接口

封装Vuex的登录Action并处理token

**目标**在vuex中封装登录的action,并处理token

在这个小节中,我们将在vuex中加入对于用户的登录的处理

在Vuex中对token进行管理

在传统模式中,我们登录的逻辑很简单,如图

image-20200812003821680

上图中,组件直接和接口打交道,这并没有什么问题,但是对于用户token这一高频使用的**钥匙**,我们需要让vuex来介入,将用户的token状态共享,更方便的读取,如图

image-20200812011826021

实现store/modules/user.js基本配置

// 状态
const state = {
    
    }
// 修改状态
const mutations = {
    
    }
// 执行异步
const actions = {
    
    }
export default {
    
    
  namespaced: true,
  state,
  mutations,
  actions
}


设置token的共享状态

const state = {
    
    
  token: null
}

我们需要知道,**钥匙**不能每次都通过登录获取,我们可以将token放置到本地的缓存中

在**utils/auth.js中,基础模板已经为我们提供了获取token,设置token,删除token**的方法,可以直接使用

只需要将存储的key放置成特定值即可

import Cookies from 'js-cookie'

const TokenKey = 'hrsaas-ihrm-token' // 设定一个独一无二的key

export function getToken() {
    
    
  return Cookies.get(TokenKey)
}

export function setToken(token) {
    
    
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
    
    
  return Cookies.remove(TokenKey)
}


初始化token状态 - store/modules/user.js

import {
    
     getToken, setToken, removeToken } from '@/utils/auth'
// 状态
// 初始化的时候从缓存中读取状态 并赋值到初始化的状态上
// Vuex的持久化 如何实现 ? Vuex和前端缓存相结合
const state = {
    
    
  token: getToken() // 设置token初始状态   token持久化 => 放到缓存中
}

提供修改token的mutations

// 修改状态
const mutations = {
    
    
  // 设置token
  setToken(state, token) {
    
    
    state.token = token // 设置token  只是修改state的数据  123 =》 1234
    // vuex变化 => 缓存数据
    setToken(token) // vuex和 缓存数据的同步
  },
  // 删除缓存
  removeToken(state) {
    
    
    state.token = null // 删除vuex的token
    removeToken() // 先清除 vuex  再清除缓存 vuex和 缓存数据的同步
  }
}

封装登录的Action

封装登录的action

登录action要做的事情,调用登录接口,成功后设置token到vuex,失败则返回失败

// 执行异步
const actions = {
    
    
  // 定义login action  也需要参数 调用action时 传递过来的参数
  async login(context, data) {
    
    
    const result = await login(data) // 实际上就是一个promise  result就是执行的结果
    // axios默认给数据加了一层data
    if (result.data.success) {
    
    
      // 表示登录接口调用成功 也就是意味着你的用户名和密码是正确的
      // 现在有用户token
      // actions 修改state 必须通过mutations
      context.commit('setToken', result.data.data)
    }
  }
}

上述代码中,我们使用了**async/await语法,如果用then**语法也是可以的

 // 为什么async/await 不用返回new Promise,因为 async函数本身就是 Promise,promise的值返回的值  
login(context, data) {
    
    
    return new Promise(function(resolve) {
    
    
      login(data).then(result => {
    
    
        if (result.data.success) {
    
    
          context.commit('setToken',  result.data.data) // 提交mutations设置token
          resolve()  // 表示执行成功了
        }
      })
    })
  }

以上两种写法都是OK的,我们在项目研发过程中,尽可能的采用前一种

除此之外,为了更好的让其他模块和组件更好的获取token数据,我们可以在**store/getters.js**中将token值作为公共的访问属性放出

const getters = {
    
    
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token // 在根级的getters上 开发子模块的属性给别人看 给别人用
}
export default getters


提交代码

通过本节内容,我们要掌握在Vuex中如何来管理共享状态

image-20200826145500269

本节任务:封装Vuex的登录Action并处理token

request中环境变量和异常的处理

**目标**设置request环境变量和异常处理

区分axios在不同环境中的请求基础地址

为什么会有环境变量之分? 如图

image-20200826150136697

从上图可以看出,开发环境实际上就是在自己的本地开发或者要求不那么高的环境,但是一旦进入生产,就是**真实的数据**。 拿银行作比喻,如果你在开发环境拿生产环境的接口做测试,银行系统就会发生很大的风险。

前端主要区分两个环境,开发环境,生产环境

也就是两个环境发出的请求地址是不同的,用什么区分呢?

环境变量

$ process.env.NODE_ENV # 当为production时为生产环境 为development时为开发环境

环境文件

我们可以在**.env.development.env.production**定义变量,变量自动就为当前环境的值

基础模板在以上文件定义了变量**VUE_APP_BASE_API,该变量可以作为axios请求的baseURL**

我们会发现,在模板中,两个值分别为**/dev-api/prod-api**

但是我们的开发环境代理是**/api**,所以可以统一下

# 开发环境的基础地址和代理对应
VUE_APP_BASE_API = '/api'

# 这里配置了/api,意味着需要在Nginx服务器上为该服务配置 nginx的反向代理对应/prod-api的地址 
VUE_APP_BASE_API = '/prod-api'  

本节注意:我们这里生产环境和开发环境设置了不同的值,后续我们还会在生产环境部署的时候,去配置该值所对应的反向代理,反向代理指向哪个地址,完全由我们自己决定,不会和开发环境冲突

在request中设置baseUrl

const service = axios.create({
    
    
  // 如果执行 npm run dev  值为 /api 正确  /api 这个代理只是给开发环境配置的代理
  // 如果执行 npm run build 值为 /prod-api  没关系  运维应该在上线的时候 给你配置上 /prod-api的代理
  baseURL: process.env.VUE_APP_BASE_API, // 设置axios请求的基础的基础地址
  timeout: 5000 // 定义5秒超时
}) // 创建一个axios的实例

处理axios的响应拦截器

OK,除此之外,axios返回的数据中默认增加了一层**data的包裹**,我们需要在这里处理下

并且,人资项目的接口,如果执行失败,只是设置了**successfalse**,并没有reject,我们需要一并处理下

处理逻辑如图

image-20200812020656210

// 响应拦截器
service.interceptors.response.use(response => {
    
    
  // axios默认加了一层data
  const {
    
     success, message, data } = response.data
  //   要根据success的成功与否决定下面的操作
  if (success) {
    
    
    return data
  } else {
    
    
    // 业务已经错误了 还能进then ? 不能 ! 应该进catch
    Message.error(message) // 提示错误消息
    return Promise.reject(new Error(message))
  }
}, error => {
    
    
  Message.error(error.message) // 提示错误信息
  return Promise.reject(error) // 返回执行错误 让当前的执行链跳出成功 直接进入 catch
})

既然在request中已经默认去除了一层data的外衣,所以我们也将上节login的action进行一下改动

处理登录的返回结构问题

  async login(context, data) {
    
    
    // 经过响应拦截器的处理之后 这里的result实际上就是 token
    const result = await login(data) // 实际上就是一个promise  result就是执行的结果
    // axios默认给数据加了一层data
    // 表示登录接口调用成功 也就是意味着你的用户名和密码是正确的
    // 现在有用户token
    // actions 修改state 必须通过mutations
    context.commit('setToken', result)
  }

提交代码

本节任务: 完成request环境变量和异常的处理

登录页面调用登录action,处理异常

目标 调用vuex中的登录action,并跳转到主页

按照如图的业务逻辑,把剩下的内容在登录页面引入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G8lzOk2f-1610539647128)(…/…/…/HR-saas%25E4%25B8%25AD%25E5%258F%25B0%25E7%25AE%25A1%25E7%2590%2586%25E9%25A1%25B9%25E7%259B%25AE%25E8%25B5%2584%25E6%2596%2599/HR-saas%25E4%25B8%25AD%25E5%258F%25B0%25E7%25AE%25A1%25E7%2590%2586%25E9%25A1%25B9%25E7%259B%25AE%25E8%25B5%2584%25E6%2596%2599/%25E8%25AE%25B2%25E4%25B9%2589/assets/image-20200812011826021.png)]

引入actions辅助函数

import {
    
     mapActions } from 'vuex'  // 引入vuex的辅助函数

引入action方法

此处,我们采用直接引入模块action的方式,后面我们采用分模块的引用方式

methods: {
    
    
    ...mapActions(['user/login'])
}


调用登录

  this.$refs.loginForm.validate(async isOK => {
    
    
        if (isOK) {
    
    
          try {
    
    
            this.loading = true
            // 只有校验通过了 我们才去调用action
            await this['user/login'](this.loginForm)
            // 应该登录成功之后
            // async标记的函数实际上一个promise对象
            // await下面的代码 都是成功执行的代码
            this.$router.push('/')
          } catch (error) {
    
    
            console.log(error)
          } finally {
    
    
            //  不论执行try 还是catch  都去关闭转圈
            this.loading = false
          }
        }
      })


提交代码

本节注意:我们调用的是Vuex中子模块的action,该模块我们进行了namespaced: true,所以引用aciton时需要带上**user/**, 并且在使用该方法时,直接使用 this['user/login'], 使用this.user/login 语法是错误的

本节任务:登录页面调用登录action,处理异常

主页的token拦截处理

目标:根据token处理主页的访问权限问题

权限拦截的流程图

我们已经完成了登录的过程,并且存储了token,但是此时主页并没有因为token的有无而被控制访问权限

接下来我们需要实现以下如下的流程图

image-20200714093601730

在基础框架阶段,我们已经知道**src/permission.js**是专门处理路由权限的,所以我们在这里处理

流程图转化代码

流程图转化的代码

// 权限拦截 导航守卫 路由守卫  router
import router from '@/router' // 引入路由实例
import store from '@/store' // 引入vuex store实例
import NProgress from 'nprogress' // 引入一份进度条插件
import 'nprogress/nprogress.css' // 引入进度条样式

const whiteList = ['/login', '/404'] // 定义白名单  所有不受权限控制的页面
// 路由的前置守卫
router.beforeEach(function(to, from, next) {
    
    
  NProgress.start() // 开启进度条
  //  首先判断有无token
  if (store.getters.token) {
    
    
    //   如果有token 继续判断是不是去登录页
    if (to.path === '/login') {
    
    
      //  表示去的是登录页
      next('/') // 跳到主页
    } else {
    
    
      next() // 直接放行
    }
  } else {
    
    
    // 如果没有token
    if (whiteList.indexOf(to.path) > -1) {
    
    
      // 如果找到了 表示在在名单里面
      next()
    } else {
    
    
      next('/login') // 跳到登录页
    }
  }
  NProgress.done() // 手动强制关闭一次  为了解决 手动切换地址时  进度条的不关闭的问题
})
// 后置守卫
router.afterEach(function() {
    
    
  NProgress.done() // 关闭进度条
})


在导航守卫的位置,我们添加了NProgress的插件,可以完成进入时的进度条效果

提交代码

本节任务:完成主页中根据有无token,进行页面访问的处理

主页的左侧导航样式

**目标**设置左侧的导航样式

接下来我们需要将左侧导航设置成如图样式

image-20200714142517337

主页的布局组件位置**src/layout**

主页布局架构

image-20200812023354631

左侧导航组件的样式文件styles/siderbar.scss

设置背景渐变色

.sidebar-container {
    
    
      background: -webkit-linear-gradient(bottom, #3d6df8, #5b8cff);
}

设置左侧导航背景图片

.scrollbar-wrapper { 
    background: url('~@/assets/common/leftnavBg.png') no-repeat 0 100%;
}

注意:在scss中,如果我们想要使用**@别名,需要在前面加上一个~**才可以

设置菜单选中颜色

    .el-menu {
    
    
      border: none;
      height: 100%;
      width: 100% !important;
      a{
    
    
        li{
    
    
          .svg-icon{
    
    
            color: #fff;
            font-size: 18px;
            vertical-align: middle;
            .icon{
    
    
              color:#fff;
            }
          }
          span{
    
    
            color: #fff;
          }
          &:hover{
    
    
            .svg-icon{
    
    
              color: #43a7fe
            }
            span{
    
    
              color: #43a7fe;
            }
          }
        }
      }
    }

注意:因为我们后期没有二级菜单,所以这里暂时不用对二级菜单的样式进行控制

显示左侧logo图片 src/setttings.js

module.exports = {
    
    

  title: '人力资源管理平台',

  /**
   * @type {boolean} true | false
   * @description Whether fix the header
   */
  fixedHeader: false,

  /**
   * @type {boolean} true | false
   * @description Whether show the logo in sidebar
   */
  sidebarLogo: true // 显示logo
}


设置头部图片结构 src/layout/components/Sidebar/Logo.vue

<div class="sidebar-logo-container" :class="{'collapse':collapse}">
    <transition name="sidebarLogoFade">
      <router-link key="collapse" class="sidebar-logo-link" to="/">
        <https://gitee.com//owahahah/hrsass/raw/master/img src="@/assets/common/logo.png" class="sidebar-logo  ">
      </router-link>
    </transition>
  </div>

设置大图和小图的样式

  &.collapse {
    .sidebar-logo {
      margin-right: 0px;
      width: 32px;
      height: 32px;
    }
  }
// 小图样式

.sidebar-logo {
      width: 140px;
      vertical-align: middle;
      margin-right: 12px;
}
// 大图样式

去除logo的背景色

image-20200827104724769

提交代码

本节任务: 完成主页的左侧导航样式

本节注意:我们该项目中没有二级显示菜单,所以二级菜单的样式并没有做过多处理,同学们不必在意

设置头部内容的布局和样式

**目标**设置头部内容的布局和样式

我们需要把页面设置成如图样式

image-20200714172602305

头部组件位置 layout/components/Navbar.vue

添加公司名称注释面包屑

  <div class="app-breadcrumb">
      江苏传智播客教育科技股份有限公司
      <span class="breadBtn">体验版</span>
  </div>
 <!-- <breadcrumb class="breadcrumb-container" /> -->


公司样式

.app-breadcrumb {
  display: inline-block;
  font-size: 18px;
  line-height: 50px;
  margin-left: 10px;
  color: #ffffff;
  cursor: text;
  .breadBtn {
    background: #84a9fe;
    font-size: 14px;
    padding: 0 10px;
    display: inline-block;
    height: 30px;
    line-height: 30px;
    border-radius: 10px;
    margin-left: 15px;
  }
}

头部背景渐变色

.navbar {
    background-image: -webkit-linear-gradient(left, #3d6df8, #5b8cff);
}

汉堡组件图标颜色 src/components/Hamburger/index.vue

注意这里的图标我们使用了svg,设置颜色需要使用svg标签的**fill属性**

设置svg图标为白色

<svg
      :class="{'is-active':isActive}"
      class="hamburger"
      viewBox="0 0 1024 1024"
      xmlns="http://www.w3.org/2000/svg"
      width="64"
      height="64"
      fill="#fff" 
    >

右侧下拉菜单设置

将下拉菜单调节成**首页/项目地址/退出登录**

    <div class="right-menu">
      <el-dropdown class="avatar-container" trigger="click">
        <div class="avatar-wrapper">
          <https://gitee.com//owahahah/hrsass/raw/master/img src="@/assets/common/bigUserHeader.png" class="user-avatar">
          <span class="name">管理员</span>
          <i class="el-icon-caret-bottom" style="color:#fff" />
        </div>
        <el-dropdown-menu slot="dropdown" class="user-dropdown">
          <router-link to="/">
            <el-dropdown-item>
              首页
            </el-dropdown-item>
          </router-link>
          <a target="_blank" href="https://gitee.com/shuiruohanyu/hrsaas53">
            <el-dropdown-item>项目地址</el-dropdown-item>
          </a>
          <el-dropdown-item divided @click.native="logout">
            <span style="display:block;">退出登录</span>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>

头像和下拉菜单样式

  .user-avatar {
    
    
          cursor: pointer;
          width: 30px;
          height: 30px;
          border-radius: 15px;
          vertical-align: middle;

   }
   .name {
    
    
          color: #fff;
          vertical-align: middle;
          margin-left:5px;
   }
   .user-dropdown {
    
    
           color: #fff;
    }

用户名和头像我们先用了假数据进行,下小章节,会进行这份数据的获取

最终效果

提交代码

获取用户资料接口和token注入

目标 封装获取用户资料的资料信息

上小节中,我们完成了头部菜单的基本布局,但是用户的头像和名称并没有,我们需要通过接口调用的方式获取当前用户的资料信息

获取用户资料接口

在**src/api/user.js**中封装获取用户资料的方法

/**
 *  获取用户的基本资料
 *
 * **/
export function getUserInfo() {
    
    
  return request({
    
    
    url: '/sys/profile',
    method: 'post'
  })
}

我们忽略了一个问题!我们的headers参数并没有在这里传入,为什么呢

headers中的Authorization相当于我们开门调用接口)时**钥匙(token),我们在打开任何带安全权限的门的时候都需要钥匙(token)** 如图

image-20200715233339927

每次在接口中携带**钥匙(token)**很麻烦,所以我们可以在axios拦截器中统一注入token

image-20200716000203862

统一注入token src/utils/request.js

service.interceptors.request.use(config => {
    
    
  // 在这个位置需要统一的去注入token
  if (store.getters.token) {
    
    
    // 如果token存在 注入token
    config.headers['Authorization'] = `Bearer ${
      
      store.getters.token}`
  }
  return config // 必须返回配置
}, error => {
    
    
  return Promise.reject(error)
}) 

本节任务: 完成获取用户资料接口和token注入

封装获取用户资料的action并共享用户状态

目标: 在用户的vuex模块中封装获取用户资料的action,并设置相关状态

用户状态会在后续的开发中,频繁用到,所以我们将用户状态同样的封装到action中

image-20200827111324564

封装获取用户资料action action src/store/modules/user.js

import {
    
     login, getUserInfo } from '@/api/user'
  
  // 获取用户资料action
  async getUserInfo (context) {
    
    
    const result = await getUserInfo()  // 获取返回值
    context.commit('setUserInfo', result) // 将整个的个人信息设置到用户的vuex数据中
    return result // 这里为什么要返回 为后面埋下伏笔
  }


同时,配套的我们还进行了关于用户状态的mutations方法的设计

初始化state state

const state = {
    
    
  token: getToken(), // 设置token初始状态   token持久化 => 放到缓存中
  userInfo: {
    
    } // 定义一个空的对象 不是null 因为后边我要开发userInfo的属性给别人用  userInfo.name
}


userInfo为什么我们不设置为null,而是设置为 {}

因为我们会在**getters**中引用userinfo的变量,如果设置为null,则会引起异常和报错

设置和删除用户资料 mutations

 // 设置用户信息
  setUserInfo(state, userInfo) {
    
    
    state.userInfo = {
    
     ...userInfo } // 用 浅拷贝的方式去赋值对象 因为这样数据更新之后,才会触发组件的更新
  },
  // 删除用户信息
  reomveUserInfo(state) {
    
    
    state.userInfo = {
    
    }
  }


同学们,我们将所有的资料设置到了userInfo这个对象中,如果想要取其中一个值,我们还可以在getters中建立相应的映射

因为我们要做映射,如果初始值为null,一旦引用了getters,就会报错

建立用户名的映射 src/store/getters.js

const getters = {
    
    
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token,
  name: state => state.user.userInfo.username // 建立用户名称的映射
}
export default getters



到现在为止,我们将用户资料的action => mutation => state => getters 都设置好了, 那么我们应该在什么位置来调用这个action呢 ?

别着急,先提交代码,下个小节,我们来揭晓答案

提交代码

**本节任务**封装获取用户资料的action并共享用户状态

权限拦截处调用获取资料action

**目标**在权限拦截处调用aciton

权限拦截器调用action

在上小节中,我们完成了用户资料的整个流程,那么这个action在哪里调用呢?

用户资料有个硬性要求,**必须有token**才可以获取,那么我们就可以在确定有token的位置去获取用户资料

image-20200716004526838

由上图可以看出,一旦确定我们进行了放行,就可以获取用户资料

image-20200813013009294

调用action src/permission.js

 if(!store.state.user.userInfo.userId) {
    
    
       await store.dispatch('user/getUserInfo')
 }


如果我们觉得获取用户id的方式写了太多层级,可以在vuex中的getters中设置一个映射 src/store/getters.js

  userId: state => state.user.userInfo.userId // 建立用户id的映射


代码就变成了

 if (!store.getters.userId) {
    
    
        // 如果没有id这个值 才会调用 vuex的获取资料的action
        await store.dispatch('user/getUserInfo')
        // 为什么要写await 因为我们想获取完资料再去放行
      }


此时,我们可以通过dev-tools工具在控制台清楚的看到数据已经获取

image-20200716012120619

最后一步,只需要将头部菜单中的名称换成真实的用户名即可

获取头像接口合并数据

头像怎么办?

我们发现头像并不在接口的返回体中(接口原因),我们可以通过另一个接口来获取头像,并把头像合并到当前的资料中

封装获取用户信息接口 src/api/user.js

/** *
 *
 * 获取用户的基本信息  现在写它 完全是为了显示头像
 * **/
export function getUserDetailById(id) {
    
    
  return request({
    
    
    url: `/sys/user/${
      
      id}`
  })
}


这个接口需要用户的userId,在前一个接口处,我们已经获取到了,所以可以直接在后面的内容去衔接

import {
    
     login, getUserInfo, getUserDetailById } from '@/api/user'
 
// 获取用户资料action
 async getUserInfo(context) {
    
    
    const result = await getUserInfo() // result就是用户的基本资料
    const baseInfo = await getUserDetailById(result.userId) // 为了获取头像
    const baseResult = {
    
     ...result, ...baseInfo } // 将两个接口结果合并
    // 此时已经获取到了用户的基本资料 迫不得已 为了头像再次调用一个接口
    context.commit('setUserInfo', baseResult) // 提交mutations
    // 加一个点睛之笔  这里这一步,暂时用不到,但是请注意,这给我们后边会留下伏笔
    return baseResult
  }


为了更好地获取头像,同样可以把头像放于getters中

  staffPhoto: state => state.user.userInfo.staffPhoto // 建立用户头像的映射


此时,我们的头像和名称已经获取到了,可以直接将之前的假数据换成真正的头像和名称

用户名 layout/components/Navbar.vue

 ...mapGetters([
      'sidebar',
      'name',
      'staffPhoto'
    ])
     <https://gitee.com//owahahah/hrsass/raw/master/img :src="staffPhoto" class="user-avatar">
     <span class="name">{
    
    {
    
     name }}</span>



通过设置,用户名已经显示,头像依然没有显示,这是因为虽然有地址,但是地址来源是私有云,目前已经失效,所以需要额外处理下图片的异常

image-20200827140451144

至于处理图片的异常,我们在下一节中,可采用自定义指令的形式来进行处理

本节任务:实现权限拦截处调用获取资料action

自定义指令-解决异常图片情况

目标: 通过自定义指令的形式解决异常图片的处理

自定义指令

注册自定义指令

Vue.directive('指令名称', {
    
    
    // 会在当前指令作用的dom元素 插入之后执行
    // options 里面是指令的表达式
    inserted: function (dom,options) {
    
    
        
    }
})


自定义指令可以采用统一的文件来管理 src/directives/index.js,这个文件负责管理所有的自定义指令

首先定义第一个自定义指令 v-imagerror

export const imagerror = {
    
    
  // 指令对象 会在当前的dom元素插入到节点之后执行
  inserted(dom, options) {
    
    
    // options是 指令中的变量的解释  其中有一个属性叫做 value
    // dom 表示当前指令作用的dom对象
    // dom认为此时就是图片
    // 当图片有地址 但是地址没有加载成功的时候 会报错 会触发图片的一个事件 => onerror
    dom.onerror = function() {
    
    
      // 当图片出现异常的时候 会将指令配置的默认图片设置为该图片的内容
      // dom可以注册error事件
      dom.src = options.value // 这里不能写死
    }
  }
}



在main.js完成自定义指令全局注册

然后,在**main.js**中完成对于该文件中所有指令的全局注册

import * as directives from '@/directives'
// 注册自定义指令
// 遍历所有的导出的指令对象 完成自定义全局注册
Object.keys(directives).forEach(key => {
    
    
  // 注册自定义指令
  Vue.directive(key, directives[key])
})


针对上面的引入语法 import * as 变量 得到的是一个对象**{ 变量1:对象1,变量2: 对象2 ... }**, 所以可以采用对象遍历的方法进行处理

指令注册成功,可以在**navbar.vue**中直接使用了

<https://gitee.com//owahahah/hrsass/raw/master/img v-imageerror="defaulthttps://gitee.com//owahahah/hrsass/raw/master/img" :src="staffPhoto" class="user-avatar">


 data() {
    
    
    return {
    
    
      defaulthttps://gitee.com//owahahah/hrsass/raw/master/img: require('@/assets/common/head.jpg')
    }
  },


本节任务:实现一个自定义指令,解决图片加载异常的问题

实现登出功能

目标:实现用户的登出操作

登出仅仅是跳到登录页吗?

不,当然不是,我们要处理如下

image-20200827143847893

同样的,登出功能,我们在vuex中的用户模块中实现对应的action

登出action src/store/modules/user.js

// 登出的action
  logout(context) {
    
    
    // 删除token
    context.commit('removeToken') // 不仅仅删除了vuex中的 还删除了缓存中的
    // 删除用户资料
    context.commit('removeUserInfo') // 删除用户信息
  }


头部菜单调用action src/layout/components/Navbar.vue

  async logout() {
    
    
      await this.$store.dispatch('user/logout') // 这里不论写不写 await 登出方法都是同步的
      this.$router.push(`/login`) // 跳到登录
  }


**注意**我们这里也可以采用vuex中的模块化引入辅助函数

import {
    
     mapGetters, createNamespacedHelpers  } from 'vuex'
const {
    
      mapActions } = createNamespacedHelpers('user') // 这是的mapAction直接对应模块下的action辅助函数
  methods: {
    
    
    ...mapActions(['lgout']),
  }


以上代码,实际上直接对user模块下的action进行了引用,

提交代码

本节任务: 实现登出功能

Token失效的主动介入

目标: 处理当token失效时业务

主动介入token处理的业务逻辑

开门的钥匙不是一直有效的,如果一直有效,会有安全风险,所以我们尝试在客户端进行一下token的时间检查

具体业务图如下

image-20200716231205153

流程图转化代码

流程图转化代码 src/utils/auth.js

const timeKey = 'hrsaas-timestamp-key' // 设置一个独一无二的key

// 获取时间戳
export function getTimeStamp() {
    
    
  return Cookies.get(timeKey)
}
// 设置时间戳
export function setTimeStamp() {
    
    
  Cookies.set(timeKey, Date.now())
}






src/utils/request.js

import axios from 'axios'
import store from '@/store'
import router from '@/router'
import {
    
     Message } from 'element-ui'
import {
    
     getTimeStamp } from '@/utils/auth'
const TimeOut = 3600 // 定义超时时间

const service = axios.create({
    
    
// 当执行 npm run dev  => .evn.development => /api => 跨域代理
  baseURL: process.env.VUE_APP_BASE_API, // npm  run dev  => /api npm run build =>  /prod-api
  timeout: 5000 // 设置超时时间
})
// 请求拦截器
service.interceptors.request.use(config => {
    
    
  // config 是请求的配置信息
  // 注入token
  if (store.getters.token) {
    
    
    // 只有在有token的情况下 才有必要去检查时间戳是否超时
    if (IsCheckTimeOut()) {
    
    
      // 如果它为true表示 过期了
      // token没用了 因为超时了
      store.dispatch('user/logout') // 登出操作
      // 跳转到登录页
      router.push('/login')
      return Promise.reject(new Error('token超时了'))
    }
    config.headers['Authorization'] = `Bearer ${
      
      store.getters.token}`
  }
  return config // 必须要返回的
}, error => {
    
    
  return Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(response => {
    
    
  // axios默认加了一层data
  const {
    
     success, message, data } = response.data
  //   要根据success的成功与否决定下面的操作
  if (success) {
    
    
    return data
  } else {
    
    
    // 业务已经错误了 还能进then ? 不能 ! 应该进catch
    Message.error(message) // 提示错误消息
    return Promise.reject(new Error(message))
  }
}, error => {
    
    
  Message.error(error.message) // 提示错误信息
  return Promise.reject(error) // 返回执行错误 让当前的执行链跳出成功 直接进入 catch
})
// 是否超时
// 超时逻辑  (当前时间  - 缓存中的时间) 是否大于 时间差
function IsCheckTimeOut() {
    
    
  var currentTime = Date.now() // 当前时间戳
  var timeStamp = getTimeStamp() // 缓存时间戳
  return (currentTime - timeStamp) / 1000 > TimeOut
}
export default service



本节注意:我们在调用登录接口的时候 一定是没有token的,所以token检查不会影响登录接口的调用

同理,在登录的时候,如果登录成功,我们应该设置时间戳

  // 定义login action  也需要参数 调用action时 传递过来的参数
  // async 标记的函数其实就是一个异步函数 -> 本质是还是 一个promise
  async login(context, data) {
    
    
    // 经过响应拦截器的处理之后 这里的result实际上就是 token
    const result = await login(data) // 实际上就是一个promise  result就是执行的结果
    // axios默认给数据加了一层data
    // 表示登录接口调用成功 也就是意味着你的用户名和密码是正确的
    // 现在有用户token
    // actions 修改state 必须通过mutations
    context.commit('setToken', result)
    // 写入时间戳
    setTimeStamp() // 将当前的最新时间写入缓存
  }


提交代码

有主动处理就有被动处理,也就是后端告诉我们超时了,我们被迫做出反应,如果后端接口没有做处理,主动介入就是一种简单的方式

本节任务:完成token超时的主动介入

Token失效的被动处理

目标: 实现token失效的被动处理

除了token的主动介入之外,我们还可以对token进行被动的处理,如图

image-20200818155842864

token超时的错误码是**10002**

代码实现 src/utils/request.js

error => {
    
    
  // error 信息 里面 response的对象
  if (error.response && error.response.data && error.response.data.code === 10002) {
    
    
    // 当等于10002的时候 表示 后端告诉我token超时了
    store.dispatch('user/logout') // 登出action 删除token
    router.push('/login')
  } else {
    
    
    Message.error(error.message) // 提示错误信息
  }
  return Promise.reject(error)
}


无论是主动介入还是被动处理,这些操作都是为了更好地处理token,减少错误异常的可能性

本节任务 Token失效的被动处理

总结

本章节我们一步步实现了如下的效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ev0yGb3-1610539647145)(…/…/…/HR-saas%25E4%25B8%25AD%25E5%258F%25B0%25E7%25AE%25A1%25E7%2590%2586%25E9%25A1%25B9%25E7%259B%25AE%25E8%25B5%2584%25E6%2596%2599/HR-saas%25E4%25B8%25AD%25E5%258F%25B0%25E7%25AE%25A1%25E7%2590%2586%25E9%25A1%25B9%25E7%259B%25AE%25E8%25B5%2584%25E6%2596%2599/%25E8%25AE%25B2%25E4%25B9%2589/assets/image-20200716015113164.png)]

实际的业务走向

image-20200827152601712

实际上,我们的主页功能有一个重要的**角色权限**功能还没有完成,此功能等到我们完成基本业务之后再进行展开

中台大型后端平台的深入是一个**抽丝剥茧**的过程,循序渐进的明白每一步的操作是非常关键的。

路由页面整理

目标 删除基础模板中附带的多余页面

基础模板帮我们提前内置了一些页面,本章节我们进行一下整理

首先,我们需要知道类似这种大型中台项目的页面路由是如何设置的。

简单项目

image-20200827153753307

当前项目结构

image-20200827155342126

为什么要拆成若干个路由模块呢?

因为复杂中台项目的页面众多,不可能把所有的业务都集中在一个文件上进行管理和维护,并且还有最重要的,前端的页面中主要分为两部分,一部分是所有人都可以访问的, 一部分是只有有权限的人才可以访问的,拆分多个模块便于更好的控制

静态路由和动态路由

image-20200716150421791

**注意**这里的动态路由并不是 路由传参的动态路由

了解完成路由设计之后,我们对当前的路由进行一下整理

删除多余的静态路由表 src/router/index.js

/**
 * constantRoutes
 * a base page that does not have permission requirements
 * all roles can be accessed
 */
export const constantRoutes = [
  {
    
    
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    
    
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },

  {
    
    
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
    
    
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: {
    
     title: 'Dashboard', icon: 'dashboard' }
    }]
  },

  // 404 page must be placed at the end !!!
  {
    
     path: '*', redirect: '/404', hidden: true }
]

上面代码,我们只对登录页/404/主页进行了保留

并且我们发现,删除了其他页面之后,左侧导航菜单的数据也只剩下了首页

image-20200716153900077

这是因为左侧导航菜单的数据来源于路由信息

删除多余的路由组件

image-20200716154315430

只保留以上三个路由组件的内容,后续慢慢增加

同样的在api目录下,存在多余的api-table.js 一并删除

提交代码

猜你喜欢

转载自blog.csdn.net/weixin_48371382/article/details/112588587