bunny笔记| VUE2前端实现后台管理系统的要点理解

一、理解前后端权限的区别

后端:后端权限可以控制某个用户是否能够查询数据,是否能够修改数据等操作
前端:前端仅视图层的展示权限的核心是在于服务器中的数据变化
所以后端才是权限的关键

简单了解后端吧

1.后端如何知道该请求是哪个用户发过来的

cookie
session
token

2.后端的权限设计使用RBAC

用户
角色
权限工

二、前端的权限的意义

1.降低非法操作的可能性
2.尽可能排除不必要请求减轻服务器压力
3.提高用户体验

三、前端的权限控制常见的分类

菜单的控制
界面的控制
按钮的控制
请求和响应的控制

四、vue的权限控制实现前的准备工作

推荐下载vue-admin-template模板 打开文件,如下图所示:

image.png

目录结构说明

├── build # 构建相关
├── mock # 项目mock 模拟数据
├── plop-templates # 基本模板
├── public # 静态资源
│ │── favicon.ico # favicon图标
│ └── index.html # html模板
├── src # 源代码
│ ├── api # 所有请求
│ ├── assets # 主题 字体等静态资源
│ ├── components # 全局公用组件
│ ├── directive # 全局指令
│ ├── filters # 全局 filter
│ ├── icons # 项目所有 svg icons
│ ├── lang # 国际化 language
│ ├── layout # 全局 layout
│ ├── router # 路由
│ ├── store # 全局 store管理
│ ├── styles # 全局样式
│ ├── utils # 全局公用方法
│ ├── vendor # 公用vendor
│ ├── views # views 所有页面
│ ├── App.vue # 入口页面
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── permission.js # 权限管理
├── tests # 测试
├── .env.xxx # 环境变量配置
├── .eslintrc.js # eslint 配置项
├── .babelrc # babel-loader 配置
├── .travis.yml # 自动化CI配置
├── vue.config.js # vue-cli 配置
├── postcss.config.js # postcss 配置
└── package.json # package.json

跨域配置vue.config.js 中配置,如图:

image.png

说明:默认的模板中,使用的是mockjs,进一步了解mockjs官方对mockjs的解释有以下4点:

1.前后端分离
2.不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据
3.数据类型丰富
4.通过随机数据,模拟各种场景

看一个项目首先要从入口文件开始看,即:main.js和App.vue

main.js程序入口文件 初始化vue实例 并引入使用需要的插件和各种公共组件
[ new Vue()是新创建的实例 el是为实例提供挂载元素 ]

App.vue项目的主组件/页面入口文件 ,所有页面都在App.vue下进行切换,app.vue负责构建定义及页面组件归集。

这里就不做过多解释了

五、理解permission.js文件

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // determine whether the user has logged in
  const hasToken = getToken()

  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done()
    } else {
      const hasGetUserInfo = store.getters.user
      if (hasGetUserInfo) {
        next()
      } else {
        try {
          // get user infob
          await store.dispatch('user/getInfo').then(res => {                        //触发获取用户信息到接口
            store.dispatch('permission/generateRoutes', res).then(() => {           //触发获取路由表的接口
              router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
              next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
            })
          })

          next()
        } catch (error) {
          // remove token and go to login page to re-login
          await store.dispatch('user/resetToken')  // 触发vuex中  resetToken => src/store/user.js 的 resetToken函数 清除token
          Message.error(error.message || 'Has Error') // 弹出异常
          next(`/login?redirect=${to.path}`)
          // 然后就执行这里 跳转到 login  redirect把从哪个页面出错的 做重定向的 => view/login/index.js的handleLogin函数
          // =>src/store/user.js 的login函数
          NProgress.done()
        }
      }
    }
  } else {
    /* has no token*/

    if (whiteList.indexOf(to.path) !== -1) {
      // in the free login whitelist, go directly
      next()
    } else {
      // other pages that do not have permission to access are redirected to the login page.
      //其他没有访问权限的页面将重定向到登录页面
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

六、理解request.js文件,理解axios的拦截请求/响应原理

import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // 跨域请求时发送cookie
  timeout: 5000 // 请求超时
})
// 请求拦截器
service.interceptors.request.use(
  config => {
    // do something before request is sent
    console.log(config)
    if (store.getters.token) {
      // 让每个请求都带有token
      // ['AdminToken'] is a custom headers key
      // 根据实际情况自行修改
      config.headers['AdminToken'] = getToken()
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)
// 响应拦截器
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
   */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * 根据HTTP状态码来判断code
   */
  response => {
    const res = response.data
    // if the custom code is not 20000, it is judged as an error.
    if (res.code !== 1) {
      Message({
        message: res.msg || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
      if (res.code == 1101) {
        // to re-login
        MessageBox.confirm('登录已失效,请重新登录!', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
            //Location.reload() 方法用来刷新当前页面。该方法只有一个参数,当值为 true 时,将强制浏览器从服务器加载页面资源,
            //当值为 false 或者未传参时,浏览器则可能从缓存中读取页面。
          })
        })
      }
      return Promise.reject(new Error(res.msg || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)
export default service

七、理解并编辑router/index.js的文件 ,动态路由渲染侧边栏

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
//布局
import Layout from '@/layout'
export const constantRouterMap = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/views/dashboard/index'),
        meta: { title: '控制面板', icon: 'dashboard' }
      }
    ]
  }
]

export const asyncRouterMap = [
  {
    path: '/permission',
    component: Layout,
    name: 'permission',
    meta: {
      title: '权限管理',
      permissions: ['roleRead', 'roleEdit', 'adminUserRead', 'adminUserEdit'],
      icon: 'el-icon-s-order'
    },
    children: [
      {
        path: 'role',
        component: () => import('@/views/pemission/role'),
        name: 'role',
        meta: {
          title: '角色管理',
          permissions: ['roleRead', 'roleEdit'],
          icon: 'el-icon-coordinate'
        }
      },
      {
        path: 'index',
        component: () => import('@/views/pemission/admin'),
        name: 'admin',
        meta: {
          title: '管理员管理',
          permissions: ['adminUserRead', 'adminUserEdit'],
          icon: 'el-icon-s-custom'
        }
      }
    ]
  },
  {
    path: '/user',
    component: Layout,
    name: 'user',
    meta: {
      title: '用户管理',
      permissions: ['userRead', 'userEdit'],
      icon: 'el-icon-user-solid'
    },
    children: [
      {
        path: 'index',
        component: () => import('@/views/user/user.vue'),
        name: 'userIndex',
        meta: {
          title: '用户管理',
          permissions: ['userRead', 'userEdit'],
          icon: 'el-icon-user'
        }
      }
    ]
  },
/*动态路由渲染的侧边栏菜单*/
 // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]
const createRouter = () =>
  new Router({
    // mode: 'history', // require service support
    scrollBehavior: () => ({ y: 0 }),
    routes: constantRouterMap
  })
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // 重置路由
}
export default router

八、新建文件,编辑api/admin.js文件

import request from '@/utils/request'
const prefix = '/admin'
// 登陆
export function login(data) {
  return request({
    url: prefix + '/login',
    method: 'post',
    data
  })
}
// 获取登录用户信息
export function getInfo() {
  return request({
    url: prefix + '/getUserInfo',
    method: 'get'
  })
}
// 退出登录
export function logout() {
  return request({
    url: prefix + '/logout',
    method: 'post'
  })
}
// 获取用户列表
export function getList(params) {
  return request({
    url: prefix + '/getUserList',
    method: 'get',
    params
  })
}
// 获取角色列表
export function getRoleList(params) {
  return request({
    url: prefix + '/getRoleList',
    method: 'get',
    params
  })
}
// 增加角色
export function addRoles(params) {
  return request({
    url: prefix + '/addRole',
    method: 'post',
    params
  })
}
// 编辑角色
export function editRoles(params) {
  return request({
    url: prefix + '/editRole',
    method: 'post',
    params
  })
}
// 删除角色
export function deleteRoles(params) {
  return request({
    url: prefix + '/deleteRole',
    method: 'post',
    params
  })
}
// 获取权限列表
export function getPermissionList(params) {
  return request({
    url: prefix + '/getAuthList',
    method: 'get',
    params
  })
}
// 新增用户
export function adminUserCreate(data) {
  return request({
    url: prefix + '/userCreate',
    method: 'post',
    data
  })
}
// 重置用户密码
export function resetAdminPassword(data) {
  return request({
    url: prefix + '/resetUserPassword',
    method: 'post',
    data
  })
}
// 编辑用户
export function editUser(data) {
  return request({
    url: prefix + '/editUser',
    method: 'post',
    data
  })
}
// 编辑用户角色
export function editAdminRoles(data) {
  return request({
    url: prefix + '/editUserRole',
    method: 'post',
    data
  })
}

九、编辑view的vue文件,此处示例 :permission/admin.vue

<template>
  <div class="app-container">
    <div class="search-container">
      <div class="search-form">
        <el-form :inline="true" :model="searchForm">
          <el-form-item label="账号">
            <el-input v-model="searchForm.account" placeholder="账号" />
          </el-form-item>
          <el-form-item>
            <el-button type="warning" @click="fetchData">查询</el-button>
            <el-button
              type="primary"
              @click="addDialogVisible = true"
            >新增</el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>

    <el-table
      v-loading="listLoading"
      :data="list"
      element-loading-text="Loading"
      border
      fit
      highlight-current-row
    >
      <el-table-column align="center" label="ID" width="95">
        <template slot-scope="scope">
          {{ scope.row.id }}
        </template>
      </el-table-column>
      <el-table-column label="用户名" width="270" align="center">
        <template slot-scope="scope">
          {{ scope.row.account }}
        </template>
      </el-table-column>
      <el-table-column label="角色" align="center">
        <template slot-scope="scope">
          <span>{{
            scope.row.is_admin == 1
              ? '超级管理员'
              : scope.row.role.map(i => i.name).join()
          }}</span>
        </template>
      </el-table-column>
      <el-table-column label="登录IP" width="250" align="center">
        <template slot-scope="scope">
          {{ scope.row.login_ip }}
        </template>
      </el-table-column>
      <el-table-column
        align="center"
        prop="login_time"
        label="登录时间"
        width="200"
      >
        <template slot-scope="scope">
          <i class="el-icon-time" />
          <span>{{ scope.row.login_time }}</span>
        </template>
      </el-table-column>
      <el-table-column
        class-name="status-col"
        label="状态"
        width="110"
        align="center"
      >
        <template slot-scope="scope">
          <el-tag :type="scope.row.status | statusFilter">{{
            scope.row.status | statusFilter3
          }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="350" align="center">
        <template slot-scope="scope">
          <el-button
            type="primary"
            @click="editRole(scope.row)"
          >编辑角色</el-button>
          <el-button
            type="primary"
            @click="resetPassword(scope.row)"
          >重置密码</el-button>
          <el-button
            :type="scope.row.status | statusFilter2"
            :disabled="scope.row.state == 1"
            @click="toggleStatus(scope.row)"
          >{{ scope.row.status | statusFilter4 }}</el-button>
        </template>
      </el-table-column>
    </el-table>

    <div class="pagination">
      <el-pagination
        :current-page="pagination.currentPage"
        :page-sizes="[10, 20, 30, 40, 50, 100]"
        :page-size="pagination.limit"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>

    <el-dialog title="新增账号" :visible.sync="addDialogVisible" width="500px">
      <div class="dialog-container">
        <el-form ref="addForm" :model="addForm" :rules="addFormRules">
          <el-form-item prop="account">
            <el-input v-model="addForm.account" placeholder="请输入账号名称" />
          </el-form-item>
          <el-form-item>
            <el-select
              v-model="addForm.roles"
              multiple
              clearable
              placeholder="请选择角色"
              style="width:100%;"
            >
              <el-option
                v-for="item in roleList"
                :key="item.id"
                :label="item.name"
                :value="item.id"
              />
            </el-select>
          </el-form-item>
        </el-form>
      </div>
      <div slot="footer" class="dialog-footer">
        <el-button @click="addDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="addAdmin">确 定</el-button>
      </div>
    </el-dialog>
    <el-dialog title="编辑角色" :visible.sync="editDialogVisible" width="500px">
      <div class="dialog-container">
        <el-form ref="editForm" :model="editForm">
          <el-form-item>
            <el-select
              v-model="editForm.roles"
              multiple
              clearable
              placeholder="请选择角色"
              style="width:100%;"
            >
              <el-option
                v-for="item in roleList"
                :key="item.id"
                :label="item.name"
                :value="item.id"
              />
            </el-select>
          </el-form-item>
        </el-form>
      </div>
      <div slot="footer" class="dialog-footer">
        <el-button @click="editDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="editAdmin">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import {
  getRoleList,
  getList,
  adminUserCreate,
  editAdminRoles,
  resetAdminPassword,
  editUser
} from '@/api/admin'
import { filterObjectEmpty } from '@/utils'

export default {
  name: 'Admin',
  filters: {
    statusFilter(status) {
      const statusMap = [, 'success', 'danger']
      return statusMap[status]
    },
    statusFilter2(status) {
      const statusMap = [, 'danger', 'success']
      return statusMap[status]
    },
    statusFilter3(status) {
      const statusMap = [, '启用', '禁用']
      return statusMap[status]
    },
    statusFilter4(status) {
      const statusMap = [, '禁用', '启用']
      return statusMap[status]
    }
  },
  data() {
    return {
      searchForm: {
        account: ''
      },
      pagination: {
        page: 1,
        limit: 10
      },
      list: null,
      total: 0,
      listLoading: true,
      addDialogVisible: false,
      addForm: {
        account: '',
        roles: []
      },
      addFormRules: {
        account: [
          { required: true, message: '请输入账号名称', trigger: 'blur' },
          { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
        ]
      },
      roleList: [],
      editDialogVisible: false,
      editForm: {
        id: '',
        roles: []
      }
    }
  },
  created() {
    this.fetchData()
    this.fetchRoleList()
  },
  methods: {
    fetchRoleList() {
      getRoleList({ limit: 100 }).then(res => {
        this.roleList = res.data.data
      })
    },
    fetchData() {
      this.listLoading = true
      const params = {
        ...filterObjectEmpty(this.searchForm),
        ...this.pagination
      }
      getList(params).then(response => {
        const { data, current_page: currentPage, total } = response.data
        this.list = data
        this.total = total
        this.pagination.page = currentPage
        this.listLoading = false
      })
    },
    handleSizeChange(size) {
      if (size != this.pagination.limit) {
        this.pagination.limit = size
        this.pagination.page = 1
        this.fetchData()
      }
    },
    handleCurrentChange(page) {
      if (page != this.pagination.page) {
        this.pagination.page = page
        this.fetchData()
      }
    },
    addAdmin() {
      this.$refs.addForm.validate(valid => {
        if (valid) {
          adminUserCreate(this.addForm).then(res => {
            this.addDialogVisible = false
            this.fetchData()
          })
        }
      })
    },
    toggleStatus(user) {
      let statu = '启用'
      let status = 1
      if (user.status == 1) {
        statu = '禁用'
        status = 2
      }
      this.$confirm(`是否确认设置为${statu}状态?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        editUser({ user_id: user.id, status }).then(res => {
          this.fetchData()
        })
      })
    },
    editRole(user) {
      this.editForm.id = user.id
      this.editForm.roles = user.role.map(role => role.id)
      this.editDialogVisible = true
    },
    editAdmin() {
      const data = {
        user_id: this.editForm.id,
        role_ids: this.editForm.roles
      }

      editAdminRoles(data).then(res => {
        this.fetchData()
        this.editDialogVisible = false
      })
    },
    resetPassword(user) {
      this.$confirm('确定重置密码?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        resetAdminPassword({ user_id: user.id }).then(res => {
          this.$message({
            message: '重置密码成功!',
            type: 'success'
          })
        })
      })
    }
  }
}
</script>

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

十、页面展示

image.png

总结

同理,结合ES6Element-UI可以完成其它管理菜单

1.首先是理解vue_admin_template集成模板自带的主要文件
2.编辑登录页面的权限配置
3.生成侧边栏菜单管理
4.实现菜单管理页面的各个功能,常见的有增/删/改/除/查、禁用状态、上传图片等
5.新页面的实现:router[新增侧边栏菜单]+api[配置接口]+view[编辑菜单管理的功能]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,646评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,979评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,391评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,356评论 0 215
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,740评论 3 293
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,836评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,022评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,764评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,487评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,728评论 2 252
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,200评论 1 263
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,548评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,217评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,134评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,921评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,919评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,766评论 2 274

推荐阅读更多精彩内容