@truenewx/tnxcore
Version:
互联网技术解决方案:JavaScript核心扩展支持
239 lines (215 loc) • 7.63 kB
text/typescript
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);
}
}