mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-16 16:54:41 +08:00
update 角色菜单权限展示优化
This commit is contained in:
parent
7aaa40b46c
commit
f269c003da
@ -8,12 +8,24 @@ export interface MenuTreeOption {
|
|||||||
label: string;
|
label: string;
|
||||||
parentId: string | number;
|
parentId: string | number;
|
||||||
weight: number;
|
weight: number;
|
||||||
|
menuType?: MenuTypeEnum | string;
|
||||||
children?: MenuTreeOption[];
|
children?: MenuTreeOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoleMenuTree {
|
export interface RoleMenuTree {
|
||||||
menus: MenuTreeOption[];
|
menus: MenuTreeOption[];
|
||||||
checkedKeys: string[];
|
checkedKeys: Array<string | number>;
|
||||||
|
buttonsMap: Record<string, RoleMenuButtonOption[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色菜单分配中的按钮节点类型
|
||||||
|
*/
|
||||||
|
export interface RoleMenuButtonOption {
|
||||||
|
menuId: string | number;
|
||||||
|
menuName: string;
|
||||||
|
parentId: string | number;
|
||||||
|
perms?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -229,16 +229,49 @@
|
|||||||
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">
|
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">
|
||||||
父子联动
|
父子联动
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
<el-tree
|
<!-- 表头与树同框,避免与树内「展开 + 勾选 + 名称」区域错位 -->
|
||||||
ref="menuRef"
|
<div class="menu-tree-permission-wrap tree-border">
|
||||||
class="tree-border"
|
<div class="menu-tree-header" role="row">
|
||||||
:data="menuOptions"
|
<span class="menu-tree-header-name">菜单名称</span>
|
||||||
show-checkbox
|
<span class="menu-tree-header-buttons">按钮权限</span>
|
||||||
node-key="id"
|
</div>
|
||||||
:check-strictly="!form.menuCheckStrictly"
|
<el-tree
|
||||||
empty-text="加载中,请稍候"
|
ref="menuRef"
|
||||||
:props="{ label: 'label', children: 'children' } as any"
|
class="menu-tree-panel"
|
||||||
></el-tree>
|
:data="menuOptions"
|
||||||
|
:indent="0"
|
||||||
|
show-checkbox
|
||||||
|
node-key="id"
|
||||||
|
:check-strictly="!form.menuCheckStrictly"
|
||||||
|
empty-text="加载中,请稍候"
|
||||||
|
:props="{ label: 'label', children: 'children', class: getMenuNodeClass } as any"
|
||||||
|
@check="syncCheckedButtonIdsFromTree"
|
||||||
|
>
|
||||||
|
<template #default="{ data, node }">
|
||||||
|
<div class="menu-tree-node-row">
|
||||||
|
<span class="menu-tree-node-label" :style="getMenuNodeLabelStyle(node.level)">
|
||||||
|
{{ data.label }}
|
||||||
|
</span>
|
||||||
|
<el-checkbox-group
|
||||||
|
v-if="getMenuButtons(data.id).length"
|
||||||
|
v-model="checkedButtonIds"
|
||||||
|
class="menu-tree-node-buttons"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<el-checkbox
|
||||||
|
v-for="button in getMenuButtons(data.id)"
|
||||||
|
:key="button.menuId"
|
||||||
|
:value="button.menuId"
|
||||||
|
@change="handleButtonPermissionChange($event, button.menuId)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
{{ button.menuName }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="数据权限" name="data">
|
<el-tab-pane label="数据权限" name="data">
|
||||||
<el-form-item label="权限范围">
|
<el-form-item label="权限范围">
|
||||||
@ -287,7 +320,7 @@
|
|||||||
<script setup name="Role" lang="ts">
|
<script setup name="Role" lang="ts">
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { roleMenuTreeselect, treeselect as menuTreeselect } from '@/api/system/menu/index';
|
import { roleMenuTreeselect, treeselect as menuTreeselect } from '@/api/system/menu/index';
|
||||||
import { MenuTreeOption, RoleMenuTree } from '@/api/system/menu/types';
|
import { MenuTreeOption, RoleMenuButtonOption, RoleMenuTree } from '@/api/system/menu/types';
|
||||||
import {
|
import {
|
||||||
addRole,
|
addRole,
|
||||||
changeRoleStatus,
|
changeRoleStatus,
|
||||||
@ -316,6 +349,10 @@ const multiple = ref(true);
|
|||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const dateRange = ref<any>(['', '']);
|
const dateRange = ref<any>(['', '']);
|
||||||
const menuOptions = ref<MenuTreeOption[]>([]);
|
const menuOptions = ref<MenuTreeOption[]>([]);
|
||||||
|
/** 菜单ID到按钮列表映射(后端返回) */
|
||||||
|
const buttonsMap = ref<Record<string, RoleMenuButtonOption[]>>({});
|
||||||
|
/** 按钮权限勾选集合 */
|
||||||
|
const checkedButtonIds = ref<Array<string | number>>([]);
|
||||||
const menuExpand = ref(false);
|
const menuExpand = ref(false);
|
||||||
const menuNodeAll = ref(false);
|
const menuNodeAll = ref(false);
|
||||||
const deptExpand = ref(true);
|
const deptExpand = ref(true);
|
||||||
@ -344,6 +381,56 @@ const dataScopeRef = ref<ElFormInstance>();
|
|||||||
const menuRef = ref<ElTreeInstance>();
|
const menuRef = ref<ElTreeInstance>();
|
||||||
const deptRef = ref<ElTreeInstance>();
|
const deptRef = ref<ElTreeInstance>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据菜单节点ID获取其按钮列表,用于树节点行内展示。
|
||||||
|
*/
|
||||||
|
const getMenuButtons = (menuId: string | number): RoleMenuButtonOption[] => {
|
||||||
|
return buttonsMap.value[String(menuId)] ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮节点ID集合:用于隐藏树中按钮节点并同步按钮勾选状态。
|
||||||
|
*/
|
||||||
|
const buttonIdSet = computed(() => {
|
||||||
|
const buttonIds = new Set<string>();
|
||||||
|
Object.values(buttonsMap.value).forEach(buttons => {
|
||||||
|
buttons.forEach(button => buttonIds.add(String(button.menuId)));
|
||||||
|
});
|
||||||
|
return buttonIds;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮节点在树中隐藏,避免与右侧按钮区重复展示。
|
||||||
|
*/
|
||||||
|
const getMenuNodeClass = (data: MenuTreeOption): string => {
|
||||||
|
return buttonIdSet.value.has(String(data.id)) ? 'is-hidden-button-node' : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将树的勾选状态同步到右侧按钮勾选集合。
|
||||||
|
*/
|
||||||
|
const syncCheckedButtonIdsFromTree = () => {
|
||||||
|
const checkedKeys = (menuRef.value?.getCheckedKeys(false) ?? []) as Array<string | number>;
|
||||||
|
checkedButtonIds.value = checkedKeys.filter(id => buttonIdSet.value.has(String(id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击右侧按钮时,直接勾选/取消隐藏按钮节点,由树自动处理级联关系。
|
||||||
|
*/
|
||||||
|
const handleButtonPermissionChange = (checked: string | number | boolean, buttonMenuId: string | number) => {
|
||||||
|
menuRef.value?.setChecked(buttonMenuId, Boolean(checked), false);
|
||||||
|
syncCheckedButtonIdsFromTree();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于节点层级给菜单名称增加缩进,确保按钮列始终固定对齐。
|
||||||
|
*/
|
||||||
|
const getMenuNodeLabelStyle = (level: number) => {
|
||||||
|
return {
|
||||||
|
paddingLeft: `${Math.max(0, level - 1) * 18}px`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const initForm: RoleForm = {
|
const initForm: RoleForm = {
|
||||||
roleId: undefined,
|
roleId: undefined,
|
||||||
roleSort: 1,
|
roleSort: 1,
|
||||||
@ -468,6 +555,8 @@ const getDeptAllCheckedKeys = (): any => {
|
|||||||
/** 重置新增的表单以及其他数据 */
|
/** 重置新增的表单以及其他数据 */
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
menuRef.value?.setCheckedKeys([]);
|
menuRef.value?.setCheckedKeys([]);
|
||||||
|
buttonsMap.value = {};
|
||||||
|
checkedButtonIds.value = [];
|
||||||
menuExpand.value = false;
|
menuExpand.value = false;
|
||||||
menuNodeAll.value = false;
|
menuNodeAll.value = false;
|
||||||
deptExpand.value = true;
|
deptExpand.value = true;
|
||||||
@ -499,6 +588,7 @@ const handleUpdate = async (row?: RoleVO) => {
|
|||||||
const getRoleMenuTreeselect = (roleId: string | number) => {
|
const getRoleMenuTreeselect = (roleId: string | number) => {
|
||||||
return roleMenuTreeselect(roleId).then((res): RoleMenuTree => {
|
return roleMenuTreeselect(roleId).then((res): RoleMenuTree => {
|
||||||
menuOptions.value = res.data.menus;
|
menuOptions.value = res.data.menus;
|
||||||
|
buttonsMap.value = res.data.buttonsMap ?? {};
|
||||||
return res.data;
|
return res.data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -512,11 +602,11 @@ const getRoleDeptTreeSelect = async (roleId: string | number) => {
|
|||||||
const handleCheckedTreeExpand = (value: unknown, type: string) => {
|
const handleCheckedTreeExpand = (value: unknown, type: string) => {
|
||||||
const expanded = Boolean(value);
|
const expanded = Boolean(value);
|
||||||
if (type == 'menu') {
|
if (type == 'menu') {
|
||||||
const treeList = menuOptions.value;
|
const nodeMap = menuRef.value?.store?.nodesMap as Record<string, { expanded: boolean }> | undefined;
|
||||||
for (let i = 0; i < treeList.length; i++) {
|
if (nodeMap) {
|
||||||
if (menuRef.value) {
|
Object.keys(nodeMap).forEach(nodeId => {
|
||||||
menuRef.value.store.nodesMap[treeList[i].id].expanded = expanded;
|
nodeMap[nodeId].expanded = expanded;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
} else if (type == 'dept') {
|
} else if (type == 'dept') {
|
||||||
const treeList = deptOptions.value;
|
const treeList = deptOptions.value;
|
||||||
@ -552,7 +642,7 @@ const getMenuAllCheckedKeys = (): any => {
|
|||||||
if (halfCheckedKeys) {
|
if (halfCheckedKeys) {
|
||||||
checkedKeys?.unshift(...halfCheckedKeys);
|
checkedKeys?.unshift(...halfCheckedKeys);
|
||||||
}
|
}
|
||||||
return checkedKeys;
|
return checkedKeys ?? [];
|
||||||
};
|
};
|
||||||
/** 提交按钮 */
|
/** 提交按钮 */
|
||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
@ -588,6 +678,7 @@ const handleDataScope = async (row: RoleVO) => {
|
|||||||
dialog.title = '分配权限';
|
dialog.title = '分配权限';
|
||||||
await nextTick(() => {
|
await nextTick(() => {
|
||||||
menuRef.value?.setCheckedKeys(menuRes.checkedKeys);
|
menuRef.value?.setCheckedKeys(menuRes.checkedKeys);
|
||||||
|
syncCheckedButtonIdsFromTree();
|
||||||
deptRef.value?.setCheckedKeys(res.checkedKeys);
|
deptRef.value?.setCheckedKeys(res.checkedKeys);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -609,6 +700,8 @@ const cancelDataScope = () => {
|
|||||||
form.value = { ...initForm };
|
form.value = { ...initForm };
|
||||||
permissionTab.value = 'menu';
|
permissionTab.value = 'menu';
|
||||||
menuRef.value?.setCheckedKeys([]);
|
menuRef.value?.setCheckedKeys([]);
|
||||||
|
buttonsMap.value = {};
|
||||||
|
checkedButtonIds.value = [];
|
||||||
openDataScope.value = false;
|
openDataScope.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -633,6 +726,10 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.permission-dialog-form {
|
.permission-dialog-form {
|
||||||
|
--menu-name-col-width: 220px;
|
||||||
|
/* 与 el-tree 首级「展开图标 + 勾选框」占位大致对齐,减轻表头与节点文字错位感 */
|
||||||
|
--menu-tree-leading-offset: 44px;
|
||||||
|
|
||||||
:deep(.el-tabs__header) {
|
:deep(.el-tabs__header) {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@ -643,5 +740,92 @@ onMounted(() => {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 菜单权限:表头 + 树同一容器,滚动只发生在树区域 */
|
||||||
|
.menu-tree-permission-wrap.tree-border {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 320px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-tree-permission-wrap .menu-tree-panel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 覆盖 el-tree 默认固定行高,允许节点行按按钮数量自适应高度。 */
|
||||||
|
:deep(.menu-tree-panel .el-tree-node__content) {
|
||||||
|
min-height: 28px;
|
||||||
|
height: auto;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 3px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮节点保留在树数据中参与级联,但在左侧树区域中隐藏。 */
|
||||||
|
:deep(.menu-tree-panel .el-tree-node.is-hidden-button-node) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.menu-tree-panel .el-tree-node__content.is-hidden-button-node) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-tree-permission-wrap .menu-tree-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 12px 8px calc(var(--menu-tree-leading-offset) + 8px);
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-tree-permission-wrap .menu-tree-header-name {
|
||||||
|
width: var(--menu-name-col-width);
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-tree-permission-wrap .menu-tree-header-buttons {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单与按钮同一树节点行展示,模拟树表“名称列 + 按钮列”效果。 */
|
||||||
|
:deep(.menu-tree-node-row) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.menu-tree-node-label) {
|
||||||
|
width: var(--menu-name-col-width);
|
||||||
|
flex: none;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.menu-tree-node-buttons) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
--menu-name-col-width: 160px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user