# 主页模块

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

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

image-20210216132302394

# 主页布局-左侧菜单

目标:实现左侧菜单效果

  • 左侧导航样式处理 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;
        }
      }
    }
  }
}

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

注意:variables.scss文件的变量要进行替换

# 主页布局-左侧图标

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

  • 左侧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;
  }
}

总结:汉堡菜单;面包屑导航;右侧下拉菜单

注意: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-20210405174513239

# 回顾

  • 登录
    • 基本布局
    • 表单验证
    • 基于action方式调用接口实现登录
    • 封装本地存储的操作
    • 处理响应拦截器
    • 导航守卫控制
    • 控制密码的显示与否:$nextTick
    • 环境变量的用法:控制开发和生产环境采用不同的资源
  • 主页
    • 基本布局
      • 左侧导航
      • 顶部导航
    • 调用接口获取用户信息
    • 配置前端跨域的代理:Vue/cli提供的配置
    • 配置请求拦截器,统一添加请求头token
    • 理解跨域配置流程(这种方案仅仅用于开发阶段)

# vuex存储用户资料

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

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

  • 封装获取用户资料action action src/store/modules/user.js
import { reqLogin, getUserInfo } from '@/api/user'
actions: {
  // 获取用户数据
  async getUserInfo (context) {
    const ret = await reqGetUserInfo()
    context.commit('setUserInfo', ret.data)
  }
}
  • navBar 组件中调用
import { mapActions } from 'vuex'

created() {
  this.getUserInfo()
},
methods: {
  ...mapActions('user', ['getUserInfo']),
}
  • 提交mutation, 将用户信息存储到 vuex 中
const state = {
  token: getToken(), // 优先从cookie中读取
  userInfo: {}
}

// mutations重要的原则: 只能是同步的
const mutations = {
  // 设置token
  ...,
  setUserInfo(state, newUserInfo) {
    state.userInfo = newUserInfo
  }
}
  • 页面中使用
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
computed: {
  ...mapGetters([
    'sidebar',
    'avatar',
    'name'
  ])
},
<span class="name">{{name}}</span>

总结:

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

# 获取用户头像

目标:获取用户头像信息

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

  • 封装获取用户信息接口 src/api/user.js
// 获取用户头像信息
export function reqGetUserDetailById(id) {
  return request({
    url: `/sys/user/${id}`
  })
}
import { reqLogin, reqGetUserInfo, reqGetUserDetailById } from '@/api/user'
 
// 获取用户数据
async getUserInfo (context) {
  // 获取用户的基本信息
  const baseInfo = await reqGetUserInfo()
  // 根据用户id获取详细信息
  const detailInfo = await reqGetUserDetailById(baseInfo.data.userId)
  // 更新用户所有信息
  context.commit('setUserInfo', {
    ...baseInfo.data,
    ...detailInfo.data
  })
}
  • 为了页面中更好地获取头像,同样可以把头像放于getters中
avatar: state => state.user.userInfo.staffPhoto // 建立用户头像的映射
  • 展示头像**layout/components/Navbar.vue**
computed: {
  ...mapGetters([
    'sidebar',
    'avatar',
    'name'
  ])
},
<img :src="avatar" class="user-avatar">
<span class="name">{{ name }}</span>

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

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

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

# 处理头像失效问题

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

  • 注册自定义指令基本用法
    • v-model.lazy='uname'
    • lazy指令修饰符的作用:把v-model的默认事件input修改为change事件
      • change事件的触发条件是输入框失去焦点(内容需要有变化)
      • input事件的触发条件是文本发生变化
      • blur失去焦点触发
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.defaultImg
        }
      }
    })
  }
}
// main.js
// 导入插件
import MyPlugins from '@/utils/plugins.js'
// 配置插件
Vue.use(MyPlugins, { defaultImg: 'default.png' })
  • 使用指令, 这里图片如果是用本地图片, 需要导入, 如果是完整地址的网图, 直接赋值即可
<img v-imgerror="defaultImg" :src="avatar" class="user-avatar">
import Img from '@/assets/common/head.jpg'
data() {
  return {
    defaultImg: Img
  }
},
// 或者直接完整地址的网图赋值
data() {
  return {
    defaultImg: 'https://dss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2344451607,2404623174&fm=111&gp=0.jpg'
  }
},

总结:

  1. 自定义指令的基本规则
  2. 插件基本使用规则:先定义,再导入并配置
  3. 配置插件时,可以传递options选项
  4. 扩展图片加载的自定义指令
  5. 使用自定义指令

# 退出功能

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

image-20210216134315084

  • 退出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
methods: {
     ...mapMutations('user', ['setUserInfo', 'removeToken']),
     async logout() {
      // await this.$store.dispatch('user/logout')
      // this.$router.push(`/login?redirect=${this.$route.fullPath}`)
      // 删除用户的token和信息,跳转到登录页面
      this.removeToken()
      this.setUserInfo({})
      this.$router.push('/login')
    }
}

总结:

  1. 清除token
  2. 清除用户信息
  3. 跳转到登录页面

# 处理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/setUserInfo', {})
    router.push('/login')
  } else {
    // 不是401,也不是200,那么说明是其他错误,直接进行提示
    Message.error(error.response.message)
  }
  return Promise.reject(error)
})

总结:

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