前端启动成功
This commit is contained in:
255
schoolNewsWeb/src/utils/permission.ts
Normal file
255
schoolNewsWeb/src/utils/permission.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* @description 路由守卫和权限检查
|
||||
* @author yslg
|
||||
* @since 2025-10-07
|
||||
*/
|
||||
|
||||
import type { Router, NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
|
||||
import type { Store } from 'vuex';
|
||||
import { AuthState } from '@/store/modules/auth';
|
||||
|
||||
/**
|
||||
* 白名单路由 - 无需登录即可访问
|
||||
*/
|
||||
const WHITE_LIST = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/forgot-password',
|
||||
'/404',
|
||||
'/403',
|
||||
'/500'
|
||||
];
|
||||
|
||||
/**
|
||||
* 设置路由守卫
|
||||
* @param router Vue Router实例
|
||||
* @param store Vuex Store实例
|
||||
*/
|
||||
export function setupRouterGuards(router: Router, store: Store<any>) {
|
||||
|
||||
// 全局前置守卫
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 开始页面加载进度条
|
||||
startProgress();
|
||||
|
||||
try {
|
||||
await handleRouteGuard(to, from, next, store);
|
||||
} catch (error) {
|
||||
console.error('路由守卫执行失败:', error);
|
||||
// 发生错误时跳转到500页面
|
||||
next('/500');
|
||||
}
|
||||
});
|
||||
|
||||
// 全局后置钩子
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 结束页面加载进度条
|
||||
finishProgress();
|
||||
|
||||
// 设置页面标题
|
||||
setPageTitle(to.meta?.title as string);
|
||||
});
|
||||
|
||||
// 全局解析守卫(在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用)
|
||||
router.beforeResolve(async (to, from, next) => {
|
||||
// 这里可以处理一些最终的权限检查或数据预加载
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理路由守卫逻辑
|
||||
*/
|
||||
async function handleRouteGuard(
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
store: Store<any>
|
||||
) {
|
||||
const authState: AuthState = store.state.auth;
|
||||
const { isAuthenticated } = store.getters['auth/isAuthenticated'];
|
||||
|
||||
// 检查是否在白名单中
|
||||
if (isInWhiteList(to.path)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 检查用户是否已登录
|
||||
if (!isAuthenticated) {
|
||||
// 未登录,重定向到登录页
|
||||
return next({
|
||||
path: '/login',
|
||||
query: {
|
||||
redirect: to.fullPath // 记录用户想要访问的页面
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 用户已登录,检查是否需要生成动态路由
|
||||
if (!authState.routesLoaded) {
|
||||
try {
|
||||
// 生成动态路由
|
||||
await store.dispatch('auth/generateRoutes');
|
||||
|
||||
// 重新导航到目标路由
|
||||
return next({ ...to, replace: true });
|
||||
} catch (error) {
|
||||
console.error('生成动态路由失败:', error);
|
||||
// 清除认证信息并跳转到登录页
|
||||
store.commit('auth/CLEAR_AUTH');
|
||||
return next('/login');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查页面权限
|
||||
const hasPermission = await checkPagePermission(to, store);
|
||||
if (!hasPermission) {
|
||||
// 无权限访问,跳转到403页面
|
||||
return next('/403');
|
||||
}
|
||||
|
||||
// 所有检查通过,继续导航
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否在白名单中
|
||||
*/
|
||||
function isInWhiteList(path: string): boolean {
|
||||
return WHITE_LIST.some(whitePath => {
|
||||
if (whitePath.endsWith('*')) {
|
||||
// 支持通配符匹配
|
||||
const prefix = whitePath.slice(0, -1);
|
||||
return path.startsWith(prefix);
|
||||
}
|
||||
return path === whitePath;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查页面权限
|
||||
*/
|
||||
async function checkPagePermission(
|
||||
route: RouteLocationNormalized,
|
||||
store: Store<any>
|
||||
): Promise<boolean> {
|
||||
// 如果路由元信息中没有要求权限,则允许访问
|
||||
if (route.meta?.requiresAuth === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查路由是否需要特定权限
|
||||
const requiredPermissions = route.meta?.permissions as string[] | undefined;
|
||||
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
// 无特定权限要求,但需要登录,已经在前面检查过登录状态
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查用户是否有所需权限
|
||||
const hasPermission = store.getters['auth/hasAnyPermission'];
|
||||
return hasPermission(requiredPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面标题
|
||||
*/
|
||||
function setPageTitle(title?: string) {
|
||||
const appTitle = '校园新闻管理系统';
|
||||
document.title = title ? `${title} - ${appTitle}` : appTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始进度条(可以集成 NProgress 或其他进度条库)
|
||||
*/
|
||||
function startProgress() {
|
||||
// TODO: 集成进度条库,如 NProgress
|
||||
// NProgress.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成进度条
|
||||
*/
|
||||
function finishProgress() {
|
||||
// TODO: 集成进度条库,如 NProgress
|
||||
// NProgress.done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Token自动刷新中间件
|
||||
*/
|
||||
export function setupTokenRefresh(store: Store<any>) {
|
||||
// 设置定时器自动刷新Token
|
||||
setInterval(async () => {
|
||||
const authState: AuthState = store.state.auth;
|
||||
|
||||
if (!authState.token || !authState.loginDomain) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查Token是否快要过期(例如:提前5分钟刷新)
|
||||
const tokenExpireTime = authState.loginDomain.tokenExpireTime;
|
||||
if (tokenExpireTime) {
|
||||
const expireTime = new Date(tokenExpireTime).getTime();
|
||||
const currentTime = Date.now();
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
|
||||
if (expireTime - currentTime <= fiveMinutes) {
|
||||
try {
|
||||
await store.dispatch('auth/refreshToken');
|
||||
console.log('Token自动刷新成功');
|
||||
} catch (error) {
|
||||
console.error('Token自动刷新失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 60 * 1000); // 每分钟检查一次
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查工具函数
|
||||
*/
|
||||
export class PermissionChecker {
|
||||
private store: Store<any>;
|
||||
|
||||
constructor(store: Store<any>) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有指定权限
|
||||
*/
|
||||
hasPermission(permissionCode: string): boolean {
|
||||
return this.store.getters['auth/hasPermission'](permissionCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有任意一个权限
|
||||
*/
|
||||
hasAnyPermission(permissionCodes: string[]): boolean {
|
||||
return this.store.getters['auth/hasAnyPermission'](permissionCodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有所有权限
|
||||
*/
|
||||
hasAllPermissions(permissionCodes: string[]): boolean {
|
||||
return this.store.getters['auth/hasAllPermissions'](permissionCodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有指定角色
|
||||
*/
|
||||
hasRole(roleCode: string): boolean {
|
||||
const userRoles = this.store.getters['auth/userRoles'];
|
||||
return userRoles.some((role: any) => role.code === roleCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有任意一个角色
|
||||
*/
|
||||
hasAnyRole(roleCodes: string[]): boolean {
|
||||
return roleCodes.some(code => this.hasRole(code));
|
||||
}
|
||||
}
|
||||
295
schoolNewsWeb/src/utils/route-generator.ts
Normal file
295
schoolNewsWeb/src/utils/route-generator.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* @description 动态路由生成器
|
||||
* @author yslg
|
||||
* @since 2025-10-07
|
||||
*/
|
||||
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import type { SysMenu } from '@/types';
|
||||
import { MenuType } from '@/types/enums';
|
||||
|
||||
/**
|
||||
* 布局组件映射
|
||||
*/
|
||||
const LAYOUT_MAP: Record<string, () => Promise<any>> = {
|
||||
// 基础布局
|
||||
'BasicLayout': () => import('@/layouts/BasicLayout.vue'),
|
||||
// 空白布局
|
||||
'BlankLayout': () => import('@/layouts/BlankLayout.vue'),
|
||||
// 页面布局
|
||||
'PageLayout': () => import('@/layouts/PageLayout.vue'),
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据菜单生成路由配置
|
||||
* @param menus 用户菜单列表
|
||||
* @returns Vue Router路由配置数组
|
||||
*/
|
||||
export function generateRoutes(menus: SysMenu[]): RouteRecordRaw[] {
|
||||
if (!menus || menus.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const routes: RouteRecordRaw[] = [];
|
||||
|
||||
// 构建菜单树
|
||||
const menuTree = buildMenuTree(menus);
|
||||
|
||||
// 生成路由
|
||||
menuTree.forEach(menu => {
|
||||
const route = generateRouteFromMenu(menu);
|
||||
if (route) {
|
||||
routes.push(route);
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据单个菜单生成路由
|
||||
* @param menu 菜单对象
|
||||
* @returns 路由配置
|
||||
*/
|
||||
function generateRouteFromMenu(menu: SysMenu): RouteRecordRaw | null {
|
||||
// 只处理目录和菜单类型,忽略按钮类型
|
||||
if (menu.type === MenuType.BUTTON) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const route: any = {
|
||||
path: menu.url || `/${menu.menuID}`,
|
||||
name: menu.menuID,
|
||||
meta: {
|
||||
title: menu.name,
|
||||
icon: menu.icon,
|
||||
menuId: menu.menuID,
|
||||
parentId: menu.parentID,
|
||||
orderNum: menu.orderNum,
|
||||
type: menu.type,
|
||||
hideInMenu: false,
|
||||
requiresAuth: true,
|
||||
}
|
||||
};
|
||||
|
||||
// 根据菜单类型处理组件
|
||||
if (menu.type === MenuType.DIRECTORY) {
|
||||
// 目录类型 - 使用布局组件
|
||||
route.component = getComponent(menu.component || 'BasicLayout');
|
||||
|
||||
// 处理子菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
route.children = [];
|
||||
menu.children.forEach(child => {
|
||||
const childRoute = generateRouteFromMenu(child);
|
||||
if (childRoute) {
|
||||
route.children!.push(childRoute);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 如果是目录但没有子菜单,设置重定向
|
||||
route.redirect = route.path + '/index';
|
||||
}
|
||||
|
||||
} else if (menu.type === MenuType.MENU) {
|
||||
// 菜单类型 - 使用页面组件
|
||||
if (!menu.component) {
|
||||
console.warn(`菜单 ${menu.name} 缺少component字段`);
|
||||
return null;
|
||||
}
|
||||
route.component = getComponent(menu.component);
|
||||
}
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据组件名称获取组件
|
||||
* @param componentName 组件名称/路径
|
||||
* @returns 组件异步加载函数
|
||||
*/
|
||||
function getComponent(componentName: string) {
|
||||
// 检查是否是布局组件
|
||||
if (LAYOUT_MAP[componentName]) {
|
||||
return LAYOUT_MAP[componentName];
|
||||
}
|
||||
|
||||
// 处理页面组件路径
|
||||
let componentPath = componentName;
|
||||
|
||||
// 如果不是以@/开头的完整路径,则添加@/views/前缀
|
||||
if (!componentPath.startsWith('@/')) {
|
||||
// 如果不是以/开头,添加/
|
||||
if (!componentPath.startsWith('/')) {
|
||||
componentPath = '/' + componentPath;
|
||||
}
|
||||
componentPath = '@/views' + componentPath;
|
||||
}
|
||||
|
||||
// 如果没有.vue扩展名,添加它
|
||||
if (!componentPath.endsWith('.vue')) {
|
||||
componentPath += '.vue';
|
||||
}
|
||||
|
||||
// 动态导入组件
|
||||
return () => import(/* @vite-ignore */ componentPath).catch((error) => {
|
||||
console.warn(`组件加载失败: ${componentPath}`, error);
|
||||
// 返回404组件或空组件
|
||||
return import('@/views/error/404.vue').catch(() =>
|
||||
Promise.resolve({
|
||||
template: `<div class="component-error">
|
||||
<h3>组件加载失败</h3>
|
||||
<p>无法加载组件: ${componentPath}</p>
|
||||
<p>错误: ${error.message}</p>
|
||||
</div>`,
|
||||
style: `
|
||||
.component-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #f56565;
|
||||
background: #fed7d7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单树结构
|
||||
* @param menus 菜单列表
|
||||
* @returns 菜单树
|
||||
*/
|
||||
function buildMenuTree(menus: SysMenu[]): SysMenu[] {
|
||||
if (!menus || menus.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const menuMap = new Map<string, SysMenu>();
|
||||
const rootMenus: SysMenu[] = [];
|
||||
|
||||
// 创建菜单映射
|
||||
menus.forEach(menu => {
|
||||
if (menu.menuID) {
|
||||
menuMap.set(menu.menuID, { ...menu, children: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// 构建树结构
|
||||
menus.forEach(menu => {
|
||||
const menuNode = menuMap.get(menu.menuID!);
|
||||
if (!menuNode) return;
|
||||
|
||||
if (!menu.parentID || menu.parentID === '0') {
|
||||
// 根菜单
|
||||
rootMenus.push(menuNode);
|
||||
} else {
|
||||
// 子菜单
|
||||
const parent = menuMap.get(menu.parentID);
|
||||
if (parent) {
|
||||
parent.children = parent.children || [];
|
||||
parent.children.push(menuNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 按orderNum排序
|
||||
const sortMenus = (menus: SysMenu[]): SysMenu[] => {
|
||||
return menus
|
||||
.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0))
|
||||
.map(menu => ({
|
||||
...menu,
|
||||
children: menu.children ? sortMenus(menu.children) : []
|
||||
}));
|
||||
};
|
||||
|
||||
return sortMenus(rootMenus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据权限过滤菜单
|
||||
* @param menus 菜单列表
|
||||
* @param permissions 用户权限列表
|
||||
* @returns 过滤后的菜单列表
|
||||
*/
|
||||
export function filterMenusByPermissions(
|
||||
menus: SysMenu[],
|
||||
permissions: string[]
|
||||
): SysMenu[] {
|
||||
if (!menus || menus.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return menus
|
||||
.filter(() => {
|
||||
// 如果菜单没有设置权限要求,则默认显示
|
||||
// 这里可以根据实际业务需求调整权限检查逻辑
|
||||
return true; // 暂时返回true,后续可以根据菜单的权限字段进行过滤
|
||||
})
|
||||
.map(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
return {
|
||||
...menu,
|
||||
children: filterMenusByPermissions(menu.children, permissions)
|
||||
};
|
||||
}
|
||||
return menu;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找路由路径对应的菜单
|
||||
* @param menus 菜单树
|
||||
* @param path 路由路径
|
||||
* @returns 匹配的菜单
|
||||
*/
|
||||
export function findMenuByPath(menus: SysMenu[], path: string): SysMenu | null {
|
||||
for (const menu of menus) {
|
||||
if (menu.url === path) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findMenuByPath(menu.children, path);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单路径数组(面包屑导航用)
|
||||
* @param menus 菜单树
|
||||
* @param targetMenuId 目标菜单ID
|
||||
* @returns 菜单路径数组
|
||||
*/
|
||||
export function getMenuPath(menus: SysMenu[], targetMenuId: string): SysMenu[] {
|
||||
const path: SysMenu[] = [];
|
||||
|
||||
function findPath(menuList: SysMenu[]): boolean {
|
||||
for (const menu of menuList) {
|
||||
path.push(menu);
|
||||
|
||||
if (menu.menuID === targetMenuId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
if (findPath(menu.children)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
path.pop();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
findPath(menus);
|
||||
return path;
|
||||
}
|
||||
Reference in New Issue
Block a user