fix: 修复角色权限管理模块的API认证和数据加载问题

主要修复:
1. 修复所有RBAC API函数使用axios-client(自动添加JWT token)
   - getRoles, createRole, updateRole, deleteRole 从rbacFetch切换到axios-client
   - 解决401未授权导致的数据加载失败问题

2. 修复用户ID字段不匹配问题
   - getAllUsers函数使用user_id字段(兼容user.user_id || user.id)
   - 确保角色分配时使用正确的用户ID

3. 修复路由ID不匹配问题
   - getRoutes函数改用真实后端API(GET /rbac/user/routes)
   - 解决前端Mock路由ID与数据库不一致导致的400错误

4. 增强axios-client成功响应识别
   - 支持code=200作为成功状态(原本只支持code=0)
   - 兼容不同后端API的响应格式

5. 实现用户单角色限制功能
   - 添加getUserRoles API函数
   - 分配角色前检查用户现有角色
   - 在用户列表中显示当前角色标签

6. 改进创建角色的表单验证
   - role_key必须以字母开头(正则:^[a-z][a-z0-9_]*$)
   - 添加实时验证提示
   - 更新提示文案说明规则

7. 添加删除操作的安全确认机制
   - 删除角色/移除用户角色前显示确认模态框
   - 3秒倒计时后才能确认删除
   - 成功删除后自动刷新数据

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-24 18:03:57 +08:00
parent 1b546e6818
commit 689ef6bc3d
5 changed files with 2802 additions and 174 deletions
+27 -1
View File
@@ -81,6 +81,7 @@ axiosInstance.interceptors.request.use(
(config) => {
// 检查是否在白名单中
if (isInAuthWhitelist(config.url)) {
console.log('🔓 [Request Interceptor] URL在白名单中,跳过Authorization:', config.url);
return config;
}
@@ -89,12 +90,24 @@ axiosInstance.interceptors.request.use(
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
console.log('🔑 [Request Interceptor] 添加Authorization头:', {
url: config.url,
method: config.method,
hasToken: !!token,
tokenPreview: token.substring(0, 20) + '...'
});
} else {
console.warn('⚠️ [Request Interceptor] 没有找到access_token:', {
url: config.url,
localStorage: Object.keys(localStorage)
});
}
}
return config;
},
(error) => {
console.error('❌ [Request Interceptor] 请求拦截器错误:', error);
return Promise.reject(error);
}
);
@@ -114,9 +127,21 @@ export class AuthenticationError extends Error {
*/
axiosInstance.interceptors.response.use(
(response) => {
console.log('✅ [Response Interceptor] 请求成功:', {
url: response.config.url,
status: response.status,
statusText: response.statusText
});
return response;
},
(error) => {
console.error('❌ [Response Interceptor] 请求失败:', {
url: error.config?.url,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data
});
if (isAxiosError(error) && error.response?.status === 401) {
// 检查是否在错误容忍白名单中
const requestUrl = error.config?.url;
@@ -442,7 +467,8 @@ export async function apiRequest<T>(
// 检查API返回的状态码
const data = response.data;
if (data && typeof data === 'object' && 'code' in data && data.code !== 0) {
// 修复:支持code=0PostgREST)和code=200RBAC API)两种成功响应
if (data && typeof data === 'object' && 'code' in data && data.code !== 0 && data.code !== 200) {
const errorMessage = data.message || data.msg || '未知错误';
console.error(`API请求失败: ${errorMessage} - ${url}`);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+224
View File
@@ -505,6 +505,230 @@
}
}
/* ==================== 表单样式 ==================== */
.role-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.form-group label.required::after {
content: ' *';
color: var(--color-error);
}
.form-input,
.form-textarea,
.form-select {
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
color: #303133;
transition: border-color 0.2s;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.1);
}
.form-input.error,
.form-textarea.error,
.form-select.error {
border-color: var(--color-error);
}
.form-input:disabled,
.form-textarea:disabled,
.form-select:disabled {
background: #f5f7fa;
cursor: not-allowed;
color: #909399;
}
.form-hint {
font-size: 12px;
color: #909399;
line-height: 1.5;
}
.form-error {
font-size: 12px;
color: var(--color-error);
line-height: 1.5;
}
.form-notice {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-radius: 4px;
font-size: 14px;
background: #ecf5ff;
border: 1px solid #b3d8ff;
color: #606266;
}
.form-notice.warning {
background: #fef0f0;
border-color: #fbc4c4;
color: #606266;
}
.form-notice i {
font-size: 18px;
color: #409eff;
}
.form-notice.warning i {
color: var(--color-error);
}
/* 分配用户模态框 */
.assign-user-modal {
display: flex;
flex-direction: column;
gap: 16px;
}
/* 全选栏 */
.select-all-bar {
padding: 12px 16px;
background: #f5f7fa;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.select-all-bar .user-checkbox-item {
padding: 0;
margin: 0;
}
.select-all-bar .user-checkbox-item:hover {
background: transparent;
}
.select-all-bar .user-name {
font-weight: 600;
color: #303133;
}
/* 用户复选框列表 */
.users-checkbox-list {
max-height: 400px;
overflow-y: auto;
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 12px;
background: #fafbfc;
}
.user-checkbox-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.user-checkbox-item:hover {
background: #e6e8eb;
}
.user-checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--color-primary);
}
.user-checkbox-item .user-info {
flex: 1;
min-width: 0;
}
.user-checkbox-item .user-name {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.user-checkbox-item .user-meta {
font-size: 12px;
color: #909399;
}
/* 搜索框 */
.search-box {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
background: white;
margin-bottom: 16px;
}
.search-box i {
font-size: 18px;
color: #909399;
}
.search-box input {
flex: 1;
border: none;
outline: none;
font-size: 14px;
color: #303133;
}
.search-box input::placeholder {
color: #c0c4cc;
}
/* 无权限卡片 */
.no-permission-card {
max-width: 800px;
margin: 60px auto;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.no-permission-card .empty-state {
text-align: center;
}
.no-permission-card h2 {
margin: 0;
}
.no-permission-card p {
margin: 0;
}
/* ==================== 响应式布局 ==================== */
/* 响应式布局 */
@media (max-width: 1200px) {
.permissions-container {