mirror of
https://gitee.com/bootx/dax-pay-ui
synced 2026-08-12 23:45:39 +08:00
feat(admin): 菜单支持子页面分组并完善类型编辑体验
新增 subpage_group 类型与路由/列表/编辑支持;编辑表单按类型展示功能说明,且编辑/查看时锁定菜单类型(全量展示对照)。
This commit is contained in:
@@ -44,6 +44,41 @@ function resolveMenuTitle(menu: PermMenuResult): string {
|
||||
return menu.titleCn || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 面包屑链项
|
||||
*/
|
||||
interface BreadcrumbChainItem {
|
||||
icon?: string;
|
||||
path?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 id → 祖先链(含自身)的映射
|
||||
* 子页面路由提升后会丢失 menu 父级,提升前用原始树构建完整层级链,供面包屑补全
|
||||
*/
|
||||
function buildBreadcrumbChainMap(menus: PermMenuResult[]): Map<string, BreadcrumbChainItem[]> {
|
||||
const map = new Map<string, BreadcrumbChainItem[]>();
|
||||
function walk(nodes: PermMenuResult[], ancestors: BreadcrumbChainItem[]) {
|
||||
for (const node of nodes) {
|
||||
const item: BreadcrumbChainItem = {
|
||||
title: resolveMenuTitle(node),
|
||||
path: node.path || undefined,
|
||||
icon: node.icon || undefined,
|
||||
};
|
||||
const chain = [...ancestors, item];
|
||||
if (node.id) {
|
||||
map.set(node.id, chain);
|
||||
}
|
||||
if (node.children?.length) {
|
||||
walk(node.children, chain);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(menus, []);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 menuType 过滤按钮类型节点(按钮不生成可导航路由,仅作权限标识)
|
||||
*/
|
||||
@@ -76,8 +111,8 @@ function filterPermissionAnchorMenus(menus: PermMenuResult[]): PermMenuResult[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个菜单节点,提取其子菜单中的子页面并提升到当前层级
|
||||
* 中文:子页面在数据库中作为菜单的子节点维护,但路由生成时需要提升到目录级别
|
||||
* 处理单个菜单节点,提取其子菜单中的子页面/子页面分组并提升到当前层级
|
||||
* 中文:子页面/子页面分组在数据库中作为菜单的子节点维护,但路由生成时需要提升到目录级别
|
||||
*/
|
||||
function processMenuNode(menu: PermMenuResult): PermMenuResult {
|
||||
if (!menu.children || menu.children.length === 0) {
|
||||
@@ -85,24 +120,30 @@ function processMenuNode(menu: PermMenuResult): PermMenuResult {
|
||||
}
|
||||
|
||||
const processedChildren: PermMenuResult[] = [];
|
||||
const liftedSubpages: PermMenuResult[] = [];
|
||||
// 从 menu 类型子节点中提升出来的子页面类节点(subpage / subpage_group)
|
||||
const lifted: PermMenuResult[] = [];
|
||||
|
||||
for (const child of menu.children) {
|
||||
const processedChild = processMenuNode(child);
|
||||
|
||||
if (processedChild.children && processedChild.children.length > 0) {
|
||||
const subpages = processedChild.children.filter((c) => c.menuType === 'subpage');
|
||||
const nonSubpages = processedChild.children.filter((c) => c.menuType !== 'subpage');
|
||||
// 仅 menu 类型节点下的子页面类需要提升到目录层级;
|
||||
// subpage_group 下的 subpage 保留不动(分组整体提升,保持 group>subpage 嵌套用于面包屑)
|
||||
if (processedChild.menuType === 'menu' && processedChild.children && processedChild.children.length > 0) {
|
||||
const liftable = processedChild.children.filter(
|
||||
(c) => c.menuType === 'subpage' || c.menuType === 'subpage_group',
|
||||
);
|
||||
const remaining = processedChild.children.filter(
|
||||
(c) => c.menuType !== 'subpage' && c.menuType !== 'subpage_group',
|
||||
);
|
||||
|
||||
liftedSubpages.push(...subpages);
|
||||
|
||||
processedChild.children = nonSubpages.length > 0 ? nonSubpages : undefined;
|
||||
lifted.push(...liftable);
|
||||
processedChild.children = remaining.length > 0 ? remaining : undefined;
|
||||
}
|
||||
|
||||
processedChildren.push(processedChild);
|
||||
}
|
||||
|
||||
const finalChildren = [...processedChildren, ...liftedSubpages];
|
||||
const finalChildren = [...processedChildren, ...lifted];
|
||||
|
||||
return {
|
||||
...menu,
|
||||
@@ -128,7 +169,10 @@ function extractAndLiftSubpages(menus: PermMenuResult[]): PermMenuResult[] {
|
||||
* - link:外链路由,写入 meta.link + meta.external
|
||||
* - button:不过滤(已在入参层移除),保留子级处理
|
||||
*/
|
||||
function convertMenuToRoute(menu: PermMenuResult): RouteRecordStringComponent {
|
||||
function convertMenuToRoute(
|
||||
menu: PermMenuResult,
|
||||
breadcrumbMap?: Map<string, BreadcrumbChainItem[]>,
|
||||
): RouteRecordStringComponent {
|
||||
const title = resolveMenuTitle(menu);
|
||||
const menuType = menu.menuType || 'menu';
|
||||
|
||||
@@ -150,7 +194,7 @@ function convertMenuToRoute(menu: PermMenuResult): RouteRecordStringComponent {
|
||||
badgeType: (menu.badgeType as 'dot' | 'normal') || 'normal',
|
||||
badgeVariants: menu.badgeVariants || 'subtle',
|
||||
},
|
||||
children: menu.children ? menu.children.map((child) => convertMenuToRoute(child)) : undefined,
|
||||
children: menu.children ? menu.children.map((child) => convertMenuToRoute(child, breadcrumbMap)) : undefined,
|
||||
};
|
||||
|
||||
// 根据 menuType 分支处理
|
||||
@@ -182,6 +226,20 @@ function convertMenuToRoute(menu: PermMenuResult): RouteRecordStringComponent {
|
||||
// 子页面:使用 component,强制隐藏菜单
|
||||
route.component = menu.component || '';
|
||||
route.meta!.hideInMenu = true;
|
||||
// 注入完整面包屑链(含 catalog/menu/group),弥补路由提升后 matched 缺失 menu 层
|
||||
if (menu.id && breadcrumbMap?.has(menu.id)) {
|
||||
route.meta!.customBreadcrumb = breadcrumbMap.get(menu.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'subpage_group': {
|
||||
// 子页面分组:透明容器(无组件),强制隐藏菜单
|
||||
// 分组无 path,用 id 派生唯一 name/path(有 children 时 component 会被框架移除成为透明容器)
|
||||
route.name = `subpage-group-${menu.id}`;
|
||||
route.component = '';
|
||||
route.path = `/_subpage-group/${menu.id}`;
|
||||
route.meta!.hideInMenu = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -201,8 +259,10 @@ function convertMenuToRoute(menu: PermMenuResult): RouteRecordStringComponent {
|
||||
*/
|
||||
export function convertMenuListToRoutes(menus: PermMenuResult[]): RouteRecordStringComponent[] {
|
||||
const filtered = filterPermissionAnchorMenus(filterButtonMenus(menus));
|
||||
// 提升前构建面包屑祖先链映射(提升会破坏 menu→subpage 父子关系,必须在提升前建立)
|
||||
const breadcrumbMap = buildBreadcrumbChainMap(filtered);
|
||||
const processed = extractAndLiftSubpages(filtered);
|
||||
return processed.map((menu) => convertMenuToRoute(menu));
|
||||
return processed.map((menu) => convertMenuToRoute(menu, breadcrumbMap));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -77,8 +77,43 @@ function filterPermissionAnchorMenus(menus: PermMenu[]): PermMenu[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个菜单节点,提取其子菜单中的子页面并提升到当前层级
|
||||
* 中文:子页面在数据库中作为菜单的子节点维护,但路由生成时需要提升到目录级别
|
||||
* 面包屑链项
|
||||
*/
|
||||
interface BreadcrumbChainItem {
|
||||
icon?: string;
|
||||
path?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 id → 祖先链(含自身)的映射
|
||||
* 子页面路由提升后会丢失 menu 父级,提升前用原始树构建完整层级链,供面包屑补全
|
||||
*/
|
||||
function buildBreadcrumbChainMap(menus: PermMenu[]): Map<string, BreadcrumbChainItem[]> {
|
||||
const map = new Map<string, BreadcrumbChainItem[]>();
|
||||
function walk(nodes: PermMenu[], ancestors: BreadcrumbChainItem[]) {
|
||||
for (const node of nodes) {
|
||||
const item: BreadcrumbChainItem = {
|
||||
title: resolveMenuTitle(node),
|
||||
path: node.path || undefined,
|
||||
icon: node.icon || undefined,
|
||||
};
|
||||
const chain = [...ancestors, item];
|
||||
if (node.id) {
|
||||
map.set(node.id, chain);
|
||||
}
|
||||
if (node.children?.length) {
|
||||
walk(node.children, chain);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(menus, []);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个菜单节点,提取其子菜单中的子页面/子页面分组并提升到当前层级
|
||||
* 中文:子页面/子页面分组在数据库中作为菜单的子节点维护,但路由生成时需要提升到目录级别
|
||||
*/
|
||||
function processMenuNode(menu: PermMenu): PermMenu {
|
||||
if (!menu.children || menu.children.length === 0) {
|
||||
@@ -86,24 +121,34 @@ function processMenuNode(menu: PermMenu): PermMenu {
|
||||
}
|
||||
|
||||
const processedChildren: PermMenu[] = [];
|
||||
const liftedSubpages: PermMenu[] = [];
|
||||
// 从 menu 类型子节点中提升出来的子页面类节点(subpage / subpage_group)
|
||||
const lifted: PermMenu[] = [];
|
||||
|
||||
for (const child of menu.children) {
|
||||
const processedChild = processMenuNode(child);
|
||||
|
||||
if (processedChild.children && processedChild.children.length > 0) {
|
||||
const subpages = processedChild.children.filter((c) => c.menuType === MenuTypeEnum.SUBPAGE);
|
||||
const nonSubpages = processedChild.children.filter((c) => c.menuType !== MenuTypeEnum.SUBPAGE);
|
||||
// 仅 menu 类型节点下的子页面类需要提升到目录层级;
|
||||
// subpage_group 下的 subpage 保留不动(分组整体提升,保持 group>subpage 嵌套用于面包屑)
|
||||
if (
|
||||
processedChild.menuType === MenuTypeEnum.MENU &&
|
||||
processedChild.children &&
|
||||
processedChild.children.length > 0
|
||||
) {
|
||||
const liftable = processedChild.children.filter(
|
||||
(c) => c.menuType === MenuTypeEnum.SUBPAGE || c.menuType === MenuTypeEnum.SUBPAGE_GROUP,
|
||||
);
|
||||
const remaining = processedChild.children.filter(
|
||||
(c) => c.menuType !== MenuTypeEnum.SUBPAGE && c.menuType !== MenuTypeEnum.SUBPAGE_GROUP,
|
||||
);
|
||||
|
||||
liftedSubpages.push(...subpages);
|
||||
|
||||
processedChild.children = nonSubpages.length > 0 ? nonSubpages : undefined;
|
||||
lifted.push(...liftable);
|
||||
processedChild.children = remaining.length > 0 ? remaining : undefined;
|
||||
}
|
||||
|
||||
processedChildren.push(processedChild);
|
||||
}
|
||||
|
||||
const finalChildren = [...processedChildren, ...liftedSubpages];
|
||||
const finalChildren = [...processedChildren, ...lifted];
|
||||
|
||||
return {
|
||||
...menu,
|
||||
@@ -129,7 +174,10 @@ function extractAndLiftSubpages(menus: PermMenu[]): PermMenu[] {
|
||||
* - link:外链路由,写入 meta.link + meta.external
|
||||
* - button:不过滤(已在入参层移除),保留子级处理
|
||||
*/
|
||||
function convertMenuToRoute(menu: PermMenu): RouteRecordStringComponent {
|
||||
function convertMenuToRoute(
|
||||
menu: PermMenu,
|
||||
breadcrumbMap?: Map<string, BreadcrumbChainItem[]>,
|
||||
): RouteRecordStringComponent {
|
||||
const title = resolveMenuTitle(menu);
|
||||
const menuType = menu.menuType || MenuTypeEnum.MENU;
|
||||
|
||||
@@ -151,7 +199,7 @@ function convertMenuToRoute(menu: PermMenu): RouteRecordStringComponent {
|
||||
badgeType: (menu.badgeType as 'dot' | 'normal') || 'normal',
|
||||
badgeVariants: menu.badgeVariants || 'subtle',
|
||||
},
|
||||
children: menu.children ? menu.children.map((child) => convertMenuToRoute(child)) : undefined,
|
||||
children: menu.children ? menu.children.map((child) => convertMenuToRoute(child, breadcrumbMap)) : undefined,
|
||||
};
|
||||
|
||||
// 根据 menuType 分支处理
|
||||
@@ -183,6 +231,20 @@ function convertMenuToRoute(menu: PermMenu): RouteRecordStringComponent {
|
||||
// 子页面:使用 component,强制隐藏菜单
|
||||
route.component = menu.component || '';
|
||||
route.meta!.hideInMenu = true;
|
||||
// 注入完整面包屑链(含 catalog/menu/group),弥补路由提升后 matched 缺失 menu 层
|
||||
if (menu.id && breadcrumbMap?.has(menu.id)) {
|
||||
route.meta!.customBreadcrumb = breadcrumbMap.get(menu.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuTypeEnum.SUBPAGE_GROUP: {
|
||||
// 子页面分组:透明容器(无组件),强制隐藏菜单
|
||||
// 分组无 path,用 id 派生唯一 name/path(有 children 时 component 会被框架移除成为透明容器)
|
||||
route.name = `subpage-group-${menu.id}`;
|
||||
route.component = '';
|
||||
route.path = `/_subpage-group/${menu.id}`;
|
||||
route.meta!.hideInMenu = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -202,8 +264,10 @@ function convertMenuToRoute(menu: PermMenu): RouteRecordStringComponent {
|
||||
*/
|
||||
export function convertMenuListToRoutes(menus: PermMenu[]): RouteRecordStringComponent[] {
|
||||
const filtered = filterPermissionAnchorMenus(filterButtonMenus(menus));
|
||||
// 提升前构建面包屑祖先链映射(提升会破坏 menu→subpage 父子关系,必须在提升前建立)
|
||||
const breadcrumbMap = buildBreadcrumbChainMap(filtered);
|
||||
const processed = extractAndLiftSubpages(filtered);
|
||||
return processed.map((menu) => convertMenuToRoute(menu));
|
||||
return processed.map((menu) => convertMenuToRoute(menu, breadcrumbMap));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,8 @@ export const MenuTypeEnum = {
|
||||
MENU: 'menu',
|
||||
/** 子页面 */
|
||||
SUBPAGE: 'subpage',
|
||||
/** 子页面分组 */
|
||||
SUBPAGE_GROUP: 'subpage_group',
|
||||
} as const;
|
||||
|
||||
export type MenuTypeEnum = (typeof MenuTypeEnum)[keyof typeof MenuTypeEnum];
|
||||
@@ -23,6 +25,7 @@ export const menuTypeOptions: { label: string; value: string }[] = [
|
||||
{ label: 'iam.menu.typeCatalog', value: MenuTypeEnum.CATALOG },
|
||||
{ label: 'iam.menu.typeMenu', value: MenuTypeEnum.MENU },
|
||||
{ label: 'iam.menu.typeSubpage', value: MenuTypeEnum.SUBPAGE },
|
||||
{ label: 'iam.menu.typeSubpageGroup', value: MenuTypeEnum.SUBPAGE_GROUP },
|
||||
{ label: 'iam.menu.typeEmbedded', value: MenuTypeEnum.EMBEDDED },
|
||||
{ label: 'iam.menu.typeLink', value: MenuTypeEnum.LINK },
|
||||
];
|
||||
@@ -34,6 +37,7 @@ export const menuTypeColorMap: Record<string, string> = {
|
||||
[MenuTypeEnum.CATALOG]: 'blue',
|
||||
[MenuTypeEnum.MENU]: 'green',
|
||||
[MenuTypeEnum.SUBPAGE]: 'cyan',
|
||||
[MenuTypeEnum.SUBPAGE_GROUP]: 'geekblue',
|
||||
[MenuTypeEnum.EMBEDDED]: 'orange',
|
||||
[MenuTypeEnum.LINK]: 'purple',
|
||||
};
|
||||
@@ -45,6 +49,19 @@ export const menuTypeI18nMap: Record<string, string> = {
|
||||
[MenuTypeEnum.CATALOG]: 'iam.menu.typeCatalog',
|
||||
[MenuTypeEnum.MENU]: 'iam.menu.typeMenu',
|
||||
[MenuTypeEnum.SUBPAGE]: 'iam.menu.typeSubpage',
|
||||
[MenuTypeEnum.SUBPAGE_GROUP]: 'iam.menu.typeSubpageGroup',
|
||||
[MenuTypeEnum.EMBEDDED]: 'iam.menu.typeEmbedded',
|
||||
[MenuTypeEnum.LINK]: 'iam.menu.typeLink',
|
||||
};
|
||||
|
||||
/**
|
||||
* 菜单类型功能说明国际化Key映射(编辑表单顶部 Alert)
|
||||
*/
|
||||
export const menuTypeTipI18nMap: Record<string, string> = {
|
||||
[MenuTypeEnum.CATALOG]: 'iam.menu.typeCatalogTip',
|
||||
[MenuTypeEnum.MENU]: 'iam.menu.typeMenuTip',
|
||||
[MenuTypeEnum.SUBPAGE]: 'iam.menu.typeSubpageTip',
|
||||
[MenuTypeEnum.SUBPAGE_GROUP]: 'iam.menu.typeSubpageGroupTip',
|
||||
[MenuTypeEnum.EMBEDDED]: 'iam.menu.typeEmbeddedTip',
|
||||
[MenuTypeEnum.LINK]: 'iam.menu.typeLinkTip',
|
||||
};
|
||||
|
||||
@@ -19,22 +19,34 @@
|
||||
"subpageOverviewTab": "Subpage Overview",
|
||||
"subpageOverviewSearch": "Search subpage, parent menu or catalog",
|
||||
"subpageList": "Subpages",
|
||||
"subpageManage": "Subpage Management",
|
||||
"childMenuList": "Child Menus",
|
||||
"selectNodeHint": "Select a catalog or menu on the left to view its children",
|
||||
"addSubpage": "Add Subpage",
|
||||
"editSubpage": "Edit Subpage",
|
||||
"viewSubpage": "View Subpage",
|
||||
"addSubpageGroup": "Add Group",
|
||||
"editSubpageGroup": "Edit Group",
|
||||
"viewSubpageGroup": "View Group",
|
||||
"subpagePathExtra": "Usually inherits the parent menu path prefix; adjust as needed for routing",
|
||||
"subpageComponentExtra": "Vue component path rendered for this subpage",
|
||||
"subpageCount": "Subpages({count})",
|
||||
"subpageGroupCount": "Groups({count})",
|
||||
"parentCatalog": "Catalog",
|
||||
"confirmDelete": "Confirm delete this record?",
|
||||
"menuType": "Menu Type",
|
||||
"typeCatalog": "Catalog",
|
||||
"typeMenu": "Menu",
|
||||
"typeSubpage": "Subpage",
|
||||
"typeSubpageGroup": "Subpage Group",
|
||||
"typeEmbedded": "Embedded",
|
||||
"typeLink": "Link",
|
||||
"typeCatalogTip": "Sidebar group container; does not render a page. Can hold catalogs, menus, embedded pages, or links",
|
||||
"typeMenuTip": "Clickable page entry with path and component. Can hold subpages or subpage groups",
|
||||
"typeSubpageTip": "Hidden route under a menu or group; not shown in the sidebar (e.g. detail/edit pages)",
|
||||
"typeSubpageGroupTip": "Logical group for subpages; no page render and not shown in the sidebar",
|
||||
"typeEmbeddedTip": "Embed an external page via iframe inside the system; requires an iframe URL",
|
||||
"typeLinkTip": "Open an external URL in a new window; requires a link URL",
|
||||
"parentMenu": "Parent Menu",
|
||||
"selectParent": "Please select parent menu",
|
||||
"titleCn": "Title",
|
||||
|
||||
@@ -19,22 +19,34 @@
|
||||
"subpageOverviewTab": "子页面总览",
|
||||
"subpageOverviewSearch": "搜索子页面、所属菜单或目录",
|
||||
"subpageList": "子页面列表",
|
||||
"subpageManage": "子页面管理",
|
||||
"childMenuList": "子菜单列表",
|
||||
"selectNodeHint": "请在左侧选择目录或菜单,查看其子项",
|
||||
"addSubpage": "新增子页面",
|
||||
"editSubpage": "编辑子页面",
|
||||
"viewSubpage": "查看子页面",
|
||||
"addSubpageGroup": "新增分组",
|
||||
"editSubpageGroup": "编辑分组",
|
||||
"viewSubpageGroup": "查看分组",
|
||||
"subpagePathExtra": "通常继承父菜单路径前缀,可按实际路由调整",
|
||||
"subpageComponentExtra": "子页面实际渲染的 Vue 组件路径",
|
||||
"subpageCount": "子页面({count})",
|
||||
"subpageGroupCount": "分组({count})",
|
||||
"parentCatalog": "所属目录",
|
||||
"confirmDelete": "是否删除该条数据",
|
||||
"menuType": "菜单类型",
|
||||
"typeCatalog": "目录",
|
||||
"typeMenu": "菜单",
|
||||
"typeSubpage": "子页面",
|
||||
"typeSubpageGroup": "子页面分组",
|
||||
"typeEmbedded": "内嵌页",
|
||||
"typeLink": "外链",
|
||||
"typeCatalogTip": "侧栏分组容器,本身不渲染页面;可挂下级目录、菜单、内嵌页或外链",
|
||||
"typeMenuTip": "可点击的页面入口,需配置路径与组件;下级可挂子页面或子页面分组",
|
||||
"typeSubpageTip": "挂在菜单或分组下的隐藏路由页,不出现在侧栏,常用于详情、编辑等",
|
||||
"typeSubpageGroupTip": "子页面的逻辑分组容器,不渲染页面、不显示在侧栏",
|
||||
"typeEmbeddedTip": "系统内通过 iframe 嵌入外部页面,需填写内嵌地址",
|
||||
"typeLinkTip": "新窗口打开外部链接,需填写外链地址",
|
||||
"parentMenu": "上级菜单",
|
||||
"selectParent": "请选择父级菜单",
|
||||
"titleCn": "标题",
|
||||
|
||||
@@ -11,7 +11,12 @@
|
||||
import { menuTypeColorMap, MenuTypeEnum, menuTypeI18nMap } from '#/enums/menuType';
|
||||
import { usePermission } from '#/hooks/usePermission';
|
||||
|
||||
import { getDirectMenusUnderCatalog, getDirectSubpages, matchesMenuKeyword } from './menu-tree.util';
|
||||
import {
|
||||
getDirectMenuChildren,
|
||||
getDirectMenusUnderCatalog,
|
||||
getDirectSubpages,
|
||||
matchesMenuKeyword,
|
||||
} from './menu-tree.util';
|
||||
|
||||
const props = defineProps<{
|
||||
clientCode: string;
|
||||
@@ -24,6 +29,7 @@
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
addGroup: [parent: Menu];
|
||||
addSubpage: [parent: Menu];
|
||||
delete: [row: Menu];
|
||||
edit: [row: Menu];
|
||||
@@ -43,13 +49,16 @@
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 面板模式:menu 下子页面 / catalog 下直属菜单
|
||||
const panelMode = computed<'catalogMenus' | 'menuSubpages' | 'none'>(() => {
|
||||
// 面板模式:catalog 下直属菜单 / menu 下分组+子页面 / 分组下子页面
|
||||
const panelMode = computed<'catalogMenus' | 'groupSubpages' | 'menuChildren' | 'none'>(() => {
|
||||
if (!props.selectedNode?.menuType) {
|
||||
return 'none';
|
||||
}
|
||||
if (props.selectedNode.menuType === MenuTypeEnum.MENU) {
|
||||
return 'menuSubpages';
|
||||
return 'menuChildren';
|
||||
}
|
||||
if (props.selectedNode.menuType === MenuTypeEnum.SUBPAGE_GROUP) {
|
||||
return 'groupSubpages';
|
||||
}
|
||||
if (props.selectedNode.menuType === MenuTypeEnum.CATALOG) {
|
||||
return 'catalogMenus';
|
||||
@@ -62,7 +71,10 @@
|
||||
return '';
|
||||
}
|
||||
const name = getDisplayTitle(props.selectedNode);
|
||||
if (panelMode.value === 'menuSubpages') {
|
||||
if (panelMode.value === 'menuChildren') {
|
||||
return `${$t('iam.menu.subpageManage')} - ${name}`;
|
||||
}
|
||||
if (panelMode.value === 'groupSubpages') {
|
||||
return `${$t('iam.menu.subpageList')} - ${name}`;
|
||||
}
|
||||
if (panelMode.value === 'catalogMenus') {
|
||||
@@ -75,7 +87,10 @@
|
||||
if (!props.selectedNode || panelMode.value === 'none') {
|
||||
return [] as Menu[];
|
||||
}
|
||||
if (panelMode.value === 'menuSubpages') {
|
||||
if (panelMode.value === 'menuChildren') {
|
||||
return getDirectMenuChildren(props.selectedNode, props.menuMap);
|
||||
}
|
||||
if (panelMode.value === 'groupSubpages') {
|
||||
return getDirectSubpages(props.selectedNode, props.menuMap);
|
||||
}
|
||||
return getDirectMenusUnderCatalog(props.selectedNode, props.menuMap);
|
||||
@@ -212,7 +227,7 @@
|
||||
<template>
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<template v-if="panelMode === 'none'">
|
||||
<a-empty class="mt-16" :description="$t('iam.menu.selectNodeHint')" />
|
||||
<a-empty class="!mt-16" :description="$t('iam.menu.selectNodeHint')" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="mb-3 shrink-0 font-medium">{{ panelTitle }}</div>
|
||||
@@ -226,7 +241,17 @@
|
||||
style="width: 280px"
|
||||
/>
|
||||
<a-button
|
||||
v-if="panelMode === 'menuSubpages' && hasPermission(PermCodes.Iam.PermMenu.MANAGE)"
|
||||
v-if="panelMode === 'menuChildren' && hasPermission(PermCodes.Iam.PermMenu.MANAGE)"
|
||||
type="primary"
|
||||
@click="emits('addGroup', selectedNode!)"
|
||||
>
|
||||
{{ $t('iam.menu.addSubpageGroup') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="
|
||||
(panelMode === 'menuChildren' || panelMode === 'groupSubpages') &&
|
||||
hasPermission(PermCodes.Iam.PermMenu.MANAGE)
|
||||
"
|
||||
type="primary"
|
||||
@click="emits('addSubpage', selectedNode!)"
|
||||
>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { type Menu, MenuApi } from '#/api/iam/perm/menu.api';
|
||||
import { IconPicker } from '#/components/icon-picker';
|
||||
import { FormEditType } from '#/enums/formEditType';
|
||||
import { MenuTypeEnum, menuTypeOptions } from '#/enums/menuType';
|
||||
import { MenuTypeEnum, menuTypeOptions, menuTypeTipI18nMap } from '#/enums/menuType';
|
||||
import { useFormEdit } from '#/hooks/useFormEdit';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
import { useValidate } from '#/hooks/useValidate';
|
||||
@@ -23,6 +23,8 @@
|
||||
clientCode?: string;
|
||||
parentRow?: Menu;
|
||||
parentMenuType?: string;
|
||||
/** 外部预设菜单类型(如"添加分组"按钮强制 subpage_group) */
|
||||
defaultMenuType?: string;
|
||||
}
|
||||
|
||||
const { initFormEditType, handleCancel, visible, title, confirmLoading, showable, formEditType } = useFormEdit();
|
||||
@@ -42,9 +44,10 @@
|
||||
// 父级菜单树数据
|
||||
const treeData = ref<any[]>([]);
|
||||
|
||||
// 可用的菜单类型选项(根据父菜单类型动态计算)
|
||||
// 可用的菜单类型选项(编辑/查看展示全部并锁定;新增时根据父菜单类型动态计算)
|
||||
const availableMenuTypeOptions = computed(() => {
|
||||
if (formEditType.value === FormEditType.Edit && form.value.menuType === MenuTypeEnum.SUBPAGE) {
|
||||
// 编辑/查看:展示全部类型(disabled 锁定),便于对照理解
|
||||
if (formEditType.value !== FormEditType.Add) {
|
||||
return menuTypeOptions.map((o) => ({ label: $t(o.label), value: o.value }));
|
||||
}
|
||||
if (extraParams.value?.parentMenuType) {
|
||||
@@ -62,12 +65,16 @@
|
||||
);
|
||||
}
|
||||
case MenuTypeEnum.MENU: {
|
||||
return (
|
||||
menuTypeOptions
|
||||
.filter((o) => o.value === MenuTypeEnum.SUBPAGE)
|
||||
// 国际化:获取菜单类型选项的显示标签
|
||||
.map((o) => ({ label: $t(o.label), value: o.value }))
|
||||
);
|
||||
// menu 下可选子页面或子页面分组
|
||||
return menuTypeOptions
|
||||
.filter((o) => o.value === MenuTypeEnum.SUBPAGE || o.value === MenuTypeEnum.SUBPAGE_GROUP)
|
||||
.map((o) => ({ label: $t(o.label), value: o.value }));
|
||||
}
|
||||
case MenuTypeEnum.SUBPAGE_GROUP: {
|
||||
// 分组下只能子页面
|
||||
return menuTypeOptions
|
||||
.filter((o) => o.value === MenuTypeEnum.SUBPAGE)
|
||||
.map((o) => ({ label: $t(o.label), value: o.value }));
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
@@ -76,7 +83,7 @@
|
||||
}
|
||||
return (
|
||||
menuTypeOptions
|
||||
.filter((o) => o.value !== MenuTypeEnum.SUBPAGE)
|
||||
.filter((o) => o.value !== MenuTypeEnum.SUBPAGE && o.value !== MenuTypeEnum.SUBPAGE_GROUP)
|
||||
// 国际化:获取菜单类型选项的显示标签
|
||||
.map((o) => ({ label: $t(o.label), value: o.value }))
|
||||
);
|
||||
@@ -128,19 +135,23 @@
|
||||
);
|
||||
const menuCodeRequired = computed(() => form.value.menuType === MenuTypeEnum.MENU);
|
||||
const showSortNo = computed(() => form.value.menuType !== MenuTypeEnum.SUBPAGE);
|
||||
const showHidden = computed(() => form.value.menuType !== MenuTypeEnum.SUBPAGE);
|
||||
const showHidden = computed(
|
||||
() => form.value.menuType !== MenuTypeEnum.SUBPAGE && form.value.menuType !== MenuTypeEnum.SUBPAGE_GROUP,
|
||||
);
|
||||
const showAffixTab = computed(() => form.value.menuType === MenuTypeEnum.MENU);
|
||||
const showBadge = computed(() => form.value.menuType !== MenuTypeEnum.SUBPAGE);
|
||||
const showBadge = computed(
|
||||
() => form.value.menuType !== MenuTypeEnum.SUBPAGE && form.value.menuType !== MenuTypeEnum.SUBPAGE_GROUP,
|
||||
);
|
||||
// 有可选类型时展示菜单类型区(含子页面仅一项场景)
|
||||
const showMenuType = computed(() => availableMenuTypeOptions.value.length > 0);
|
||||
// 子页面场景锁定类型不可切换
|
||||
const menuTypeLocked = computed(
|
||||
() =>
|
||||
showable.value ||
|
||||
form.value.menuType === MenuTypeEnum.SUBPAGE ||
|
||||
extraParams.value?.parentMenuType === MenuTypeEnum.MENU,
|
||||
);
|
||||
// 仅新增时可切换类型;编辑/查看一律锁定
|
||||
const menuTypeLocked = computed(() => formEditType.value !== FormEditType.Add);
|
||||
const isSubpage = computed(() => form.value.menuType === MenuTypeEnum.SUBPAGE);
|
||||
// 当前菜单类型的功能说明(编辑表单顶部 Alert)
|
||||
const menuTypeTip = computed(() => {
|
||||
const key = form.value.menuType && menuTypeTipI18nMap[form.value.menuType];
|
||||
return key ? $t(key) : '';
|
||||
});
|
||||
// 过滤后的上级菜单树数据 - 使用 disabled 标记方式显示完整树
|
||||
const parentTreeData = computed(() => {
|
||||
return markDisabledNodes(treeData.value);
|
||||
@@ -244,6 +255,16 @@
|
||||
form.value.hidden = true;
|
||||
break;
|
||||
}
|
||||
case MenuTypeEnum.SUBPAGE_GROUP: {
|
||||
// 子页面分组:纯容器,清空路由/组件/编码相关字段
|
||||
form.value.component = '';
|
||||
form.value.path = '';
|
||||
form.value.menuCode = '';
|
||||
form.value.iframeSrc = '';
|
||||
form.value.link = '';
|
||||
form.value.hidden = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -290,9 +311,13 @@
|
||||
if (params?.parentRow) {
|
||||
// 根据父菜单类型设置默认的子菜单类型
|
||||
let defaultMenuType: string = MenuTypeEnum.MENU;
|
||||
if (params.parentMenuType === MenuTypeEnum.MENU) {
|
||||
if (params.parentMenuType === MenuTypeEnum.MENU || params.parentMenuType === MenuTypeEnum.SUBPAGE_GROUP) {
|
||||
defaultMenuType = MenuTypeEnum.SUBPAGE;
|
||||
}
|
||||
// 外部预设类型优先(如"添加分组"按钮强制 subpage_group)
|
||||
if (params.defaultMenuType) {
|
||||
defaultMenuType = params.defaultMenuType;
|
||||
}
|
||||
form.value = {
|
||||
...form.value,
|
||||
clientCode: params.clientCode,
|
||||
@@ -311,22 +336,23 @@
|
||||
*/
|
||||
function applySubpageDrawerTitle(menuType?: string) {
|
||||
const type = menuType ?? form.value.menuType;
|
||||
if (type !== MenuTypeEnum.SUBPAGE) {
|
||||
if (type !== MenuTypeEnum.SUBPAGE && type !== MenuTypeEnum.SUBPAGE_GROUP) {
|
||||
return;
|
||||
}
|
||||
const isGroup = type === MenuTypeEnum.SUBPAGE_GROUP;
|
||||
switch (formEditType.value) {
|
||||
case FormEditType.Add: {
|
||||
title.value = $t('iam.menu.addSubpage');
|
||||
title.value = isGroup ? $t('iam.menu.addSubpageGroup') : $t('iam.menu.addSubpage');
|
||||
|
||||
break;
|
||||
}
|
||||
case FormEditType.Edit: {
|
||||
title.value = $t('iam.menu.editSubpage');
|
||||
title.value = isGroup ? $t('iam.menu.editSubpageGroup') : $t('iam.menu.editSubpage');
|
||||
|
||||
break;
|
||||
}
|
||||
case FormEditType.Show: {
|
||||
title.value = $t('iam.menu.viewSubpage');
|
||||
title.value = isGroup ? $t('iam.menu.viewSubpageGroup') : $t('iam.menu.viewSubpage');
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -382,15 +408,23 @@
|
||||
* 标记不可选的节点(显示完整树,但禁用不符合条件的节点)
|
||||
*/
|
||||
function markDisabledNodes(nodes: any[]): any[] {
|
||||
return nodes.map((node) => ({
|
||||
...node,
|
||||
// 子页面只能选择菜单类型,其他类型只能选择目录类型
|
||||
disabled:
|
||||
form.value.menuType === MenuTypeEnum.SUBPAGE
|
||||
? node.menuType !== MenuTypeEnum.MENU
|
||||
: node.menuType !== MenuTypeEnum.CATALOG,
|
||||
children: node.children ? markDisabledNodes(node.children) : undefined,
|
||||
}));
|
||||
const currentType = form.value.menuType;
|
||||
return nodes.map((node) => {
|
||||
// 各菜单类型可选的父级类型:subpage→menu/group;subpage_group→menu;其余→catalog
|
||||
let disabled: boolean;
|
||||
if (currentType === MenuTypeEnum.SUBPAGE) {
|
||||
disabled = node.menuType !== MenuTypeEnum.MENU && node.menuType !== MenuTypeEnum.SUBPAGE_GROUP;
|
||||
} else if (currentType === MenuTypeEnum.SUBPAGE_GROUP) {
|
||||
disabled = node.menuType !== MenuTypeEnum.MENU;
|
||||
} else {
|
||||
disabled = node.menuType !== MenuTypeEnum.CATALOG;
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
disabled,
|
||||
children: node.children ? markDisabledNodes(node.children) : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -456,6 +490,8 @@
|
||||
{{ item.label }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
<!-- 当前类型功能说明 -->
|
||||
<a-alert v-if="menuTypeTip" class="!mt-3" type="info" show-icon :message="menuTypeTip" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 国际化:菜单编码 -->
|
||||
|
||||
@@ -75,7 +75,10 @@
|
||||
*/
|
||||
function getActionMenu(row: Menu) {
|
||||
const items = [];
|
||||
if (row.menuType && ([MenuTypeEnum.CATALOG, MenuTypeEnum.MENU] as string[]).includes(row.menuType)) {
|
||||
if (
|
||||
row.menuType &&
|
||||
([MenuTypeEnum.CATALOG, MenuTypeEnum.MENU, MenuTypeEnum.SUBPAGE_GROUP] as string[]).includes(row.menuType)
|
||||
) {
|
||||
items.push({ key: 'addChild', label: $t('iam.menu.addChild') });
|
||||
}
|
||||
items.push({ key: 'delete', label: $t('common.delete'), danger: true });
|
||||
@@ -193,7 +196,11 @@
|
||||
if (!row?.menuType) {
|
||||
return;
|
||||
}
|
||||
if (row.menuType === MenuTypeEnum.CATALOG || row.menuType === MenuTypeEnum.MENU) {
|
||||
if (
|
||||
row.menuType === MenuTypeEnum.CATALOG ||
|
||||
row.menuType === MenuTypeEnum.MENU ||
|
||||
row.menuType === MenuTypeEnum.SUBPAGE_GROUP
|
||||
) {
|
||||
selectedNode.value = row;
|
||||
panelSearchKeyword.value = '';
|
||||
highlightSubpageId.value = undefined;
|
||||
@@ -234,13 +241,25 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 从右侧面板添加子页面
|
||||
* 从右侧面板添加子页面(父可能是 menu 直挂或 subpage_group)
|
||||
*/
|
||||
function handleAddSubpage(parentMenu: Menu) {
|
||||
menuEdit.value.init(undefined, FormEditType.Add, {
|
||||
clientCode: clientCode.value,
|
||||
parentRow: parentMenu,
|
||||
parentMenuType: parentMenu.menuType,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从右侧面板添加子页面分组
|
||||
*/
|
||||
function handleAddGroup(parentMenu: Menu) {
|
||||
menuEdit.value.init(undefined, FormEditType.Add, {
|
||||
clientCode: clientCode.value,
|
||||
parentRow: parentMenu,
|
||||
parentMenuType: MenuTypeEnum.MENU,
|
||||
defaultMenuType: MenuTypeEnum.SUBPAGE_GROUP,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -384,7 +403,21 @@
|
||||
<template #default="{ row }">
|
||||
<IconifyIcon v-if="row.icon" :icon="row.icon" class="text-lg inline-block align-middle mr-2" />
|
||||
<span>{{ getDisplayTitle(row) }}</span>
|
||||
<a-tag v-if="row.menuType === MenuTypeEnum.MENU && row.subpageCount" class="ml-2" color="blue">
|
||||
<a-tag
|
||||
v-if="row.menuType === MenuTypeEnum.MENU && row.subpageGroupCount"
|
||||
class="!ml-2"
|
||||
color="blue"
|
||||
>
|
||||
{{ $t('iam.menu.subpageGroupCount', { count: row.subpageGroupCount }) }}
|
||||
</a-tag>
|
||||
<a-tag v-if="row.menuType === MenuTypeEnum.MENU && row.subpageCount" class="!ml-2" color="cyan">
|
||||
{{ $t('iam.menu.subpageCount', { count: row.subpageCount }) }}
|
||||
</a-tag>
|
||||
<a-tag
|
||||
v-if="row.menuType === MenuTypeEnum.SUBPAGE_GROUP && row.subpageCount"
|
||||
class="!ml-2"
|
||||
color="cyan"
|
||||
>
|
||||
{{ $t('iam.menu.subpageCount', { count: row.subpageCount }) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
@@ -453,6 +486,7 @@
|
||||
:highlight-row-id="highlightSubpageId"
|
||||
:menu-map="menuMap"
|
||||
:selected-node="selectedNode"
|
||||
@add-group="handleAddGroup"
|
||||
@add-subpage="handleAddSubpage"
|
||||
@delete="handleDeleteConfirm"
|
||||
@edit="handleEdit"
|
||||
|
||||
@@ -7,10 +7,12 @@ import { MenuTypeEnum } from '#/enums/menuType';
|
||||
* 路由侧由 convertMenuListToRoutes 提升子页面,不在管理端开放多级 subpage。
|
||||
*/
|
||||
|
||||
/** 骨架树节点:不含 subpage 子节点,附带子页面数量 */
|
||||
/** 骨架树节点:不含 subpage 子节点,附带子页面/分组数量 */
|
||||
export interface MenuSkeletonNode extends Menu {
|
||||
/** 直属子页面数量 */
|
||||
/** 直属子页面数量(menu 为未分组直挂数,subpage_group 为其下子页面数) */
|
||||
subpageCount?: number;
|
||||
/** 直属子页面分组数量(仅 menu 有值) */
|
||||
subpageGroupCount?: number;
|
||||
children?: MenuSkeletonNode[];
|
||||
}
|
||||
|
||||
@@ -34,17 +36,26 @@ export function countDirectSubpages(node: Menu): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从树中剥离 subpage,构建骨架树(catalog / menu / embedded / link)
|
||||
* 从树中剥离 subpage,构建骨架树(catalog / menu / embedded / link / subpage_group)
|
||||
* subpage_group 作为可展开分组节点保留,其下 subpage 仍剥离
|
||||
*/
|
||||
export function buildSkeletonTree(data: Menu[]): MenuSkeletonNode[] {
|
||||
return data.map((item) => {
|
||||
const subpages = (item.children || []).filter((child) => child.menuType === MenuTypeEnum.SUBPAGE);
|
||||
const nonSubpageChildren = (item.children || []).filter((child) => child.menuType !== MenuTypeEnum.SUBPAGE);
|
||||
const skeletonChildren = buildSkeletonTree(nonSubpageChildren);
|
||||
const directSubpages = (item.children || []).filter((c) => c.menuType === MenuTypeEnum.SUBPAGE);
|
||||
const directGroups = (item.children || []).filter((c) => c.menuType === MenuTypeEnum.SUBPAGE_GROUP);
|
||||
// 骨架子节点:排除 subpage(由右侧面板管理),保留 subpage_group 作为可展开分组节点
|
||||
const skeletonChildrenRaw = (item.children || []).filter((c) => c.menuType !== MenuTypeEnum.SUBPAGE);
|
||||
const builtChildren = buildSkeletonTree(skeletonChildrenRaw);
|
||||
return {
|
||||
...item,
|
||||
children: skeletonChildren.length > 0 ? skeletonChildren : undefined,
|
||||
subpageCount: item.menuType === MenuTypeEnum.MENU ? subpages.length : undefined,
|
||||
children: builtChildren.length > 0 ? builtChildren : undefined,
|
||||
// menu 记直属分组数
|
||||
subpageGroupCount: item.menuType === MenuTypeEnum.MENU ? directGroups.length : undefined,
|
||||
// menu 记未分组直挂子页面数;subpage_group 记其下子页面数
|
||||
subpageCount:
|
||||
item.menuType === MenuTypeEnum.MENU || item.menuType === MenuTypeEnum.SUBPAGE_GROUP
|
||||
? directSubpages.length
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -65,7 +76,7 @@ export function flattenMenuMap(data: Menu[], map = new Map<string, Menu>()): Map
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某节点下直属子页面列表
|
||||
* 获取某节点下直属子页面列表(适用于 menu 直挂 / subpage_group 下)
|
||||
*/
|
||||
export function getDirectSubpages(parent: Menu, allMap: Map<string, Menu>): Menu[] {
|
||||
const node = parent.id ? allMap.get(parent.id) : parent;
|
||||
@@ -75,6 +86,30 @@ export function getDirectSubpages(parent: Menu, allMap: Map<string, Menu>): Menu
|
||||
return node.children.filter((child) => child.menuType === MenuTypeEnum.SUBPAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 menu 下直属子页面分组 + 直挂子页面(未分组的老数据)
|
||||
*/
|
||||
export function getDirectMenuChildren(menu: Menu, allMap: Map<string, Menu>): Menu[] {
|
||||
const node = menu.id ? allMap.get(menu.id) : menu;
|
||||
if (!node?.children?.length) {
|
||||
return [];
|
||||
}
|
||||
return node.children.filter(
|
||||
(child) => child.menuType === MenuTypeEnum.SUBPAGE_GROUP || child.menuType === MenuTypeEnum.SUBPAGE,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 menu 下直属子页面分组列表
|
||||
*/
|
||||
export function getDirectSubpageGroups(menu: Menu, allMap: Map<string, Menu>): Menu[] {
|
||||
const node = menu.id ? allMap.get(menu.id) : menu;
|
||||
if (!node?.children?.length) {
|
||||
return [];
|
||||
}
|
||||
return node.children.filter((child) => child.menuType === MenuTypeEnum.SUBPAGE_GROUP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目录下直属 menu / embedded / link(不含 subpage,不递归)
|
||||
*/
|
||||
@@ -101,14 +136,15 @@ export function matchesMenuKeyword(node: Menu, keyword: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在全量菜单树中查找匹配关键字的子页面
|
||||
* 在全量菜单树中查找匹配关键字的子页面(递归进入子页面分组)
|
||||
*/
|
||||
export function findMatchingSubpages(fullTree: Menu[], keyword: string): SubpageMatch[] {
|
||||
const matches: SubpageMatch[] = [];
|
||||
|
||||
function walk(nodes: Menu[]) {
|
||||
for (const node of nodes) {
|
||||
if (node.menuType === MenuTypeEnum.MENU) {
|
||||
// 子页面可挂在 menu 或 subpage_group 下
|
||||
if (node.menuType === MenuTypeEnum.MENU || node.menuType === MenuTypeEnum.SUBPAGE_GROUP) {
|
||||
for (const child of node.children || []) {
|
||||
if (child.menuType === MenuTypeEnum.SUBPAGE && matchesMenuKeyword(child, keyword)) {
|
||||
matches.push({ subpage: child, parentMenu: node });
|
||||
@@ -169,7 +205,7 @@ function collectIdsInTree(tree: MenuSkeletonNode[]): Set<string> {
|
||||
/**
|
||||
* 在骨架树中查找目标节点的祖先路径(含自身)
|
||||
*/
|
||||
function findPathToNode(tree: MenuSkeletonNode[], targetId: string, path: string[] = []): string[] | null {
|
||||
function findPathToNode(tree: MenuSkeletonNode[], targetId: string, path: string[] = []): null | string[] {
|
||||
for (const node of tree) {
|
||||
const currentPath = node.id ? [...path, node.id] : path;
|
||||
if (node.id === targetId) {
|
||||
|
||||
@@ -43,6 +43,11 @@ interface RouteMeta {
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| string;
|
||||
/**
|
||||
* 自定义面包屑链
|
||||
* 优先于 route.matched 渲染,用于子页面路由提升后补全完整层级(catalog>menu>group>subpage)
|
||||
*/
|
||||
customBreadcrumb?: { icon?: string; path?: string; title?: string }[];
|
||||
/**
|
||||
* 路由对应dom是否缓存起来
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,32 @@ const breadcrumbs = computed((): IBreadcrumb[] => {
|
||||
|
||||
const resultBreadcrumb: IBreadcrumb[] = [];
|
||||
|
||||
// 优先使用叶子路由自定义面包屑链(子页面路由提升后补全完整层级)
|
||||
const leaf = matched[matched.length - 1];
|
||||
if (leaf?.meta?.customBreadcrumb?.length) {
|
||||
for (const item of leaf.meta.customBreadcrumb) {
|
||||
if (!item.path) {
|
||||
continue;
|
||||
}
|
||||
resultBreadcrumb.push({
|
||||
icon: item.icon as any,
|
||||
path: item.path || route.path,
|
||||
title: item.title ? $t(item.title) : '',
|
||||
});
|
||||
}
|
||||
if (props.showHome) {
|
||||
resultBreadcrumb.unshift({
|
||||
icon: 'mdi:home-outline',
|
||||
isHome: true,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
if (props.hideWhenOnlyOne && resultBreadcrumb.length === 1) {
|
||||
return [];
|
||||
}
|
||||
return resultBreadcrumb;
|
||||
}
|
||||
|
||||
for (const match of matched) {
|
||||
const { meta, path } = match;
|
||||
const { hideChildrenInMenu, hideInBreadcrumb, icon, name, title } =
|
||||
|
||||
Reference in New Issue
Block a user