一个半吊子的前端搭建用vue + element ui搭建的管理员后台

鄙人做服务端的,想学学前端,顺便用vue + element ui搭了一个管理员后台。

在这里参考了官网提供的管理员后台模板vue-elemetn-admin
https://panjiachen.github.io/vue-element-admin-site/zh/guide/
https://github.com/PanJiaChen/vue-element-admin
官网的,自然更好看,功能更强大, 我做的只能算是超级阉割版,主要是为了学习。
我做这个管理员后台还有一个原因,权限问题。
https://juejin.cn/post/6844903478880370701

注意:这篇文章还是要一点基础才能看得懂的

先说说官方的权限与左侧菜单的思路。菜单与路由关联的,路由常亮加路由变量(登录后根据用户角色计算出的),菜单项与用户角色挂钩,拥有这个角色的用户才能看到这个菜单。
这里是维护了一个全量的异步路由表,需要权限判断的菜单项都与角色关联,用户登录成功后根据用户角色过滤这个全量的路由表,再动态挂在到路由上。这里就出现了一个比较严重的问题:正常情况下,一个平台或者系统不可能只有一两个权限或者角色,根据官方的这种思路,新增角色的话,就需要修改这个全量的异步路由表(JS里面),这样就显得不灵活了,而且不满足需要。所以我就自己做了一个管理员后台,从服务端加载用户有权限的路由,再异步挂载。

先说说我用户、角色、权限的实现思路,这篇文章不包括服务端相关内容,所有的数据我都是在js里面写死的,到时候对接服务端的时候需要稍作修改。
一个用户有多个角色,一个角色有多个权限,用户与权限不直接关联。权限与路由绑定,角色授权权限,用户授权角色,这样就能获取到用户拥有的权限了。

这里一样的保留了登录,可以跟oauth2的密码模式很好的结合。

首先简单看一下相关的版本,主要是vue3 + element-plus

{
  "name": "front-end",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "element-plus": "^1.0.2-beta.44",
    "js-cookie": "^2.2.1",
    "nprogress": "^0.2.0",
    "vue": "^3.0.0",
    "vue-router": "^4.0.0-0",
    "vuex": "^4.0.0-0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "^4.5.13",
    "@vue/cli-plugin-vuex": "^4.5.13",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

基本布局与说明

首先看一下布局
一个半吊子的前端搭建用vue + element ui搭建的管理员后台_第1张图片
最外层容器
左侧el-aside:左侧菜单栏
上方el-header:收缩、展开按钮,面包屑导航,搜索,头像下拉
主要,el-main:选项卡

再看代码结构,没有什么花里胡哨的,我是从零开始搭建的
一个半吊子的前端搭建用vue + element ui搭建的管理员后台_第2张图片

注意layout文件夹
dashboard:首页
NavBreadcrumb:面包屑导航
NavHeader:头
NavMenu:左侧菜单
NavTab:选项卡

左侧菜单

首先,服务端会生成一个树,需要还用这个解析树生成路由对象并且添加到异步路由。后台维护的component是字符串,这里需要解析为组件。
由于这个树可能会有很多层级,所以把el-menu-item单独抽出来生成一个组件,可以递归遍历树。







使用组件


这里遇到过一个bug,就是在menu-item外面包一层div,会出现收缩的时候效果不理想,F12调试会发现,左侧菜单内容与正常的菜单内容不一样。

展开、搜索

这是个状态保存在vuex里面,左侧菜单根据这个状态展开还是搜索
这个按钮


        
        
        

isCollapse 直接从状态里面获得

computed: {
	isCollapse() {
    	return this.$store.getters.isCollapse
    }
},
methods: {
	fold(isCollapse) {
	  // 更新状态
      this.$store.dispatch('navMenu/toggleCollapse', isCollapse)
    }
}

面包屑导航

这里是只读的,这样简单,充当一个展示效果







搜索


下拉


          

选项卡

这里需要注意,选项卡头部要定格在最上方,不能与内容一起滚动,这里是用css样式控制的。

全部源码

代码里面备注写的很多了,应该能看懂的。

  • vue.config.js
const path = require('path')

function resolve(dir) {
  return path.join(__dirname, dir)
}

module.exports = {
  publicPath: '/',
  devServer: {
    port: 9527,
    open: false
  },
  configureWebpack: {
    // 设置标题
    name: 'Admin Template',
    resolve: {
      alias: {
        '@': resolve('src')
      }
    }
  }
}

  • package.json
{
  "name": "front-end",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "element-plus": "^1.0.2-beta.44",
    "js-cookie": "^2.2.1",
    "nprogress": "^0.2.0",
    "vue": "^3.0.0",
    "vue-router": "^4.0.0-0",
    "vuex": "^4.0.0-0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "^4.5.13",
    "@vue/cli-plugin-vuex": "^4.5.13",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

  • src/layout/index.vue






  • src/layout/NavBreadcrumb/index.vue






  • src/layout/NavHeader/index.vue






  • src/layout/NavMenu/index.vue






  • src/layout/NavMenu/NavMenuItem/index.vue






  • src/layout/NavTab/index.vue






  • src/store/index.js
import { createStore } from 'vuex'
import navMenu from './modules/navMenu'
import permission from './modules/permission'
import user from './modules/user'
import tabItem from './modules/tabItem'
import getters from './getters'

export default createStore({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    navMenu,
    permission,
    user,
    tabItem
  },
  getters
})

  • src/store/getters.js

const getters = {
  isCollapse: state => state.navMenu.isCollapse, // 左侧菜单是否展开
  routers: state => state.permission.routers, // 路由
  openTabs: state => state.tabItem.openTabs, // 选项卡
  activeItem: state => state.tabItem.activeItem // 当前选中菜单和选项卡,路径
}
export default getters

  • src/modules/navMenu.js
const state = {
  isCollapse: false
}

const mutations = {
  TOGGLE_COLLAPSE: (state, isCollapse) => {
    state.isCollapse = isCollapse
  }
}

const actions = {
  toggleCollapse({ commit }, isCollapse) {
    commit('TOGGLE_COLLAPSE', isCollapse)
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

  • src/store/modules/permission.js
/*
* 权限、路由相关
*/

import Layout from '@/layout'

const state = {
  routers: []
}

const mutations = {
  // 设置路由
  SET_ROUTER: (state, routers) => {
    state.routers = routers
  }
}

const actions = {
  // 清空路由
  clearRouter: ({ commit }) => {
    commit('SET_ROUTER', [])
  },
  // 生成路由
  generateRoute({ commit }) {
    return new Promise((resolve, reject) => {
      // TODO 自定义父子树比较组装复杂,由服务端生成
      const asyncRoute = [
        {
          name: 'SystemManage', // 唯一名称,与选项卡name对应
          title: '系统管理', // 菜单标题,与选项卡标题对应@deprecated
          path: '/system', // 路径
          // redirect: '/dashboard', // 重定向路径,TODO 不需要重定向,只有最后一个路由才会调转
          // component: '../../layout', // 对应组件路径,为空表示一级路由,以都放在src下面,记得以"/"开始,TODO 这里还需要解析一次生成对应的组件
          meta: {
            hidden: false, // 是否隐藏,添加页面、编辑页面等,默认不隐藏
            icon: 'el-icon-plus', // 图标,针对一级路由生效
            title: '系统管理' // 选项卡和菜单展示名称
          },
          // 子路由
          children: [
            {
              name: 'DemoManage',
              title: '测试管理',
              path: 'demo/manage',
              component: '/demo/manage/index',
              meta: {
                title: '系统管理'
              }
            },
            {
              name: 'DemoAdd',
              title: '测试添加',
              path: 'demo/add',
              component: '/demo/add/index',
              meta: {
                hidden: true,
                title: '测试添加'
              }
            },
            {
              name: 'DemoEdit',
              title: '测试编辑',
              path: 'demo/edit',
              component: '/demo/edit/index',
              meta: {
                hidden: true,
                title: '测试编辑'
              }
            }
          ]
        },
        {
          name: 'nested',
          path: '/nested',
          meta: {
            icon: 'el-icon-plus',
            title: '一级嵌套路由'
          },
          children: [
            {
              name: 'nested1',
              path: 'nested1',
              component: '/nested/index-1',
              meta: {
                title: '一级嵌套路由1'
              },
              children: [
                {
                  name: 'nested1-1',
                  path: 'nested1-1',
                  component: '/nested/nested/index-1-1',
                  meta: {
                    title: '二级嵌套路由1'
                  }
                },
                {
                  name: 'nested1-2',
                  path: 'nested1-2',
                  component: '/nested/nested/index-1-2',
                  meta: {
                    title: '二级嵌套路由2',
                    hidden: false
                  }
                }
              ]
            },
            {
              name: 'nested2',
              path: 'nested2',
              component: '/nested/index-2',
              meta: {
                title: '一级嵌套路由2'
              }
            }
          ]
        }
      ]

      // 状态保存
      commit('SET_ROUTER', asyncRoute)
      // todo 解析
      resolve(analyseRoute(asyncRoute))
    })
  }
}

/**
 * 导入vue组件
 * @param file
 * @returns {function(): *}
 * @private
 */
function _import(file) {
  return () => import('@/views' + file + '.vue')
}

/**
 * 路由树生成路由对象,component由字符串变为组件
 * @param routes
 */
function analyseRoute(routes) {
  // 最终生成的路由
  const result = []
  // 递归
  recurrenceRoute(result, routes)
  return result
}

/**
 * 递归
 * @param result 返回结果
 * @param routes 数组
 */
export function recurrenceRoute(result, routes) {
  routes.forEach(route => {
    const temp = {
      name: route.name,
      path: route.path,
      meta: {},
      component: route.component ? _import(route.component) : Layout
    }
    if (route.meta) {
      if (route.meta.icon) {
        temp.meta.icon = route.meta.icon
      }
      if (route.meta.hidden !== undefined) {
        temp.meta.hidden = route.meta.hidden
      }
      if (route.meta.title) {
        temp.meta.title = route.meta.title
      }
    }
    if (route.children) {
      temp.children = []
      recurrenceRoute(temp.children, route.children)
    }
    result.push(temp)
  })
}

/**
 * 解析面包屑数组
 * @param routers 路由直接获取
 * @param activeMenu 当前激活的菜单,路径
 */
export function analyseBreadcreumb(routers, activeMenu) {
  // 面包屑导航
  const array = []
  while (activeMenu && activeMenu.length > 0) {
    // 查找路由
    const route = searchByActiveMenu(routers, activeMenu)
    if (route && route.meta && route.meta.title) {
      array.unshift(route.meta.title)
    }
    activeMenu = activeMenu.substr(0, activeMenu.lastIndexOf('/'))
  }
  return array
}

/**
 * 检索第一条符合条件的路由
 * @param routers
 * @param activeMenu
 */
function searchByActiveMenu(routers, activeMenu) {
  for (let i = 0; i < routers.length; i++) {
    if (routers[i].path === activeMenu) {
      return routers[i]
    }
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

  • src/store/modules/tabItem.js
/*
* 选项卡
*/
const state = {
  openTabs: [], // 选项卡已有数据,【首页】为第一条
  activeItem: '' // 当选选中的选项卡,路径
}

const mutations = {
  /**
   * 添加选项卡,如果选项卡存在(根据路径确定),则选中该选项卡,不存在则添加
   * {
   *     name: '', 名称
   *     path: '/dashboard',  # 路由访问路径, 这个绝对路径
   *     title: ''   # 标题,选项卡展示的那个
   * }
   * @param state
   * @param item
   * @constructor
   */
  ADD_ITEM: (state, item) => {
    // 已有选项卡名称
    const pathes = state.openTabs.map(tab => tab.path)
    // 判断选项卡是否已存在
    if (pathes.indexOf(item.path) === -1) {
      // 选项卡不存在,添加
      state.openTabs.push(item)
    }
    // 选中菜单
    state.activeItem = item.path
  },
  // 设置当前选中的选项卡名称
  SET_ACTIVE_ITEM: (state, activeItem) => {
    state.activeItem = activeItem
  },
  // 根据index删除选项卡
  DELETE_ITEM: (state, index) => {
    // 删除
    state.openTabs.splice(index, 1)
  },
  // 重置选项卡
  RESET_TABS: (state) => {
    state.openTabs = []
    state.activeMenu = ''
  }
}

const actions = {
  // 添加选项卡
  addItem({ commit }, item) {
    return new Promise(resolve => {
      commit('ADD_ITEM', item)
      resolve(item.path)
    })
  },
  // 设置选项卡选中
  setActive({ commit }, activeItem) {
    commit('SET_ACTIVE_ITEM', activeItem)
  },
  // 删除选项卡,根据名称删除
  deleteItem({ commit, state }, path) {
    return new Promise((resolve, reject) => {
      for (let i = 0; i < state.openTabs.length; i++) {
        if (state.openTabs[i].path === path) {
          // 删除选项卡
          commit('DELETE_ITEM', i)
          resolve(state.openTabs[i - 1].path)
        }
      }
    })
  },
  // 重置选项卡
  resetTabs({ commit }) {
    commit('RESET_TABS')
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

  • src/store/modules/user.js
/*
* 用户基本操作
*/

import { removeToken, setToken } from '../../utils/auth'

const state = {
  // 头像
  avatar: '',
  // 用户名
  username: ''
}

const mutations = {
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  },
  SET_USERNAME: (state, username) => {
    state.username = username
  }
}

const actions = {
  // 登录操作, username + password,登录成功获取access_token并保存在cookie里面
  login({ commit }, userinfo) {
    const { username, password } = userinfo
    return new Promise((resolve, reject) => {
      // TODO 登录查询
      console.log('login username', username)
      console.log('login password', password)
      if (username !== 'admin') {
        reject('用户名密码错误')
      } else {
        setToken('this_is_admin_token')
        console.log('设置token')
        resolve()
      }
    })
  },
  // 重置token,主要是删除cookie里面的token
  resetToken({ commit }) {
    return new Promise(resolve => {
      // 清空cookie里面token
      removeToken()
      // 清空头像
      commit('SET_AVATAR', '')
      // 清空用户名
      commit('SET_USERNAME', '')
      resolve()
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

  • src/utils/auth.js
/**
 * cookie存取用户登录token
 */
import Cookies from 'js-cookie'

const accessToken = 'access_token'

export function getToken() {
  return Cookies.get(accessToken)
}

export function setToken(token) {
  return Cookies.set(accessToken, token)
}

export function removeToken() {
  return Cookies.remove(accessToken)
}

  • src/views/login/index.vue






由于遇到的问题很多,这里就没有意义描述了,这里贴出了全部源码

有需要的联系我的163邮箱,见url地址
或者到csdn下载https://download.csdn.net/download/admin_15082037343/18785160

你可能感兴趣的:(管理员后台模板,vue3,element-plus)