新增编辑
权限菜单
这里说明下 至于super_admin 这个角色 目前编辑菜单也不生效 因为默认 路由和菜单都是全部 其他角色会根据这里设置生效
2-1 角色管理页面
src/views/system/role/index.vue
<template>
<div class="role-container">
<h2>角色管理</h2>
<div class="role-list">
<el-button
type="primary"
plain
icon="el-icon-plus"
@click="handleAddRole"
>添加角色</el-button>
<el-table
:data="roles"
max-height="400"
>
<el-table-column
prop="name"
label="角色名称"
>
</el-table-column>
<el-table-column
prop="description"
label="说明"
>
</el-table-column>
<el-table-column
prop="is_default"
label="是否默认角色"
:formatter="formatter"
>
</el-table-column>
<el-table-column
prop="createdAt"
label="创建时间"
>
</el-table-column>
<el-table-column
prop="updatedAt"
label="更新时间"
>
</el-table-column>
<el-table-column
label="操作"
fixed="right"
width="150px"
>
<template #default="scope">
<el-button
type="text"
size="mini"
@click="handleRoleMenu(scope.$index, scope.row)">
菜单权限
</el-button>
<el-button
type="text"
size="mini"
@click="handleEditRole(scope.$index, scope.row)">编辑</el-button>
<el-button
type="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-menu
v-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 title
const 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 = 0
editData.value = { ...row }
panelVisible.value = true
}
// 添加角色处理
const handleAddRole = () => {
editType.value = 1
editData.value = {} as IRole
panelVisible.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 = true
roleData.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-form
ref="editFormRef"
:model="editData"
:rules="menuFormRules"
label-width="100px"
>
<el-form-item label="角色名称" prop="name">
<el-input
v-model="editData.name"
placeholder="请输入角色名称"
/>
</el-form-item>
<el-form-item label="说明" prop="description">
<el-input
v-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-button
type="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-dialog
v-model="dialogVisible"
:title="dialogTitle"
>
<el-tree
:data="treeData"
show-checkbox
default-expand-all
node-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 IRole
const 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是菜单id
const keys = tree.getCheckedKeys(false)
// 获取所有半选中节点key 这里key是菜单id
const 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 = true
getRoleAccess(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>
// 定义state
const state: IRoleState = {
roles: [],
count: 0
}
// 定义mutation
const 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;
}
// 定义actions
const actions: IActions = {
getRoles({ commit }, params: RoleParams) {
return new Promise<void>((resolve, reject) => {
getRoles(params).then(res => {
const { data } = res
commit('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 } = data
addRole(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 } = data
updateRole(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 } = data
removeRole(id).then(res => {
if (res.code === 0) {
dispatch('getRoles', {
pageSize,
pageNum
})
}
resolve()
}).catch(reject)
})
}
}
// 定义menu module
const 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'
// 定义全局getters
const 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-description
export 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-types
export 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)
}