refactor(20241225-role): 角色权限分配优化

1、优化角色权限分配逻辑
2、采用实时保存,提高速度
This commit is contained in:
李小涛
2024-12-25 08:59:54 +08:00
parent 282ab9a6a1
commit db27235f61
9 changed files with 632 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
<template>
<el-drawer
v-model="RoleDrawer.drawerVisible"
title="权限配置"
direction="rtl"
size="80%"
:close-on-click-modal="false"
:before-close="RoleDrawer.handleDrawerClose"
:destroy-on-close="true"
>
<template #header>
<div>
当前授权角色
<el-tag>{{ RoleDrawer.roleName }}</el-tag>
</div>
</template>
<splitpanes class="default-theme" style="height: 100%">
<pane min-size="20" size="22">
<div class="pane-box">
<MenuTree />
</div>
</pane>
<pane min-size="20">
<div class="pane-box">
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="接口权限" name="first"><MenuBtn /></el-tab-pane>
<el-tab-pane label="列字段权限" name="second"><MenuField /></el-tab-pane>
</el-tabs>
</div>
</pane>
</splitpanes>
</el-drawer>
</template>
<script setup lang="ts">
import { Splitpanes, Pane } from 'splitpanes';
import 'splitpanes/dist/splitpanes.css';
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
import { defineAsyncComponent, ref } from 'vue';
const MenuTree = defineAsyncComponent(() => import('./RoleMenuTree.vue'));
const MenuBtn = defineAsyncComponent(() => import('./RoleMenuBtn.vue'));
const MenuField = defineAsyncComponent(() => import('./RoleMenuField.vue'));
const RoleDrawer = RoleDrawerStores(); // 抽屉参数
const activeName = ref('first');
</script>
<style lang="scss" scoped>
.pane-box {
width: 100vw; /* 视口宽度 */
height: 100vh; /* 视口高度 */
max-width: 100%; /* 确保不超过父元素的宽度 */
max-height: 100%; /* 确保不超过父元素的高度 */
overflow: auto; /* 当内容超出容器尺寸时显示滚动条 */
padding: 10px;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0">
<div class="menu-form-alert">配置操作功能接口权限配置数据权限点击小齿轮</div>
<el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)">
<div class="btn-item">
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
<el-icon>
<Setting />
</el-icon>
</span>
</div>
</el-checkbox>
</div>
<el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false" :before-close="handleDialogClose">
<div class="pc-dialog">
<el-select v-model="selectBtn.data_range" @change="handlePermissionRangeChange" placeholder="请选择">
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-tree-select
v-show="selectBtn.data_range === 4"
node-key="id"
v-model="selectBtn.dept"
:props="defaultTreeProps"
:data="deptData"
multiple
check-strictly
:render-after-expand="false"
show-checkbox
class="dialog-tree"
/>
</div>
<template #footer>
<div>
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
<el-button @click="handleDialogClose"> 取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
import { RoleMenuBtnType } from '../types';
import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api';
import XEUtils from 'xe-utils';
import { ElMessage } from 'element-plus';
const RoleDrawer = RoleDrawerStores(); // 角色-菜单
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
const dialogVisible = ref(false);
// 选中的按钮
const selectBtn = ref<RoleMenuBtnType>({
id: 0,
menu_btn_pre_id: 0,
/** 是否选中 */
isCheck: false,
/** 按钮名称 */
name: '',
/** 数据权限范围 */
data_range: 0,
dept: [],
});
/**
* 数据权限范围
*/
const dataPermissionRange = ref([
{ label: '仅本人数据权限', value: 0 },
{ label: '本部门及以下数据权限', value: 1 },
{ label: '本部门数据权限', value: 2 },
{ label: '全部数据权限', value: 3 },
{ label: '自定数据权限', value: 4 },
]);
/**
* 自定义数据权限的部门树配置
*/
const defaultTreeProps = {
children: 'children',
label: 'name',
value: 'id',
};
/**
* 自定数据权限下拉选择事件
*/
const handlePermissionRangeChange = async (val: number) => {
if (val < 4) {
selectBtn.value.dept = [];
}
};
/**
* 格式化按钮数据范围
*/
const formatDataRange = computed(() => {
return function (datarange: number) {
const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => {
if (item.value === datarange) {
return item.label;
}
});
return datarangeitem.label;
};
});
/**
* 勾选按钮
*/
const handleCheckChange = async (btn: RoleMenuBtnType) => {
const put_data = {
isCheck: btn.isCheck,
roleId: RoleDrawer.roleId,
menuId: RoleMenuTree.id,
btnId: btn.id,
};
const { data, msg } = await setRoleMenuBtn(put_data);
RoleMenuBtn.updateState(data);
ElMessage({ message: msg, type: 'success' });
};
/**
* 按钮-数据范围确定
*/
const handleDialogConfirm = async () => {
const { data, msg } = await setRoleMenuBtnDataRange(selectBtn.value);
selectBtn.value = data;
dialogVisible.value = false;
ElMessage({ message: msg, type: 'success' });
};
/**
* 数据范围关闭
*/
const handleDialogClose = () => {
dialogVisible.value = false;
};
/**
* 齿轮点击
*/
const handleSettingClick = async (btn: RoleMenuBtnType) => {
selectBtn.value = btn;
dialogVisible.value = true;
};
/**
* 部门数据
*
*/
const deptData = ref<number[]>([]);
// 页面打开后获取列表数据
onMounted(async () => {
const res = await getRoleToDeptAll({ role: RoleDrawer.roleId, menu_button: selectBtn.value.id });
const depts = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
deptData.value = depts;
});
</script>
<style lang="scss" scoped>
.pccm-item {
margin-bottom: 10px;
.menu-form-alert {
color: #fff;
line-height: 24px;
padding: 8px 16px;
margin-bottom: 20px;
border-radius: 4px;
background-color: var(--el-color-primary);
}
}
// .el-checkbox {
// width: 200px;
// }
.btn-item {
display: flex;
align-items: center;
justify-content: center; /* 水平居中 */
.el-icon {
margin-left: 5px;
}
}
.dialog-tree {
width: 100%;
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="pccm-item" v-if="RoleMenuField.$state.length > 0">
<div class="menu-form-alert">
<el-button size="small" @click="handleSaveField">保存 </el-button>
配置数据列字段权限
</div>
<ul class="columns-list">
<li class="columns-head">
<div class="width-txt">
<span>字段</span>
</div>
<div v-for="(head, hIndex) in RoleMenuFieldHeader.$state" :key="hIndex" class="width-check">
<el-checkbox v-model="head.checked" @change="handleColumnChange($event, head.value, head.disabled)">
<span>{{ head.label }}</span>
</el-checkbox>
</div>
</li>
<div class="columns-content">
<li v-for="(c_item, c_index) in RoleMenuField.$state" :key="c_index" class="columns-item">
<div class="width-txt">{{ c_item.title }}</div>
<div v-for="(col, cIndex) in RoleMenuFieldHeader.$state" :key="cIndex" class="width-check">
<el-checkbox v-model="c_item[col.value]" class="ci-checkout" :disabled="c_item[col.disabled]"></el-checkbox>
</div>
</li>
</div>
</ul>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus';
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
import { RoleMenuFieldStores, RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
import { setRoleMenuField } from './api';
const RoleMenuField = RoleMenuFieldStores();
const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();
const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
/** 全选 */
const handleColumnChange = (val: boolean, btnType: string, disabledType: string) => {
for (const iterator of RoleMenuField.$state) {
iterator[btnType] = iterator[disabledType] ? iterator[btnType] : val;
}
};
const handleSaveField = async () => {
const res = await setRoleMenuField(RoleDrawer.$state.roleId, RoleMenuField.$state);
ElMessage({ message: res.msg, type: 'success' });
};
</script>
<style lang="scss" scoped>
.pccm-item {
margin-bottom: 10px;
.menu-form-alert {
color: #fff;
line-height: 24px;
padding: 8px 16px;
margin-bottom: 20px;
border-radius: 4px;
background-color: var(--el-color-primary);
}
.menu-form-btn {
margin-left: 10px;
height: 40px;
padding: 8px 16px;
margin-bottom: 20px;
}
.btn-item {
display: flex;
align-items: center;
span {
margin-left: 5px;
}
}
.columns-list {
.width-txt {
width: 200px;
}
.width-check {
width: 100px;
}
.width-icon {
cursor: pointer;
}
.columns-head {
display: flex;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
span {
font-weight: 900;
}
}
.columns-content {
max-height: calc(100vh - 240px); /* 视口高度 */
overflow-y: auto; /* 当内容超出高度时显示垂直滚动条 */
.columns-item {
display: flex;
align-items: center;
padding: 6px 0;
box-sizing: border-box;
.ci-checkout {
height: auto !important;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<el-tree
ref="treeRef"
:data="menuData"
:props="defaultTreeProps"
:default-checked-keys="menuDefaultCheckedKeys"
@check-change="handleMenuChange"
@node-click="handleMenuClick"
node-key="id"
check-strictly
highlight-current
show-checkbox
default-expand-all
>
</el-tree>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
import { RoleMenuFieldStores } from '../stores/RoleMenuFieldStores';
import { RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
import { getRoleMenu, getRoleMenuBtnField, setRoleMenu } from './api';
import { ElMessage } from 'element-plus';
import XEUtils from 'xe-utils';
import { RoleMenuTreeType } from '../types';
const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
const RoleMenuField = RoleMenuFieldStores(); // 角色-菜单-列字段
const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();// 角色-菜单-列字段
const menuData = ref<RoleMenuTreeType[]>([]); // 菜单列表数据
const menuDefaultCheckedKeys = ref<(number | string | undefined)[]>([]); // 默认选中的菜单列表
// 菜单配置
const defaultTreeProps = {
children: 'children',
label: 'name',
value: 'id',
};
/**
* 菜单复选框选中
* @param node当前节点的 Node 对象
* @param checked布尔值表示当前节点是否被选中
*/
const handleMenuChange = (node: any, checked: boolean) => {
setRoleMenu({ roleId: RoleDrawer.roleId, menuId: node.id, isCheck: checked }).then((res: any) => {
ElMessage({ message: res.msg, type: 'success' });
});
};
/**
* 菜单点击事件
*/
const handleMenuClick = async (selectNode: RoleMenuTreeType) => {
if (!selectNode.is_catalog) {
RoleMenuTree.setRoleMenuTree(selectNode); // 更新当前选中的菜单
// 获取当前菜单的按钮列表
const { data } = await getRoleMenuBtnField({
roleId: RoleDrawer.roleId,
menuId: selectNode.id,
});
RoleMenuBtn.setState(data.menu_btn); // 更新按钮列表
RoleMenuField.setState(data.menu_field); // 更新列字段列表
} else {
RoleMenuBtn.setState([]); // 更新按钮列表
RoleMenuField.setState([]); // 更新列字段列表
}
RoleMenuFieldHeader.$reset()
};
// 页面打开后获取列表数据
onMounted(async () => {
menuData.value = await getRoleMenu({ roleId: RoleDrawer.roleId });
menuDefaultCheckedKeys.value = XEUtils.toTreeArray(menuData.value)
.filter((i) => i.isCheck)
.map((i) => i.id);
});
</script>

View File

@@ -0,0 +1,88 @@
import { request } from '/@/utils/service';
import XEUtils from 'xe-utils';
/**
* 获取 角色-菜单
* @param query
*/
export function getRoleMenu(query: object) {
return request({
url: '/api/system/role_menu_button_permission/get_role_menu/',
method: 'get',
params: query,
}).then((res: any) => {
return XEUtils.toArrayTree(res.data, { key: 'id', parentKey: 'parent', children: 'children', strict: false });
});
}
/**
* 设置 角色-菜单
* @param data
* @returns
*/
export function setRoleMenu(data: object) {
return request({
url: '/api/system/role_menu_button_permission/set_role_menu/',
method: 'put',
data,
});
}
/**
* 获取 角色-菜单-按钮-列字段
* @param query
*/
export function getRoleMenuBtnField(query: object) {
return request({
url: '/api/system/role_menu_button_permission/get_role_menu_btn_field/',
method: 'get',
params: query,
});
}
/**
* 设置 角色-菜单-按钮
* @param data
*/
export function setRoleMenuBtn(data: object) {
return request({
url: '/api/system/role_menu_button_permission/set_role_menu_btn/',
method: 'put',
data,
});
}
/**
* 设置 角色-菜单-列字段
* @param data
*/
export function setRoleMenuField(roleId: string | number | undefined, data: object) {
return request({
url: `/api/system/role_menu_button_permission/${roleId}/set_role_menu_field/`,
method: 'put',
data,
});
}
/**
* 设置 角色-菜单-按钮-数据权限
* @param query
* @returns
*/
export function setRoleMenuBtnDataRange(data: object) {
return request({
url: '/api/system/role_menu_button_permission/set_role_menu_btn_data_range/',
method: 'put',
data,
});
}
/**
* 获取当前用户角色下所能授权的部门
* @param query
* @returns
*/
export function getRoleToDeptAll(query: object) {
return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get',
params: query,
});
}

View File

@@ -0,0 +1,32 @@
import { defineStore } from 'pinia';
import { RoleDrawerType } from '../types';
/**
* 权限配置:抽屉
*/
const initialState: RoleDrawerType = {
drawerVisible: false,
roleId: undefined,
roleName: undefined,
};
export const RoleDrawerStores = defineStore('RoleDrawerStores', {
state: (): RoleDrawerType => ({
...initialState,
}),
actions: {
/**
* 打开权限修改抽屉
*/
handleDrawerOpen(row: any) {
this.drawerVisible = true;
this.roleName = row.name;
this.roleId = row.id;
},
/**
* 关闭权限修改抽屉
*/
handleDrawerClose() {
Object.assign(this.$state, initialState);
},
},
});

View File

@@ -0,0 +1,24 @@
import { defineStore } from 'pinia';
import { RoleMenuBtnType } from '../types';
/**
* 权限配置:角色-菜单-按钮
*/
export const RoleMenuBtnStores = defineStore('RoleMenuBtnStores', {
state: (): RoleMenuBtnType[] => [],
actions: {
/**
* 初始化
*/
setState(data: RoleMenuBtnType[]) {
this.$state = data;
this.$state.length = data.length;
},
updateState(data: RoleMenuBtnType) {
const index = this.$state.findIndex((item) => item.id === data.id);
if (index !== -1) {
this.$state[index] = data;
}
},
},
});

View File

@@ -0,0 +1,24 @@
import { defineStore } from 'pinia';
import { RoleMenuFieldType, RoleMenuFieldHeaderType } from '../types';
/**
* 权限配置:角色-菜单-列字段
*/
export const RoleMenuFieldStores = defineStore('RoleMenuFieldStores', {
state: (): RoleMenuFieldType[] => [],
actions: {
/** 重置 */
setState(data: RoleMenuFieldType[]) {
this.$state = data;
this.$state.length = data.length;
},
},
});
export const RoleMenuFieldHeaderStores = defineStore('RoleMenuFieldHeaderStores', {
state: (): RoleMenuFieldHeaderType[] => [
{ value: 'is_create', label: '新增可见', disabled: 'disabled_create', checked: false },
{ value: 'is_update', label: '编辑可见', disabled: 'disabled_update', checked: false },
{ value: 'is_query', label: '列表可见', disabled: 'disabled_query', checked: false },
],
});

View File

@@ -0,0 +1,21 @@
import { defineStore } from 'pinia';
import { RoleMenuTreeType } from '../types';
/**
* 权限抽屉:角色-菜单
*/
export const RoleMenuTreeStores = defineStore('RoleMenuTreeStores', {
state: (): RoleMenuTreeType => ({
id: 0,
parent: 0,
name: '',
isCheck: false,
is_catalog: false,
}),
actions: {
/** 赋值 */
setRoleMenuTree(data: RoleMenuTreeType) {
this.$state = data;
},
},
});