UNPKG

@truenewx/tnxcore

Version:

互联网技术解决方案:JavaScript核心扩展支持

239 lines (215 loc) 7.63 kB
export class MenuItem { name: string; caption: string; desc?: string; type?: string; rank?: string; permission?: string | boolean; subs?: MenuItem[]; level?: number; path?: string; permittedPath?: string[]; visible?: boolean; constructor(obj?: Record<string, any>) { if (obj) { Object.assign(this, obj); } } isSubVisible(): boolean { if (this.subs && this.subs.length) { let visible = true; for (let sub of this.subs) { visible = (sub.visible !== false) && visible; } return visible; } return false; } } function buildLevel(items: MenuItem[], parentLevel: number): MenuItem[] { if (items) { let level = parentLevel ? (parentLevel + 1) : 1; for (let item of items) { item.level = level; buildLevel(item.subs, level); } } return items; } function findItemChain(path: string, items: MenuItem[], fnItemChain: (item: MenuItem, chain?: MenuItem[]) => MenuItem[]): MenuItem[] { for (let item of items) { if (matches(item, path)) { return fnItemChain(item); } // 直接路径不匹配,则尝试在子菜单中查找 if (item.subs) { let chain = findItemChain(path, item.subs, fnItemChain); if (chain) { return fnItemChain(item, chain); } } } return []; } function matches(item: MenuItem, path: string): boolean { // 去掉可能的请求参数部分 const index = path.indexOf('?'); if (index >= 0) { path = path.substring(index); } if (item.path && matchesPath(item.path, path)) { return true; } if (typeof item.permission === 'string') { // 如果没有指定路径但指定了许可名 if (item.permittedPath) { // 如果指定了许可路径,则先尝试匹配许可路径和指定路径 for (let permittedPath of item.permittedPath) { if (matchesPath(permittedPath, path)) { return true; } } } // 此时还未匹配,则将指定路径按照默认规则转换为许可名尝试匹配 let permission = getDefaultPermission(path); return item.permission === permission; } return false; } function matchesPath(pathPattern: string, actualPath: string): boolean { let pattern = pathPattern.replace(/\/:[a-zA-Z0-9_]+/g, '/[a-zA-Z0-9_\\*]+'); if (pattern === pathPattern) { // 无路径参数 return pathPattern === actualPath; } else { // 有路径参数,正则表达式全字符串比较匹配 return new RegExp('^' + pattern + '$', 'g').test(actualPath); } } function getDefaultPermission(path: string): string { // 确保路径头尾都有/ if (!path.startsWith('/')) { path = '/' + path; } if (!path.endsWith('/')) { path += '/'; } // 移除可能包含的路径变量 let permission = path.replace(/\/:[^/]+\//g, '/').replace(/\*/g, ''); // 去掉许可头尾的/ if (permission.startsWith('/')) { permission = permission.substring(1); } if (permission.endsWith('/')) { permission = permission.substring(0, permission.length - 1); } // 许可所有中间的/替换为_ permission = permission.replace(/\//g, '_'); return permission; } function addMatchedItemTo(items: MenuItem[], path: string, targetItems: MenuItem[]): void { if (items) { for (let item of items) { if (matches(item, path)) { // 找到匹配的菜单项,则加入目标项目清单,直接返回 targetItems.push(item); return; } // 不匹配则尝试比较下级菜单项 addMatchedItemTo(item.subs, path, targetItems); if (targetItems.length > 0) { // 如果在下级菜单中找到匹配,则当前级别需要插入到目标清单的首位 targetItems.unshift(item); return; } } } } function findItemByPermission(app: string, items: MenuItem[], permission: string): MenuItem | null { for (let item of items) { prepareItem(item); if (item.permission === permission) { return item; } if (app && (app + '.' + item.permission) === permission) { return item; } if (item.subs) { const sub = findItemByPermission(app, item.subs, permission); if (sub) { return sub; } } } return null; } function prepareItem(item: MenuItem) { if (item.path && item.permission === true) { item.permission = getDefaultPermission(item.path); } } function findAssignableItems(items: MenuItem[]): MenuItem[] { const assignableItems: MenuItem[] = []; items.forEach(item => { let assignableItem = new MenuItem({ subs: [], }); prepareItem(item); if (item.subs && item.subs.length) { assignableItem.subs = findAssignableItems(item.subs); } // 当前菜单有许可限定,或有可分配的子级或操作,则当前菜单项为可分配项 if (item.permission || assignableItem.subs.length) { assignableItem = Object.assign({}, item, assignableItem); assignableItems.push(assignableItem); } }); return assignableItems; } export default class Menu { ordinal = 0; app = ''; name: string; caption: string; items: MenuItem[]; constructor(obj?: Record<string, any>) { if (obj) { Object.assign(this, obj); this.setItems(obj.items); } } setItems(items: MenuItem[]): void { this.items = buildLevel(items, 0); } getItemByPath(path: string): MenuItem | null { let chain = findItemChain(path, this.items, (item: MenuItem, subs): MenuItem[] => { return subs || [item]; }); return chain[0] || null; } getBreadcrumbItems(path: string) { return findItemChain(path, this.items, (item: MenuItem, breadcrumbItems: MenuItem[]): MenuItem[] => { if (breadcrumbItems && breadcrumbItems.length) { breadcrumbItems.unshift(item); return breadcrumbItems; } else { return [item]; } }); } findBelongingItem(path: string, level: number = 2): MenuItem | null { let items = []; addMatchedItemTo(this.items, path, items); // 从后往前遍历结果清单,以便于取到级别不高于目标级别的菜单项 for (let i = items.length - 1; i >= 0; i--) { let item = items[i]; if (item.level <= level && item.visible !== false) { let parentItem = items[i - 1]; if (!parentItem || parentItem.isSubVisible()) { return item; } } } return null; } getItemByPermission(permission: string): MenuItem | null { return findItemByPermission(this.app, this.items, permission); } getAssignableItems() { return findAssignableItems(this.items); } }