Files

751 lines
29 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* =============================================
* 权限管理模块 - Layui版本
* Auth + RBAC 权限控制完整实现
* =============================================
*
* 使用说明:
* 1. 放置在layui的modules目录下
* 2. 使用:layui.use('permission', function(mod){...})
* 3. 或者直接:var permission = layui.permission;
*
* @version 2.0.0 - Layui Module
* @author Permission Module
* @date 2024-01-01
*/
layui.define(function (exports) {
'use strict';
/**
* =============================================
* 权限配置管理器
* =============================================
*/
const PermissionConfig = (function () {
// 私有变量
const _defaultToolbar = {
create: { title: '创建资源', layEvent: 'dataCreate', icon: 'layui-icon-add-1', permission: 'create', color: 'layui-btn-normal', size: '', show: true },
recycle: { title: '回收站', layEvent: 'dataRecycle', icon: 'layui-icon-refresh', permission: 'recycle', color: 'layui-btn-warm', size: '', show: true },
filter: { title: '筛选', layEvent: 'filter', icon: 'layui-icon-search', permission: 'filter', color: 'layui-btn-primary', size: '', show: true },
export: { title: '导出', layEvent: 'export', icon: 'layui-icon-download-circle', permission: 'export', color: 'layui-btn-primary', size: '', show: true },
print: { title: '打印', layEvent: 'print', icon: 'layui-icon-print', permission: 'print', color: 'layui-btn-primary', size: '', show: true },
refresh: { title: '刷新', layEvent: 'refresh', icon: 'layui-icon-refresh', permission: 'refresh', color: 'layui-btn-primary', size: '', show: true },
batchDelete: { title: '批量删除', layEvent: 'batchDelete', icon: 'layui-icon-delete', permission: 'batch_delete', color: 'layui-btn-danger', size: '', show: true },
import: { title: '导入', layEvent: 'import', icon: 'layui-icon-upload-circle', permission: 'import', color: 'layui-btn-primary', size: '', show: true }
};
const _rowToolbar = {
view: { title: '查看', layEvent: 'dataView', icon: 'layui-icon-eye', permission: 'view', color: 'layui-btn-primary layui-btn-xs', size: 'layui-btn-xs', show: true },
edit: { title: '编辑', layEvent: 'dataEdit', icon: 'layui-icon-edit', permission: 'edit', color: 'layui-btn-normal layui-btn-xs', size: 'layui-btn-xs', show: true },
delete: { title: '删除', layEvent: 'dataDelete', icon: 'layui-icon-delete', permission: 'delete', color: 'layui-btn-danger layui-btn-xs', size: 'layui-btn-xs', show: true },
copy: { title: '复制', layEvent: 'dataCopy', icon: 'layui-icon-file-b', permission: 'copy', color: 'layui-btn-warm layui-btn-xs', size: 'layui-btn-xs', show: true },
enable: { title: '启用', layEvent: 'dataEnable', icon: 'layui-icon-play', permission: 'enable', color: 'layui-btn-normal layui-btn-xs', size: 'layui-btn-xs', show: true },
disable: { title: '禁用', layEvent: 'dataDisable', icon: 'layui-icon-pause', permission: 'disable', color: 'layui-btn-warm layui-btn-xs', size: 'layui-btn-xs', show: true },
audit: { title: '审核', layEvent: 'dataAudit', icon: 'layui-icon-ok-circle', permission: 'audit', color: 'layui-btn-normal layui-btn-xs', size: 'layui-btn-xs', show: true },
unaudit: { title: '取消审核', layEvent: 'dataUnaudit', icon: 'layui-icon-close', permission: 'unaudit', color: 'layui-btn-warm layui-btn-xs', size: 'layui-btn-xs', show: true }
};
const _customButtons = {
toolbar: {},
row: {}
};
// 工具函数
const _isArray = function (obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
};
// 公共方法
return {
/**
* 获取默认工具条配置
*/
getDefaultToolbar: function () {
return Object.assign({}, _defaultToolbar);
},
/**
* 获取行工具条配置
*/
getRowToolbar: function () {
return Object.assign({}, _rowToolbar);
},
/**
* 注册自定义按钮
* @param {String} type - 按钮类型 (toolbar/row)
* @param {String} key - 按钮唯一标识
* @param {Object} config - 按钮配置
*/
registerButton: function (type, key, config) {
if (!['toolbar', 'row'].includes(type)) {
console.error('按钮类型必须是 toolbar 或 row');
return false;
}
if (!_customButtons[type]) {
_customButtons[type] = {};
}
_customButtons[type][key] = Object.assign({
title: key,
layEvent: key,
icon: 'layui-icon-more',
permission: key,
color: 'layui-btn-primary',
size: '',
show: true
}, config);
return true;
},
/**
* 获取工具条配置
* @param {String} type - toolbar/row
* @param {Array} keys - 需要的按钮key列表
* @param {Object} customConfig - 自定义配置(覆盖默认配置)
* @returns {Array} 配置数组
*/
getToolbarConfig: function (type, keys = [], customConfig = {}) {
const baseConfig = type === 'toolbar' ? _defaultToolbar : _rowToolbar;
const customBtns = _customButtons[type] || {};
// 合并基础配置和自定义配置
const allConfig = Object.assign({}, baseConfig, customBtns);
// 应用自定义配置覆盖
Object.keys(customConfig).forEach(key => {
if (allConfig[key]) {
allConfig[key] = Object.assign(allConfig[key], customConfig[key]);
}
});
// 如果指定了keys,只返回指定的按钮
if (keys.length > 0) {
return keys.map(key => allConfig[key]).filter(Boolean);
}
// 返回所有配置
return Object.values(allConfig).filter(btn => btn.show !== false);
},
/**
* 获取单个按钮配置
* @param {String} type - toolbar/row
* @param {String} key - 按钮key
* @returns {Object} 按钮配置
*/
getButtonConfig: function (type, key) {
const config = this.getToolbarConfig(type);
return config.find(btn => btn.layEvent === key || (btn.permission && btn.permission.includes(key)));
},
/**
* 批量注册按钮
* @param {String} type - 按钮类型
* @param {Object} buttons - 按钮配置对象
*/
registerButtons: function (type, buttons) {
Object.keys(buttons).forEach(key => {
this.registerButton(type, key, buttons[key]);
});
},
/**
* 清除自定义按钮
* @param {String} type - 按钮类型
* @param {String} key - 按钮key,不传则清除所有
*/
clearCustomButtons: function (type, key) {
if (key) {
delete _customButtons[type]?.[key];
} else {
_customButtons[type] = {};
}
}
};
})();
/**
* =============================================
* 权限验证工具类
* =============================================
*/
const Auth = (function () {
// 私有变量
let _permissions = [];
let _permissionCache = {};
let _user = null;
let _controller = null; // 当前页面对应的控制器(用于裸动作 token 解析)
// 工具函数
const _isArray = function (obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
};
const _isString = function (obj) {
return typeof obj === 'string';
};
// 前端短词 → 后端动作 同义词(按钮配置常用 create,权限 key 用 add
const _synonyms = {
create: 'add',
recycle: 'recyclebin',
batch_delete: 'delete',
batchDelete: 'delete'
};
const _splitPermissions = function (perm) {
if (_isArray(perm)) return perm;
if (_isString(perm)) return perm.split(',').map(p => p.trim());
return [];
};
// 公共方法
return {
/**
* 初始化权限
* @param {Array} permissions - 用户权限列表
* @param {Object} user - 用户信息
*/
init: function (permissions = [], user = null) {
_permissions = permissions || [];
_user = user;
_controller = null;
this.clearCache();
},
/**
* 获取当前用户
* @returns {Object} 用户信息
*/
getUser: function () {
return _user;
},
/**
* 设置当前用户
* @param {Object} user - 用户信息
*/
setUser: function (user) {
_user = user;
},
/**
* 获取权限列表
* @returns {Array} 权限列表
*/
getPermissions: function () {
return _permissions;
},
/**
* 设置当前页面对应的控制器(用于裸动作 token 解析,如 has('view') → admin:view
*/
setController: function (controller) {
_controller = controller || null;
this.clearCache();
return this;
},
/**
* 检查单个权限
* 支持:
* - 精确 keyadmin:edit
* - 控制器通配:admin:*(覆盖该控制器下所有动作)
* - 超级管理员:* / *:*
* - 裸动作 token(结合 setController):view → admin:viewcreate → admin:add(同义词)
* @param {String} permission - 权限标识
* @returns {Boolean}
*/
has: function (permission) {
if (!permission) return true;
// 使用缓存
if (_permissionCache[permission] !== undefined) {
return _permissionCache[permission];
}
const perms = _splitPermissions(permission);
const result = perms.some(p => this._match(p));
_permissionCache[permission] = result;
return result;
},
/**
* 单个权限标识匹配(内部)
*/
_match: function (p) {
if (!p) return true;
// 精确匹配
if (_permissions.indexOf(p) > -1) return true;
// 超级管理员放行
if (_permissions.indexOf('*') > -1 || _permissions.indexOf('*:*') > -1) return true;
// 完整 keycontroller:action / controller:*
if (p.indexOf(':') > -1) {
const parts = p.split(':');
const ctrl = parts[0];
const act = parts[1];
if (act === '*') {
return _permissions.indexOf(ctrl + ':*') > -1;
}
return _permissions.indexOf(ctrl + ':' + act) > -1
|| _permissions.indexOf(ctrl + ':*') > -1;
}
// 裸动作 token:结合当前控制器解析(含同义词)
const action = _synonyms[p] || p;
if (_controller) {
if (_permissions.indexOf(_controller + ':*') > -1) return true;
if (_permissions.indexOf(_controller + ':' + action) > -1) return true;
}
return false;
},
/**
* 检查多个权限(满足一个即可)
* @param {Array} permissions - 权限列表
* @returns {Boolean}
*/
hasAny: function (permissions) {
if (!_isArray(permissions) || permissions.length === 0) return false;
return permissions.some(p => this.has(p));
},
/**
* 检查所有权限
* @param {Array} permissions - 权限列表
* @returns {Boolean}
*/
hasAll: function (permissions) {
if (!_isArray(permissions) || permissions.length === 0) return false;
return permissions.every(p => this.has(p));
},
/**
* 检查权限并返回权限标识
* @param {String|Array} permissions - 权限标识
* @returns {String|Boolean} 有权限返回权限标识,无权限返回false
*/
check: function (permissions) {
if (_isArray(permissions)) {
for (let p of permissions) {
if (this.has(p)) {
return p;
}
}
return false;
}
return this.has(permissions) ? permissions : false;
},
/**
* 过滤有权限的按钮
* @param {Array} buttons - 按钮配置数组
* @returns {Array} 有权限的按钮数组
*/
filterButtons: function (buttons) {
if (!_isArray(buttons)) return [];
return buttons.filter(btn => {
// 如果没有设置permission,默认显示
if (!btn.permission) return true;
// 支持多个权限,用逗号分隔
const perms = _splitPermissions(btn.permission);
return this.hasAny(perms);
});
},
/**
* 生成工具条HTML
* @param {Array} buttons - 按钮配置数组
* @param {String} containerClass - 容器样式类
* @param {Object} options - 额外选项
* @returns {String} HTML字符串
*/
renderToolbar: function (buttons, containerClass = 'layui-btn-container', options = {}) {
const filteredButtons = this.filterButtons(buttons);
if (filteredButtons.length === 0) return '';
console.log('🔧 渲染工具条 - 需要在项目中实现',filteredButtons);
// const html = filteredButtons.map(btn => {
// const color = btn.color || 'layui-btn-primary';
// const size = btn.size || '';
// const icon = btn.icon ? `<i class="${btn.icon}"></i>` : '';
// const disabled = btn.disabled ? 'disabled' : '';
// const style = btn.style ? `style="${btn.style}"` : '';
// return `<button class="layui-btn ${color} ${size}" lay-event="${btn.layEvent}" title="${btn.title}" ${disabled} ${style}>
// ${icon}${btn.title}
// </button>`;
// }).join('');
// const containerStyle = options.containerStyle ? `style="${options.containerStyle}"` : '';
// return `<div class="${containerClass}" ${containerStyle}>${html}</div>`;
},
/**
* 生成行工具条HTML
* @param {Array} buttons - 按钮配置数组
* @param {Object} options - 额外选项
* @returns {String} HTML字符串
*/
renderRowToolbar: function (buttons, options = {}) {
const filteredButtons = this.filterButtons(buttons);
if (filteredButtons.length === 0) return '';
const html = filteredButtons.map(btn => {
const color = btn.color || 'layui-btn-primary layui-btn-xs';
const icon = btn.icon ? `<i class="${btn.icon}"></i>` : '';
const disabled = btn.disabled ? 'disabled' : '';
const style = btn.style ? `style="${btn.style}"` : '';
return `<a class="layui-btn ${color}" lay-event="${btn.layEvent}" title="${btn.title}" ${disabled} ${style}>
${icon}
</a>`;
}).join('');
return `<div class="layui-btn-group">${html}</div>`;
},
/**
* 清除缓存
*/
clearCache: function () {
_permissionCache = {};
},
/**
* 批量检查权限
* @param {Array} permissions - 权限列表
* @returns {Object} 权限检查结果对象
*/
batchCheck: function (permissions) {
if (!_isArray(permissions)) return {};
return permissions.reduce((result, perm) => {
result[perm] = this.has(perm);
return result;
}, {});
},
/**
* 预加载常用权限
* @param {Array} permissions - 常用权限列表
*/
preload: function (permissions = ['create', 'edit', 'delete', 'view']) {
permissions.forEach(perm => this.has(perm));
}
};
})();
/**
* =============================================
* 动态权限更新管理器
* =============================================
*/
const PermissionManager = (function () {
// 私有变量
const _listeners = [];
let _refreshTimeout = null;
// 公共方法
return {
/**
* 添加权限变更监听器
* @param {Function} callback - 回调函数
* @param {String} id - 监听器ID(用于移除)
*/
onPermissionChange: function (callback, id = null) {
_listeners.push({ callback, id });
},
/**
* 移除权限变更监听器
* @param {String} id - 监听器ID
*/
offPermissionChange: function (id) {
const index = _listeners.findIndex(l => l.id === id);
if (index !== -1) {
_listeners.splice(index, 1);
}
},
/**
* 触发权限变更
* @param {Array} newPermissions - 新的权限列表
* @param {Object} user - 用户信息
*/
triggerChange: function (newPermissions, user = null) {
// 更新权限
Auth.init(newPermissions, user);
// 保存到localStorage
if (typeof localStorage !== 'undefined') {
localStorage.setItem('permissions', JSON.stringify(newPermissions));
if (user) {
localStorage.setItem('user', JSON.stringify(user));
}
}
// 通知所有监听器
_listeners.forEach(({ callback }) => {
try {
callback(newPermissions, user);
} catch (e) {
console.error('权限变更监听器执行出错:', e);
}
});
// 防抖刷新表格
clearTimeout(_refreshTimeout);
_refreshTimeout = setTimeout(() => {
this.refreshTables();
}, 100);
},
/**
* 刷新所有表格
*/
refreshTables: function () {
console.log('🔄 刷新表格 - 需要在项目中实现');
// 触发自定义事件
const event = new CustomEvent('permissionChanged', {
detail: { permissions: Auth.getPermissions() }
});
if (typeof window !== 'undefined') {
window.dispatchEvent(event);
}
},
/**
* 从服务器同步最新权限
* @param {String} url - 权限接口地址
* @returns {Promise} 同步结果
*/
syncPermissions: function (url = '/api/user/permissions') {
// return new Promise((resolve, reject) => {
// if (typeof $ !== 'undefined' && $.ajax) {
// $.get(url, function (res) {
// if (res.code === 200 || res.success) {
// this.triggerChange(res.data.permissions || res.data, res.data.user);
// resolve(true);
// } else {
// reject(new Error(res.msg || '同步权限失败'));
// }
// }.bind(this)).fail(function (xhr) {
// reject(new Error('网络请求失败: ' + xhr.statusText));
// });
// } else if (typeof fetch !== 'undefined') {
// fetch(url)
// .then(res => res.json())
// .then(res => {
// if (res.code === 200 || res.success) {
// this.triggerChange(res.data.permissions || res.data, res.data.user);
// resolve(true);
// } else {
// reject(new Error(res.msg || '同步权限失败'));
// }
// })
// .catch(err => reject(err));
// } else {
// reject(new Error('不支持的HTTP请求方式'));
// }
// }.bind(this));
},
/**
* 清除所有监听器
*/
clearListeners: function () {
_listeners.length = 0;
}
};
})();
/**
* =============================================
* 权限按钮组件
* =============================================
*/
class AuthButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._clickHandler = this._handleClick.bind(this);
}
static get observedAttributes() {
return ['permission', 'text', 'icon', 'type', 'size', 'disabled'];
}
connectedCallback() {
this.render();
}
disconnectedCallback() {
this.shadowRoot.removeEventListener('click', this._clickHandler);
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
_handleClick(e) {
if (!this.hasAttribute('disabled')) {
this.dispatchEvent(new CustomEvent('auth-click', {
bubbles: true,
composed: true,
detail: { button: this }
}));
}
}
render() {
const permission = this.getAttribute('permission');
const text = this.getAttribute('text') || '按钮';
const icon = this.getAttribute('icon') || '';
const type = this.getAttribute('type') || 'primary';
const size = this.getAttribute('size') || '';
const disabled = this.hasAttribute('disabled');
const hasPermission = !permission || Auth.has(permission);
// 无权限时隐藏
if (!hasPermission) {
this.shadowRoot.innerHTML = '';
this.style.display = 'none';
return;
}
this.style.display = '';
const iconHtml = icon ? `<i class="${icon}"></i>` : '';
const btnClass = `layui-btn layui-btn-${type} ${size}`;
const disabledAttr = disabled ? 'disabled' : '';
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
}
.layui-btn {
display: inline-block;
height: 38px;
line-height: 38px;
padding: 0 18px;
background-color: #009688;
color: #fff;
white-space: nowrap;
text-align: center;
font-size: 14px;
border: none;
border-radius: 2px;
cursor: pointer;
box-sizing: border-box;
text-decoration: none;
}
.layui-btn:hover {
opacity: 0.9;
}
.layui-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.layui-btn-xs {
height: 26px;
line-height: 26px;
padding: 0 10px;
font-size: 12px;
}
.layui-btn-sm {
height: 30px;
line-height: 30px;
padding: 0 10px;
font-size: 12px;
}
.layui-btn-lg {
height: 44px;
line-height: 44px;
padding: 0 25px;
font-size: 16px;
}
.layui-btn-primary {
background-color: #fff;
color: #333;
border: 1px solid #d2d2d2;
}
.layui-btn-normal {
background-color: #1E9FFF;
}
.layui-btn-warm {
background-color: #FFB800;
}
.layui-btn-danger {
background-color: #FF5722;
}
i {
margin-right: 5px;
}
</style>
<button class="${btnClass}" ${disabledAttr}>
${iconHtml}${text}
</button>
`;
// 添加点击事件
const btn = this.shadowRoot.querySelector('button');
btn.removeEventListener('click', this._clickHandler);
btn.addEventListener('click', this._clickHandler);
}
}
// 注册自定义元素
if (typeof customElements !== 'undefined' && !customElements.get('auth-button')) {
customElements.define('auth-button', AuthButton);
}
/**
* =============================================
* 导出模块
* =============================================
*/
exports('auth', {
// 核心对象auth
Auth: Auth,
PermissionConfig: PermissionConfig,
PermissionManager: PermissionManager,
// 便捷方法(直接暴露常用方法)
init: Auth.init.bind(Auth),
setController: Auth.setController.bind(Auth),
has: Auth.has.bind(Auth),
hasAny: Auth.hasAny.bind(Auth),
hasAll: Auth.hasAll.bind(Auth),
getPermissions: Auth.getPermissions.bind(Auth),
renderToolbar: Auth.renderToolbar.bind(Auth),
renderRowToolbar: Auth.renderRowToolbar.bind(Auth),
filterButtons: Auth.filterButtons.bind(Auth),
// 配置方法
getToolbarConfig: PermissionConfig.getToolbarConfig.bind(PermissionConfig),
registerButton: PermissionConfig.registerButton.bind(PermissionConfig),
registerButtons: PermissionConfig.registerButtons.bind(PermissionConfig),
// 管理器方法
onPermissionChange: PermissionManager.onPermissionChange.bind(PermissionManager),
triggerChange: PermissionManager.triggerChange.bind(PermissionManager),
syncPermissions: PermissionManager.syncPermissions.bind(PermissionManager),
// 按钮组件
AuthButton: AuthButton
});
});