完善标签设计器/销售产品模板绑定与打印面单 UX,统一入库出库到仓扫码页布局,并补充库存绑定与相关文档配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
429 lines
13 KiB
TypeScript
429 lines
13 KiB
TypeScript
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,
|
||
};
|
||
}
|