UNPKG

ohayolibs

Version:

Ohayo is a set of essential modules for ohayojp.

278 lines (235 loc) 7.37 kB
import { Inject, Injectable, OnDestroy, Optional } from '@angular/core'; import { ACLService } from '@ohayo/acl'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { share } from 'rxjs/operators'; import { OhayoI18NService, OHAYO_I18N_TOKEN } from '../i18n/i18n'; import { Menu, MenuIcon, MenuInner } from './interface'; /** * 菜单服务,[在线文档](https://ohayojp.com/theme/menu) */ @Injectable({ providedIn: 'root' }) export class MenuService implements OnDestroy { private _change$: BehaviorSubject<Menu[]> = new BehaviorSubject<Menu[]>([]); private i18n$: Subscription; private data: Menu[] = []; constructor( @Optional() @Inject(OHAYO_I18N_TOKEN) private i18nSrv: OhayoI18NService, @Optional() private aclService: ACLService, ) { this.i18n$ = this.i18nSrv.change.subscribe(() => this.resume()); } get change(): Observable<Menu[]> { return this._change$.pipe(share()); } visit(data: Menu[], callback: (item: Menu, parentMenum: Menu | null, depth?: number) => void): void { const inFn = (list: Menu[], parentMenu: Menu | null, depth: number) => { for (const item of list) { callback(item, parentMenu, depth); if (item.children && item.children.length > 0) { inFn(item.children, item, depth + 1); } else { item.children = []; } } }; inFn(data, null, 0); } add(items: Menu[]): void { this.data = items; this.resume(); } private fixItem(item: MenuInner): void { item._aclResult = true; if (!item.link) item.link = ''; if (!item.externalLink) item.externalLink = ''; // badge if (item.badge) { if (item.badgeDot !== true) { item.badgeDot = false; } if (!item.badgeStatus) { item.badgeStatus = 'error'; } } if (!Array.isArray(item.children)) { item.children = []; } // icon if (typeof item.icon === 'string') { let type = 'class'; let value = item.icon; // compatible `anticon anticon-user` if (~item.icon.indexOf(`anticon-`)) { type = 'icon'; value = value.split('-').slice(1).join('-'); } else if (/^https?:\/\//.test(item.icon)) { type = 'img'; } item.icon = { type, value } as any; } if (item.icon != null) { item.icon = { theme: 'outline', spin: false, ...(item.icon as MenuIcon) }; } item.text = item.i18n && this.i18nSrv ? this.i18nSrv.fanyi(item.i18n) : item.text; // group item.group = item.group !== false; // hidden item._hidden = typeof item.hide === 'undefined' ? false : item.hide; // disabled item.disabled = typeof item.disabled === 'undefined' ? false : item.disabled; // acl item._aclResult = item.acl && this.aclService ? this.aclService.can(item.acl) : true; } /** * 重置菜单,可能I18N、用户权限变动时需要调用刷新 */ resume(callback?: (item: Menu, parentMenum: Menu | null, depth?: number) => void): void { let i = 1; const shortcuts: Menu[] = []; this.visit(this.data, (item: MenuInner, parent, depth) => { item._id = i++; item._parent = parent; item._depth = depth; this.fixItem(item); // shortcut if (parent && item.shortcut === true && parent.shortcutRoot !== true) { shortcuts.push(item); } if (callback) callback(item, parent, depth); }); this.loadShortcut(shortcuts); this._change$.next(this.data); } /** * 加载快捷菜单,加载位置规则如下: * 1、统一在下标0的节点下(即【主导航】节点下方) * 1、若 children 存在 【shortcutRoot: true】则最优先【推荐】这种方式 * 2、否则查找带有【dashboard】字样链接,若存在则在此菜单的下方创建快捷入口 * 3、否则放在0节点位置 */ private loadShortcut(shortcuts: MenuInner[]): void { if (shortcuts.length === 0 || this.data.length === 0) { return; } const ls = this.data[0].children as MenuInner[]; let pos = ls.findIndex(w => w.shortcutRoot === true); if (pos === -1) { pos = ls.findIndex(w => w.link!.includes('dashboard')); pos = (pos !== -1 ? pos : -1) + 1; const shortcutMenu = { text: '快捷菜单', i18n: 'shortcut', icon: 'icon-rocket', children: [], } as MenuInner; this.data[0].children!.splice(pos, 0, shortcutMenu); } let _data = this.data[0].children![pos]; if (_data.i18n && this.i18nSrv) _data.text = this.i18nSrv.fanyi(_data.i18n); // tslint:disable-next-line:prefer-object-spread _data = Object.assign(_data, { shortcutRoot: true, _id: -1, _parent: null, _depth: 1, } as MenuInner); _data.children = shortcuts.map(i => { i._depth = 2; i._parent = _data; return i; }); } get menus(): Menu[] { return this.data; } /** * 清空菜单 */ clear(): void { this.data = []; this._change$.next(this.data); } getHit(data: Menu[], url: string, recursive: boolean = false, cb: ((i: Menu) => void) | null = null): Menu | null { let item: Menu | null = null; while (!item && url) { this.visit(data, i => { if (cb) { cb(i); } if (i.link != null && i.link === url) { item = i; } }); if (!recursive) break; if (/[?;]/g.test(url)) { url = url.split(/[?;]/g)[0]; } else { url = url.split('/').slice(0, -1).join('/'); } } return item; } /** * 根据URL设置菜单 `_open` 属性 * - 若 `recursive: true` 则会自动向上递归查找 * - 菜单数据源包含 `/ware`,则 `/ware/1` 也视为 `/ware` 项 */ openedByUrl(url: string | null, recursive: boolean = false): void { if (!url) return; let findItem = this.getHit(this.data, url, recursive, (i: MenuInner) => { i._selected = false; i._open = false; }) as MenuInner; if (findItem == null) return; do { findItem._selected = true; findItem._open = true; findItem = findItem._parent!; } while (findItem); } /** * 根据url获取菜单列表 * - 若 `recursive: true` 则会自动向上递归查找 * - 菜单数据源包含 `/ware`,则 `/ware/1` 也视为 `/ware` 项 */ getPathByUrl(url: string, recursive: boolean = false): Menu[] { const ret: Menu[] = []; let item = this.getHit(this.data, url, recursive) as MenuInner; if (!item) return ret; do { ret.splice(0, 0, item); item = item._parent!; } while (item); return ret; } /** * Get menu based on `key` */ getItem(key: string): Menu | null { let res: Menu | null = null; this.visit(this.data, item => { if (res == null && item.key === key) { res = item; } }); return res; } /** * Set menu based on `key` */ setItem(key: string, value: Menu): void { const item = this.getItem(key); if (item == null) return; Object.keys(value).forEach(k => { item[k] = value[k]; }); this.fixItem(item); this._change$.next(this.data); } ngOnDestroy(): void { this._change$.unsubscribe(); this.i18n$.unsubscribe(); } }