image.png
新增编辑
image.png
权限菜单
image.png
这里说明下 至于super_admin 这个角色 目前编辑菜单也不生效 因为默认 路由和菜单都是全部 其他角色会根据这里设置生效

2-1 角色管理页面

image.png
src/views/system/role/index.vue

  1. <template>
  2. <div class="role-container">
  3. <h2>角色管理</h2>
  4. <div class="role-list">
  5. <el-button
  6. type="primary"
  7. plain
  8. icon="el-icon-plus"
  9. @click="handleAddRole"
  10. >添加角色</el-button>
  11. <el-table
  12. :data="roles"
  13. max-height="400"
  14. >
  15. <el-table-column
  16. prop="name"
  17. label="角色名称"
  18. >
  19. </el-table-column>
  20. <el-table-column
  21. prop="description"
  22. label="说明"
  23. >
  24. </el-table-column>
  25. <el-table-column
  26. prop="is_default"
  27. label="是否默认角色"
  28. :formatter="formatter"
  29. >
  30. </el-table-column>
  31. <el-table-column
  32. prop="createdAt"
  33. label="创建时间"
  34. >
  35. </el-table-column>
  36. <el-table-column
  37. prop="updatedAt"
  38. label="更新时间"
  39. >
  40. </el-table-column>
  41. <el-table-column
  42. label="操作"
  43. fixed="right"
  44. width="150px"
  45. >
  46. <template #default="scope">
  47. <el-button
  48. type="text"
  49. size="mini"
  50. @click="handleRoleMenu(scope.$index, scope.row)">
  51. 菜单权限
  52. </el-button>
  53. <el-button
  54. type="text"
  55. size="mini"
  56. @click="handleEditRole(scope.$index, scope.row)">编辑</el-button>
  57. <el-button
  58. type="text"
  59. size="mini"
  60. @click="handleDeleteRole(scope.$index, scope.row)">删除</el-button>
  61. </template>
  62. </el-table-column>
  63. </el-table>
  64. <div class="role-pagination">
  65. <el-pagination
  66. @size-change="handleSizeChange"
  67. @current-change="handleCurrentChange"
  68. background
  69. :total="total"
  70. :page-sizes="[1, 5, 10, 20]"
  71. :page-size="pageSize"
  72. layout="total, prev, pager, next, sizes,jumper"
  73. ></el-pagination>
  74. </div>
  75. </div>
  76. <!-- 新增角色 编辑角色面板 -->
  77. <right-panel v-model="panelVisible" :title="panelTitle" :size="330">
  78. <editor-role
  79. :type="editType"
  80. :data="editData"
  81. @submit="handleSubmitRole"
  82. />
  83. </right-panel>
  84. <!-- 权限菜单树 -->
  85. <role-menu
  86. v-if="roleData && roleMenuVisible"
  87. :role="roleData"
  88. v-model="roleMenuVisible"
  89. />
  90. </div>
  91. </template>
  92. <script lang="ts">
  93. import { computed, defineComponent, ref, watchEffect, getCurrentInstance, onMounted } from 'vue'
  94. import { useStore } from '@/store'
  95. import { IRole } from '@/store/modules/role'
  96. import EditorRole from './components/editorRole.vue'
  97. import RightPanel from '@/components/RightPanel/index.vue'
  98. import RoleMenu from './components/roleMenu.vue'
  99. export default defineComponent({
  100. name: 'Role',
  101. components: {
  102. EditorRole,
  103. RightPanel,
  104. RoleMenu
  105. },
  106. setup () {
  107. const { proxy } = getCurrentInstance()!
  108. const store = useStore()
  109. // 角色列表
  110. const roles = computed(() => store.state.role.roles)
  111. // 总条数
  112. const total = computed(() => store.state.role.count)
  113. // 分页页码 条数 页码后端是从0开始 前端是从1开始
  114. const pageNum = ref(0)
  115. const pageSize = ref(1)
  116. // 暂存新增和编辑数据
  117. const editData = ref<IRole | undefined>(undefined)
  118. // 编辑面板显示
  119. const panelVisible = ref(false)
  120. // 面板操作类型
  121. const editType = ref(1) // 0编辑 1新增
  122. // panel title
  123. const panelTitle = computed(() => editType.value === 1 ? '新增角色' : '编辑角色')
  124. // 获取角色列表
  125. const getRoleList = () => {
  126. store.dispatch('role/getRoles', {
  127. pageNum: pageNum.value,
  128. pageSize: pageSize.value
  129. })
  130. }
  131. // 获取全部菜单
  132. onMounted(() => {
  133. store.dispatch('menu/getAllMenuList')
  134. })
  135. // 自动追踪相关依赖属性变动获取数据
  136. watchEffect(() => {
  137. getRoleList()
  138. })
  139. // 编辑角色处理
  140. const handleEditRole = (index: number, row: IRole) => {
  141. editType.value = 0
  142. editData.value = { ...row }
  143. panelVisible.value = true
  144. }
  145. // 添加角色处理
  146. const handleAddRole = () => {
  147. editType.value = 1
  148. editData.value = {} as IRole
  149. panelVisible.value = true
  150. }
  151. // 删除角色处理
  152. const handleDeleteRole = (index: number, row: IRole) => {
  153. proxy?.$confirm(`您确认要删除角色${row.name}吗?`, '删除确认', {
  154. type: 'warning'
  155. }).then(() => {
  156. store.dispatch('role/removeRole', {
  157. id: row.id,
  158. pageSize: pageSize.value,
  159. pageNum: pageNum.value
  160. }).then(() => {
  161. proxy?.$message.success('角色删除成功')
  162. })
  163. }).catch(() => {
  164. proxy?.$message({
  165. type: 'info',
  166. message: '已取消删除'
  167. })
  168. })
  169. }
  170. // 新增编辑
  171. const dispatchAction = (action: string, data: IRole, message: string) => {
  172. store.dispatch(action, {
  173. ...data,
  174. pageSize: pageSize.value,
  175. pageNum: pageNum.value
  176. }).then(() => {
  177. proxy?.$message.success(message)
  178. panelVisible.value = false
  179. })
  180. }
  181. // 新增角色
  182. const addNewRole = (data: IRole) => {
  183. dispatchAction('role/addRole', data, '角色添加成功')
  184. }
  185. // 编辑角色
  186. const editRole = (data: IRole) => {
  187. dispatchAction('role/editRole', data, '角色编辑成功')
  188. }
  189. // 提交角色信息
  190. const handleSubmitRole = (data: IRole) => {
  191. if (editType.value === 1) { // 新增
  192. addNewRole(data)
  193. } else if (editType.value === 0) { // 编辑
  194. editRole(data)
  195. }
  196. }
  197. // 权限菜单处理
  198. const roleMenuVisible = ref(false)
  199. const roleData = ref<IRole|null>(null)
  200. const handleRoleMenu = (index: number, row: IRole) => {
  201. roleMenuVisible.value = true
  202. roleData.value = row
  203. }
  204. const formatter = (row: IRole) => {
  205. return row.is_default ? '是' : '否'
  206. }
  207. // pageSize 改变
  208. const handleSizeChange = (val: number) => {
  209. pageSize.value = val
  210. }
  211. // pageNum 改变
  212. const handleCurrentChange = (val: number) => {
  213. pageNum.value = val - 1 // 页码后端是从0开始的
  214. }
  215. return {
  216. roles,
  217. handleEditRole,
  218. handleAddRole,
  219. handleDeleteRole,
  220. handleRoleMenu,
  221. formatter,
  222. total,
  223. handleSizeChange,
  224. handleCurrentChange,
  225. pageSize,
  226. panelVisible,
  227. editData,
  228. editType,
  229. panelTitle,
  230. handleSubmitRole,
  231. roleData,
  232. roleMenuVisible
  233. }
  234. }
  235. })
  236. </script>
  237. <style lang="scss" scoped>
  238. .role-container {
  239. padding: 30px;
  240. .role-pagination {
  241. margin-top: 10px;
  242. text-align: right;
  243. }
  244. }
  245. </style>

editorRole组件

src/views/system/role/components/editorRole.vue

  1. <template>
  2. <div class="editor-container">
  3. <el-form
  4. ref="editFormRef"
  5. :model="editData"
  6. :rules="menuFormRules"
  7. label-width="100px"
  8. >
  9. <el-form-item label="角色名称" prop="name">
  10. <el-input
  11. v-model="editData.name"
  12. placeholder="请输入角色名称"
  13. />
  14. </el-form-item>
  15. <el-form-item label="说明" prop="description">
  16. <el-input
  17. v-model="editData.description"
  18. placeholder="请输入说明"
  19. />
  20. </el-form-item>
  21. <el-form-item label="是否默认角色" prop="is_default">
  22. <el-switch v-model="editData.is_default" />
  23. </el-form-item>
  24. <el-form-item>
  25. <el-button
  26. type="primary"
  27. @click="submitMenuForm"
  28. :loading="loading"
  29. >提交</el-button>
  30. </el-form-item>
  31. </el-form>
  32. </div>
  33. </template>
  34. <script lang="ts">
  35. import { defineComponent, PropType, ref, watchEffect } from 'vue'
  36. import { ElForm } from 'element-plus'
  37. import { IRole } from '@/store/modules/role'
  38. type FormInstance = InstanceType<typeof ElForm>
  39. export default defineComponent({
  40. name: 'EditorMenu',
  41. props: {
  42. type: {
  43. type: Number,
  44. required: true
  45. },
  46. data: {
  47. type: Object as PropType<IRole>
  48. }
  49. },
  50. emits: ['submit'],
  51. setup(props, { emit }) {
  52. const loading = ref(false)
  53. const editFormRef = ref<FormInstance|null>(null)
  54. const editData = ref({
  55. name: '',
  56. description: '',
  57. is_default: false
  58. })
  59. // 验证规则
  60. const menuFormRules = {
  61. name: {
  62. required: true,
  63. message: '请输入角色名称',
  64. trigger: 'blur'
  65. },
  66. description: {
  67. required: true,
  68. message: '请输入说明',
  69. trigger: 'blur'
  70. }
  71. }
  72. const defaultProps = {
  73. name: '',
  74. description: '',
  75. is_default: false
  76. }
  77. watchEffect(() => { // 利用watchEffect自动响应依赖变化
  78. if (props.data) {
  79. // 移除之前表单效验结果
  80. editFormRef.value?.clearValidate()
  81. editData.value = { ...defaultProps, ...props.data }
  82. }
  83. })
  84. // 提交编辑菜单
  85. const submitMenuForm = () => {
  86. (editFormRef.value as FormInstance).validate(valid => {
  87. if (valid) {
  88. emit('submit', editData.value)
  89. }
  90. })
  91. }
  92. return {
  93. editData,
  94. submitMenuForm,
  95. editFormRef,
  96. menuFormRules,
  97. loading
  98. }
  99. }
  100. })
  101. </script>
  102. <style>
  103. .editor-container {
  104. padding: 20px;
  105. }
  106. </style>

权限菜单组件 roleMenu

src/views/system/role/components/roleMenu.vue

  1. <template>
  2. <div v-if="modelValue">
  3. <el-dialog
  4. v-model="dialogVisible"
  5. :title="dialogTitle"
  6. >
  7. <el-tree
  8. :data="treeData"
  9. show-checkbox
  10. default-expand-all
  11. node-key="id"
  12. ref="menuTree"
  13. highlight-current
  14. :check-strictly="checkStrictly"
  15. :props="defaultProps">
  16. </el-tree>
  17. <template #footer>
  18. <span class="dialog-footer">
  19. <el-button type="primary" plain @click="handleCheckAll">全部选择</el-button>
  20. <el-button type="primary" @click="handleSubmit">提交</el-button>
  21. </span>
  22. </template>
  23. </el-dialog>
  24. </div>
  25. </template>
  26. <script lang="ts">
  27. import { computed, defineComponent, PropType, ref, watch, getCurrentInstance, onMounted, nextTick } from 'vue'
  28. import { IRole } from '@/store/modules/role'
  29. import { useStore } from '@/store'
  30. import { ElTree } from 'element-plus'
  31. import { allocRoleAccess, getRoleAccess } from '@/api/roleAccess'
  32. type ElTreeInstance = InstanceType<typeof ElTree>
  33. export default defineComponent({
  34. name: 'RoleMenu',
  35. props: {
  36. modelValue: {
  37. type: Boolean,
  38. default: false
  39. },
  40. role: {
  41. type: Object as PropType<IRole>,
  42. required: true
  43. }
  44. },
  45. emits: ['update:modelValue'],
  46. setup(props, { emit }) {
  47. const { proxy } = getCurrentInstance()!
  48. const store = useStore()
  49. const menuTree = ref<ElTreeInstance | null>(null)
  50. const role = props.role as IRole
  51. const dialogVisible = ref(true)
  52. const defaultProps = ref({
  53. children: 'children',
  54. label: 'title'
  55. })
  56. // tree父节点与子节点是否强关联
  57. const checkStrictly = ref(false) // false关联 true不关联
  58. const dialogTitle = computed(() => `分配 ${role.name} 菜单权限`)
  59. const treeData = computed(() => store.getters.menusTree)
  60. watch(dialogVisible, (value) => {
  61. emit('update:modelValue', value)
  62. })
  63. // 发送选中key与role id关联请求
  64. const handleRoleWithMenu = (keys: number[], roleId: number) => {
  65. // 发送关联请求
  66. allocRoleAccess(roleId, keys).then(res => {
  67. if (res.code === 0) {
  68. proxy?.$message.success(res.message)
  69. emit('update:modelValue', false)
  70. reloadPage()
  71. }
  72. })
  73. }
  74. // 重新刷新整个系统
  75. const reloadPage = () => {
  76. proxy?.$confirm('菜单已发生改动,是否要立即刷新系统', '刷新确认', {
  77. type: 'warning'
  78. }).then(() => {
  79. window.location.reload()
  80. }).catch(() => {
  81. proxy?.$message({
  82. type: 'info',
  83. message: '已取消刷新'
  84. })
  85. })
  86. }
  87. // 提交选择的菜单和当前角色做关联
  88. const handleSubmit = () => {
  89. const tree = (menuTree.value as ElTreeInstance)
  90. // 获取所有checkbox全选节点key 这里key是菜单id
  91. const keys = tree.getCheckedKeys(false)
  92. // 获取所有半选中节点key 这里key是菜单id
  93. const halfKeys = tree.getHalfCheckedKeys()
  94. const selectedKeys = [...halfKeys, ...keys]
  95. handleRoleWithMenu(selectedKeys as number[], role.id)
  96. }
  97. // 根据权限列表 设置权限选中
  98. const setAccessTreeChecked = (access: number[]) => {
  99. (menuTree.value as ElTreeInstance).setCheckedKeys(access, false)
  100. nextTick(() => {
  101. checkStrictly.value = false
  102. })
  103. }
  104. // 获取当前角色 权限列表
  105. const getRoleAccessList = () => {
  106. checkStrictly.value = true
  107. getRoleAccess(role.id).then(res => {
  108. if (res.code === 0) {
  109. const access = res.data.map(item => item.access_id)
  110. // 设置选中权限
  111. setAccessTreeChecked(access)
  112. }
  113. }).catch(() => {
  114. checkStrictly.value = false
  115. })
  116. }
  117. // tree 全部选中
  118. const isCheckAll = ref(false)
  119. const handleCheckAll = () => {
  120. if (!isCheckAll.value) {
  121. (menuTree.value as ElTreeInstance).setCheckedNodes(treeData.value, false)
  122. } else {
  123. (menuTree.value as ElTreeInstance).setCheckedNodes([], false)
  124. }
  125. isCheckAll.value = !isCheckAll.value
  126. }
  127. onMounted(() => {
  128. getRoleAccessList()
  129. })
  130. return {
  131. dialogVisible,
  132. dialogTitle,
  133. treeData,
  134. defaultProps,
  135. handleSubmit,
  136. menuTree,
  137. checkStrictly,
  138. handleCheckAll
  139. }
  140. }
  141. })
  142. </script>

2-2 角色相关store

src/store/modules/role.ts

  1. /* eslint-disable camelcase */
  2. import { Module, MutationTree, ActionTree } from 'vuex'
  3. import { IRootState } from '../index'
  4. import { addRole, getRoles, removeRole, RoleParams, updateRole } from '../../api/role'
  5. export interface IRoleAccess {
  6. id: number;
  7. role_id: number;
  8. access_id: number;
  9. }
  10. export type IRoleAccessList = IRoleAccess[]
  11. export interface IRole {
  12. id: number;
  13. name: string;
  14. description: string;
  15. is_default: boolean;
  16. createdAt: string;
  17. updatedAt: string;
  18. }
  19. // 定义state类型
  20. export interface IRoleState {
  21. roles: IRole[];
  22. count: number;
  23. }
  24. // mutations类型
  25. type IMutations = MutationTree<IRoleState>
  26. // actions类型
  27. type IActions = ActionTree<IRoleState, IRootState>
  28. // 定义state
  29. const state: IRoleState = {
  30. roles: [],
  31. count: 0
  32. }
  33. // 定义mutation
  34. const mutations: IMutations = {
  35. SET_ROLES(state, data: IRoleState['roles']) {
  36. state.roles = data
  37. },
  38. SET_COUNT(state, count: number) {
  39. state.count = count
  40. }
  41. }
  42. type ActionRoleParams = IRole & {
  43. pageSize: number;
  44. pageNum: number;
  45. }
  46. // 定义actions
  47. const actions: IActions = {
  48. getRoles({ commit }, params: RoleParams) {
  49. return new Promise<void>((resolve, reject) => {
  50. getRoles(params).then(res => {
  51. const { data } = res
  52. commit('SET_ROLES', data.roles)
  53. commit('SET_COUNT', data.count)
  54. resolve()
  55. }).catch(reject)
  56. })
  57. },
  58. // 添加角色
  59. addRole({ dispatch }, data: ActionRoleParams) {
  60. return new Promise<void>((resolve, reject) => {
  61. const { pageSize, pageNum, ...params } = data
  62. addRole(params).then(res => {
  63. if (res.code === 0) {
  64. dispatch('getRoles', {
  65. pageSize,
  66. pageNum
  67. })
  68. }
  69. resolve()
  70. }).catch(reject)
  71. })
  72. },
  73. // 编辑角色
  74. editRole({ dispatch }, data: ActionRoleParams) {
  75. return new Promise<void>((resolve, reject) => {
  76. const { pageSize, pageNum, ...params } = data
  77. updateRole(params.id, params).then(res => {
  78. if (res.code === 0) {
  79. dispatch('getRoles', {
  80. pageSize,
  81. pageNum
  82. })
  83. }
  84. resolve()
  85. }).catch(reject)
  86. })
  87. },
  88. // 移除角色
  89. removeRole({ dispatch }, data: ActionRoleParams) {
  90. return new Promise<void>((resolve, reject) => {
  91. const { pageSize, pageNum, id } = data
  92. removeRole(id).then(res => {
  93. if (res.code === 0) {
  94. dispatch('getRoles', {
  95. pageSize,
  96. pageNum
  97. })
  98. }
  99. resolve()
  100. }).catch(reject)
  101. })
  102. }
  103. }
  104. // 定义menu module
  105. const role: Module<IRoleState, IRootState> = {
  106. namespaced: true,
  107. state,
  108. mutations,
  109. actions
  110. }
  111. export default role

getters

src/store/getters.ts

  1. import { GetterTree } from 'vuex'
  2. import { IRootState } from './index'
  3. // 定义全局getters
  4. const getters: GetterTree<IRootState, IRootState> = {
  5. sidebar: (state) => state.app.sidebar,
  6. size: state => state.app.size,
  7. themeColor: state => state.settings.theme,
  8. menusTree: state => state.menu.menuTreeData,
  9. roles: state => state.user.roles,
  10. roleIds: state => (state.user.roles || []).map(role => role.id),
  11. roleNames: state => (state.user.roles || []).map(role => role.name)
  12. }
  13. export default getters

store.ts

src/store/index.ts

  1. import { InjectionKey } from 'vue'
  2. import { createStore, Store, useStore as baseUseStore } from 'vuex'
  3. import createPersistedState from 'vuex-persistedstate'
  4. import app, { IAppState } from '@/store/modules/app'
  5. import tagsView, { ITagsViewState } from '@/store/modules/tagsView'
  6. import settings, { ISettingsState } from '@/store/modules/settings'
  7. import user, { IUserState } from '@/store/modules/user'
  8. import getters from './getters'
  9. import menu, { IMenusState } from './modules/menu'
  10. import role, { IRoleState } from './modules/role'
  11. import permission, { IPermissionState } from './modules/permission'
  12. // 模块声明在根状态下
  13. export interface IRootState {
  14. app: IAppState;
  15. user: IUserState;
  16. menu: IMenusState;
  17. role: IRoleState;
  18. tagsView: ITagsViewState;
  19. settings: ISettingsState;
  20. permission: IPermissionState;
  21. }
  22. // 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。
  23. // https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage
  24. // eslint-disable-next-line symbol-description
  25. export const key: InjectionKey<Store<IRootState>> = Symbol()
  26. // 对于getters在组件使用时没有类型提示
  27. // 有人提交了pr #1896 为getters创建泛型 应该还未发布
  28. // https://github.com/vuejs/vuex/pull/1896
  29. // 代码pr内容详情
  30. // https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce
  31. // vuex store持久化 默认使用localstorage持久化
  32. const persisteAppState = createPersistedState({
  33. storage: window.sessionStorage, // 指定storage 也可自定义
  34. key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  35. // paths: ['app'] // 针对app这个模块持久化
  36. // 只针对app模块下sidebar.opened状态持久化
  37. paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径
  38. })
  39. const persisteSettingsState = createPersistedState({
  40. storage: window.sessionStorage, // 指定storage 也可自定义
  41. key: 'vuex_setting', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  42. // paths: ['app'] // 针对app这个模块持久化
  43. // 只针对app模块下sidebar.opened状态持久化
  44. paths: ['settings.theme', 'settings.originalStyle', 'settings.tagsView', 'settings.sidebarLogo'] // 通过点连接符指定state路径
  45. })
  46. export default createStore<IRootState>({
  47. plugins: [
  48. persisteAppState,
  49. persisteSettingsState
  50. ],
  51. getters,
  52. modules: {
  53. app,
  54. user,
  55. tagsView,
  56. settings,
  57. menu,
  58. role,
  59. permission
  60. }
  61. })
  62. // 定义自己的 `useStore` 组合式函数
  63. // https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95
  64. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  65. export function useStore () {
  66. return baseUseStore(key)
  67. }
  68. // vuex持久化 vuex-persistedstate文档说明
  69. // https://www.npmjs.com/package/vuex-persistedstate

2-3 角色api

src/api/role.ts

  1. import request from '@/api/config/request'
  2. import { IRole, IRoleState } from '@/store/modules/role'
  3. import { ApiResponse } from './type'
  4. // 获取角色
  5. export interface RoleParams {
  6. pageNum: number;
  7. pageSize: number;
  8. }
  9. export const getRoles = (params = { pageNum: 0, pageSize: 10 }): Promise<ApiResponse<IRoleState>> => {
  10. return request.get('/role', {
  11. params
  12. })
  13. }
  14. // 删除角色
  15. export const removeRole = (id: number): Promise<ApiResponse> => {
  16. return request.delete(`/role/${id}`)
  17. }
  18. // 添加角色
  19. export const addRole = (data: IRole): Promise<ApiResponse> => {
  20. return request.post('/role', data)
  21. }
  22. // 编辑角色
  23. export const updateRole = (id: number, data: IRole): Promise<ApiResponse> => {
  24. return request.put(`/role/${id}`, data)
  25. }