角色管理

作者: amanohina | 来源:发表于2021-03-09 11:50 被阅读0次

    角色管理属于权限管理的一个部分,可以预设一些角色比如课程管理员,广告管理员,超级管理员,普通用户等等,然后在实际使用时分配给不同不同的用户不同的角色,便于操作(不需要给某个用户进行详细的功能设置)
    我们涉及到的功能有:

    • 制作添加角色,编辑角色,分配菜单
    • 布局,列表展示,删除功能,分配资源
      基础准备工作:

    views/role
    .
    ├── components
    │ └── list.vue 角色列表组件
    └── index.vue 角色组件
    角色页面初始化代码:

    <template>
      <div class="role">
        <role-list></role-list>
      </div>
    </template>
    
    <script>
    import RoleList from './components/list'
    export default {
      name: 'RoleIndex',
      components: {
        RoleList
      }
    }
    </script>
    
    <style lang="scss" scoped></style>
    
    

    角色列表组件初始化代码(实现展示和删除功能)因为展示和删除我们已经做了很多次了这里就不再赘述

    <template>
      <div class="role-list">
        <el-card class="box-card">
          <div slot="header" class="clearfix">
            <el-form ref="form" :model="form">
              <el-form-item label="角色名称">
                <el-input v-model="form.name"></el-input>
              </el-form-item>
            <el-form-item>
                <el-button
                type="primary"
                @click="onSubmit"
                v-loading:="isLoading"
                >查询</el-button>
                <el-button @click="onReset">重置</el-button>
              </el-form-item>
            </el-form>
          </div>
          <el-button>添加角色</el-button>
          <el-table
          v-loading="isLoading"
          :data="roles"
          style="width: 100%">
          <el-table-column
            prop="id"
            label="编号"
            >
          </el-table-column>
          <el-table-column
            prop="name"
            label="角色名称"
            />
          <el-table-column
            prop="description"
            label="描述"
            />
          <el-table-column
            prop="createdTime"
            label="添加时间"
            >
            <template slot-scope="scope">
                  <span>{{ scope.row.createdTime | dateFormat }}</span>
              </template>
          </el-table-column>
          <el-table-column
            align="center"
            label="操作"
            width="150px"
            >
            <template slot-scope="scope">
                <div>
                    <el-button
                    type="text"
                    >分配菜单</el-button>
                    <el-button
                    type="text"
                    >分配资源</el-button>
                </div>
                <div>
                    <el-button
                    type="text"
                    @click="handleEdit(scope.row)"
                    >编辑</el-button>
                    <el-button
                    type="text"
                    @click="handleDelete(scope.row)"
                    >删除</el-button>
                </div>
            </template>
            </el-table-column>
        </el-table>
        </el-card>
      </div>
    </template>
    
    <script>
    // 引入请求方法
    import { getRoles, deleteRole } from '@/services/role'
    // 因为不止一个地方使用了fliter时间日期过滤器了,所以我们封装这个功能,再从工具类中引入
    import dateFormat from '@/utils/dateformat'
    export default {
      name: 'RoleList',
      data () {
        return {
          form: {
            name: ''
          },
          isLoading: false,
          roles: []
        }
      },
      created () {
        this.loadRoles()
      },
      methods: {
        onSubmit () {
        },
        onReset () {
        },
        handleEdit (role) {
    
        },
        handleDelete (role) {
          // 删除角色
          this.$confirm(`确认删除角色:${role.name}?`, '删除提示')
            .then(async () => {
              await deleteRole(role.id)
              this.$message.success('删除成功')
              this.loadRoles()
            })
            .catch(err => {
              if (err && err.response) {
                this.$message.error('删除失败,请重试')
              } else {
                this.$message.info('取消删除')
              }
            })
        },
        async loadRoles () {
          this.isLoading = true
          const { data } = await getRoles(this.form)
          this.roles = data.data.records
          this.isLoading = false
        }
      },
      filters: {
        dateFormat
      }
    }
    </script>
    
    <style lang="scss" scoped>
    </style>
    
    

    创建services/role.js角色接口操作模块

    import request from '@/utils/request'
    // 获取角色
    export const getRoles = data => {
      return request({
        method: 'POST',
        url: '/boss/role/getRolePages',
        data
      })
    }
    
    // 删除角色
    export const deleteRole = id => {
      return request({
        method: 'DELETE',
        url: `/boss/role/${id}`
      })
    }
    
    

    再三强调,再三强调,要核实英语单词拼写(本人这里又因为单词问题浪费了很多时间来调试)
    准备工作到此结束

    添加角色布局

    使用Element的Dialog对话框组件

    // Element 官方示例:Dialog 对话框组件
    <el-dialog
      title="提示"
      :visible.sync="dialogVisible"
      width="30%"
      :before-close="handleClose">
      <span>这是一段信息</span>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
      </span>
    </el-dialog>
    <script>
      export default {
        data() {
          return {
            dialogVisible: false
          };
        },
        methods: {
          handleClose(done) {
            this.$confirm('确认关闭?')
              .then(_ => {
                done();
              })
              .catch(_ => {});
          }
        }
      };
    </script>
    

    添加到页面中最后位置,不需要关闭处理,删除就可以了

    • 点击顶部添加按钮时,将dialogVisible设置为true,让对话框显示
    // role/components/list.vue
    <template>
      <div class="role-list">
        <el-card class="box-card">
                ...
          <!-- 点击添加按钮,显示对话框 -->
          <el-button @click="dialogVisible = true" >添加角色</el-button>
          ...
        </el-card>
        <!-- 对话框组件:删除 :before-close="handleClose"  -->
        <el-dialog
          title="添加角色"
          :visible.sync="dialogVisible"
          width="30%"
        >
          <span>这是一段信息</span>
          <span slot="footer" class="dialog-footer">
            <el-button @click="dialogVisible = false">取 消</el-button>
            <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
          </span>
        </el-dialog>
      </div>
    </template>
    <script>
      export default {
        data() {
          return {
            ...
            dialogVisible: false
          };
        }
      };
    </script>
    

    内部的form表单结构不建议直接书写,因为不止一个功能使用这个结构,建议单独封装一个组件进行处理

    // role/components/create-or-edit.vue
    <template>
      <div>
        <el-form>
          <el-form-item label="角色名称">
            <el-input></el-input>
          </el-form-item>
          <el-form-item label="角色编码">
            <el-input></el-input>
          </el-form-item>
          <el-form-item label="角色描述">
            <el-input type="textarea"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button>取消</el-button>
            <el-button type="primary">确认</el-button>
          </el-form-item>
        </el-form>
      </div>
    </template>
    
    <script>
    export default {
      name: 'CreateOrEdit'
    }
    </script>
    
    <style lang="scss" scoped></style>
    

    引入到list组件中,并将其注册为子组件

    // role/components/list.vue
    <el-dialog
      title="添加角色"
      :visible.sync="dialogVisible"
      width="30%">
      <!-- 将对话框内容更换为自定义组件 -->
      <create-or-edit></create-or-edit>
    </el-dialog>
    ...
    <script>
    ...
    import CreateOrEdit from './create-or-edit'
    
    export default {
      ...
      components: {
        CreateOrEdit
      },
        ...
    }
    </script>
    

    结构准备完毕,设置请求,提交表单用

    // services/role.js
    ...
    // 添加或编辑角色
    export const createOrUpdate = data => {
      return request({
        method: 'POST',
        url: '/boss/role/saveOrUpdate',
        data
      })
    }
    

    引入文件,根据接口要求声明绑定数据,绑定给元素,浏览器可以查看效果

    // create-or-edit.vue
    <el-form>
      <el-form-item label="角色名称">
        <el-input v-model="role.name"></el-input>
      </el-form-item>
      <el-form-item label="角色编码">
        <el-input v-model="role.code"></el-input>
      </el-form-item>
      <el-form-item label="角色描述">
        <el-input v-model="role.description" type="textarea"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button>取消</el-button>
        <el-button type="primary">确认</el-button>
      </el-form-item>
    </el-form>
    
    <script>
    import { createOrEdit } from "@/services/role"
    
    export default {
      name: 'CreateOrEdit',
      data () {
        return {
          // 根据接口要求绑定数据
          role: {
            code: '',
            name: '',
            description: ''
          }
        }
      }
    }
    </script>
    

    点击确认,提交表单

    // create-or-edit.vue
    <el-button type="primary" @click="onSubmit">确认</el-button>
    ...
    <script>
    ...
      methods: {
        async onSubmit () {
          // 省略验证步骤
          const { data } = await createOrEdit(this.role)
          if (data.code === '000000') {
          }
        }
      }
    ...
    </script>
    

    成功之后呢,应该关闭对话框并且刷新列表,这些都是list.vue的功能,这个需要用一个子传父的通信功能了

    • 给子组件注册自定义事件
    • 父组件监听事件,触发就说明添加成功,就可以进行下一步的处理了
    // create-or-edit.vue
    ...
    async onSubmit () {
      const { data } = await createOrEdit(this.role)
      if (data.code === '000000') {
        // 提示
        this.$message.success('添加成功')
        // 子组件触发自定义事件
        this.$emit('success')
        // 清空内容
        this.role = {}
      }
    }
    ...
    
    // list.vue
    <create-or-edit
      @success="onSuccess"
    ></create-or-edit>
    ...
    <script>
    ...
      // 添加成功的操作
      onSuccess () {
        // 关闭对话框
        this.dialogVisible = false
        // 刷新列表数据
        this.loadRoles()
      },
    ...
    </script>
    

    当然了,取消按钮也是需要设置的,点击取消,关闭对话框,同样的需要子传父

    // create-or-edit.vue
    <el-button
      @click="onCancel"
    >取消</el-button>
    ...
    <script>
    ...
      onCancel () {
        this.$emit('cancel')
        this.role = {}
      }
    ...
    </script>
    

    由于取消无需刷新列表,可以直接在行内标签写入我们所需的关闭弹窗

    // list.vue
    <create-or-edit
      ...
      @cancel="dialogVisible = false"
    ></create-or-edit>
    

    编辑角色

    编辑功能和添加功能类似,采用同一组件,操作时使用isEdit传递状态

    // list.vue
    <script>
    ...
    data () {
      return {
            ...
        // 添加或编辑
        isEdit: false,
        ...
      }
    },
    ...
    </script>
    ...
    <el-dialog
      :title="isEdit ? '编辑角色' : '添加角色'"
      ...>
      <!-- 将对话框内容更换为自定义组件 -->
      <create-or-edit
        :is-edit="isEdit"
        ...
      ></create-or-edit>
    </el-dialog>
    

    子组件接收状态

    // create-or-edit.vue
    props: {
      isEdit: {
        type: Boolean,
        default: false
      }
    },
    

    点击添加时,设置isEdit为false

    // list.vue
    <el-button @click="handleAdd" >添加角色</el-button>
    ...
    <script>
    ...
      handleAdd () {
        this.dialogVisible = true
        this.isEdit = false
      },
    ...
    </script>
    

    点击编辑按钮,设置isEdit为true,同时弹出对话框

    // list.vue
    ...
    handleEdit (role) {
      // 显示对话框
      this.dialogVisible = true
      // 设置编辑状态
        this.isEdit = true
    },
    

    点击编辑时将数据绑定给表单,需要父传子将编辑的角色id传入

    • 传入id由子组件重新查询,可以确保数据为最新的数据,避免数据不齐全的情况
    // list.vue
    <script>
    ...
      data () {
          ...
          // 正在编辑的角色ID
          roleId: null
        }
      },
      handleEdit (role) {
        this.dialogVisible = true
        this.isEdit = true
        // 将要编辑的角色 ID 传递给表单展示
        this.roleId = role.id
      },
    ...
    </script>
    ...
    <create-or-edit
      :is-edit="isEdit"
      :role-id="roleId"
        @success="onSuccess"
        @cancel="dialogVisible = false"
    ></create-or-edit>
    

    子组件接收,并且在created钩子函数下展示

    // create-or-list.vue
    props: {
      ...
      roleId: {
        type: Object
      }
    },
    created () {
      if (this.isEdit) {
        console.log(roleId)
      }
    },
    

    封装请求功能

    // services/role.js
    ...
    // 获取角色
    export const getRoleById = id => {
      return request({
        method: 'GET',
        url: `/boss/role/${id}`,
      })
    }
    

    引入请求角色数据

    // create-or-edit.vue
    created () {
      if (this.isEdit) {
        // 加载用户数据
        this.loadRole()
      }
    },
    ...
    async loadRole () {
      const { data } = await getRoleById(this.roleId)
      if (data.code === '000000') {
        // 将角色数据更新给 role 即可
        this.role = data.data
      }
    },
    

    提交功能因为我们的接口是新增和编辑的区别在于id,role中自动包含了参数id,所以无需修改直接提交
    优化:role数据渲染问题

    使用监听器监听id变化,一旦变化之后,比如我先点击了添加按钮,那么edit界面就不会再有渲染数据,需要再次进行加载
    通过子传父的自定义事件,当我们点击了取消按钮,isEdit,roleId都会重置,这样以来数据就便于控制,表单可以正常执行功能
    以上的方法是本人自行解决的办法,但是有更好的做法:

    由于当前组件只是显示和隐藏,组件没有销毁和创建,所以我们通过v-if来控制对话框,达到生命周期函数created重新运行的目的,可以避免数据只会渲染一次的错误

    // list.vue
    <create-or-edit
      v-if="dialogVisible"
      ...
    ></create-or-edit>
    

    完成

    分配菜单

    分配菜单用于设置角色可以访问哪些菜单功能

    布局与路由处理

    创建文件

    // role/alloc-menu.vue
    <template>
      <div class="alloc-menu"></div>
    </template>
    
    <script>
    export default {
      name: 'AllocMenu'
    }
    </script>
    
    <style lang="scss" scoped></style>
    

    添加路由,设置动态参数

    // router/index.js
    ...
      {
        path: '/role/:roleId/alloc-menu',
        name: 'alloc-menu',
        component: () => import(/* webpackChunkName: 'alloc-menu' */'@/views/role/alloc-menu')
      }
    ]
    ...
    

    点击分配菜单按钮,进行导航跳转

    • 跳转后,可以通过$route来获取要分配菜单的用户id
    // list.vue
    <!-- scope.row 为作用域插槽提供的当前行数据 -->
    <el-button
      type="text"
      @click="$router.push({
        name: 'alloc-menu',
        params: {
          roleId: scope.row.id
        }
      })"
    >分配菜单</el-button>
    

    使用这种方式非常简单,但是会让组件与路由耦合(组件无法独立于路由使用)

    • 如果希望组件与路由解耦,可以将动态路由参数替换为props(这个方法可以详细参考一下Vue Router阶段->路由传参处理
    // router/index.js
    //  - 设置 props: true,让路径参数通过 props 方式传递给组件
    {
      path: '/role/:roleId/alloc-menu',
      name: 'alloc-menu',
      component: () => import(/* webpackChunkName: 'alloc-menu' */'@/views/role/alloc-menu'),
      props: true
    },
    
    // alloc-menu.vue
    props: {
      // 组件内需要通过 props 接收路径传递的参数,实现解耦
      //   - 注意,$route 里的数据虽然还能访问,但不要用,否则组件与路由还是耦合,解耦就白写了
      roleId: {
        type: [String, Number],
        required: true
      }
    }
    

    菜单列表展示

    这里使用Element的Tree树形控件组件

    // Element 官方示例:Tree 树形控件基础用法
    <el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
    
    <script>
      export default {
        data() {
          return {
            data: [{
              label: '一级 1',
              children: [{
                label: '二级 1-1',
                children: [{
                  label: '三级 1-1-1'
                }]
              }]
            }, {
              label: '一级 2',
              children: [{
                label: '二级 2-1',
                children: [{
                  label: '三级 2-1-1'
                }]
              }, {
                label: '二级 2-2',
                children: [{
                  label: '三级 2-2-1'
                }]
              }]
            }, {
              label: '一级 3',
              children: [{
                label: '二级 3-1',
                children: [{
                  label: '三级 3-1-1'
                }]
              }, {
                label: '二级 3-2',
                children: [{
                  label: '三级 3-2-1'
                }]
              }]
            }],
            defaultProps: {
              children: 'children',
              label: 'label'
            }
          };
        },
        methods: {
          handleNodeClick(data) {
            console.log(data);
          }
        }
      };
    </script>
    

    引入到页面中,去除事件

    • 设置一个Card作为容器
    • 设置默认全部展开,使用<el-tree>组件的
      dafault-expand-all属性
    • 设置复选框提示,使用show-checkbox属性
    • data代表的是树形图数据,更改为menus避免混淆
    • defaultProps代表读取数据时,属性对应的功能,因为请求接口时,不是所有的接口都用label作为标题,具体根据接口返回的数据为标准
    • 请求数据接口:接口地址
    • 将其封装到services/menu.js中
    // services/menu.js
    ...
    // 获取所有菜单并按层级展示(注意,这是菜单功能,保存到 menu.js 中)
    export const getMenuNodeList = () => {
      return request({
        method: 'GET',
        url: '/boss/menu/getMenuNodeList'
      })
    }
    
    • 引入请求数据,由于对应标题和层级的属性与默认名称不同,需要进行设置,menus中的默认数据也可以删掉
    <template>
      <div class="alloc-menu">
        <el-card>
          <el-tree
            :data="menus"
            :props="defaultProps"
            show-checkbox
            default-expand-all
            >
          </el-tree>
        </el-card>
      </div>
    </template>
    
    <script>
    import { getMenuNodeList } from '@/services/menu'
    export default {
      name: 'AllocMenu',
      props: {
        roleId: {
          type: [Number, String],
          required: true
        }
      },
      created () {
        this.loadMenus()
      },
      data () {
        return {
          menus: [],
          defaultProps: {
            children: 'subMenuList',
            label: 'name'
          }
        }
      },
      methods: {
        async loadMenus () {
          const { data } = await getMenuNodeList()
          if (data.code === '000000') {
            this.menus = data.data
          }
        }
      }
    }
    </script>
    
    <style lang="scss" scoped>
    
    </style>
    
    

    保存与清空

    设置按钮结构

    // alloc-menu.vue
    <el-card>
      <el-tree
        ...
      ></el-tree>
      <div style="margin: 20px;">
        <el-button>清空</el-button>
        <el-button type="primary">保存</el-button>
      </div>
    </el-card>
    

    保存

    • 观察接口,需要用户id与菜单列表(参数有点复杂)
    // services/menu.js
    ...
    // 给角色分配菜单
    export const allocateRoleMenus = data => {
      return request({
        method: 'POST',
        url: '/boss/menu/allocateRoleMenus',
        data
      })
    }
    

    引入并且在调用保存按钮时调用

    • 请求参数需要分配的菜单列表(由勾选选项组成),可以通过tree组件的getCheckedKeys方法来获取
    • 首先要给tree组件设置一个ref,才能找到指定的tree组件
    • 如果通过getCheckedKeys方法获取,需要给tree组件设置一个node-key来设置通过哪个属性作为唯一标识(比如id)
    • 提交成功,跳转加提示信息
    // alloc-menu.vue
    ...
     <el-tree
       ref="menu-tree"
       node-key="id"
       ...
     ></el-tree>
    ...
    <el-button type="primary" @click="onSave">保存</el-button>
    ...
    <script>
    import { getMenuNodeList, allocateRoleMenus } from '@/services/menu'
    ...
    async onSave () {
      // 传入通过路径参数接收的角色ID
      const { data } = await allocateRoleMenus({
        roleId: this.roleId,
        menuIdList: this.$refs['menu-tree'].getCheckedKeys()
      })
      if (data.code === '000000') {
        // 提示
        this.$message.success('分配菜单成功')
        // 返回角色列表页即可
        this.$router.push('/role')
      }
    }
    

    改进(勾选已分配选项)

    Tree组件具有default-checked-keys属性,值为数组,当node-key为id时,数组内存放的id对应的选项会被选择
    通过接口来请求当前角色拥有的菜单列表,接口地址

    // services/menu.js
    ...
    // 获取角色拥有的菜单列表
    export const getRoleMenus = roleId => {
      return request({
        method: 'GET',
        // 下面两种方式均可
        // url: `/boss/menu/getRoleMenus?roleId=${roleId}`,
        url: '/boss/menu/getRoleMenus',
        params: { // axios 会把 params 转换为 urlencoded 并以 ? 连接到 url 后
          roleId
        }
      })
    }
    

    引入并调用

    // alloc-menu.vue
    ..
    import { getMenuNodeList, allocateRoleMenus, getRoleMenus } from '@/services/menu'
    ...
    created () {
      ...
      // 加载角色拥有的菜单列表
      this.loadRoleMenus()
    },
    ...
    async loadRoleMenus () {
      // 请求角色拥有的菜单列表
      const { data } = await getRoleMenus(this.roleId)
      if (data.code === '000000') {
        console.log(data)
      }
    },
    

    观察响应数据可以发现一点:每一个菜单项都有selected属性,以此来判断是否选中了数据,拿到了这些数据的id之后,设置给default-checked-keys就可以了

    data () {
        return {
         checkedKeys: []
      }
    }
    ...
    methods: {
    // 封装的用于数据筛选的方法(筛选出被选中选项的菜单项的id)
        getCheckedKeys (menus) {
          // 遍历数据(将所有存在子节点的node排除,对子节点列表进行遍历)
          menus.forEach(menu => {
            // 未选中,结束
            if (!menu.selected) {
              return
            }
            // 是否存在子节点
            if (menu.subMenuList) {
              return this.getCheckedKeys(menu.subMenuList)
            }
            // 选中的叶子节点,没有子节点的节点,存储Id即可
            // this.checkedKeys.push(menu.id),采用赋值的方法可以避免因为频繁的异步操作导致视图不更新的问题
            this.checkedKeys = [...this.checkedKeys, menu.id]
          })
        },
        async loadRoleMenus () {
          const { data } = await getRoleMenus(this.roleId)
          if (data.code === '000000') {
            this.getCheckedKeys(data.data)
          }
        },
    ...
    }
    

    清空

    Element-Tree-树型节点的选择实例演示了如何进行清空

    • 通过this.$refs.tree.setCheckedKeys([])即可清空
    // alloc-menu.vue
    <el-button @click="resetChecked">清空</el-button>
    ...
    // 设置清空方法
     resetChecked () {
       this.$refs['menu-tree'].setCheckedKeys([])
     }
    

    完成,旁边的按钮,分配资源以及查询重置都是重复的内容,可以快速完成

    相关文章

      网友评论

        本文标题:角色管理

        本文链接:https://www.haomeiwen.com/subject/krijqltx.html