HR-03-主页模块

156 阅读8分钟

主页模块

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

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

image.png
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;
        }
      }
    }
  }
}

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

主页布局-左侧图标

image.png

需求:定制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">
  HR人力资源管理平台
  <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 reqGetUserInfo() {
  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 ${store.getters.token}`
    }
  }
  return config
}, function(error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})
  • 前端解决跨域问题
// 在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: {
    updateUserInfo (state, payload) {
      // 初始化用户信息
      state.userInfo = payload
    }
 },
actions: {
    // 获取用户基本信息
    async getInfo (context) {
      // 调用接口获取数据
      const ret = await getInfo()
      // 初始化用户信息
      context.commit('updateUserInfo', 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,
  uname: state => state.user.userInfo.username // 建立用户名称的映射
}
export default getters
computed: {
  ...mapGetters([
    'sidebar',
    'avatar',
    'uname'
  ])
},
<span class="name">{{uname}}</span>

总结:

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


获取用户的头像

目标:获取用户头像信息

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

  • 封装获取用户信息接口  **src/api/user.js**
// 获取用户头像信息
export function getDetailInfo (id) {
  // 参数ID表示当前登录系统的用户id
  return request({
    method: 'get',
    url: '/sys/user/' + id
  })
}
import { login, getInfo, getDetailInfo } from '@/api/user.js'
updateUserInfo (state, payload) {
    // 初始化用户信息
    if (state.userInfo) {
        // 原来有值(合并两个对象的所有属性到一块)
        state.userInfo = {
            ...state.userInfo,
            ...payload
        }
    } else {
        // 第一次给他初始化
        state.userInfo = payload
    }
}
  • 为了页面中更好地获取头像,同样可以把头像放于getters中
avatar: state => state.user.userInfo.staffPhoto // 建立用户头像的映射
  • 展示头像**layout/components/Navbar.vue**
computed: {
  ...mapGetters([
    'sidebar',
    'avatar',
    'uname'
  ])
},
<img :src="avatar" class="user-avatar">
<span class="name">{{ name }}</span>

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

  1. 基于延展运算符合并对象的写法
  2. 上一个接口调用的结果作为下一个接口调用的参数(注意第一次调用的await是必要的)
  • 优化功能代码
  1. 封装一个独立的action:处理两个接口调用
    // 获取用户基本信息
    async getInfo (context) {
      // 调用接口获取用户基本数据
      const info = await getInfo()
      // 调用接口获取用户详细数据
      const detail = await getDetailInfo(info.data.userId)
      // 更新用户数据
      context.commit('updateUserInfo', {
        ...info.data,
        ...detail.data
      })
    }
  1. 处理mutation初始化操作
    updateUserInfo (state, payload) {
      state.userInfo = payload
    }
  1. 组件中触发action
  methods: {
    ...mapActions('user', ['getInfo']),
  }
  async created () {
  	this.getInfo()
  }

总结:action中可以处理多个异步接口调用(基于async函数)

处理头像失效问题

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

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

  • 关于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, 'defaultImg.png')

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

  • 注册自定义指令基本用法
Vue.directive('指令名称', {
    // 会在当前指令作用的dom元素 插入之后执行
    // el 指令所在dom元素
    // bindings 里面是指令的参数信息对象
    inserted(el, bindings) {
        
    }
})
  • 当图片有地址 但是地址没有加载成功的时候 会报错 会触发图片的一个事件 => onerror
/*
  封装Vue插件
*/
export default {
  install (Vue, options) {
    // 扩展一个自定义指令,处理图片加载失败的情况
    // <img v-imgerror='default.png' src="a.png" alt=""/>
    Vue.directive('imgerror', {
      // bindings包含指令相关的参数信息
      inserted (el, bindings) {
        console.dir(bindings)
        // 如何知道img标签图片加载失败了?
        el.onerror = () => {
          // 加载失败后触发该函数
          el.src = bindings.value || options
        }
      }
    })
  }
}
// main.js
// 导入插件
import MyPlugins from '@/utils/plugins.js'
// 配置插件
Vue.use(MyPlugins, 'default.png' )
  • 使用指令, 这里图片如果是用本地图片, 需要导入, 如果是完整地址的网图, 直接赋值即可
<img v-imgerror="defaultImg" :src="avatar" class="user-avatar">
// 基于ES6导入单独的图片也是可以的
import Img from '@/assets/common/head.jpg'
data() {
  return {
    defaultImg: Img
  }
},
// 或者直接完整地址的网图赋值
data() {
  return {
    defaultImg: 'https://www.baidu.com/img/flexible/logo/pc/result@2.png'
  }
},

总结:

  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'

// mutations重要的原则: 只能是同步的
mutations: {
  // 删除缓存的token
  removeToken (state) {
    state.token = ''
    removeToken()
  }
},
  • 头部菜单调用action  **src/layout/components/Navbar.vue**
async logout () {
  // 点击退出时需要提示是否退出
  this.$confirm('确认要退出吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    // 点击确定,执行then方法
    // 删除token,删除用户信息
    this.removeToken()
    this.updateUserInfo(null)
    // 跳转到登录页面
    this.$router.push('/login')
  }).catch(() => {
    // 点击取消,执行catch方法
    console.log('cancel')
  })
}

总结:

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

处理token失效问题

**目标**: 实现token失效的处理

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

  • 拦截器处理token失效 **src/utils/request.js**
// 响应拦截器
instance.interceptors.response.use(function(response) {
  // 对响应数据做点什么
  return response.data
}, function(error) {
  // 判断token是否失效
  if (error.response.status === 401 && error.response.data.code === 10002) {
    // token已经失效,删除用户信息,跳转到登录页面
    store.commit('user/removeToken')
    store.commit('user/updateUserInfo', {})
    router.push('/login')
  } else {
    // 不是401,也不是200,那么说明是其他错误,直接进行提示
    Message.error(error.response.message)
  }
  return Promise.reject(error)
})

总结:

  1. 判断token过期的情况
  2. 判断服务器失败的其他情况
  3. 在组件中触发mutation没有添加user前缀,因为映射时已经添加
  4. 在非组件环境触发mutation需要添加模块的前缀