# 二、Layout 处理

## 创建首页组件并配置路由

1、创建 `src/views/home/index.vue`

```html
<template>
  <div class="home-container">首页</div>
</template>

<script>
export default {
  name: 'HomeIndex',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped lang="less"></style>

```

2、然后在路由表中

<img src="assets/image-20200421172340709.png" alt="image-20200421172340709" style="zoom:50%;" />

3、登录成功,跳转到首页测试



## 创建 Layout 组件并配置路由

1、创建 `src/views/layout/index.vue`

```html
<template>
  <div class="layout-container">
    <div>顶部导航栏</div>

    <div>侧边导航栏</div>

    <!-- 子路由出口 -->
    <router-view />
  </div>
</template>

<script>
export default {
  name: 'LayoutIndex',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped lang="less"></style>
```

2、配置 layout 路由

<img src="assets/image-20200421172555035.png" alt="image-20200421172555035" style="zoom:50%;" />

3、最后测试

## 使用 Container 布局容器 搭建页面结构

> 参考文档:[Container 布局容器](https://element.eleme.cn/#/zh-CN/component/container)

```html
<template>
  <el-container class="layout-container">
    <el-aside
      class="aside"
      width="200px"
    >Aside</el-aside>
    <el-container>
      <el-header class="header">Header</el-header>
      <el-main class="main">
        <!-- 子路由出口 -->
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<script>
export default {
  name: 'LayoutIndex',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped lang="less">
.layout-container {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
}

.aside {
  background-color: #d3dce6;
}

.header {
  background-color: #b3c0d1;
}

.main {
  background-color: #e9eef3;
}
</style>
```



## 处理侧边栏导航菜单

1、创建 `src/views/layout/components/aside.vue`

```html
<template>
  <!--
    el-menu-item 的 index 不能重复,确保唯一即可
   -->
  <el-menu
    class="nav-menu"
    default-active="/"
    background-color="#002033"
    text-color="#fff"
    active-text-color="#ffd04b"
    router
  >
    <el-menu-item index="/">
      <i class="el-icon-s-home"></i>
      <span slot="title">首页</span>
    </el-menu-item>
    <el-menu-item index="/article">
      <i class="el-icon-document"></i>
      <span slot="title">内容管理</span>
    </el-menu-item>
    <el-menu-item index="/image">
      <i class="iconfont iconimage"></i>
      <span slot="title">素材管理</span>
    </el-menu-item>
    <el-menu-item index="/publish">
      <i class="iconfont iconpublish"></i>
      <span slot="title">发布文章</span>
    </el-menu-item>
    <el-menu-item index="/comment">
      <i class="iconfont iconcomment"></i>
      <span slot="title">评论管理</span>
    </el-menu-item>
    <el-menu-item index="/fans">
      <i class="el-icon-setting"></i>
      <span slot="title">粉丝管理</span>
    </el-menu-item>
    <el-menu-item index="/settings">
      <i class="el-icon-setting"></i>
      <span slot="title">个人设置</span>
    </el-menu-item>
  </el-menu>
</template>

<script>
export default {
  name: 'AppAside',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped lang="less">
.nav-menu {
  .iconfont {
    margin-right: 10px;
    padding-left: 5px;
  }
}
</style>

```

2、在 layout 中加载使用侧边栏导航菜单组件

<img src="assets/image-20200421173310913.png" alt="image-20200421173310913" style="zoom:50%;" />



## 处理页面顶栏

1、创建 `src/views/layout/components/header.vue` 组件

```html
<template>
  <div class="header-container">
    <div>
      <i class="el-icon-s-fold"></i>
      <span>江苏传智播客科技教育有限公司</span>
    </div>
    <el-dropdown>
      <div class="avatar-wrap">
        <img class="avatar" src="http://toutiao.meiduo.site/FrvifflobfNNRM9V_ZBTI2ZaTH4n" alt="">
        <span>用户昵称</span>
        <i class="el-icon-arrow-down el-icon--right"></i>
      </div>
      <el-dropdown-menu slot="dropdown">
        <el-dropdown-item>设置</el-dropdown-item>
        <el-dropdown-item>退出</el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script>
export default {
  name: 'AppHeader',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped lang="less">
.header-container {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid #ccc;
  .avatar-wrap {
    display: flex;
    align-items: center;
    .avatar {
      width: 30px;
      height: 30px;
      border-radius: 50%;
      margin-right: 10px;
    }
  }
}
</style>

```

2、然后在 layout 中加载使用

<img src="assets/image-20200421174653839.png" alt="image-20200421174653839" style="zoom:50%;" />

## 在顶栏中展示当前登录用户

1、在 `api/user.js` 中封装请求方法

```js
// 获取用户信息
export const getUserProfile = () => {
  return request({
    method: 'GET',
    url: '/mp/v1_0/user/profile',
    // 后端要求把需要授权的用户身份放到请求头中
    // axios 可以通过 headers 选项设置请求头
    headers: {
      // 属性名和值都得看接口的要求
      // 属性名:Authorization,接口要求的
      // 属性值:Bearer空格token数据
      Authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTg5MDkxMjYsInVzZXJfaWQiOjEsInJlZnJlc2giOmZhbHNlLCJ2ZXJpZmllZCI6dHJ1ZX0.EdKErKDqMc3snkYxqt02jSa8t9G44002yWKY3CMOMJg'
    }
  })
}
```

2、在 header 组件中请求获取数据

<img src="assets/image-20200421174841178.png" alt="image-20200421174841178" style="zoom:50%;" />

3、把请求到的数据绑定到模板中

<img src="assets/image-20200421175004383.png" alt="image-20200421175004383" style="zoom:50%;" />

## Token 处理

1、在登录成功以后将用户信息存储到本地存储

<img src="assets/image-20200422120805576.png" alt="image-20200422120805576" style="zoom:50%;" />

> 注意:代码是不折行的
>
> 本地存储只能存字符串,如果需要存储数组或者对象数据,则转为 JSON 格式字符串

2、然后在请求的时候获取本地存储中的 user 数据使用 token

<img src="assets/image-20200422120925785.png" alt="image-20200422120925785" style="zoom:50%;" />

## 使用拦截器统一设置用户 Token

> axios 拦截器官方示例:https://github.com/axios/axios#interceptors

在 request 请求模块中添加如下代码:

```js
// 请求拦截器
request.interceptors.request.use(
  // 任何所有请求会经过这里
  // config 是当前请求相关的配置信息对象
  // config 是可以修改的
  function (config) {
    const user = JSON.parse(window.localStorage.getItem('user'))

    // 如果有登录用户信息,则统一设置 token
    if (user) {
      config.headers.Authorization = `Bearer ${user.token}`
    }

    // 然后我们就可以在允许请求出去之前定制统一业务功能处理
    // 例如:统一的设置 token

    // 当这里 return config 之后请求在会真正的发出去
    return config
  },
  // 请求失败,会经过这里
  function (error) {
    return Promise.reject(error)
  }
)
```

## 处理侧边菜单的展开/收起状态

1、在 layout 组件中声明数据用来控制侧边导航菜单的展开收起状态

<img src="assets/image-20200422121315411.png" alt="image-20200422121315411" style="zoom:50%;" />

2、在 layout 组件中处理图标的点击状态

<img src="assets/image-20200422121352145.png" alt="image-20200422121352145" style="zoom:50%;" />

3、将 layout 组件中的 `isCollapse` 传递给侧边栏组件

<img src="assets/image-20200422121446475.png" alt="image-20200422121446475" style="zoom:50%;" />

> 别忘了把 el-aside 的 width 设置为 auto

4、在 aside 组件中声明接收 props 数据并绑定到导航菜单组件中

<img src="assets/image-20200422121540402.png" alt="image-20200422121540402" style="zoom:50%;" />

## 控制页面访问权限

在我们的项目中,除了登录页面,其它所有页面都需要具有登录状态才能访问。也就是说我们要给这些需要登录才能访问的页面进行统一控制。

通常的做法就是利用[路由的导航守卫]([https://router.vuejs.org/zh/guide/advanced/navigation-guards.html)来统一处理。

所谓的路由拦截器就是一个公共的页面访问门卫,说白了就是所有的页面访问都要经过这里,我们可以在这里执行一共公共的操作,例如校验是否具有登录状态。

> 提示:官方文档叫导航守卫,都是一个意思。



具体做法就是在 `src/router/index.js` 中:

```js
// 路由导航守卫:说白了所有页面的导航都会经过这里
// 守卫页面的导航的
// to:要去的路由信息
// from:来自哪里的路由信息
// next:放行方法
router.beforeEach((to, from, next) => {
  // 如果要访问的页面不是 /login,校验登录状态
  // 如果没有登录,则跳转到登录页面
  // 如果登录了,则允许通过
  // 允许通过
  // next()

  const user = JSON.parse(window.localStorage.getItem('user'))

  // 校验非登录页面的登录状态
  if (to.path !== '/login') {
    if (user) {
      // 已登录,允许通过
      next()
    } else {
      // 没有登录,跳转到登录页面
      next('/login')
    }
  } else {
    // 登录页面,正常允许通过
    next()
  }
})
```

> 关于路由导航守卫更详细的用户请参考官方文档:[https://router.vuejs.org/zh/guide/advanced/navigation-guards.html

## 结合导航守卫实现页面切换顶部进度条

- [nprogress](https://github.com/rstacruz/nprogress)
- 路由前置钩子
- 路由后置钩子

1、安装 nprogress

```bash
# yarn add nprogress
npm i nprogress
```

> 注意:项目中不要乱用包管理工具,要从一而终,不要一会儿这个,一会儿那个的。否则的话会导致一些包被莫名删除。
>
> 提示:如果想要从一个包管理工具切换到另一个包管理工具:
>
> 1、手动删除 node_modules
>
> 2、执行 `npm install` 或者 `yarn install` 或者 `cnpm install` 把所有依赖项重新安装一遍
>
> 3、之后固定使用 npm、yarn、cnpm 来装包
>
> 注意:cnpm 就不建议使用了。

2、在 `main.js` 中引入 `nprogress.css` 样式文件

```js
// 加载 nprogress 中的指定的样式文件
// 注意:加载第三方包中的具体文件不需要写具体路径,直接写包名即可
// 总结就是:"包名/具体文件路径"
import "nprogress/nprogress.css";
```

3、在路由的全局前置守卫中,开启进度条

```js
...
+ import NProgress from 'nprogress'


router.beforeEach((to, from, next) => {
  // 开启顶部导航进度条特效
+  NProgress.start()

  // 停止导航
  // 我们可以在一些特殊情况下,停留在当前页面,中断当前导航
  // next(false)

  // next()

  // 1. 如果访问的是登录页面,则直接放行
  if (to.path === '/login') {
    next()

    // 停止代码往后执行
    return
  }

  // 2. 非登录页面,校验登录状态

  // 2.1 获取用户 token
  const token = window.localStorage.getItem('user-token')

  // 2.2 判断是否有 token,有就通过
  if (token) {
    // 导航通过,放行,访问哪里就往哪里走
    next()
  } else {
    // 2.3 没有,就跳转到登录页
    next('/login') // 跳转到指定路由
  }
})
```

4、在路由的全局后置钩子中,关闭进度条特效

```js
router.afterEach((to, from) => {
  // 结束顶部的导航进度条
  NProgress.done();
});
```

最后,回到浏览器中测试访问。

## 用户退出

1、给退出按钮注册点击事件

<img src="assets/image-20200422172829358.png" alt="image-20200422172829358" style="zoom:50%;" />

> 注意:并不是所有的组件在注册事件的时候需要使用 `.native` 修饰符,例如 el-button 组件注册点击事件就不需要,这主要是因为该组件内部处理了。
>
> 什么时候使用 `.native`?首先肯定是在组件上注册事件可能会用到,如果普通方式注册不上,这个时候加 `.native` 修饰符。
>
> 例如你给一个组件注册一个 `input` 事件,如果直接 `@input` 注册无效,那就试一下 `@input.native`。

2、处理函数如下

```js
onLogout () {
  this.$confirm('确认退出吗?', '退出提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    // 把用户的登录状态清除
    window.localStorage.removeItem('user')

    // 跳转到登录页面
    this.$router.push('/login')
  }).catch(() => {
    this.$message({
      type: 'info',
      message: '已取消退出'
    })
  })
}
```

最后,回到浏览器测试。