Skip to content

一般的中后台管理系统都有着一个统一的页面布局,而且我们使用的element-plus组件库已经给我们提供了比较好的布局组件,我们要做的就很简单了。

一、实现页面布局

vue
<script setup lang="ts">
import Header from './components/header.vue'
</script>

<template>
  <div class="common-layout">
    <el-container>
      <el-header>
        <Header />
      </el-header>
      <el-container>
        <el-aside width="200px">
          <el-menu
            default-active="/"
            class="el-menu-vertical-demo"
            background-color="#545c64"
            router
          >
            <el-menu-item index="/">项目描述</el-menu-item>
            <el-menu-item index="/user">用户列表</el-menu-item>
            <el-menu-item index="role">角色列表</el-menu-item>
            <el-menu-item index="/auth">权限列表</el-menu-item>
          </el-menu>
        </el-aside>
        <el-main>
          <router-view />
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<style lang="less" scoped>
.common-layout {
  height: 100vh;
  .el-container {
    height: 100%;
  }
  .el-aside {
    background-color: #545c64;
  }
}
.el-header {
  padding: 0;
}
.el-menu {
  border-right: none;
}
</style>
vue
<template>
  <div class="header">
    <div class="flex-center">logo</div>
    <div class="flex-grow"></div>
    <div class="flex-center color-dark-black m05">
      <i-ep-user />
      {{ username }}
    </div>
    <div
      class="flex-center color-dark-black m05 setting"
      @click="handleSetting"
    >
      <i-ep-setting />
    </div>
  </div>
  <el-drawer v-model="setting" :show-close="false" :with-header="false">
    <div class="setting-header">
      <h2>项目配置</h2>
      <i-ep-close @click="setting = false" />
    </div>
    <div class="out">
      <el-button type="primary" @click="logout">退出</el-button>
    </div>
  </el-drawer>
</template>

<style lang="less" scoped>
.header {
  display: flex;
  align-items: center;
  padding: 0 15px;
  width: 100%;
  height: 60px;
}
.setting {
  cursor: pointer;
}
.setting-header {
  display: flex;
  justify-content: space-between;
  padding: 0 15px;
  height: 60px;
  color: var(--ry-color-black);
}
.out {
  padding: 15px;
  text-align: center;
  .el-button {
    width: 100%;
  }
}
</style>

通过以上代码,我们实现的页面基本布局如下所示: image.png 点击设置图标还会唤醒右侧的抽屉组件,在这个抽屉组件里我们可以点击退出 image.png

二、重写路由

2.1 重写路由配置

写完上面的布局之后,我们需要重新调整一下,路由的配置,让这个基本布局作为整个页面的根组件,其他页面都在这个页面中显示,具体的修改如下:

vue
export default {
  path: '/',
  name: 'Layout',
  component: () => import('@/layout/index.vue'),
  redirect: '/home',
  children: [
    {
      path: 'home',
      name: 'Home',
      component: () => import('@/views/home/index.vue'),
      meta: {},
    },
    {
      path: 'user',
      name: 'User',
      component: () => import('@/views/user/index.vue'),
      meta: {},
    },
    {
      path: 'role',
      name: 'Role',
      component: () => import('@/views/role/index.vue'),
      meta: {},
    },
    {
      path: 'auth',
      name: 'Auth',
      component: () => import('@/views/auth/index.vue'),
      meta: {},
    },
  ],
}

2.2 添加路由守卫

在用户登录之后,我们应该给路由添加一些权限,只有登录过的用户才能访问,没有登录过的一律要返回登录页进行登录,在vue中vue-router给我们提供了路由守卫的钩子函数beforeEach和afterEach,在这里我们使用beforeEach这个钩子来实现路由守卫

typescript
const whiteList = ['/login', '/404']
// @ts-expect-error from is optional
router.beforeEach((to, from, next) => {
  NProgress.start()
  const token = sessionStorage.getItem('user')
  if (whiteList.includes(to.path) || token) {
    next()
  } else {
    next('/login')
  }
})

在路由守卫中我们获取登录时就已经持久化在本地的token值,根据token是否有值来决定是否放行,除此之外,我们还可以添加一些路由白名单,不需要经过路由守卫的检测用户就能进入,比如登录页等。

2.3 使用store修复刷新时左侧菜单啊选中项丢失的问题

element-plus组件库的el-menu组件给我们提供了一个属性default-active,它表示菜单的默认选中项,除此之外,还给我们提供了一个事件select,因此,我们可以利用这两个东西以及store修复刷新时选中项丢失的问题。

vue
<el-menu
  :default-active="path"
  class="el-menu-vertical-demo"
  background-color="#545c64"
  router
  @select="handleSelect"
>
  <el-menu-item index="/">项目描述</el-menu-item>
  <el-menu-item index="/user">用户列表</el-menu-item>
  <el-menu-item index="role">角色列表</el-menu-item>
  <el-menu-item index="/auth">权限列表</el-menu-item>
</el-menu>
typescript
import { defineStore } from 'pinia'
import { IUserState } from '@/store/user/type'
import { loginRequest } from '@/api/user/type'
import { userLogin } from '@/api/user/user'
import { ElMessage } from 'element-plus'

const useUserStore = defineStore('user', {
  state: (): IUserState => ({
    username: '',
    accessToken: '',
    refreshToken: '',
    roles: [],
    path: '/',
  }),
  actions: {
    async userLoginAction(data: loginRequest) {
      const userLoginResult = await userLogin(data)
      if (userLoginResult.code === 0) {
        this.username = userLoginResult.data.username
        this.accessToken = userLoginResult.data.accessToken
        this.roles = userLoginResult.data.roles
        ElMessage.success(userLoginResult.message)
      } else {
        ElMessage.error(userLoginResult.message)
      }
    },
    setPath(path: string) {
      this.path = path
    },
  },
  persist: {
    key: 'user',
    paths: ['accessToken', 'path'],
    storage: sessionStorage,
  },
})

export default useUserStore

因为store也存在刷新时丢失的问题,我们需要用到持久化存储,并且因为继承了pinia-plugin-persistedstate插件,因此只需要在persist对象的paths属性添加我们要持久化存储的路径数据path了。还有,我在actions里写了一个方法来修改保存在state中的path。

vue
<script setup lang="ts">
  import Header from './components/header.vue'
  import useUserStore from '@/store/user'
  import { storeToRefs } from 'pinia'

  const userStore = useUserStore()
  const { path } = storeToRefs(userStore)

  const handleSelect = (index: string) => {
    userStore.setPath(index)
  }
</script>

通过以上代码,就可以实现路径的永久保存了,这样用户在刷新也不会丢失了。

三、实现退出功能

vue
<script setup lang="ts">
import { ref } from 'vue'
import useUserStore from '@/store/user'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'

const setting = ref(false)
const userStore = useUserStore()
const { username } = storeToRefs(userStore)
const router = useRouter()
const handleSetting = () => {
  setting.value = true
}
const logout = () => {
  sessionStorage.removeItem('user')
  router.push('/login')
}
</script>