Browse Source
- 在 routeConfig.ts 中添加 scenarios.binding 路由映射 - 配置场景绑定页面组件懒加载 - 设置路由路径为 'binding' - 确保动态路由系统支持场景绑定功能 - 与现有路由配置保持一致的命名规范refactor/permission-config
64 changed files with 5479 additions and 1618 deletions
@ -0,0 +1,11 @@ |
|||
using X1.Domain.Common; |
|||
using MediatR; |
|||
using X1.Application.Features.Users.Queries.Dtos; |
|||
|
|||
namespace X1.Application.Features.Users.Queries.GetCurrentUser; |
|||
|
|||
/// <summary>
|
|||
/// 获取当前用户查询
|
|||
/// 用于获取当前登录用户详细信息的查询对象
|
|||
/// </summary>
|
|||
public sealed record GetCurrentUserQuery() : IRequest<OperationResult<UserDto>>; |
|||
@ -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; |
|||
|
|||
/// <summary>
|
|||
/// 获取当前用户查询处理器
|
|||
/// 处理获取当前登录用户详细信息的查询请求
|
|||
/// </summary>
|
|||
public sealed class GetCurrentUserQueryHandler : IRequestHandler<GetCurrentUserQuery, OperationResult<UserDto>> |
|||
{ |
|||
private readonly UserManager<AppUser> _userManager; |
|||
private readonly RoleManager<AppRole> _roleManager; |
|||
private readonly IUserRoleRepository _userRoleRepository; |
|||
private readonly IRolePermissionRepository _rolePermissionRepository; |
|||
private readonly ICurrentUserService _currentUserService; |
|||
private readonly ILogger<GetCurrentUserQueryHandler> _logger; |
|||
|
|||
/// <summary>
|
|||
/// 初始化查询处理器
|
|||
/// </summary>
|
|||
public GetCurrentUserQueryHandler( |
|||
UserManager<AppUser> userManager, |
|||
RoleManager<AppRole> roleManager, |
|||
IUserRoleRepository userRoleRepository, |
|||
IRolePermissionRepository rolePermissionRepository, |
|||
ICurrentUserService currentUserService, |
|||
ILogger<GetCurrentUserQueryHandler> logger) |
|||
{ |
|||
_userManager = userManager; |
|||
_roleManager = roleManager; |
|||
_userRoleRepository = userRoleRepository; |
|||
_rolePermissionRepository = rolePermissionRepository; |
|||
_currentUserService = currentUserService; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理获取当前用户查询
|
|||
/// </summary>
|
|||
/// <param name="request">查询请求</param>
|
|||
/// <param name="cancellationToken">取消令牌</param>
|
|||
/// <returns>操作结果,包含当前用户信息</returns>
|
|||
public async Task<OperationResult<UserDto>> Handle( |
|||
GetCurrentUserQuery request, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
try |
|||
{ |
|||
// 获取当前用户ID
|
|||
var currentUserId = _currentUserService.GetCurrentUserId(); |
|||
if (string.IsNullOrEmpty(currentUserId)) |
|||
{ |
|||
_logger.LogWarning("无法获取当前用户ID"); |
|||
return OperationResult<UserDto>.CreateFailure("无法获取当前用户信息"); |
|||
} |
|||
|
|||
// 根据ID查询用户
|
|||
var user = await _userManager.FindByIdAsync(currentUserId); |
|||
if (user == null) |
|||
{ |
|||
_logger.LogWarning("当前用户 {UserId} 不存在", currentUserId); |
|||
return OperationResult<UserDto>.CreateFailure("用户不存在"); |
|||
} |
|||
|
|||
// 获取用户的角色(使用自定义仓储)
|
|||
var userRoleIds = await _userRoleRepository.GetUserRolesAsync(user.Id, cancellationToken); |
|||
|
|||
// 获取角色ID和名称
|
|||
var roleIds = new List<string>(); |
|||
var roleNames = new List<string>(); |
|||
|
|||
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<string>(); |
|||
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<UserDto>.CreateSuccess(dto); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取当前用户信息时发生异常"); |
|||
return OperationResult<UserDto>.CreateFailure("系统错误,请稍后重试"); |
|||
} |
|||
} |
|||
} |
|||
@ -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<MenuTreeNode[]>([]); |
|||
const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(new Set()); |
|||
const [rolePermissions, setRolePermissions] = useState<Set<string>>(new Set()); |
|||
const [allPermissions, setAllPermissions] = useState<PermissionInfo[]>([]); |
|||
|
|||
// 获取菜单树和角色权限
|
|||
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<string, MenuTreeNode>(); |
|||
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 ( |
|||
<div key={menu.id} className="w-full min-w-0"> |
|||
<div |
|||
className={` |
|||
flex items-center gap-2 p-3 rounded-lg cursor-pointer transition-colors min-w-0 overflow-hidden |
|||
${isSelected ? 'bg-primary/10 border border-primary/20' : 'hover:bg-muted/50'} |
|||
${menu.level > 0 ? 'ml-4' : ''} |
|||
`}
|
|||
> |
|||
{/* 权限选择复选框 */} |
|||
{menu.permissionCode && ( |
|||
<Checkbox |
|||
checked={isSelected} |
|||
onCheckedChange={() => togglePermissionSelection(menu.permissionCode!)} |
|||
className="flex-shrink-0" |
|||
/> |
|||
)} |
|||
|
|||
{/* 展开/折叠图标 */} |
|||
{canExpand && ( |
|||
<button |
|||
onClick={(e) => { |
|||
e.stopPropagation(); |
|||
toggleMenuExpansion(menu.id); |
|||
}} |
|||
className="p-1 hover:bg-muted rounded flex-shrink-0" |
|||
> |
|||
{menu.isExpanded ? ( |
|||
<ChevronDown className="h-4 w-4" /> |
|||
) : ( |
|||
<ChevronRight className="h-4 w-4" /> |
|||
)} |
|||
</button> |
|||
)} |
|||
|
|||
{/* 菜单类型图标 */} |
|||
<div className="flex items-center gap-2 flex-shrink-0"> |
|||
{menu.type === NavigationMenuType.StandaloneMenuItem && ( |
|||
<FileText className="h-4 w-4 text-blue-500" /> |
|||
)} |
|||
{menu.type === NavigationMenuType.MenuGroup && ( |
|||
<FolderOpen className="h-4 w-4 text-green-500" /> |
|||
)} |
|||
{menu.type === NavigationMenuType.SubMenuItem && ( |
|||
<FileText className="h-4 w-4 text-gray-500" /> |
|||
)} |
|||
</div> |
|||
|
|||
{/* 菜单标题 */} |
|||
<span className="flex-1 text-sm font-medium truncate min-w-0 overflow-hidden"> |
|||
{menu.title} |
|||
</span> |
|||
|
|||
{/* 权限状态 */} |
|||
{menu.permissionCode && ( |
|||
<div className="flex-shrink-0 ml-2"> |
|||
{wasAssigned && ( |
|||
<ShieldCheck className="h-4 w-4 text-green-600" /> |
|||
)} |
|||
<Badge variant="secondary" className="text-xs whitespace-nowrap ml-2"> |
|||
{menu.permissionCode} |
|||
</Badge> |
|||
</div> |
|||
)} |
|||
</div> |
|||
|
|||
{/* 子菜单 */} |
|||
{canExpand && menu.isExpanded && ( |
|||
<div className="ml-4 min-w-0 overflow-hidden"> |
|||
{menu.children.map(child => renderMenuNode(child))} |
|||
</div> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
if (!role) return null; |
|||
|
|||
return ( |
|||
<Dialog open={open} onOpenChange={onOpenChange}> |
|||
<DialogContent className="max-w-4xl max-h-[80vh]"> |
|||
<DialogHeader> |
|||
<DialogTitle className="flex items-center gap-2"> |
|||
<Shield className="h-5 w-5" /> |
|||
为角色 "{role.name}" 分配权限 |
|||
</DialogTitle> |
|||
</DialogHeader> |
|||
|
|||
<div className="space-y-4"> |
|||
{/* 搜索框 */} |
|||
<div className="relative"> |
|||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> |
|||
<Input |
|||
placeholder="搜索菜单或权限..." |
|||
value={searchTerm} |
|||
onChange={(e) => setSearchTerm(e.target.value)} |
|||
className="pl-10" |
|||
/> |
|||
</div> |
|||
|
|||
{/* 批量操作按钮 */} |
|||
<div className="flex gap-2"> |
|||
<Button variant="outline" size="sm" onClick={handleSelectAll}> |
|||
全选 |
|||
</Button> |
|||
<Button variant="outline" size="sm" onClick={handleDeselectAll}> |
|||
取消全选 |
|||
</Button> |
|||
<Badge variant="secondary" className="ml-auto"> |
|||
已选择 {selectedPermissions.size} 个权限 |
|||
</Badge> |
|||
</div> |
|||
|
|||
{/* 菜单树 */} |
|||
<ScrollArea className="h-[400px] border rounded-md p-4"> |
|||
{loading ? ( |
|||
<div className="text-center text-muted-foreground py-8"> |
|||
加载中... |
|||
</div> |
|||
) : filteredMenuTree.length === 0 ? ( |
|||
<div className="text-center text-muted-foreground py-8"> |
|||
没有找到匹配的菜单 |
|||
</div> |
|||
) : ( |
|||
<div className="space-y-2"> |
|||
{filteredMenuTree.map(menu => renderMenuNode(menu))} |
|||
</div> |
|||
)} |
|||
</ScrollArea> |
|||
|
|||
{/* 操作按钮 */} |
|||
<div className="flex justify-end gap-2 pt-4 border-t"> |
|||
<Button variant="outline" onClick={() => onOpenChange(false)}> |
|||
取消 |
|||
</Button> |
|||
<Button |
|||
onClick={handleSave} |
|||
disabled={loading} |
|||
className="min-w-[100px]" |
|||
> |
|||
{loading ? '保存中...' : '保存'} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</DialogContent> |
|||
</Dialog> |
|||
); |
|||
} |
|||
@ -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<Role | null>(null); |
|||
|
|||
const handleSetPermissions = (role: Role) => { |
|||
setSelectedRole(role); |
|||
setDialogOpen(true); |
|||
}; |
|||
|
|||
return ( |
|||
<div> |
|||
{/* 角色表格 */} |
|||
<RoleTable onSetPermissions={handleSetPermissions} /> |
|||
|
|||
{/* 权限分配对话框 */} |
|||
<PermissionAssignmentDialog |
|||
open={dialogOpen} |
|||
onOpenChange={setDialogOpen} |
|||
role={selectedRole} |
|||
onSuccess={() => { |
|||
console.log('权限分配成功'); |
|||
// 刷新角色列表或其他操作 |
|||
}} |
|||
/> |
|||
</div> |
|||
); |
|||
} |
|||
``` |
|||
|
|||
### 2. 在 RoleTable 中集成 |
|||
|
|||
```tsx |
|||
// RoleTable.tsx |
|||
export default function RoleTable({ onSetPermissions, ...props }) { |
|||
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false); |
|||
const [selectedRole, setSelectedRole] = useState<Role | null>(null); |
|||
|
|||
const handleSetPermissions = (role: Role) => { |
|||
setSelectedRole(role); |
|||
setPermissionDialogOpen(true); |
|||
}; |
|||
|
|||
return ( |
|||
<div> |
|||
{/* 表格内容 */} |
|||
<Table> |
|||
{/* ... 表格行 ... */} |
|||
<span onClick={() => handleSetPermissions(role)}> |
|||
设置权限 |
|||
</span> |
|||
</Table> |
|||
|
|||
{/* 权限分配对话框 */} |
|||
<PermissionAssignmentDialog |
|||
open={permissionDialogOpen} |
|||
onOpenChange={setPermissionDialogOpen} |
|||
role={selectedRole} |
|||
onSuccess={() => { |
|||
// 权限分配成功后的回调 |
|||
}} |
|||
/> |
|||
</div> |
|||
); |
|||
} |
|||
``` |
|||
|
|||
## 权限数据结构 |
|||
|
|||
### 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. 批量操作 |
|||
- 批量角色权限分配 |
|||
- 权限复制功能 |
|||
- 权限迁移工具 |
|||
@ -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<typeof ScrollAreaPrimitive.Root>, |
|||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> |
|||
>(({ className, children, ...props }, ref) => ( |
|||
<ScrollAreaPrimitive.Root |
|||
ref={ref} |
|||
className={cn("relative overflow-hidden", className)} |
|||
{...props} |
|||
> |
|||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> |
|||
{children} |
|||
</ScrollAreaPrimitive.Viewport> |
|||
<ScrollBar /> |
|||
<ScrollAreaPrimitive.Corner /> |
|||
</ScrollAreaPrimitive.Root> |
|||
)) |
|||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName |
|||
|
|||
const ScrollBar = React.forwardRef< |
|||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, |
|||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> |
|||
>(({ className, orientation = "vertical", ...props }, ref) => ( |
|||
<ScrollAreaPrimitive.ScrollAreaScrollbar |
|||
ref={ref} |
|||
orientation={orientation} |
|||
className={cn( |
|||
"flex touch-none select-none transition-colors", |
|||
orientation === "vertical" && |
|||
"h-full w-2.5 border-l border-l-transparent p-[1px]", |
|||
orientation === "horizontal" && |
|||
"h-2.5 flex-col border-t border-t-transparent p-[1px]", |
|||
className |
|||
)} |
|||
{...props} |
|||
> |
|||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> |
|||
</ScrollAreaPrimitive.ScrollAreaScrollbar> |
|||
)) |
|||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName |
|||
|
|||
export { ScrollArea, ScrollBar } |
|||
@ -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); |
|||
}; |
|||
@ -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); |
|||
}; |
|||
@ -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<string, NavigationMenuInfo>(); |
|||
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<MenuItem[]> => { |
|||
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<MenuItem[]> => { |
|||
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<MenuItem[]> => { |
|||
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<MenuItem[]> => { |
|||
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); |
|||
}; |
|||
}; |
|||
|
|||
// 向后兼容的静态导出
|
|||
export let menuItems: MenuItem[] = []; |
|||
|
|||
// 初始化菜单
|
|||
getMenuItems().then(items => { |
|||
menuItems = items; |
|||
}).catch(error => { |
|||
console.error('初始化菜单失败:', error); |
|||
menuItems = []; |
|||
}); |
|||
@ -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 类型的菜单都能在菜单树中显示 |
|||
- ✅ **子菜单正常展开**: 任何有子菜单的菜单都能展开显示子菜单 |
|||
- ✅ **菜单类型正确识别**: 所有菜单类型都能正确识别和显示 |
|||
- ✅ **父子关系正确建立**: 菜单的层级关系正确显示 |
|||
File diff suppressed because it is too large
@ -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 = () => ( |
|||
<div className="flex items-center justify-center min-h-screen"> |
|||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> |
|||
</div> |
|||
); |
|||
import DynamicRoutes from './DynamicRoutes'; |
|||
|
|||
// 导出动态路由组件
|
|||
export function AppRouter() { |
|||
return ( |
|||
<Routes> |
|||
<Route path="/" element={<Navigate to="/dashboard" replace />} /> |
|||
<Route |
|||
path="/login" |
|||
element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<LoginPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} |
|||
/> |
|||
<Route |
|||
path="/forgot-password" |
|||
element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<ForgotPasswordPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} |
|||
/> |
|||
<Route |
|||
path="/register" |
|||
element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<RegisterPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} |
|||
/> |
|||
<Route path="/403" element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<ForbiddenPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} /> |
|||
<Route |
|||
path="/dashboard/*" |
|||
element={ |
|||
<ProtectedRoute requiredPermission="dashboard.view"> |
|||
<DashboardLayout> |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<Routes> |
|||
<Route index element={<DashboardHome />} /> |
|||
|
|||
|
|||
{/* 场景管理路由 */} |
|||
<Route path="scenarios"> |
|||
<Route index element={<Navigate to="config" replace />} /> |
|||
<Route path="config" element={ |
|||
<ProtectedRoute requiredPermission="scenarios.view"> |
|||
<AnimatedContainer> |
|||
<ScenarioConfigView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="binding" element={ |
|||
<ProtectedRoute requiredPermission="scenarios.manage"> |
|||
<AnimatedContainer> |
|||
<ScenarioBindingView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 用例管理路由 */} |
|||
<Route path="testcases"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="testcases.view"> |
|||
<AnimatedContainer> |
|||
<TestCasesListView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="create" element={ |
|||
<ProtectedRoute requiredPermission="testcases.create"> |
|||
<AnimatedContainer> |
|||
<TestCasesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="steps" element={ |
|||
<ProtectedRoute requiredPermission="teststeps.view"> |
|||
<AnimatedContainer> |
|||
<TestStepsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 任务管理路由 */} |
|||
<Route path="tasks"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="tasks.view"> |
|||
<AnimatedContainer> |
|||
<TasksView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="reviews" element={ |
|||
<ProtectedRoute requiredPermission="taskreviews.view"> |
|||
<AnimatedContainer> |
|||
<TaskReviewView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="executions" element={ |
|||
<ProtectedRoute requiredPermission="taskexecutions.view"> |
|||
<AnimatedContainer> |
|||
<TaskExecutionView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 结果分析路由 */} |
|||
<Route path="analysis"> |
|||
<Route index element={<Navigate to="functional" replace />} /> |
|||
<Route path="functional" element={ |
|||
<ProtectedRoute requiredPermission="functionalanalysis.view"> |
|||
<AnimatedContainer> |
|||
<FunctionalAnalysisView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="performance" element={ |
|||
<ProtectedRoute requiredPermission="performanceanalysis.view"> |
|||
<AnimatedContainer> |
|||
<PerformanceAnalysisView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="issue" element={ |
|||
<ProtectedRoute requiredPermission="issueanalysis.view"> |
|||
<AnimatedContainer> |
|||
<IssueAnalysisView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="ue" element={ |
|||
<ProtectedRoute requiredPermission="ueanalysis.view"> |
|||
<AnimatedContainer> |
|||
<UEAnalysisView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 仪表管理路由 */} |
|||
<Route path="instruments"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="devices.view"> |
|||
<AnimatedContainer> |
|||
<DevicesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="protocols" element={ |
|||
<ProtectedRoute requiredPermission="protocols.view"> |
|||
<AnimatedContainer> |
|||
<ProtocolsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="device-runtimes"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="deviceruntimes.view"> |
|||
<AnimatedContainer> |
|||
<DeviceRuntimesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
</Route> |
|||
|
|||
{/* 终端服务管理路由 */} |
|||
<Route path="terminal-services"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="terminalservices.view"> |
|||
<AnimatedContainer> |
|||
<TerminalServicesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="adb-operations" element={ |
|||
<ProtectedRoute requiredPermission="adboperations.view"> |
|||
<AnimatedContainer> |
|||
<AdbOperationsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="at-operations" element={ |
|||
<ProtectedRoute requiredPermission="atoperations.view"> |
|||
<AnimatedContainer> |
|||
<AtOperationsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 终端设备管理路由 */} |
|||
<Route path="terminal-devices"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="terminaldevices.view"> |
|||
<AnimatedContainer> |
|||
<TerminalDevicesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 信令分析路由 */} |
|||
<Route path="protocol-logs"> |
|||
<Route index element={<Navigate to="online-logs" replace />} /> |
|||
<Route path="online-logs" element={ |
|||
<ProtectedRoute requiredPermission="protocollogs.view"> |
|||
<AnimatedContainer> |
|||
<OnlineProtocolLogsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="history-logs" element={ |
|||
<ProtectedRoute requiredPermission="protocollogs.view"> |
|||
<AnimatedContainer> |
|||
<HistoryProtocolLogsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 网络栈配置管理路由 */} |
|||
<Route path="network-stack-configs"> |
|||
<Route index element={<Navigate to="ran-configurations" replace />} /> |
|||
<Route path="ran-configurations" element={ |
|||
<ProtectedRoute requiredPermission="ranconfigurations.view"> |
|||
<AnimatedContainer> |
|||
<RANConfigurationsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="ims-configurations" element={ |
|||
<ProtectedRoute requiredPermission="imsconfigurations.view"> |
|||
<AnimatedContainer> |
|||
<IMSConfigurationsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="core-network-configs" element={ |
|||
<ProtectedRoute requiredPermission="corenetworkconfigs.view"> |
|||
<AnimatedContainer> |
|||
<CoreNetworkConfigsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="network-stack-configs" element={ |
|||
<ProtectedRoute requiredPermission="networkstackconfigs.view"> |
|||
<AnimatedContainer> |
|||
<NetworkStackConfigsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
<Route path="users"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="users.view"> |
|||
<AnimatedContainer> |
|||
<UsersView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="roles" element={ |
|||
<ProtectedRoute requiredPermission="roles.view"> |
|||
<AnimatedContainer> |
|||
<RolesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 系统设置路由 */} |
|||
<Route path="settings"> |
|||
<Route index element={<Navigate to="navigation-menus" replace />} /> |
|||
<Route path="navigation-menus" element={ |
|||
<ProtectedRoute requiredPermission="navigationmenus.view"> |
|||
<AnimatedContainer> |
|||
<NavigationMenusView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="permissions" element={ |
|||
<ProtectedRoute requiredPermission="permissions.view"> |
|||
<AnimatedContainer> |
|||
<PermissionsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="button-permissions" element={ |
|||
<ProtectedRoute requiredPermission="buttonpermissions.view"> |
|||
<AnimatedContainer> |
|||
<ButtonPermissionsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 添加更多路由 */} |
|||
</Routes> |
|||
</Suspense> |
|||
</DashboardLayout> |
|||
</ProtectedRoute> |
|||
} |
|||
/> |
|||
</Routes> |
|||
); |
|||
return <DynamicRoutes />; |
|||
} |
|||
@ -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 = () => ( |
|||
<div className="flex items-center justify-center min-h-screen"> |
|||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> |
|||
</div> |
|||
); |
|||
|
|||
export function AppRouter() { |
|||
return ( |
|||
<Routes> |
|||
<Route path="/" element={<Navigate to="/dashboard" replace />} /> |
|||
<Route |
|||
path="/login" |
|||
element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<LoginPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} |
|||
/> |
|||
<Route |
|||
path="/forgot-password" |
|||
element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<ForgotPasswordPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} |
|||
/> |
|||
<Route |
|||
path="/register" |
|||
element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<RegisterPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} |
|||
/> |
|||
<Route path="/403" element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<ForbiddenPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} /> |
|||
<Route |
|||
path="/dashboard/*" |
|||
element={ |
|||
<ProtectedRoute requiredPermission="dashboard.view"> |
|||
<DashboardLayout> |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<Routes> |
|||
<Route index element={<DashboardHome />} /> |
|||
|
|||
|
|||
{/* 场景管理路由 */} |
|||
<Route path="scenarios"> |
|||
<Route index element={<Navigate to="config" replace />} /> |
|||
<Route path="config" element={ |
|||
<ProtectedRoute requiredPermission="scenarios.view"> |
|||
<AnimatedContainer> |
|||
<ScenarioConfigView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="binding" element={ |
|||
<ProtectedRoute requiredPermission="scenarios.manage"> |
|||
<AnimatedContainer> |
|||
<ScenarioBindingView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 用例管理路由 */} |
|||
<Route path="testcases"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="testcases.view"> |
|||
<AnimatedContainer> |
|||
<TestCasesListView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="create" element={ |
|||
<ProtectedRoute requiredPermission="testcases.create"> |
|||
<AnimatedContainer> |
|||
<TestCasesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="steps" element={ |
|||
<ProtectedRoute requiredPermission="teststeps.view"> |
|||
<AnimatedContainer> |
|||
<TestStepsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 任务管理路由 */} |
|||
<Route path="tasks"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="tasks.view"> |
|||
<AnimatedContainer> |
|||
<TasksView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="reviews" element={ |
|||
<ProtectedRoute requiredPermission="taskreviews.view"> |
|||
<AnimatedContainer> |
|||
<TaskReviewView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="executions" element={ |
|||
<ProtectedRoute requiredPermission="taskexecutions.view"> |
|||
<AnimatedContainer> |
|||
<TaskExecutionView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 结果分析路由 */} |
|||
<Route path="analysis"> |
|||
<Route index element={<Navigate to="functional" replace />} /> |
|||
<Route path="functional" element={ |
|||
<ProtectedRoute requiredPermission="functionalanalysis.view"> |
|||
<AnimatedContainer> |
|||
<FunctionalAnalysisView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="performance" element={ |
|||
<ProtectedRoute requiredPermission="performanceanalysis.view"> |
|||
<AnimatedContainer> |
|||
<PerformanceAnalysisView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="issue" element={ |
|||
<ProtectedRoute requiredPermission="issueanalysis.view"> |
|||
<AnimatedContainer> |
|||
<IssueAnalysisView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="ue" element={ |
|||
<ProtectedRoute requiredPermission="ueanalysis.view"> |
|||
<AnimatedContainer> |
|||
<UEAnalysisView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 仪表管理路由 */} |
|||
<Route path="instruments"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="devices.view"> |
|||
<AnimatedContainer> |
|||
<DevicesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="protocols" element={ |
|||
<ProtectedRoute requiredPermission="protocols.view"> |
|||
<AnimatedContainer> |
|||
<ProtocolsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="device-runtimes"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="deviceruntimes.view"> |
|||
<AnimatedContainer> |
|||
<DeviceRuntimesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
</Route> |
|||
|
|||
{/* 终端服务管理路由 */} |
|||
<Route path="terminal-services"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="terminalservices.view"> |
|||
<AnimatedContainer> |
|||
<TerminalServicesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="adb-operations" element={ |
|||
<ProtectedRoute requiredPermission="adboperations.view"> |
|||
<AnimatedContainer> |
|||
<AdbOperationsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="at-operations" element={ |
|||
<ProtectedRoute requiredPermission="atoperations.view"> |
|||
<AnimatedContainer> |
|||
<AtOperationsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 终端设备管理路由 */} |
|||
<Route path="terminal-devices"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="terminaldevices.view"> |
|||
<AnimatedContainer> |
|||
<TerminalDevicesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 信令分析路由 */} |
|||
<Route path="protocol-logs"> |
|||
<Route index element={<Navigate to="online-logs" replace />} /> |
|||
<Route path="online-logs" element={ |
|||
<ProtectedRoute requiredPermission="protocollogs.view"> |
|||
<AnimatedContainer> |
|||
<OnlineProtocolLogsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="history-logs" element={ |
|||
<ProtectedRoute requiredPermission="protocollogs.view"> |
|||
<AnimatedContainer> |
|||
<HistoryProtocolLogsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 网络栈配置管理路由 */} |
|||
<Route path="network-stack-configs"> |
|||
<Route index element={<Navigate to="ran-configurations" replace />} /> |
|||
<Route path="ran-configurations" element={ |
|||
<ProtectedRoute requiredPermission="ranconfigurations.view"> |
|||
<AnimatedContainer> |
|||
<RANConfigurationsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="ims-configurations" element={ |
|||
<ProtectedRoute requiredPermission="imsconfigurations.view"> |
|||
<AnimatedContainer> |
|||
<IMSConfigurationsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="core-network-configs" element={ |
|||
<ProtectedRoute requiredPermission="corenetworkconfigs.view"> |
|||
<AnimatedContainer> |
|||
<CoreNetworkConfigsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="network-stack-configs" element={ |
|||
<ProtectedRoute requiredPermission="networkstackconfigs.view"> |
|||
<AnimatedContainer> |
|||
<NetworkStackConfigsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
<Route path="users"> |
|||
<Route index element={<Navigate to="list" replace />} /> |
|||
<Route path="list" element={ |
|||
<ProtectedRoute requiredPermission="users.view"> |
|||
<AnimatedContainer> |
|||
<UsersView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="roles" element={ |
|||
<ProtectedRoute requiredPermission="roles.view"> |
|||
<AnimatedContainer> |
|||
<RolesView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 系统设置路由 */} |
|||
<Route path="settings"> |
|||
<Route index element={<Navigate to="navigation-menus" replace />} /> |
|||
<Route path="navigation-menus" element={ |
|||
<ProtectedRoute requiredPermission="navigationmenus.view"> |
|||
<AnimatedContainer> |
|||
<NavigationMenusView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="permissions" element={ |
|||
<ProtectedRoute requiredPermission="permissions.view"> |
|||
<AnimatedContainer> |
|||
<PermissionsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
<Route path="button-permissions" element={ |
|||
<ProtectedRoute requiredPermission="buttonpermissions.view"> |
|||
<AnimatedContainer> |
|||
<ButtonPermissionsView /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} /> |
|||
</Route> |
|||
|
|||
{/* 添加更多路由 */} |
|||
</Routes> |
|||
</Suspense> |
|||
</DashboardLayout> |
|||
</ProtectedRoute> |
|||
} |
|||
/> |
|||
</Routes> |
|||
); |
|||
} |
|||
@ -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 ( |
|||
<Route |
|||
key={routeKey} |
|||
path={routePath} |
|||
element={ |
|||
<ProtectedRoute requiredPermission={menuItem.permission}> |
|||
<AnimatedContainer> |
|||
<Component /> |
|||
</AnimatedContainer> |
|||
</ProtectedRoute> |
|||
} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
// 生成路由组
|
|||
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 ( |
|||
<Route key={menuItem.id} path={parentPath}> |
|||
{/* 默认重定向到第一个子路由 */} |
|||
<Route |
|||
index |
|||
element={<Navigate to={defaultPath} replace />} |
|||
/> |
|||
{childRoutes} |
|||
</Route> |
|||
); |
|||
}; |
|||
|
|||
// 动态路由生成器主组件
|
|||
export const DynamicRouteGenerator: React.FC<DynamicRouteGeneratorProps> = ({ 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[]; |
|||
}; |
|||
@ -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 = () => ( |
|||
<div className="flex items-center justify-center min-h-screen"> |
|||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> |
|||
</div> |
|||
); |
|||
|
|||
// 动态路由组件
|
|||
const DynamicRoutes: React.FC = () => { |
|||
const [menuItems, setMenuItems] = useState<MenuItem[]>([]); |
|||
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 <LoadingFallback />; |
|||
} |
|||
|
|||
// 生成动态路由
|
|||
const dynamicRoutes = generateAllRoutes(menuItems); |
|||
|
|||
return ( |
|||
<Routes> |
|||
<Route path="/" element={<Navigate to="/dashboard" replace />} /> |
|||
|
|||
{/* 静态认证路由 */} |
|||
<Route |
|||
path="/login" |
|||
element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<LoginPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} |
|||
/> |
|||
<Route |
|||
path="/forgot-password" |
|||
element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<ForgotPasswordPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} |
|||
/> |
|||
<Route |
|||
path="/register" |
|||
element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<RegisterPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} |
|||
/> |
|||
<Route path="/403" element={ |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<AnimatedContainer> |
|||
<ForbiddenPage /> |
|||
</AnimatedContainer> |
|||
</Suspense> |
|||
} /> |
|||
|
|||
{/* 动态仪表板路由 */} |
|||
<Route |
|||
path="/dashboard/*" |
|||
element={ |
|||
<ProtectedRoute requiredPermission="dashboard.view"> |
|||
<DashboardLayout> |
|||
<Suspense fallback={<LoadingFallback />}> |
|||
<Routes> |
|||
<Route index element={<DashboardHome />} /> |
|||
|
|||
{/* 动态生成的路由 */} |
|||
{...dynamicRoutes} |
|||
|
|||
{/* 404 路由 */} |
|||
<Route path="*" element={<Navigate to="/403" replace />} /> |
|||
</Routes> |
|||
</Suspense> |
|||
</DashboardLayout> |
|||
</ProtectedRoute> |
|||
} |
|||
/> |
|||
|
|||
{/* 404 路由 */} |
|||
<Route path="*" element={<Navigate to="/403" replace />} /> |
|||
</Routes> |
|||
); |
|||
}; |
|||
|
|||
export default DynamicRoutes; |
|||
@ -0,0 +1,142 @@ |
|||
import { lazy } from 'react'; |
|||
import { MenuItem } from '@/constants/menuConfig'; |
|||
|
|||
// 路由组件映射表
|
|||
export const routeComponentMap: Record<string, React.LazyExoticComponent<any>> = { |
|||
// 场景管理
|
|||
'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<string, string> = { |
|||
// 场景管理
|
|||
'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<any> | 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; |
|||
}; |
|||
@ -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; |
|||
} |
|||
@ -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<string, LucideIcon> = { |
|||
// 基础图标名称映射
|
|||
'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<string, React.ReactNode> = {}; |
|||
COMMON_ICONS.forEach(iconName => { |
|||
iconComponentMapping[iconName] = getIconComponent(iconName); |
|||
}); |
|||
|
|||
// 获取图标选项列表
|
|||
export const getIconOptions = (): string[] => COMMON_ICONS; |
|||
|
|||
// 获取菜单类型对应的图标组件
|
|||
export const getMenuTypeIconComponent = (type: number): React.ReactNode => { |
|||
const iconMap: Record<number, LucideIcon> = { |
|||
1: LayoutDashboard, // StandaloneMenuItem
|
|||
2: Plus, // MenuGroup
|
|||
3: FileText, // SubMenuItem
|
|||
}; |
|||
const IconComponent = iconMap[type] || LayoutDashboard; |
|||
return React.createElement(IconComponent, { className: "h-4 w-4" }); |
|||
}; |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue