feat(权限): 实现基于角色的路由权限控制
- 新增路由元信息类型定义 (requiresAuth, roles, title) - 实现路由守卫中的角色权限校验逻辑 - 新增 403 禁止访问页面 - 提取权限校验函数 checkRoutePermission,提高可测试性 - 修复 JSON.parse 异常处理,增强健壮性 - 优化页面标题动态设置 测试优化: - 重构 global-setup.ts,支持 JAR 文件启动后端服务 - 优化测试用例等待逻辑,减少硬编码延迟 - 简化 playwright 配置,移除多浏览器支持 - 新增路由权限守卫单元测试 关联需求:权限系统完善
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const mockLocalStorage = {
|
||||
store: {} as Record<string, string>,
|
||||
getItem(key: string) {
|
||||
return this.store[key] || null
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
this.store[key] = value
|
||||
},
|
||||
removeItem(key: string) {
|
||||
delete this.store[key]
|
||||
},
|
||||
clear() {
|
||||
this.store = {}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockLocalStorage
|
||||
})
|
||||
|
||||
const createTestRouter = (routes: RouteRecordRaw[]) => {
|
||||
return createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
}
|
||||
|
||||
describe('路由守卫权限检查', () => {
|
||||
beforeEach(() => {
|
||||
mockLocalStorage.clear()
|
||||
})
|
||||
|
||||
describe('基础认证检查', () => {
|
||||
it('未登录用户访问受保护路由应重定向到登录页', async () => {
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
await router.push('/dashboard')
|
||||
expect(router.currentRoute.value.path).toBe('/login')
|
||||
})
|
||||
|
||||
it('已登录用户访问受保护路由应允许通过', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
await router.push('/dashboard')
|
||||
expect(router.currentRoute.value.path).toBe('/dashboard')
|
||||
})
|
||||
})
|
||||
|
||||
describe('角色权限检查', () => {
|
||||
it('普通用户访问管理员路由应重定向到403页面', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
mockLocalStorage.setItem('roles', JSON.stringify(['user']))
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: { template: '<div>403 Forbidden</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: { template: '<div>UserManagement</div>' },
|
||||
meta: { roles: ['admin'] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
const userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const hasRole = to.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
if (!hasRole) {
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
await router.push('/users')
|
||||
expect(router.currentRoute.value.path).toBe('/403')
|
||||
})
|
||||
|
||||
it('管理员用户访问管理员路由应允许通过', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
mockLocalStorage.setItem('roles', JSON.stringify(['admin']))
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: { template: '<div>403 Forbidden</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: { template: '<div>UserManagement</div>' },
|
||||
meta: { roles: ['admin'] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
const userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const hasRole = to.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
if (!hasRole) {
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
await router.push('/users')
|
||||
expect(router.currentRoute.value.path).toBe('/users')
|
||||
})
|
||||
|
||||
it('无角色要求的路由所有登录用户都可访问', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
mockLocalStorage.setItem('roles', JSON.stringify(['user']))
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
const userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const hasRole = to.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
if (!hasRole) {
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
await router.push('/dashboard')
|
||||
expect(router.currentRoute.value.path).toBe('/dashboard')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<template #title>
|
||||
<el-icon v-if="menu.icon">
|
||||
<component :is="menu.icon" />
|
||||
<component :is="iconComponents[menu.icon]" />
|
||||
</el-icon>
|
||||
<span>{{ menu.name }}</span>
|
||||
</template>
|
||||
@@ -21,7 +21,7 @@
|
||||
:index="menu.path"
|
||||
>
|
||||
<el-icon v-if="menu.icon">
|
||||
<component :is="menu.icon" />
|
||||
<component :is="iconComponents[menu.icon]" />
|
||||
</el-icon>
|
||||
<span>{{ menu.name }}</span>
|
||||
</el-menu-item>
|
||||
@@ -29,6 +29,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MenuItem as MenuItemType } from '@/stores/permission'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import { markRaw, type Component } from 'vue'
|
||||
|
||||
const iconComponents: Record<string, Component> = {}
|
||||
Object.keys(ElementPlusIconsVue).forEach(key => {
|
||||
iconComponents[key] = markRaw(ElementPlusIconsVue[key as keyof typeof ElementPlusIconsVue])
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
menu: MenuItemType
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
active-text-color="#409eff"
|
||||
router
|
||||
>
|
||||
<div v-if="menuTree.length === 0" style="padding: 20px; text-align: center; color: #999;">
|
||||
菜单加载中...
|
||||
</div>
|
||||
<menu-item
|
||||
v-for="menu in menuTree"
|
||||
:key="menu.id"
|
||||
@@ -75,7 +78,9 @@ const username = ref(localStorage.getItem('username') || 'Admin')
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const activeMenu = computed(() => route.path)
|
||||
const menuTree = computed(() => permissionStore.menus)
|
||||
const menuTree = computed(() => {
|
||||
return permissionStore.menus
|
||||
})
|
||||
|
||||
const handleCommand = (command: string) => {
|
||||
if (command === 'profile') {
|
||||
@@ -87,12 +92,21 @@ const handleCommand = (command: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (!token) {
|
||||
router.push('/login')
|
||||
} else if (!permissionStore.loaded) {
|
||||
permissionStore.initFromStorage()
|
||||
|
||||
if (!permissionStore.loaded || permissionStore.menus.length === 0) {
|
||||
try {
|
||||
await permissionStore.fetchUserMenus()
|
||||
} catch (error) {
|
||||
console.error('获取用户菜单失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,71 +1,98 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
requiresAuth?: boolean
|
||||
roles?: string[]
|
||||
title?: string
|
||||
}
|
||||
}
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/system/Login.vue')
|
||||
component: () => import('@/views/system/Login.vue'),
|
||||
meta: { title: '登录' }
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: () => import('@/views/system/Forbidden.vue'),
|
||||
meta: { title: '无权限' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/DefaultLayout.vue'),
|
||||
redirect: '/dashboard',
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/system/Dashboard.vue')
|
||||
component: () => import('@/views/system/Dashboard.vue'),
|
||||
meta: { title: '仪表盘' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: () => import('@/views/system/UserManagement.vue')
|
||||
component: () => import('@/views/system/UserManagement.vue'),
|
||||
meta: { title: '用户管理' }
|
||||
},
|
||||
{
|
||||
path: 'roles',
|
||||
name: 'RoleManagement',
|
||||
component: () => import('@/views/system/RoleManagement.vue')
|
||||
component: () => import('@/views/system/RoleManagement.vue'),
|
||||
meta: { title: '角色管理' }
|
||||
},
|
||||
{
|
||||
path: 'menus',
|
||||
name: 'MenuManagement',
|
||||
component: () => import('@/views/system/MenuManagement.vue')
|
||||
component: () => import('@/views/system/MenuManagement.vue'),
|
||||
meta: { title: '菜单管理' }
|
||||
},
|
||||
{
|
||||
path: 'sys/config',
|
||||
name: 'ConfigManagement',
|
||||
component: () => import('@/views/config/ConfigManagement.vue')
|
||||
component: () => import('@/views/config/ConfigManagement.vue'),
|
||||
meta: { title: '参数配置' }
|
||||
},
|
||||
{
|
||||
path: 'dict',
|
||||
name: 'DictManagement',
|
||||
component: () => import('@/views/config/DictManagement.vue')
|
||||
component: () => import('@/views/config/DictManagement.vue'),
|
||||
meta: { title: '字典管理' }
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
name: 'FileManagement',
|
||||
component: () => import('@/views/file/FileManagement.vue')
|
||||
component: () => import('@/views/file/FileManagement.vue'),
|
||||
meta: { title: '文件管理' }
|
||||
},
|
||||
{
|
||||
path: 'notice',
|
||||
name: 'NoticeManagement',
|
||||
component: () => import('@/views/notify/NoticeManagement.vue')
|
||||
component: () => import('@/views/notify/NoticeManagement.vue'),
|
||||
meta: { title: '通知公告' }
|
||||
},
|
||||
{
|
||||
path: 'loginlog',
|
||||
name: 'LoginLog',
|
||||
component: () => import('@/views/audit/LoginLog.vue')
|
||||
component: () => import('@/views/audit/LoginLog.vue'),
|
||||
meta: { title: '登录日志' }
|
||||
},
|
||||
{
|
||||
path: 'oplog',
|
||||
name: 'OperationLog',
|
||||
component: () => import('@/views/audit/OperationLog.vue')
|
||||
component: () => import('@/views/audit/OperationLog.vue'),
|
||||
meta: { title: '操作日志' }
|
||||
},
|
||||
{
|
||||
path: 'exceptionlog',
|
||||
name: 'ExceptionLog',
|
||||
component: () => import('@/views/audit/ExceptionLog.vue')
|
||||
component: () => import('@/views/audit/ExceptionLog.vue'),
|
||||
meta: { title: '异常日志' }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -76,9 +103,29 @@ const router = createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
function checkRoutePermission(route: RouteLocationNormalized, userRoles: string[]): boolean {
|
||||
if (!route.meta.roles || !Array.isArray(route.meta.roles) || route.meta.roles.length === 0) {
|
||||
return true
|
||||
}
|
||||
return route.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
}
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
let userRoles: string[] = []
|
||||
|
||||
try {
|
||||
userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
} catch (e) {
|
||||
console.warn('解析用户角色失败,将使用空数组:', e)
|
||||
userRoles = []
|
||||
}
|
||||
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - Novalon 管理系统`
|
||||
}
|
||||
|
||||
if (to.path === '/login') {
|
||||
if (token) {
|
||||
@@ -86,12 +133,21 @@ router.beforeEach((to, from, next) => {
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else if (to.path === '/403') {
|
||||
next()
|
||||
} else {
|
||||
if (token) {
|
||||
next()
|
||||
} else {
|
||||
if (to.meta.requiresAuth !== false && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (!checkRoutePermission(to, userRoles)) {
|
||||
console.warn(`用户角色 ${userRoles} 无权访问路由 ${to.path},需要角色: ${to.meta.roles}`)
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('路由守卫错误:', error)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,53 +1,44 @@
|
||||
export const formatDateTime = (dateTime: string | Date | null | undefined): string => {
|
||||
if (!dateTime) return '-'
|
||||
import { format, parseISO } from 'date-fns'
|
||||
import { zhCN } from 'date-fns/locale'
|
||||
|
||||
export function formatDateTime(date: string | Date | null | undefined): string {
|
||||
if (!date) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
try {
|
||||
const date = typeof dateTime === 'string' ? new Date(dateTime) : dateTime
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date
|
||||
return format(dateObj, 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })
|
||||
} catch (error) {
|
||||
console.error('时间格式化失败:', error)
|
||||
return String(dateTime)
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
export const formatDate = (date: string | Date | null | undefined): string => {
|
||||
if (!date) return '-'
|
||||
|
||||
export function formatDate(date: string | Date | null | undefined): string {
|
||||
if (!date) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
try {
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day}`
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date
|
||||
return format(dateObj, 'yyyy-MM-dd', { locale: zhCN })
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error)
|
||||
return String(date)
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
export const formatTime = (time: string | Date | null | undefined): string => {
|
||||
if (!time) return '-'
|
||||
|
||||
export function formatTime(date: string | Date | null | undefined): string {
|
||||
if (!date) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
try {
|
||||
const t = typeof time === 'string' ? new Date(time) : time
|
||||
|
||||
const hours = String(t.getHours()).padStart(2, '0')
|
||||
const minutes = String(t.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(t.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${hours}:${minutes}:${seconds}`
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date
|
||||
return format(dateObj, 'HH:mm:ss', { locale: zhCN })
|
||||
} catch (error) {
|
||||
console.error('时间格式化失败:', error)
|
||||
return String(time)
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,29 @@ export interface PermissionMapping {
|
||||
}
|
||||
|
||||
const permissionMapping: PermissionMapping = {
|
||||
'GET /users': 'user:list',
|
||||
'POST /users': 'user:create',
|
||||
'PUT /users': 'user:update',
|
||||
'DELETE /users': 'user:delete',
|
||||
'GET /roles': 'role:list',
|
||||
'POST /roles': 'role:create',
|
||||
'PUT /roles': 'role:update',
|
||||
'DELETE /roles': 'role:delete',
|
||||
'GET /menus': 'menu:list',
|
||||
'POST /menus': 'menu:create',
|
||||
'PUT /menus': 'menu:update',
|
||||
'DELETE /menus': 'menu:delete',
|
||||
'GET /dict': 'dict:list',
|
||||
'POST /dict': 'dict:create',
|
||||
'PUT /dict': 'dict:update',
|
||||
'DELETE /dict': 'dict:delete',
|
||||
'GET /sys/config': 'config:list',
|
||||
'POST /sys/config': 'config:create',
|
||||
'PUT /sys/config': 'config:update',
|
||||
'DELETE /sys/config': 'config:delete',
|
||||
'GET /users': 'system:user:list',
|
||||
'POST /users': 'system:user:add',
|
||||
'PUT /users': 'system:user:edit',
|
||||
'DELETE /users': 'system:user:remove',
|
||||
'GET /roles': 'system:role:list',
|
||||
'POST /roles': 'system:role:add',
|
||||
'PUT /roles': 'system:role:edit',
|
||||
'DELETE /roles': 'system:role:remove',
|
||||
'GET /menus': 'system:menu:list',
|
||||
'POST /menus': 'system:menu:add',
|
||||
'PUT /menus': 'system:menu:edit',
|
||||
'DELETE /menus': 'system:menu:remove',
|
||||
'GET /dict': 'system:dict:list',
|
||||
'POST /dict': 'system:dict:add',
|
||||
'PUT /dict': 'system:dict:edit',
|
||||
'DELETE /dict': 'system:dict:remove',
|
||||
'GET /sys/config': 'system:config:list',
|
||||
'POST /sys/config': 'system:config:add',
|
||||
'PUT /sys/config': 'system:config:edit',
|
||||
'DELETE /sys/config': 'system:config:remove',
|
||||
'GET /files': 'system:file:list',
|
||||
'POST /files': 'system:file:upload',
|
||||
'DELETE /files': 'system:file:delete',
|
||||
}
|
||||
|
||||
export function checkApiPermission(method: string, url: string): boolean {
|
||||
@@ -37,6 +40,10 @@ export function checkApiPermission(method: string, url: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
if (key === 'GET /menus') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Array.isArray(requiredPermission)) {
|
||||
return requiredPermission.some(p => permissionStore.hasPermission(p))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026'
|
||||
const SIGNATURE_SECRET = import.meta.env.VITE_SIGNATURE_SECRET || 'NovalonManageSystemSecretKey2026'
|
||||
|
||||
export interface SignatureHeaders {
|
||||
'X-Signature': string
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
v-for="item in recentLogins"
|
||||
:key="item.id"
|
||||
:type="item.status === '0' ? 'success' : 'danger'"
|
||||
:timestamp="item.loginTime"
|
||||
:timestamp="formatDateTime(item.loginTime)"
|
||||
placement="top"
|
||||
>
|
||||
<div class="login-item">
|
||||
@@ -171,6 +171,7 @@
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { User, UserFilled, ArrowRight, Document, Clock, Location, Setting, Star, Cpu, Monitor, Coin } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const stats = reactive({
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="forbidden-container">
|
||||
<el-result
|
||||
icon="warning"
|
||||
title="403"
|
||||
sub-title="抱歉,您没有权限访问此页面"
|
||||
>
|
||||
<template #extra>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="goBack"
|
||||
>
|
||||
返回上一页
|
||||
</el-button>
|
||||
<el-button @click="goHome">
|
||||
返回首页
|
||||
</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.forbidden-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
@@ -105,8 +105,8 @@ const onFinish = async () => {
|
||||
|
||||
try {
|
||||
await permissionStore.fetchUserMenus()
|
||||
} catch (fetchError) {
|
||||
console.warn('获取用户菜单失败:', fetchError)
|
||||
} catch (menuError) {
|
||||
console.error('获取用户菜单失败:', menuError)
|
||||
}
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
|
||||
Reference in New Issue
Block a user