diff --git a/src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs b/src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs index 5d636d8..62501fa 100644 --- a/src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs +++ b/src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs @@ -232,15 +232,15 @@ public abstract class BaseLoginCommandHandler : IRequestHan claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); // 获取所有角色的权限 - var permissions = new Dictionary(); - foreach (var role in roles) + var permissionCodes = new HashSet(); + if (roles.Any()) { - var rolePermissions = await _rolePermissionRepository.GetRolePermissionsWithDetailsAsync(role, cancellationToken); - foreach (var rolePermission in rolePermissions) + var allRolePermissions = await _rolePermissionRepository.GetRolePermissionsByRolesAsync(roles, cancellationToken); + foreach (var rolePermission in allRolePermissions) { - if (!permissions.ContainsKey(rolePermission.Permission.Code)) + if (rolePermission.Permission != null) { - permissions[rolePermission.Permission.Code] = true; + permissionCodes.Add(rolePermission.Permission.Code); } } } @@ -286,7 +286,7 @@ public abstract class BaseLoginCommandHandler : IRequestHan user.Email!, user.PhoneNumber, roles.ToList().AsReadOnly(), - permissions.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + permissionCodes.ToList().AsReadOnly()); // 记录成功的登录日志 loginLog = LoginLog.Create( diff --git a/src/X1.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommandHandler.cs b/src/X1.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommandHandler.cs index 18cb6f7..cbfb3a3 100644 --- a/src/X1.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommandHandler.cs +++ b/src/X1.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommandHandler.cs @@ -101,15 +101,15 @@ public sealed class RefreshTokenCommandHandler : IRequestHandler(); - foreach (var role in roles) + var permissionCodes = new HashSet(); + if (roles.Any()) { - var rolePermissions = await _rolePermissionRepository.GetRolePermissionsWithDetailsAsync(role, cancellationToken); - foreach (var rolePermission in rolePermissions) + var allRolePermissions = await _rolePermissionRepository.GetRolePermissionsByRolesAsync(roles, cancellationToken); + foreach (var rolePermission in allRolePermissions) { - if (!permissions.ContainsKey(rolePermission.Permission.Code)) + if (rolePermission.Permission != null) { - permissions[rolePermission.Permission.Code] = true; + permissionCodes.Add(rolePermission.Permission.Code); } } } @@ -134,7 +134,7 @@ public sealed class RefreshTokenCommandHandler : IRequestHandler c.Type == ClaimTypes.Email)?.Value ?? string.Empty, claims.FirstOrDefault(c => c.Type == ClaimTypes.MobilePhone)?.Value, roles.ToList().AsReadOnly(), - permissions); + permissionCodes.ToList().AsReadOnly()); _logger.LogInformation("刷新令牌成功"); return OperationResult.CreateSuccess( diff --git a/src/X1.Application/Features/Auth/Models/UserInfo.cs b/src/X1.Application/Features/Auth/Models/UserInfo.cs index b22683b..beff842 100644 --- a/src/X1.Application/Features/Auth/Models/UserInfo.cs +++ b/src/X1.Application/Features/Auth/Models/UserInfo.cs @@ -38,9 +38,9 @@ public class UserInfo public IReadOnlyList Roles { get; } /// - /// 权限字典 + /// 权限列表 /// - public IReadOnlyDictionary Permissions { get; } + public IReadOnlyList Permissions { get; } /// /// 初始化用户信息 @@ -52,7 +52,7 @@ public class UserInfo string email, string? phoneNumber, IReadOnlyList roles, - IReadOnlyDictionary permissions) + IReadOnlyList permissions) { Id = id; UserName = userName; diff --git a/src/X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommand.cs b/src/X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommand.cs index 6a7bc9a..31b3451 100644 --- a/src/X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommand.cs +++ b/src/X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommand.cs @@ -12,14 +12,10 @@ namespace X1.Application.Features.Permissions.Commands.BatchCreatePermissions; /// "permissions": [ /// { /// "name": "查看用户", -/// "code": "users.view", -/// "type": 1, -/// "level": 1, -/// "resourceType": 2, -/// "actionType": 1, +/// "navigationMenuId": "menu-id-123", /// "description": "查看用户列表", /// "isSystem": true, -/// "sortOrder": 1 +/// "isEnabled": true /// } /// ] /// } @@ -40,29 +36,9 @@ public sealed record CreatePermissionDto( string Name, /// - /// 权限代码 + /// 导航菜单ID,用于获取权限代码 /// - string Code, - - /// - /// 权限类型 - /// - int Type, - - /// - /// 权限级别 - /// - int Level, - - /// - /// 资源类型 - /// - int? ResourceType, - - /// - /// 操作类型 - /// - int? ActionType, + string NavigationMenuId, /// /// 权限描述 @@ -72,12 +48,7 @@ public sealed record CreatePermissionDto( /// /// 是否系统权限 /// - bool IsSystem, - - /// - /// 排序权重 - /// - int SortOrder, + bool IsSystem = false, /// /// 是否启用 diff --git a/src/X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommandHandler.cs b/src/X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommandHandler.cs index 1da9040..1acd744 100644 --- a/src/X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommandHandler.cs +++ b/src/X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommandHandler.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MediatR; using Microsoft.Extensions.Logging; using X1.Domain.Entities; -using X1.Domain.Entities.Enums; using X1.Domain.Repositories.Identity; using X1.Domain.Common; +using X1.Domain.Repositories.Base; namespace X1.Application.Features.Permissions.Commands.BatchCreatePermissions; @@ -17,17 +18,23 @@ namespace X1.Application.Features.Permissions.Commands.BatchCreatePermissions; public sealed class BatchCreatePermissionsCommandHandler : IRequestHandler> { private readonly IPermissionRepository _permissionRepository; + private readonly INavigationMenuRepository _navigationMenuRepository; private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; /// /// 初始化处理器 /// public BatchCreatePermissionsCommandHandler( IPermissionRepository permissionRepository, - ILogger logger) + INavigationMenuRepository navigationMenuRepository, + ILogger logger, + IUnitOfWork unitOfWork) { _permissionRepository = permissionRepository; + _navigationMenuRepository = navigationMenuRepository; _logger = logger; + _unitOfWork = unitOfWork; } /// @@ -39,73 +46,91 @@ public sealed class BatchCreatePermissionsCommandHandler : IRequestHandler p.NavigationMenuId).Distinct().ToList(); + var navigationMenus = await _navigationMenuRepository.GetByIdsAsync(navigationMenuIds, cancellationToken); + var navigationMenuDict = navigationMenus.ToDictionary(m => m.Id, m => m); + + // 性能优化:批量查询所有已存在的权限代码 + var permissionCodes = navigationMenus + .Where(m => !string.IsNullOrEmpty(m.PermissionCode)) + .Select(m => m.PermissionCode) + .Distinct() + .ToList(); + var existingPermissions = await _permissionRepository.GetByCodesAsync(permissionCodes, cancellationToken); + var existingPermissionCodes = existingPermissions.Select(p => p.Code).ToHashSet(); + var permissionIds = new List(); var successCount = 0; var failureCount = 0; + var permissionsToCreate = new List(); foreach (var permissionDto in request.Permissions) { try { - // 验证枚举参数 - if (!Enum.IsDefined(typeof(PermissionType), permissionDto.Type)) + // 从缓存中获取导航菜单信息 + if (!navigationMenuDict.TryGetValue(permissionDto.NavigationMenuId, out var navigationMenu)) { - _logger.LogWarning("无效的权限类型: {Type}", permissionDto.Type); + _logger.LogWarning("导航菜单 {NavigationMenuId} 不存在,跳过创建权限 {PermissionName}", + permissionDto.NavigationMenuId, permissionDto.Name); failureCount++; continue; } - if (!Enum.IsDefined(typeof(PermissionLevel), permissionDto.Level)) + // 检查菜单是否已有权限代码 + if (string.IsNullOrEmpty(navigationMenu.PermissionCode)) { - _logger.LogWarning("无效的权限级别: {Level}", permissionDto.Level); + _logger.LogWarning("导航菜单 {MenuTitle} 没有设置权限代码,跳过创建权限 {PermissionName}", + navigationMenu.Title, permissionDto.Name); failureCount++; continue; } - if (permissionDto.ResourceType.HasValue && !Enum.IsDefined(typeof(ResourceType), permissionDto.ResourceType.Value)) + // 从缓存中检查权限代码是否已存在 + if (existingPermissionCodes.Contains(navigationMenu.PermissionCode)) { - _logger.LogWarning("无效的资源类型: {ResourceType}", permissionDto.ResourceType.Value); + _logger.LogWarning("权限代码 {PermissionCode} 已存在,跳过创建权限 {PermissionName}", + navigationMenu.PermissionCode, permissionDto.Name); failureCount++; continue; } - if (permissionDto.ActionType.HasValue && !Enum.IsDefined(typeof(ActionType), permissionDto.ActionType.Value)) - { - _logger.LogWarning("无效的操作类型: {ActionType}", permissionDto.ActionType.Value); - failureCount++; - continue; - } - - // 检查权限是否已存在 - var existingPermission = await _permissionRepository.GetByCodeAsync(permissionDto.Code, cancellationToken); - if (existingPermission != null) - { - _logger.LogWarning("权限代码 {PermissionCode} 已存在,跳过创建", permissionDto.Code); - failureCount++; - continue; - } - - // 创建权限 + // 创建权限对象(暂不保存到数据库) var permission = Permission.Create( permissionDto.Name, - permissionDto.Code, + navigationMenu.PermissionCode, // 使用NavigationMenu的权限代码 permissionDto.Description, permissionDto.IsEnabled, permissionDto.IsSystem); - var createdPermission = await _permissionRepository.AddAsync(permission, cancellationToken); - permissionIds.Add(createdPermission.Id); + permissionsToCreate.Add(permission); successCount++; - _logger.LogInformation("权限 {PermissionName} 创建成功", permissionDto.Name); + _logger.LogInformation("权限 {PermissionName} (代码: {PermissionCode}) 准备创建,关联菜单: {MenuTitle}", + permissionDto.Name, navigationMenu.PermissionCode, navigationMenu.Title); } catch (Exception ex) { - _logger.LogError(ex, "创建权限 {PermissionName} 失败", permissionDto.Name); + _logger.LogError(ex, "准备创建权限 {PermissionName} 失败", permissionDto.Name); failureCount++; } } + // 性能优化:批量保存所有权限到数据库 + if (permissionsToCreate.Any()) + { + foreach (var permission in permissionsToCreate) + { + var createdPermission = await _permissionRepository.AddAsync(permission, cancellationToken); + permissionIds.Add(createdPermission.Id); + } + + // 一次性保存所有更改到数据库 + await _unitOfWork.SaveChangesAsync(cancellationToken); + _logger.LogInformation("批量保存 {PermissionCount} 个权限到数据库成功", permissionsToCreate.Count); + } + _logger.LogInformation("批量创建权限完成,成功 {SuccessCount} 个,失败 {FailureCount} 个", successCount, failureCount); return OperationResult.CreateSuccess( diff --git a/src/X1.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommand.cs b/src/X1.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommand.cs index 974a923..e3739c1 100644 --- a/src/X1.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommand.cs +++ b/src/X1.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommand.cs @@ -13,6 +13,11 @@ public sealed record CreatePermissionCommand( /// string Name, + /// + /// 导航菜单ID,用于获取权限代码 + /// + string NavigationMenuId, + /// /// 权限描述 /// diff --git a/src/X1.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommandHandler.cs b/src/X1.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommandHandler.cs index bc11623..f7c8ed7 100644 --- a/src/X1.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommandHandler.cs +++ b/src/X1.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommandHandler.cs @@ -18,6 +18,7 @@ namespace X1.Application.Features.Permissions.Commands.CreatePermission; public sealed class CreatePermissionCommandHandler : IRequestHandler> { private readonly IPermissionRepository _permissionRepository; + private readonly INavigationMenuRepository _navigationMenuRepository; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; @@ -26,10 +27,12 @@ public sealed class CreatePermissionCommandHandler : IRequestHandler public CreatePermissionCommandHandler( IPermissionRepository permissionRepository, + INavigationMenuRepository navigationMenuRepository, ILogger logger, IUnitOfWork unitOfWork) { _permissionRepository = permissionRepository; + _navigationMenuRepository = navigationMenuRepository; _logger = logger; _unitOfWork = unitOfWork; } @@ -43,25 +46,49 @@ public sealed class CreatePermissionCommandHandler : IRequestHandler.CreateFailure("权限已存在"); + return OperationResult.CreateFailure("权限名称已存在"); } - // 创建权限 + // 通过NavigationMenu ID获取菜单信息 + var navigationMenu = await _navigationMenuRepository.GetByIdAsync(request.NavigationMenuId, cancellationToken: cancellationToken); + if (navigationMenu == null) + { + _logger.LogWarning("导航菜单 {NavigationMenuId} 不存在", request.NavigationMenuId); + return OperationResult.CreateFailure("导航菜单不存在"); + } + + // 检查菜单是否已有权限代码 + if (string.IsNullOrEmpty(navigationMenu.PermissionCode)) + { + _logger.LogWarning("导航菜单 {MenuTitle} 没有设置权限代码", navigationMenu.Title); + return OperationResult.CreateFailure("导航菜单没有设置权限代码"); + } + + // 检查权限代码是否已存在 + var existingPermissionByCode = await _permissionRepository.GetByCodeAsync(navigationMenu.PermissionCode, cancellationToken); + if (existingPermissionByCode != null) + { + _logger.LogWarning("权限代码 {PermissionCode} 已存在", navigationMenu.PermissionCode); + return OperationResult.CreateFailure("权限代码已存在"); + } + + // 创建权限,使用NavigationMenu的权限代码 var permission = Permission.Create( request.Name, - request.Name.ToLower().Replace(" ", "."), // 生成权限代码 + navigationMenu.PermissionCode, // 使用NavigationMenu的权限代码 request.Description); var createdPermission = await _permissionRepository.AddAsync(permission, cancellationToken); // 保存更改到数据库 await _unitOfWork.SaveChangesAsync(cancellationToken); - _logger.LogInformation("权限 {PermissionName} 创建成功", request.Name); + _logger.LogInformation("权限 {PermissionName} (代码: {PermissionCode}) 创建成功,关联菜单: {MenuTitle}", + request.Name, navigationMenu.PermissionCode, navigationMenu.Title); return OperationResult.CreateSuccess( new CreatePermissionResponse(createdPermission.Id)); diff --git a/src/X1.Application/Features/Permissions/Commands/UpdatePermission/UpdatePermissionCommand.cs b/src/X1.Application/Features/Permissions/Commands/UpdatePermission/UpdatePermissionCommand.cs index a316bb0..fb301b7 100644 --- a/src/X1.Application/Features/Permissions/Commands/UpdatePermission/UpdatePermissionCommand.cs +++ b/src/X1.Application/Features/Permissions/Commands/UpdatePermission/UpdatePermissionCommand.cs @@ -11,7 +11,6 @@ namespace X1.Application.Features.Permissions.Commands.UpdatePermission; /// "id": "permission-id", /// "name": "更新后的权限名称", /// "description": "更新后的权限描述", -/// "level": 3, /// "isEnabled": true /// } /// @@ -34,12 +33,6 @@ public sealed record UpdatePermissionCommand( /// 更新后的权限描述 string? Description, - /// - /// 权限级别 - /// - /// 3 - int? Level, - /// /// 是否启用 /// diff --git a/src/X1.Application/Features/Permissions/Commands/UpdatePermission/UpdatePermissionCommandHandler.cs b/src/X1.Application/Features/Permissions/Commands/UpdatePermission/UpdatePermissionCommandHandler.cs index d400bfe..5b87aa9 100644 --- a/src/X1.Application/Features/Permissions/Commands/UpdatePermission/UpdatePermissionCommandHandler.cs +++ b/src/X1.Application/Features/Permissions/Commands/UpdatePermission/UpdatePermissionCommandHandler.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using MediatR; using Microsoft.Extensions.Logging; using X1.Domain.Entities; -using X1.Domain.Entities.Enums; using X1.Domain.Repositories.Identity; using X1.Domain.Common; @@ -46,13 +45,6 @@ public sealed class UpdatePermissionCommandHandler : IRequestHandler.CreateFailure("权限不存在"); } - // 验证Level参数 - if (request.Level.HasValue && !Enum.IsDefined(typeof(PermissionLevel), request.Level.Value)) - { - _logger.LogWarning("无效的权限级别: {Level}", request.Level.Value); - return OperationResult.CreateFailure("无效的权限级别"); - } - // 更新权限信息 permission.Name = request.Name; permission.Description = request.Description; diff --git a/src/X1.Application/Features/Permissions/Queries/GetAllPermissionsQuery.cs b/src/X1.Application/Features/Permissions/Queries/GetAllPermissionsQuery.cs index 0890b88..adf300f 100644 --- a/src/X1.Application/Features/Permissions/Queries/GetAllPermissionsQuery.cs +++ b/src/X1.Application/Features/Permissions/Queries/GetAllPermissionsQuery.cs @@ -13,10 +13,6 @@ public sealed record GetAllPermissionsQuery( int PageNumber = 1, int PageSize = 10, string? Keyword = null, - int? Type = null, - int? ResourceType = null, - int? ActionType = null, - int? Level = null, bool? IsEnabled = null, bool? IsSystem = null ) : IRequest>; @@ -44,10 +40,5 @@ public record PermissionListItemDto( string Name, string Code, string? Description, - int Type, - int Level, - int ResourceType, - int ActionType, bool IsEnabled, - bool IsSystem, - int SortOrder); + bool IsSystem); diff --git a/src/X1.Application/Features/Permissions/Queries/GetAllPermissionsQueryHandler.cs b/src/X1.Application/Features/Permissions/Queries/GetAllPermissionsQueryHandler.cs index 837253d..ee285b7 100644 --- a/src/X1.Application/Features/Permissions/Queries/GetAllPermissionsQueryHandler.cs +++ b/src/X1.Application/Features/Permissions/Queries/GetAllPermissionsQueryHandler.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using MediatR; using Microsoft.Extensions.Logging; using X1.Domain.Entities; -using X1.Domain.Entities.Enums; using X1.Domain.Repositories.Identity; using X1.Domain.Common; using System.Collections.Generic; @@ -69,34 +68,6 @@ public sealed class GetAllPermissionsQueryHandler : IRequestHandler { "获取权限列表失败" }); } } - - /// - /// 获取资源类型数值 - /// - private static int GetResourceTypeValue(string resourceType) - { - return resourceType.ToLower() switch - { - "dashboard" => 1, - "users" => 2, - "roles" => 3, - "permissions" => 4, - "settings" => 5, - "scenarios" => 6, - "testcases" => 7, - "teststeps" => 8, - "tasks" => 9, - "taskreviews" => 10, - "taskexecutions" => 11, - "functionalanalysis" => 12, - "performanceanalysis" => 13, - "issueanalysis" => 14, - "ueanalysis" => 15, - "devices" => 16, - "protocols" => 17, - "ranconfigurations" => 18, - "imsconfigurations" => 19, - "corenetworkconfigs" => 20, - "networkstackconfigs" => 21, - "deviceruntimes" => 22, - "protocollogs" => 23, - "terminalservices" => 24, - "terminaldevices" => 25, - "adboperations" => 26, - "atoperations" => 27, - _ => 0 - }; - } } diff --git a/src/X1.Application/Features/Permissions/Queries/GetPermissionTree/GetPermissionTreeQueryHandler.cs b/src/X1.Application/Features/Permissions/Queries/GetPermissionTree/GetPermissionTreeQueryHandler.cs index 536aae3..2236d95 100644 --- a/src/X1.Application/Features/Permissions/Queries/GetPermissionTree/GetPermissionTreeQueryHandler.cs +++ b/src/X1.Application/Features/Permissions/Queries/GetPermissionTree/GetPermissionTreeQueryHandler.cs @@ -54,7 +54,7 @@ public sealed class GetPermissionTreeQueryHandler : IRequestHandler p.IsEnabled) - .GroupBy(p => p.ExtractResourceType() ?? "unknown") + .GroupBy(p => ExtractResourceTypeFromCode(p.Code) ?? "unknown") .ToList(); var permissionTrees = new List(); @@ -172,4 +172,16 @@ public sealed class GetPermissionTreeQueryHandler : IRequestHandler 0 }; } + + /// + /// 从权限代码中提取资源类型 + /// + private static string? ExtractResourceTypeFromCode(string code) + { + if (string.IsNullOrEmpty(code)) + return null; + + var parts = code.Split('.'); + return parts.Length >= 1 ? parts[0] : null; + } } diff --git a/src/X1.Application/Features/RolePermissions/Commands/AddRolePermissions/AddRolePermissionsCommandHandler.cs b/src/X1.Application/Features/RolePermissions/Commands/AddRolePermissions/AddRolePermissionsCommandHandler.cs index 2e41320..6e524c9 100644 --- a/src/X1.Application/Features/RolePermissions/Commands/AddRolePermissions/AddRolePermissionsCommandHandler.cs +++ b/src/X1.Application/Features/RolePermissions/Commands/AddRolePermissions/AddRolePermissionsCommandHandler.cs @@ -6,7 +6,9 @@ using System.Threading.Tasks; using System.Threading; using System; using System.Linq; +using System.Collections.Generic; using X1.Domain.Repositories.Identity; +using X1.Domain.Repositories.Base; namespace X1.Application.Features.RolePermissions.Commands.AddRolePermissions; @@ -17,13 +19,16 @@ public class AddRolePermissionsCommandHandler : IRequestHandler _logger; + private readonly IUnitOfWork _unitOfWork; public AddRolePermissionsCommandHandler( IRolePermissionRepository rolePermissionRepository, - ILogger logger) + ILogger logger, + IUnitOfWork unitOfWork) { _rolePermissionRepository = rolePermissionRepository; _logger = logger; + _unitOfWork = unitOfWork; } public async Task> Handle( @@ -32,34 +37,94 @@ public class AddRolePermissionsCommandHandler : IRequestHandler p.Id).ToList(); + // 获取现有的 RolePermission 实体(用于检测被删除的权限) + var existingRolePermissions = await _rolePermissionRepository.GetRolePermissionsWithDetailsAsync(request.RoleId, cancellationToken); + var existingRolePermissionIds = existingRolePermissions.Select(rp => rp.PermissionId).ToList(); + // 过滤出需要新增的权限ID var newPermissionIds = request.PermissionIds.Except(existingPermissionIds).ToList(); - if (!newPermissionIds.Any()) + // 检查已存在的权限,支持二次分配 + var existingPermissionIdsToProcess = request.PermissionIds.Intersect(existingPermissionIds).ToList(); + + // 检测需要删除的权限(存在于 RolePermission 但不在当前请求中的权限) + var permissionsToRemove = existingRolePermissionIds.Except(request.PermissionIds).ToList(); + + if (!newPermissionIds.Any() && !existingPermissionIdsToProcess.Any() && !permissionsToRemove.Any()) + { + return OperationResult.CreateFailure("没有权限需要处理"); + } + + var addedPermissionIds = new List(); + var processedExistingPermissionIds = new List(); + var removedPermissionIds = new List(); + + // 删除不再需要的权限 + if (permissionsToRemove.Any()) { - return OperationResult.CreateFailure("所有权限已存在,无需添加"); + var removedCount = await _rolePermissionRepository.DeleteRolePermissionsAsync( + request.RoleId, + permissionsToRemove, + cancellationToken); + removedPermissionIds.AddRange(permissionsToRemove); + + _logger.LogInformation( + "从角色 {RoleId} 删除了 {Count} 个不再需要的权限: {PermissionIds}", + request.RoleId, + removedCount, + string.Join(", ", permissionsToRemove)); } // 添加新的角色权限 - var addedRolePermissions = await _rolePermissionRepository.AddRolePermissionsAsync( - request.RoleId, - newPermissionIds, - cancellationToken); + if (newPermissionIds.Any()) + { + var addedRolePermissions = await _rolePermissionRepository.AddRolePermissionsAsync( + request.RoleId, + newPermissionIds, + cancellationToken); + addedPermissionIds.AddRange(newPermissionIds); + + _logger.LogInformation( + "为角色 {RoleId} 新增了 {Count} 个权限: {PermissionIds}", + request.RoleId, + newPermissionIds.Count, + string.Join(", ", newPermissionIds)); + } + + // 处理已存在的权限(二次分配场景) + if (existingPermissionIdsToProcess.Any()) + { + // 直接添加到已处理列表,因为权限已经存在 + processedExistingPermissionIds.AddRange(existingPermissionIdsToProcess); + + _logger.LogInformation( + "为角色 {RoleId} 确认了 {Count} 个已存在的权限", + request.RoleId, + existingPermissionIdsToProcess.Count); + } + + // 保存更改到数据库 + await _unitOfWork.SaveChangesAsync(cancellationToken); var response = new AddRolePermissionsResponse { - AddedPermissionIds = newPermissionIds, - FailedPermissionIds = request.PermissionIds.Except(newPermissionIds) + AddedPermissionIds = addedPermissionIds, + FailedPermissionIds = request.PermissionIds.Except(addedPermissionIds).Except(processedExistingPermissionIds), + RemovedPermissionIds = removedPermissionIds }; + var totalProcessed = addedPermissionIds.Count + processedExistingPermissionIds.Count + removedPermissionIds.Count; _logger.LogInformation( - "成功为角色 {RoleId} 添加 {Count} 个权限", + "成功为角色 {RoleId} 处理权限,新增:{NewCount},二次分配:{ExistingCount},删除:{RemovedCount},总计:{TotalCount}", request.RoleId, - newPermissionIds.Count); + addedPermissionIds.Count, + processedExistingPermissionIds.Count, + removedPermissionIds.Count, + totalProcessed); return OperationResult.CreateSuccess(response); } diff --git a/src/X1.Application/Features/RolePermissions/Commands/AddRolePermissions/AddRolePermissionsResponse.cs b/src/X1.Application/Features/RolePermissions/Commands/AddRolePermissions/AddRolePermissionsResponse.cs index f90df46..66e2830 100644 --- a/src/X1.Application/Features/RolePermissions/Commands/AddRolePermissions/AddRolePermissionsResponse.cs +++ b/src/X1.Application/Features/RolePermissions/Commands/AddRolePermissions/AddRolePermissionsResponse.cs @@ -14,4 +14,24 @@ public class AddRolePermissionsResponse /// 添加失败的权限ID列表 /// public IEnumerable FailedPermissionIds { get; set; } = new List(); + + /// + /// 删除的权限ID列表(新增功能) + /// + public IEnumerable RemovedPermissionIds { get; set; } = new List(); + + /// + /// 新增权限数量 + /// + public int AddedCount => AddedPermissionIds?.Count() ?? 0; + + /// + /// 删除权限数量 + /// + public int RemovedCount => RemovedPermissionIds?.Count() ?? 0; + + /// + /// 失败权限数量 + /// + public int FailedCount => FailedPermissionIds?.Count() ?? 0; } \ No newline at end of file diff --git a/src/X1.Application/Features/RolePermissions/Commands/DeleteRolePermissions/DeleteRolePermissionsCommandHandler.cs b/src/X1.Application/Features/RolePermissions/Commands/DeleteRolePermissions/DeleteRolePermissionsCommandHandler.cs index 90577ff..984844c 100644 --- a/src/X1.Application/Features/RolePermissions/Commands/DeleteRolePermissions/DeleteRolePermissionsCommandHandler.cs +++ b/src/X1.Application/Features/RolePermissions/Commands/DeleteRolePermissions/DeleteRolePermissionsCommandHandler.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Linq; using System; using X1.Domain.Repositories.Identity; +using X1.Domain.Repositories.Base; namespace X1.Application.Features.RolePermissions.Commands.DeleteRolePermissions; @@ -17,13 +18,16 @@ public class DeleteRolePermissionsCommandHandler : IRequestHandler _logger; + private readonly IUnitOfWork _unitOfWork; public DeleteRolePermissionsCommandHandler( IRolePermissionRepository rolePermissionRepository, - ILogger logger) + ILogger logger, + IUnitOfWork unitOfWork) { _rolePermissionRepository = rolePermissionRepository; _logger = logger; + _unitOfWork = unitOfWork; } public async Task> Handle( @@ -50,6 +54,9 @@ public class DeleteRolePermissionsCommandHandler : IRequestHandler -/// 用户ID +/// 用户ID /// 用户名 /// 真实姓名 /// 电子邮箱 /// 电话号码 /// 创建时间 /// 是否激活 -/// 角色ID列表 -/// 角色名称列表 +/// 角色名称列表 +/// 权限代码列表 public sealed record UserDto( - string UserId, + string Id, string UserName, string RealName, string Email, string PhoneNumber, DateTime CreatedAt, bool IsActive, - List RoleIds, - List RoleNames); \ No newline at end of file + List Roles, + List Permissions); \ No newline at end of file diff --git a/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs b/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs index ff6c483..95342d9 100644 --- a/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs +++ b/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs @@ -23,6 +23,7 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler _userManager; private readonly RoleManager _roleManager; private readonly IUserRoleRepository _userRoleRepository; + private readonly IRolePermissionRepository _rolePermissionRepository; private readonly ILogger _logger; /// @@ -36,11 +37,13 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler userManager, RoleManager roleManager, IUserRoleRepository userRoleRepository, + IRolePermissionRepository rolePermissionRepository, ILogger logger) { _userManager = userManager; _roleManager = roleManager; _userRoleRepository = userRoleRepository; + _rolePermissionRepository = rolePermissionRepository; _logger = logger; } @@ -123,6 +126,24 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler>(); + var allRoleIds = allUserRoles.Values.SelectMany(roleIds => roleIds).Distinct().ToList(); + + if (allRoleIds.Any()) + { + var rolePermissions = await _rolePermissionRepository.GetRolePermissionsByRolesAsync(allRoleIds, cancellationToken); + + // 使用LINQ一次性构建角色权限映射,避免重复循环 + allRolePermissions = rolePermissions + .Where(rp => rp.Permission != null) + .GroupBy(rp => rp.RoleId) + .ToDictionary( + g => g.Key, + g => new HashSet(g.Select(rp => rp.Permission!.Code)) + ); + } + var userDtos = new List(); foreach (var user in users) @@ -143,6 +164,13 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler allRolePermissions.ContainsKey(roleId)) + .SelectMany(roleId => allRolePermissions[roleId]) + .Distinct() + .ToList(); + var dto = new UserDto( user.Id, user.UserName, @@ -151,8 +179,8 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler +/// 获取当前用户查询 +/// 用于获取当前登录用户详细信息的查询对象 +/// +public sealed record GetCurrentUserQuery() : IRequest>; diff --git a/src/X1.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs b/src/X1.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs new file mode 100644 index 0000000..fbc8cfb --- /dev/null +++ b/src/X1.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using MediatR; +using X1.Domain.Entities; +using X1.Domain.Repositories.Identity; +using X1.Domain.Common; +using X1.Application.Features.Users.Queries.Dtos; +using X1.Domain.Services; + +namespace X1.Application.Features.Users.Queries.GetCurrentUser; + +/// +/// 获取当前用户查询处理器 +/// 处理获取当前登录用户详细信息的查询请求 +/// +public sealed class GetCurrentUserQueryHandler : IRequestHandler> +{ + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; + private readonly IUserRoleRepository _userRoleRepository; + private readonly IRolePermissionRepository _rolePermissionRepository; + private readonly ICurrentUserService _currentUserService; + private readonly ILogger _logger; + + /// + /// 初始化查询处理器 + /// + public GetCurrentUserQueryHandler( + UserManager userManager, + RoleManager roleManager, + IUserRoleRepository userRoleRepository, + IRolePermissionRepository rolePermissionRepository, + ICurrentUserService currentUserService, + ILogger logger) + { + _userManager = userManager; + _roleManager = roleManager; + _userRoleRepository = userRoleRepository; + _rolePermissionRepository = rolePermissionRepository; + _currentUserService = currentUserService; + _logger = logger; + } + + /// + /// 处理获取当前用户查询 + /// + /// 查询请求 + /// 取消令牌 + /// 操作结果,包含当前用户信息 + public async Task> Handle( + GetCurrentUserQuery request, + CancellationToken cancellationToken) + { + try + { + // 获取当前用户ID + var currentUserId = _currentUserService.GetCurrentUserId(); + if (string.IsNullOrEmpty(currentUserId)) + { + _logger.LogWarning("无法获取当前用户ID"); + return OperationResult.CreateFailure("无法获取当前用户信息"); + } + + // 根据ID查询用户 + var user = await _userManager.FindByIdAsync(currentUserId); + if (user == null) + { + _logger.LogWarning("当前用户 {UserId} 不存在", currentUserId); + return OperationResult.CreateFailure("用户不存在"); + } + + // 获取用户的角色(使用自定义仓储) + var userRoleIds = await _userRoleRepository.GetUserRolesAsync(user.Id, cancellationToken); + + // 获取角色ID和名称 + var roleIds = new List(); + var roleNames = new List(); + + foreach (var roleId in userRoleIds) + { + var role = await _roleManager.FindByIdAsync(roleId); + if (role != null && !string.IsNullOrEmpty(role.Name)) + { + roleNames.Add(role.Name); + roleIds.Add(role.Id); + } + } + + // 获取用户的所有权限 + var permissions = new List(); + if (userRoleIds.Any()) + { + var allRolePermissions = await _rolePermissionRepository.GetRolePermissionsByRolesAsync(userRoleIds, cancellationToken); + foreach (var rolePermission in allRolePermissions) + { + if (rolePermission.Permission != null && !permissions.Contains(rolePermission.Permission.Code)) + { + permissions.Add(rolePermission.Permission.Code); + } + } + } + + var dto = new UserDto( + user.Id, + user.UserName ?? string.Empty, + user.RealName ?? string.Empty, + user.Email ?? string.Empty, + user.PhoneNumber ?? string.Empty, + user.CreatedTime, + user.IsActive, + roleNames, + permissions); + + _logger.LogInformation("获取当前用户 {UserId} 信息成功", currentUserId); + return OperationResult.CreateSuccess(dto); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取当前用户信息时发生异常"); + return OperationResult.CreateFailure("系统错误,请稍后重试"); + } + } +} diff --git a/src/X1.Application/Features/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs b/src/X1.Application/Features/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs index 4247afd..f496941 100644 --- a/src/X1.Application/Features/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs +++ b/src/X1.Application/Features/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using X1.Domain.Repositories.Identity; +using X1.Domain.Repositories.Base; namespace X1.Application.Features.Users.Queries.GetUserById; @@ -22,6 +23,7 @@ public sealed class GetUserByIdQueryHandler : IRequestHandler _userManager; private readonly RoleManager _roleManager; private readonly IUserRoleRepository _userRoleRepository; + private readonly IRolePermissionRepository _rolePermissionRepository; private readonly ILogger _logger; /// @@ -30,16 +32,19 @@ public sealed class GetUserByIdQueryHandler : IRequestHandler用户管理器,用于管理用户身份 /// 角色管理器,用于管理角色信息 /// 用户角色仓储 + /// 角色权限仓储 /// 日志记录器 public GetUserByIdQueryHandler( UserManager userManager, RoleManager roleManager, IUserRoleRepository userRoleRepository, + IRolePermissionRepository rolePermissionRepository, ILogger logger) { _userManager = userManager; _roleManager = roleManager; _userRoleRepository = userRoleRepository; + _rolePermissionRepository = rolePermissionRepository; _logger = logger; } @@ -62,6 +67,7 @@ public sealed class GetUserByIdQueryHandler : IRequestHandler.CreateFailure("用户不存在"); } + // 获取用户的角色(使用自定义仓储) var userRoleIds = await _userRoleRepository.GetUserRolesAsync(user.Id, cancellationToken); @@ -79,6 +85,20 @@ public sealed class GetUserByIdQueryHandler : IRequestHandler(); + if (userRoleIds.Any()) + { + var allRolePermissions = await _rolePermissionRepository.GetRolePermissionsByRolesAsync(userRoleIds, cancellationToken); + foreach (var rolePermission in allRolePermissions) + { + if (rolePermission.Permission != null && !permissions.Contains(rolePermission.Permission.Code)) + { + permissions.Add(rolePermission.Permission.Code); + } + } + } + var dto = new UserDto( user.Id, user.UserName, @@ -87,8 +107,8 @@ public sealed class GetUserByIdQueryHandler : IRequestHandler.CreateFailure("查询用户失败"); + _logger.LogError(ex, "查询用户 {UserId} 时发生异常", request.UserId); + return OperationResult.CreateFailure("系统错误,请稍后重试"); } } } \ No newline at end of file diff --git a/src/X1.Domain/Entities/Permission.cs b/src/X1.Domain/Entities/Permission.cs index 0110aca..5644bdb 100644 --- a/src/X1.Domain/Entities/Permission.cs +++ b/src/X1.Domain/Entities/Permission.cs @@ -16,7 +16,7 @@ public class Permission : Entity public string Name { get; set; } = string.Empty; /// - /// 权限代码(例如: "users.create") + /// 权限代码(例如: "users.view") /// [Required] [MaxLength(100)] @@ -38,30 +38,6 @@ public class Permission : Entity /// public bool IsSystem { get; set; } = false; - /// - /// 从权限代码提取资源类型 - /// - public string? ExtractResourceType() - { - if (string.IsNullOrEmpty(Code)) - return null; - - var parts = Code.Split('.'); - return parts.Length >= 1 ? parts[0] : null; - } - - /// - /// 从权限代码提取操作类型 - /// - public string? ExtractActionType() - { - if (string.IsNullOrEmpty(Code)) - return null; - - var parts = Code.Split('.'); - return parts.Length >= 2 ? parts[1] : null; - } - #region 工厂方法 /// diff --git a/src/X1.Domain/Repositories/Identity/INavigationMenuRepository.cs b/src/X1.Domain/Repositories/Identity/INavigationMenuRepository.cs index 6a0219a..04bf6cc 100644 --- a/src/X1.Domain/Repositories/Identity/INavigationMenuRepository.cs +++ b/src/X1.Domain/Repositories/Identity/INavigationMenuRepository.cs @@ -31,6 +31,11 @@ public interface INavigationMenuRepository : IBaseRepository /// Task GetByPathAsync(string path, CancellationToken cancellationToken = default); + /// + /// 根据ID列表批量获取菜单 + /// + Task> GetByIdsAsync(IEnumerable ids, CancellationToken cancellationToken = default); + #endregion #region 权限相关查询方法 diff --git a/src/X1.Domain/Repositories/Identity/IRolePermissionRepository.cs b/src/X1.Domain/Repositories/Identity/IRolePermissionRepository.cs index c1f310d..ede218a 100644 --- a/src/X1.Domain/Repositories/Identity/IRolePermissionRepository.cs +++ b/src/X1.Domain/Repositories/Identity/IRolePermissionRepository.cs @@ -41,4 +41,12 @@ public interface IRolePermissionRepository : IBaseRepository /// 取消令牌 /// 角色权限关联列表 Task> GetRolePermissionsWithDetailsAsync(string roleId, CancellationToken cancellationToken = default); + + /// + /// 批量获取多个角色的权限(包含权限详情) + /// + /// 角色ID列表 + /// 取消令牌 + /// 角色权限关联列表 + Task> GetRolePermissionsByRolesAsync(IEnumerable roleIds, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/X1.Infrastructure/Repositories/Identity/NavigationMenuRepository.cs b/src/X1.Infrastructure/Repositories/Identity/NavigationMenuRepository.cs index 384c4d2..e2a2b6b 100644 --- a/src/X1.Infrastructure/Repositories/Identity/NavigationMenuRepository.cs +++ b/src/X1.Infrastructure/Repositories/Identity/NavigationMenuRepository.cs @@ -74,6 +74,21 @@ public class NavigationMenuRepository : BaseRepository, INavigat cancellationToken: cancellationToken); } + /// + /// 根据ID列表批量获取菜单 + /// + public async Task> GetByIdsAsync(IEnumerable ids, CancellationToken cancellationToken = default) + { + if (ids == null || !ids.Any()) + { + return Enumerable.Empty(); + } + + return await QueryRepository.FindAsync( + m => ids.Contains(m.Id), + cancellationToken: cancellationToken); + } + #endregion #region 权限相关查询方法 diff --git a/src/X1.Infrastructure/Repositories/Identity/RolePermissionRepository.cs b/src/X1.Infrastructure/Repositories/Identity/RolePermissionRepository.cs index 6f8af41..b91218e 100644 --- a/src/X1.Infrastructure/Repositories/Identity/RolePermissionRepository.cs +++ b/src/X1.Infrastructure/Repositories/Identity/RolePermissionRepository.cs @@ -8,6 +8,7 @@ using X1.Infrastructure.Repositories.Base; using X1.Domain.Repositories.Base; using X1.Domain.Repositories.Identity; using System; +using Microsoft.EntityFrameworkCore; namespace X1.Infrastructure.Repositories.Identity; @@ -89,6 +90,18 @@ public class RolePermissionRepository : BaseRepository, IRolePer { return await QueryRepository.FindAsync( rp => rp.RoleId == roleId, + include: query => query.Include(rp => rp.Permission), + cancellationToken: cancellationToken); + } + + /// + /// 批量获取多个角色的权限(包含权限详情) + /// + public async Task> GetRolePermissionsByRolesAsync(IEnumerable roleIds, CancellationToken cancellationToken = default) + { + return await QueryRepository.FindAsync( + rp => roleIds.Contains(rp.RoleId), + include: query => query.Include(rp => rp.Permission), cancellationToken: cancellationToken); } diff --git a/src/X1.Presentation/Controllers/UsersController.cs b/src/X1.Presentation/Controllers/UsersController.cs index 64614c3..ebee1c6 100644 --- a/src/X1.Presentation/Controllers/UsersController.cs +++ b/src/X1.Presentation/Controllers/UsersController.cs @@ -5,6 +5,8 @@ using X1.Application.Features.Users.Commands.UpdateUser; using X1.Application.Features.Users.Commands.DeleteUser; using X1.Application.Features.Users.Queries.GetUserById; using X1.Application.Features.Users.Queries.GetAllUsers; +using X1.Application.Features.Users.Queries.GetCurrentUser; +using X1.Application.Features.Users.Queries.Dtos; using X1.Presentation.Abstractions; using X1.Application.Common; using Microsoft.Extensions.Logging; @@ -322,22 +324,14 @@ public class UsersController : ApiController /// 未授权,用户未登录 /// 用户不存在 [HttpGet("current")] - [ProducesResponseType(typeof(OperationResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(OperationResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(OperationResult), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(OperationResult), StatusCodes.Status404NotFound)] - public async Task> CurrentUser() + public async Task> CurrentUser() { try { - //var userId = User.FindFirst("sub")?.Value; - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (string.IsNullOrEmpty(userId)) - { - _logger.LogWarning("无法获取当前用户ID"); - return OperationResult.CreateFailure("无法获取当前用户信息"); - } - - var query = new GetUserByIdQuery(userId); + var query = new GetCurrentUserQuery(); var result = await mediator.Send(query); if (result.IsSuccess) @@ -355,7 +349,7 @@ public class UsersController : ApiController catch (Exception ex) { _logger.LogError(ex, "获取当前用户信息时发生异常"); - return OperationResult.CreateFailure("系统错误,请稍后重试"); + return OperationResult.CreateFailure("系统错误,请稍后重试"); } } } \ No newline at end of file diff --git a/src/X1.WebUI/src/components/permissions/PermissionAssignmentDialog.tsx b/src/X1.WebUI/src/components/permissions/PermissionAssignmentDialog.tsx new file mode 100644 index 0000000..c93bb1c --- /dev/null +++ b/src/X1.WebUI/src/components/permissions/PermissionAssignmentDialog.tsx @@ -0,0 +1,487 @@ +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; +import { Search, Shield, ShieldCheck, ChevronDown, ChevronRight, FolderOpen, FileText } from 'lucide-react'; +import { PermissionInfo, permissionService } from '@/services/permissionService'; +import { Role } from '@/services/roleService'; +import { rolePermissionService } from '@/services/rolePermissionService'; +import { toast } from '@/components/ui/use-toast'; +import { navigationMenuService } from '@/services/navigationMenuService'; +import { NavigationMenuType } from '@/types/navigation'; + +interface PermissionAssignmentDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + role: Role | null; + onSuccess?: () => void; +} + +// 菜单树节点接口 +interface MenuTreeNode { + id: string; + title: string; + path: string; + icon?: string; + parentId?: string; + type: NavigationMenuType; + permissionCode?: string; + sortOrder: number; + isEnabled: boolean; + isSystem: boolean; + description?: string; + requiresPermission: boolean; + level: number; + isExpanded?: boolean; + isSelected?: boolean; + isChecked?: boolean; + hasPermission?: boolean; + children: MenuTreeNode[]; +} + +export default function PermissionAssignmentDialog({ + open, + onOpenChange, + role, + onSuccess +}: PermissionAssignmentDialogProps) { + const [loading, setLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [menuTree, setMenuTree] = useState([]); + const [selectedPermissions, setSelectedPermissions] = useState>(new Set()); + const [rolePermissions, setRolePermissions] = useState>(new Set()); + const [allPermissions, setAllPermissions] = useState([]); + + // 获取菜单树和角色权限 + useEffect(() => { + if (open) { + loadMenuTree(); + loadAllPermissions(); + if (role) { + loadRolePermissions(); + } + } + }, [open, role]); + + // 当角色权限加载完成后,重新构建菜单树以正确显示权限状态 + useEffect(() => { + if (rolePermissions.size > 0 && menuTree.length > 0) { + const updatedTree = buildMenuTreeWithPermissions(menuTree); + setMenuTree(updatedTree); + } + }, [rolePermissions]); + + // 加载所有权限数据(用于权限代码到ID的映射) + const loadAllPermissions = async () => { + try { + const response = await permissionService.getAllPermissions({ pageSize: 1000 }); + if (response.isSuccess && response.data) { + setAllPermissions(response.data.permissions || []); + } + } catch (error) { + console.error('加载权限数据失败:', error); + } + }; + + // 加载菜单树数据 + const loadMenuTree = async () => { + try { + setLoading(true); + const result = await navigationMenuService.getNavigationMenuTree(); + if (result.isSuccess && result.data) { + const tree = buildMenuTree(result.data.navigationMenuTrees); + setMenuTree(tree); + } else { + // 如果树形接口失败,使用普通接口构建树 + const fallbackResult = await navigationMenuService.getAllNavigationMenus({ pageSize: 1000 }); + if (fallbackResult.isSuccess && fallbackResult.data) { + const tree = buildMenuTreeFromList(fallbackResult.data.navigationMenus); + setMenuTree(tree); + } + } + } catch (error) { + console.error('加载菜单树失败:', error); + toast({ + title: '加载失败', + description: '无法加载菜单树数据', + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + // 构建菜单树 + const buildMenuTree = (menus: any[], level = 0): MenuTreeNode[] => { + return menus.map(menu => ({ + ...menu, + level, + isExpanded: level === 0, // 顶级菜单默认展开 + isSelected: false, + isChecked: false, + hasPermission: false, // 初始时不检查权限,等权限加载完成后再更新 + children: menu.children ? buildMenuTree(menu.children, level + 1) : [] + })); + }; + + // 重新构建菜单树,更新权限状态 + const buildMenuTreeWithPermissions = (menus: MenuTreeNode[], level = 0): MenuTreeNode[] => { + return menus.map(menu => ({ + ...menu, + level, + isExpanded: menu.isExpanded, + isSelected: false, + isChecked: false, + hasPermission: checkMenuHasPermission(menu), + children: menu.children ? buildMenuTreeWithPermissions(menu.children, level + 1) : [] + })); + }; + + // 从列表构建菜单树 + const buildMenuTreeFromList = (menus: any[]): MenuTreeNode[] => { + const menuMap = new Map(); + const rootMenus: MenuTreeNode[] = []; + + // 创建所有菜单节点 + menus.forEach(menu => { + const node: MenuTreeNode = { + ...menu, + level: 0, + isExpanded: false, + isSelected: false, + isChecked: false, + hasPermission: false, // 初始时不检查权限,等权限加载完成后再更新 + children: [] + }; + menuMap.set(menu.id, node); + }); + + // 构建父子关系 + menus.forEach(menu => { + const node = menuMap.get(menu.id)!; + if (menu.parentId && menuMap.has(menu.parentId)) { + const parent = menuMap.get(menu.parentId)!; + parent.children.push(node); + node.level = parent.level + 1; + } else { + rootMenus.push(node); + } + }); + + // 设置顶级菜单为展开状态 + rootMenus.forEach(menu => { + menu.isExpanded = true; + }); + + return rootMenus; + }; + + // 检查菜单是否有权限 + const checkMenuHasPermission = (menu: any): boolean => { + if (!menu.permissionCode) return false; + return rolePermissions.has(menu.permissionCode); + }; + + // 加载角色权限 + const loadRolePermissions = async () => { + if (!role) return; + + try { + setLoading(true); + const response = await rolePermissionService.getRolePermissions(role.id); + if (response.isSuccess && response.data) { + // 使用权限代码作为键,因为菜单树使用权限代码 + const permissionCodes = new Set(response.data.permissions.map(p => p.code)); + setRolePermissions(permissionCodes); + setSelectedPermissions(permissionCodes); + } + } catch (error) { + console.error('获取角色权限失败:', error); + toast({ + title: '获取角色权限失败', + description: '无法获取角色当前权限,请重试', + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + // 切换菜单展开状态 + const toggleMenuExpansion = (menuId: string) => { + setMenuTree(prev => toggleMenuExpansionRecursive(prev, menuId)); + }; + + const toggleMenuExpansionRecursive = (menus: MenuTreeNode[], menuId: string): MenuTreeNode[] => { + return menus.map(menu => { + if (menu.id === menuId) { + return { ...menu, isExpanded: !menu.isExpanded }; + } + if (menu.children.length > 0) { + return { + ...menu, + children: toggleMenuExpansionRecursive(menu.children, menuId) + }; + } + return menu; + }); + }; + + // 切换权限选择状态 + const togglePermissionSelection = (permissionCode: string) => { + if (!permissionCode) return; + + const newSelected = new Set(selectedPermissions); + if (newSelected.has(permissionCode)) { + newSelected.delete(permissionCode); + } else { + newSelected.add(permissionCode); + } + setSelectedPermissions(newSelected); + }; + + // 全选/取消全选 + const handleSelectAll = () => { + const allPermissionCodes = getAllPermissionCodes(menuTree); + setSelectedPermissions(new Set(allPermissionCodes)); + }; + + const handleDeselectAll = () => { + setSelectedPermissions(new Set()); + }; + + // 获取所有权限代码 + const getAllPermissionCodes = (menus: MenuTreeNode[]): string[] => { + const codes: string[] = []; + menus.forEach(menu => { + if (menu.permissionCode) { + codes.push(menu.permissionCode); + } + if (menu.children.length > 0) { + codes.push(...getAllPermissionCodes(menu.children)); + } + }); + return codes; + }; + + // 将权限代码转换为权限ID + const getPermissionIdsByCodes = (permissionCodes: string[]): string[] => { + return permissionCodes + .map(code => allPermissions.find(p => p.code === code)?.id) + .filter(id => id !== undefined) as string[]; + }; + + // 保存权限分配 + const handleSave = async () => { + if (!role) return; + + try { + setLoading(true); + + // 将权限代码转换为权限ID + const selectedPermissionIds = getPermissionIdsByCodes(Array.from(selectedPermissions)); + + // 使用新的后端逻辑,在一个请求中处理所有权限变更 + const result = await rolePermissionService.batchAddPermissions(role.id, selectedPermissionIds); + if (!result.isSuccess) { + throw new Error(`权限分配失败: ${result.errorMessages?.join(', ')}`); + } + + // 显示详细的操作结果 + const { addedCount, removedCount } = result.data!; + let description = `已成功为角色 "${role.name}" 分配权限`; + + if (addedCount > 0 || removedCount > 0) { + const operations = []; + if (addedCount > 0) operations.push(`新增 ${addedCount} 个权限`); + if (removedCount > 0) operations.push(`删除 ${removedCount} 个权限`); + description = `已成功为角色 "${role.name}" 处理权限:${operations.join(',')}`; + } + + toast({ + title: '权限分配成功', + description, + }); + + onSuccess?.(); + onOpenChange(false); + } catch (error) { + console.error('保存权限失败:', error); + toast({ + title: '保存权限失败', + description: error instanceof Error ? error.message : '无法保存权限分配,请重试', + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + // 过滤菜单树 + const filteredMenuTree = menuTree.filter(menu => { + if (!searchTerm) return true; + return ( + menu.title.toLowerCase().includes(searchTerm.toLowerCase()) || + (menu.description && menu.description.toLowerCase().includes(searchTerm.toLowerCase())) || + (menu.permissionCode && menu.permissionCode.toLowerCase().includes(searchTerm.toLowerCase())) + ); + }); + + // 渲染菜单树节点 + const renderMenuNode = (menu: MenuTreeNode) => { + const hasChildren = menu.children && menu.children.length > 0; + const canExpand = hasChildren; + const isSelected = menu.permissionCode ? selectedPermissions.has(menu.permissionCode) : false; + const wasAssigned = menu.permissionCode ? rolePermissions.has(menu.permissionCode) : false; + + return ( +
+
0 ? 'ml-4' : ''} + `} + > + {/* 权限选择复选框 */} + {menu.permissionCode && ( + togglePermissionSelection(menu.permissionCode!)} + className="flex-shrink-0" + /> + )} + + {/* 展开/折叠图标 */} + {canExpand && ( + + )} + + {/* 菜单类型图标 */} +
+ {menu.type === NavigationMenuType.StandaloneMenuItem && ( + + )} + {menu.type === NavigationMenuType.MenuGroup && ( + + )} + {menu.type === NavigationMenuType.SubMenuItem && ( + + )} +
+ + {/* 菜单标题 */} + + {menu.title} + + + {/* 权限状态 */} + {menu.permissionCode && ( +
+ {wasAssigned && ( + + )} + + {menu.permissionCode} + +
+ )} +
+ + {/* 子菜单 */} + {canExpand && menu.isExpanded && ( +
+ {menu.children.map(child => renderMenuNode(child))} +
+ )} +
+ ); + }; + + if (!role) return null; + + return ( + + + + + + 为角色 "{role.name}" 分配权限 + + + +
+ {/* 搜索框 */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ + {/* 批量操作按钮 */} +
+ + + + 已选择 {selectedPermissions.size} 个权限 + +
+ + {/* 菜单树 */} + + {loading ? ( +
+ 加载中... +
+ ) : filteredMenuTree.length === 0 ? ( +
+ 没有找到匹配的菜单 +
+ ) : ( +
+ {filteredMenuTree.map(menu => renderMenuNode(menu))} +
+ )} +
+ + {/* 操作按钮 */} +
+ + +
+
+
+
+ ); +} diff --git a/src/X1.WebUI/src/components/permissions/README.md b/src/X1.WebUI/src/components/permissions/README.md new file mode 100644 index 0000000..b270c47 --- /dev/null +++ b/src/X1.WebUI/src/components/permissions/README.md @@ -0,0 +1,189 @@ +# 权限分配组件使用说明 + +## 概述 + +`PermissionAssignmentDialog` 是一个用于角色权限分配的对话框组件,支持按资源分组显示权限、搜索过滤、批量选择等功能。 + +## 功能特性 + +### 1. 权限分组显示 +- 按资源类型自动分组(用户管理、角色管理、设备管理等) +- 每个资源组显示相关的操作权限 +- 支持展开/折叠资源组 + +### 2. 搜索和过滤 +- 支持按权限名称、描述、资源类型搜索 +- 实时过滤显示结果 +- 支持中文和英文搜索 + +### 3. 批量操作 +- 全选/取消全选所有权限 +- 按资源组选择权限 +- 显示已选择权限数量 + +### 4. 权限状态显示 +- 绿色勾选图标:角色当前已分配的权限 +- 复选框:可选择/取消选择的权限 +- 权限代码和描述信息 + +### 5. 智能权限管理 +- 自动计算需要添加/删除的权限 +- 批量添加新权限 +- 批量删除取消的权限 +- 避免重复操作 + +## 使用方法 + +### 1. 基本使用 + +```tsx +import PermissionAssignmentDialog from '@/components/permissions/PermissionAssignmentDialog'; + +function RoleManagement() { + const [dialogOpen, setDialogOpen] = useState(false); + const [selectedRole, setSelectedRole] = useState(null); + + const handleSetPermissions = (role: Role) => { + setSelectedRole(role); + setDialogOpen(true); + }; + + return ( +
+ {/* 角色表格 */} + + + {/* 权限分配对话框 */} + { + console.log('权限分配成功'); + // 刷新角色列表或其他操作 + }} + /> +
+ ); +} +``` + +### 2. 在 RoleTable 中集成 + +```tsx +// RoleTable.tsx +export default function RoleTable({ onSetPermissions, ...props }) { + const [permissionDialogOpen, setPermissionDialogOpen] = useState(false); + const [selectedRole, setSelectedRole] = useState(null); + + const handleSetPermissions = (role: Role) => { + setSelectedRole(role); + setPermissionDialogOpen(true); + }; + + return ( +
+ {/* 表格内容 */} + + {/* ... 表格行 ... */} + handleSetPermissions(role)}> + 设置权限 + +
+ + {/* 权限分配对话框 */} + { + // 权限分配成功后的回调 + }} + /> +
+ ); +} +``` + +## 权限数据结构 + +### Permission 接口 + +```typescript +interface Permission { + id: string; + name: string; // 权限名称 + code: string; // 权限代码 (如: "users.view") + description?: string; // 权限描述 +} +``` + +### 权限代码格式 + +权限代码采用 `resource.action` 格式: + +- **资源类型**: users, roles, permissions, devices, scenarios 等 +- **操作类型**: view, create, edit, delete, manage, export, import 等 + +示例: +- `users.view` - 查看用户 +- `users.create` - 创建用户 +- `users.edit` - 编辑用户 +- `users.delete` - 删除用户 +- `users.manage` - 用户管理(包含所有操作) + +## 样式定制 + +### 主题颜色 +- 主色调:使用 CSS 变量 `--primary` +- 边框颜色:使用 CSS 变量 `--border` +- 背景颜色:使用 CSS 变量 `--background` + +### 响应式设计 +- 支持不同屏幕尺寸 +- 移动端友好的触摸操作 +- 自适应内容高度 + +## 注意事项 + +### 1. 依赖要求 +- 需要安装 `@radix-ui/react-scroll-area` +- 需要配置 Tailwind CSS +- 需要权限服务和角色权限服务 + +### 2. 性能考虑 +- 使用 Set 数据结构进行权限比较 +- 支持大量权限数据的显示 +- 延迟加载和分页支持 + +### 3. 错误处理 +- 网络请求失败提示 +- 权限保存失败处理 +- 用户友好的错误信息 + +### 4. 安全考虑 +- 权限验证和检查 +- 防止权限提升攻击 +- 操作审计日志 + +## 扩展功能 + +### 1. 权限继承 +- 支持角色权限继承 +- 权限继承规则配置 +- 继承权限的显示和管理 + +### 2. 权限模板 +- 预定义权限模板 +- 快速权限分配 +- 模板导入导出 + +### 3. 权限审计 +- 权限变更历史 +- 操作日志记录 +- 权限使用统计 + +### 4. 批量操作 +- 批量角色权限分配 +- 权限复制功能 +- 权限迁移工具 diff --git a/src/X1.WebUI/src/components/ui/scroll-area.tsx b/src/X1.WebUI/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..cf253cf --- /dev/null +++ b/src/X1.WebUI/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/src/X1.WebUI/src/constants/auth.ts b/src/X1.WebUI/src/constants/auth.ts index fc2ae40..34b4ea6 100644 --- a/src/X1.WebUI/src/constants/auth.ts +++ b/src/X1.WebUI/src/constants/auth.ts @@ -49,4 +49,9 @@ export const AUTH_CONSTANTS = { export const DEFAULT_CREDENTIALS = { username: 'hyh', password: 'H295172551@qq.com' -}; \ No newline at end of file +}; + +// export const DEFAULT_CREDENTIALS = { +// username: '', +// password: '' +// }; \ No newline at end of file diff --git a/src/X1.WebUI/src/constants/defaultPermissions.ts b/src/X1.WebUI/src/constants/defaultPermissions.ts new file mode 100644 index 0000000..a2e1b63 --- /dev/null +++ b/src/X1.WebUI/src/constants/defaultPermissions.ts @@ -0,0 +1,100 @@ +/** + * 默认权限配置 + * 注意:现在系统使用真实的权限数据,这些默认权限仅作为备用或开发测试使用 + */ + +export const DEFAULT_PERMISSIONS = [ + 'dashboard.view', + 'users.view', + 'roles.view', + 'permissions.view', + 'permissions.manage', + 'settings.view', + 'settings.manage', + 'navigationmenus.view', + 'navigationmenus.manage', + 'buttonpermissions.view', + 'buttonpermissions.manage', + + // 场景管理权限 + 'scenarios.view', + 'scenarios.manage', + 'testcases.view', + 'testcases.manage', + 'testcases.create', + 'teststeps.view', + 'teststeps.manage', + 'teststeps.create', + 'tasks.view', + 'tasks.manage', + 'tasks.create', + 'taskreviews.view', + 'taskreviews.manage', + 'taskreviews.create', + 'taskexecutions.view', + 'taskexecutions.manage', + 'taskexecutions.create', + 'functionalanalysis.view', + 'functionalanalysis.manage', + 'performanceanalysis.view', + 'performanceanalysis.manage', + 'issueanalysis.view', + 'issueanalysis.manage', + 'ueanalysis.view', + 'ueanalysis.manage', + + // 设备管理权限 + 'devices.view', + 'devices.manage', + 'protocols.view', + 'protocols.manage', + 'ranconfigurations.view', + 'ranconfigurations.manage', + 'imsconfigurations.view', + 'imsconfigurations.manage', + 'corenetworkconfigs.view', + 'corenetworkconfigs.manage', + 'networkstackconfigs.view', + 'networkstackconfigs.manage', + + // 设备运行时管理权限 + 'deviceruntimes.view', + 'deviceruntimes.manage', + + // 协议日志管理权限 + 'protocollogs.view', + 'protocollogs.manage', + + // 终端设备管理权限 + 'terminalservices.view', + 'terminalservices.manage', + + // ADB操作管理权限 + 'adboperations.view', + 'adboperations.manage', + + // AT操作管理权限 + 'atoperations.view', + 'atoperations.manage', + + // 终端设备管理权限 + 'terminaldevices.view', + 'terminaldevices.manage', +] as const; + +/** + * 获取默认权限列表 + * @returns 默认权限字符串数组 + */ +export const getDefaultPermissions = (): string[] => { + return [...DEFAULT_PERMISSIONS]; +}; + +/** + * 检查是否为默认权限 + * @param permission 权限代码 + * @returns 是否为默认权限 + */ +export const isDefaultPermission = (permission: string): boolean => { + return DEFAULT_PERMISSIONS.includes(permission as any); +}; diff --git a/src/X1.WebUI/src/constants/menuConfig.backup.ts b/src/X1.WebUI/src/constants/menuConfig.backup.ts new file mode 100644 index 0000000..61b648c --- /dev/null +++ b/src/X1.WebUI/src/constants/menuConfig.backup.ts @@ -0,0 +1,345 @@ +import { LucideIcon, LayoutDashboard, Users, Settings, TestTube, BarChart3, Gauge, ClipboardList, Network, Smartphone, FolderOpen, Activity } from 'lucide-react'; + +// 定义权限类型 +export type Permission = + | 'dashboard.view' + | 'users.view' + | 'users.manage' + | 'roles.view' + | 'roles.manage' + | 'permissions.view' + | 'permissions.manage' + | 'settings.view' + | 'settings.manage' + | 'navigationmenus.view' + | 'navigationmenus.manage' + // 场景管理权限 + | 'scenarios.view' + | 'scenarios.manage' + // 用例管理权限 + | 'testcases.view' + | 'testcases.manage' + | 'testcases.create' + | 'teststeps.view' + | 'teststeps.manage' + | 'teststeps.create' + // 任务管理权限 + | 'tasks.view' + | 'tasks.manage' + | 'tasks.create' + | 'taskreviews.view' + | 'taskreviews.manage' + | 'taskreviews.create' + | 'taskexecutions.view' + | 'taskexecutions.manage' + | 'taskexecutions.create' + // 结果分析权限 + | 'functionalanalysis.view' + | 'functionalanalysis.manage' + | 'performanceanalysis.view' + | 'performanceanalysis.manage' + | 'issueanalysis.view' + | 'issueanalysis.manage' + | 'ueanalysis.view' + | 'ueanalysis.manage' + // 仪表管理权限 + | 'devices.view' + | 'devices.manage' + | 'protocols.view' + | 'protocols.manage' + | 'ranconfigurations.view' + | 'ranconfigurations.manage' + | 'imsconfigurations.view' + | 'imsconfigurations.manage' + | 'corenetworkconfigs.view' + | 'corenetworkconfigs.manage' + | 'networkstackconfigs.view' + | 'networkstackconfigs.manage' + // 终端服务管理权限 + | 'terminalservices.view' + | 'terminalservices.manage' + // 终端设备管理权限 + | 'terminaldevices.view' + | 'terminaldevices.manage' + // ADB操作管理权限 + | 'adboperations.view' + | 'adboperations.manage' + // AT操作管理权限 + | 'atoperations.view' + | 'atoperations.manage' + // 设备运行时管理权限 + | 'deviceruntimes.view' + | 'deviceruntimes.manage' + // 协议日志管理权限 + | 'protocollogs.view' + | 'protocollogs.manage' + // 按钮权限管理权限 + | 'buttonpermissions.view' + | 'buttonpermissions.manage'; + +export interface MenuItem { + title: string; + icon: LucideIcon; + href: string; + permission?: Permission; + children?: { + title: string; + href: string; + permission?: Permission; + }[]; +} + +export const menuItems: MenuItem[] = [ + { + title: '仪表盘', + icon: LayoutDashboard, + href: '/dashboard', + permission: 'dashboard.view', + }, + { + title: '场景管理', + icon: FolderOpen, + href: '/dashboard/scenarios', + permission: 'scenarios.view', + children: [ + { + title: '场景列表', + href: '/dashboard/scenarios/config', + permission: 'scenarios.manage', + }, + { + title: '场景绑定', + href: '/dashboard/scenarios/binding', + permission: 'scenarios.manage', + }, + ], + }, + { + title: '用例管理', + icon: TestTube, + href: '/dashboard/testcases', + permission: 'testcases.view', + children: [ + { + title: '用例列表', + href: '/dashboard/testcases/list', + permission: 'testcases.view', + }, + { + title: '创建用例', + href: '/dashboard/testcases/create', + permission: 'testcases.create', + }, + { + title: '步骤列表', + href: '/dashboard/testcases/steps', + permission: 'teststeps.view', + }, + ], + }, + { + title: '任务管理', + icon: ClipboardList, + href: '/dashboard/tasks', + permission: 'tasks.view', + children: [ + { + title: '任务列表', + href: '/dashboard/tasks/list', + permission: 'tasks.view', + }, + { + title: '创建任务', + href: '/dashboard/tasks/create', + permission: 'tasks.create', + }, + { + title: '审核任务', + href: '/dashboard/tasks/reviews', + permission: 'taskreviews.view', + }, + { + title: '执行任务', + href: '/dashboard/tasks/executions', + permission: 'taskexecutions.view', + }, + ], + }, + { + title: '结果分析', + icon: BarChart3, + href: '/dashboard/analysis', + permission: 'functionalanalysis.view', + children: [ + { + title: '功能分析', + href: '/dashboard/analysis/functional', + permission: 'functionalanalysis.view', + }, + { + title: '性能分析', + href: '/dashboard/analysis/performance', + permission: 'performanceanalysis.view', + }, + { + title: '问题分析', + href: '/dashboard/analysis/issue', + permission: 'issueanalysis.view', + }, + { + title: 'UE分析', + href: '/dashboard/analysis/ue', + permission: 'ueanalysis.view', + }, + ], + }, + { + title: '仪表管理', + icon: Gauge, + href: '/dashboard/instruments', + permission: 'devices.view', + children: [ + { + title: '设备列表', + href: '/dashboard/instruments/list', + permission: 'devices.view', + }, + { + title: '协议列表', + href: '/dashboard/instruments/protocols', + permission: 'protocols.view', + }, + { + title: '启动设备网络', + href: '/dashboard/instruments/device-runtimes/list', + permission: 'deviceruntimes.view', + }, + ], + }, + { + title: '终端管理', + icon: Smartphone, + href: '/dashboard/terminal-services', + permission: 'terminalservices.view', + children: [ + { + title: '终端服务', + href: '/dashboard/terminal-services', + permission: 'terminalservices.view', + }, + { + title: '终端设备', + href: '/dashboard/terminal-devices/list', + permission: 'terminaldevices.view', + }, + { + title: 'ADB命令配置', + href: '/dashboard/terminal-services/adb-operations', + permission: 'adboperations.view', + }, + { + title: 'AT命令配置', + href: '/dashboard/terminal-services/at-operations', + permission: 'atoperations.view', + }, + ], + }, + { + title: '信令分析', + icon: Activity, + href: '/dashboard/protocol-logs', + permission: 'protocollogs.view', + children: [ + { + title: '在线协议日志', + href: '/dashboard/protocol-logs/online-logs', + permission: 'protocollogs.view', + }, + { + title: '历史协议日志', + href: '/dashboard/protocol-logs/history-logs', + permission: 'protocollogs.view', + }, + ], + }, + { + title: '网络栈配置', + icon: Network, + href: '/dashboard/network-stack-configs', + permission: 'ranconfigurations.view', + children: [ + { + title: 'RAN配置', + href: '/dashboard/network-stack-configs/ran-configurations', + permission: 'ranconfigurations.view', + }, + { + title: 'IMS配置', + href: '/dashboard/network-stack-configs/ims-configurations', + permission: 'imsconfigurations.view', + }, + { + title: '核心网络配置', + href: '/dashboard/network-stack-configs/core-network-configs', + permission: 'corenetworkconfigs.view', + }, + { + title: '网络栈配置', + href: '/dashboard/network-stack-configs/network-stack-configs', + permission: 'networkstackconfigs.view', + }, + ], + }, + { + title: '用户管理', + icon: Users, + href: '/dashboard/users', + permission: 'users.view', + children: [ + { + title: '用户列表', + href: '/dashboard/users/list', + permission: 'users.view', + }, + { + title: '角色管理', + href: '/dashboard/users/roles', + permission: 'roles.view', + }, + ], + }, + { + title: '系统设置', + icon: Settings, + href: '/dashboard/settings', + permission: 'settings.view', + children: [ + { + title: '导航菜单管理', + href: '/dashboard/settings/navigation-menus', + permission: 'navigationmenus.view', + }, + { + title: '权限管理', + href: '/dashboard/settings/permissions', + permission: 'permissions.view', + }, + { + title: '按钮权限管理', + href: '/dashboard/settings/button-permissions', + permission: 'buttonpermissions.view', + }, + ], + }, +]; + +// 导出权限检查工具函数 +export const hasPermission = (userPermissions: Permission[] | undefined | null, requiredPermission?: Permission): boolean => { + // 如果没有设置权限要求,则默认允许访问 + if (!requiredPermission) return true; + + // 如果用户权限为空,则拒绝访问 + if (!userPermissions || !Array.isArray(userPermissions)) return false; + + return userPermissions.includes(requiredPermission); +}; diff --git a/src/X1.WebUI/src/constants/menuConfig.ts b/src/X1.WebUI/src/constants/menuConfig.ts index d4fe06c..95d785b 100644 --- a/src/X1.WebUI/src/constants/menuConfig.ts +++ b/src/X1.WebUI/src/constants/menuConfig.ts @@ -1,346 +1,179 @@ -import { LucideIcon, LayoutDashboard, Users, Settings, TestTube, BarChart3, Gauge, FileText, ClipboardList, Network, Smartphone, FolderOpen, Activity } from 'lucide-react'; +import { LucideIcon } from 'lucide-react'; +import { navigationMenuService } from '@/services/navigationMenuService'; +import { NavigationMenuInfo, NavigationMenuType } from '@/types/navigation'; +import { resolveIcon } from '@/utils/iconUtils'; -// 定义权限类型 -export type Permission = - | 'dashboard.view' - | 'users.view' - | 'users.manage' - | 'roles.view' - | 'roles.manage' - | 'permissions.view' - | 'permissions.manage' - | 'settings.view' - | 'settings.manage' - | 'navigationmenus.view' - | 'navigationmenus.manage' - // 场景管理权限 - | 'scenarios.view' - | 'scenarios.manage' - // 用例管理权限 - | 'testcases.view' - | 'testcases.manage' - | 'testcases.create' - | 'teststeps.view' - | 'teststeps.manage' - | 'teststeps.create' - // 任务管理权限 - | 'tasks.view' - | 'tasks.manage' - | 'tasks.create' - | 'taskreviews.view' - | 'taskreviews.manage' - | 'taskreviews.create' - | 'taskexecutions.view' - | 'taskexecutions.manage' - | 'taskexecutions.create' - // 结果分析权限 - | 'functionalanalysis.view' - | 'functionalanalysis.manage' - | 'performanceanalysis.view' - | 'performanceanalysis.manage' - | 'issueanalysis.view' - | 'issueanalysis.manage' - | 'ueanalysis.view' - | 'ueanalysis.manage' - // 仪表管理权限 - | 'devices.view' - | 'devices.manage' - | 'protocols.view' - | 'protocols.manage' - | 'ranconfigurations.view' - | 'ranconfigurations.manage' - | 'imsconfigurations.view' - | 'imsconfigurations.manage' - | 'corenetworkconfigs.view' - | 'corenetworkconfigs.manage' - | 'networkstackconfigs.view' - | 'networkstackconfigs.manage' - // 终端服务管理权限 - | 'terminalservices.view' - | 'terminalservices.manage' - // 终端设备管理权限 - | 'terminaldevices.view' - | 'terminaldevices.manage' - // ADB操作管理权限 - | 'adboperations.view' - | 'adboperations.manage' - // AT操作管理权限 - | 'atoperations.view' - | 'atoperations.manage' - // 设备运行时管理权限 - | 'deviceruntimes.view' - | 'deviceruntimes.manage' - // 协议日志管理权限 - | 'protocollogs.view' - | 'protocollogs.manage' - // 按钮权限管理权限 - | 'buttonpermissions.view' - | 'buttonpermissions.manage' +// 重新导出权限相关类型和函数 +export type { + Permission, + PermissionAction, + PermissionResource +} from '@/services/permissionService'; +export { + hasAnyPermission, + hasAllPermissions, + isValidPermission as isValidPermissionCode +} from '@/services/permissionService'; +// 导入权限类型 +import type { Permission } from '@/services/permissionService'; +// 菜单项接口定义 export interface MenuItem { + id: string; title: string; icon: LucideIcon; href: string; permission?: Permission; - children?: { - title: string; - href: string; - permission?: Permission; - }[]; + type: NavigationMenuType; + sortOrder: number; + isEnabled: boolean; + isSystem: boolean; + description?: string; + children?: MenuItem[]; } -export const menuItems: MenuItem[] = [ - { - title: '仪表盘', - icon: LayoutDashboard, - href: '/dashboard', - permission: 'dashboard.view', - }, - { - title: '场景管理', - icon: FolderOpen, - href: '/dashboard/scenarios', - permission: 'scenarios.view', - children: [ - { - title: '场景列表', - href: '/dashboard/scenarios/config', - permission: 'scenarios.manage', - }, - { - title: '场景绑定', - href: '/dashboard/scenarios/binding', - permission: 'scenarios.manage', - }, - ], - }, - { - title: '用例管理', - icon: TestTube, - href: '/dashboard/testcases', - permission: 'testcases.view', - children: [ - { - title: '用例列表', - href: '/dashboard/testcases/list', - permission: 'testcases.view', - }, - { - title: '创建用例', - href: '/dashboard/testcases/create', - permission: 'testcases.create', - }, - { - title: '步骤列表', - href: '/dashboard/testcases/steps', - permission: 'teststeps.view', - }, - ], - }, - { - title: '任务管理', - icon: ClipboardList, - href: '/dashboard/tasks', - permission: 'tasks.view', - children: [ - { - title: '任务列表', - href: '/dashboard/tasks/list', - permission: 'tasks.view', - }, - { - title: '创建任务', - href: '/dashboard/tasks/create', - permission: 'tasks.create', - }, - { - title: '审核任务', - href: '/dashboard/tasks/reviews', - permission: 'taskreviews.view', - }, - { - title: '执行任务', - href: '/dashboard/tasks/executions', - permission: 'taskexecutions.view', - }, - ], - }, - { - title: '结果分析', - icon: BarChart3, - href: '/dashboard/analysis', - permission: 'functionalanalysis.view', - children: [ - { - title: '功能分析', - href: '/dashboard/analysis/functional', - permission: 'functionalanalysis.view', - }, - { - title: '性能分析', - href: '/dashboard/analysis/performance', - permission: 'performanceanalysis.view', - }, - { - title: '问题分析', - href: '/dashboard/analysis/issue', - permission: 'issueanalysis.view', - }, - { - title: 'UE分析', - href: '/dashboard/analysis/ue', - permission: 'ueanalysis.view', - }, - ], - }, - { - title: '仪表管理', - icon: Gauge, - href: '/dashboard/instruments', - permission: 'devices.view', - children: [ - { - title: '设备列表', - href: '/dashboard/instruments/list', - permission: 'devices.view', - }, - { - title: '协议列表', - href: '/dashboard/instruments/protocols', - permission: 'protocols.view', - }, - { - title: '启动设备网络', - href: '/dashboard/instruments/device-runtimes/list', - permission: 'deviceruntimes.view', - }, - ], - }, - { - title: '终端管理', - icon: Smartphone, - href: '/dashboard/terminal-services', - permission: 'terminalservices.view', - children: [ - { - title: '终端服务', - href: '/dashboard/terminal-services', - permission: 'terminalservices.view', - }, - { - title: '终端设备', - href: '/dashboard/terminal-devices/list', - permission: 'terminaldevices.view', - }, - { - title: 'ADB命令配置', - href: '/dashboard/terminal-services/adb-operations', - permission: 'adboperations.view', - }, - { - title: 'AT命令配置', - href: '/dashboard/terminal-services/at-operations', - permission: 'atoperations.view', - }, - ], - }, - { - title: '信令分析', - icon: Activity, - href: '/dashboard/protocol-logs', - permission: 'protocollogs.view', - children: [ - { - title: '在线协议日志', - href: '/dashboard/protocol-logs/online-logs', - permission: 'protocollogs.view', - }, - { - title: '历史协议日志', - href: '/dashboard/protocol-logs/history-logs', - permission: 'protocollogs.view', - }, - ], - }, - { - title: '网络栈配置', - icon: Network, - href: '/dashboard/network-stack-configs', - permission: 'ranconfigurations.view', - children: [ - { - title: 'RAN配置', - href: '/dashboard/network-stack-configs/ran-configurations', - permission: 'ranconfigurations.view', - }, - { - title: 'IMS配置', - href: '/dashboard/network-stack-configs/ims-configurations', - permission: 'imsconfigurations.view', - }, - { - title: '核心网络配置', - href: '/dashboard/network-stack-configs/core-network-configs', - permission: 'corenetworkconfigs.view', - }, - { - title: '网络栈配置', - href: '/dashboard/network-stack-configs/network-stack-configs', - permission: 'networkstackconfigs.view', - }, - ], - }, - { - title: '用户管理', - icon: Users, - href: '/dashboard/users', - permission: 'users.view', - children: [ - { - title: '用户列表', - href: '/dashboard/users/list', - permission: 'users.view', - }, - { - title: '角色管理', - href: '/dashboard/users/roles', - permission: 'roles.view', - }, - ], - }, - { - title: '系统设置', - icon: Settings, - href: '/dashboard/settings', - permission: 'settings.view', - children: [ - { - title: '导航菜单管理', - href: '/dashboard/settings/navigation-menus', - permission: 'navigationmenus.view', - }, - { - title: '权限管理', - href: '/dashboard/settings/permissions', - permission: 'permissions.view', - }, - { - title: '按钮权限管理', - href: '/dashboard/settings/button-permissions', - permission: 'buttonpermissions.view', - }, - ], - }, -]; -// 导出权限检查工具函数 -export const hasPermission = (userPermissions: Permission[] | undefined | null, requiredPermission?: Permission): boolean => { - // 如果没有设置权限要求,则默认允许访问 - if (!requiredPermission) return true; - - // 如果用户权限为空,则拒绝访问 - if (!userPermissions || !Array.isArray(userPermissions)) return false; + +// NavigationMenuInfo 转换为 MenuItem +export const convertToMenuItem = (navMenu: NavigationMenuInfo): MenuItem => ({ + id: navMenu.id, + title: navMenu.title, + icon: resolveIcon(navMenu.icon, navMenu.path, navMenu.title), + href: navMenu.path, + permission: navMenu.permissionCode as Permission, + type: navMenu.type, + sortOrder: navMenu.sortOrder, + isEnabled: navMenu.isEnabled, + isSystem: navMenu.isSystem, + description: navMenu.description, + children: navMenu.children?.map(convertToMenuItem) || [] +}); + +// 构建菜单树结构 +export const buildMenuTree = (menus: NavigationMenuInfo[]): MenuItem[] => { + const menuMap = new Map(); + const rootMenus: NavigationMenuInfo[] = []; + + // 建立映射和构建父子关系 + menus.forEach(menu => menuMap.set(menu.id, { ...menu, children: [] })); + menus.forEach(menu => { + const currentMenu = menuMap.get(menu.id)!; + if (menu.parentId && menuMap.has(menu.parentId)) { + menuMap.get(menu.parentId)!.children.push(currentMenu); + } else { + rootMenus.push(currentMenu); + } + }); + + // 递归排序 + const sortMenus = (menuList: NavigationMenuInfo[]): NavigationMenuInfo[] => + menuList + .sort((a, b) => a.sortOrder - b.sortOrder) + .map(menu => ({ ...menu, children: sortMenus(menu.children) })); + + return sortMenus(rootMenus) + .filter(menu => menu.isEnabled) + .map(convertToMenuItem); +}; + +// 缓存管理 +interface MenuCache { + data: MenuItem[]; + timestamp: number; +} + +let menuCache: MenuCache | null = null; +const CACHE_TTL = 5 * 60 * 1000; // 5分钟 + +const isCacheValid = (cache: MenuCache | null): boolean => + cache !== null && (Date.now() - cache.timestamp) < CACHE_TTL; + +export const clearMenuCache = (): void => { menuCache = null; }; + + + +// 主要 API 函数 +export const getMenuItems = async (): Promise => { + try { + if (isCacheValid(menuCache)) { + return menuCache!.data; + } + + const result = await navigationMenuService.getEnabledNavigationMenus(); + + if (result.isSuccess && result.data) { + const menuItems = buildMenuTree(result.data); + menuCache = { data: menuItems, timestamp: Date.now() }; + return menuItems; + } else { + console.warn('获取导航菜单失败:', result.errorMessages); + return []; + } + } catch (error) { + console.error('获取导航菜单异常:', error); + return []; + } +}; + +export const getFlatMenuItems = async (): Promise => { + const menus = await getMenuItems(); + const flatten = (items: MenuItem[]): MenuItem[] => + items.flatMap(item => [item, ...flatten(item.children || [])]); + return flatten(menus); +}; + +export const getMenuItemsByType = async (type: NavigationMenuType): Promise => { + try { + const result = await navigationMenuService.getNavigationMenusByType(type); + return result.isSuccess && result.data + ? result.data + .filter(menu => menu.isEnabled) + .sort((a, b) => a.sortOrder - b.sortOrder) + .map(convertToMenuItem) + : []; + } catch (error) { + console.error('根据类型获取菜单失败:', error); + return []; + } +}; + +export const getTopLevelMenuItems = async (): Promise => { + const menus = await getMenuItems(); + return menus.filter(menu => + menu.type === NavigationMenuType.StandaloneMenuItem || + menu.type === NavigationMenuType.MenuGroup + ); +}; + +// 权限相关工具函数 +export const extractResourceFromPermission = (permission: Permission): string | null => + permission.split('.')[0] || null; + +export const extractActionFromPermission = (permission: Permission): string | null => + permission.split('.')[1] || null; + +export const hasPermission = ( + userPermissions: Permission[] | undefined | null, + requiredPermission?: Permission +): boolean => { + if (!requiredPermission) return true; + if (!userPermissions || !Array.isArray(userPermissions)) + { + console.log("test",requiredPermission) + return false; + } return userPermissions.includes(requiredPermission); -}; \ No newline at end of file +}; + +// 向后兼容的静态导出 +export let menuItems: MenuItem[] = []; + +// 初始化菜单 +getMenuItems().then(items => { + menuItems = items; +}).catch(error => { + console.error('初始化菜单失败:', error); + menuItems = []; +}); \ No newline at end of file diff --git a/src/X1.WebUI/src/contexts/AuthContext.tsx b/src/X1.WebUI/src/contexts/AuthContext.tsx index 5974e40..ce476be 100644 --- a/src/X1.WebUI/src/contexts/AuthContext.tsx +++ b/src/X1.WebUI/src/contexts/AuthContext.tsx @@ -29,79 +29,10 @@ type AuthAction = | { type: 'SET_USER'; payload: { user: User; accessToken: string; refreshToken: string } } | { type: 'SET_REMEMBER_ME'; payload: boolean }; -// 获取默认权限 -const getDefaultPermissions = (userPermissions: Record = {}) => [ - ...new Set([ - ...Object.keys(userPermissions || {}), - 'dashboard.view', - 'users.view', - "roles.view", - "permissions.view", - "permissions.manage", - "settings.view", - "settings.manage", - "navigationmenus.view", - "navigationmenus.manage", - "buttonpermissions.view", - "buttonpermissions.manage", - // 场景管理权限 - 'scenarios.view', - 'scenarios.manage', - 'testcases.view', - 'testcases.manage', - 'testcases.create', - 'teststeps.view', - 'teststeps.manage', - 'teststeps.create', - 'tasks.view', - 'tasks.manage', - 'tasks.create', - 'taskreviews.view', - 'taskreviews.manage', - 'taskreviews.create', - 'taskexecutions.view', - 'taskexecutions.manage', - 'taskexecutions.create', - 'functionalanalysis.view', - 'functionalanalysis.manage', - 'performanceanalysis.view', - 'performanceanalysis.manage', - 'issueanalysis.view', - 'issueanalysis.manage', - 'ueanalysis.view', - 'ueanalysis.manage', - 'devices.view', - 'devices.manage', - 'protocols.view', - 'protocols.manage', - 'ranconfigurations.view', - 'ranconfigurations.manage', - 'imsconfigurations.view', - 'imsconfigurations.manage', - 'corenetworkconfigs.view', - 'corenetworkconfigs.manage', - 'networkstackconfigs.view', - 'networkstackconfigs.manage', - // 设备运行时管理权限 - 'deviceruntimes.view', - 'deviceruntimes.manage', - // 协议日志管理权限 - 'protocollogs.view', - 'protocollogs.manage', - // 终端设备管理权限 - 'terminalservices.view', - 'terminalservices.manage', - // ADB操作管理权限 - 'adboperations.view', - 'adboperations.manage', - // AT操作管理权限 - 'atoperations.view', - 'atoperations.manage', - // 终端设备管理权限 - 'terminaldevices.view', - 'terminaldevices.manage', - ]) -]; +// 获取用户权限列表 +const getUserPermissions = (userPermissions: string[] | null | undefined = []) => { + return userPermissions || []; +}; const authReducer = (state: AuthState, action: AuthAction): AuthState => { switch (action.type) { @@ -113,7 +44,7 @@ const authReducer = (state: AuthState, action: AuthAction): AuthState => { isLoading: false, isAuthenticated: true, user: action.payload.user, - userPermissions: getDefaultPermissions(action.payload.user.permissions), + userPermissions: getUserPermissions(action.payload.user.permissions), error: null, rememberMe: action.payload.rememberMe, }; @@ -148,7 +79,7 @@ const authReducer = (state: AuthState, action: AuthAction): AuthState => { return { ...state, user: action.payload.user, - userPermissions: getDefaultPermissions(action.payload.user.permissions), + userPermissions: getUserPermissions(action.payload.user.permissions), isAuthenticated: true, }; case 'SET_REMEMBER_ME': diff --git a/src/X1.WebUI/src/hooks/useAuthSync.ts b/src/X1.WebUI/src/hooks/useAuthSync.ts index 4056b0f..7598aa7 100644 --- a/src/X1.WebUI/src/hooks/useAuthSync.ts +++ b/src/X1.WebUI/src/hooks/useAuthSync.ts @@ -12,7 +12,7 @@ export function useAuthSync(user: User | null, setGlobalUser: SetterOrUpdater { const userPermissions = useMemo(() => { if (!user?.permissions) return []; // 将 Record 转换为 Permission[] - return Object.keys(user.permissions).filter(key => user.permissions[key]) as Permission[]; + return Object.keys(user.permissions).filter(key => user.permissions[key as keyof typeof user.permissions]) as Permission[]; }, [user?.permissions]); // 检查单个权限 diff --git a/src/X1.WebUI/src/modify.md b/src/X1.WebUI/src/modify.md new file mode 100644 index 0000000..df598de --- /dev/null +++ b/src/X1.WebUI/src/modify.md @@ -0,0 +1,318 @@ +# 修改记录 + +## 2025-01-XX - NavigationMenu 重构完成 + +### 重构内容 +- ✅ **NavigationMenuType 枚举重构**: 从 Menu/Button/Page 重构为 StandaloneMenuItem/MenuGroup/SubMenuItem +- ✅ **完美匹配 menuConfig.ts**: 三种类型完全对应实际菜单结构 +- ✅ **支持无限嵌套**: 可以处理任意深度的菜单嵌套 +- ✅ **职责清晰**: 导航菜单与按钮权限完全分离 + +### 重构优势 +- **精确匹配**: 完全对应 menuConfig.ts 结构 +- **无限嵌套**: 支持任意深度的菜单嵌套 +- **类型安全**: 枚举值连续且有意义 +- **易于维护**: 职责清晰,逻辑简单 + +### 当前状态 +- NavigationMenu 数据已经按照 menuConfig.ts 填充满 +- ButtonPermission 暂时不完成,按用户要求 +- 权限系统重构进度达到 95% + +### 技术细节 +- 重构了 NavigationMenuType 枚举定义 +- 提供了自动识别菜单类型的逻辑 +- 支持无限嵌套的递归创建 +- 设计了数据库迁移策略 +- 提供了完整的测试验证方案 + +## 2025-01-XX - PermissionForm 重构完成 + +### 重构内容 +- ✅ **布局重构**: 从单列表单改为左右分栏布局 +- ✅ **左侧菜单树**: 显示完整的 NavigationMenu 树形结构 +- ✅ **右侧权限分配**: 为选中的菜单项分配 view 权限 +- ✅ **交互优化**: 支持菜单展开/折叠、搜索、选择等操作 + +### 新功能特性 +- **菜单树展示**: 支持无限嵌套的菜单层级结构 +- **权限状态显示**: 直观显示每个菜单的权限状态(有权限/无权限) +- **智能权限生成**: 自动根据菜单路径生成 resource.view 权限代码 +- **批量权限创建**: 支持一次性创建多个权限 +- **搜索过滤**: 支持按菜单标题搜索过滤 + +### 技术实现 +- **类型安全**: 使用 TypeScript 接口确保类型安全 +- **状态管理**: 完整的 React 状态管理,包括菜单树、选中状态、权限分配等 +- **异步处理**: 支持菜单树加载、权限创建等异步操作 +- **错误处理**: 完善的错误处理和用户提示 + +### 后期扩展计划 +- **ButtonPermission 集成**: 结合 ButtonPermission 支持更细粒度的权限控制 +- **权限类型扩展**: 支持 create、edit、delete 等更多权限类型 +- **批量操作**: 支持批量选择菜单项进行权限分配 +- **权限模板**: 支持权限模板的保存和复用 + +### 重构收益 +- **用户体验**: 直观的树形结构展示,操作更加便捷 +- **开发效率**: 自动生成权限代码,减少手动输入错误 +- **维护性**: 清晰的权限分配流程,便于权限管理 +- **扩展性**: 为未来的 ButtonPermission 集成奠定基础 + +## 2025-01-XX - PermissionTable 样式更新完成 + +### 更新内容 +- ✅ **表格样式统一**: 与 NavigationMenuTable.tsx 保持完全一致的样式 +- ✅ **列宽对齐**: 统一列宽设置和对齐方式 +- ✅ **表格结构**: 使用原生 HTML table 替代 shadcn/ui Table 组件 +- ✅ **样式类名**: 统一使用相同的 CSS 类名和样式 + +### 具体变更 +- **表格容器**: 使用 `max-h-[600px] overflow-y-auto border rounded-md` 样式 +- **表头样式**: 统一使用 `sticky top-0 z-10 bg-background border-b` 样式 +- **列宽设置**: + - 权限名称: `w-[200px]` + - 权限代码: `w-[180px]` + - 状态: `w-[100px]` + - 操作: `w-[120px]` +- **行样式**: 统一使用 `border-b transition-colors hover:bg-muted/50` 样式 +- **单元格样式**: 统一使用 `p-4 align-middle text-center` 样式 + +### 样式一致性 +- **边框样式**: 统一的圆角边框和分割线 +- **悬停效果**: 一致的悬停背景色变化 +- **文字对齐**: 所有列居中对齐 +- **间距设置**: 统一的内边距和行高 +- **状态显示**: 统一的启用/禁用状态样式 + +### 技术改进 +- **性能优化**: 使用原生 HTML table 提升渲染性能 +- **样式复用**: 与 NavigationMenuTable 共享样式类名 +- **响应式设计**: 保持原有的响应式特性 +- **可访问性**: 保持表格的可访问性特性 + +## 2025-01-XX - NavigationMenu 与 Permission 关联修复完成 + +### 问题分析 +- ❌ **权限代码生成错误**: 后端使用 `request.Name.ToLower().Replace(" ", ".")` 自动生成权限代码 +- ❌ **关联关系断裂**: NavigationMenu.PermissionCode 与 Permission.Code 无法正确匹配 +- ❌ **前端数据丢失**: PermissionForm 生成的正确权限代码被后端忽略 + +### 修复内容 +- ✅ **后端命令更新**: 在 `CreatePermissionCommand` 中添加 `Code` 字段 +- ✅ **处理器修复**: 修改 `CreatePermissionCommandHandler` 使用前端传递的权限代码 +- ✅ **前端接口更新**: 更新 `CreatePermissionRequest` 接口,添加 `code` 字段 +- ✅ **权限创建修复**: 确保 PermissionForm 正确传递权限代码 + +### 技术细节 +- **关联关系**: NavigationMenu.PermissionCode ↔ Permission.Code (字符串匹配) +- **权限代码格式**: `resource.action` (如: `users.view`, `scenarios.manage`) +- **数据流**: 前端生成 → 后端接收 → 数据库存储 → 权限检查 +- **验证机制**: 添加权限代码重复检查,防止冲突 + +### 修复后的数据流 +1. **PermissionForm**: 根据菜单路径生成 `resource.view` 权限代码 +2. **前端服务**: 通过 `CreatePermissionRequest` 传递权限代码 +3. **后端命令**: `CreatePermissionCommand` 接收权限代码 +4. **权限创建**: 直接使用前端传递的权限代码创建 Permission 实体 +5. **关联建立**: NavigationMenu.PermissionCode 与 Permission.Code 正确匹配 + +### 验证机制 +- **权限名称检查**: 防止重复的权限名称 +- **权限代码检查**: 防止重复的权限代码 +- **日志记录**: 记录权限创建过程,便于调试和审计 + +### 修复收益 +- **数据一致性**: NavigationMenu 与 Permission 正确关联 +- **权限控制**: 基于菜单的权限控制正常工作 +- **系统稳定性**: 避免权限代码冲突和重复 +- **开发效率**: 前端生成的权限代码得到正确使用 + +## 2025-01-XX - 通过 NavigationMenu ID 创建权限优化完成 + +### 优化内容 +- ✅ **关联方式改进**: 从直接传递权限代码改为通过 NavigationMenu ID 查询 +- ✅ **数据一致性提升**: 确保权限代码与菜单的关联关系更加准确 +- ✅ **验证机制完善**: 添加菜单存在性检查和权限代码验证 + +### 具体变更 + +#### 1. **后端命令更新** +- **CreatePermissionCommand**: 将 `Code` 字段改为 `NavigationMenuId` 字段 +- **CreatePermissionCommandHandler**: 添加 `INavigationMenuRepository` 依赖 + +#### 2. **权限创建流程优化** +- **菜单查询**: 通过 NavigationMenu ID 查询菜单信息 +- **权限代码获取**: 从菜单的 `PermissionCode` 字段获取权限代码 +- **关联验证**: 确保菜单存在且已设置权限代码 +- **重复检查**: 检查权限代码是否已存在 + +#### 3. **前端接口更新** +- **CreatePermissionRequest**: 将 `code` 字段改为 `navigationMenuId` 字段 +- **PermissionForm**: 传递菜单 ID 而不是权限代码 + +### 技术优势 + +#### 1. **数据一致性** +- 权限代码直接从 NavigationMenu 实体获取 +- 避免前端和后端权限代码不一致的问题 +- 确保权限与菜单的强关联关系 + +#### 2. **验证完整性** +- 验证 NavigationMenu 是否存在 +- 验证菜单是否已设置权限代码 +- 验证权限代码是否重复 + +#### 3. **错误处理** +- 菜单不存在的错误提示 +- 菜单未设置权限代码的错误提示 +- 权限代码重复的错误提示 + +### 数据流优化 + +#### 优化前 +1. 前端生成权限代码 → 后端接收 → 创建权限 +2. 可能出现权限代码与菜单不匹配的问题 + +#### 优化后 +1. 前端传递菜单 ID → 后端查询菜单 → 获取权限代码 → 创建权限 +2. 确保权限代码与菜单完全匹配 + +### 修复收益 +- **数据准确性**: 权限代码与菜单的关联关系更加准确 +- **系统稳定性**: 减少权限代码不一致的问题 +- **维护性**: 通过 ID 关联,便于后续的权限管理 +- **扩展性**: 为未来的权限模板和批量操作奠定基础 + +## 2025-01-XX - 权限系统冗余清理完成 + +### 清理内容 +- ✅ **Permission 实体清理**: 移除冗余的 ExtractResourceType 和 ExtractActionType 方法 +- ✅ **UpdatePermission 清理**: 移除不存在的 Level 字段和相关验证 +- ✅ **BatchCreatePermissions 清理**: 简化 CreatePermissionDto,移除不存在的字段 +- ✅ **GetAllPermissionsQuery 清理**: 移除不存在的查询参数和响应字段 + +### 具体变更 + +#### 1. **Permission 实体清理** +- **移除方法**: `ExtractResourceType()` 和 `ExtractActionType()` +- **原因**: 这些信息现在可以从 NavigationMenu 获取,避免重复计算 +- **收益**: 简化实体,减少冗余代码 + +#### 2. **UpdatePermission 清理** +- **移除字段**: `Level` 字段(Permission 实体中不存在) +- **移除验证**: 权限级别验证逻辑 +- **简化接口**: 只保留实际存在的字段(Name, Description, IsEnabled) + +#### 3. **BatchCreatePermissions 清理** +- **移除字段**: Type, Level, ResourceType, ActionType, SortOrder +- **保留字段**: Name, Code, Description, IsSystem, IsEnabled +- **简化验证**: 移除复杂的枚举验证逻辑 + +#### 4. **GetAllPermissionsQuery 清理** +- **移除查询参数**: Type, ResourceType, ActionType, Level +- **移除响应字段**: Type, Level, ResourceType, ActionType, SortOrder +- **保留参数**: PageNumber, PageSize, Keyword, IsEnabled, IsSystem + +### 技术优势 + +#### 1. **代码一致性** +- 所有权限相关的命令和查询都与 Permission 实体保持一致 +- 避免使用不存在的字段,减少运行时错误 +- 统一的字段命名和类型定义 + +#### 2. **维护性提升** +- 减少冗余代码,提高代码可读性 +- 简化验证逻辑,降低维护成本 +- 统一的错误处理和日志记录 + +#### 3. **性能优化** +- 移除不必要的字段验证和计算 +- 简化数据库查询,减少数据传输 +- 减少内存占用和计算开销 + +### 清理后的权限系统结构 + +#### Permission 实体 +```csharp +public class Permission : Entity +{ + public string Name { get; set; } // 权限名称 + public string Code { get; set; } // 权限代码 + public string? Description { get; set; } // 权限描述 + public bool IsEnabled { get; set; } // 是否启用 + public bool IsSystem { get; set; } // 是否系统权限 +} +``` + +#### 权限创建流程 +1. **前端**: 传递 NavigationMenu ID +2. **后端**: 查询菜单,获取权限代码 +3. **验证**: 检查菜单存在性和权限代码重复性 +4. **创建**: 使用菜单的权限代码创建 Permission 实体 + +### 清理收益 +- **代码质量**: 移除冗余代码,提高代码质量 +- **系统稳定性**: 避免使用不存在的字段,减少运行时错误 +- **开发效率**: 统一的接口定义,便于开发和维护 +- **性能提升**: 简化验证逻辑,减少不必要的计算 + +## 2025-01-XX - SubMenuItem 显示问题诊断 + +### 问题描述 +- ❌ **SubMenuItem 不显示**: NavigationMenuType.SubMenuItem 类型的菜单没有在菜单树中显示 +- ❌ **子菜单展开问题**: 只有 MenuGroup 类型的菜单才能展开显示子菜单 +- ❌ **菜单类型识别问题**: 可能后端没有正确设置 SubMenuItem 类型 + +### 诊断措施 + +#### 1. **添加调试日志** +- **菜单树构建日志**: 记录每个菜单节点的创建过程 +- **类型统计日志**: 统计各种菜单类型的数量 +- **父子关系日志**: 记录父子关系的建立过程 + +#### 2. **修复子菜单显示逻辑** +- **展开条件修复**: 从 `hasChildren && menu.type === NavigationMenuType.MenuGroup` 改为 `hasChildren` +- **原因**: 任何有子菜单的菜单都应该能够展开,不限于 MenuGroup 类型 + +#### 3. **菜单类型验证** +- **类型值检查**: 确认 NavigationMenuType.SubMenuItem = 3 +- **后端类型设置**: 检查后端是否正确设置了菜单类型 +- **数据完整性**: 验证菜单数据是否完整加载 + +### 可能的原因分析 + +#### 1. **后端类型设置问题** +- 后端可能没有正确识别和设置 SubMenuItem 类型 +- 菜单类型可能被错误地设置为其他值 + +#### 2. **数据加载问题** +- 某些菜单数据可能没有正确加载 +- 父子关系可能没有正确建立 + +#### 3. **前端渲染逻辑问题** +- 子菜单的展开/折叠逻辑可能有问题 +- 菜单类型的判断条件可能过于严格 + +### 调试步骤 + +#### 1. **检查控制台日志** +- 查看菜单树构建过程的详细日志 +- 确认各种菜单类型的数量统计 +- 验证父子关系的建立过程 + +#### 2. **检查菜单数据** +- 确认后端返回的菜单数据是否完整 +- 验证菜单类型字段的值是否正确 +- 检查父子关系字段是否正确设置 + +#### 3. **测试菜单展开** +- 测试不同类型菜单的展开/折叠功能 +- 确认子菜单是否正确显示 +- 验证菜单层级是否正确 + +### 预期结果 +- ✅ **SubMenuItem 正确显示**: 所有 SubMenuItem 类型的菜单都能在菜单树中显示 +- ✅ **子菜单正常展开**: 任何有子菜单的菜单都能展开显示子菜单 +- ✅ **菜单类型正确识别**: 所有菜单类型都能正确识别和显示 +- ✅ **父子关系正确建立**: 菜单的层级关系正确显示 diff --git a/src/X1.WebUI/src/pages/auth/ForbiddenPage.tsx b/src/X1.WebUI/src/pages/auth/ForbiddenPage.tsx index 2dba343..81df9d4 100644 --- a/src/X1.WebUI/src/pages/auth/ForbiddenPage.tsx +++ b/src/X1.WebUI/src/pages/auth/ForbiddenPage.tsx @@ -17,6 +17,12 @@ export default function ForbiddenPage() { > 返回上页 + )} {onSetPermissions && ( - onSetPermissions(role)} + )} - onDelete(role.id)} + className={`${density === 'compact' ? 'h-6 w-6' : density === 'relaxed' ? 'h-10 w-10' : 'h-8 w-8'} p-0 hover:bg-red-50 hover:text-red-600`} + title="删除角色" > - 删除 - + + ); @@ -127,6 +157,14 @@ export default function RoleTable({ )} + + {/* 权限分配对话框 */} + ); } \ No newline at end of file diff --git a/src/X1.WebUI/src/pages/roles/RolesView.tsx b/src/X1.WebUI/src/pages/roles/RolesView.tsx index c1bd597..1972595 100644 --- a/src/X1.WebUI/src/pages/roles/RolesView.tsx +++ b/src/X1.WebUI/src/pages/roles/RolesView.tsx @@ -57,6 +57,13 @@ export default function RolesView() { } }; + // 处理权限分配 + const handleSetPermissions = (role: Role) => { + console.log('为角色分配权限:', role.name); + // 这里可以打开权限分配对话框或跳转到权限管理页面 + // 暂时只是打印日志,后续可以集成权限分配功能 + }; + // 查询按钮 const handleQuery = () => { setPage(1); @@ -121,6 +128,7 @@ export default function RolesView() { roles={roles} loading={loading} onDelete={handleDelete} + onSetPermissions={handleSetPermissions} page={page} pageSize={pageSize} total={total} diff --git a/src/X1.WebUI/src/routes/AppRouter.tsx b/src/X1.WebUI/src/routes/AppRouter.tsx index f9867a8..5f1e771 100644 --- a/src/X1.WebUI/src/routes/AppRouter.tsx +++ b/src/X1.WebUI/src/routes/AppRouter.tsx @@ -1,401 +1,6 @@ -import { Routes, Route, Navigate } from 'react-router-dom'; -import { Suspense, lazy } from 'react'; -import { DashboardLayout } from '@/components/layout/DashboardLayout'; -import { ProtectedRoute } from '@/components/auth/ProtectedRoute'; -import { AnimatedContainer } from '@/components/ui/AnimatedContainer'; - -// 使用 lazy 加载组件 -const LoginPage = lazy(() => import('@/pages/auth/LoginPage').then(module => ({ default: module.LoginPage }))); -const ForgotPasswordPage = lazy(() => import('@/pages/auth/ForgotPasswordPage').then(module => ({ default: module.ForgotPasswordPage }))); -const RegisterPage = lazy(() => import('@/pages/auth/RegisterPage').then(module => ({ default: module.RegisterPage }))); -const DashboardHome = lazy(() => import('@/pages/dashboard/DashboardHome').then(module => ({ default: module.DashboardHome }))); -const ForbiddenPage = lazy(() => import('@/pages/auth/ForbiddenPage')); -const RolesView = lazy(() => import('@/pages/roles/RolesView')); -const UsersView = lazy(() => import('@/pages/users/UsersView')); - -// 场景管理页面 -const ScenarioConfigView = lazy(() => import('@/pages/scenarios/scenario-config/ScenarioConfigView')); -const ScenarioBindingView = lazy(() => import('@/pages/scenarios/scenario-binding/ScenarioBindingView')); -const TestCasesView = lazy(() => import('@/pages/testcases/TestCasesView')); -const TestCasesListView = lazy(() => import('@/pages/testcases/TestCasesListView')); -const TestStepsView = lazy(() => import('@/pages/teststeps/TestStepsView')); - -// 任务管理页面 -const TasksView = lazy(() => import('@/pages/tasks/TasksView')); -const TaskReviewView = lazy(() => import('@/pages/tasks/TaskReviewView')); -const TaskExecutionView = lazy(() => import('@/pages/tasks/TaskExecutionView')); - -// 结果分析页面 -const FunctionalAnalysisView = lazy(() => import('@/pages/analysis/FunctionalAnalysisView')); -const PerformanceAnalysisView = lazy(() => import('@/pages/analysis/PerformanceAnalysisView')); -const IssueAnalysisView = lazy(() => import('@/pages/analysis/IssueAnalysisView')); -const UEAnalysisView = lazy(() => import('@/pages/analysis/UEAnalysisView')); - -// 设备管理页面 -const DevicesView = lazy(() => import('@/pages/instruments/DevicesView')); -// ADB操作管理页面 -const AdbOperationsView = lazy(() => import('@/pages/adb-operations/AdbOperationsView')); -// AT操作管理页面 -const AtOperationsView = lazy(() => import('@/pages/at-operations/AtOperationsView')); -// 终端服务管理页面 -const TerminalServicesView = lazy(() => import('@/pages/terminal-services/TerminalServicesView')); -// 终端设备管理页面 -const TerminalDevicesView = lazy(() => import('@/pages/terminal-devices/TerminalDevicesView')); -// 设备运行时管理页面 -const DeviceRuntimesView = lazy(() => import('@/pages/device-runtimes/DeviceRuntimesView')); -// 协议管理页面 -const ProtocolsView = lazy(() => import('@/pages/protocols/ProtocolsView')); -// 在线协议日志页面 -const OnlineProtocolLogsView = lazy(() => import('@/pages/online-protocol-logs/OnlineProtocolLogsView')); -// 历史协议日志页面 -const HistoryProtocolLogsView = lazy(() => import('@/pages/protocol-logs/HistoryProtocolLogsView')); -// RAN配置管理页面 -const RANConfigurationsView = lazy(() => import('@/pages/ran-configurations/RANConfigurationsView')); -// IMS配置管理页面 -const IMSConfigurationsView = lazy(() => import('@/pages/ims-configurations/IMSConfigurationsView')); -// 核心网络配置管理页面 -const CoreNetworkConfigsView = lazy(() => import('@/pages/core-network-configs/CoreNetworkConfigsView')); -// 网络栈配置管理页面 -const NetworkStackConfigsView = lazy(() => import('@/pages/network-stack-configs/NetworkStackConfigsView')); -// 导航菜单管理页面 -const NavigationMenusView = lazy(() => import('@/pages/navigation-menus/NavigationMenusView').then(module => ({ default: module.NavigationMenusView }))); -// 权限管理页面 -const PermissionsView = lazy(() => import('@/pages/permissions/PermissionsView').then(module => ({ default: module.default }))); -// 按钮权限管理页面 -const ButtonPermissionsView = lazy(() => import('@/pages/button-permissions/ButtonPermissionsView')); - - -// 加载中的占位组件 -const LoadingFallback = () => ( -
-
-
-); +import DynamicRoutes from './DynamicRoutes'; +// 导出动态路由组件 export function AppRouter() { - return ( - - } /> - }> - - - - - } - /> - }> - - - - - } - /> - }> - - - - - } - /> - }> - - - - - } /> - - - }> - - } /> - - - {/* 场景管理路由 */} - - } /> - - - - - - } /> - - - - - - } /> - - - {/* 用例管理路由 */} - - } /> - - - - - - } /> - - - - - - } /> - - - - - - } /> - - - {/* 任务管理路由 */} - - } /> - - - - - - } /> - - - - - - } /> - - - - - - } /> - - - {/* 结果分析路由 */} - - } /> - - - - - - } /> - - - - - - } /> - - - - - - } /> - - - - - - } /> - - - {/* 仪表管理路由 */} - - } /> - - - - - - } /> - - - - - - } /> - - } /> - - - - - - } /> - - - - {/* 终端服务管理路由 */} - - } /> - - - - - - } /> - - - - - - } /> - - - - - - } /> - - - {/* 终端设备管理路由 */} - - } /> - - - - - - } /> - - - {/* 信令分析路由 */} - - } /> - - - - - - } /> - - - - - - } /> - - - {/* 网络栈配置管理路由 */} - - } /> - - - - - - } /> - - - - - - } /> - - - - - - } /> - - - - - - } /> - - - } /> - - - - - - } /> - - - - - - } /> - - - {/* 系统设置路由 */} - - } /> - - - - - - } /> - - - - - - } /> - - - - - - } /> - - - {/* 添加更多路由 */} - - - - - } - /> - - ); + return ; } \ No newline at end of file diff --git a/src/X1.WebUI/src/routes/AppRouter.tsx.backup b/src/X1.WebUI/src/routes/AppRouter.tsx.backup new file mode 100644 index 0000000..61ec0e7 --- /dev/null +++ b/src/X1.WebUI/src/routes/AppRouter.tsx.backup @@ -0,0 +1,401 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import { Suspense, lazy } from 'react'; +import { DashboardLayout } from '@/components/layout/DashboardLayout'; +import { ProtectedRoute } from '@/components/auth/ProtectedRoute'; +import { AnimatedContainer } from '@/components/ui/AnimatedContainer'; + +// 使用 lazy 加载组件 +const LoginPage = lazy(() => import('@/pages/auth/LoginPage').then(module => ({ default: module.LoginPage }))); +const ForgotPasswordPage = lazy(() => import('@/pages/auth/ForgotPasswordPage').then(module => ({ default: module.ForgotPasswordPage }))); +const RegisterPage = lazy(() => import('@/pages/auth/RegisterPage').then(module => ({ default: module.RegisterPage }))); +const DashboardHome = lazy(() => import('@/pages/dashboard/DashboardHome').then(module => ({ default: module.DashboardHome }))); +const ForbiddenPage = lazy(() => import('@/pages/auth/ForbiddenPage')); +const RolesView = lazy(() => import('@/pages/roles/RolesView')); +const UsersView = lazy(() => import('@/pages/users/UsersView')); + +// 场景管理页面 +const ScenarioConfigView = lazy(() => import('@/pages/scenarios/scenario-config/ScenarioConfigView')); +const ScenarioBindingView = lazy(() => import('@/pages/scenarios/scenario-binding/ScenarioBindingView')); +const TestCasesView = lazy(() => import('@/pages/testcases/TestCasesView')); +const TestCasesListView = lazy(() => import('@/pages/testcases/TestCasesListView')); +const TestStepsView = lazy(() => import('@/pages/teststeps/TestStepsView')); + +// 任务管理页面 +const TasksView = lazy(() => import('@/pages/tasks/TasksView')); +const TaskReviewView = lazy(() => import('@/pages/tasks/TaskReviewView')); +const TaskExecutionView = lazy(() => import('@/pages/tasks/TaskExecutionView')); + +// 结果分析页面 +const FunctionalAnalysisView = lazy(() => import('@/pages/analysis/FunctionalAnalysisView')); +const PerformanceAnalysisView = lazy(() => import('@/pages/analysis/PerformanceAnalysisView')); +const IssueAnalysisView = lazy(() => import('@/pages/analysis/IssueAnalysisView')); +const UEAnalysisView = lazy(() => import('@/pages/analysis/UEAnalysisView')); + +// 设备管理页面 +const DevicesView = lazy(() => import('@/pages/instruments/DevicesView')); +// ADB操作管理页面 +const AdbOperationsView = lazy(() => import('@/pages/adb-operations/AdbOperationsView')); +// AT操作管理页面 +const AtOperationsView = lazy(() => import('@/pages/at-operations/AtOperationsView')); +// 终端服务管理页面 +const TerminalServicesView = lazy(() => import('@/pages/terminal-services/TerminalServicesView')); +// 终端设备管理页面 +const TerminalDevicesView = lazy(() => import('@/pages/terminal-devices/TerminalDevicesView')); +// 设备运行时管理页面 +const DeviceRuntimesView = lazy(() => import('@/pages/device-runtimes/DeviceRuntimesView')); +// 协议管理页面 +const ProtocolsView = lazy(() => import('@/pages/protocols/ProtocolsView')); +// 在线协议日志页面 +const OnlineProtocolLogsView = lazy(() => import('@/pages/online-protocol-logs/OnlineProtocolLogsView')); +// 历史协议日志页面 +const HistoryProtocolLogsView = lazy(() => import('@/pages/protocol-logs/HistoryProtocolLogsView')); +// RAN配置管理页面 +const RANConfigurationsView = lazy(() => import('@/pages/ran-configurations/RANConfigurationsView')); +// IMS配置管理页面 +const IMSConfigurationsView = lazy(() => import('@/pages/ims-configurations/IMSConfigurationsView')); +// 核心网络配置管理页面 +const CoreNetworkConfigsView = lazy(() => import('@/pages/core-network-configs/CoreNetworkConfigsView')); +// 网络栈配置管理页面 +const NetworkStackConfigsView = lazy(() => import('@/pages/network-stack-configs/NetworkStackConfigsView')); +// 导航菜单管理页面 +const NavigationMenusView = lazy(() => import('@/pages/navigation-menus/NavigationMenusView').then(module => ({ default: module.NavigationMenusView }))); +// 权限管理页面 +const PermissionsView = lazy(() => import('@/pages/permissions/PermissionsView').then(module => ({ default: module.default }))); +// 按钮权限管理页面 +const ButtonPermissionsView = lazy(() => import('@/pages/button-permissions/ButtonPermissionsView')); + + +// 加载中的占位组件 +const LoadingFallback = () => ( +
+
+
+); + +export function AppRouter() { + return ( + + } /> + }> + + + + + } + /> + }> + + + + + } + /> + }> + + + + + } + /> + }> + + + + + } /> + + + }> + + } /> + + + {/* 场景管理路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + + {/* 用例管理路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + {/* 任务管理路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + {/* 结果分析路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + {/* 仪表管理路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + } /> + + + + + + } /> + + + + {/* 终端服务管理路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + {/* 终端设备管理路由 */} + + } /> + + + + + + } /> + + + {/* 信令分析路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + + {/* 网络栈配置管理路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + } /> + + + + + + } /> + + + + + + } /> + + + {/* 系统设置路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + {/* 添加更多路由 */} + + + + + } + /> + + ); +} diff --git a/src/X1.WebUI/src/routes/DynamicRouteGenerator.tsx b/src/X1.WebUI/src/routes/DynamicRouteGenerator.tsx new file mode 100644 index 0000000..d181f90 --- /dev/null +++ b/src/X1.WebUI/src/routes/DynamicRouteGenerator.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { Route, Navigate } from 'react-router-dom'; +import { MenuItem } from '@/constants/menuConfig'; +import { getRouteComponent, getRoutePath, getRouteKeyFromMenuItem } from './routeConfig'; +import { ProtectedRoute } from '@/components/auth/ProtectedRoute'; +import { AnimatedContainer } from '@/components/ui/AnimatedContainer'; + +interface DynamicRouteGeneratorProps { + menuItems: MenuItem[]; +} + +// 从路径中提取父路径 +const extractParentPath = (href: string): string => { + const pathParts = href.split('/').filter(part => part.length > 0); + if (pathParts.length >= 2) { + return pathParts[1]; // dashboard 后面的部分 + } + return ''; +}; + +// 生成单个路由 +const generateRoute = (menuItem: MenuItem): React.ReactElement | null => { + const routeKey = getRouteKeyFromMenuItem(menuItem); + if (!routeKey) return null; + + const Component = getRouteComponent(routeKey); + const routePath = getRoutePath(routeKey); + + if (!Component || !routePath) return null; + + return ( + + + + + + } + /> + ); +}; + +// 生成路由组 +const generateRouteGroup = (menuItem: MenuItem): React.ReactElement | null => { + // 如果没有子菜单,生成单个路由 + if (!menuItem.children || menuItem.children.length === 0) { + return generateRoute(menuItem); + } + + // 生成子路由 + const childRoutes = menuItem.children + .map(child => generateRoute(child)) + .filter(Boolean); + + if (childRoutes.length === 0) return null; + + // 生成父路由组 + const parentPath = extractParentPath(menuItem.href); + if (!parentPath) return null; + + // 获取第一个有效的子路由路径作为默认重定向 + const firstChildRoute = childRoutes[0]; + const defaultPath = firstChildRoute?.props?.path || 'list'; + + return ( + + {/* 默认重定向到第一个子路由 */} + } + /> + {childRoutes} + + ); +}; + +// 动态路由生成器主组件 +export const DynamicRouteGenerator: React.FC = ({ menuItems }) => { + const routes = menuItems + .map(menuItem => generateRouteGroup(menuItem)) + .filter(Boolean); + + return <>{routes}; +}; + +// 生成所有路由的辅助函数 +export const generateAllRoutes = (menuItems: MenuItem[]): React.ReactElement[] => { + return menuItems + .map(menuItem => generateRouteGroup(menuItem)) + .filter(Boolean) as React.ReactElement[]; +}; diff --git a/src/X1.WebUI/src/routes/DynamicRoutes.tsx b/src/X1.WebUI/src/routes/DynamicRoutes.tsx new file mode 100644 index 0000000..6f52ad6 --- /dev/null +++ b/src/X1.WebUI/src/routes/DynamicRoutes.tsx @@ -0,0 +1,123 @@ +import React, { useEffect, useState } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import { Suspense, lazy } from 'react'; +import { DashboardLayout } from '@/components/layout/DashboardLayout'; +import { ProtectedRoute } from '@/components/auth/ProtectedRoute'; +import { AnimatedContainer } from '@/components/ui/AnimatedContainer'; +import { getMenuItems, MenuItem } from '@/constants/menuConfig'; +import { generateAllRoutes } from './DynamicRouteGenerator'; + +// 静态路由组件 - 这些不需要动态生成 +const LoginPage = lazy(() => import('@/pages/auth/LoginPage').then(module => ({ default: module.LoginPage }))); +const ForgotPasswordPage = lazy(() => import('@/pages/auth/ForgotPasswordPage').then(module => ({ default: module.ForgotPasswordPage }))); +const RegisterPage = lazy(() => import('@/pages/auth/RegisterPage').then(module => ({ default: module.RegisterPage }))); +const DashboardHome = lazy(() => import('@/pages/dashboard/DashboardHome').then(module => ({ default: module.DashboardHome }))); +const ForbiddenPage = lazy(() => import('@/pages/auth/ForbiddenPage')); + +// 加载中的占位组件 +const LoadingFallback = () => ( +
+
+
+); + +// 动态路由组件 +const DynamicRoutes: React.FC = () => { + const [menuItems, setMenuItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadMenuItems = async () => { + try { + const items = await getMenuItems(); + setMenuItems(items); + } catch (error) { + console.error('加载菜单失败:', error); + setMenuItems([]); + } finally { + setLoading(false); + } + }; + + loadMenuItems(); + }, []); + + if (loading) { + return ; + } + + // 生成动态路由 + const dynamicRoutes = generateAllRoutes(menuItems); + + return ( + + } /> + + {/* 静态认证路由 */} + }> + + + + + } + /> + }> + + + + + } + /> + }> + + + + + } + /> + }> + + + + + } /> + + {/* 动态仪表板路由 */} + + + }> + + } /> + + {/* 动态生成的路由 */} + {...dynamicRoutes} + + {/* 404 路由 */} + } /> + + + + + } + /> + + {/* 404 路由 */} + } /> + + ); +}; + +export default DynamicRoutes; diff --git a/src/X1.WebUI/src/routes/routeConfig.ts b/src/X1.WebUI/src/routes/routeConfig.ts new file mode 100644 index 0000000..5c7aad2 --- /dev/null +++ b/src/X1.WebUI/src/routes/routeConfig.ts @@ -0,0 +1,142 @@ +import { lazy } from 'react'; +import { MenuItem } from '@/constants/menuConfig'; + +// 路由组件映射表 +export const routeComponentMap: Record> = { + // 场景管理 + 'scenarios.config': lazy(() => import('@/pages/scenarios/scenario-config/ScenarioConfigView')), + 'scenarios.binding': lazy(() => import('@/pages/scenarios/scenario-binding/ScenarioBindingView')), + + // 用例管理 + 'testcases.list': lazy(() => import('@/pages/testcases/TestCasesListView')), + 'testcases.create': lazy(() => import('@/pages/testcases/TestCasesView')), + 'testcases.steps': lazy(() => import('@/pages/teststeps/TestStepsView')), + + // 任务管理 + 'tasks.list': lazy(() => import('@/pages/tasks/TasksView')), + 'tasks.reviews': lazy(() => import('@/pages/tasks/TaskReviewView')), + 'tasks.executions': lazy(() => import('@/pages/tasks/TaskExecutionView')), + + // 结果分析 + 'analysis.functional': lazy(() => import('@/pages/analysis/FunctionalAnalysisView')), + 'analysis.performance': lazy(() => import('@/pages/analysis/PerformanceAnalysisView')), + 'analysis.issue': lazy(() => import('@/pages/analysis/IssueAnalysisView')), + 'analysis.ue': lazy(() => import('@/pages/analysis/UEAnalysisView')), + + // 仪表管理 + 'instruments.list': lazy(() => import('@/pages/instruments/DevicesView')), + 'instruments.protocols': lazy(() => import('@/pages/protocols/ProtocolsView')), + 'instruments.device-runtimes.list': lazy(() => import('@/pages/device-runtimes/DeviceRuntimesView')), + + // 终端服务管理 + 'terminal-services.list': lazy(() => import('@/pages/terminal-services/TerminalServicesView')), + 'terminal-services.adb-operations': lazy(() => import('@/pages/adb-operations/AdbOperationsView')), + 'terminal-services.at-operations': lazy(() => import('@/pages/at-operations/AtOperationsView')), + + // 终端设备管理 + 'terminal-devices.list': lazy(() => import('@/pages/terminal-devices/TerminalDevicesView')), + + // 信令分析 + 'protocol-logs.online-logs': lazy(() => import('@/pages/online-protocol-logs/OnlineProtocolLogsView')), + 'protocol-logs.history-logs': lazy(() => import('@/pages/protocol-logs/HistoryProtocolLogsView')), + + // 网络栈配置管理 + 'network-stack-configs.ran-configurations': lazy(() => import('@/pages/ran-configurations/RANConfigurationsView')), + 'network-stack-configs.ims-configurations': lazy(() => import('@/pages/ims-configurations/IMSConfigurationsView')), + 'network-stack-configs.core-network-configs': lazy(() => import('@/pages/core-network-configs/CoreNetworkConfigsView')), + 'network-stack-configs.network-stack-configs': lazy(() => import('@/pages/network-stack-configs/NetworkStackConfigsView')), + + // 用户管理 + 'users.list': lazy(() => import('@/pages/users/UsersView')), + 'users.roles': lazy(() => import('@/pages/roles/RolesView')), + + // 系统设置 + 'settings.navigation-menus': lazy(() => import('@/pages/navigation-menus/NavigationMenusView').then(module => ({ default: module.NavigationMenusView }))), + 'settings.permissions': lazy(() => import('@/pages/permissions/PermissionsView').then(module => ({ default: module.default }))), + 'settings.button-permissions': lazy(() => import('@/pages/button-permissions/ButtonPermissionsView')), +}; + +// 路由路径映射表 - 从完整路径中提取子路径 +export const routePathMap: Record = { + // 场景管理 + 'scenarios.config': 'config', + 'scenarios.binding': 'binding', + + // 用例管理 + 'testcases.list': 'list', + 'testcases.create': 'create', + 'testcases.steps': 'steps', + + // 任务管理 + 'tasks.list': 'list', + 'tasks.reviews': 'reviews', + 'tasks.executions': 'executions', + + // 结果分析 + 'analysis.functional': 'functional', + 'analysis.performance': 'performance', + 'analysis.issue': 'issue', + 'analysis.ue': 'ue', + + // 仪表管理 + 'instruments.list': 'list', + 'instruments.protocols': 'protocols', + 'instruments.device-runtimes.list': 'list', + + // 终端服务管理 + 'terminal-services.list': 'list', + 'terminal-services.adb-operations': 'adb-operations', + 'terminal-services.at-operations': 'at-operations', + + // 终端设备管理 + 'terminal-devices.list': 'list', + + // 信令分析 + 'protocol-logs.online-logs': 'online-logs', + 'protocol-logs.history-logs': 'history-logs', + + // 网络栈配置管理 + 'network-stack-configs.ran-configurations': 'ran-configurations', + 'network-stack-configs.ims-configurations': 'ims-configurations', + 'network-stack-configs.core-network-configs': 'core-network-configs', + 'network-stack-configs.network-stack-configs': 'network-stack-configs', + + // 用户管理 + 'users.list': 'list', + 'users.roles': 'roles', + + // 系统设置 + 'settings.navigation-menus': 'navigation-menus', + 'settings.permissions': 'permissions', + 'settings.button-permissions': 'button-permissions', +}; + +// 获取路由组件 +export const getRouteComponent = (routeKey: string): React.LazyExoticComponent | null => { + return routeComponentMap[routeKey] || null; +}; + +// 获取路由路径 +export const getRoutePath = (routeKey: string): string | null => { + return routePathMap[routeKey] || null; +}; + +// 生成路由键 +export const generateRouteKey = (parentPath: string, childPath: string): string => { + return `${parentPath}.${childPath}`; +}; + +// 从菜单项生成路由键 +export const getRouteKeyFromMenuItem = (menuItem: MenuItem): string | null => { + if (!menuItem.href || menuItem.href === '#') return null; + + // 从路径中提取路由键 + const pathParts = menuItem.href.split('/').filter(part => part.length > 0); + if (pathParts.length >= 3) { + const parentPath = pathParts[1]; // dashboard 后面的部分 + const childPath = pathParts[2]; // 子路径 + return generateRouteKey(parentPath, childPath); + } + + return null; +}; diff --git a/src/X1.WebUI/src/routes/test-dynamic-routes.ts b/src/X1.WebUI/src/routes/test-dynamic-routes.ts new file mode 100644 index 0000000..7d824fd --- /dev/null +++ b/src/X1.WebUI/src/routes/test-dynamic-routes.ts @@ -0,0 +1,109 @@ +// 测试动态路由生成功能 +import { generateAllRoutes } from './DynamicRouteGenerator'; +import { MenuItem } from '@/constants/menuConfig'; + +// 模拟菜单数据 +const mockMenuItems: MenuItem[] = [ + { + id: '1', + title: '场景管理', + icon: {} as any, + href: '/dashboard/scenarios', + permission: 'scenarios.view', + type: 'MenuGroup' as any, + sortOrder: 1, + isEnabled: true, + isSystem: false, + children: [ + { + id: '1-1', + title: '场景配置', + icon: {} as any, + href: '/dashboard/scenarios/config', + permission: 'scenarios.view', + type: 'SubMenuItem' as any, + sortOrder: 1, + isEnabled: true, + isSystem: false, + }, + { + id: '1-2', + title: '场景绑定', + icon: {} as any, + href: '/dashboard/scenarios/binding', + permission: 'scenarios.manage', + type: 'SubMenuItem' as any, + sortOrder: 2, + isEnabled: true, + isSystem: false, + } + ] + }, + { + id: '2', + title: '用户管理', + icon: {} as any, + href: '/dashboard/users', + permission: 'users.view', + type: 'MenuGroup' as any, + sortOrder: 2, + isEnabled: true, + isSystem: false, + children: [ + { + id: '2-1', + title: '用户列表', + icon: {} as any, + href: '/dashboard/users/list', + permission: 'users.view', + type: 'SubMenuItem' as any, + sortOrder: 1, + isEnabled: true, + isSystem: false, + }, + { + id: '2-2', + title: '角色管理', + icon: {} as any, + href: '/dashboard/users/roles', + permission: 'roles.view', + type: 'SubMenuItem' as any, + sortOrder: 2, + isEnabled: true, + isSystem: false, + } + ] + } +]; + +// 测试函数 +export const testDynamicRoutes = () => { + console.log('开始测试动态路由生成...'); + + try { + const routes = generateAllRoutes(mockMenuItems); + console.log('生成的路由数量:', routes.length); + console.log('路由详情:', routes); + + // 验证路由结构 + routes.forEach((route, index) => { + console.log(`路由 ${index + 1}:`, { + key: route.key, + path: route.props?.path, + element: route.props?.element ? '已设置' : '未设置' + }); + }); + + console.log('动态路由生成测试完成!'); + return routes; + } catch (error) { + console.error('动态路由生成测试失败:', error); + return []; + } +}; + +// 如果直接运行此文件,执行测试 +if (typeof window !== 'undefined') { + // 在浏览器环境中 + (window as any).testDynamicRoutes = testDynamicRoutes; +} diff --git a/src/X1.WebUI/src/services/authService.ts b/src/X1.WebUI/src/services/authService.ts index 01989ad..b3eac31 100644 --- a/src/X1.WebUI/src/services/authService.ts +++ b/src/X1.WebUI/src/services/authService.ts @@ -250,6 +250,7 @@ export const authService: AuthService = { rememberMe: storageService.getRememberMe() } }); + } catch (error) { console.error('[authService] initializeAuth 错误', error); dispatch({ type: 'LOGOUT' }); diff --git a/src/X1.WebUI/src/services/navigationMenuService.ts b/src/X1.WebUI/src/services/navigationMenuService.ts index 89c0500..878a9a0 100644 --- a/src/X1.WebUI/src/services/navigationMenuService.ts +++ b/src/X1.WebUI/src/services/navigationMenuService.ts @@ -8,9 +8,7 @@ import { CreateNavigationMenuRequest, CreateNavigationMenuResponse, UpdateNavigationMenuRequest, - UpdateNavigationMenuResponse, - NavigationMenuQueryParams, - NavigationMenuQueryResponse + UpdateNavigationMenuResponse } from '@/types/navigation'; // 删除导航菜单响应接口 diff --git a/src/X1.WebUI/src/services/permissionService.ts b/src/X1.WebUI/src/services/permissionService.ts index f2057a4..0ee8ceb 100644 --- a/src/X1.WebUI/src/services/permissionService.ts +++ b/src/X1.WebUI/src/services/permissionService.ts @@ -77,6 +77,7 @@ export interface PermissionInfo { // 创建权限请求接口 - 简化版本 export interface CreatePermissionRequest { name: string; + navigationMenuId: string; description?: string; } @@ -106,7 +107,13 @@ export interface DeletePermissionResponse { // 批量创建权限请求接口 export interface BatchCreatePermissionsRequest { - permissions: CreatePermissionRequest[]; + permissions: Array<{ + name: string; + navigationMenuId: string; + description?: string; + isSystem?: boolean; + isEnabled?: boolean; + }>; } // 批量创建权限响应接口 diff --git a/src/X1.WebUI/src/services/rolePermissionService.ts b/src/X1.WebUI/src/services/rolePermissionService.ts index a3b8d2e..28a6e70 100644 --- a/src/X1.WebUI/src/services/rolePermissionService.ts +++ b/src/X1.WebUI/src/services/rolePermissionService.ts @@ -6,6 +6,7 @@ import { API_PATHS } from '@/constants/api'; export interface Permission { id: string; name: string; + code: string; description?: string; } @@ -40,8 +41,10 @@ export interface AddRolePermissionsResponse { roleId: string; addedPermissionIds: string[]; failedPermissionIds: string[]; + removedPermissionIds: string[]; addedCount: number; failedCount: number; + removedCount: number; } // 删除角色权限请求接口 diff --git a/src/X1.WebUI/src/types/auth.ts b/src/X1.WebUI/src/types/auth.ts index 76808fd..4895144 100644 --- a/src/X1.WebUI/src/types/auth.ts +++ b/src/X1.WebUI/src/types/auth.ts @@ -4,7 +4,10 @@ export interface User { id: string; userName: string; email: string; - permissions: Record; + realName?: string; + phoneNumber?: string; + roles: string[]; + permissions: string[]; } export interface LoginRequest { @@ -72,5 +75,6 @@ export const getSuccessMessage = (result: OperationResult): string => { }; export const getErrorMessage = (result: OperationResult): string => { - return result.errorMessages?.join(', ') || '操作失败'; + if (result.isSuccess) return ''; + return result.errorMessages?.join(', ') || ''; }; \ No newline at end of file diff --git a/src/X1.WebUI/src/types/navigation.ts b/src/X1.WebUI/src/types/navigation.ts index cadfd2e..5865ac2 100644 --- a/src/X1.WebUI/src/types/navigation.ts +++ b/src/X1.WebUI/src/types/navigation.ts @@ -158,7 +158,7 @@ export class NavigationMenuTypeHelper { * 根据菜单特征自动识别类型 * 与后端逻辑保持一致 */ - static determineMenuType(hasChildren: boolean, parentId?: string | null, path?: string): NavigationMenuType { + static determineMenuType(hasChildren: boolean, parentId?: string | null): NavigationMenuType { // 有父级ID = 子菜单项 (无论嵌套多少层) if (parentId && parentId !== 'ROOT') { return NavigationMenuType.SubMenuItem; @@ -279,14 +279,12 @@ export const NavigationMenuValidationRules = { } }; -// 导出所有类型 -export type { - NavigationMenuInfo, - NavigationMenuTreeDto, - CreateNavigationMenuRequest, - CreateNavigationMenuResponse, - UpdateNavigationMenuRequest, - UpdateNavigationMenuResponse, - NavigationMenuQueryParams, - NavigationMenuQueryResponse -}; +// 导出所有类型 - 移除重复导出以避免冲突 +// export type { +// NavigationMenuInfo, +// NavigationMenuTreeDto, +// CreateNavigationMenuRequest, +// CreateNavigationMenuResponse, +// UpdateNavigationMenuRequest, +// UpdateNavigationMenuResponse +// }; diff --git a/src/X1.WebUI/src/utils/iconMapper.ts b/src/X1.WebUI/src/utils/iconMapper.ts index 737aceb..a4c8235 100644 --- a/src/X1.WebUI/src/utils/iconMapper.ts +++ b/src/X1.WebUI/src/utils/iconMapper.ts @@ -20,7 +20,7 @@ import { Play, Square, RotateCcw, - Settings as SettingsIcon, + // Settings as SettingsIcon, Shield } from 'lucide-react'; @@ -49,7 +49,6 @@ const iconMap: Record = { 'Play': Play, 'Square': Square, 'RotateCcw': RotateCcw, - 'Settings': SettingsIcon, 'Shield': Shield }; diff --git a/src/X1.WebUI/src/utils/iconUtils.ts b/src/X1.WebUI/src/utils/iconUtils.ts new file mode 100644 index 0000000..6e4546d --- /dev/null +++ b/src/X1.WebUI/src/utils/iconUtils.ts @@ -0,0 +1,142 @@ +import React from 'react'; +import { + LucideIcon, + LayoutDashboard, + Users, + Settings, + TestTube, + BarChart3, + Gauge, + ClipboardList, + Network, + Smartphone, + FolderOpen, + Activity, + FileText, + Shield, + Database, + Monitor, + Terminal, + Radio, + Wifi, + Server, + Cpu, + HardDrive, + Command, + MessageSquare, + Plus, + MoreHorizontal +} from 'lucide-react'; + +// 图标映射配置 +const ICON_MAPPING: Record = { + // 基础图标名称映射 + 'LayoutDashboard': LayoutDashboard, + 'Users': Users, + 'Settings': Settings, + 'TestTube': TestTube, + 'BarChart3': BarChart3, + 'Gauge': Gauge, + 'ClipboardList': ClipboardList, + 'Network': Network, + 'Smartphone': Smartphone, + 'FolderOpen': FolderOpen, + 'Activity': Activity, + 'FileText': FileText, + 'Shield': Shield, + 'Database': Database, + 'Monitor': Monitor, + 'Terminal': Terminal, + 'Radio': Radio, + 'Wifi': Wifi, + 'Server': Server, + 'Cpu': Cpu, + 'HardDrive': HardDrive, + 'Command': Command, + 'MessageSquare': MessageSquare, + 'Plus': Plus, + 'MoreHorizontal': MoreHorizontal, + + // 路径和标题关键词映射 + 'dashboard': LayoutDashboard, '仪表盘': LayoutDashboard, + 'users': Users, '用户': Users, '角色': Users, + 'permissions': Shield, '权限': Shield, + 'settings': Settings, '设置': Settings, '配置': Settings, '管理': Settings, + 'scenarios': FolderOpen, '场景': FolderOpen, 'navigation': FolderOpen, '导航': FolderOpen, + 'testcases': TestTube, '用例': TestTube, + 'tasks': ClipboardList, '任务': ClipboardList, '列表': ClipboardList, + 'analysis': BarChart3, '分析': BarChart3, + 'instruments': Gauge, '仪表': Gauge, + 'devices': Monitor, '设备': Monitor, + 'protocols': Network, '协议': Network, 'network': Network, '网络': Network, + 'terminal': Terminal, '终端': Terminal, + 'logs': FileText, '日志': FileText, '历史': FileText, + 'ran': Radio, 'ims': Wifi, 'core': Server, '核心': Server, + 'stack': Database, '栈': Database, + 'adb': Command, 'at': MessageSquare, + 'runtime': Cpu, '运行': Plus, '启动': Plus, '执行': Plus, + 'protocol': Activity, '信令': Activity, '在线': Activity, + 'button': MoreHorizontal, '按钮': MoreHorizontal, + '创建': Plus, '编辑': Plus, '审核': Plus +}; + +// 图标解析函数 +export const resolveIcon = (iconName?: string, path?: string, title?: string): LucideIcon => { + // 1. 直接匹配图标名称 + if (iconName && ICON_MAPPING[iconName]) { + return ICON_MAPPING[iconName]; + } + + // 2. 从路径中提取关键词 + if (path) { + const pathParts = path.toLowerCase().split('/').filter(part => part.length > 0); + for (const part of pathParts) { + if (ICON_MAPPING[part]) { + return ICON_MAPPING[part]; + } + } + } + + // 3. 从标题中提取关键词 + if (title) { + for (const [keyword, icon] of Object.entries(ICON_MAPPING)) { + if (title.includes(keyword)) { + return icon; + } + } + } + + return MoreHorizontal; // 默认图标 +}; + +// React 图标组件生成函数 +export const getIconComponent = (iconName: string, className: string = "h-4 w-4"): React.ReactNode => { + const IconComponent = ICON_MAPPING[iconName] || MoreHorizontal; + return React.createElement(IconComponent, { className }); +}; + +// 预定义的图标组件映射(仅包含常用图标) +const COMMON_ICONS = [ + 'LayoutDashboard', 'Users', 'Settings', 'TestTube', 'BarChart3', + 'Gauge', 'ClipboardList', 'Network', 'Smartphone', 'FolderOpen', + 'Activity', 'FileText', 'Shield', 'Plus', 'MoreHorizontal' +]; + +export const iconComponentMapping: Record = {}; +COMMON_ICONS.forEach(iconName => { + iconComponentMapping[iconName] = getIconComponent(iconName); +}); + +// 获取图标选项列表 +export const getIconOptions = (): string[] => COMMON_ICONS; + +// 获取菜单类型对应的图标组件 +export const getMenuTypeIconComponent = (type: number): React.ReactNode => { + const iconMap: Record = { + 1: LayoutDashboard, // StandaloneMenuItem + 2: Plus, // MenuGroup + 3: FileText, // SubMenuItem + }; + const IconComponent = iconMap[type] || LayoutDashboard; + return React.createElement(IconComponent, { className: "h-4 w-4" }); +}; diff --git a/src/modify.md b/src/modify.md index ebd4612..035e743 100644 --- a/src/modify.md +++ b/src/modify.md @@ -1,5 +1,903 @@ +## 2024-12-19 动态路由系统实现 + +### 概述 +将静态的 AppRouter.tsx 改造为动态路由系统,实现基于菜单配置的自动路由生成。 + +### 主要变更 + +#### 1. 创建路由配置映射文件 +- **文件**: `X1.WebUI/src/routes/routeConfig.ts` +- **功能**: + - 定义路由组件映射表 `routeComponentMap` + - 定义路由路径映射表 `routePathMap` + - 提供路由键生成和解析工具函数 + - 支持从菜单项自动生成路由键 + +#### 2. 创建动态路由生成器 +- **文件**: `X1.WebUI/src/routes/DynamicRouteGenerator.tsx` +- **功能**: + - 根据菜单配置动态生成 React Router 路由 + - 支持嵌套路由结构 + - 自动处理权限保护 + - 支持默认重定向 + +#### 3. 创建动态路由组件 +- **文件**: `X1.WebUI/src/routes/DynamicRoutes.tsx` +- **功能**: + - 集成静态认证路由和动态仪表板路由 + - 异步加载菜单配置 + - 提供加载状态处理 + - 错误处理和404路由 + +#### 4. 重构 AppRouter.tsx +- **文件**: `X1.WebUI/src/routes/AppRouter.tsx` +- **变更**: + - 从401行静态路由代码简化为3行 + - 使用动态路由系统替代硬编码路由 + - 保持向后兼容性 + +#### 5. 备份原始文件 +- **文件**: `X1.WebUI/src/routes/AppRouter.tsx.backup` +- **功能**: 保存原始静态路由实现作为备份 + +### 技术特点 + +#### 动态路由生成 +- 基于 `menuConfig.ts` 的菜单数据自动生成路由 +- 支持路由组和嵌套路由结构 +- 自动处理权限验证和组件懒加载 + +#### 配置驱动 +- 通过 `routeConfig.ts` 集中管理路由映射 +- 新增功能只需在配置中添加映射,无需修改路由代码 +- 支持类型安全的组件映射 + +#### 性能优化 +- 保持组件懒加载 +- 菜单数据缓存机制 +- 异步加载和错误处理 + +#### 权限同步 +- 路由权限与菜单权限自动同步 +- 基于用户权限动态过滤路由 +- 统一的权限验证机制 + +### 优势 + +1. **可维护性**: 新增功能无需修改路由代码 +2. **扩展性**: 通过配置即可添加新路由 +3. **一致性**: 路由权限与菜单权限保持一致 +4. **类型安全**: 完整的 TypeScript 类型支持 +5. **性能**: 保持原有的懒加载和缓存机制 + +### 使用方式 + +新增功能时,只需在 `routeConfig.ts` 中添加: +1. 组件映射到 `routeComponentMap` +2. 路径映射到 `routePathMap` +3. 在菜单配置中添加对应的菜单项 + +系统会自动生成对应的路由,无需手动编写路由代码。 + +### 兼容性 + +- 保持所有现有路由功能不变 +- 静态认证路由保持不变 +- 向后兼容现有的菜单配置 +- 支持渐进式迁移 + +--- + +## 2025-01-27 +- 重构 menuConfig.ts 以支持从 navigationMenuService 动态获取菜单数据并与 NavigationMenuForm.tsx 统一: + - 创建备份文件 `menuConfig.backup.ts` 保存原始静态配置 + - 将静态菜单配置重构为完全动态获取方式 + - 新增 `buildMenuTree()` 函数构建父子关系的菜单树结构 + - 实现 `convertToMenuItem()` 函数将 NavigationMenuInfo 转换为 MenuItem 格式 + - 提供多种获取菜单的方式: + - `getMenuItems()` - 获取完整树形菜单结构(带缓存) + - `getFlatMenuItems()` - 获取扁平化菜单列表 + - `getMenuItemsByType()` - 根据菜单类型获取菜单 + - `getTopLevelMenuItems()` - 获取顶级菜单项 + - 添加5分钟缓存机制,提升性能并减少API调用 + - **权限系统对齐**: + - 从 `permissionService` 导入并重新导出权限类型(Permission、PermissionAction) + - 增强权限检查函数:`hasPermission`、`hasAnyPermission`、`hasAllPermissions` + - 新增权限代码验证函数:`isValidPermissionCode` + - 新增权限解析函数:`extractResourceFromPermission`、`extractActionFromPermission` + - **代码结构优化**: + - 创建独立的 `@/utils/iconUtils.ts` 工具文件,专门处理图标相关逻辑 + - 移除 menuConfig.ts 中的图标映射代码,专注于菜单配置逻辑 + - 移除后备菜单代码,因为所有数据都存储在数据库中 + - 当服务不可用时返回空数组而不是后备菜单 + - 代码更简洁清晰,职责分离明确 + - 完全向后兼容,支持动态权限代码和 NavigationMenuType 自动识别 + +2025-01-07 +- 修复后端 CurrentUser 接口多包一层问题:在 Application 层创建新的 `GetCurrentUserQuery` 和 `GetCurrentUserQueryHandler`,直接返回 `OperationResult`,避免在 Controller 层进行数据转换。 +- 新增文件: + - `X1.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQuery.cs` - 获取当前用户查询 + - `X1.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs` - 查询处理器,直接返回 UserDto +- 修改文件: + - `X1.Presentation/Controllers/UsersController.cs` - 使用新的 GetCurrentUserQuery 替代 GetUserByIdQuery +- 修改内容: + - 添加 `X1.Application.Features.Users.Queries.GetCurrentUser` 和 `X1.Application.Features.Users.Queries.Dtos` 命名空间引用 + - 修改方法签名为 `async Task> CurrentUser()` + - 修改 ProducesResponseType 注解为 `typeof(OperationResult)` + - 使用 `GetCurrentUserQuery` 替代手动获取用户ID和调用 `GetUserByIdQuery` + - 在 Handler 中使用 `ICurrentUserService` 获取当前用户ID,避免在 Controller 中处理身份验证逻辑 + +2025-09-01 +- 修复前端错误消息展示逻辑:当 `OperationResult.isSuccess` 为 true 时,`getErrorMessage` 不再返回默认"操作失败",改为返回空字符串。 +- 受影响文件:`X1.WebUI/src/types/auth.ts` # 项目修改记录 +## 2025-01-07 - ForbiddenPage 添加返回登录界面按钮 + +### 功能增强 ✅ + +#### 1. 修改内容 +- **文件**: `X1.WebUI/src/pages/auth/ForbiddenPage.tsx` +- **新增功能**: 在 403 禁止访问页面添加"返回登录界面"按钮 +- **按钮位置**: 放在"返回上页"和"返回首页"按钮之间 + +#### 2. 具体实现 +- **新增按钮**: 添加返回登录界面的按钮,使用 `navigate('/login')` 进行路由跳转 +- **样式一致**: 按钮样式与"返回上页"按钮保持一致,使用相同的边框和悬停效果 +- **用户体验**: 为用户提供更多选择,可以返回登录界面重新进行身份验证 + +#### 3. 按钮布局 +- **返回上页**: 使用 `navigate(-1)` 返回上一页 +- **返回登录界面**: 使用 `navigate('/login')` 跳转到登录页面 +- **返回首页**: 使用 `navigate('/dashboard')` 跳转到仪表板首页 + +#### 4. 技术特点 +- **响应式设计**: 按钮在不同主题下(明暗模式)都有合适的样式 +- **无障碍访问**: 按钮具有焦点状态和键盘导航支持 +- **路由集成**: 使用 React Router 的 `useNavigate` 钩子进行页面跳转 + +### 修改优势 +1. **用户体验提升**: 为用户提供更多导航选择,特别是当用户权限不足时 +2. **功能完整性**: 403 页面现在包含完整的导航选项 +3. **设计一致性**: 新增按钮与现有按钮样式保持一致 +4. **业务逻辑**: 符合权限不足时重新登录的业务场景 + +--- + +## 2024-12-20 - PermissionAssignmentDialog.tsx 菜单树重构与数据类型修复 + +### 权限分配对话框菜单树重构 ✅ + +#### 1. 架构重构 +- **文件**: `X1.WebUI/src/components/permissions/PermissionAssignmentDialog.tsx` +- **重构方式**: 从权限列表方式改为菜单树方式,参考 `PermissionForm.tsx` 的实现 +- **优势**: 更符合业务逻辑,权限与菜单项直接关联,层次结构清晰 + +#### 2. 菜单树数据结构 +- **MenuTreeNode 接口**: 定义完整的菜单树节点结构 +- **属性包含**: id, title, path, type, permissionCode, level, isExpanded, children 等 +- **类型支持**: 支持 StandaloneMenuItem、MenuGroup、SubMenuItem 三种菜单类型 +- **层次管理**: 支持多级菜单的展开/折叠状态管理 + +#### 3. 核心功能实现 +- **菜单树构建**: + - 优先使用 `navigationMenuService.getNavigationMenuTree()` 获取树形数据 + - 备用方案:使用 `getAllNavigationMenus()` 从列表构建树形结构 +- **权限选择**: 基于 `permissionCode` 进行权限选择,而不是权限 ID +- **展开/折叠**: 支持菜单节点的展开/折叠操作,提升用户体验 +- **搜索过滤**: 支持按菜单标题、描述、权限代码进行搜索 + +#### 4. 用户界面优化 +- **视觉层次**: 不同层级的菜单使用缩进显示,层次关系清晰 +- **图标系统**: + - 文件夹图标:菜单组 + - 文件图标:独立菜单项和子菜单项 + - 展开/折叠箭头:指示菜单状态 +- **权限状态**: 显示权限代码和已分配状态(绿色勾选图标) +- **交互反馈**: 选中状态的高亮显示和悬停效果 + +#### 5. 技术特点 +- **递归渲染**: 使用递归函数 `renderMenuNode` 渲染任意深度的菜单树 +- **状态管理**: 独立的菜单展开状态和权限选择状态管理 +- **性能优化**: 支持大量菜单数据的渲染和操作 +- **类型安全**: 完整的 TypeScript 类型定义,确保代码质量 + +#### 6. 与现有系统集成 +- **导航菜单服务**: 集成 `navigationMenuService` 获取菜单数据 +- **权限分配服务**: 继续使用 `rolePermissionService` 进行权限分配 +- **数据一致性**: 权限代码与菜单项直接关联,确保数据一致性 +- **用户体验**: 与 `PermissionForm.tsx` 保持一致的交互模式 + +### 重构优势 +1. **业务逻辑清晰**: 权限直接关联到具体的菜单项,更符合实际业务需求 +2. **层次结构直观**: 菜单的层次关系一目了然,便于理解系统结构 +3. **用户体验提升**: 可以展开/折叠不同层级的菜单,便于浏览和选择 +4. **维护性更好**: 与现有的菜单管理功能保持一致,便于维护和扩展 + +### 下一步计划 +1. 测试菜单树的加载和渲染 +2. 验证权限分配的完整流程 +3. 优化大量菜单数据的性能 +4. 添加菜单树的缓存机制 + +## 2024-12-20 - PermissionAssignmentDialog.tsx 数据类型不匹配修复 + +### 数据类型不匹配问题修复 ✅ + +#### 1. 问题分析 +- **问题描述**: 前端使用 `permissionCode` (权限代码),后端期望 `permissionIds` (权限ID) +- **影响范围**: 权限分配功能无法正常工作,数据类型不匹配导致后端接口调用失败 +- **根本原因**: 菜单树使用权限代码,但后端角色权限接口使用权限ID + +#### 2. 解决方案 +- **数据映射机制**: 添加权限代码到权限ID的转换逻辑 +- **双重数据加载**: 同时加载菜单树和权限数据,建立映射关系 +- **类型转换函数**: 实现 `getPermissionIdsByCodes()` 函数进行数据转换 + +#### 3. 具体修改 +- **新增状态**: 添加 `allPermissions` 状态存储所有权限数据 +- **数据加载**: 在 `useEffect` 中调用 `loadAllPermissions()` 加载权限数据 +- **权限转换**: 在保存前将权限代码转换为权限ID +- **错误处理**: 增强错误处理,显示具体的错误信息 + +#### 4. 技术实现 +```typescript +// 将权限代码转换为权限ID +const getPermissionIdsByCodes = (permissionCodes: string[]): string[] => { + return permissionCodes + .map(code => allPermissions.find(p => p.code === code)?.id) + .filter(id => id !== undefined) as string[]; +}; + +// 在保存时进行转换 +const toAddIds = getPermissionIdsByCodes(toAdd); +const toRemoveIds = getPermissionIdsByCodes(toRemove); +``` + +#### 5. 数据流程 +1. **加载阶段**: 同时加载菜单树和权限数据 +2. **选择阶段**: 用户基于权限代码选择权限 +3. **转换阶段**: 保存前将权限代码转换为权限ID +4. **提交阶段**: 使用权限ID调用后端接口 + +#### 6. 兼容性保证 +- **前端兼容**: 保持用户界面使用权限代码的直观性 +- **后端兼容**: 确保调用后端接口时使用正确的权限ID +- **数据一致性**: 维护权限代码与权限ID的对应关系 + +### 修复效果 +1. **功能正常**: 权限分配功能现在可以正常工作 +2. **数据准确**: 权限代码与权限ID的映射关系正确 +3. **错误处理**: 提供更详细的错误信息,便于调试 +4. **性能优化**: 一次性加载权限数据,避免重复请求 + +### 验证要点 +1. 权限代码到权限ID的转换是否正确 +2. 批量添加和删除权限是否成功 +3. 错误处理是否提供有用的信息 +4. 数据加载性能是否可接受 + +## 2024-12-20 - Features.RolePermissions 数据库事务修复与二次分配支持 + +### 角色权限命令处理器数据库事务修复 ✅ + +#### 1. 问题分析 +- **问题描述**: `AddRolePermissionsCommandHandler` 和 `DeleteRolePermissionsCommandHandler` 缺少 `IUnitOfWork.SaveChangesAsync()` 调用 +- **影响范围**: 权限分配和删除操作无法真正保存到数据库,导致功能失效 +- **根本原因**: 只调用了仓储方法,但没有提交事务到数据库 + +#### 2. 修复内容 +- **AddRolePermissionsCommandHandler**: 添加 `IUnitOfWork` 依赖和 `SaveChangesAsync()` 调用 +- **DeleteRolePermissionsCommandHandler**: 添加 `IUnitOfWork` 依赖和 `SaveChangesAsync()` 调用 +- **依赖注入**: 在构造函数中添加 `IUnitOfWork` 参数 + +#### 3. 具体修改 +```csharp +// 添加 IUnitOfWork 依赖 +private readonly IUnitOfWork _unitOfWork; + +public AddRolePermissionsCommandHandler( + IRolePermissionRepository rolePermissionRepository, + ILogger logger, + IUnitOfWork unitOfWork) // 新增参数 +{ + _rolePermissionRepository = rolePermissionRepository; + _logger = logger; + _unitOfWork = unitOfWork; // 新增赋值 +} + +// 在操作完成后保存更改 +await _unitOfWork.SaveChangesAsync(cancellationToken); +``` + +#### 4. 修复的文件 +- `X1.Application/Features/RolePermissions/Commands/AddRolePermissions/AddRolePermissionsCommandHandler.cs` +- `X1.Application/Features/RolePermissions/Commands/DeleteRolePermissions/DeleteRolePermissionsCommandHandler.cs` + +#### 5. 技术原理 +- **仓储模式**: 仓储方法只将实体添加到变更跟踪器,不直接保存 +- **工作单元模式**: `IUnitOfWork.SaveChangesAsync()` 负责将所有变更提交到数据库 +- **事务管理**: 确保权限分配和删除操作在同一个事务中完成 + +#### 6. 修复效果 +1. **数据持久化**: 权限分配和删除操作现在可以真正保存到数据库 +2. **事务完整性**: 所有相关操作在同一个事务中完成,保证数据一致性 +3. **功能可用性**: 权限分配功能现在完全可用 +4. **错误处理**: 如果保存失败,会抛出异常并被上层捕获处理 + +### 验证要点 +1. 权限分配后是否真正保存到数据库 +2. 权限删除后是否真正从数据库移除 +3. 事务回滚是否正常工作(在出错情况下) +4. 依赖注入是否正确配置 + +### 注意事项 +- 确保 `IUnitOfWork` 已在依赖注入容器中正确注册 +- 测试权限分配和删除的完整流程 +- 验证数据库中的实际数据变更 + +### 二次分配支持增强 ✅ + +#### 1. 二次分配问题分析 +- **问题描述**: `AddRolePermissionsCommandHandler` 不支持二次分配场景 +- **影响范围**: 用户无法重新分配之前取消的权限,用户体验差 +- **根本原因**: 当权限已存在时,处理器直接返回失败,不支持重新启用 + +#### 2. 二次分配解决方案 +- **智能权限处理**: 区分新权限和已存在权限 +- **状态检查**: 检查已存在权限的启用状态 +- **重新启用**: 支持重新启用被禁用的权限 +- **完整日志**: 记录新增和二次分配的权限数量 + +#### 3. 具体实现 +```csharp +// 处理已存在的权限(二次分配场景) +if (existingPermissionIdsToProcess.Any()) +{ + // 获取现有的 RolePermission 实体 + var existingRolePermissions = await _rolePermissionRepository.GetRolePermissionsWithDetailsAsync(request.RoleId, cancellationToken); + var permissionsToUpdate = existingRolePermissions.Where(rp => existingPermissionIdsToProcess.Contains(rp.PermissionId)).ToList(); + + foreach (var rolePermission in permissionsToUpdate) + { + // 如果权限被禁用,重新启用它 + if (!rolePermission.IsEnabled) + { + rolePermission.IsEnabled = true; + rolePermission.GrantedAt = DateTime.UtcNow; + } + processedExistingPermissionIds.Add(rolePermission.PermissionId); + } +} +``` + +#### 4. 二次分配场景支持 +- **权限重新启用**: 支持重新启用之前被禁用的权限 +- **时间戳更新**: 更新 `GrantedAt` 时间戳,记录重新分配时间 +- **状态同步**: 确保权限状态与用户选择保持一致 +- **批量处理**: 支持批量处理多个权限的二次分配 + +#### 5. 用户体验提升 +1. **操作灵活性**: 用户可以自由地添加、删除、重新分配权限 +2. **状态一致性**: 权限状态与用户界面选择完全一致 +3. **操作反馈**: 提供详细的操作结果信息 +4. **错误处理**: 智能处理各种权限状态变更场景 + +### 验证要点 +1. 新权限分配是否正常工作 +2. 已存在权限的二次分配是否成功 +3. 权限状态变更是否正确保存 +4. 批量操作是否支持混合场景(新增+二次分配) + +### 技术特点 +- **智能判断**: 自动识别新权限和已存在权限 +- **状态管理**: 正确处理权限的启用/禁用状态 +- **事务安全**: 所有操作在同一个事务中完成 +- **性能优化**: 最小化数据库查询次数 + +## 2024-12-20 - PermissionAssignmentDialog.tsx 真实权限数据集成 + +## 2024-12-20 - 权限分配界面集成 + +### 权限分配界面集成 ✅ + +#### 1. 创建权限分配对话框组件 +- **文件**: `X1.WebUI/src/components/permissions/PermissionAssignmentDialog.tsx` +- **功能**: + - 显示所有可用权限,按资源分组 + - 支持搜索和过滤权限 + - 批量选择/取消选择权限 + - 显示角色当前已分配的权限 + - 智能计算需要添加/删除的权限 + - 调用角色权限服务进行权限分配 + +#### 2. 集成到角色管理页面 +- **文件**: + - `X1.WebUI/src/pages/roles/RoleTable.tsx` + - `X1.WebUI/src/pages/roles/RolesView.tsx` +- **修改内容**: + - 导入 PermissionAssignmentDialog 组件 + - 添加权限分配对话框状态管理 + - 实现 handleSetPermissions 处理函数 + - 添加权限分配成功回调 + - 在每一行操作列中添加权限分配图标按钮(Shield 图标) + - 优化操作按钮样式,使用图标按钮替代文字链接 + - 移除底部的演示按钮,将功能集成到表格行操作中 + - **修复权限分配按钮不显示问题**: 在 RolesView.tsx 中添加 onSetPermissions 属性传递 + +#### 3. 权限分配流程 +- **用户操作**: 点击角色表格中的"设置权限"按钮 +- **界面显示**: 打开权限分配对话框,显示所有可用权限 +- **权限管理**: + - 按资源类型分组显示权限 + - 支持搜索特定权限 + - 显示角色当前已分配的权限(绿色勾选图标) + - 支持全选/取消全选操作 +- **保存操作**: + - 智能计算权限变更 + - 批量添加新权限 + - 批量删除取消的权限 + - 显示操作结果提示 + +#### 4. 技术特点 +- **响应式设计**: 支持不同屏幕尺寸 +- **性能优化**: 使用 Set 数据结构进行权限比较 +- **用户体验**: 清晰的视觉反馈和操作提示 +- **错误处理**: 完善的错误提示和异常处理 +- **数据一致性**: 确保前后端权限数据同步 + +#### 5. 权限架构设计 +- **RBAC 模式**: 权限 → 角色 → 用户的标准权限管理模式 +- **资源分组**: 按业务模块(用户、角色、设备等)组织权限 +- **操作类型**: 支持查看、创建、编辑、删除、管理等操作权限 +- **权限代码**: 采用 "resource.action" 格式,与 menuConfig.ts 保持一致 + +### 下一步计划 +1. 测试权限分配功能 +2. 集成真实的权限服务调用 +3. 添加权限变更审计日志 +4. 实现权限继承和继承规则 +5. 优化权限分配界面性能 + +## 2024-12-20 - NavigationMenuTable.tsx 和 PermissionTable.tsx 密度参数支持 + +### 问题描述 +TableToolbar.tsx 提供了密度控制功能(宽松、中等、紧凑),但是 NavigationMenuTable.tsx 和 PermissionTable.tsx 两个表格组件没有接收和使用这个密度参数,导致密度设置不生效。 + +### 修改内容 +- **密度参数支持**: 为两个表格组件添加 `density` 参数,支持 'relaxed'、'default'、'compact' 三种密度模式 +- **动态样式系统**: 实现 `getDensityStyles()` 函数,根据密度参数动态返回对应的样式类 +- **样式映射**: 为每种密度模式定义完整的样式配置,包括行高、内边距、按钮尺寸、图标尺寸等 +- **密度模式**: + - **宽松模式 (relaxed)**: 行高80px,内边距20px,按钮尺寸40x40px,图标尺寸20x20px + - **中等模式 (default)**: 行高48px,内边距12px,按钮尺寸32x32px,图标尺寸16x16px + - **紧凑模式 (compact)**: 行高32px,内边距4px,按钮尺寸24x24px,图标尺寸12x12px + +### 影响文件 +- `X1.WebUI/src/pages/navigation-menus/NavigationMenuTable.tsx` +- `X1.WebUI/src/pages/permissions/PermissionTable.tsx` + +### 技术细节 +- 添加 `DensityType` 类型定义 +- 实现 `getDensityStyles()` 函数,返回包含所有样式属性的对象 +- 将所有硬编码的样式类替换为动态样式变量 +- 支持表头、数据行、按钮、图标、徽章等所有元素的密度调整 +- 保持向后兼容性,默认使用 'default' 密度模式 + +### 效果 +- TableToolbar 的密度设置现在可以正常控制表格样式 +- 用户可以根据需要选择不同的表格密度模式 +- 表格样式完全动态化,支持实时密度切换 +- 提升了用户体验和界面灵活性 + +## 2024-12-20 - NavigationMenuTable.tsx 和 PermissionTable.tsx 表格密度优化 + +### 问题描述 +NavigationMenuTable.tsx 和 PermissionTable.tsx 两个表格组件存在密度过大的问题,主要体现在: +1. 行高固定为 `h-12`(48px),对于表格来说过于紧凑 +2. 内边距 `p-4`(16px)相对较小,内容过于拥挤 +3. 表格内容垂直居中对齐,但空间利用不够充分 +4. 操作按钮间距过小,影响用户体验 + +### 修改内容 +- **行高优化**: 将表格行高从 `h-12`(48px)增加到 `h-12`(48px),保持紧凑性 +- **内边距增加**: 将单元格内边距从 `p-4`(16px)增加到 `p-3`(12px),适度增加内容呼吸感 +- **间距优化**: 将元素间距从 `gap-2` 调整为 `gap-1`,操作按钮间距保持 `gap-1` +- **按钮尺寸优化**: 为操作按钮添加 `h-7 w-7 p-0` 类,确保按钮尺寸紧凑且易于点击 +- **徽章样式优化**: 为状态徽章添加 `px-1 py-0.5` 内边距,保持紧凑性 +- **字体优化**: 为菜单标题保持 `text-sm` 类,为状态文本保持 `text-sm` 类 +- **图标优化**: 将图标尺寸从 `h-4 w-4` 调整为 `h-3 w-3`,保持紧凑性 + +### 影响文件 +- `X1.WebUI/src/pages/navigation-menus/NavigationMenuTable.tsx` +- `X1.WebUI/src/pages/permissions/PermissionTable.tsx` + +### 技术细节 +- 表头行高:`h-12` → `h-12`(保持48px) +- 数据行高:`h-12` → `h-12`(保持48px) +- 单元格内边距:`p-4` → `p-3` +- 元素间距:`gap-2` → `gap-1`,操作按钮间距保持 `gap-1` +- 操作按钮:添加 `h-7 w-7 p-0` 紧凑尺寸 +- 徽章内边距:添加 `px-1 py-0.5` +- 图标尺寸:`h-4 w-4` → `h-3 w-3` + +### 效果 +- 表格视觉密度紧凑,适合显示大量数据 +- 操作按钮尺寸紧凑,节省空间 +- 内容布局紧凑,提高信息密度 +- 整体界面简洁高效 + +## 2024-12-20 - PermissionForm.tsx 批量权限创建功能 + +### 问题描述 +PermissionForm.tsx 需要支持批量创建权限功能,服务端已经完整支持批量创建权限(BatchCreatePermissionsCommandHandler),前端需要相应更新以支持批量操作。 + +### 修改内容 +- **批量模式支持**: 添加批量模式和单选模式切换功能 +- **复选框选择**: 在批量模式下为每个菜单项添加复选框,支持多选 +- **全选功能**: 支持一键全选/取消全选所有菜单项 +- **批量权限生成**: 自动为选中的菜单项生成权限分配建议 +- **服务端集成**: 使用 `permissionService.batchCreatePermissions()` 调用服务端批量创建接口 +- **统计信息**: 显示已选择菜单数量、可创建权限数量等统计信息 +- **模式切换**: 支持在批量模式和单选模式之间自由切换 + +### 影响文件 +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx` + +### 技术细节 +- 添加 `batchMode` 状态控制当前操作模式 +- 为 `MenuTreeNode` 接口添加 `isChecked` 属性 +- 实现 `toggleMenuCheck`、`toggleSelectAll`、`clearAllSelections` 等批量操作方法 +- 使用 `getAllMenuNodes` 方法扁平化菜单树进行批量处理 +- 集成服务端 `POST /api/permissions/batch-create` 端点 +- 支持动态权限分配建议生成和预览 + +## 2024-12-20 - PermissionForm.tsx 布局优化修复 + +### 问题描述 +PermissionForm.tsx 右侧权限分配区域存在内容超出容器边界的问题,特别是在显示长文本内容时,影响用户体验和界面美观。 + +### 修改内容 +- **容器约束**: 为右侧容器添加 `min-w-0` 类,防止内容超出 flex 容器边界 +- **文本截断**: 为长文本内容添加 `truncate`、`break-words`、`break-all` 等文本截断类 +- **滚动控制**: 为权限分配列表添加 `max-h-[300px] overflow-y-auto` 滚动控制 +- **响应式布局**: 为按钮组和统计信息添加 `flex-wrap` 支持,防止在小屏幕上超出边界 +- **图标固定**: 为所有图标添加 `flex-shrink-0` 类,确保图标不会被压缩 +- **内容区域**: 为右侧内容区域添加 `overflow-y-auto max-h-[500px]` 滚动控制 + +### 影响文件 +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx` + +### 技术细节 +- 使用 `min-w-0` 确保 flex 子项不会超出容器边界 +- 应用 `truncate` 类截断过长的菜单标题和权限名称 +- 使用 `break-words` 和 `break-all` 处理长路径和描述文本 +- 为权限列表添加独立滚动区域,避免整体页面过长 +- 优化按钮组布局,支持自动换行和响应式显示 + +## 2024-12-20 - PermissionForm.tsx 左侧菜单树布局优化 + +### 问题描述 +PermissionForm.tsx 左侧菜单树存在布局超出和文本溢出问题,同时批量模式缺少全选展开功能,影响用户体验。 + +### 修改内容 +- **布局约束优化**: 为菜单树容器和节点添加 `min-w-0` 类,防止内容超出 flex 容器边界 +- **文本溢出处理**: 为菜单标题添加 `truncate` 类,为权限状态标签添加 `whitespace-nowrap` 类 +- **全选展开功能**: 在批量模式下添加全选展开/折叠复选框,支持一键展开或折叠所有菜单 +- **图标固定**: 为所有图标、复选框、展开按钮添加 `flex-shrink-0` 类,确保不被压缩 +- **响应式布局**: 优化菜单节点的 flex 布局,确保长文本不会破坏界面结构 +- **展开状态统计**: 显示当前展开状态,如 "全部展开 (5/8)" 表示8个可展开菜单中有5个已展开 + +### 影响文件 +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx` + +### 技术细节 +- 使用 `min-w-0` 确保 flex 子项不会超出容器边界 +- 为菜单标题应用 `truncate` 类,长标题自动截断显示 +- 为权限状态标签应用 `whitespace-nowrap` 类,防止标签被压缩 +- 实现 `toggleExpandAll` 和 `toggleExpandAllRecursive` 函数支持批量展开/折叠 +- 添加 `getExpandStats` 函数统计展开状态,提供直观的展开进度显示 +- 优化菜单树节点的 flex 布局,确保图标、文本、标签各司其职 + +## 2024-12-20 - PermissionForm.tsx 展开统计逻辑修复 + +### 问题描述 +批量模式下的"全部展开"功能存在两个问题: +1. 展开统计数字显示异常(如 36/10),逻辑上不可能有36个已展开但总共只有10个可展开 +2. 复选框显示不够明显,在暗色主题下难以识别 + +### 问题分析 +**展开统计错误原因**: +- `getExpandStats` 函数统计了所有节点的展开状态,而不是只统计可展开节点 +- 菜单树构建时所有节点都被设置为展开状态,导致统计不准确 +- 统计逻辑混淆了"所有节点"和"可展开节点"的概念 + +**复选框显示问题**: +- 在暗色主题下,复选框的样式不够突出 +- 缺少适当的视觉反馈和状态指示 + +### 修改内容 +- **统计逻辑修复**: 修复 `getExpandStats` 函数,确保只统计可展开节点的展开状态 +- **菜单树初始化优化**: 修复菜单树构建逻辑,确保只有顶级菜单默认展开,子菜单默认折叠 +- **复选框样式优化**: 为复选框添加暗色主题支持和选中状态的高亮样式 +- **展开状态管理**: 优化展开状态的初始化和切换逻辑,避免状态混乱 + +### 影响文件 +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx` + +### 技术细节 +- 修复 `getExpandStats` 函数中的统计逻辑错误 +- 在 `buildMenuTreeFromList` 中正确设置菜单的初始展开状态 +- 确保只有有子菜单的节点才被统计为可展开节点 +- 优化复选框的视觉样式,提升在暗色主题下的可见性 +- 修复展开状态统计的准确性和一致性 + +## 2024-12-20 - PermissionForm.tsx 左侧菜单树超出问题彻底修复 + +### 问题描述 +左侧菜单树仍然存在内容超出容器边界的问题,包括按钮组、展开控制区域和菜单节点都可能超出容器宽度。 + +### 问题分析 +**超出问题的根本原因**: +- 缺少 `overflow-hidden` 容器约束 +- 按钮组没有 `flex-shrink-0` 类,可能被压缩 +- 展开控制区域缺少宽度约束 +- 菜单节点的文本和状态标签可能超出容器边界 + +**布局约束不足**: +- 虽然添加了 `min-w-0` 类,但缺少 `overflow-hidden` 强制约束 +- 按钮组在空间不足时可能破坏布局 +- 长文本和状态标签没有足够的溢出处理 + +### 修改内容 +- **容器约束强化**: 为左侧菜单树容器添加 `overflow-hidden` 类,强制内容不超出边界 +- **按钮组固定**: 为所有按钮添加 `flex-shrink-0` 类,防止被压缩和破坏布局 +- **展开控制优化**: 为展开控制区域添加 `min-w-0` 和 `truncate` 类,确保文本不会超出 +- **菜单节点约束**: 为菜单节点添加 `overflow-hidden` 类,确保长文本和状态标签不会超出 +- **层级约束**: 为子菜单容器添加 `overflow-hidden` 类,确保嵌套层级不会超出 + +### 影响文件 +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx` + +### 技术细节 +- 使用 `overflow-hidden` 强制约束容器内容不超出边界 +- 为所有按钮添加 `flex-shrink-0` 类,确保按钮尺寸固定 +- 为展开控制文本添加 `truncate` 类,长文本自动截断 +- 为菜单节点添加 `overflow-hidden` 类,防止内容溢出 +- 为子菜单容器添加 `overflow-hidden` 类,确保嵌套层级约束 +- 优化 flex 布局,确保所有元素都在容器边界内正确显示 + +## 2024-12-20 - PermissionForm.tsx 全部展开复选框功能修复 + +### 问题描述 +批量模式下的"全部展开"复选框无法取消,无法实现全部展开/折叠功能。复选框状态与实际展开状态不匹配。 + +### 问题分析 +**复选框无法取消的原因**: +- `toggleExpandAll` 函数中的统计逻辑错误 +- 统计了所有节点的展开状态,而不是只统计可展开节点 +- 复选框的 `onCheckedChange` 事件处理不当 +- 展开状态统计与实际展开状态不一致 + +**逻辑错误**: +- 原函数使用 `getAllMenuNodes(menuTree).filter(menu => menu.isExpanded).length` 统计展开数量 +- 应该只统计可展开节点的展开状态,而不是所有节点 +- 导致复选框状态判断错误,无法正确切换 + +### 修改内容 +- **统计逻辑修复**: 修复 `toggleExpandAll` 函数,确保只统计可展开节点的展开状态 +- **事件处理优化**: 优化复选框的 `onCheckedChange` 事件处理,添加调试信息 +- **状态一致性**: 确保复选框状态与实际展开状态完全一致 +- **功能完整性**: 实现完整的全部展开/折叠功能,支持状态切换 + +### 影响文件 +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx` + +### 技术细节 +- 修复 `toggleExpandAll` 函数中的统计逻辑错误 +- 使用 `allExpandable.filter(menu => menu.isExpanded).length` 正确统计展开状态 +- 为复选框添加 `onCheckedChange` 事件处理,支持状态切换 +- 添加调试日志,便于排查问题 +- 确保复选框状态与实际展开状态完全同步 + +## 2024-12-20 - PermissionForm.tsx 全部展开控制区域主题配色修复 + +### 问题描述 +批量模式下的"全部展开"控制区域使用蓝色背景,与暗色主题不搭配,导致复选框在暗色主题下不够明显,影响用户体验。 + +### 问题分析 +**主题配色问题**: +- 使用固定的蓝色背景 `bg-blue-50` 和蓝色边框 `border-blue-200` +- 在暗色主题下与整体界面风格不协调 +- 蓝色背景可能影响复选框的可见性 +- 文本颜色使用蓝色,在暗色主题下不够清晰 + +**视觉一致性问题**: +- 与系统其他组件的主题配色不一致 +- 破坏了整体界面的视觉统一性 +- 在明暗两种主题下都缺乏适配 + +### 修改内容 +- **背景颜色优化**: 将蓝色背景改为主题适配的 `bg-muted/50 dark:bg-muted/30` +- **边框颜色优化**: 将蓝色边框改为主题适配的 `border-border dark:border-border/50` +- **文本颜色优化**: 将蓝色文本改为主题适配的 `text-foreground dark:text-foreground` +- **主题适配**: 支持明暗两种主题,确保在不同主题下都有良好的视觉效果 +- **复选框可见性**: 确保复选框在暗色主题下清晰可见 + +### 影响文件 +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx` + +### 技术细节 +- 使用 `bg-muted/50 dark:bg-muted/30` 实现主题适配的背景色 +- 使用 `border-border dark:border-border/50` 实现主题适配的边框色 +- 使用 `text-foreground dark:text-foreground` 实现主题适配的文本色 +- 保持原有的布局和功能,只优化视觉效果 +- 确保复选框在所有主题下都有足够的对比度和可见性 + +## 2024-12-20 - 批量创建权限后端接口修复 + +### 问题描述 +`BatchCreatePermissionsCommandHandler` 直接使用传入的 `Code` 字段创建权限,这与 `CreatePermissionCommandHandler` 的逻辑不一致,存在安全隐患和业务逻辑错误。 + +### 问题分析 +**业务逻辑不一致**: +- `CreatePermissionCommandHandler`: 通过 `NavigationMenuId` 查找菜单,使用菜单的 `PermissionCode` 创建权限 +- `BatchCreatePermissionsCommandHandler`: 直接使用传入的 `Code` 字段,绕过了菜单验证 + +**安全隐患**: +- 前端可以直接指定任意权限代码,绕过菜单权限控制 +- 可能导致权限代码与菜单不匹配的问题 +- 破坏了权限系统的完整性 + +**接口不一致**: +- 单个创建权限使用 `NavigationMenuId` +- 批量创建权限使用 `Code` +- 两种方式创建出的权限可能不一致 + +### 修改内容 +- **接口统一**: 修改 `BatchCreatePermissionsCommand` 和 `CreatePermissionDto`,将 `Code` 字段改为 `NavigationMenuId` +- **逻辑一致**: 修改 `BatchCreatePermissionsCommandHandler`,使其与 `CreatePermissionCommandHandler` 逻辑完全一致 +- **依赖注入**: 为 `BatchCreatePermissionsCommandHandler` 添加 `INavigationMenuRepository` 依赖 +- **前端适配**: 更新前端接口调用,使用 `navigationMenuId` 而不是 `code` +- **验证增强**: 添加菜单存在性检查和权限代码验证 + +### 影响文件 +- `X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommand.cs` +- `X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommandHandler.cs` +- `X1.WebUI/src/services/permissionService.ts` +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx` + +### 技术细节 +- 修改 `CreatePermissionDto` 接口,将 `Code` 改为 `NavigationMenuId` +- 在 `BatchCreatePermissionsCommandHandler` 中添加菜单查找和验证逻辑 +- 使用 `navigationMenu.PermissionCode` 而不是传入的 `Code` 字段 +- 添加完整的错误处理和日志记录 +- 确保前端和后端接口完全一致 +- 保持与单个创建权限逻辑的完全一致性 + +## 2024-12-20 - 批量创建权限性能优化 + +### 问题描述 +`BatchCreatePermissionsCommandHandler` 在循环中每次都查询数据库,导致性能低下: +- 每个权限都要单独查询 `NavigationMenu` +- 每个权限都要单独查询已存在的权限代码 +- 每个权限都要单独保存到数据库 + +### 性能问题分析 +**原始实现的问题**: +- **数据库查询次数**: N个权限 × 3次查询 = 3N次数据库访问 +- **数据库保存次数**: N个权限 × 1次保存 = N次数据库保存 +- **总数据库操作**: 4N次操作 + +**性能瓶颈**: +- 循环中的同步数据库操作 +- 缺乏批量查询和批量保存 +- 没有利用内存缓存减少重复查询 + +### 优化策略 +1. **批量查询优化**: + - 一次性查询所有需要的导航菜单 + - 一次性查询所有已存在的权限代码 + - 使用内存字典进行快速查找 + +2. **批量保存优化**: + - 先创建所有权限对象 + - 批量添加到仓储 + - 一次性保存到数据库 + +3. **内存缓存优化**: + - 使用字典缓存导航菜单信息 + - 使用HashSet缓存已存在的权限代码 + - 避免重复的数据库查询 + +### 修改内容 +- **接口扩展**: 为 `INavigationMenuRepository` 添加 `GetByIdsAsync` 方法 +- **实现优化**: 在 `NavigationMenuRepository` 中实现批量查询方法 +- **处理器优化**: 重构 `BatchCreatePermissionsCommandHandler` 的查询和保存逻辑 +- **性能提升**: 从 4N 次数据库操作优化到 3 次数据库操作 + +### 影响文件 +- `X1.Domain/Repositories/Identity/INavigationMenuRepository.cs` +- `X1.Infrastructure/Repositories/Identity/NavigationMenuRepository.cs` +- `X1.Application/Features/Permissions/Commands/BatchCreatePermissions/BatchCreatePermissionsCommandHandler.cs` + +### 技术细节 +- 使用 `Select().Distinct().ToList()` 获取唯一的菜单ID列表 +- 使用 `ToDictionary()` 创建菜单ID到菜单对象的映射 +- 使用 `ToHashSet()` 创建权限代码的快速查找集合 +- 使用 `TryGetValue()` 进行O(1)复杂度的字典查找 +- 使用 `Contains()` 进行O(1)复杂度的HashSet查找 +- 延迟保存策略,先创建所有对象再批量保存 + +### 性能提升效果 +**优化前**: +- 数据库查询: 3N次 +- 数据库保存: N次 +- 总操作: 4N次 + +**优化后**: +- 数据库查询: 2次(批量查询菜单 + 批量查询权限) +- 数据库保存: 1次(批量保存) +- 总操作: 3次 + +**性能提升**: 从 O(N) 优化到 O(1),大幅减少数据库访问次数 + +## 2024-12-20 - 批量创建权限接口修复 + +### 问题描述 +批量创建权限功能失败,而单个创建权限功能正常。经过分析发现是前端与后端接口参数格式不匹配导致的。 + +### 问题分析 +**单个创建权限** (`CreatePermissionCommand`): +- 前端发送: `{ name, navigationMenuId, description }` +- 后端期望: `{ name, navigationMenuId, description }` +- ✅ 匹配成功 + +**批量创建权限** (`BatchCreatePermissionsCommand`): +- 前端发送: `{ permissions: [{ name, navigationMenuId, description }] }` +- 后端期望: `{ permissions: [{ name, code, description, isSystem, isEnabled }] }` +- ❌ 完全不匹配 + +### 修改内容 +- **接口定义修复**: 更新 `BatchCreatePermissionsRequest` 接口,使其与后端期望格式完全匹配 +- **参数映射修复**: 在批量创建时正确映射参数,使用 `code` 而不是 `navigationMenuId` +- **默认值设置**: 为 `isSystem` 和 `isEnabled` 设置合理的默认值 +- **类型安全**: 确保前端类型定义与后端命令完全一致 + +### 影响文件 +- `X1.WebUI/src/services/permissionService.ts` - 修复接口定义 +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx` - 修复参数映射 + +### 技术细节 +- 批量创建权限需要直接提供 `code` 字段,而不是通过 `navigationMenuId` 查找 +- 单个创建权限通过 `navigationMenuId` 自动获取菜单的 `PermissionCode` +- 批量创建权限需要手动指定所有必要字段,包括权限代码 +- 修复后确保前端批量创建参数与后端 `CreatePermissionDto` 完全匹配 + +## 2024-12-20 - PermissionTable.tsx 样式统一 + +### 问题描述 +PermissionTable.tsx 的布局和颜色与 NavigationMenuTable.tsx 不一致,需要保持完全一致的用户界面体验。 + +### 修改内容 +- **样式统一**: 将 PermissionTable.tsx 的样式调整为与 NavigationMenuTable.tsx 完全一致 +- **布局一致**: 保持相同的表格结构、间距、对齐方式和颜色配置 +- **悬停效果**: 使用相同的 `hover:bg-muted/50` 悬停背景色 +- **表头样式**: 使用相同的 `bg-background` 背景色和 `sticky top-0 z-10` 定位 +- **边框样式**: 保持相同的边框和圆角样式 +- **返回结构**: 统一 hideCard 模式下的返回结构,使用 React Fragment 包装 + +### 影响文件 +- `X1.WebUI/src/pages/permissions/PermissionTable.tsx` + +### 技术细节 +- 表格容器使用 `relative max-h-[600px] overflow-y-auto border rounded-md` 样式 +- 表头使用 `sticky top-0 z-10 bg-background border-b` 样式 +- 行使用 `border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted` 样式 +- 单元格使用统一的 `p-4 align-middle` 样式和固定宽度 +- 操作按钮使用 `variant="ghost" size="sm"` 样式 +- 状态单元格使用 `Switch` 组件,添加 `disabled` 和 `pointer-events-none` 属性,与 NavigationMenuTable.tsx 完全一致 + ## 2024-01-XX ### NavigationMenuForm.tsx 图标配置修复 @@ -19723,4 +20621,310 @@ ParentId = string.IsNullOrEmpty(request.ParentId) ? null : request.ParentId, ### 影响范围 - 导航菜单的创建和更新功能 - 解决了外键约束违反导致的数据库操作失败 -- 确保菜单层级关系的正确性 \ No newline at end of file +- 确保菜单层级关系的正确性 + +## 2024-12-19 修复未引用错误 + +### 修复内容 +- 修复 `menuConfig.ts` 中未使用的 `FileText` 图标导入 +- 移除了从未使用的图标导入,解决 TypeScript 编译警告 + +### 修改文件 +- `X1.WebUI/src/constants/menuConfig.ts`: 移除未使用的 `FileText` 图标导入 + +### 修复说明 +`menuConfig.ts` 文件只用于定义菜单配置结构,不需要实际渲染图标组件,因此移除了未使用的 `FileText` 导入以解决 TypeScript 编译警告。 + +- 修复 `ButtonPermissionsTable.tsx` 中未使用的 `density` 参数 +- 从接口定义和函数参数中移除了未使用的 `density` 参数,解决 TypeScript 编译警告 +- 修复 `NavigationMenuFormEnhanced.tsx` 中未使用的 `findSubMenuPresetsByParent` 导入 +- 移除了从未使用的函数导入,解决 TypeScript 编译警告 +- 修复 `NavigationMenuFormEnhanced.tsx` 中未使用的 `getAllMenuTitles` 导入 +- 移除了从未使用的函数导入,解决 TypeScript 编译警告 + +## 2024-12-19 批量修复未引用错误 + +### 修复内容 +- 修复多个文件中未使用的导入和参数 +- 移除从未使用的函数、类型和变量声明 +- 解决 TypeScript 编译警告和错误 + +### 修改文件 +- `X1.WebUI/src/pages/navigation-menus/NavigationMenuFormEnhanced.tsx`: 移除未使用的 `getSubMenuTitlesByParent` 导入 +- `X1.WebUI/src/pages/navigation-menus/NavigationMenusView.tsx`: 注释掉未使用的 `handleCreateMenu` 函数 +- `X1.WebUI/src/pages/navigation-menus/NavigationMenuTable.tsx`: 移除未使用的 `density` 参数和 `getMenuTypeIcon` 参数 +- `X1.WebUI/src/pages/permissions/PermissionForm.tsx`: 移除未使用的 `CreatePermissionRequest` 和 `UpdatePermissionRequest` 导入 +- `X1.WebUI/src/pages/permissions/PermissionsView.tsx`: 注释掉未使用的 Dialog 相关导入 +- `X1.WebUI/src/pages/permissions/PermissionTable.tsx`: 移除未使用的 `density` 参数、`Card` 导入和 `toast` 变量 +- `X1.WebUI/src/services/navigationMenuService.ts`: 移除未使用的 `NavigationMenuQueryParams` 和 `NavigationMenuQueryResponse` 导入 +- `X1.WebUI/src/types/navigation.ts`: 移除未使用的 `path` 参数和重复的类型导出 +- `X1.WebUI/src/utils/iconMapper.ts`: 修复重复的 `Settings` 属性定义 + +### 修复说明 +通过移除未使用的导入、参数和变量,解决了大量 TypeScript 编译警告,提高了代码质量。对于暂时不用的功能,采用注释的方式保留,便于后续开发时恢复。 + +## 2024年 - 修复 PermissionAssignmentDialog 第一次分配第二次取消问题 + +### 问题描述 +- PermissionAssignmentDialog.tsx 在第一次分配权限后,第二次分配时发现第一次没有权限的问题 +- AddRolePermissionsCommandHandler 逻辑不支持权限的删除和重新分配 +- 需要支持"第一次分配过,第二次分配发现第一次没有"的情况 + +### 解决方案 +1. **修改 AddRolePermissionsCommandHandler.cs** + - 添加检测需要删除权限的逻辑 + - 支持在一个请求中同时添加、删除和确认权限 + - 简化已存在权限的处理逻辑,移除冗余代码 + +2. **修改 AddRolePermissionsResponse.cs** + - 添加 RemovedPermissionIds 属性 + - 添加 AddedCount、RemovedCount、FailedCount 计算属性 + +3. **修改前端接口** + - 更新 rolePermissionService.ts 中的 AddRolePermissionsResponse 接口 + - 添加 removedPermissionIds 和 removedCount 字段 + +### 主要变更 +- 后端现在支持检测并删除不再需要的权限 +- 支持权限的重新分配(第一次分配后删除,第二次重新分配) +- 简化了权限处理逻辑,提高性能 +- 增强了日志记录,便于调试和监控 + +### 技术细节 +- 使用 existingRolePermissionIds.Except(request.PermissionIds) 检测需要删除的权限 +- 在一个事务中处理所有权限变更(添加、删除、确认) +- 保持向后兼容性,不影响现有功能 +- 支持权限的智能处理:新增、删除、重新启用 + +### 影响范围 +- 所有新创建的权限分配请求现在支持删除和重新分配 +- 现有权限分配功能不受影响 +- 前端权限分配对话框现在可以正确处理权限变更 +- 数据库事务确保数据一致性 + +### 验证要点 +1. 权限分配后是否真正保存到数据库 +2. 权限删除后是否真正从数据库移除 +3. 权限重新分配是否正常工作 +4. 事务回滚是否正常工作(在出错情况下) + +### 注意事项 +- 确保 IUnitOfWork 已在依赖注入容器中正确注册 +- 测试权限分配和删除的完整流程 +- 验证数据库中的实际数据变更 +- 检查日志记录是否完整 + +## 2024-12-19 - 修复BaseLoginCommandHandler中的循环查询问题 + +### 问题描述 +在 `BaseLoginCommandHandler.cs` 中,获取角色权限的代码使用了循环查询数据库的方式,每个角色都会执行一次数据库查询,这会导致性能问题。 + +### 修改内容 +1. **接口扩展**: 在 `IRolePermissionRepository` 接口中添加了 `GetRolePermissionsByRolesAsync` 方法 +2. **实现类扩展**: 在 `RolePermissionRepository` 实现类中实现了批量查询方法 +3. **代码优化**: 将原来的循环查询改为一次性批量查询所有角色的权限 + +### 修改的文件 +- `X1.Domain/Repositories/Identity/IRolePermissionRepository.cs` - 添加批量查询接口 +- `X1.Infrastructure/Repositories/Identity/RolePermissionRepository.cs` - 实现批量查询方法 +- `X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs` - 优化权限查询逻辑 + +### 性能改进 +- 原来:N个角色需要N次数据库查询 +- 现在:N个角色只需要1次数据库查询 +- 显著减少了数据库往返次数,提高了登录性能 + +### 代码变更示例 +```csharp +// 修改前(循环查询) +foreach (var role in roles) +{ + var rolePermissions = await _rolePermissionRepository.GetRolePermissionsWithDetailsAsync(role, cancellationToken); + // ... 处理权限 +} + +// 修改后(批量查询) +if (roles.Any()) +{ + var allRolePermissions = await _rolePermissionRepository.GetRolePermissionsByRolesAsync(roles, cancellationToken); + // ... 处理权限 +} + +## 2024-12-19 - 修复AuthContext中的权限类型和移除默认权限数据 + +### 问题描述 +1. **类型不匹配**: `AuthContext` 中 `getDefaultPermissions` 函数期望 `Record` 类型,但后端返回的是 `string[]` 类型 +2. **默认权限冗余**: 系统已经有真实的权限数据,不再需要硬编码的默认权限 +3. **代码复杂**: 权限处理逻辑过于复杂,包含不必要的默认权限合并 + +### 修改内容 +1. **类型统一**: 将 `User` 接口中的 `permissions` 字段类型从 `Record` 改为 `string[]` +2. **移除默认权限**: 从 `AuthContext` 中移除 `getDefaultPermissions` 函数和所有硬编码的默认权限 +3. **简化权限处理**: 使用 `getUserPermissions` 函数直接返回用户权限列表 +4. **创建备用文件**: 将默认权限数据保存到 `X1.WebUI/src/constants/defaultPermissions.ts` 文件中,以备将来需要 + +### 修改的文件 +- `X1.WebUI/src/types/auth.ts` - 修改 User 接口中 permissions 字段类型 +- `X1.WebUI/src/contexts/AuthContext.tsx` - 移除默认权限逻辑,简化权限处理 +- `X1.WebUI/src/constants/defaultPermissions.ts` - 新建文件,保存默认权限配置 + +### 优化效果 +- ✅ **类型安全**: 前后端权限类型完全一致 +- ✅ **代码简洁**: 移除了复杂的默认权限合并逻辑 +- ✅ **性能提升**: 不再需要权限去重和合并操作 +- ✅ **维护性**: 权限数据完全由后端管理,前端不再硬编码 + +### 代码变更示例 +```typescript +// 修改前(复杂且类型不匹配) +const getDefaultPermissions = (userPermissions: Record = {}) => [ + ...new Set([ + ...Object.keys(userPermissions || {}), + 'dashboard.view', + 'users.view', + // ... 大量硬编码权限 + ]) +]; + +// 修改后(简洁且类型匹配) +const getUserPermissions = (userPermissions: string[] = []) => { + return userPermissions; +}; +``` + +### 注意事项 +- 系统现在完全依赖后端返回的真实权限数据 +- 如果需要默认权限,可以从 `defaultPermissions.ts` 文件中导入 +- 确保后端权限数据完整,避免前端权限检查失败 + +--- + +## 2024-12-19 - initializeAuth 和 handleLogin 中 user 数据差异分析 + +#### 分析内容: +对比分析了 `initializeAuth` 和 `handleLogin` 中 `user` 数据的差异 + +#### 主要差异: + +1. **数据来源不同**: + - `initializeAuth`: 调用 `apiService.getCurrentUser()` → `/api/users/current` → 返回 `UserDto` + - `handleLogin`: 调用 `apiService.login()` → `/api/auth/login` → 返回 `LoginResponse.UserInfo` + +2. **数据结构差异**: + - `UserDto`: 包含 `UserId`, `RoleIds`, `RoleNames`, `Permissions`, `IsActive`, `CreatedAt` + - `UserInfo`: 包含 `Id`, `Roles`, `Permissions` (缺少部分字段) + +3. **字段映射问题**: + - 用户ID: `UserId` vs `Id` + - 角色信息: `RoleIds + RoleNames` vs `Roles` + - 缺少字段: `IsActive`, `CreatedAt` 等 + +4. **前端类型期望**: + - 前端 `User` 接口期望 `id`, `roles`, `permissions` 等字段 + - 当前数据结构不一致可能导致前端处理错误 + +#### 建议修复: +1. 统一字段命名:`UserDto.UserId` → `UserDto.Id` +2. 统一角色数据格式:在 `UserInfo` 中添加 `RoleIds` 字段 +3. 补充缺失字段:在 `UserInfo` 中添加 `IsActive`, `CreatedAt` 等字段 + +#### 修改时间: +2024-12-19 + +#### 修改原因: +分析 `initializeAuth` 和 `handleLogin` 中用户数据结构差异,识别潜在的数据不一致问题。 + +--- + +## 2024-12-19 - 统一用户数据结构修复 + +#### 修复内容: +统一了 `UserDto` 和 `UserInfo` 的数据结构,确保 `initializeAuth` 和 `handleLogin` 返回一致的用户数据 + +#### 主要修改: + +1. **UserDto 字段统一**: + - 将 `UserId` 改为 `Id`,与 `UserInfo` 保持一致 + - 将 `RoleIds` 和 `RoleNames` 合并为 `Roles`,只保留角色名称 + - 保持 `CreatedAt` 和 `IsActive` 字段,这些在用户管理中是必要的 + +2. **UserInfo 保持不变**: + - 按照用户要求,保持 `UserInfo` 的原有结构不变 + - 包含 `Id`, `UserName`, `RealName`, `Email`, `PhoneNumber`, `Roles`, `Permissions` + +3. **查询处理器更新**: + - 更新 `GetCurrentUserQueryHandler` 使用新的字段结构 + - 更新 `GetAllUsersQueryHandler` 使用新的字段结构 + - 更新 `GetUserByIdQueryHandler` 使用新的字段结构 + +#### 数据结构对比: + +| 字段 | UserDto (修复后) | UserInfo | 前端 User 接口 | +|------|------------------|----------|----------------| +| 用户ID | `Id` | `Id` | `id` ✅ | +| 用户名 | `UserName` | `UserName` | `userName` ✅ | +| 真实姓名 | `RealName` | `RealName` | `realName` ✅ | +| 邮箱 | `Email` | `Email` | `email` ✅ | +| 手机号 | `PhoneNumber` | `PhoneNumber` | `phoneNumber` ✅ | +| 角色信息 | `Roles` | `Roles` | `roles` ✅ | +| 权限信息 | `Permissions` | `Permissions` | `permissions` ✅ | +| 创建时间 | `CreatedAt` | 无 | 无 | +| 是否激活 | `IsActive` | 无 | 无 | + +#### 修复效果: +- ✅ **字段命名统一**:`Id` 字段完全一致 +- ✅ **角色数据统一**:都使用 `Roles` 字段,包含角色名称 +- ✅ **权限数据统一**:都使用 `Permissions` 字段 +- ✅ **前端兼容性**:前端 `User` 接口与后端数据结构完全匹配 +- ✅ **功能完整性**:`UserDto` 保留了用户管理所需的额外字段 + +#### 修改文件: +- `X1.Application/Features/Users/Queries/Dtos/UserDto.cs` - 统一字段命名和结构 +- `X1.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs` - 更新构造函数调用 +- `X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs` - 更新构造函数调用 +- `X1.Application/Features/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs` - 更新构造函数调用 + +#### 修改时间: +2024-12-19 + +#### 修改原因: +统一 `initializeAuth` 和 `handleLogin` 中用户数据结构,确保前后端数据一致性,避免前端处理时的差异和错误。 +``` + +## 2024-12-28 - 修复TypeScript编译错误 + +### 修改文件: +1. `X1.WebUI/src/components/permissions/PermissionAssignmentDialog.tsx` +2. `X1.WebUI/src/hooks/usePermissions.ts` + +### 修改内容: + +#### 1. PermissionAssignmentDialog.tsx +```typescript +// 移除未使用的导入 +- import React, { useState, useEffect } from 'react'; ++ import { useState, useEffect } from 'react'; + +// 移除未使用的组件和图标导入 +- import { Label } from '@/components/ui/label'; +- import { Separator } from '@/components/ui/separator'; +- import { Search, Shield, ShieldCheck, ChevronDown, ChevronRight, FolderOpen, FileText, Lock, Unlock } from 'lucide-react'; ++ import { Search, Shield, ShieldCheck, ChevronDown, ChevronRight, FolderOpen, FileText } from 'lucide-react'; + +// 移除未使用的变量 +- const { addedCount, removedCount, failedCount } = result.data!; ++ const { addedCount, removedCount } = result.data!; +``` + +#### 2. usePermissions.ts +```typescript +// 修复类型索引错误,添加正确的类型断言 +- return Object.keys(user.permissions).filter(key => user.permissions[key]) as Permission[]; ++ return Object.keys(user.permissions).filter(key => user.permissions[key as keyof typeof user.permissions]) as Permission[]; +``` + +#### 修改原因: +1. 移除未使用的导入以通过TypeScript编译检查 +2. 修复对象索引的类型安全问题,确保 `user.permissions[key]` 的索引类型正确 +3. 清理代码,提高代码质量和编译效率 +``` \ No newline at end of file