03-HR-主页模块(左侧菜单,左侧图标,头部导航,获取用户信息,vuex存储用户资料,获取用户的头像,处理头像失效问题,退出功能,处理token失效问题)

248 阅读8分钟

主页模块

目标:实现主页基本布局效果

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

image.png

主页布局-左侧菜单

目标:实现左侧菜单效果

  • 左侧导航样式处理 styles/siderbar.scss
// 设置背景渐变色
.sidebar-container {
   background: -webkit-linear-gradient(bottom, #3d6df8, #5b8cff);
}
// 设置左侧导航背景图片
.scrollbar-wrapper { 
   background: url('~@/assets/common/leftnavBg.png') no-repeat 0 100%;
}
// 设置菜单选中颜色
.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;
        }
      }
    }
  }
}

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

主页布局-左侧图标

需求:定制SideBar菜单的Logo:@/src/layout/components/Logo.vue

  • 左侧菜单展开和折叠的控制逻辑
    • 左侧菜单的展开和折叠由什么决定?el-menu组件的collapse属性决定
    • 谁会影响到这个属性的值?点击汉堡图标会影响到
    • 汉堡图标是如何影响这个collapse的值的?app这个vuex模块来管理菜单展开和折叠状态的
      • app提供菜单的默认展开状态数据给菜单组件
      • 点击汉堡图标时,通过action影响app模块的里面的状态数据
// sidebar一共有三个地方需要用到
// 1、值的初始化操作
// 2、他的值是以全局getters的方式提供给组件
// 3、点击汉堡图标,触发action修改这个值即可
  • 左侧logo图片显示效果调整
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
  <transition name="sidebarLogoFade">
    <router-link key="collapse" class="sidebar-logo-link" to="/">
      <img src="@/assets/common/logo.png" class="sidebar-logo  ">
    </router-link>
  </transition>
</div>
  • 设置大图和小图的样式
// 大图样式
& .sidebar-logo {
  width: 140px;
  vertical-align: middle;
  margin-right: 12px;
}
// 小图样式
&.collapse {
  .sidebar-logo {
    margin-right: 0px;
    width: 50px;
    height: 24px;
  }
}

总结:&和类名之间的空格问题

  1. 如果有空格,就是父子关系
  2. 如果没有空格就是并列关系(两个类名必须同时存在)

主页布局-头部导航

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

  • 头部组件效果 layout/components/Navbar.vue
<div class="app-breadcrumb">
  江苏传智播客教育科技股份有限公司
  <span class="breadBtn">体验版</span>
</div>
<!-- <breadcrumb class="breadcrumb-container" /> -->
  • 布局样式
.navbar {
    background-image: -webkit-linear-gradient(left, #3d6df8, #5b8cff);
    .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;
      }
    }
}
  • 汉堡组件图标颜色 src/components/Hamburger/index.vue
<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" 
 >

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

  • 右侧下拉菜单设置
<div class="right-menu">
  <el-dropdown class="avatar-container" trigger="click">
    <div class="avatar-wrapper">
      <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://xxx.com">
        <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>
  • 头像和下拉菜单样式
.avatar-wrapper {
  position: relative;

  .user-avatar {
    cursor: pointer;
    width: 30px;
    height: 30px;
    border-radius: 15px;
    vertical-align: middle;
  }
  .name {
    cursor: pointer;
    color: #fff;
    vertical-align: middle;
    margin-left:5px;
  }
  .user-dropdown {
    color: #fff;
  }

  .el-icon-caret-bottom {
    cursor: pointer;
    position: absolute;
    right: -20px;
    top: 20px;
    font-size: 12px;
  }
}

总结:汉堡菜单;面包屑导航;右侧下拉菜单(基于ElementUI组件dropdown实现)

注意:svg的样式控制使用fill属性填充颜色

获取用户信息

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

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

  • 获取用户资料接口 src/api/user.js
export function getInfo() {
  return request({
    url: '/sys/profile',
    method: 'post'
  })
}

这个接口, 需要配置 headers 请求头, 配置 token, 而我们在请求任何带安全权限的接口时都需要**令牌(token)** ,每次在接口中携带**令牌(token)**很麻烦,所以我们可以在axios拦截器中统一添加token。 src/utils/request.js

// 请求拦截器
instance.interceptors.request.use(function(config) {
  // 在发送请求之前做些什么
  if (store.getters.token) {
    // 如果token存在 注入token
    config.headers.Authorization = `Bearer ${token}`
    //config.headers = {
    //  Authorization: `Bearer ${store.getters.token}`
    ///}
  }
  return config
}, function(error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})
  • 添加一个action
// 获取用户信息
async getInfo({ commit, state }) {
  const ret = await getInfo()
  if (ret.code === 10000) {
    // 触发mutation更新用户信息
    commit('updateInfo', ret.data)
  }
},
  • 添加mutation
updateInfo (state, payload) {
  state.userInfo = payload
}
  • 添加状态
userInfo: null
  • 触发action
created () {
  // 获取用户信息
  this.$store.dispatch('user/getInfo')
},

总结:

  1. 添加拦截器,传递请求头
  2. vuex的基本操作:state/mutation/action
  • 前端解决跨域问题
// 在vue.config.js文件中配置代理
devServer: {
  port: port,
  // 自动打开浏览器
  open: true,
  overlay: {
    warnings: false,
    errors: true
  },
  proxy: {
    // 所有的请求路径以api开始的地址都会被代理
    // 发送的请求 http://localhost:9528/api/login
    // 代理的目标 http://ihrm-java.itheima.net/api/login
    '^/api': {
      // 代理的目标地址
      target: 'http://ihrm-java.itheima.net'
    }
  }
  // before: require('./mock/mock-server.js')
},
// 实际发送的请求基准路径调整为本地地址 .env.development
VUE_APP_BASE_API = 'http://localhost:9528/api/'

image.png

总结:

  1. 网页网址http://localhost:9528/#/dashboard
  2. 浏览器监控的接口网址http://localhost:9528/api/sys/profile
  3. 实际的发送的接口网址ihrm-java.itheima.net/api/sys/pro…

vuex存储用户资料

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

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

  • 封装获取用户资料action action src/store/modules/user.js
import { login, getInfo } from '@/api/user.js'
mutations: {
    updateInfo (state, payload) {
      // 初始化用户信息
      state.userInfo = payload
    }
 },
actions: {
    // 获取用户基本信息
    async getInfo (context) {
      // 调用接口获取数据
      const ret = await getInfo()
      // 初始化用户信息
      context.commit('updateInfo', ret.data)
    }
}
  • NavBar 组件中调用
import { mapActions } from 'vuex'

created() {
  // 原始写法
  // this.$store.dispatch('user/getInfo')
  this.getInfo()
},
methods: {
  ...mapActions('user', ['getInfo']),
}
  • 页面中使用
const getters = {
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token,
  username: state => state.user.userInfo && state.user.userInfo.username // 建立用户名称的映射
}
export default getters
computed: {
  ...mapGetters([
    'sidebar',
    'avatar',
    'username'
  ])
},
<span class="name">{{username}}</span>

总结:

  1. 通过action调用接口获取用户信息
  2. 把获取的数据更新到state里面
  3. NavBar组件中触发action
  4. 通过getters解析用户信息中的username
  5. 模板中显示用户信息

获取用户的头像

目标:获取用户头像信息

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

  • 封装获取用户信息接口 src/api/user.js
// 获取用户头像信息
export function getDetailInfo (id) {
  // 参数ID表示当前登录系统的用户id
  return request({
    method: 'get',
    url: '/sys/user/' + id
  })
}
  • 重构action
// 获取用户信息
async getInfo (context) {
  const { code, data } = await getInfo()
  // 获取用户头像等信息(上一个请求的返回结果作为下一个请求的参数)
  const { code: acode, data: adata } = await getBaseInfo(data.userId)
  if (code === 10000 && acode === 10000) {
    // 获取成功
    context.commit('updateUserInfo', {
      ...data,
      ...adata
    })
  }
}
  • 为了页面中更好地获取头像,同样可以把头像放于getters中
avatar: state => state.user.userInfo && state.user.userInfo.staffPhoto // 建立用户头像的映射
  • 展示头像**layout/components/Navbar.vue**
computed: {
  ...mapGetters([
    'sidebar',
    'avatar',
    'username'
  ])
},
<img :src="avatar" class="user-avatar">
<span class="name">{{ nsername }}</span>

总结:基于Action获取用户的详细信息

  1. 基于延展运算符合并对象的写法

  2. 上一个接口调用的结果作为下一个接口调用的参数(注意第一次调用的await是必要的)

处理头像失效问题

目标:处理图片加载失败时的默认显示效果

头像的图片如果加载失败了,就显示一张默认的图片(基于自定义指令实现)

  1. 自定义指令
  2. Vue插件(如何自己做一个Vue插件)
  • 关于Vue插件用法补充
// 定义插件
export default {
  // Vue.use(MyPlugins, 'defaultImg.png')
  // Vue.use的参数二传递给install方法的第二个参数options
  install (Vue, options) {
    console.log(options)
  }
}
// 导入并配置插件
import MyPlugins from '@/utils/plugins.js'
Vue.use(MyPlugins, { info: 'hello'} )

总结:配置和定义插件时,支持配置选项

  • 注册自定义指令基本用法
Vue.directive('指令名称', {
    // 会在当前指令作用的dom元素 插入之后执行
    // el 指令所在dom元素
    // bindings 里面是指令的参数信息对象
    inserted(el, bindings) {
        
    }
})
  • 当图片有地址 但是地址没有加载成功的时候 会报错 会触发图片的一个事件 => onerror
/*
  封装Vue插件
*/
import defaultAvatar from '@/assets/common/head.jpg'

export default {
  install (Vue, options) {
    // 扩展一个自定义指令,处理图片加载失败的情况
    // 扩展自定义指令:处理图片加载失败的情况
    Vue.directive('imgerror', {
      // 指令首次初始化时触发一次
      inserted (el, bindings) {
        // el表示绑定指令的DOM元素
        // bindings表示指令相关的参数信息
        // <img v-imgerror="avatar" class="user-avatar">
        // bindings.value表示 v-imgerror 指令等号后面的值
        if (bindings.value) {
          el.src = bindings.value
        }
      },
      // 指令依赖的数据发生更新时触发
      componentUpdated (el, bindings) {
        // 接口获取头像数据之后,会更新avatar
        el.src = bindings.value
        // 如果后端提供的数据是错误的,此时应该显示一张默认头像
        // 如果img加载图片失败了,自动触发该事件
        el.onerror = () => {
          // 显示默认图片即可
          el.src = options.defaultAvatar || defaultAvatar
        }
      }
    })
  }
}
// main.js
// 导入插件
import MyPlugins from '@/utils/plugins.js'
import defaultAvatar from '@/assets/common/head.jpg'

// 配置插件
Vue.use(MyPlugins, { defaultAvatar } )
  • 直接赋值图片的地址给指令,在指令内部给图片src赋值
<img v-imgerror="avatar" class="user-avatar">

总结:

  1. 自定义指令的基本规则
  2. 插件基本使用规则:先定义,再导入并配置(支持选项)
  3. 配置插件时,可以传递options选项
  4. 扩展图片加载的自定义指令(原生dom事件 img.onerror 表示图片加载失败)
  5. 使用自定义指令

退出功能

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

image.png

  • 基于.native事件修饰符绑定组件的原生事件
<el-dropdown-item divided @click.native="logout">
  <span style="display:block;">退出登录</span>
</el-dropdown-item>

总结:在组件的标签上绑定事件时,如果添加.native修饰符,表示把事件绑定到组件的根元素上

  • 退出提示效果
// 点击退出时需要提示是否退出
this.$confirm('确认要退出吗?', '提示', {
  confirmButtonText: '确定',
  cancelButtonText: '取消',
  type: 'warning'
}).then(() => {
  // 点击确定,执行then方法
  console.log('ok')
}).catch(() => {
  // 点击取消,执行catch方法
  console.log('cancel')
})

总结:退出API支持Promise方法,点击确定触发then,点击取消触发catch

  • 退出action src/store/modules/user.js
import { getToken, setToken, removeToken } from '@/utils/auth'

const actions = {
  // 用户退出
  logout(context) {
    // 清除token和用户信息
    context.commit('setToken', null)
    context.commit('updateUserInfo', null)
    // 删除缓存中的token
    removeToken()
    // 跳转到登录页面
    router.push('/login')
  },
  • 头部菜单调用action src/layout/components/Navbar.vue
async logout () {
  this.$confirm('确认要退出吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    // 清除token,跳转到登录页面
    this.$store.dispatch('user/logout')
  }).catch(() => {
    console.log('取消')
  })
}
  • 基于async函数进行优化
async logout () {
  const ret = await this.$confirm('确认要退出吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).catch(e => e)
  // 点击了取消操作
  if (ret === 'cancel') return
  // 点击了确定操作,触发action退出
  this.$store.dispatch('user/logout')
}

总结:

  1. 绑定事件
  2. 确认删除
  3. 清除token
  4. 清除用户信息
  5. 跳转到登录页面

注意:Promise中的catch方法return的值给了下一个then,catch方法之后可以继续链式操作。

处理token失效问题

目标: 实现token失效的处理

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

  • 拦截器处理token失效 src/utils/request.js
// 添加响应拦截器
import { removeToken } from './auth'
import router from '@/router'
instance.interceptors.response.use(res => {
  // res表示axios包装后的数据
  return res.data
}, (err) => {
  // 处理token失效的问题
  console.dir(err)
  if (err.response.status === 401) {
    // token已经失效;删除token;删除用户信息;跳转到登录页面
    store.commit('user/setToken', '')
    store.commit('user/updateUserInfo')
    removeToken()
    router.push('/login')
  }
  return Promise.reject(err)
})

总结:

  1. 判断token过期的情况
  2. 判断服务器失败的其他情况
  3. 在非组件环境触发mutation需要添加模块的前缀