/**
* TalkingQ Platform - RBAC Permission Management Page
* 参考阿里云百炼控制台风格
* 按公司分类的权限管理
*/
registerPage('rbac', function(container) {
const USERS_STORAGE_KEY = 'talkingq_rbac_users_v1';
const DEFAULT_ROLE_COMPANIES = ['company_a', 'company_b', 'company_c'];
const DEFAULT_USERS = [
{ id: 'u001', displayName: '张明华', loginName: 'zhangmh@talkingq.com', status: 'active', lastLogin: '2026-05-12 14:10', role: 'admin', remark: '超级管理员' },
{ id: 'u002', displayName: '李思远', loginName: 'lisiy@talkingq.com', status: 'active', lastLogin: '2026-05-12 11:30', role: 'worker', remark: '运营人员' },
{ id: 'u003', displayName: '王晓燕', loginName: 'wangxy@partner.com', status: 'active', lastLogin: '2026-05-11 09:00', role: 'client', companyId: 'company_a', allowedCompanies: ['company_a'], remark: '智慧星科技-客户' },
{ id: 'u004', displayName: '刘亚楠', loginName: 'liuyn@partner.com', status: 'inactive', lastLogin: '2026-04-28 16:00', role: 'client', companyId: 'company_b', allowedCompanies: ['company_b'], remark: '未来玩具厂-客户' },
{ id: 'u005', displayName: '陈志强', loginName: 'chenzq@talkingq.com', status: 'active', lastLogin: '2026-05-10 09:15', role: 'worker', remark: '开发人员' },
];
function cloneUsers(list) {
return list.map(item => ({ ...item, allowedCompanies: Array.isArray(item.allowedCompanies) ? [...item.allowedCompanies] : item.allowedCompanies }));
}
function loadUsers() {
try {
localStorage.removeItem(USERS_STORAGE_KEY);
if (Array.isArray(window.__rbacUsers) && window.__rbacUsers.length) {
return window.__rbacUsers.map((user, index) => normalizeUser(user, index));
}
} catch (error) {
console.warn('Failed to clear legacy RBAC user cache', error);
}
return cloneUsers(DEFAULT_USERS).map((user, index) => normalizeUser(user, index));
}
function saveUsers(users) {
window.__rbacUsers = users;
}
function normalizeUser(user, index = 0) {
const role = normalizeRole(user.role || inferRoleFromRemark(user.remark));
const companyId = role === 'client'
? (user.companyId || user.allowedCompanies?.[0] || DEFAULT_ROLE_COMPANIES[index % DEFAULT_ROLE_COMPANIES.length])
: null;
return {
id: user.id || `u${String(index + 1).padStart(3, '0')}`,
displayName: user.displayName || user.loginName || '未命名用户',
loginName: String(user.loginName || '').toLowerCase(),
status: user.status || 'active',
lastLogin: user.lastLogin || '-',
role,
companyId,
allowedCompanies: role === 'client'
? normalizeAllowedCompanies(user.allowedCompanies, companyId)
: DEFAULT_ROLE_COMPANIES.slice(),
remark: user.remark || (role === 'client' ? `${window.__getCompanyNameById?.(companyId) || companyId}-客户` : role === 'admin' ? '超级管理员' : '运营人员'),
};
}
function normalizeRole(role) {
const raw = String(role || '').toLowerCase();
if (raw === 'admin' || raw === 'administrator') return 'admin';
if (raw === 'worker' || raw === 'employee') return 'worker';
if (raw === 'client' || raw === 'customer') return 'client';
return 'worker';
}
function inferRoleFromRemark(remark) {
const text = String(remark || '');
if (text.includes('超级管理员')) return 'admin';
if (text.includes('客户')) return 'client';
return 'worker';
}
function normalizeAllowedCompanies(value, companyId) {
if (Array.isArray(value) && value.length) return value;
if (companyId) return [companyId];
return [];
}
function getCompanyOptionsHtml(selectedCompanyId = '') {
const companies = (window.companies || []).filter(company => company.id !== 'all');
return companies.map(company => `
`).join('');
}
function getUserById(userId) {
return users.find(user => user.id === userId);
}
function getUserRoleLabel(user) {
if (user.role === 'admin') return '管理员';
if (user.role === 'client') return '客户';
return '运营人员';
}
function getUserRoleBadgeColor(user) {
if (user.role === 'admin') return '#722ed1';
if (user.role === 'client') return '#52c41a';
return '#1890ff';
}
function getCompanyName(companyId) {
return window.__getCompanyNameById ? window.__getCompanyNameById(companyId) : companyId;
}
let users = loadUsers();
window.__rbacUsers = users;
window.__lookupRbacUserByLoginName = function(loginName) {
const normalized = String(loginName || '').trim().toLowerCase();
return users.find(user => user.loginName === normalized) || null;
};
const roles = [
{ name: 'Admin', desc: '超级管理员,拥有所有权限', color: '#722ed1', perms: ['全部功能'] },
{ name: 'Worker', desc: '运营人员,可配置应用,负责日常运营和 Bad Case 处理', color: '#1890ff', perms: ['仪表盘', '对话应用配置', '应用管理', '会话分析', 'Bad Case', '设备管理', 'OTA升级'] },
{ name: 'Client', desc: 'B端客户,只读访问自己公司的数据', color: '#52c41a', perms: ['仪表盘(只读)', '设备管理(只读)', 'OTA状态(只读)'] },
];
container.innerHTML = `
| 显示名称 |
登录名称 |
用户类型 |
备注 |
操作 |
${users.map(u => {
const userType = getUserRoleLabel(u);
const typeColor = getUserRoleBadgeColor(u);
const companyTag = u.role === 'client' && u.companyId
? `${escapeHtml(getCompanyName(u.companyId))}`
: '';
return `
${u.displayName[0]}
${u.displayName}
${u.status === 'active'
? ' '
: ' '}
|
${u.loginName} |
${userType} |
|
|
|
`}).join('')}
${roles.map(r => `
${r.name}
${r.perms.length} 项权限
${r.desc}
可访问功能:
${r.perms.map(p => `${p}`).join('')}
`).join('')}
RBAC 接口:GET /api/v1/rbac/roles、PUT /api/v1/rbac/roles/{id}、POST /api/v1/rbac/users/assign-role
| 时间 | 操作人 | 操作类型 | 目标 | IP 地址 | 结果 |
${[
{ time: '14:22:05', user: '张明华', action: '登录', target: '系统', ip: '192.168.1.10', ok: true },
{ time: '14:18:30', user: '张明华', action: '修改 Prompt', target: '小Q故事机器人 v2.3', ip: '192.168.1.10', ok: true },
{ time: '13:55:12', user: '李思远', action: '关闭 Bad Case', target: 'BC-2026-004', ip: '10.0.2.15', ok: true },
{ time: '11:30:00', user: 'system', action: '自动触发内容安全', target: 'sess-3f7a', ip: 'internal', ok: false },
{ time: '09:15:42', user: '张明华', action: '创建 OTA 任务', target: 'OTA-2026-002', ip: '192.168.1.10', ok: true },
].map(a => `
| 2026-05-12 ${a.time} |
${a.user} |
${a.action} |
${a.target} |
${a.ip} |
${a.ok ? '成功' : '告警'} |
`).join('')}
`;
lucide.createIcons({ elements: [container] });
// Tab switch
container.querySelectorAll('#rbacTabs .tab-item').forEach(tab => {
tab.addEventListener('click', () => {
container.querySelectorAll('#rbacTabs .tab-item').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
container.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
container.querySelector(`#tab-${tab.dataset.tab}`)?.classList.add('active');
});
});
// Add user button
document.getElementById('addUserBtn')?.addEventListener('click', () => {
createModal({
title: '添加用户',
content: `
`,
onConfirm: async () => {
const displayName = document.getElementById('newUserDisplayName')?.value.trim();
const loginName = document.getElementById('newUserLoginName')?.value.trim();
const role = document.getElementById('newUserRole')?.value;
const companyId = document.getElementById('newUserCompany')?.value;
const password = document.getElementById('newUserPassword')?.value;
if (!displayName || !loginName || !role || !password) {
Toast.warning('请填写用户必填信息');
return false;
}
if (role === 'client' && !companyId) {
Toast.warning('Client 角色必须选择所属公司');
return false;
}
const normalizedUser = normalizeUser({
id: `u${Date.now()}`,
displayName,
loginName,
role,
companyId: role === 'client' ? companyId : null,
allowedCompanies: role === 'client' ? [companyId] : DEFAULT_ROLE_COMPANIES.slice(),
status: 'active',
lastLogin: '-',
remark: role === 'client' ? `${getCompanyName(companyId)}-客户` : role === 'admin' ? '超级管理员' : '运营人员',
});
users.unshift(normalizedUser);
saveUsers(users);
await API.rbac.createUser({ displayName, loginName, role });
Toast.success('用户创建成功,已发送激活邮件');
navigateTo('rbac');
},
confirmText: '创建用户',
});
});
// Global functions for action buttons
window.editUser = function(userId) {
const user = getUserById(userId);
if (!user) return;
createModal({
title: '编辑用户',
content: `
`,
onConfirm: async () => {
const displayName = document.getElementById('editUserDisplayName')?.value.trim();
const role = document.getElementById('editUserRole')?.value || 'worker';
const companyId = document.getElementById('editUserCompany')?.value;
const remark = document.getElementById('editUserRemark')?.value.trim();
if (role === 'client' && !companyId) {
Toast.warning('Client 角色必须选择所属公司');
return false;
}
const nextUser = normalizeUser({
...user,
displayName: displayName || user.displayName,
role,
companyId: role === 'client' ? companyId : null,
allowedCompanies: role === 'client' ? [companyId] : DEFAULT_ROLE_COMPANIES.slice(),
remark: remark || (role === 'client' ? `${getCompanyName(companyId)}-客户` : role === 'admin' ? '超级管理员' : '运营人员'),
});
const idx = users.findIndex(item => item.id === user.id);
if (idx >= 0) users[idx] = nextUser;
saveUsers(users);
await API.rbac.updateUser(userId, {
role: role,
allowedCompanies: role === 'client' ? [companyId] : DEFAULT_ROLE_COMPANIES.slice(),
});
Toast.success(`用户 ${nextUser.displayName} 信息已更新`);
navigateTo('rbac');
},
confirmText: '保存',
});
lucide.createIcons();
};
// 切换公司访问配置显示
window.toggleCompanyAccess = function(role) {
const group = document.getElementById('companyAccessGroup');
const select = group?.querySelector('select');
if (group) {
const normalized = String(role || '').toLowerCase();
group.style.display = normalized === 'client' ? '' : 'none';
if (select) select.disabled = normalized !== 'client';
}
};
window.managePermissions = function(userId) {
const user = getUserById(userId);
if (!user) return;
createModal({
title: `权限管理 - ${user.displayName}`,
size: 'modal-lg',
content: `
为用户分配角色和细粒度权限
细粒度权限
${[
{ name: '数据仪表盘', checked: true, readonly: false },
{ name: '对话应用配置', checked: user.role !== 'client', readonly: user.role === 'client' },
{ name: '应用管理', checked: user.role !== 'client', readonly: user.role === 'client' },
{ name: '设备管理', checked: true, readonly: false },
{ name: 'OTA 升级', checked: user.role !== 'client', readonly: user.role === 'client' },
{ name: '会话详情', checked: true, readonly: false },
{ name: 'Bad Case 管理', checked: user.role !== 'client', readonly: user.role === 'client' },
{ name: '权限管理', checked: user.role !== 'client', readonly: true },
{ name: '系统设置', checked: user.role !== 'client', readonly: true },
].map(p => `
`).join('')}
`,
onConfirm: () => {
const companyId = document.getElementById('permCompanySelect')?.value || user.companyId;
const nextRole = document.getElementById('permRoleSelect')?.value || 'client';
const idx = users.findIndex(item => item.id === user.id);
if (idx >= 0) {
users[idx] = normalizeUser({
...user,
role: nextRole,
companyId: nextRole === 'client' ? companyId : null,
allowedCompanies: nextRole === 'client' ? [companyId] : DEFAULT_ROLE_COMPANIES.slice(),
remark: nextRole === 'client' ? `${getCompanyName(companyId)}-客户` : nextRole === 'admin' ? '超级管理员' : '运营人员',
});
saveUsers(users);
}
Toast.success(`${user.displayName} 的权限已更新`);
navigateTo('rbac');
},
confirmText: '保存权限',
});
lucide.createIcons();
};
window.deleteUser = function(userId) {
const user = getUserById(userId);
if (!user) return;
createModal({
title: '确认删除',
content: `
确认删除用户 ${user.displayName}?
此操作不可撤销,删除后该用户将无法登录系统
`,
onConfirm: () => {
users = users.filter(item => item.id !== userId);
saveUsers(users);
Toast.success(`用户 ${user.displayName} 已删除`);
navigateTo('rbac');
},
confirmText: '确认删除',
cancelText: '取消',
});
lucide.createIcons();
};
window.editRolePermissions = function(roleName) {
const role = roles.find(r => r.name === roleName);
if (!role) return;
// Client 角色特殊处理 - 需要配置公司级权限
if (roleName === 'Client') {
createModal({
title: '编辑 Client 角色权限',
content: `
接口:PUT /api/v1/rbac/roles/client
`,
onConfirm: async () => {
Toast.success('Client 角色权限已更新');
},
confirmText: '保存',
});
lucide.createIcons();
} else {
// Admin 和 Worker 角色显示功能权限
createModal({
title: `编辑 ${roleName} 角色权限`,
content: `
${roleName === 'Admin' ? `
` : `
`}
接口:PUT /api/v1/rbac/roles/${roleName.toLowerCase()}
`,
onConfirm: async () => {
Toast.success(`${roleName} 角色权限已更新`);
},
confirmText: '保存',
});
lucide.createIcons();
}
};
window.viewRoleUsers = function(roleName) {
const roleUsers = users.filter(u => {
if (roleName === 'Admin') return u.role === 'admin';
if (roleName === 'Worker') return u.role === 'worker';
if (roleName === 'Client') return u.role === 'client';
return false;
});
createModal({
title: `${roleName} 角色下的用户`,
content: `
共 ${roleUsers.length} 个用户
${roleUsers.map(u => `
${u.displayName[0]}
${u.displayName}
${u.loginName}
`).join('')}
`,
confirmText: '关闭',
showCancel: false,
});
};
});