feat(权限): 实现基于角色的路由权限控制

- 新增路由元信息类型定义 (requiresAuth, roles, title)
- 实现路由守卫中的角色权限校验逻辑
- 新增 403 禁止访问页面
- 提取权限校验函数 checkRoutePermission,提高可测试性
- 修复 JSON.parse 异常处理,增强健壮性
- 优化页面标题动态设置

测试优化:
- 重构 global-setup.ts,支持 JAR 文件启动后端服务
- 优化测试用例等待逻辑,减少硬编码延迟
- 简化 playwright 配置,移除多浏览器支持
- 新增路由权限守卫单元测试

关联需求:权限系统完善
This commit is contained in:
张翔
2026-04-08 15:29:03 +08:00
parent 9b2c8a47a4
commit 7420afa380
23 changed files with 933 additions and 349 deletions
+105 -4
View File
@@ -11,6 +11,92 @@ export interface MenuItem {
children?: MenuItem[]
}
interface BackendMenuItem {
id: number
menuName: string
parentId: number
orderNum: number
menuType: string
perms?: string
component?: string
status: number
children?: BackendMenuItem[]
}
function transformMenuData(backendMenus: BackendMenuItem[]): MenuItem[] {
const menuMap = new Map<number, MenuItem>()
const rootMenus: MenuItem[] = []
const componentToPathMap: Record<string, string> = {
'system/user/index': '/users',
'system/role/index': '/roles',
'system/menu/index': '/menus',
'system/dict/index': '/dict',
'system/config/index': '/sys/config',
'system/notice/index': '/notice',
'system/file/index': '/files',
'audit/operation/index': '/oplog',
'audit/login/index': '/loginlog',
'audit/exception/index': '/exceptionlog',
}
const filteredMenus = backendMenus.filter(menu => menu.menuType !== 'F')
filteredMenus.forEach(menu => {
const menuItem: MenuItem = {
id: menu.id,
name: menu.menuName,
path: menu.component ? (componentToPathMap[menu.component] || `/${menu.component.replace('/index', '').replace('system/', '')}`) : '',
icon: getMenuIcon(menu.menuName),
parentId: menu.parentId === 0 ? undefined : menu.parentId,
sort: menu.orderNum
}
menuMap.set(menu.id, menuItem)
})
filteredMenus.forEach(menu => {
const menuItem = menuMap.get(menu.id)!
if (menu.parentId === 0) {
rootMenus.push(menuItem)
} else {
const parentMenu = menuMap.get(menu.parentId)
if (parentMenu) {
if (!parentMenu.children) {
parentMenu.children = []
}
parentMenu.children.push(menuItem)
}
}
})
rootMenus.forEach(menu => {
if (menu.children) {
menu.children.sort((a, b) => a.sort - b.sort)
}
})
return rootMenus.sort((a, b) => a.sort - b.sort)
}
function getMenuIcon(menuName: string): string {
const iconMap: Record<string, string> = {
'系统管理': 'Setting',
'审计日志': 'Document',
'系统监控': 'Monitor',
'用户管理': 'User',
'角色管理': 'UserFilled',
'菜单管理': 'Menu',
'字典管理': 'Collection',
'参数配置': 'Tools',
'通知公告': 'Bell',
'文件管理': 'Folder',
'操作日志': 'Document',
'登录日志': 'Document',
'异常日志': 'Warning'
}
return iconMap[menuName] || 'Document'
}
interface PermissionState {
roles: string[]
permissions: string[]
@@ -91,13 +177,28 @@ export const usePermissionStore = defineStore('permission', {
async fetchUserMenus() {
try {
const res: any = await request.get('/menus/user')
const res: any = await request.get('/menus')
if (res && res.data) {
if (res && Array.isArray(res)) {
const transformedMenus = transformMenuData(res)
const permissions: string[] = []
const extractPermissions = (menus: BackendMenuItem[]) => {
menus.forEach(menu => {
if (menu.perms) {
permissions.push(menu.perms)
}
if (menu.children && menu.children.length > 0) {
extractPermissions(menu.children)
}
})
}
extractPermissions(res)
this.setPermissionData({
roles: JSON.parse(localStorage.getItem('roles') || '[]'),
permissions: res.data.permissions || [],
menus: res.data.menus || []
permissions: permissions,
menus: transformedMenus
})
}
} catch (error) {