UNPKG

@truenewx/tnxcore

Version:

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

365 lines (340 loc) 12.6 kB
/** * 菜单组件 * 菜单配置中的权限配置不是服务端权限判断的依据,仅用于生成具有权限的客户端菜单,以及分配权限时展示可分配的权限范围 */ function isGranted(authorities, item) { prepareItem(item); if (item.type || item.rank || item.permission) { for (let authority of authorities) { // 匹配原则: 菜单不限定视为匹配;已获权限为全部视为匹配;已获权限等于菜单限定权限视为匹配 let typeMatched = !item.type || authority.type === '*' || authority.type === item.type; if (typeMatched) { let rankMatched = !item.rank || authority.rank === '*' || authority.rank === item.rank; if (rankMatched) { if (!item.permission) { return true; } if (Array.isArray(authority.permissions)) { if (authority.permissions.contains('*') || authority.permissions.containsIgnoreCase( item.permission)) { return true; } } } } // 不匹配,则检查下一条权限 } return false; } // 当前节点未指定类型、级别和许可,且包含下级菜单,则返回undefined表示无法判断 if (item.subs && item.subs.length) { return undefined; } return true; } function prepareItem(item) { if (item.path && item.permission === true) { item.permission = getDefaultPermission(item.path); } } function getDefaultPermission(path) { // 确保路径头尾都有/ if (!path.startsWith('/')) { path = '/' + path; } if (!path.endsWith('/')) { path += '/'; } // 移除可能包含的路径变量 let permission = path.replace(/\/:[^/]+\//g, '/').replace(/\*/g, ''); // 去掉许可头尾的/ if (permission.startsWith('/')) { permission = permission.substr(1); } if (permission.endsWith('/')) { permission = permission.substr(0, permission.length - 1); } // 许可所有中间的/替换为_ permission = permission.replace(/\//g, '_'); return permission; } function applyGrantedItemToItems(authorities, item, items) { const granted = isGranted(authorities, item); if (granted === true) { // 授权匹配 items.push(Object.assign({}, item)); } else if (granted === false) { // 授权不匹配 // 不做处理 } else { // 无法判断,需到子节点中查找 if (item.subs && item.subs.length) { const subs = []; for (let sub of item.subs) { if (isGranted(authorities, sub)) { subs.push(Object.assign({}, sub)); } } if (subs.length) { items.push(Object.assign({}, item, { subs: subs })); } } } } function findItem(path, items, callback) { if (path && items && items.length && typeof callback === 'function') { for (let item of items) { if (matches(item, path)) { return callback(item); } // 直接路径不匹配,则尝试在子菜单中查找 if (item.subs) { let result = findItem(path, item.subs, callback); if (result) { return callback(item, result); } } } } return undefined; } function matches(item, path) { // 去掉可能的请求参数部分 const index = path.indexOf('?'); if (index >= 0) { path = path.substr(index); } if (item.path && matchesPath(item.path, path)) { return true; } if (typeof item.permission === 'string') { // 如果没有指定路径但指定了许可名 if (item.permittedPath) { // 如果指定了许可路径,则先尝试匹配许可路径和指定路径 if (!Array.isArray(item.permittedPath)) { item.permittedPath = [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, actualPath) { 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 buildLevel(items, parentLevel) { if (items) { let level = parentLevel ? (parentLevel + 1) : 1; for (let item of items) { item.level = level; item.isSubVisible = function () { if (item.subVisible === false) { return false; } if (item.subs && item.subs.length) { let visible = true; for (let sub of item.subs) { visible = (sub.visible !== false) && visible; } return visible; } return false; }; buildLevel(item.subs, level); } } return items; } const Menu = function Menu(config) { this.name = config.name; this.app = config.app; this.caption = config.caption; this.items = buildLevel(config.items); this.scope = config.scope; this._url = config.url; this._grantedItems = null; this.authorities = []; } Menu.prototype.getItemByPath = function (path) { return findItem(path, this.items, (item, sub) => { return sub || item; }); }; function addMatchedItemTo(items, path, targetItems) { 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; } } } } Menu.prototype.findBelongingItem = function (path, level) { level = level || 2; 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 undefined; } function findItemByPermission(app, items, permission) { 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 undefined; } Menu.prototype.getItemByPermission = function (permission) { return findItemByPermission(this.app, this.items, permission); } function findAssignableItems(items) { const assignableItems = []; items.forEach(item => { let assignableItem = { 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; } Menu.prototype.getAssignableItems = function () { return findAssignableItems(this.items); } Menu.prototype.getBreadcrumbItems = function (path) { let breadcrumbItems = findItem(path, this.items, (item, breadcrumbItems) => { if (breadcrumbItems && breadcrumbItems.length) { breadcrumbItems.unshift(item); return breadcrumbItems; } else { return [item]; } }); return breadcrumbItems || []; }; Menu.prototype.isGranted = function (path) { let breadcrumbItems = this.getBreadcrumbItems(path); if (breadcrumbItems.length) { for (let i = breadcrumbItems.length - 1; i >= 0; i--) { let item = breadcrumbItems[i]; let granted = isGranted(this.authorities, item); if (granted !== undefined) { return granted; } } return true; // 有匹配菜单项,但各级菜单项均未设置可鉴权,则说明无需鉴权,视为鉴权通过 } return false; }; Menu.prototype.loadGrantedItems = function (scope, callback) { if (typeof scope === 'function') { callback = scope; scope = null; } if (scope && scope !== this.scope) { this._grantedItems = null; } if (this._grantedItems) { callback(this._grantedItems); } else { const _this = this; if (this._url) { window.tnx.app.rpc.get(this._url, function (authorities) { _this._applyAuthorities(authorities, scope, callback); }, function (errors) { let error = errors[0]; if (error && error.code && error.code.startsWith('error.service.security.')) { // 没有操作权限,说明当前用户类型不被允许访问当前系统 _this.handleLoadingAuthoritiesError(); } }); } else { // 未指定获权地址,则视为具有所有权限,用于没有或不关心服务端权限的场景 this._applyAuthorities({ rank: '*', permissions: ['*'], }, scope, callback); } } } Menu.prototype._applyAuthorities = function (authorities, scope, callback) { if (!Array.isArray(authorities)) { authorities = [authorities]; } this.authorities = authorities; this.scope = scope; this._grantedItems = []; for (let item of this.items) { applyGrantedItemToItems(this.authorities, item, this._grantedItems); } callback(this._grantedItems); } Menu.prototype.resolveItemPathParams = function (vm, item) { if (item && item.path) { let path = item.path; if (path.contains('/:')) { // 包含路径参数 // 确保路径头尾都有/ if (!path.startsWith('/')) { path = '/' + path; } if (!path.endsWith('/')) { path += '/'; } // 替换路径参数 let params = vm.$route.params; Object.keys(params).forEach(key => { path = path.replace('/:' + key + '/', '/' + params[key] + '/'); }); path = path.substr(0, path.length - 1); // 去掉末尾的/ } return path; } return undefined; } Menu.prototype.handleLoadingAuthoritiesError = function () { window.tnx.error('没有访问该系统的权限,请重新登录。', function () { window.tnx.app.logout(); }); } export default Menu;