front/src/utils/syncMenuLevel3Buttons.ts
zzh c4979a1fb4 feat: 面单标签设计与仓内操作页优化,接通模板打印链路
完善标签设计器/销售产品模板绑定与打印面单 UX,统一入库出库到仓扫码页布局,并补充库存绑定与相关文档配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 17:49:18 +08:00

429 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MenuService, PermissionService } from '@/api/rbac';
import type { MenuTreeDto, PermissionDefinitionDto, PermissionGroupDto } from '@/api/rbac';
import {
getCatalogButtons,
inferButtonName,
normalizeResourcePermissionCode,
type PageButtonDef,
} from '@/constants/pageButtonCatalog';
import {
findModuleRouteByMenu,
findPageChildMenu,
getPageRoutesFromModule,
isModuleMenu,
type RoutePageMenuDef,
} from '@/utils/syncMenuRouteCatalog';
export interface SyncLevel3ButtonsResult {
created: number;
skipped: number;
total: number;
createdNames: string[];
}
export interface SyncLevel3Options {
/** 无三级按钮时不抛错,返回 created=0 */
allowEmpty?: boolean;
}
export interface SyncMenuTreeResult {
mode: 'module' | 'page';
level2Created: number;
level2Skipped: number;
level3Created: number;
level3Skipped: number;
level3FailedPages: string[];
}
export { isModuleMenu };
function permissionNodeToButton(
child: PermissionDefinitionDto,
order: number,
): PageButtonDef | null {
const code = child.code?.trim();
if (!code) return null;
return {
permissionCode: code,
name: inferButtonName(code, child.name),
order,
};
}
/**
* 从权限树解析三级按钮:
* - 菜单为 Rbac.Menus → 取 Rbac.Menus 节点的 children
* - 菜单为 Rbac.Menus.Query → 取 Rbac.Menus 下除自身外的 children
*/
function resolveButtonsFromPermissionTree(
groups: PermissionGroupDto[],
menuPermissionCode: string,
): PageButtonDef[] {
const menuCode = menuPermissionCode.trim();
const resourceCode = normalizeResourcePermissionCode(menuCode);
if (!menuCode) return [];
const buttons: PageButtonDef[] = [];
const seen = new Set<string>();
const pushButton = (btn: PageButtonDef | null) => {
if (!btn || !btn.permissionCode || seen.has(btn.permissionCode)) return;
if (btn.permissionCode === menuCode) return;
seen.add(btn.permissionCode);
buttons.push(btn);
};
const collectChildren = (
children: PermissionDefinitionDto[] | null | undefined,
excludeCode?: string,
) => {
(children || []).forEach((child, index) => {
const childCode = child.code?.trim();
if (!childCode || childCode === excludeCode) return;
pushButton(permissionNodeToButton(child, index));
});
};
const visitNodes = (nodes: PermissionDefinitionDto[] | null | undefined) => {
(nodes || []).forEach((node) => {
const nodeCode = node.code?.trim() || '';
if (nodeCode === resourceCode) {
if (menuCode !== resourceCode) {
collectChildren(node.children, menuCode);
} else {
collectChildren(node.children);
}
}
if (nodeCode === menuCode && node.children?.length) {
collectChildren(node.children);
}
visitNodes(node.children);
});
};
groups.forEach((group) => visitNodes(group.permissions));
// 兼容扁平结构:通过 parentCode 关联
const scanParentCode = (nodes: PermissionDefinitionDto[] | null | undefined) => {
(nodes || []).forEach((node) => {
const nodeCode = node.code?.trim() || '';
const parent = node.parentCode?.trim() || '';
if (
nodeCode &&
(parent === resourceCode || parent === menuCode) &&
nodeCode !== menuCode
) {
pushButton(
permissionNodeToButton(node, buttons.length + 1),
);
}
scanParentCode(node.children);
});
};
groups.forEach((group) => scanParentCode(group.permissions));
return buttons.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
function mergeButtonDefs(...lists: PageButtonDef[][]): PageButtonDef[] {
const map = new Map<string, PageButtonDef>();
lists.flat().forEach((item) => {
if (!item.permissionCode) return;
const existing = map.get(item.permissionCode);
map.set(item.permissionCode, {
permissionCode: item.permissionCode,
name: item.name || existing?.name || inferButtonName(item.permissionCode),
order: item.order ?? existing?.order ?? 0,
});
});
return Array.from(map.values()).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
function collectExistingButtonCodes(menu: MenuTreeDto): Set<string> {
const codes = new Set<string>();
(menu.children || []).forEach((child) => {
if (child.type === 2 && child.permissionCode?.trim()) {
codes.add(child.permissionCode.trim());
}
});
return codes;
}
function filterSyncTargets(
buttons: PageButtonDef[],
menuPermissionCode: string,
): PageButtonDef[] {
const menuCode = menuPermissionCode.trim();
return buttons.filter((btn) => btn.permissionCode !== menuCode);
}
function findMenuById(tree: MenuTreeDto[], id: string): MenuTreeDto | null {
for (const node of tree) {
if (node.id === id) return node;
if (node.children?.length) {
const found = findMenuById(node.children, id);
if (found) return found;
}
}
return null;
}
async function reloadMenuNode(menuId: string): Promise<MenuTreeDto | null> {
const tree = await MenuService.getRbacMenuGetMenuTree();
return findMenuById(tree ?? [], menuId);
}
async function ensurePageMenu(
parentMenu: MenuTreeDto,
pageDef: RoutePageMenuDef,
): Promise<{ menu: MenuTreeDto; created: boolean }> {
const existing = findPageChildMenu(parentMenu, pageDef.route, pageDef.permissionCode);
if (existing?.id) {
// 已有二级菜单:补齐或纠正 permissionCode(Xxx.Query → Xxx),便于同步「查询」等三级按钮
const existingCode = existing.permissionCode?.trim() || '';
const nextCode = pageDef.permissionCode?.trim() || '';
const shouldPatchCode =
!!nextCode &&
(!existingCode ||
existingCode === nextCode ||
normalizeResourcePermissionCode(existingCode) === nextCode);
if (shouldPatchCode && existingCode !== nextCode) {
try {
await MenuService.putRbacMenuUpdate(existing.id, {
name: existing.name || pageDef.name,
permissionCode: nextCode,
icon: existing.icon || pageDef.icon,
route: existing.route || pageDef.route,
component: existing.component || pageDef.component,
order: existing.order ?? pageDef.order,
isHidden: existing.isHidden ?? pageDef.isHidden,
isCached: existing.isCached ?? false,
description: existing.description || `同步二级菜单权限码: ${nextCode}`,
});
return {
menu: { ...existing, permissionCode: nextCode },
created: false,
};
} catch {
// 更新失败时仍返回原节点,后续三级同步可能因无权限码跳过
}
}
return { menu: existing, created: false };
}
const createdMenu = await MenuService.postRbacMenuCreate({
pId: parentMenu.id,
name: pageDef.name,
type: 1,
permissionCode: pageDef.permissionCode,
icon: pageDef.icon,
route: pageDef.route,
component: pageDef.component,
order: pageDef.order,
isHidden: pageDef.isHidden,
isCached: false,
description: `同步二级菜单: ${pageDef.route}`,
});
return {
menu: {
id: createdMenu.id,
pId: parentMenu.id,
name: pageDef.name,
type: 1,
permissionCode: pageDef.permissionCode,
icon: pageDef.icon,
route: pageDef.route,
component: pageDef.component,
order: pageDef.order,
isHidden: pageDef.isHidden,
children: [],
},
created: true,
};
}
/**
* 将前端目录 + 权限树中的三级按钮同步为菜单子节点(type=2)
*/
export async function syncMenuLevel3Buttons(
menu: MenuTreeDto,
options?: SyncLevel3Options,
): Promise<SyncLevel3ButtonsResult> {
let parentPermissionCode = menu.permissionCode?.trim() || '';
if (!parentPermissionCode) {
throw new Error('当前菜单未配置权限编码,无法同步三级按钮');
}
if (!menu.id) {
throw new Error('菜单 ID 无效');
}
if (menu.type !== 1) {
throw new Error('仅支持对页面菜单同步三级按钮');
}
// 二级菜单若误存为 Xxx.Query,纠正为资源码,使「查询」可作为三级按钮同步
const resourceCode = normalizeResourcePermissionCode(parentPermissionCode);
if (resourceCode && resourceCode !== parentPermissionCode) {
try {
await MenuService.putRbacMenuUpdate(menu.id, {
name: menu.name,
permissionCode: resourceCode,
icon: menu.icon,
route: menu.route,
component: menu.component,
order: menu.order ?? 0,
isHidden: menu.isHidden ?? false,
isCached: menu.isCached ?? false,
description: menu.description || `同步二级菜单权限码: ${resourceCode}`,
});
parentPermissionCode = resourceCode;
menu = { ...menu, permissionCode: resourceCode };
} catch {
// 纠正失败时仍按原编码尝试同步(查询按钮可能被过滤)
}
}
const catalogButtons = getCatalogButtons(parentPermissionCode);
let permissionTreeButtons: PageButtonDef[] = [];
try {
const groups = await PermissionService.getRbacPermissionGetPermissionTree();
permissionTreeButtons = resolveButtonsFromPermissionTree(groups ?? [], parentPermissionCode);
} catch {
// 权限树不可用时仅使用前端目录
}
const targetButtons = filterSyncTargets(
mergeButtonDefs(catalogButtons, permissionTreeButtons),
parentPermissionCode,
);
if (!targetButtons.length) {
if (options?.allowEmpty) {
return { created: 0, skipped: 0, total: 0, createdNames: [] };
}
const resourceCode = normalizeResourcePermissionCode(parentPermissionCode);
throw new Error(
`未找到可同步的三级按钮(菜单编码: ${parentPermissionCode}${resourceCode !== parentPermissionCode ? `,资源编码: ${resourceCode}` : ''})。请确认菜单 permissionCode 与权限树/前端目录一致。`,
);
}
const existingCodes = collectExistingButtonCodes(menu);
const createdNames: string[] = [];
let created = 0;
let skipped = 0;
for (const btn of targetButtons) {
if (existingCodes.has(btn.permissionCode)) {
skipped += 1;
continue;
}
await MenuService.postRbacMenuCreate({
pId: menu.id,
name: btn.name,
type: 2,
permissionCode: btn.permissionCode,
order: btn.order ?? 0,
isHidden: false,
isCached: false,
description: `同步三级按钮: ${btn.permissionCode}`,
});
existingCodes.add(btn.permissionCode);
createdNames.push(btn.name);
created += 1;
}
return {
created,
skipped,
total: targetButtons.length,
createdNames,
};
}
/**
* 同步菜单:一级模块同步二级子菜单 + 各页面三级按钮;二级页面仅同步三级按钮
*/
export async function syncMenuTree(menu: MenuTreeDto): Promise<SyncMenuTreeResult> {
if (!menu.id) {
throw new Error('菜单 ID 无效');
}
if (menu.type !== 1) {
throw new Error('仅支持对菜单类型节点执行同步');
}
if (!isModuleMenu(menu)) {
const result = await syncMenuLevel3Buttons(menu);
return {
mode: 'page',
level2Created: 0,
level2Skipped: 0,
level3Created: result.created,
level3Skipped: result.skipped,
level3FailedPages: [],
};
}
const moduleRoute = findModuleRouteByMenu(menu);
if (!moduleRoute) {
throw new Error('未找到匹配的路由模块,请确认菜单路由路径与前端路由配置一致');
}
const pageDefs = getPageRoutesFromModule(moduleRoute);
if (!pageDefs.length) {
throw new Error('路由模块下没有可同步的二级页面菜单');
}
let level2Created = 0;
let level2Skipped = 0;
let level3Created = 0;
let level3Skipped = 0;
const level3FailedPages: string[] = [];
let currentParent = menu;
for (const pageDef of pageDefs) {
const { menu: pageMenu, created } = await ensurePageMenu(currentParent, pageDef);
if (created) {
level2Created += 1;
const refreshedParent = await reloadMenuNode(menu.id);
if (refreshedParent) {
currentParent = refreshedParent;
}
} else {
level2Skipped += 1;
}
if (!pageMenu.permissionCode?.trim()) {
continue;
}
const pageNode =
findPageChildMenu(currentParent, pageDef.route, pageDef.permissionCode) || pageMenu;
const freshPage = pageNode.id ? await reloadMenuNode(pageNode.id) : null;
const targetPage = freshPage || pageNode;
try {
const btnResult = await syncMenuLevel3Buttons(targetPage, { allowEmpty: true });
level3Created += btnResult.created;
level3Skipped += btnResult.skipped;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
level3FailedPages.push(`${pageDef.name}: ${message}`);
}
}
return {
mode: 'module',
level2Created,
level2Skipped,
level3Created,
level3Skipped,
level3FailedPages,
};
}