Merge branch 'Wren' into shiy-login
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios';
|
||||
import { mockData, type MockApiResponse } from './mock';
|
||||
import { API_BASE_URL, DOCUMENT_URL } from '../config/api-config';
|
||||
import { toastService } from '../components/ui/Toast';
|
||||
|
||||
/**
|
||||
* API响应类型
|
||||
@@ -173,6 +174,19 @@ axiosInstance.interceptors.response.use(
|
||||
}
|
||||
}
|
||||
|
||||
// 🔒 403 无权限错误处理
|
||||
if (isAxiosError(error) && error.response?.status === 403) {
|
||||
console.warn('⚠️ [403 Forbidden] 无权限访问:', error.config?.url);
|
||||
|
||||
// 只在客户端显示 toast 提示
|
||||
if (typeof window !== 'undefined') {
|
||||
toastService.warning('无权限访问该资源');
|
||||
}
|
||||
|
||||
// 修改错误消息为友好提示,避免显示原始的 "Request failed with status code 403"
|
||||
error.message = '无权限访问该资源';
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -59,6 +59,18 @@ function handleApiResponse<T>(response: ApiResponse<any>): T {
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/**
|
||||
* API权限信息(关联到路由的权限)
|
||||
* v3.0新增:每个路由可以关联多个API操作权限
|
||||
*/
|
||||
export interface ApiPermission {
|
||||
id: number;
|
||||
permission_key: string; // 权限标识,如 "evaluation_group:create:write"
|
||||
display_name: string; // 显示名称,如 "创建评查点分组"
|
||||
api_method: string; // HTTP方法:GET | POST | PUT | DELETE
|
||||
api_path: string; // API路径,如 "/api/v3/evaluation-point-groups"
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由信息
|
||||
*/
|
||||
@@ -74,6 +86,7 @@ export interface RouteInfo {
|
||||
is_hidden: boolean;
|
||||
is_cache: boolean;
|
||||
status: number;
|
||||
permissions?: ApiPermission[]; // v3.0新增:关联的API权限列表
|
||||
children?: RouteInfo[];
|
||||
}
|
||||
|
||||
@@ -620,6 +633,162 @@ export async function getRoleRoutePermissions(roleId: number): Promise<RoleRoute
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色的路由权限(含API权限)- v3.0新增
|
||||
* 返回树形结构的路由,每个路由包含关联的API权限
|
||||
* @param roleId 角色ID
|
||||
*/
|
||||
export async function getRoleRoutesWithPermissions(roleId: number): Promise<{
|
||||
routes: RouteInfo[];
|
||||
selectedRouteIds: number[];
|
||||
selectedPermissionIds: number[];
|
||||
}> {
|
||||
try {
|
||||
const { get } = await import('~/api/axios-client');
|
||||
|
||||
console.log('🔍 [getRoleRoutesWithPermissions] 开始调用后端API:', `/rbac/roles/${roleId}/routes`);
|
||||
|
||||
const response = await get<any>(`/rbac/roles/${roleId}/routes`);
|
||||
console.log('📦 [getRoleRoutesWithPermissions] 后端API完整响应:', JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
// 后端响应格式: { code: 200, msg: "success", data: { role_id, routes: [...] } }
|
||||
let routes: any[] = [];
|
||||
if (response.data && response.data.data && Array.isArray(response.data.data.routes)) {
|
||||
routes = response.data.data.routes;
|
||||
} else if (response.data && Array.isArray(response.data.routes)) {
|
||||
routes = response.data.routes;
|
||||
}
|
||||
|
||||
// 递归转换路由数据格式
|
||||
const mapRouteData = (route: any): RouteInfo => ({
|
||||
id: route.id,
|
||||
route_path: route.route_path,
|
||||
route_name: route.route_name,
|
||||
route_title: route.route_title,
|
||||
icon: route.icon || '',
|
||||
sort_order: route.sort_order || 0,
|
||||
is_hidden: route.is_hidden || false,
|
||||
is_cache: route.is_cache !== false,
|
||||
status: route.status || 1,
|
||||
parent_id: route.parent_id || null,
|
||||
component: route.component,
|
||||
// v3.0: 转换permissions数组
|
||||
permissions: Array.isArray(route.permissions) ? route.permissions.map((p: any) => ({
|
||||
id: p.id,
|
||||
permission_key: p.permission_key,
|
||||
display_name: p.display_name,
|
||||
api_method: p.api_method,
|
||||
api_path: p.api_path
|
||||
})) : [],
|
||||
children: route.children ? route.children.map(mapRouteData) : undefined
|
||||
});
|
||||
|
||||
const mappedRoutes = routes.map(mapRouteData);
|
||||
|
||||
// 收集所有已选中的路由ID
|
||||
const collectRouteIds = (routes: RouteInfo[]): number[] => {
|
||||
let ids: number[] = [];
|
||||
routes.forEach(route => {
|
||||
ids.push(route.id);
|
||||
if (route.children) {
|
||||
ids = ids.concat(collectRouteIds(route.children));
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
};
|
||||
|
||||
// 收集所有已选中的权限ID(从当前角色的权限中)
|
||||
const collectPermissionIds = (routes: RouteInfo[]): number[] => {
|
||||
let ids: number[] = [];
|
||||
routes.forEach(route => {
|
||||
if (route.permissions) {
|
||||
ids = ids.concat(route.permissions.map(p => p.id));
|
||||
}
|
||||
if (route.children) {
|
||||
ids = ids.concat(collectPermissionIds(route.children));
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
};
|
||||
|
||||
const selectedRouteIds = collectRouteIds(mappedRoutes);
|
||||
const selectedPermissionIds = collectPermissionIds(mappedRoutes);
|
||||
|
||||
console.log('✅ [getRoleRoutesWithPermissions] 成功获取路由权限数据');
|
||||
console.log(' - 路由数量:', mappedRoutes.length);
|
||||
console.log(' - 已选路由ID:', selectedRouteIds);
|
||||
console.log(' - 已选权限ID:', selectedPermissionIds);
|
||||
|
||||
return {
|
||||
routes: mappedRoutes,
|
||||
selectedRouteIds,
|
||||
selectedPermissionIds
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ [getRoleRoutesWithPermissions] 获取角色路由权限失败:', error);
|
||||
return {
|
||||
routes: [],
|
||||
selectedRouteIds: [],
|
||||
selectedPermissionIds: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存角色的API权限 - v3.0新增
|
||||
* @param roleId 角色ID
|
||||
* @param permissionIds 权限ID数组
|
||||
*/
|
||||
export async function saveRoleApiPermissions(
|
||||
roleId: number,
|
||||
permissionIds: number[]
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const { post } = await import('~/api/axios-client');
|
||||
|
||||
console.log('🔍 [saveRoleApiPermissions] 开始调用后端API:', `/api/v3/rbac/roles/${roleId}/permissions`, permissionIds);
|
||||
|
||||
// 构建权限配置
|
||||
const permissions = permissionIds.map(id => ({
|
||||
permission_id: id,
|
||||
grant_type: 'GRANT',
|
||||
data_scope: 'ALL'
|
||||
}));
|
||||
|
||||
const response = await post<any>(`/api/v3/rbac/roles/${roleId}/permissions`, {
|
||||
permissions,
|
||||
replace: true // 替换模式:先删除现有权限,再插入新权限
|
||||
});
|
||||
|
||||
console.log('📦 [saveRoleApiPermissions] 后端API完整响应:', JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
let message = 'API权限保存成功';
|
||||
if (response.data && response.data.message) {
|
||||
message = response.data.message;
|
||||
} else if (response.data && response.data.data) {
|
||||
const { assigned_count } = response.data.data;
|
||||
message = `成功分配 ${assigned_count} 个API权限`;
|
||||
}
|
||||
|
||||
console.log('✅ [saveRoleApiPermissions] API权限保存成功');
|
||||
return { success: true, message };
|
||||
} catch (error) {
|
||||
console.error('❌ [saveRoleApiPermissions] 保存API权限失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '保存API权限失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色的路由权限
|
||||
* @param roleId 角色ID
|
||||
@@ -1145,24 +1314,46 @@ export async function deletePermission(
|
||||
// ==================== 角色权限关联 API ====================
|
||||
|
||||
/**
|
||||
* 获取角色的所有权限
|
||||
* 获取角色的所有权限(已分配的API权限)
|
||||
* @param roleId 角色ID
|
||||
*/
|
||||
export async function getRolePermissions(roleId: number): Promise<RolePermissionDetail[]> {
|
||||
try {
|
||||
const response = await get<any>(`${RBAC_API_BASE}/roles/${roleId}/permissions`);
|
||||
const data = handleApiResponse<{ permissions: any[] }>(response);
|
||||
const { get } = await import('~/api/axios-client');
|
||||
|
||||
return data.permissions.map(perm => ({
|
||||
console.log('🔍 [getRolePermissions] 开始调用后端API:', `/api/v3/rbac/roles/${roleId}/permissions`);
|
||||
|
||||
const response = await get<any>(`/api/v3/rbac/roles/${roleId}/permissions`);
|
||||
console.log('📦 [getRolePermissions] 后端API完整响应:', JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
let permissions: any[] = [];
|
||||
if (response.data?.data?.permissions) {
|
||||
permissions = response.data.data.permissions;
|
||||
} else if (response.data?.permissions) {
|
||||
permissions = response.data.permissions;
|
||||
} else if (Array.isArray(response.data?.data)) {
|
||||
permissions = response.data.data;
|
||||
} else if (Array.isArray(response.data)) {
|
||||
permissions = response.data;
|
||||
}
|
||||
|
||||
console.log('✅ [getRolePermissions] 解析出的权限数组:', permissions);
|
||||
|
||||
return permissions.map(perm => ({
|
||||
id: perm.id,
|
||||
permission_id: perm.permission_id,
|
||||
permission_id: perm.permission_id || perm.id, // 兼容:如果没有 permission_id,使用 id
|
||||
permission_key: perm.permission_key,
|
||||
display_name: perm.display_name,
|
||||
grant_type: perm.grant_type || 'GRANT',
|
||||
data_scope: perm.data_scope || 'ALL'
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取角色权限失败:', error);
|
||||
console.error('❌ [getRolePermissions] 获取角色权限失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* RBAC API 代理 - 单个角色操作
|
||||
* GET /api/v3/rbac/roles/:roleId - 获取角色详情
|
||||
* PUT /api/v3/rbac/roles/:roleId - 更新角色
|
||||
* DELETE /api/v3/rbac/roles/:roleId - 删除角色
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { mockRoles, updateRole as updateMockRole, deleteRole as deleteMockRole } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// GET - 获取角色详情
|
||||
export async function loader({ params }: LoaderFunctionArgs) {
|
||||
const roleId = parseInt(params.roleId || '0');
|
||||
console.log('📡 [API Route] GET /api/v3/rbac/roles/' + roleId);
|
||||
|
||||
const role = mockRoles.find(r => r.id === roleId);
|
||||
|
||||
if (!role) {
|
||||
return json({
|
||||
detail: '角色不存在'
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: role
|
||||
});
|
||||
}
|
||||
|
||||
// PUT/DELETE
|
||||
export async function action({ request, params }: LoaderFunctionArgs) {
|
||||
const roleId = parseInt(params.roleId || '0');
|
||||
const method = request.method;
|
||||
|
||||
console.log('📡 [API Route]', method, '/api/v3/rbac/roles/' + roleId);
|
||||
|
||||
const role = mockRoles.find(r => r.id === roleId);
|
||||
|
||||
if (!role) {
|
||||
return json({
|
||||
detail: '角色不存在'
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
if (method === 'PUT') {
|
||||
// 更新角色
|
||||
const body = await request.json();
|
||||
console.log('📋 [API Route] 更新数据:', body);
|
||||
|
||||
// 系统角色保护
|
||||
if (role.is_system && body.role_key) {
|
||||
return json({
|
||||
detail: '系统角色的role_key不可修改'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 使用共享Mock数据更新
|
||||
updateMockRole(roleId, body);
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '角色更新成功',
|
||||
data: role
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'DELETE') {
|
||||
// 删除角色
|
||||
if (role.is_system) {
|
||||
return json({
|
||||
detail: '系统角色不能删除'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 使用共享Mock数据删除
|
||||
deleteMockRole(roleId);
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '角色删除成功'
|
||||
});
|
||||
}
|
||||
|
||||
return json({ code: 405, message: 'Method Not Allowed' }, { status: 405 });
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* RBAC API 代理 - 角色用户管理
|
||||
* GET /api/v3/rbac/roles/:roleId/users - 获取角色的用户列表
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { mockUsers, mockUserRoles, getRoleUsers } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// GET - 获取角色的用户列表
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const roleId = parseInt(params.roleId || '0');
|
||||
console.log('📡 [API Route] GET /api/v3/rbac/roles/' + roleId + '/users');
|
||||
|
||||
// 解析查询参数
|
||||
const url = new URL(request.url);
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '20');
|
||||
const area = url.searchParams.get('area');
|
||||
const username = url.searchParams.get('username');
|
||||
|
||||
// 使用共享Mock数据获取角色用户
|
||||
let users = getRoleUsers(roleId);
|
||||
|
||||
// 过滤
|
||||
if (area) {
|
||||
users = users.filter(u => u.area.includes(area));
|
||||
}
|
||||
|
||||
if (username) {
|
||||
users = users.filter(u =>
|
||||
u.username.includes(username) || u.nick_name.includes(username)
|
||||
);
|
||||
}
|
||||
|
||||
// 添加assigned_at时间戳
|
||||
const usersWithTime = users.map(user => {
|
||||
const userRole = mockUserRoles.find(
|
||||
ur => ur.user_id === (user.user_id || user.id) && ur.role_id === roleId
|
||||
);
|
||||
return {
|
||||
...user,
|
||||
user_id: user.user_id || user.id,
|
||||
assigned_at: userRole?.assigned_at || new Date().toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
// 分页
|
||||
const total = usersWithTime.length;
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const items = usersWithTime.slice(start, end);
|
||||
|
||||
console.log('✅ [API Route] 返回用户数据:', { roleId, total, itemsCount: items.length });
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
total,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
items
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* RBAC API 代理 - 获取角色列表
|
||||
* GET /api/v3/rbac/roles
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { mockRoles, mockUserRoles, addRole } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// GET - 获取角色列表
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
console.log('📡 [API Route] GET /api/v3/rbac/roles 被调用');
|
||||
|
||||
// 解析查询参数
|
||||
const url = new URL(request.url);
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '20');
|
||||
const roleKey = url.searchParams.get('role_key');
|
||||
const roleName = url.searchParams.get('role_name');
|
||||
|
||||
console.log('📋 [API Route] 查询参数:', { page, pageSize, roleKey, roleName });
|
||||
|
||||
// 过滤数据
|
||||
let filteredRoles = [...mockRoles];
|
||||
|
||||
if (roleKey) {
|
||||
filteredRoles = filteredRoles.filter(r => r.role_key.includes(roleKey));
|
||||
}
|
||||
|
||||
if (roleName) {
|
||||
filteredRoles = filteredRoles.filter(r => r.role_name.includes(roleName));
|
||||
}
|
||||
|
||||
// 添加用户数统计
|
||||
const rolesWithCount = filteredRoles.map(role => ({
|
||||
...role,
|
||||
user_count: mockUserRoles.filter(ur => ur.role_id === role.id).length,
|
||||
permission_count: 0 // 暂时写死
|
||||
}));
|
||||
|
||||
// 分页
|
||||
const total = rolesWithCount.length;
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const items = rolesWithCount.slice(start, end);
|
||||
|
||||
// 返回标准格式
|
||||
const response = {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
total,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
items
|
||||
}
|
||||
};
|
||||
|
||||
console.log('✅ [API Route] 返回数据:', { total, itemsCount: items.length });
|
||||
|
||||
return json(response, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// POST - 创建角色
|
||||
export async function action({ request }: LoaderFunctionArgs) {
|
||||
const method = request.method;
|
||||
|
||||
if (method === 'POST') {
|
||||
console.log('📡 [API Route] POST /api/v3/rbac/roles 被调用');
|
||||
|
||||
const body = await request.json();
|
||||
console.log('📋 [API Route] 请求体:', body);
|
||||
|
||||
// 使用共享Mock数据
|
||||
const newRole = addRole({
|
||||
role_key: body.role_key,
|
||||
role_name: body.role_name,
|
||||
description: body.description || '',
|
||||
data_scope: body.data_scope || 'SELF',
|
||||
priority: body.priority || 10,
|
||||
is_system: false
|
||||
});
|
||||
|
||||
console.log('✅ [API Route] 角色创建成功:', newRole);
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '角色创建成功',
|
||||
data: newRole
|
||||
});
|
||||
}
|
||||
|
||||
return json({ code: 405, message: 'Method Not Allowed' }, { status: 405 });
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* RBAC API 代理 - 移除用户角色
|
||||
* DELETE /api/v3/rbac/users/:userId/roles/:roleId
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { removeUserRole } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// DELETE - 移除用户角色
|
||||
export async function action({ params }: LoaderFunctionArgs) {
|
||||
const userId = parseInt(params.userId || '0');
|
||||
const roleId = parseInt(params.roleId || '0');
|
||||
|
||||
console.log('📡 [API Route] DELETE /api/v3/rbac/users/' + userId + '/roles/' + roleId);
|
||||
|
||||
// 使用共享Mock数据移除角色
|
||||
const success = removeUserRole(userId, roleId);
|
||||
|
||||
if (success) {
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '用户角色移除成功'
|
||||
});
|
||||
}
|
||||
|
||||
return json({
|
||||
detail: '用户角色关联不存在'
|
||||
}, { status: 404 });
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* RBAC API 代理 - 用户角色管理
|
||||
* POST /api/v3/rbac/users/:userId/roles - 为用户分配角色
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { assignUserRole } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// POST - 为用户分配角色
|
||||
export async function action({ request, params }: LoaderFunctionArgs) {
|
||||
const userId = parseInt(params.userId || '0');
|
||||
const method = request.method;
|
||||
|
||||
console.log('📡 [API Route]', method, '/api/v3/rbac/users/' + userId + '/roles');
|
||||
|
||||
if (method === 'POST') {
|
||||
const body = await request.json();
|
||||
const roleIds = body.role_ids || [];
|
||||
|
||||
console.log('📋 [API Route] 分配角色:', { userId, roleIds });
|
||||
|
||||
// 使用共享Mock数据分配角色
|
||||
roleIds.forEach((roleId: number) => {
|
||||
assignUserRole(userId, roleId);
|
||||
});
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '角色分配成功',
|
||||
data: {
|
||||
user_id: userId,
|
||||
roles: roleIds.map((id: number) => ({ role_id: id }))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return json({ code: 405, message: 'Method Not Allowed' }, { status: 405 });
|
||||
}
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
getRoutes,
|
||||
getRoleRoutePermissions,
|
||||
updateRoleRoutePermissions,
|
||||
getRoleRoutesWithPermissions,
|
||||
saveRoleApiPermissions,
|
||||
getRolePermissions,
|
||||
getRoleUsers,
|
||||
getAllUsers,
|
||||
assignUserRoles,
|
||||
@@ -19,7 +22,8 @@ import {
|
||||
getUserRoles,
|
||||
type RoleInfo,
|
||||
type RouteInfo,
|
||||
type UserInfo
|
||||
type UserInfo,
|
||||
type ApiPermission
|
||||
} from "~/api/role-permissions/role-permissions";
|
||||
import rolePermissionsStyles from "~/styles/pages/role-permissions.css?url";
|
||||
|
||||
@@ -800,6 +804,12 @@ export default function RolePermissions() {
|
||||
const [selectedRouteIds, setSelectedRouteIds] = useState<number[]>([]);
|
||||
const [roleUsers, setRoleUsers] = useState<UserInfo[]>([]);
|
||||
|
||||
// v3.0: API权限相关状态
|
||||
const [selectedPermissionIds, setSelectedPermissionIds] = useState<number[]>([]);
|
||||
const [expandedRouteIds, setExpandedRouteIds] = useState<number[]>([]);
|
||||
// 存储每个路由的 permissions(routeId -> permissions[])
|
||||
const [routePermissionsMap, setRoutePermissionsMap] = useState<Map<number, ApiPermission[]>>(new Map());
|
||||
|
||||
// 加载初始数据
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -845,13 +855,41 @@ export default function RolePermissions() {
|
||||
const handleSelectRole = async (role: RoleInfo) => {
|
||||
setSelectedRole(role);
|
||||
|
||||
// 加载该角色的权限
|
||||
const permissions = await getRoleRoutePermissions(role.id);
|
||||
const routeIds = permissions.map(p => p.route_id);
|
||||
setSelectedRouteIds(routeIds);
|
||||
// v3.0: 并行加载数据
|
||||
const [routesResult, rolePermissions, users] = await Promise.all([
|
||||
getRoleRoutesWithPermissions(role.id),
|
||||
getRolePermissions(role.id), // 获取该角色已分配的权限
|
||||
getRoleUsers(role.id)
|
||||
]);
|
||||
|
||||
// 加载该角色的用户列表
|
||||
const users = await getRoleUsers(role.id);
|
||||
const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult;
|
||||
|
||||
// 构建 routePermissionsMap:从返回的路由中提取每个路由的可用 permissions
|
||||
const permMap = new Map<number, ApiPermission[]>();
|
||||
const extractPermissions = (routes: RouteInfo[]) => {
|
||||
routes.forEach(route => {
|
||||
if (route.permissions && route.permissions.length > 0) {
|
||||
permMap.set(route.id, route.permissions);
|
||||
}
|
||||
if (route.children) {
|
||||
extractPermissions(route.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
extractPermissions(routesWithPerms);
|
||||
|
||||
// 从 getRolePermissions 结果中提取已分配的权限ID
|
||||
const assignedPermissionIds = rolePermissions.map(p => p.permission_id);
|
||||
|
||||
console.log('🔍 [handleSelectRole] 角色权限数据:');
|
||||
console.log(' - routePermissionsMap:', permMap);
|
||||
console.log(' - rolePermissions:', rolePermissions);
|
||||
console.log(' - assignedPermissionIds:', assignedPermissionIds);
|
||||
|
||||
setRoutePermissionsMap(permMap);
|
||||
setSelectedRouteIds(routeIds);
|
||||
setSelectedPermissionIds(assignedPermissionIds); // 使用实际已分配的权限ID
|
||||
setExpandedRouteIds([]); // 重置展开状态
|
||||
setRoleUsers(users);
|
||||
};
|
||||
|
||||
@@ -893,6 +931,50 @@ export default function RolePermissions() {
|
||||
}
|
||||
};
|
||||
|
||||
// v3.0: 切换路由展开状态(显示/隐藏权限列表)
|
||||
const handleToggleRouteExpand = (routeId: number) => {
|
||||
setExpandedRouteIds(prev =>
|
||||
prev.includes(routeId)
|
||||
? prev.filter(id => id !== routeId)
|
||||
: [...prev, routeId]
|
||||
);
|
||||
};
|
||||
|
||||
// v3.0: 判断是否是"所有权限"项(用于过滤)
|
||||
const isAllPermission = (permission: ApiPermission): boolean => {
|
||||
const key = permission.permission_key?.toLowerCase() || '';
|
||||
const name = permission.display_name || '';
|
||||
return key.includes(':all:') || key.includes(':*:') ||
|
||||
key.endsWith(':all') || key.endsWith(':*') ||
|
||||
name.includes('所有权限') || name.includes('全部权限');
|
||||
};
|
||||
|
||||
// v3.0: 过滤掉"所有权限"项
|
||||
const filterPermissions = (permissions: ApiPermission[]): ApiPermission[] => {
|
||||
return permissions.filter(p => !isAllPermission(p));
|
||||
};
|
||||
|
||||
// v3.0: 切换单个API权限
|
||||
const handleTogglePermission = (permissionId: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedPermissionIds([...selectedPermissionIds, permissionId]);
|
||||
} else {
|
||||
setSelectedPermissionIds(selectedPermissionIds.filter(id => id !== permissionId));
|
||||
}
|
||||
};
|
||||
|
||||
// v3.0: 获取HTTP方法对应的标签样式
|
||||
const getMethodTagStyle = (method: string): React.CSSProperties => {
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
'GET': { backgroundColor: '#e6f7ed', color: '#52c41a', border: '1px solid #b7eb8f' },
|
||||
'POST': { backgroundColor: '#e6f0ff', color: '#1890ff', border: '1px solid #91caff' },
|
||||
'PUT': { backgroundColor: '#fff7e6', color: '#faad14', border: '1px solid #ffd591' },
|
||||
'DELETE': { backgroundColor: '#fff1f0', color: '#f5222d', border: '1px solid #ffa39e' },
|
||||
'PATCH': { backgroundColor: '#f0f5ff', color: '#722ed1', border: '1px solid #d3adf7' }
|
||||
};
|
||||
return styles[method.toUpperCase()] || { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #d9d9d9' };
|
||||
};
|
||||
|
||||
// 编辑角色
|
||||
const handleEditRole = (role: RoleInfo) => {
|
||||
setRoleToEdit(role);
|
||||
@@ -993,18 +1075,34 @@ export default function RolePermissions() {
|
||||
}
|
||||
};
|
||||
|
||||
// 保存权限
|
||||
// 保存权限 - v3.0: 同时保存路由权限和API权限
|
||||
const handleSavePermissions = async () => {
|
||||
if (!selectedRole) return;
|
||||
|
||||
try {
|
||||
// 直接调用API函数而不是发送POST请求
|
||||
const result = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
|
||||
// 1. 保存路由权限
|
||||
const routeResult = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
|
||||
|
||||
if (result.success) {
|
||||
toastService.success(result.message);
|
||||
if (!routeResult.success) {
|
||||
toastService.error(routeResult.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 保存API权限(如果有选中的权限)
|
||||
if (selectedPermissionIds.length > 0) {
|
||||
const permResult = await saveRoleApiPermissions(selectedRole.id, selectedPermissionIds);
|
||||
|
||||
if (!permResult.success) {
|
||||
toastService.error(permResult.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.success(`路由权限保存成功,${permResult.message}`);
|
||||
} else {
|
||||
toastService.error(result.message);
|
||||
// 没有选中API权限时,清空该角色的所有API权限
|
||||
const permResult = await saveRoleApiPermissions(selectedRole.id, []);
|
||||
|
||||
toastService.success(routeResult.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存权限失败:", error);
|
||||
@@ -1012,11 +1110,16 @@ export default function RolePermissions() {
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染路由树
|
||||
const renderRouteTree = (routes: RouteInfo[], level = 0) => {
|
||||
return routes.map(route => {
|
||||
// 渲染路由树 - v3.0: 支持展开显示API权限
|
||||
const renderRouteTree = (routeList: RouteInfo[], level = 0) => {
|
||||
return routeList.map(route => {
|
||||
const hasChildren = route.children && route.children.length > 0;
|
||||
// v3.0: 从 routePermissionsMap 获取该路由的 permissions,并过滤掉"所有权限"
|
||||
const rawPermissions = routePermissionsMap.get(route.id) || [];
|
||||
const permissions = filterPermissions(rawPermissions);
|
||||
const hasPermissions = permissions.length > 0;
|
||||
const isChecked = selectedRouteIds.includes(route.id);
|
||||
const isExpanded = expandedRouteIds.includes(route.id);
|
||||
const allChildIds = hasChildren ? getAllRouteIds(route.children!) : [];
|
||||
const checkedChildCount = allChildIds.filter(id =>
|
||||
selectedRouteIds.includes(id)
|
||||
@@ -1024,6 +1127,12 @@ export default function RolePermissions() {
|
||||
const isIndeterminate =
|
||||
hasChildren && checkedChildCount > 0 && checkedChildCount < allChildIds.length;
|
||||
|
||||
// 计算该路由下已选中的权限数量(使用过滤后的权限)
|
||||
const routePermissionIds = permissions.map(p => p.id);
|
||||
const selectedPermCount = routePermissionIds.filter(id =>
|
||||
selectedPermissionIds.includes(id)
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div key={route.id} className="route-item" style={{ paddingLeft: `${level * 20}px` }}>
|
||||
<div className="route-item-content">
|
||||
@@ -1048,8 +1157,91 @@ export default function RolePermissions() {
|
||||
<span className="route-title">{route.route_title}</span>
|
||||
<span className="route-path">{route.route_path}</span>
|
||||
</label>
|
||||
|
||||
{/* v3.0: 显示权限展开按钮 */}
|
||||
{hasPermissions && (
|
||||
<button
|
||||
type="button"
|
||||
className="permission-expand-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleRouteExpand(route.id);
|
||||
}}
|
||||
style={{
|
||||
marginLeft: '8px',
|
||||
padding: '2px 8px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: selectedPermCount > 0 ? '#e6f7ed' : '#f5f5f5',
|
||||
color: selectedPermCount > 0 ? '#52c41a' : '#666',
|
||||
border: selectedPermCount > 0 ? '1px solid #b7eb8f' : '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
>
|
||||
<i className={`ri-${isExpanded ? 'arrow-up-s' : 'arrow-down-s'}-line`}></i>
|
||||
<span>API权限 ({selectedPermCount}/{permissions.length})</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* v3.0: 展开的API权限列表(过滤掉"所有权限"项) */}
|
||||
{hasPermissions && isExpanded && (
|
||||
<div
|
||||
className="permissions-list"
|
||||
style={{
|
||||
marginTop: '8px',
|
||||
marginLeft: '24px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
>
|
||||
{permissions.map(permission => (
|
||||
<label
|
||||
key={permission.id}
|
||||
className="permission-item"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '6px 0',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPermissionIds.includes(permission.id)}
|
||||
onChange={(e) => handleTogglePermission(permission.id, e.target.checked)}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
...getMethodTagStyle(permission.api_method),
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
minWidth: '50px',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{permission.api_method}
|
||||
</span>
|
||||
<span style={{ color: '#333', fontSize: '13px' }}>
|
||||
{permission.display_name}
|
||||
</span>
|
||||
<span style={{ color: '#999', fontSize: '11px', marginLeft: 'auto' }}>
|
||||
{permission.api_path}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasChildren && (
|
||||
<div className="route-children">
|
||||
{renderRouteTree(route.children!, level + 1)}
|
||||
@@ -1222,13 +1414,20 @@ export default function RolePermissions() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* v3.0: 始终使用 routes 渲染所有可用路由,permissions 从 routePermissionsMap 获取 */}
|
||||
<div className="routes-tree">
|
||||
{renderRouteTree(routes)}
|
||||
</div>
|
||||
|
||||
{/* v3.0: 更新权限统计,显示路由和API权限数量 */}
|
||||
<div className="permissions-summary">
|
||||
<i className="ri-information-line"></i>
|
||||
已选择 <strong>{selectedRouteIds.length}</strong> 个路由权限
|
||||
{selectedPermissionIds.length > 0 && (
|
||||
<>
|
||||
,<strong>{selectedPermissionIds.length}</strong> 个API权限
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -314,10 +314,14 @@ export default function RuleGroupsIndex() {
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const result = await batchDeleteEvaluationPointGroups(selectedIds, frontendJWT);
|
||||
toastService.success(`成功删除 ${result.deleted_count} 个分组`);
|
||||
if (result.failed_ids.length > 0) {
|
||||
toastService.warning(`有 ${result.failed_ids.length} 个分组删除失败`);
|
||||
|
||||
// 检查返回状态
|
||||
if (!result.success) {
|
||||
toastService.error(result.error || '删除失败');
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.success(`成功删除 ${result.deleted_groups || 0} 个分组`);
|
||||
// 刷新页面以重新加载数据
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
/**
|
||||
* RBAC Mock数据 - 共享存储
|
||||
* 所有Remix API路由共享这个数据源
|
||||
*/
|
||||
|
||||
// ==================== 角色数据(与数据库实际数据一致)====================
|
||||
|
||||
export const mockRoles = [
|
||||
{
|
||||
id: 1,
|
||||
role_key: 'admin',
|
||||
role_name: '市级管理员',
|
||||
description: '负责本地区的所有业务管理,不包括系统设置和角色权限管理',
|
||||
data_scope: 'DEPT',
|
||||
is_system: false,
|
||||
priority: 0,
|
||||
created_at: '2025-07-18T10:35:39+08:00',
|
||||
updated_at: '2025-07-18T10:35:39+08:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
role_key: 'common',
|
||||
role_name: '普通员工',
|
||||
description: '仅能操作自己的数据',
|
||||
data_scope: 'SELF',
|
||||
is_system: false,
|
||||
priority: 0,
|
||||
created_at: '2025-07-18T10:35:39+08:00',
|
||||
updated_at: '2025-07-18T10:35:39+08:00'
|
||||
},
|
||||
{
|
||||
id: 52,
|
||||
role_key: 'provincial_admin',
|
||||
role_name: '省级管理员',
|
||||
description: '拥有全部权限,可以管理所有地区的评查点规则、提示词、动态按钮、评查组',
|
||||
data_scope: 'ALL',
|
||||
is_system: true,
|
||||
priority: 1,
|
||||
created_at: '2025-11-19T17:25:45+08:00',
|
||||
updated_at: '2025-11-19T17:25:45+08:00'
|
||||
}
|
||||
];
|
||||
|
||||
// ==================== 用户数据 ====================
|
||||
|
||||
export const mockUsers = [
|
||||
{
|
||||
id: 1,
|
||||
user_id: 1,
|
||||
username: 'admin',
|
||||
nick_name: '系统管理员',
|
||||
phone_number: '13800138000',
|
||||
email: 'admin@example.com',
|
||||
ou_name: '广东省烟草专卖局',
|
||||
area: '广东省',
|
||||
status: 1,
|
||||
is_leader: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user_id: 2,
|
||||
username: 'zhangsan',
|
||||
nick_name: '张三',
|
||||
phone_number: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
ou_name: '梅州市烟草专卖局',
|
||||
area: '梅州',
|
||||
status: 1,
|
||||
is_leader: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
user_id: 3,
|
||||
username: 'lisi',
|
||||
nick_name: '李四',
|
||||
phone_number: '13800138002',
|
||||
email: 'lisi@example.com',
|
||||
ou_name: '云浮市烟草专卖局',
|
||||
area: '云浮',
|
||||
status: 1,
|
||||
is_leader: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
user_id: 4,
|
||||
username: 'wangwu',
|
||||
nick_name: '王五',
|
||||
phone_number: '13800138003',
|
||||
email: 'wangwu@example.com',
|
||||
ou_name: '揭阳市烟草专卖局',
|
||||
area: '揭阳',
|
||||
status: 1,
|
||||
is_leader: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
user_id: 5,
|
||||
username: 'zhaoliu',
|
||||
nick_name: '赵六',
|
||||
phone_number: '13800138004',
|
||||
email: 'zhaoliu@example.com',
|
||||
ou_name: '潮州市烟草专卖局',
|
||||
area: '潮州',
|
||||
status: 1,
|
||||
is_leader: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
user_id: 6,
|
||||
username: 'sunqi',
|
||||
nick_name: '孙七',
|
||||
phone_number: '13800138005',
|
||||
email: 'sunqi@example.com',
|
||||
ou_name: '汕头市烟草专卖局',
|
||||
area: '汕头',
|
||||
status: 1,
|
||||
is_leader: true
|
||||
}
|
||||
];
|
||||
|
||||
// ==================== 用户-角色关联数据 ====================
|
||||
|
||||
export const mockUserRoles: Array<{ user_id: number; role_id: number; assigned_at: string }> = [
|
||||
{ user_id: 1, role_id: 52, assigned_at: '2025-01-20T10:00:00' }, // admin - provincial_admin (id=52)
|
||||
{ user_id: 2, role_id: 1, assigned_at: '2025-01-21T10:00:00' }, // zhangsan - 市级管理员 (id=1)
|
||||
{ user_id: 3, role_id: 1, assigned_at: '2025-01-21T11:00:00' }, // lisi - 市级管理员 (id=1)
|
||||
{ user_id: 4, role_id: 2, assigned_at: '2025-01-21T12:00:00' }, // wangwu - 普通员工 (id=2)
|
||||
];
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
/**
|
||||
* 获取角色的用户列表
|
||||
*/
|
||||
export function getRoleUsers(roleId: number) {
|
||||
const userIds = mockUserRoles
|
||||
.filter(ur => ur.role_id === roleId)
|
||||
.map(ur => ur.user_id);
|
||||
|
||||
return mockUsers.filter(u => userIds.includes(u.id || u.user_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户分配角色
|
||||
*/
|
||||
export function assignUserRole(userId: number, roleId: number) {
|
||||
// 检查是否已存在
|
||||
const exists = mockUserRoles.some(
|
||||
ur => ur.user_id === userId && ur.role_id === roleId
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
mockUserRoles.push({
|
||||
user_id: userId,
|
||||
role_id: roleId,
|
||||
assigned_at: new Date().toISOString()
|
||||
});
|
||||
console.log('✅ [Mock Data] 用户角色分配成功:', { userId, roleId });
|
||||
console.log('📊 [Mock Data] 当前用户-角色关联:', mockUserRoles);
|
||||
} else {
|
||||
console.log('⚠️ [Mock Data] 用户角色关联已存在:', { userId, roleId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户角色
|
||||
*/
|
||||
export function removeUserRole(userId: number, roleId: number) {
|
||||
const index = mockUserRoles.findIndex(
|
||||
ur => ur.user_id === userId && ur.role_id === roleId
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
mockUserRoles.splice(index, 1);
|
||||
console.log('✅ [Mock Data] 用户角色移除成功:', { userId, roleId });
|
||||
console.log('📊 [Mock Data] 当前用户-角色关联:', mockUserRoles);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('⚠️ [Mock Data] 用户角色关联不存在:', { userId, roleId });
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新角色
|
||||
*/
|
||||
export function addRole(roleData: any) {
|
||||
const newRole = {
|
||||
id: Math.max(...mockRoles.map(r => r.id), 0) + 1,
|
||||
...roleData,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
mockRoles.push(newRole);
|
||||
console.log('✅ [Mock Data] 角色创建成功:', newRole);
|
||||
return newRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
export function updateRole(roleId: number, updates: any) {
|
||||
const role = mockRoles.find(r => r.id === roleId);
|
||||
if (role) {
|
||||
Object.assign(role, updates, {
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
console.log('✅ [Mock Data] 角色更新成功:', role);
|
||||
return role;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
export function deleteRole(roleId: number) {
|
||||
const index = mockRoles.findIndex(r => r.id === roleId);
|
||||
if (index > -1) {
|
||||
const deleted = mockRoles.splice(index, 1)[0];
|
||||
console.log('✅ [Mock Data] 角色删除成功:', deleted);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user