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

src/views/system/role/index.vue
<template><div class="role-container"><h2>角色管理</h2><div class="role-list"><el-buttontype="primary"plainicon="el-icon-plus"@click="handleAddRole">添加角色</el-button><el-table:data="roles"max-height="400"><el-table-columnprop="name"label="角色名称"></el-table-column><el-table-columnprop="description"label="说明"></el-table-column><el-table-columnprop="is_default"label="是否默认角色":formatter="formatter"></el-table-column><el-table-columnprop="createdAt"label="创建时间"></el-table-column><el-table-columnprop="updatedAt"label="更新时间"></el-table-column><el-table-columnlabel="操作"fixed="right"width="150px"><template #default="scope"><el-buttontype="text"size="mini"@click="handleRoleMenu(scope.$index, scope.row)">菜单权限</el-button><el-buttontype="text"size="mini"@click="handleEditRole(scope.$index, scope.row)">编辑</el-button><el-buttontype="text"size="mini"@click="handleDeleteRole(scope.$index, scope.row)">删除</el-button></template></el-table-column></el-table><div class="role-pagination"><el-pagination@size-change="handleSizeChange"@current-change="handleCurrentChange"background:total="total":page-sizes="[1, 5, 10, 20]":page-size="pageSize"layout="total, prev, pager, next, sizes,jumper"></el-pagination></div></div><!-- 新增角色 编辑角色面板 --><right-panel v-model="panelVisible" :title="panelTitle" :size="330"><editor-role:type="editType":data="editData"@submit="handleSubmitRole"/></right-panel><!-- 权限菜单树 --><role-menuv-if="roleData && roleMenuVisible":role="roleData"v-model="roleMenuVisible"/></div></template><script lang="ts">import { computed, defineComponent, ref, watchEffect, getCurrentInstance, onMounted } from 'vue'import { useStore } from '@/store'import { IRole } from '@/store/modules/role'import EditorRole from './components/editorRole.vue'import RightPanel from '@/components/RightPanel/index.vue'import RoleMenu from './components/roleMenu.vue'export default defineComponent({name: 'Role',components: {EditorRole,RightPanel,RoleMenu},setup () {const { proxy } = getCurrentInstance()!const store = useStore()// 角色列表const roles = computed(() => store.state.role.roles)// 总条数const total = computed(() => store.state.role.count)// 分页页码 条数 页码后端是从0开始 前端是从1开始const pageNum = ref(0)const pageSize = ref(1)// 暂存新增和编辑数据const editData = ref<IRole | undefined>(undefined)// 编辑面板显示const panelVisible = ref(false)// 面板操作类型const editType = ref(1) // 0编辑 1新增// panel titleconst panelTitle = computed(() => editType.value === 1 ? '新增角色' : '编辑角色')// 获取角色列表const getRoleList = () => {store.dispatch('role/getRoles', {pageNum: pageNum.value,pageSize: pageSize.value})}// 获取全部菜单onMounted(() => {store.dispatch('menu/getAllMenuList')})// 自动追踪相关依赖属性变动获取数据watchEffect(() => {getRoleList()})// 编辑角色处理const handleEditRole = (index: number, row: IRole) => {editType.value = 0editData.value = { ...row }panelVisible.value = true}// 添加角色处理const handleAddRole = () => {editType.value = 1editData.value = {} as IRolepanelVisible.value = true}// 删除角色处理const handleDeleteRole = (index: number, row: IRole) => {proxy?.$confirm(`您确认要删除角色${row.name}吗?`, '删除确认', {type: 'warning'}).then(() => {store.dispatch('role/removeRole', {id: row.id,pageSize: pageSize.value,pageNum: pageNum.value}).then(() => {proxy?.$message.success('角色删除成功')})}).catch(() => {proxy?.$message({type: 'info',message: '已取消删除'})})}// 新增编辑const dispatchAction = (action: string, data: IRole, message: string) => {store.dispatch(action, {...data,pageSize: pageSize.value,pageNum: pageNum.value}).then(() => {proxy?.$message.success(message)panelVisible.value = false})}// 新增角色const addNewRole = (data: IRole) => {dispatchAction('role/addRole', data, '角色添加成功')}// 编辑角色const editRole = (data: IRole) => {dispatchAction('role/editRole', data, '角色编辑成功')}// 提交角色信息const handleSubmitRole = (data: IRole) => {if (editType.value === 1) { // 新增addNewRole(data)} else if (editType.value === 0) { // 编辑editRole(data)}}// 权限菜单处理const roleMenuVisible = ref(false)const roleData = ref<IRole|null>(null)const handleRoleMenu = (index: number, row: IRole) => {roleMenuVisible.value = trueroleData.value = row}const formatter = (row: IRole) => {return row.is_default ? '是' : '否'}// pageSize 改变const handleSizeChange = (val: number) => {pageSize.value = val}// pageNum 改变const handleCurrentChange = (val: number) => {pageNum.value = val - 1 // 页码后端是从0开始的}return {roles,handleEditRole,handleAddRole,handleDeleteRole,handleRoleMenu,formatter,total,handleSizeChange,handleCurrentChange,pageSize,panelVisible,editData,editType,panelTitle,handleSubmitRole,roleData,roleMenuVisible}}})</script><style lang="scss" scoped>.role-container {padding: 30px;.role-pagination {margin-top: 10px;text-align: right;}}</style>
editorRole组件
src/views/system/role/components/editorRole.vue
<template><div class="editor-container"><el-formref="editFormRef":model="editData":rules="menuFormRules"label-width="100px"><el-form-item label="角色名称" prop="name"><el-inputv-model="editData.name"placeholder="请输入角色名称"/></el-form-item><el-form-item label="说明" prop="description"><el-inputv-model="editData.description"placeholder="请输入说明"/></el-form-item><el-form-item label="是否默认角色" prop="is_default"><el-switch v-model="editData.is_default" /></el-form-item><el-form-item><el-buttontype="primary"@click="submitMenuForm":loading="loading">提交</el-button></el-form-item></el-form></div></template><script lang="ts">import { defineComponent, PropType, ref, watchEffect } from 'vue'import { ElForm } from 'element-plus'import { IRole } from '@/store/modules/role'type FormInstance = InstanceType<typeof ElForm>export default defineComponent({name: 'EditorMenu',props: {type: {type: Number,required: true},data: {type: Object as PropType<IRole>}},emits: ['submit'],setup(props, { emit }) {const loading = ref(false)const editFormRef = ref<FormInstance|null>(null)const editData = ref({name: '',description: '',is_default: false})// 验证规则const menuFormRules = {name: {required: true,message: '请输入角色名称',trigger: 'blur'},description: {required: true,message: '请输入说明',trigger: 'blur'}}const defaultProps = {name: '',description: '',is_default: false}watchEffect(() => { // 利用watchEffect自动响应依赖变化if (props.data) {// 移除之前表单效验结果editFormRef.value?.clearValidate()editData.value = { ...defaultProps, ...props.data }}})// 提交编辑菜单const submitMenuForm = () => {(editFormRef.value as FormInstance).validate(valid => {if (valid) {emit('submit', editData.value)}})}return {editData,submitMenuForm,editFormRef,menuFormRules,loading}}})</script><style>.editor-container {padding: 20px;}</style>
权限菜单组件 roleMenu
src/views/system/role/components/roleMenu.vue
<template><div v-if="modelValue"><el-dialogv-model="dialogVisible":title="dialogTitle"><el-tree:data="treeData"show-checkboxdefault-expand-allnode-key="id"ref="menuTree"highlight-current:check-strictly="checkStrictly":props="defaultProps"></el-tree><template #footer><span class="dialog-footer"><el-button type="primary" plain @click="handleCheckAll">全部选择</el-button><el-button type="primary" @click="handleSubmit">提交</el-button></span></template></el-dialog></div></template><script lang="ts">import { computed, defineComponent, PropType, ref, watch, getCurrentInstance, onMounted, nextTick } from 'vue'import { IRole } from '@/store/modules/role'import { useStore } from '@/store'import { ElTree } from 'element-plus'import { allocRoleAccess, getRoleAccess } from '@/api/roleAccess'type ElTreeInstance = InstanceType<typeof ElTree>export default defineComponent({name: 'RoleMenu',props: {modelValue: {type: Boolean,default: false},role: {type: Object as PropType<IRole>,required: true}},emits: ['update:modelValue'],setup(props, { emit }) {const { proxy } = getCurrentInstance()!const store = useStore()const menuTree = ref<ElTreeInstance | null>(null)const role = props.role as IRoleconst dialogVisible = ref(true)const defaultProps = ref({children: 'children',label: 'title'})// tree父节点与子节点是否强关联const checkStrictly = ref(false) // false关联 true不关联const dialogTitle = computed(() => `分配 ${role.name} 菜单权限`)const treeData = computed(() => store.getters.menusTree)watch(dialogVisible, (value) => {emit('update:modelValue', value)})// 发送选中key与role id关联请求const handleRoleWithMenu = (keys: number[], roleId: number) => {// 发送关联请求allocRoleAccess(roleId, keys).then(res => {if (res.code === 0) {proxy?.$message.success(res.message)emit('update:modelValue', false)reloadPage()}})}// 重新刷新整个系统const reloadPage = () => {proxy?.$confirm('菜单已发生改动,是否要立即刷新系统', '刷新确认', {type: 'warning'}).then(() => {window.location.reload()}).catch(() => {proxy?.$message({type: 'info',message: '已取消刷新'})})}// 提交选择的菜单和当前角色做关联const handleSubmit = () => {const tree = (menuTree.value as ElTreeInstance)// 获取所有checkbox全选节点key 这里key是菜单idconst keys = tree.getCheckedKeys(false)// 获取所有半选中节点key 这里key是菜单idconst halfKeys = tree.getHalfCheckedKeys()const selectedKeys = [...halfKeys, ...keys]handleRoleWithMenu(selectedKeys as number[], role.id)}// 根据权限列表 设置权限选中const setAccessTreeChecked = (access: number[]) => {(menuTree.value as ElTreeInstance).setCheckedKeys(access, false)nextTick(() => {checkStrictly.value = false})}// 获取当前角色 权限列表const getRoleAccessList = () => {checkStrictly.value = truegetRoleAccess(role.id).then(res => {if (res.code === 0) {const access = res.data.map(item => item.access_id)// 设置选中权限setAccessTreeChecked(access)}}).catch(() => {checkStrictly.value = false})}// tree 全部选中const isCheckAll = ref(false)const handleCheckAll = () => {if (!isCheckAll.value) {(menuTree.value as ElTreeInstance).setCheckedNodes(treeData.value, false)} else {(menuTree.value as ElTreeInstance).setCheckedNodes([], false)}isCheckAll.value = !isCheckAll.value}onMounted(() => {getRoleAccessList()})return {dialogVisible,dialogTitle,treeData,defaultProps,handleSubmit,menuTree,checkStrictly,handleCheckAll}}})</script>
2-2 角色相关store
src/store/modules/role.ts
/* eslint-disable camelcase */import { Module, MutationTree, ActionTree } from 'vuex'import { IRootState } from '../index'import { addRole, getRoles, removeRole, RoleParams, updateRole } from '../../api/role'export interface IRoleAccess {id: number;role_id: number;access_id: number;}export type IRoleAccessList = IRoleAccess[]export interface IRole {id: number;name: string;description: string;is_default: boolean;createdAt: string;updatedAt: string;}// 定义state类型export interface IRoleState {roles: IRole[];count: number;}// mutations类型type IMutations = MutationTree<IRoleState>// actions类型type IActions = ActionTree<IRoleState, IRootState>// 定义stateconst state: IRoleState = {roles: [],count: 0}// 定义mutationconst mutations: IMutations = {SET_ROLES(state, data: IRoleState['roles']) {state.roles = data},SET_COUNT(state, count: number) {state.count = count}}type ActionRoleParams = IRole & {pageSize: number;pageNum: number;}// 定义actionsconst actions: IActions = {getRoles({ commit }, params: RoleParams) {return new Promise<void>((resolve, reject) => {getRoles(params).then(res => {const { data } = rescommit('SET_ROLES', data.roles)commit('SET_COUNT', data.count)resolve()}).catch(reject)})},// 添加角色addRole({ dispatch }, data: ActionRoleParams) {return new Promise<void>((resolve, reject) => {const { pageSize, pageNum, ...params } = dataaddRole(params).then(res => {if (res.code === 0) {dispatch('getRoles', {pageSize,pageNum})}resolve()}).catch(reject)})},// 编辑角色editRole({ dispatch }, data: ActionRoleParams) {return new Promise<void>((resolve, reject) => {const { pageSize, pageNum, ...params } = dataupdateRole(params.id, params).then(res => {if (res.code === 0) {dispatch('getRoles', {pageSize,pageNum})}resolve()}).catch(reject)})},// 移除角色removeRole({ dispatch }, data: ActionRoleParams) {return new Promise<void>((resolve, reject) => {const { pageSize, pageNum, id } = dataremoveRole(id).then(res => {if (res.code === 0) {dispatch('getRoles', {pageSize,pageNum})}resolve()}).catch(reject)})}}// 定义menu moduleconst role: Module<IRoleState, IRootState> = {namespaced: true,state,mutations,actions}export default role
getters
src/store/getters.ts
import { GetterTree } from 'vuex'import { IRootState } from './index'// 定义全局gettersconst getters: GetterTree<IRootState, IRootState> = {sidebar: (state) => state.app.sidebar,size: state => state.app.size,themeColor: state => state.settings.theme,menusTree: state => state.menu.menuTreeData,roles: state => state.user.roles,roleIds: state => (state.user.roles || []).map(role => role.id),roleNames: state => (state.user.roles || []).map(role => role.name)}export default getters
store.ts
src/store/index.ts
import { InjectionKey } from 'vue'import { createStore, Store, useStore as baseUseStore } from 'vuex'import createPersistedState from 'vuex-persistedstate'import app, { IAppState } from '@/store/modules/app'import tagsView, { ITagsViewState } from '@/store/modules/tagsView'import settings, { ISettingsState } from '@/store/modules/settings'import user, { IUserState } from '@/store/modules/user'import getters from './getters'import menu, { IMenusState } from './modules/menu'import role, { IRoleState } from './modules/role'import permission, { IPermissionState } from './modules/permission'// 模块声明在根状态下export interface IRootState {app: IAppState;user: IUserState;menu: IMenusState;role: IRoleState;tagsView: ITagsViewState;settings: ISettingsState;permission: IPermissionState;}// 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。// https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage// eslint-disable-next-line symbol-descriptionexport const key: InjectionKey<Store<IRootState>> = Symbol()// 对于getters在组件使用时没有类型提示// 有人提交了pr #1896 为getters创建泛型 应该还未发布// https://github.com/vuejs/vuex/pull/1896// 代码pr内容详情// https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce// vuex store持久化 默认使用localstorage持久化const persisteAppState = createPersistedState({storage: window.sessionStorage, // 指定storage 也可自定义key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖// paths: ['app'] // 针对app这个模块持久化// 只针对app模块下sidebar.opened状态持久化paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径})const persisteSettingsState = createPersistedState({storage: window.sessionStorage, // 指定storage 也可自定义key: 'vuex_setting', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖// paths: ['app'] // 针对app这个模块持久化// 只针对app模块下sidebar.opened状态持久化paths: ['settings.theme', 'settings.originalStyle', 'settings.tagsView', 'settings.sidebarLogo'] // 通过点连接符指定state路径})export default createStore<IRootState>({plugins: [persisteAppState,persisteSettingsState],getters,modules: {app,user,tagsView,settings,menu,role,permission}})// 定义自己的 `useStore` 组合式函数// https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-typesexport function useStore () {return baseUseStore(key)}// vuex持久化 vuex-persistedstate文档说明// https://www.npmjs.com/package/vuex-persistedstate
2-3 角色api
src/api/role.ts
import request from '@/api/config/request'import { IRole, IRoleState } from '@/store/modules/role'import { ApiResponse } from './type'// 获取角色export interface RoleParams {pageNum: number;pageSize: number;}export const getRoles = (params = { pageNum: 0, pageSize: 10 }): Promise<ApiResponse<IRoleState>> => {return request.get('/role', {params})}// 删除角色export const removeRole = (id: number): Promise<ApiResponse> => {return request.delete(`/role/${id}`)}// 添加角色export const addRole = (data: IRole): Promise<ApiResponse> => {return request.post('/role', data)}// 编辑角色export const updateRole = (id: number, data: IRole): Promise<ApiResponse> => {return request.put(`/role/${id}`, data)}
