Merge branch 'Wren' into shiy-login

This commit is contained in:
2025-11-26 18:05:24 +08:00
11 changed files with 830 additions and 582 deletions
-86
View File
@@ -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
}
});
}
-99
View File
@@ -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 });
}
+215 -16
View File
@@ -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[]>([]);
// 存储每个路由的 permissionsrouteId -> 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>
)}
+7 -3
View File
@@ -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) {