提示:关于使用 Vue 和 ElementUI 做后台管理系统的权限


前言

现在很多管理系统都会需要权限管理,控制不同角色的菜单、页面功能、通过直接输入url跳转。此篇记录的是角色和用户的关系,以及给角色配置权限列表,如何根据权限列表控制菜单及页面功能请参考下篇

提示:以下是本篇文章正文内容,下面案例可供参考

一、角色与用户关系

该项目实现的是角色和用户可以互选,多对多的关系,一个用户可以选择多个角色,一个角色会对应多个用户。若不需要多对多的关系,只需给一个用户选择一个角色即可,针对某个角色设置对应的权限列表。

二、针对角色设置对应的权限列表

1.实现效果

  • 权限维护页面
    在这里插入图片描述

2.权限维护页面

代码如下(示例):

<template>
  <div class="detail">
    <div class="bg-content">
      <div class="operate">
        <el-button type="primary" size="small" @click="goBack">
          返回
        </el-button>
        <el-button type="primary" size="mini" @click="updateData">
          保存
        </el-button>
      </div>
      <div class="container">
        <div class="table-box">
          <el-table
            :data="tableData"
            :span-method="objectSpanMethod"
            border
            height="100%"
          >
            <el-table-column prop="firstMenu" label="一级菜单">
              <template slot-scope="{ row, $index }">
                <el-checkbox
                  v-model="row.isCheckedFirstMenu"
                  :label="row.firstMenu"
                  @change="changeFirstMenu($index)"
                />
              </template>
            </el-table-column>
            <el-table-column prop="secondMenu" label="二级菜单">
              <template slot-scope="{ row, $index }">
                <el-checkbox
                  v-if="row.secondMenu"
                  v-model="row.isCheckedSecondMenu"
                  :label="row.secondMenu"
                  @change="changeSecondMenu($index)"
                />
              </template>
            </el-table-column>
            <el-table-column prop="operateList" label="操作">
              <template slot-scope="{ row, $index }">
                <span
                  v-for="(operate, operateIndex) in row.operateList"
                  :key="operateIndex"
                  style="margin-right: 10px"
                >
                  <el-checkbox
                    v-if="operate.function"
                    v-model="operate.isChecked"
                    :label="operate.function"
                    @change="changeOperateMenu($index, operateIndex)"
                  />
                </span>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import {
  getPermissionsMenuListByRoleId,
  getPermissionsLineByRoleId,
  roleBindPermissions
} from '@/aaa'
import lodash from 'lodash'
let childrenIndex = 0
export default {
  components: {},
  filters: {},
  data() {
    return {
      tableData: []
    }
  },
  created() {
    this.productLineList()
    this.getPermissionsMenuListByRoleId(this.$route.params.id)
  },
  mounted() {},
  methods: {
    // 获取菜单列表
    getPermissionsMenuListByRoleId(roleId) {
      getPermissionsMenuListByRoleId(roleId).then(res => {
        if (res.data.code == '200') {
          const tempData = res.data.data
          const checkedFirstMenuList = res.data.data
            ?.filter(item => item.isCheckedFirstMenu)
            ?.map(item => item.firstMenuId)
          this.tableData = tempData.map(item => {
            if (checkedFirstMenuList.includes(item.firstMenuId)) {
              return { ...item, isCheckedFirstMenu: true }
            }
            return { ...item }
          })
        } else {
          this.$notify({
            title: '提示',
            message: res.data.message || '获取菜单列表失败',
            type: 'error',
            duration: 2000
          })
        }
      })
    },
    // 合并单元格
    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
      if (columnIndex === 0) {
        // 合并行
        if (childrenIndex > 1) {
          childrenIndex--
          return {
            rowspan: 0,
            colspan: 0
          }
        } else {
          childrenIndex = row.children
          return {
            rowspan: row.children > 0 ? row.children : 1,
            colspan: 1
          }
        }
      }
    },
    // 返回按钮
    goBack() {
      this.$router.push({
        name: `rolePermission`,
        params: {
          currentPage: this.$route.params.currentPage,
          size: this.$route.params.size
        }
      })
    },
    // 一级菜单值变化
    changeFirstMenu(index) {
      const val = this.tableData[index].isCheckedFirstMenu
      const firstMenu = this.tableData[index].firstMenu
      this.tableData = this.tableData?.map((item1, index1) => {
        if (item1.firstMenu === firstMenu) {
          return {
            ...item1,
            isCheckedFirstMenu: val,
            isCheckedSecondMenu: val,
            operateList:
              item1.operateList?.map(item2 => {
                return { ...item2, isChecked: val }
              }) || []
          }
        }
        return { ...item1 }
      })
    },
    // 二级菜单值变化
    changeSecondMenu(index) {
      const val = this.tableData[index].isCheckedSecondMenu
      const firstMenu = this.tableData[index].firstMenu
      const secondMenu = this.tableData[index].secondMenu
      let flag = false
      const indexItems = []
      this.tableData = this.tableData?.map((item1, index1) => {
        // 点击的当前行
        if (item1.secondMenu === secondMenu) {
          if (!val) {
            indexItems.push({ index: index1, item: item1 })
          }
          if (
            item1.firstMenu === firstMenu &&
            item1.isCheckedFirstMenu !== val
          ) {
            return {
              ...item1,
              isCheckedFirstMenu: val,
              operateList:
                item1.operateList?.map(item2 => {
                  return { ...item2, isChecked: val }
                }) || []
            }
          }

          return {
            ...item1,
            operateList:
              item1.operateList?.map(item2 => {
                return { ...item2, isChecked: val }
              }) || []
          }
        } else if (item1.firstMenu === firstMenu) {
          // if(item1.isCheckedFirstMenu)
          // 当二级菜单为true时
          if (val) {
            // 判断一级菜单与它相等的数据 一级菜单有没有选,没有选则手动选上,否则不做处理
            if (!item1.isCheckedFirstMenu) {
              return {
                ...item1,
                isCheckedFirstMenu: val
              }
            }
            return { ...item1 }
          } else {
            // 当二级菜单为false时
            // 当与当前选择的行不同,但一级菜单相同,且二级菜单只为true
            if (index1 !== index && item1.isCheckedSecondMenu) {
              flag = true
              indexItems.push({ index: index1, item: item1 })
            }
            return { ...item1 }
          }
        }
        return { ...item1 }
      })
      // 二级菜单全部取消,需要将对应一级菜单设置为false
      if (indexItems.length === 1) {
        const tempTableData = lodash.cloneDeep(this.tableData)
        tempTableData.forEach((item, index) => {
          if (indexItems[0].item.firstMenu === item.firstMenu) {
            this.$set(tempTableData, index, {
              ...item,
              isCheckedFirstMenu: false
            })
          }
        })
        this.tableData = lodash.cloneDeep(tempTableData)
      }
      // 二级菜单取消时控制一级菜单
      if (flag) {
        const tempTableData = lodash.cloneDeep(this.tableData)
        indexItems.forEach(item => {
          this.$set(tempTableData, item.index, {
            ...tempTableData[item.index],
            isCheckedFirstMenu: true
          })
        })
        this.tableData = lodash.cloneDeep(tempTableData)
      }
      this.$forceUpdate()
    },
    // 页面功能选择
    changeOperateMenu(index, operateIndex) {
      const val = this.tableData[index].operateList[operateIndex].isChecked
      const firstMenu = this.tableData[index].firstMenu
      const operateList = this.tableData[index].operateList
      let flag = false
      const indexItems = []
      if (val) {
        // 按钮操作选为true
        const selectList = operateList.filter(
          operate => operate.isChecked === true
        )
        if (selectList.length === 1) {
          this.$set(this.tableData, index, {
            ...this.tableData[index],
            isCheckedSecondMenu: true
          })
          this.tableData = this.tableData.map(item1 => {
            if (firstMenu === item1.firstMenu) {
              return { ...item1, isCheckedFirstMenu: true }
            }
            return { ...item1 }
          })
        }
      } else {
        // 按钮操作选为false
        const noselectList = operateList.filter(
          operate => operate.isChecked === false
        )
        if (noselectList.length === operateList.length) {
          this.$set(this.tableData, index, {
            ...this.tableData[index],
            isCheckedSecondMenu: false
          })
        }
        this.tableData.forEach((item1, index1) => {
          if (firstMenu === item1.firstMenu && item1.isCheckedSecondMenu) {
            flag = true
            indexItems.push({ index: index1, item: item1 })
          }
        })
        // 二级菜单全部取消,需要将对应一级菜单设置为false
        if (indexItems.length === 0) {
          this.tableData = this.tableData.map(item1 => {
            if (firstMenu === item1.firstMenu) {
              return { ...item1, isCheckedFirstMenu: false }
            }
            return { ...item1 }
          })
        }
        // 对应一级菜单下只要有一个二级菜单被选中,就需要设置为true
        if (flag) {
          const tempTableData = lodash.cloneDeep(this.tableData)
          indexItems.forEach(item => {
            this.$set(tempTableData, item.index, {
              ...tempTableData[item.index],
              isCheckedFirstMenu: true
            })
          })
          this.tableData = lodash.cloneDeep(tempTableData)
        }
      }
    },
    updateData() {
      // 遍历表格数据找出选中的一级、二级菜单以及操作按钮的id
          let permissionIdList = []
          this.tableData.forEach((item1, index1) => {
            // 存在一级级菜单的情况
            if (item1.firstMenu && item1.isCheckedFirstMenu) {
              permissionIdList.push(item1.firstMenuId)
            }
            // 存在二级菜单的情况
            if (item1.secondMenu && item1.isCheckedSecondMenu) {
              permissionIdList.push(item1.firstMenuId)
              permissionIdList.push(item1.secondMenuId)
            }
            // 存在操作列表的情况
            if (item1.operateList && item1.operateList.length > 0) {
              item1.operateList.forEach((item2, index2) => {
                if (item2.isChecked) {
                  permissionIdList.push(item2.functionId)
                }
              })
            }
          })
          // 数组去重
          permissionIdList = Array.from(new Set(permissionIdList))
          const param = {
            roleId: this.$route.params.id,
            permissionIds: permissionIdList.join(','),
          }

          roleBindPermissions(param)
            .then(res => {
              if (res.data.code === '200') {
                this.$notify({
                  title: '成功',
                  message: '保存成功',
                  type: 'success',
                  duration: 1000
                })
                this.goBack()
              } else {
                this.$notify({
                  title: '提示',
                  message: res.data.message || '保存失败',
                  type: 'error',
                  duration: 2000
                })
              }
            })
            .catch(message => {
              this.$message.error(message)
            })
    }
  }
}
</script>
<style lang="scss" scoped>
.detail{
  padding: 10px;
  background-color: #eaeff5;
  width: 100%;
  height: calc(100vh - 50px);
  box-sizing: border-box;
  .bg-content {
    width: 100%;
    height: 100%;
    background-color: #ffffff;
    padding: 24px;
    .operate {
      margin-bottom: 24px;
      .el-button {
        margin: 0 14px 0 0;
        padding: 9px 18px;
        cursor: pointer;
      }
    }
    .container {
      background-color: #ffffff;
      // overflow-y: scroll;
      padding: 0;
      height: calc(100vh - 160px);
      display: flex;
      flex-direction: column;
    }
    .container::-webkit-scrollbar {
      background: #fff;
      width: 4px;
    }
    .container::-webkit-scrollbar-thumb {
      background: rgba(0, 0, 0, 0.2);
      width: 4px;
    }
    .container::-webkit-scrollbar-track {
      background: #fff;
      width: 4px;
    }
    .table-box {
      flex: 1;
      overflow-y: scroll;
    }
  }
}
</style>
  • 表格数据结构如下:
const tableData = [
  {
    firstMenu: '菜单1',
    secondMenu: '',
    operateList: null,
    hasChildren: false,
    isCheckedFirstMenu: false,
    isCheckedSecondMenu: false,
    children: 0
  },
  {
    firstMenu: '菜单2',
    secondMenu: '菜单2-1',
    operateList: [
      { function: '查询', isChecked: false },
      { function: '新增', isChecked: false },
      { function: '编辑', isChecked: false },
      { function: '详情', isChecked: false },
      { function: '删除', isChecked: false }
    ],
    isCheckedFirstMenu: false,
    isCheckedSecondMenu: false,
    hasChildren: true,
    children: 5
  },
  {
    firstMenu: '菜单2',
    secondMenu: '菜单2-2',
    operateList: [
      { function: '查询', isChecked: false },
      { function: '新增', isChecked: false },
      { function: '编辑', isChecked: false },
      { function: '详情', isChecked: false },
      { function: '删除', isChecked: false }
    ],
    isCheckedFirstMenu: false,
    isCheckedSecondMenu: false,
    hasChildren: true,
    children: 5
  },
  {
    firstMenu: '菜单2',
    secondMenu: '菜单2-3',
    operateList: [
      { function: '查询', isChecked: false },
      { function: '新增', isChecked: false },
      { function: '编辑', isChecked: false },
      { function: '详情', isChecked: false },
      { function: '删除', isChecked: false }
    ],
    isCheckedFirstMenu: false,
    isCheckedSecondMenu: false,
    hasChildren: true,
    children: 5
  },
  {
    firstMenu: '菜单2',
    secondMenu: '菜单2-4',
    operateList: [
      { function: '查询', isChecked: false },
      { function: '新增', isChecked: false },
      { function: '编辑', isChecked: false },
      { function: '详情', isChecked: false },
      { function: '删除', isChecked: false }
    ],
    hasChildren: false,
    isCheckedFirstMenu: false,
    isCheckedSecondMenu: false,
    children: 5
  },
  {
    firstMenu: '菜单2',
    secondMenu: '菜单2-5',
    operateList: [
      { function: '查询', isChecked: false },
      { function: '新增', isChecked: false },
      { function: '编辑', isChecked: false },
      { function: '详情', isChecked: false },
      { function: '删除', isChecked: false }
    ],
    hasChildren: false,
    isCheckedFirstMenu: false,
    isCheckedSecondMenu: false,
    children: 5
  }
]

3.页面实现逻辑说明

为了页面更直观,没有使用树形结构,由于使用ElementUI 的el-table实现,需要合并单元格(objectSpanMethod),合并单元格需要知道要合并几行,所以此处通过行数据row的children字段控制合并行数。
  • 选择一级菜单会同时勾选该一级菜单下的所有二级菜单以及页面功能,取消一级菜单也会同时取消该一级菜单下的所有二级菜单以及页面功能,相关方法为 changeFirstMenu
  • 选择二级菜单会勾选该二级菜单的一级菜单和该二级菜单的所有页面功能,取消二级菜单会判断该二级菜单对应的一级菜单下是否还有其他二级菜单已勾选,有则不取消一级菜单,没有则取消对应一级菜单,同时会取消该二级菜单下的所有页面功能
  • 选择页面功能会同时勾选该页面功能的一级菜单和二级菜单,取消页面功能首先会判断该页面功能对应的二级菜单下是否还有其他页面功能已勾选,有则不取消二级菜单,没有则继续判断该二级菜单对应的一级菜单下是否还有其他二级菜单已勾选,有则不取消一级菜单,没有则取消对应一级菜单,同时会取消该二级菜单下的所有页面功能

总结

需求要求如此展示,所以页面未使用树形结构,由于合并单元格的原因,数据处理显得有些复杂,若有更简洁的办法,欢迎指教
Logo

前往低代码交流专区

更多推荐