update 优化 菜单勾选栏改为左菜单右按钮结构 增加禁用与隐藏图标

This commit is contained in:
疯狂的狮子Li
2026-04-17 12:08:51 +08:00
parent f269c003da
commit 2d13215304
3 changed files with 475 additions and 106 deletions
+5 -1
View File
@@ -9,13 +9,15 @@ export interface MenuTreeOption {
parentId: string | number;
weight: number;
menuType?: MenuTypeEnum | string;
visible?: string;
status?: string;
disabled?: boolean;
children?: MenuTreeOption[];
}
export interface RoleMenuTree {
menus: MenuTreeOption[];
checkedKeys: Array<string | number>;
buttonsMap: Record<string, RoleMenuButtonOption[]>;
}
/**
@@ -26,6 +28,8 @@ export interface RoleMenuButtonOption {
menuName: string;
parentId: string | number;
perms?: string;
status?: string;
disabled?: boolean;
}
/**
+11 -2
View File
@@ -24,6 +24,7 @@
var(--app-surface-bg);
.el-dialog__header {
position: relative;
padding: 18px 20px 14px;
box-sizing: border-box;
border-bottom: 1px solid var(--app-surface-border);
@@ -40,12 +41,20 @@
}
.el-dialog__headerbtn {
top: 15px;
position: absolute;
top: 50%;
right: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
margin: 0;
border: 0;
border-radius: 10px;
background: var(--app-elevated-close-bg);
transform: translateY(-50%);
transition:
background-color 0.2s ease,
transform 0.2s ease,
@@ -58,7 +67,7 @@
&:hover {
background: var(--app-elevated-close-hover-bg);
transform: translateY(-1px);
transform: translateY(calc(-50% - 1px));
.el-dialog__close {
color: var(--app-accent-strong);
+459 -103
View File
@@ -87,14 +87,7 @@
</div>
</template>
<el-table
ref="roleTableRef"
border
class="data-table"
v-loading="loading"
:data="roleList"
@selection-change="handleSelectionChange"
>
<el-table border class="data-table" v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column v-if="false" label="角色编号" prop="roleId" width="120" />
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
@@ -229,7 +222,6 @@
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">
父子联动
</el-checkbox>
<!-- 表头与树同框避免与树内展开 + 勾选 + 名称区域错位 -->
<div class="menu-tree-permission-wrap tree-border">
<div class="menu-tree-header" role="row">
<span class="menu-tree-header-name">菜单名称</span>
@@ -244,30 +236,41 @@
node-key="id"
:check-strictly="!form.menuCheckStrictly"
empty-text="加载中请稍候"
:props="{ label: 'label', children: 'children', class: getMenuNodeClass } as any"
@check="syncCheckedButtonIdsFromTree"
:props="{ label: 'label', children: 'children', disabled: 'disabled' } as any"
@check="handleMenuTreeCheck"
>
<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
<span
class="menu-tree-node-label"
:class="{ 'is-hidden': isMenuPermissionHidden(data), 'is-disabled': data.disabled }"
:style="getMenuNodeLabelStyle(node.level)"
>
<span>{{ data.label }}</span>
<el-tooltip v-if="isMenuPermissionHidden(data)" content="隐藏" placement="top">
<el-icon class="menu-visibility-icon"><Hide /></el-icon>
</el-tooltip>
<el-tooltip v-if="data.disabled" content="停用" placement="top">
<el-icon class="menu-disabled-icon"><CircleCloseFilled /></el-icon>
</el-tooltip>
</span>
<div v-if="data.buttonPermissions?.length" class="menu-tree-node-buttons" @click.stop>
<el-checkbox
v-for="button in getMenuButtons(data.id)"
v-for="button in data.buttonPermissions"
:key="button.menuId"
:value="button.menuId"
@change="handleButtonPermissionChange($event, button.menuId)"
:model-value="isPermissionChecked(button.menuId)"
:disabled="button.disabled"
@change="handleButtonPermissionChange(data.id, button.menuId, $event)"
@click.stop
>
{{ button.menuName }}
<span class="menu-button-label" :class="{ 'is-disabled': button.disabled }">
<span>{{ button.menuName }}</span>
<el-tooltip v-if="button.disabled" content="停用" placement="top">
<el-icon class="menu-button-disabled-icon"><CircleCloseFilled /></el-icon>
</el-tooltip>
</span>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</template>
</el-tree>
@@ -319,7 +322,8 @@
<script setup name="Role" lang="ts">
import { useRouter } from 'vue-router';
import { roleMenuTreeselect, treeselect as menuTreeselect } from '@/api/system/menu/index';
import { roleMenuTreeselect } from '@/api/system/menu';
import { MenuTypeEnum } from '@/enums/MenuTypeEnum';
import { MenuTreeOption, RoleMenuButtonOption, RoleMenuTree } from '@/api/system/menu/types';
import {
addRole,
@@ -340,19 +344,36 @@ import { parseTime, addDateRange } from '@/utils/ruoyi';
const router = useRouter();
const { sys_normal_disable } = toRefs<any>(useDict('sys_normal_disable'));
interface RoleMenuPermissionOption extends MenuTreeOption {
buttonPermissions: RoleMenuButtonOption[];
disabled?: boolean;
children?: RoleMenuPermissionOption[];
}
interface RoleMenuPermissionMeta {
treeOptions: RoleMenuPermissionOption[];
buttonIds: Set<string>;
disabledButtonIds: Set<string>;
buttonParentMap: Map<string, string>;
menuAncestorMap: Map<string, Array<string | number>>;
menuButtonIdsMap: Map<string, Array<string | number>>;
}
const roleList = ref<RoleVO[]>();
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const dateRange = ref<any>(['', '']);
const menuOptions = ref<MenuTreeOption[]>([]);
/** 菜单ID到按钮列表映射(后端返回) */
const buttonsMap = ref<Record<string, RoleMenuButtonOption[]>>({});
/** 按钮权限勾选集合 */
const checkedButtonIds = ref<Array<string | number>>([]);
const menuPermissionMeta = ref<RoleMenuPermissionMeta>({
treeOptions: [],
buttonIds: new Set<string>(),
disabledButtonIds: new Set<string>(),
buttonParentMap: new Map<string, string>(),
menuAncestorMap: new Map<string, Array<string | number>>(),
menuButtonIdsMap: new Map<string, Array<string | number>>()
});
const menuExpand = ref(false);
const menuNodeAll = ref(false);
const deptExpand = ref(true);
@@ -380,47 +401,26 @@ const roleFormRef = ref<ElFormInstance>();
const dataScopeRef = ref<ElFormInstance>();
const menuRef = ref<ElTreeInstance>();
const deptRef = ref<ElTreeInstance>();
const selectedMenuPermissionIds = ref<Array<string | number>>([]);
const selectedButtonPermissionIds = ref<Array<string | number>>([]);
const lastCheckedMenuKeys = ref<string[]>([]);
const syncingMenuTree = ref(false);
/**
* 根据菜单节点ID获取其按钮列表,用于树节点行内展示。
*/
const getMenuButtons = (menuId: string | number): RoleMenuButtonOption[] => {
return buttonsMap.value[String(menuId)] ?? [];
const menuOptions = computed(() => menuPermissionMeta.value.treeOptions);
const isButtonPermission = (permissionId: string | number) => {
return menuPermissionMeta.value.buttonIds.has(String(permissionId));
};
/**
* 按钮节点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 isButtonPermissionDisabled = (permissionId: string | number) => {
return menuPermissionMeta.value.disabledButtonIds.has(String(permissionId));
};
/**
* 将树的勾选状态同步到右侧按钮勾选集合。
*/
const syncCheckedButtonIdsFromTree = () => {
const checkedKeys = (menuRef.value?.getCheckedKeys(false) ?? []) as Array<string | number>;
checkedButtonIds.value = checkedKeys.filter(id => buttonIdSet.value.has(String(id)));
const isPermissionChecked = (permissionId: string | number) => {
return selectedButtonPermissionIds.value.some(menuId => String(menuId) === String(permissionId));
};
/**
* 点击右侧按钮时,直接勾选/取消隐藏按钮节点,由树自动处理级联关系。
*/
const handleButtonPermissionChange = (checked: string | number | boolean, buttonMenuId: string | number) => {
menuRef.value?.setChecked(buttonMenuId, Boolean(checked), false);
syncCheckedButtonIdsFromTree();
};
const isMenuPermissionHidden = (menu: MenuTreeOption) => menu.visible === '1';
/**
* 基于节点层级给菜单名称增加缩进,确保按钮列始终固定对齐。
@@ -431,6 +431,329 @@ const getMenuNodeLabelStyle = (level: number) => {
};
};
const getCheckedMenuKeys = () => {
const checkedKeys = (menuRef.value?.getCheckedKeys(false) ?? []) as Array<string | number>;
const halfCheckedKeys = (menuRef.value?.getHalfCheckedKeys() ?? []) as Array<string | number>;
return {
checkedKeys,
halfCheckedKeys,
allCheckedKeys: [...halfCheckedKeys, ...checkedKeys]
};
};
const createEmptyMenuPermissionMeta = (): RoleMenuPermissionMeta => ({
treeOptions: [],
buttonIds: new Set<string>(),
disabledButtonIds: new Set<string>(),
buttonParentMap: new Map<string, string>(),
menuAncestorMap: new Map<string, Array<string | number>>(),
menuButtonIdsMap: new Map<string, Array<string | number>>()
});
const isMenuPermissionDisabledByStatus = (menu: MenuTreeOption) => menu.status === '1';
const buildRoleMenuPermissionOptions = (
menuTree: MenuTreeOption[],
disabledButtonIds: Set<string>,
buttonIds: Set<string>,
buttonParentMap: Map<string, string>,
ancestorDirectoryDisabled = false
): RoleMenuPermissionOption[] => {
return menuTree.reduce<RoleMenuPermissionOption[]>((options, menu) => {
if (menu.menuType === MenuTypeEnum.F) {
return options;
}
const selfDisabled = isMenuPermissionDisabledByStatus(menu);
const nextDirectoryDisabled = ancestorDirectoryDisabled || (menu.menuType === MenuTypeEnum.M && selfDisabled);
const nodeDisabled = ancestorDirectoryDisabled || selfDisabled;
const buttonPermissions =
menu.children
?.filter(child => child.menuType === MenuTypeEnum.F)
.map(button => {
const buttonDisabled = nodeDisabled || isMenuPermissionDisabledByStatus(button);
buttonIds.add(String(button.id));
buttonParentMap.set(String(button.id), String(menu.id));
if (buttonDisabled) {
disabledButtonIds.add(String(button.id));
}
return {
menuId: button.id,
menuName: button.label,
parentId: button.parentId,
status: button.status,
disabled: buttonDisabled
};
}) ?? [];
const children = menu.children?.length
? buildRoleMenuPermissionOptions(menu.children, disabledButtonIds, buttonIds, buttonParentMap, nextDirectoryDisabled)
: [];
options.push({
...menu,
disabled: nodeDisabled,
buttonPermissions,
children
});
return options;
}, []);
};
const buildMenuPermissionMeta = (menuTree: MenuTreeOption[]): RoleMenuPermissionMeta => {
const buttonIds = new Set<string>();
const disabledButtonIds = new Set<string>();
const buttonParentMap = new Map<string, string>();
const treeOptions = buildRoleMenuPermissionOptions(menuTree, disabledButtonIds, buttonIds, buttonParentMap);
const menuAncestorMap = new Map<string, Array<string | number>>();
const menuButtonIdsMap = new Map<string, Array<string | number>>();
const collectMenuMeta = (nodes: RoleMenuPermissionOption[], ancestors: Array<string | number> = []) => {
return nodes.reduce<Array<string | number>>((permissionIds, node) => {
menuAncestorMap.set(String(node.id), ancestors);
const childButtonIds = node.children?.length ? collectMenuMeta(node.children, [...ancestors, node.id]) : [];
const ownButtonIds = node.buttonPermissions.map(button => button.menuId);
const currentButtonIds = [...ownButtonIds, ...childButtonIds];
menuButtonIdsMap.set(String(node.id), currentButtonIds);
permissionIds.push(...currentButtonIds);
return permissionIds;
}, []);
};
collectMenuMeta(treeOptions);
return {
treeOptions,
buttonIds,
disabledButtonIds,
buttonParentMap,
menuAncestorMap,
menuButtonIdsMap
};
};
const collectMenuNodeIds = (
nodes: RoleMenuPermissionOption[] = menuOptions.value,
includeDisabled = true
): Array<string | number> => {
return nodes.reduce<Array<string | number>>((menuIds, node) => {
if (includeDisabled || !node.disabled) {
menuIds.push(node.id);
}
if (node.children?.length) {
menuIds.push(...collectMenuNodeIds(node.children, includeDisabled));
}
return menuIds;
}, []);
};
const collectButtonPermissionIds = (includeDisabled = true) => {
const buttonIds = new Set<string | number>();
menuPermissionMeta.value.menuButtonIdsMap.forEach(ids => {
ids.forEach(id => {
if (includeDisabled || !isButtonPermissionDisabled(id)) {
buttonIds.add(id);
}
});
});
return [...buttonIds];
};
const getSelectedButtonPermissionIds = () => {
return selectedButtonPermissionIds.value;
};
const getSelectedMenuPermissionIds = () => {
return selectedMenuPermissionIds.value;
};
const normalizePermissionIds = (permissionIds: Iterable<string | number>) => {
const normalizedIds = new Map<string, string | number>();
for (const permissionId of permissionIds) {
normalizedIds.set(String(permissionId), permissionId);
}
return [...normalizedIds.values()];
};
const getPermissionStateIds = () => {
return normalizePermissionIds([...getSelectedMenuPermissionIds(), ...getSelectedButtonPermissionIds()]);
};
const getDerivedMenuIdsFromButtons = (buttonIds: Iterable<string | number> = getSelectedButtonPermissionIds()) => {
if (!form.value.menuCheckStrictly) {
return [];
}
const derivedMenuIds = new Map<string, string | number>();
for (const buttonId of buttonIds) {
const parentMenuId = menuPermissionMeta.value.buttonParentMap.get(String(buttonId));
if (!parentMenuId) {
continue;
}
derivedMenuIds.set(String(parentMenuId), parentMenuId);
menuPermissionMeta.value.menuAncestorMap.get(parentMenuId)?.forEach(ancestorId => {
derivedMenuIds.set(String(ancestorId), ancestorId);
});
}
return [...derivedMenuIds.values()];
};
const setSelectedButtonPermissionIds = (permissionIds: Iterable<string | number>) => {
selectedButtonPermissionIds.value = normalizePermissionIds(permissionIds);
};
const setSelectedMenuPermissionIds = (permissionIds: Iterable<string | number>) => {
selectedMenuPermissionIds.value = normalizePermissionIds(permissionIds);
};
const syncFormMenuPermissionIds = () => {
form.value.menuIds = getMenuAllCheckedKeys();
};
const initPermissionState = (permissionIds: Array<string | number>) => {
setSelectedMenuPermissionIds(permissionIds.filter(permissionId => !isButtonPermission(permissionId)));
setSelectedButtonPermissionIds(permissionIds.filter(permissionId => isButtonPermission(permissionId)));
};
const updateMenuCheckAllState = () => {
const checkedMenuIds = new Set(getCheckedMenuKeys().checkedKeys.map(menuId => String(menuId)));
const allMenuIds = collectMenuNodeIds(menuOptions.value, false);
const allButtonIds = collectButtonPermissionIds(false);
menuNodeAll.value =
allMenuIds.length > 0 &&
allMenuIds.every(menuId => checkedMenuIds.has(String(menuId))) &&
allButtonIds.every(buttonId =>
selectedButtonPermissionIds.value.some(permissionId => String(permissionId) === String(buttonId))
);
};
const updateMenuPermissionOptions = (menuTree: MenuTreeOption[]) => {
menuPermissionMeta.value = buildMenuPermissionMeta(menuTree);
};
const getDisplayCheckedMenuKeys = (checkedKeys: Array<string | number>) => {
const displayMenuKeys = new Set<string | number>();
checkedKeys.forEach(key => {
if (form.value.menuCheckStrictly && isButtonPermission(key)) {
const parentMenuId = menuPermissionMeta.value.buttonParentMap.get(String(key));
if (parentMenuId) {
displayMenuKeys.add(parentMenuId);
}
return;
}
displayMenuKeys.add(key);
});
return [...displayMenuKeys];
};
const applyMenuTreeCheckedState = (checkedKeys: Array<string | number>) => {
const tree = menuRef.value;
if (!tree) {
return;
}
syncingMenuTree.value = true;
tree.setCheckedKeys([]);
getDisplayCheckedMenuKeys(checkedKeys).forEach(key => {
tree.setChecked(key, true, false);
});
syncingMenuTree.value = false;
};
const buildCheckedButtonPermissionIds = (
selectedButtonIds: Iterable<string | number>,
newlyCheckedMenuIds: Array<string | number> = []
) => {
const { allCheckedKeys } = getCheckedMenuKeys();
const activeMenuIds = new Set(allCheckedKeys.map(key => String(key)));
const nextButtonIds = new Set<string | number>();
for (const buttonId of selectedButtonIds) {
if (!form.value.menuCheckStrictly) {
nextButtonIds.add(buttonId);
continue;
}
const parentMenuId = menuPermissionMeta.value.buttonParentMap.get(String(buttonId));
if (parentMenuId && activeMenuIds.has(parentMenuId)) {
nextButtonIds.add(buttonId);
}
}
if (form.value.menuCheckStrictly) {
newlyCheckedMenuIds.forEach(menuId => {
menuPermissionMeta.value.menuButtonIdsMap.get(String(menuId))?.forEach(buttonId => nextButtonIds.add(buttonId));
});
}
return [...nextButtonIds];
};
const syncButtonPermissionStateFromTree = () => {
const previousCheckedMenuIds = new Set(lastCheckedMenuKeys.value);
const checkedMenuIds = getCheckedMenuKeys().checkedKeys;
const newlyCheckedMenuIds = checkedMenuIds.filter(menuId => !previousCheckedMenuIds.has(String(menuId)));
const selectedButtonIds = getSelectedButtonPermissionIds();
setSelectedButtonPermissionIds(buildCheckedButtonPermissionIds(selectedButtonIds, newlyCheckedMenuIds));
updateMenuCheckAllState();
lastCheckedMenuKeys.value = checkedMenuIds.map(key => String(key));
};
const refreshMenuTreeCheckedState = (checkedKeys: Array<string | number>) => {
applyMenuTreeCheckedState(checkedKeys);
updateMenuCheckAllState();
lastCheckedMenuKeys.value = getCheckedMenuKeys().checkedKeys.map(key => String(key));
};
const handleMenuTreeCheck = () => {
if (syncingMenuTree.value) {
return;
}
const previousEffectiveMenuIds = new Set([
...getSelectedMenuPermissionIds().map(menuId => String(menuId)),
...getDerivedMenuIdsFromButtons().map(menuId => String(menuId))
]);
const currentMenuIds = getCheckedMenuKeys().allCheckedKeys;
const currentMenuIdSet = new Set(currentMenuIds.map(menuId => String(menuId)));
const nextManualMenuIds = new Set(getSelectedMenuPermissionIds().map(menuId => String(menuId)));
currentMenuIds.forEach(menuId => {
const normalizedMenuId = String(menuId);
if (!previousEffectiveMenuIds.has(normalizedMenuId)) {
nextManualMenuIds.add(normalizedMenuId);
}
});
previousEffectiveMenuIds.forEach(menuId => {
if (!currentMenuIdSet.has(menuId)) {
nextManualMenuIds.delete(menuId);
}
});
setSelectedMenuPermissionIds(currentMenuIds.filter(menuId => nextManualMenuIds.has(String(menuId))));
syncButtonPermissionStateFromTree();
};
const handleButtonPermissionChange = (
_parentMenuId: string | number,
buttonMenuId: string | number,
checked: string | number | boolean
) => {
if (isButtonPermissionDisabled(buttonMenuId)) {
return;
}
const selectedButtonIds = new Set(getSelectedButtonPermissionIds());
if (checked) {
selectedButtonIds.add(buttonMenuId);
} else {
selectedButtonIds.delete(buttonMenuId);
}
setSelectedButtonPermissionIds(selectedButtonIds);
refreshMenuTreeCheckedState(getPermissionStateIds());
};
const initForm: RoleForm = {
roleId: undefined,
roleSort: 1,
@@ -516,7 +839,6 @@ const handleExport = () => {
const handleSelectionChange = (selection: RoleVO[]) => {
ids.value = selection.map((item: RoleVO) => item.roleId);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 角色状态修改 */
@@ -536,11 +858,6 @@ const handleAuthUser = (row: RoleVO) => {
router.push('/system/role-auth/user/' + row.roleId);
};
/** 查询菜单树结构 */
const getMenuTreeselect = async () => {
const res = await menuTreeselect();
menuOptions.value = res.data;
};
/** 所有部门节点数据 */
const getDeptAllCheckedKeys = (): any => {
// 目前被选中的部门节点
@@ -555,8 +872,11 @@ const getDeptAllCheckedKeys = (): any => {
/** 重置新增的表单以及其他数据 */
const reset = () => {
menuRef.value?.setCheckedKeys([]);
buttonsMap.value = {};
checkedButtonIds.value = [];
menuPermissionMeta.value = createEmptyMenuPermissionMeta();
setSelectedMenuPermissionIds([]);
setSelectedButtonPermissionIds([]);
lastCheckedMenuKeys.value = [];
syncingMenuTree.value = false;
menuExpand.value = false;
menuNodeAll.value = false;
deptExpand.value = true;
@@ -587,8 +907,7 @@ const handleUpdate = async (row?: RoleVO) => {
/** 根据角色ID查询菜单树结构 */
const getRoleMenuTreeselect = (roleId: string | number) => {
return roleMenuTreeselect(roleId).then((res): RoleMenuTree => {
menuOptions.value = res.data.menus;
buttonsMap.value = res.data.buttonsMap ?? {};
updateMenuPermissionOptions(res.data.menus);
return res.data;
});
};
@@ -620,35 +939,32 @@ const handleCheckedTreeExpand = (value: unknown, type: string) => {
/** 树权限(全选/全不选) */
const handleCheckedTreeNodeAll = (value: any, type: string) => {
if (type == 'menu') {
menuRef.value?.setCheckedNodes(value ? (menuOptions.value as any) : []);
setSelectedMenuPermissionIds(value ? collectMenuNodeIds(menuOptions.value, false) : []);
setSelectedButtonPermissionIds(value ? collectButtonPermissionIds(false) : []);
refreshMenuTreeCheckedState(getPermissionStateIds());
} else if (type == 'dept') {
deptRef.value?.setCheckedNodes(value ? (deptOptions.value as any) : []);
}
};
/** 树权限(父子联动) */
const handleCheckedTreeConnect = (value: any, type: string) => {
const handleCheckedTreeConnect = async (value: any, type: string) => {
if (type == 'menu') {
form.value.menuCheckStrictly = value;
await nextTick();
refreshMenuTreeCheckedState(getPermissionStateIds());
} else if (type == 'dept') {
form.value.deptCheckStrictly = value;
}
};
/** 所有菜单节点数据 */
const getMenuAllCheckedKeys = (): any => {
// 目前被选中的菜单节点
const checkedKeys = menuRef.value?.getCheckedKeys();
// 半选中的菜单节点
const halfCheckedKeys = menuRef.value?.getHalfCheckedKeys();
if (halfCheckedKeys) {
checkedKeys?.unshift(...halfCheckedKeys);
}
return checkedKeys ?? [];
return normalizePermissionIds([...getCheckedMenuKeys().allCheckedKeys, ...getSelectedButtonPermissionIds()]);
};
/** 提交按钮 */
const submitForm = () => {
roleFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
form.value.menuIds = getMenuAllCheckedKeys();
syncFormMenuPermissionIds();
form.value.roleId ? await updateRole(form.value) : await addRole(form.value);
modal.msgSuccess('操作成功');
dialog.visible = false;
@@ -677,16 +993,19 @@ const handleDataScope = async (row: RoleVO) => {
openDataScope.value = true;
dialog.title = '分配权限';
await nextTick(() => {
menuRef.value?.setCheckedKeys(menuRes.checkedKeys);
syncCheckedButtonIdsFromTree();
initPermissionState(menuRes.checkedKeys);
syncFormMenuPermissionIds();
refreshMenuTreeCheckedState(getPermissionStateIds());
deptRef.value?.setCheckedKeys(res.checkedKeys);
handleCheckedTreeExpand(menuExpand.value, 'menu');
handleCheckedTreeExpand(deptExpand.value, 'dept');
});
};
/** 提交按钮(数据权限) */
const submitDataScope = async () => {
if (form.value.roleId) {
// 权限信息统一提交:菜单权限 + 数据权限。
form.value.menuIds = getMenuAllCheckedKeys();
syncFormMenuPermissionIds();
form.value.deptIds = getDeptAllCheckedKeys();
await updateRolePermission(form.value);
modal.msgSuccess('修改成功');
@@ -700,8 +1019,15 @@ const cancelDataScope = () => {
form.value = { ...initForm };
permissionTab.value = 'menu';
menuRef.value?.setCheckedKeys([]);
buttonsMap.value = {};
checkedButtonIds.value = [];
menuPermissionMeta.value = createEmptyMenuPermissionMeta();
setSelectedMenuPermissionIds([]);
setSelectedButtonPermissionIds([]);
lastCheckedMenuKeys.value = [];
syncingMenuTree.value = false;
menuExpand.value = false;
menuNodeAll.value = false;
deptExpand.value = true;
deptNodeAll.value = false;
openDataScope.value = false;
};
@@ -727,7 +1053,6 @@ onMounted(() => {
.permission-dialog-form {
--menu-name-col-width: 220px;
/* 与 el-tree 首级「展开图标 + 勾选框」占位大致对齐,减轻表头与节点文字错位感 */
--menu-tree-leading-offset: 44px;
:deep(.el-tabs__header) {
@@ -758,7 +1083,6 @@ onMounted(() => {
overflow: auto;
}
/* 覆盖 el-tree 默认固定行高,允许节点行按按钮数量自适应高度。 */
:deep(.menu-tree-panel .el-tree-node__content) {
min-height: 28px;
height: auto;
@@ -767,15 +1091,6 @@ onMounted(() => {
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;
@@ -800,7 +1115,6 @@ onMounted(() => {
min-width: 0;
}
/* 菜单与按钮同一树节点行展示,模拟树表“名称列 + 按钮列”效果。 */
:deep(.menu-tree-node-row) {
width: 100%;
min-height: 22px;
@@ -812,11 +1126,36 @@ onMounted(() => {
:deep(.menu-tree-node-label) {
width: var(--menu-name-col-width);
flex: none;
display: inline-flex;
align-items: center;
gap: 6px;
line-height: 24px;
color: var(--el-text-color-primary);
box-sizing: border-box;
}
:deep(.menu-tree-node-label.is-hidden:not(.is-disabled)) {
color: var(--el-text-color-secondary);
}
:deep(.menu-tree-node-label.is-disabled) {
color: var(--el-color-danger);
}
:deep(.menu-visibility-icon) {
flex: 0 0 auto;
margin-top: 1px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
:deep(.menu-disabled-icon) {
flex: 0 0 auto;
margin-top: 1px;
font-size: 14px;
color: var(--el-color-danger);
}
:deep(.menu-tree-node-buttons) {
flex: 1;
display: flex;
@@ -824,6 +1163,23 @@ onMounted(() => {
gap: 8px 12px;
}
:deep(.menu-button-label) {
display: inline-flex;
align-items: center;
gap: 4px;
}
:deep(.menu-button-label.is-disabled) {
color: var(--el-color-danger);
}
:deep(.menu-button-disabled-icon) {
flex: 0 0 auto;
margin-top: 1px;
font-size: 14px;
color: var(--el-color-danger);
}
@media (max-width: 900px) {
--menu-name-col-width: 160px;
}