mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-11 20:05:44 +00:00
fix(web): 修复代码格式和文件清理问题
清理无用文件和资源,调整代码格式统一双引号,修复部分组件路径引用问题 删除大量无用图片资源和组件文件,统一代码风格使用双引号,修复国际化文件路径引用,调整部分组件逻辑,优化项目结构
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* 组件加载器
|
||||
*
|
||||
* 负责动态加载 Vue 组件
|
||||
*
|
||||
* @module router/core/ComponentLoader
|
||||
* @author Fastapi Admin Team
|
||||
*/
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
export class ComponentLoader {
|
||||
private modules: Record<string, () => Promise<any>>;
|
||||
|
||||
constructor() {
|
||||
// 动态导入 views 目录下所有 .vue 组件
|
||||
this.modules = import.meta.glob('../../views/**/*.vue');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载组件
|
||||
*/
|
||||
load(componentPath: string): () => Promise<any> {
|
||||
if (!componentPath) {
|
||||
return this.createEmptyComponent();
|
||||
}
|
||||
|
||||
// 构建可能的路径
|
||||
const fullPath = `../../views${componentPath}.vue`;
|
||||
const fullPathWithIndex = `../../views${componentPath}/index.vue`;
|
||||
|
||||
// 先尝试直接路径,再尝试添加/index的路径
|
||||
const module = this.modules[fullPath] || this.modules[fullPathWithIndex];
|
||||
|
||||
if (!module) {
|
||||
console.error(
|
||||
`[ComponentLoader] 未找到组件: ${componentPath},尝试过的路径: ${fullPath} 和 ${fullPathWithIndex}`
|
||||
);
|
||||
return this.createErrorComponent(componentPath);
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载布局组件
|
||||
*/
|
||||
loadLayout(): () => Promise<any> {
|
||||
return () => import('@/views/index/index.vue');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 iframe 组件
|
||||
*/
|
||||
loadIframe(): () => Promise<any> {
|
||||
return () => import('@/views/outside/Iframe.vue');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空组件
|
||||
*/
|
||||
private createEmptyComponent(): () => Promise<any> {
|
||||
return () =>
|
||||
Promise.resolve({
|
||||
render() {
|
||||
return h('div', {});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误提示组件
|
||||
*/
|
||||
private createErrorComponent(componentPath: string): () => Promise<any> {
|
||||
return () =>
|
||||
Promise.resolve({
|
||||
render() {
|
||||
return h('div', { class: 'route-error' }, `组件未找到: ${componentPath}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* Iframe 路由管理器
|
||||
*
|
||||
* 负责管理 iframe 类型的路由
|
||||
*
|
||||
* @module router/core/IframeRouteManager
|
||||
* @author Fastapi Admin Team
|
||||
*/
|
||||
|
||||
import type { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export class IframeRouteManager {
|
||||
private static instance: IframeRouteManager;
|
||||
private iframeRoutes: AppRouteRecord[] = [];
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): IframeRouteManager {
|
||||
if (!IframeRouteManager.instance) {
|
||||
IframeRouteManager.instance = new IframeRouteManager();
|
||||
}
|
||||
return IframeRouteManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 iframe 路由
|
||||
*/
|
||||
add(route: AppRouteRecord): void {
|
||||
if (!this.iframeRoutes.find((r) => r.path === route.path)) {
|
||||
this.iframeRoutes.push(route);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 iframe 路由
|
||||
*/
|
||||
getAll(): AppRouteRecord[] {
|
||||
return this.iframeRoutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径查找 iframe 路由
|
||||
*/
|
||||
findByPath(path: string): AppRouteRecord | undefined {
|
||||
return this.iframeRoutes.find((route) => route.path === path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有 iframe 路由
|
||||
*/
|
||||
clear(): void {
|
||||
this.iframeRoutes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存到 sessionStorage
|
||||
*/
|
||||
save(): void {
|
||||
if (this.iframeRoutes.length > 0) {
|
||||
sessionStorage.setItem('iframeRoutes', JSON.stringify(this.iframeRoutes));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 sessionStorage 加载
|
||||
*/
|
||||
load(): void {
|
||||
try {
|
||||
const data = sessionStorage.getItem('iframeRoutes');
|
||||
if (data) {
|
||||
this.iframeRoutes = JSON.parse(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[IframeRouteManager] 加载 iframe 路由失败:', error);
|
||||
this.iframeRoutes = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
/**
|
||||
* 菜单处理器
|
||||
*
|
||||
* 负责菜单数据的获取、过滤和处理
|
||||
*
|
||||
* @module router/core/MenuProcessor
|
||||
* @author Fastapi Admin Team
|
||||
*/
|
||||
|
||||
import type { AppRouteRecord } from '@/types/router';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { useAppMode } from '@/hooks/core/useAppMode';
|
||||
import { fetchGetMenuList } from '@/api/system-manage';
|
||||
import { asyncRoutes } from '../routes/asyncRoutes';
|
||||
import { RoutesAlias } from '../routesAlias';
|
||||
import { formatMenuTitle } from '@/utils';
|
||||
|
||||
export class MenuProcessor {
|
||||
/**
|
||||
* 获取菜单数据
|
||||
*/
|
||||
async getMenuList(): Promise<AppRouteRecord[]> {
|
||||
const { isFrontendMode } = useAppMode();
|
||||
|
||||
let menuList: AppRouteRecord[];
|
||||
if (isFrontendMode.value) {
|
||||
menuList = await this.processFrontendMenu();
|
||||
} else {
|
||||
menuList = await this.processBackendMenu();
|
||||
}
|
||||
|
||||
// 在规范化路径之前,验证原始路径配置
|
||||
this.validateMenuPaths(menuList);
|
||||
|
||||
// 规范化路径(将相对路径转换为完整路径)
|
||||
return this.normalizeMenuPaths(menuList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理前端控制模式的菜单
|
||||
*/
|
||||
private async processFrontendMenu(): Promise<AppRouteRecord[]> {
|
||||
const userStore = useUserStore();
|
||||
const roles = userStore.info?.roles;
|
||||
|
||||
let menuList = [...asyncRoutes];
|
||||
|
||||
// 根据角色过滤菜单
|
||||
if (roles && roles.length > 0) {
|
||||
menuList = this.filterMenuByRoles(menuList, roles);
|
||||
}
|
||||
|
||||
return this.filterEmptyMenus(menuList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理后端控制模式的菜单
|
||||
*/
|
||||
private async processBackendMenu(): Promise<AppRouteRecord[]> {
|
||||
const list = await fetchGetMenuList();
|
||||
return this.filterEmptyMenus(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色过滤菜单
|
||||
*/
|
||||
private filterMenuByRoles(menu: AppRouteRecord[], roles: string[]): AppRouteRecord[] {
|
||||
return menu.reduce((acc: AppRouteRecord[], item) => {
|
||||
const itemRoles = item.meta?.roles;
|
||||
const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role));
|
||||
|
||||
if (hasPermission) {
|
||||
const filteredItem = { ...item };
|
||||
if (filteredItem.children?.length) {
|
||||
filteredItem.children = this.filterMenuByRoles(filteredItem.children, roles);
|
||||
}
|
||||
acc.push(filteredItem);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归过滤空菜单项
|
||||
*/
|
||||
private filterEmptyMenus(menuList: AppRouteRecord[]): AppRouteRecord[] {
|
||||
return menuList
|
||||
.map((item) => {
|
||||
// 如果有子菜单,先递归过滤子菜单
|
||||
if (item.children && item.children.length > 0) {
|
||||
const filteredChildren = this.filterEmptyMenus(item.children);
|
||||
return {
|
||||
...item,
|
||||
children: filteredChildren,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item) => {
|
||||
// 如果定义了 children 属性(即使是空数组),说明这是一个目录菜单,应该保留
|
||||
if ('children' in item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果有外链或 iframe,保留
|
||||
if (item.meta?.isIframe === true || item.meta?.link) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果有有效的 component,保留
|
||||
if (item.component && item.component !== '' && item.component !== RoutesAlias.Layout) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 其他情况过滤掉
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证菜单列表是否有效
|
||||
*/
|
||||
validateMenuList(menuList: AppRouteRecord[]): boolean {
|
||||
return Array.isArray(menuList) && menuList.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化菜单路径
|
||||
* 将相对路径转换为完整路径,确保菜单跳转正确
|
||||
*/
|
||||
private normalizeMenuPaths(menuList: AppRouteRecord[], parentPath = ''): AppRouteRecord[] {
|
||||
return menuList.map((item) => {
|
||||
// 构建完整路径
|
||||
const fullPath = this.buildFullPath(item.path || '', parentPath);
|
||||
|
||||
// 递归处理子菜单
|
||||
const children = item.children?.length
|
||||
? this.normalizeMenuPaths(item.children, fullPath)
|
||||
: item.children;
|
||||
|
||||
const redirect = item.redirect || this.resolveDefaultRedirect(children);
|
||||
|
||||
return {
|
||||
...item,
|
||||
path: fullPath,
|
||||
redirect,
|
||||
children,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为目录型菜单推导默认跳转地址
|
||||
*/
|
||||
private resolveDefaultRedirect(children?: AppRouteRecord[]): string | undefined {
|
||||
if (!children?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
if (this.isNavigableRoute(child)) {
|
||||
return child.path;
|
||||
}
|
||||
|
||||
const nestedRedirect = this.resolveDefaultRedirect(child.children);
|
||||
if (nestedRedirect) {
|
||||
return nestedRedirect;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断子路由是否可以作为默认落点
|
||||
*/
|
||||
private isNavigableRoute(route: AppRouteRecord): boolean {
|
||||
return Boolean(
|
||||
route.path &&
|
||||
route.path !== '/' &&
|
||||
!route.meta?.link &&
|
||||
route.meta?.isIframe !== true &&
|
||||
route.component &&
|
||||
route.component !== ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证菜单路径配置
|
||||
* 检测非一级菜单是否错误使用了 / 开头的路径
|
||||
*/
|
||||
/**
|
||||
* 验证菜单路径配置
|
||||
* 检测非一级菜单是否错误使用了 / 开头的路径
|
||||
*/
|
||||
private validateMenuPaths(menuList: AppRouteRecord[], level = 1): void {
|
||||
menuList.forEach((route) => {
|
||||
if (!route.children?.length) return;
|
||||
|
||||
const parentName = String(route.name || route.path || '未知路由');
|
||||
|
||||
route.children.forEach((child) => {
|
||||
const childPath = child.path || '';
|
||||
|
||||
// 跳过合法的绝对路径:外部链接和 iframe 路由
|
||||
if (this.isValidAbsolutePath(childPath)) return;
|
||||
|
||||
// 检测非法的绝对路径
|
||||
if (childPath.startsWith('/')) {
|
||||
this.logPathError(child, childPath, parentName, level);
|
||||
}
|
||||
});
|
||||
|
||||
// 递归检查更深层级的子路由
|
||||
this.validateMenuPaths(route.children, level + 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为合法的绝对路径
|
||||
*/
|
||||
private isValidAbsolutePath(path: string): boolean {
|
||||
return (
|
||||
path.startsWith('http://') ||
|
||||
path.startsWith('https://') ||
|
||||
path.startsWith('/outside/iframe/')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出路径配置错误日志
|
||||
*/
|
||||
private logPathError(
|
||||
route: AppRouteRecord,
|
||||
path: string,
|
||||
parentName: string,
|
||||
level: number
|
||||
): void {
|
||||
const routeName = String(route.name || path || '未知路由');
|
||||
const menuTitle = route.meta?.title || routeName;
|
||||
const suggestedPath = path.split('/').pop() || path.slice(1);
|
||||
|
||||
console.error(
|
||||
`[路由配置错误] 菜单 "${formatMenuTitle(menuTitle)}" (name: ${routeName}, path: ${path}) 配置错误\n` +
|
||||
` 位置: ${parentName} > ${routeName}\n` +
|
||||
` 问题: ${level + 1}级菜单的 path 不能以 / 开头\n` +
|
||||
` 当前配置: path: '${path}'\n` +
|
||||
` 应该改为: path: '${suggestedPath}'`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整路径
|
||||
*/
|
||||
private buildFullPath(path: string, parentPath: string): string {
|
||||
if (!path) return '';
|
||||
|
||||
// 外部链接直接返回
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// 如果已经是绝对路径,直接返回
|
||||
if (path.startsWith('/')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// 拼接父路径和当前路径
|
||||
if (parentPath) {
|
||||
// 移除父路径末尾的斜杠,移除子路径开头的斜杠,然后拼接
|
||||
const cleanParent = parentPath.replace(/\/$/, '');
|
||||
const cleanChild = path.replace(/^\//, '');
|
||||
return `${cleanParent}/${cleanChild}`;
|
||||
}
|
||||
|
||||
// 没有父路径,添加前导斜杠
|
||||
return `/${path}`;
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* 路由权限验证模块
|
||||
*
|
||||
* 提供路由权限验证和路径检查功能
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* - 验证路径是否在用户菜单权限中
|
||||
* - 构建菜单路径集合(扁平化处理)
|
||||
* - 支持动态路由参数匹配
|
||||
* - 路径前缀匹配
|
||||
*
|
||||
* ## 使用场景
|
||||
*
|
||||
* - 路由守卫中验证用户权限
|
||||
* - 动态路由注册后的权限检查
|
||||
* - 防止用户访问无权限的页面
|
||||
*
|
||||
* @module router/core/RoutePermissionValidator
|
||||
* @author Fastapi Admin Team
|
||||
*/
|
||||
|
||||
import type { AppRouteRecord } from '@/types/router';
|
||||
|
||||
/**
|
||||
* 路由权限验证器
|
||||
*/
|
||||
export class RoutePermissionValidator {
|
||||
/**
|
||||
* 验证路径是否在用户菜单权限中
|
||||
* @param targetPath 目标路径
|
||||
* @param menuList 菜单列表
|
||||
* @returns 是否有权限访问
|
||||
*/
|
||||
static hasPermission(targetPath: string, menuList: AppRouteRecord[]): boolean {
|
||||
// 根路径始终允许访问
|
||||
if (targetPath === '/') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.matchRoute(targetPath, menuList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单路径集合(扁平化处理)
|
||||
* @param menuList 菜单列表
|
||||
* @param pathSet 路径集合
|
||||
* @returns 路径集合
|
||||
*/
|
||||
static buildMenuPathSet(
|
||||
menuList: AppRouteRecord[],
|
||||
pathSet: Set<string> = new Set()
|
||||
): Set<string> {
|
||||
if (!Array.isArray(menuList) || menuList.length === 0) {
|
||||
return pathSet;
|
||||
}
|
||||
|
||||
for (const menuItem of menuList) {
|
||||
if (!menuItem.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 标准化路径并添加到集合
|
||||
const menuPath = menuItem.path.startsWith('/') ? menuItem.path : `/${menuItem.path}`;
|
||||
pathSet.add(menuPath);
|
||||
|
||||
// 递归处理子菜单
|
||||
if (menuItem.children?.length) {
|
||||
this.buildMenuPathSet(menuItem.children, pathSet);
|
||||
}
|
||||
}
|
||||
|
||||
return pathSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查目标路径是否匹配集合中的某个路径前缀
|
||||
* 用于支持动态路由参数匹配,如 /user/123 匹配 /user
|
||||
* @param targetPath 目标路径
|
||||
* @param pathSet 路径集合
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
static checkPathPrefix(targetPath: string, pathSet: Set<string>): boolean {
|
||||
// 遍历路径集合,检查是否有前缀匹配
|
||||
for (const menuPath of pathSet) {
|
||||
if (targetPath.startsWith(`${menuPath}/`)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归匹配路由配置,支持隐藏路由和动态参数路由
|
||||
*/
|
||||
static matchRoute(targetPath: string, routes: AppRouteRecord[]): boolean {
|
||||
if (!Array.isArray(routes) || routes.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const routePath = route.path.startsWith('/') ? route.path : `/${route.path}`;
|
||||
|
||||
if (
|
||||
routePath === targetPath ||
|
||||
this.isDynamicRouteMatch(targetPath, routePath) ||
|
||||
targetPath.startsWith(`${routePath}/`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (route.children?.length && this.matchRoute(targetPath, route.children)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查目标路径是否匹配动态参数路由,如 /demo/123 匹配 /demo/:id
|
||||
*/
|
||||
static isDynamicRouteMatch(targetPath: string, routePath: string): boolean {
|
||||
if (!routePath.includes(':')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pattern = routePath
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/:([^/]+)/g, '[^/]+')
|
||||
.replace(/\\\*/g, '.*');
|
||||
|
||||
return new RegExp(`^${pattern}$`).test(targetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并返回有效的路径
|
||||
* 如果目标路径无权限,返回首页路径
|
||||
* @param targetPath 目标路径
|
||||
* @param menuList 菜单列表
|
||||
* @param homePath 首页路径
|
||||
* @returns 验证后的路径
|
||||
*/
|
||||
static validatePath(
|
||||
targetPath: string,
|
||||
menuList: AppRouteRecord[],
|
||||
homePath: string = '/'
|
||||
): { path: string; hasPermission: boolean } {
|
||||
const hasPermission = this.hasPermission(targetPath, menuList);
|
||||
|
||||
if (hasPermission) {
|
||||
return { path: targetPath, hasPermission: true };
|
||||
}
|
||||
|
||||
return { path: homePath, hasPermission: false };
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* 路由注册核心类
|
||||
*
|
||||
* 负责动态路由的注册、验证和管理
|
||||
*
|
||||
* @module router/core/RouteRegistry
|
||||
* @author Fastapi Admin Team
|
||||
*/
|
||||
|
||||
import type { Router, RouteRecordRaw } from 'vue-router';
|
||||
import type { AppRouteRecord } from '@/types/router';
|
||||
import { ComponentLoader } from './ComponentLoader';
|
||||
import { RouteValidator } from './RouteValidator';
|
||||
import { RouteTransformer } from './RouteTransformer';
|
||||
|
||||
export class RouteRegistry {
|
||||
private router: Router;
|
||||
private componentLoader: ComponentLoader;
|
||||
private validator: RouteValidator;
|
||||
private transformer: RouteTransformer;
|
||||
private removeRouteFns: (() => void)[] = [];
|
||||
private registered = false;
|
||||
|
||||
constructor(router: Router) {
|
||||
this.router = router;
|
||||
this.componentLoader = new ComponentLoader();
|
||||
this.validator = new RouteValidator();
|
||||
this.transformer = new RouteTransformer(this.componentLoader);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册动态路由
|
||||
*/
|
||||
register(menuList: AppRouteRecord[]): void {
|
||||
if (this.registered) {
|
||||
console.warn('[RouteRegistry] 路由已注册,跳过重复注册');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证路由配置
|
||||
const validationResult = this.validator.validate(menuList);
|
||||
if (!validationResult.valid) {
|
||||
throw new Error(`路由配置验证失败: ${validationResult.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
// 转换并注册路由
|
||||
const removeRouteFns: (() => void)[] = [];
|
||||
|
||||
menuList.forEach((route) => {
|
||||
if (route.name && !this.router.hasRoute(route.name)) {
|
||||
const routeConfig = this.transformer.transform(route);
|
||||
const removeRouteFn = this.router.addRoute(routeConfig as RouteRecordRaw);
|
||||
removeRouteFns.push(removeRouteFn);
|
||||
}
|
||||
});
|
||||
|
||||
this.removeRouteFns = removeRouteFns;
|
||||
this.registered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除所有动态路由
|
||||
*/
|
||||
unregister(): void {
|
||||
this.removeRouteFns.forEach((fn) => fn());
|
||||
this.removeRouteFns = [];
|
||||
this.registered = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已注册
|
||||
*/
|
||||
isRegistered(): boolean {
|
||||
return this.registered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取移除函数列表(用于 store 管理)
|
||||
*/
|
||||
getRemoveRouteFns(): (() => void)[] {
|
||||
return this.removeRouteFns;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已注册(用于错误处理场景,避免重复请求)
|
||||
*/
|
||||
markAsRegistered(): void {
|
||||
this.registered = true;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
/**
|
||||
* 路由转换器
|
||||
*
|
||||
* 负责将菜单数据转换为 Vue Router 路由配置
|
||||
*
|
||||
* @module router/core/RouteTransformer
|
||||
* @author Fastapi Admin Team
|
||||
*/
|
||||
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import type { AppRouteRecord } from '@/types/router';
|
||||
import { ComponentLoader } from './ComponentLoader';
|
||||
import { IframeRouteManager } from './IframeRouteManager';
|
||||
|
||||
interface ConvertedRoute extends Omit<RouteRecordRaw, 'children'> {
|
||||
id?: number;
|
||||
children?: ConvertedRoute[];
|
||||
component?: RouteRecordRaw['component'] | (() => Promise<any>);
|
||||
}
|
||||
|
||||
export class RouteTransformer {
|
||||
private componentLoader: ComponentLoader;
|
||||
private iframeManager: IframeRouteManager;
|
||||
|
||||
constructor(componentLoader: ComponentLoader) {
|
||||
this.componentLoader = componentLoader;
|
||||
this.iframeManager = IframeRouteManager.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换路由配置
|
||||
*/
|
||||
transform(route: AppRouteRecord, depth = 0): ConvertedRoute {
|
||||
const { component, children, ...routeConfig } = route;
|
||||
|
||||
// 基础路由配置
|
||||
const converted: ConvertedRoute = {
|
||||
...routeConfig,
|
||||
component: undefined,
|
||||
};
|
||||
|
||||
// 处理不同类型的路由
|
||||
if (route.meta.isIframe) {
|
||||
this.handleIframeRoute(converted, route, depth);
|
||||
} else if (this.isFirstLevelRoute(route, depth)) {
|
||||
this.handleFirstLevelRoute(converted, route, component as string);
|
||||
} else {
|
||||
this.handleNormalRoute(converted, component as string);
|
||||
}
|
||||
|
||||
// 递归处理子路由
|
||||
if (children?.length) {
|
||||
converted.children = children.map((child) => this.transform(child, depth + 1));
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为一级路由(需要 Layout 包裹)
|
||||
*/
|
||||
private isFirstLevelRoute(route: AppRouteRecord, depth: number): boolean {
|
||||
return depth === 0 && (!route.children || route.children.length === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 iframe 类型路由
|
||||
*/
|
||||
private handleIframeRoute(
|
||||
targetRoute: ConvertedRoute,
|
||||
sourceRoute: AppRouteRecord,
|
||||
depth: number
|
||||
): void {
|
||||
if (depth === 0) {
|
||||
// 顶级 iframe:用 Layout 包裹
|
||||
targetRoute.component = this.componentLoader.loadLayout();
|
||||
targetRoute.path = this.extractFirstSegment(sourceRoute.path || '');
|
||||
targetRoute.name = '';
|
||||
|
||||
targetRoute.children = [
|
||||
{
|
||||
...sourceRoute,
|
||||
component: this.componentLoader.loadIframe(),
|
||||
} as ConvertedRoute,
|
||||
];
|
||||
} else {
|
||||
// 非顶级(嵌套)iframe:直接使用 Iframe.vue
|
||||
targetRoute.component = this.componentLoader.loadIframe();
|
||||
}
|
||||
|
||||
// 记录 iframe 路由
|
||||
this.iframeManager.add(sourceRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理一级菜单路由
|
||||
*/
|
||||
private handleFirstLevelRoute(
|
||||
converted: ConvertedRoute,
|
||||
route: AppRouteRecord,
|
||||
component: string | undefined
|
||||
): void {
|
||||
converted.component = this.componentLoader.loadLayout();
|
||||
converted.path = this.extractFirstSegment(route.path || '');
|
||||
converted.name = '';
|
||||
route.meta.isFirstLevel = true;
|
||||
|
||||
converted.children = [
|
||||
{
|
||||
...route,
|
||||
component: component ? this.componentLoader.load(component) : undefined,
|
||||
} as ConvertedRoute,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理普通路由
|
||||
*/
|
||||
private handleNormalRoute(converted: ConvertedRoute, component: string | undefined): void {
|
||||
if (component) {
|
||||
converted.component = this.componentLoader.load(component);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取路径的第一段
|
||||
*/
|
||||
private extractFirstSegment(path: string): string {
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
return segments.length > 0 ? `/${segments[0]}` : '/';
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* 路由验证器
|
||||
*
|
||||
* 负责验证路由配置的合法性
|
||||
*
|
||||
* @module router/core/RouteValidator
|
||||
* @author Fastapi Admin Team
|
||||
*/
|
||||
|
||||
import type { AppRouteRecord } from '@/types/router';
|
||||
import { RoutesAlias } from '../routesAlias';
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export class RouteValidator {
|
||||
// 用于记录已经提示过的路由,避免重复提示
|
||||
private warnedRoutes = new Set<string>();
|
||||
|
||||
/**
|
||||
* 验证路由配置
|
||||
*/
|
||||
validate(routes: AppRouteRecord[]): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// 检测重复路由
|
||||
this.checkDuplicates(routes, errors, warnings);
|
||||
|
||||
// 检测组件配置
|
||||
this.checkComponents(routes, errors, warnings);
|
||||
|
||||
// 检测嵌套菜单的 /index/index 配置
|
||||
this.checkNestedIndexComponent(routes);
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测重复路由
|
||||
*/
|
||||
private checkDuplicates(
|
||||
routes: AppRouteRecord[],
|
||||
errors: string[],
|
||||
warnings: string[],
|
||||
parentPath = ''
|
||||
): void {
|
||||
const routeNameMap = new Map<string, string>();
|
||||
const componentPathMap = new Map<string, string>();
|
||||
|
||||
const checkRoutes = (routes: AppRouteRecord[], parentPath = '') => {
|
||||
routes.forEach((route) => {
|
||||
const currentPath = route.path || '';
|
||||
const fullPath = this.resolvePath(parentPath, currentPath);
|
||||
|
||||
// 名称重复检测
|
||||
if (route.name) {
|
||||
const routeName = String(route.name);
|
||||
if (routeNameMap.has(routeName)) {
|
||||
warnings.push(`路由名称重复: "${routeName}" (${fullPath})`);
|
||||
} else {
|
||||
routeNameMap.set(routeName, fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 组件路径重复检测
|
||||
if (route.component && typeof route.component === 'string') {
|
||||
const componentPath = route.component;
|
||||
if (componentPath !== RoutesAlias.Layout) {
|
||||
const componentKey = `${parentPath}:${componentPath}`;
|
||||
if (componentPathMap.has(componentKey)) {
|
||||
warnings.push(`组件路径重复: "${componentPath}" (${fullPath})`);
|
||||
} else {
|
||||
componentPathMap.set(componentKey, fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子路由
|
||||
if (route.children?.length) {
|
||||
checkRoutes(route.children, fullPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
checkRoutes(routes, parentPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测组件配置
|
||||
*/
|
||||
private checkComponents(
|
||||
routes: AppRouteRecord[],
|
||||
errors: string[],
|
||||
warnings: string[],
|
||||
parentPath = ''
|
||||
): void {
|
||||
routes.forEach((route) => {
|
||||
const hasExternalLink = !!route.meta?.link?.trim();
|
||||
const hasChildren = Array.isArray(route.children) && route.children.length > 0;
|
||||
const routePath = route.path || '[未定义路径]';
|
||||
const isIframe = route.meta?.isIframe;
|
||||
|
||||
// 如果配置了 component,则无需校验
|
||||
if (route.component) {
|
||||
// 递归检查子路由
|
||||
if (route.children?.length) {
|
||||
const fullPath = this.resolvePath(parentPath, route.path || '');
|
||||
this.checkComponents(route.children, errors, warnings, fullPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 一级菜单:必须指定 Layout,除非是外链或 iframe
|
||||
if (parentPath === '' && !hasExternalLink && !isIframe) {
|
||||
errors.push(`一级菜单(${routePath}) 缺少 component,必须指向 ${RoutesAlias.Layout}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 非一级菜单:如果既不是外链、iframe,也没有子路由,则必须配置 component
|
||||
if (!hasExternalLink && !isIframe && !hasChildren) {
|
||||
errors.push(`路由(${routePath}) 缺少 component 配置`);
|
||||
}
|
||||
|
||||
// 递归检查子路由
|
||||
if (route.children?.length) {
|
||||
const fullPath = this.resolvePath(parentPath, route.path || '');
|
||||
this.checkComponents(route.children, errors, warnings, fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测嵌套菜单的 Layout 组件配置
|
||||
* 只有一级菜单才能使用 Layout,二级及以下菜单不能使用
|
||||
*/
|
||||
private checkNestedIndexComponent(routes: AppRouteRecord[], level = 1): void {
|
||||
routes.forEach((route) => {
|
||||
// 检查二级及以下菜单是否错误使用了 Layout
|
||||
if (level > 1 && route.component === RoutesAlias.Layout) {
|
||||
this.logLayoutError(route, level);
|
||||
}
|
||||
|
||||
// 递归检查子路由
|
||||
if (route.children?.length) {
|
||||
this.checkNestedIndexComponent(route.children, level + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 Layout 组件配置错误日志
|
||||
*/
|
||||
private logLayoutError(route: AppRouteRecord, level: number): void {
|
||||
const routeName = String(route.name || route.path || '未知路由');
|
||||
const routeKey = `${routeName}_${route.path}`;
|
||||
|
||||
// 避免重复提示
|
||||
if (this.warnedRoutes.has(routeKey)) return;
|
||||
this.warnedRoutes.add(routeKey);
|
||||
|
||||
const menuTitle = route.meta?.title || routeName;
|
||||
const routePath = route.path || '/';
|
||||
|
||||
console.error(
|
||||
`[路由配置错误] 菜单 "${menuTitle}" (name: ${routeName}, path: ${routePath}) 配置错误\n` +
|
||||
` 问题: ${level}级菜单不能使用 ${RoutesAlias.Layout} 作为 component\n` +
|
||||
` 说明: 只有一级菜单才能使用 ${RoutesAlias.Layout},二级及以下菜单应该指向具体的组件路径\n` +
|
||||
` 当前配置: component: '${RoutesAlias.Layout}'\n` +
|
||||
` 应该改为: component: '/your/component/path' 或留空 ''(如果是目录菜单)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径解析
|
||||
*/
|
||||
private resolvePath(parent: string, child: string): string {
|
||||
return [parent.replace(/\/$/, ''), child.replace(/^\//, '')].filter(Boolean).join('/');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* 路由核心模块导出
|
||||
*
|
||||
* @module router/core
|
||||
* @author Fastapi Admin Team
|
||||
*/
|
||||
|
||||
export { RouteRegistry } from './RouteRegistry';
|
||||
export { ComponentLoader } from './ComponentLoader';
|
||||
export { RouteValidator } from './RouteValidator';
|
||||
export { RouteTransformer } from './RouteTransformer';
|
||||
export { IframeRouteManager } from './IframeRouteManager';
|
||||
export { MenuProcessor } from './MenuProcessor';
|
||||
export { RoutePermissionValidator } from './RoutePermissionValidator';
|
||||
@@ -1,33 +0,0 @@
|
||||
import { nextTick } from 'vue';
|
||||
import { useSettingStore } from '@/store/modules/setting';
|
||||
import { Router } from 'vue-router';
|
||||
import NProgress from 'nprogress';
|
||||
import { useCommon } from '@/hooks/core/useCommon';
|
||||
import { loadingService } from '@/utils/ui';
|
||||
import { getPendingLoading, resetPendingLoading } from './beforeEach';
|
||||
|
||||
/** 路由全局后置守卫 */
|
||||
export function setupAfterEachGuard(router: Router) {
|
||||
router.afterEach(() => {
|
||||
const { scrollToTop } = useCommon();
|
||||
scrollToTop();
|
||||
|
||||
// 关闭进度条
|
||||
const settingStore = useSettingStore();
|
||||
if (settingStore.showNprogress) {
|
||||
NProgress.done();
|
||||
// 确保进度条完全移除,避免残影
|
||||
setTimeout(() => {
|
||||
NProgress.remove();
|
||||
}, 600);
|
||||
}
|
||||
|
||||
// 关闭 loading 效果
|
||||
if (getPendingLoading()) {
|
||||
nextTick(() => {
|
||||
loadingService.hideLoading();
|
||||
resetPendingLoading();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,423 +0,0 @@
|
||||
/**
|
||||
* 路由全局前置守卫模块
|
||||
*
|
||||
* 提供完整的路由导航守卫功能
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* - 登录状态验证和重定向
|
||||
* - 动态路由注册和权限控制
|
||||
* - 菜单数据获取和处理(前端/后端模式)
|
||||
* - 用户信息获取和缓存
|
||||
* - 页面标题设置
|
||||
* - 工作标签页管理
|
||||
* - 进度条和加载动画控制
|
||||
* - 静态路由识别和处理
|
||||
* - 错误处理和异常跳转
|
||||
*
|
||||
* ## 使用场景
|
||||
*
|
||||
* - 路由跳转前的权限验证
|
||||
* - 动态菜单加载和路由注册
|
||||
* - 用户登录状态管理
|
||||
* - 页面访问控制
|
||||
* - 路由级别的加载状态管理
|
||||
*
|
||||
* ## 工作流程
|
||||
*
|
||||
* 1. 检查登录状态,未登录跳转到登录页
|
||||
* 2. 首次访问时获取用户信息和菜单数据
|
||||
* 3. 根据权限动态注册路由
|
||||
* 4. 设置页面标题和工作标签页
|
||||
* 5. 处理根路径重定向到首页
|
||||
* 6. 未匹配路由跳转到 404 页面
|
||||
*
|
||||
* @module router/guards/beforeEach
|
||||
* @author Fastapi Admin Team
|
||||
*/
|
||||
import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
|
||||
import { nextTick } from 'vue';
|
||||
import NProgress from 'nprogress';
|
||||
import { useSettingStore } from '@/store/modules/setting';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { useMenuStore } from '@/store/modules/menu';
|
||||
import { setWorktab } from '@/utils/navigation';
|
||||
import { setPageTitle } from '@/utils/router';
|
||||
import { RoutesAlias } from '../routesAlias';
|
||||
import { staticRoutes } from '../routes/staticRoutes';
|
||||
import { loadingService } from '@/utils/ui';
|
||||
import { useCommon } from '@/hooks/core/useCommon';
|
||||
import { useWorktabStore } from '@/store/modules/worktab';
|
||||
import { fetchGetUserInfo } from '@/api/auth';
|
||||
import { ApiStatus } from '@/utils/http/status';
|
||||
import { isHttpError } from '@/utils/http/error';
|
||||
import {
|
||||
RouteRegistry,
|
||||
MenuProcessor,
|
||||
IframeRouteManager,
|
||||
RoutePermissionValidator,
|
||||
} from '../core';
|
||||
|
||||
// 路由注册器实例
|
||||
let routeRegistry: RouteRegistry | null = null;
|
||||
|
||||
// 菜单处理器实例
|
||||
const menuProcessor = new MenuProcessor();
|
||||
|
||||
// 跟踪是否需要关闭 loading
|
||||
let pendingLoading = false;
|
||||
|
||||
// 路由初始化失败标记,防止死循环
|
||||
// 一旦设置为 true,只有刷新页面或重新登录才能重置
|
||||
let routeInitFailed = false;
|
||||
|
||||
// 路由初始化进行中标记,防止并发请求
|
||||
let routeInitInProgress = false;
|
||||
|
||||
/**
|
||||
* 获取 pendingLoading 状态
|
||||
*/
|
||||
export function getPendingLoading(): boolean {
|
||||
return pendingLoading;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 pendingLoading 状态
|
||||
*/
|
||||
export function resetPendingLoading(): void {
|
||||
pendingLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路由初始化失败状态
|
||||
*/
|
||||
export function getRouteInitFailed(): boolean {
|
||||
return routeInitFailed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置路由初始化状态(用于重新登录场景)
|
||||
*/
|
||||
export function resetRouteInitState(): void {
|
||||
routeInitFailed = false;
|
||||
routeInitInProgress = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置路由全局前置守卫
|
||||
*/
|
||||
export function setupBeforeEachGuard(router: Router): void {
|
||||
// 初始化路由注册器
|
||||
routeRegistry = new RouteRegistry(router);
|
||||
|
||||
router.beforeEach(
|
||||
async (
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext
|
||||
) => {
|
||||
try {
|
||||
await handleRouteGuard(to, from, next, router);
|
||||
} catch (error) {
|
||||
console.error('[RouteGuard] 路由守卫处理失败:', error);
|
||||
closeLoading();
|
||||
next({ name: 'Exception500' });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 loading 效果
|
||||
*/
|
||||
function closeLoading(): void {
|
||||
if (pendingLoading) {
|
||||
nextTick(() => {
|
||||
loadingService.hideLoading();
|
||||
pendingLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理路由守卫逻辑
|
||||
*/
|
||||
async function handleRouteGuard(
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
router: Router
|
||||
): Promise<void> {
|
||||
const settingStore = useSettingStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 启动进度条
|
||||
if (settingStore.showNprogress) {
|
||||
NProgress.start();
|
||||
}
|
||||
|
||||
// 1. 检查登录状态
|
||||
if (!handleLoginStatus(to, userStore, next)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 检查路由初始化是否已失败(防止死循环)
|
||||
if (routeInitFailed) {
|
||||
// 已经失败过,直接放行到错误页面,不再重试
|
||||
if (to.matched.length > 0) {
|
||||
next();
|
||||
} else {
|
||||
// 未匹配到路由,跳转到 500 页面
|
||||
next({ name: 'Exception500', replace: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 处理动态路由注册
|
||||
if (!routeRegistry?.isRegistered() && userStore.isLogin) {
|
||||
// 防止并发请求(快速连续导航场景)
|
||||
if (routeInitInProgress) {
|
||||
// 正在初始化中,等待完成后重新导航
|
||||
next(false);
|
||||
return;
|
||||
}
|
||||
await handleDynamicRoutes(to, next, router);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 处理根路径重定向
|
||||
if (handleRootPathRedirect(to, next)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 处理已匹配的路由
|
||||
if (to.matched.length > 0) {
|
||||
setWorktab(to);
|
||||
setPageTitle(to);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. 未匹配到路由,跳转到 404
|
||||
next({ name: 'Exception404' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录状态
|
||||
* @returns true 表示可以继续,false 表示已处理跳转
|
||||
*/
|
||||
function handleLoginStatus(
|
||||
to: RouteLocationNormalized,
|
||||
userStore: ReturnType<typeof useUserStore>,
|
||||
next: NavigationGuardNext
|
||||
): boolean {
|
||||
// 已登录或访问登录页或静态路由,直接放行
|
||||
if (userStore.isLogin || to.path === RoutesAlias.Login || isStaticRoute(to.path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未登录且访问需要权限的页面,跳转到登录页并携带 redirect 参数
|
||||
userStore.logOut();
|
||||
next({
|
||||
name: 'Login',
|
||||
query: { redirect: to.fullPath },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路由是否为静态路由
|
||||
*/
|
||||
function isStaticRoute(path: string): boolean {
|
||||
const checkRoute = (routes: any[], targetPath: string): boolean => {
|
||||
return routes.some((route) => {
|
||||
// 404 catch-all 路由不应视为可匿名访问的静态页,
|
||||
// 否则未登录时手动输入任意地址会直接落到 404,无法跳转登录页。
|
||||
if (route.name === 'Exception404') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 处理动态路由参数匹配
|
||||
const routePath = route.path;
|
||||
const pattern = routePath.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*');
|
||||
const regex = new RegExp(`^${pattern}$`);
|
||||
|
||||
if (regex.test(targetPath)) {
|
||||
return true;
|
||||
}
|
||||
if (route.children && route.children.length > 0) {
|
||||
return checkRoute(route.children, targetPath);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
return checkRoute(staticRoutes, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理动态路由注册
|
||||
*/
|
||||
async function handleDynamicRoutes(
|
||||
to: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
router: Router
|
||||
): Promise<void> {
|
||||
// 标记初始化进行中
|
||||
routeInitInProgress = true;
|
||||
|
||||
// 显示 loading
|
||||
pendingLoading = true;
|
||||
loadingService.showLoading();
|
||||
|
||||
try {
|
||||
// 1. 获取用户信息
|
||||
await fetchUserInfo();
|
||||
|
||||
// 2. 获取菜单数据
|
||||
const menuList = await menuProcessor.getMenuList();
|
||||
|
||||
// 3. 验证菜单数据
|
||||
if (!menuProcessor.validateMenuList(menuList)) {
|
||||
throw new Error('获取菜单列表失败,请重新登录');
|
||||
}
|
||||
|
||||
// 4. 注册动态路由
|
||||
routeRegistry?.register(menuList);
|
||||
|
||||
// 5. 保存菜单数据到 store
|
||||
const menuStore = useMenuStore();
|
||||
menuStore.setMenuList(menuList);
|
||||
menuStore.addRemoveRouteFns(routeRegistry?.getRemoveRouteFns() || []);
|
||||
|
||||
// 6. 保存 iframe 路由
|
||||
IframeRouteManager.getInstance().save();
|
||||
|
||||
// 7. 验证工作标签页
|
||||
useWorktabStore().validateWorktabs(router);
|
||||
|
||||
// 8. 静态路由不依赖菜单权限,初始化后直接恢复目标地址。
|
||||
if (isStaticRoute(to.path)) {
|
||||
routeInitInProgress = false;
|
||||
next({
|
||||
path: to.path,
|
||||
query: to.query,
|
||||
hash: to.hash,
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 8. 验证目标路径权限
|
||||
const { homePath } = useCommon();
|
||||
const { path: validatedPath, hasPermission } = RoutePermissionValidator.validatePath(
|
||||
to.path,
|
||||
menuList,
|
||||
homePath.value || '/'
|
||||
);
|
||||
|
||||
// 初始化成功,重置进行中标记
|
||||
routeInitInProgress = false;
|
||||
|
||||
// 9. 重新导航到目标路由
|
||||
if (!hasPermission) {
|
||||
// 无权限访问,跳转到首页
|
||||
closeLoading();
|
||||
|
||||
// 输出警告信息
|
||||
console.warn(`[RouteGuard] 用户无权限访问路径: ${to.path},已跳转到首页`);
|
||||
|
||||
// 直接跳转到首页
|
||||
next({
|
||||
path: validatedPath,
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
// 有权限,正常导航
|
||||
next({
|
||||
path: to.path,
|
||||
query: to.query,
|
||||
hash: to.hash,
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RouteGuard] 动态路由注册失败:', error);
|
||||
|
||||
// 关闭 loading
|
||||
closeLoading();
|
||||
|
||||
// 401 错误:axios 拦截器已处理退出登录,取消当前导航
|
||||
if (isUnauthorizedError(error)) {
|
||||
// 重置状态,允许重新登录后再次初始化
|
||||
routeInitInProgress = false;
|
||||
next(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记初始化失败,防止死循环
|
||||
routeInitFailed = true;
|
||||
routeInitInProgress = false;
|
||||
|
||||
// 输出详细错误信息,便于排查
|
||||
if (isHttpError(error)) {
|
||||
console.error(`[RouteGuard] 错误码: ${error.code}, 消息: ${error.message}`);
|
||||
}
|
||||
|
||||
// 跳转到 500 页面,使用 replace 避免产生历史记录
|
||||
next({ name: 'Exception500', replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
async function fetchUserInfo(): Promise<void> {
|
||||
const userStore = useUserStore();
|
||||
const data = await fetchGetUserInfo();
|
||||
userStore.setUserInfo(data);
|
||||
// 检查并清理工作台标签页(如果是不同用户登录)
|
||||
userStore.checkAndClearWorktabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置路由相关状态
|
||||
*/
|
||||
export function resetRouterState(delay: number): void {
|
||||
setTimeout(() => {
|
||||
routeRegistry?.unregister();
|
||||
IframeRouteManager.getInstance().clear();
|
||||
|
||||
const menuStore = useMenuStore();
|
||||
menuStore.removeAllDynamicRoutes();
|
||||
menuStore.setMenuList([]);
|
||||
|
||||
// 重置路由初始化状态,允许重新登录后再次初始化
|
||||
resetRouteInitState();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理根路径重定向到首页
|
||||
* @returns true 表示已处理跳转,false 表示无需跳转
|
||||
*/
|
||||
function handleRootPathRedirect(to: RouteLocationNormalized, next: NavigationGuardNext): boolean {
|
||||
if (to.path !== '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { homePath } = useCommon();
|
||||
if (homePath.value && homePath.value !== '/') {
|
||||
next({ path: homePath.value, replace: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为未授权错误(401)
|
||||
*/
|
||||
function isUnauthorizedError(error: unknown): boolean {
|
||||
return isHttpError(error) && error.code === ApiStatus.unauthorized;
|
||||
}
|
||||
@@ -1,82 +1,77 @@
|
||||
import type { App } from 'vue';
|
||||
import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router';
|
||||
import { staticRoutes } from './routes/staticRoutes';
|
||||
import { configureNProgress } from '@/utils/router';
|
||||
import { setupBeforeEachGuard } from './guards/beforeEach';
|
||||
import { setupAfterEachGuard } from './guards/afterEach';
|
||||
|
||||
export const Layout = () => import('@/layouts/index.vue');
|
||||
import type { App } from "vue";
|
||||
import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";
|
||||
export const Layout = () => import("@/layouts/index.vue");
|
||||
/**
|
||||
* 静态路由
|
||||
*/
|
||||
export const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/redirect',
|
||||
path: "/redirect",
|
||||
meta: { hidden: true },
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '/redirect/:path(.*)',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
path: "/redirect/:path(.*)",
|
||||
component: () => import("@/views/redirect/index.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
meta: { hidden: true },
|
||||
component: () => import('@/views/module_system/auth/index.vue'),
|
||||
component: () => import("@/views/module_system/auth/index.vue"),
|
||||
},
|
||||
{
|
||||
path: '/401',
|
||||
name: '401',
|
||||
meta: { hidden: true, title: '401' },
|
||||
component: () => import('@/views/error/401.vue'),
|
||||
path: "/401",
|
||||
name: "401",
|
||||
meta: { hidden: true, title: "401" },
|
||||
component: () => import("@/views/error/401.vue"),
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: '404',
|
||||
meta: { hidden: true, title: '404' },
|
||||
component: () => import('@/views/error/404.vue'),
|
||||
path: "/404",
|
||||
name: "404",
|
||||
meta: { hidden: true, title: "404" },
|
||||
component: () => import("@/views/error/404.vue"),
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: '500',
|
||||
meta: { hidden: true, title: '500' },
|
||||
component: () => import('@/views/error/500.vue'),
|
||||
path: "/500",
|
||||
name: "500",
|
||||
meta: { hidden: true, title: "500" },
|
||||
component: () => import("@/views/error/500.vue"),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: '/',
|
||||
redirect: '/home',
|
||||
path: "/",
|
||||
name: "/",
|
||||
redirect: "/home",
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
path: "home",
|
||||
component: () => import("@/views/dashboard/index.vue"),
|
||||
// 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致
|
||||
// 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude
|
||||
name: 'Home',
|
||||
name: "Home",
|
||||
meta: {
|
||||
title: '首页',
|
||||
icon: 'homepage',
|
||||
title: "首页",
|
||||
icon: "homepage",
|
||||
affix: true,
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'Profile',
|
||||
meta: { title: '个人中心', icon: 'user', hidden: true },
|
||||
component: () => import('@/views/current/profile.vue'),
|
||||
path: "profile",
|
||||
name: "Profile",
|
||||
meta: { title: "个人中心", icon: "user", hidden: true },
|
||||
component: () => import("@/views/current/profile.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 通配 404 必须置于静态路由最后,否则会抢先匹配 /、/home 等
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: () => import('@/views/error/404.vue'),
|
||||
meta: { hidden: true, title: '404' },
|
||||
path: "/:pathMatch(.*)*",
|
||||
component: () => import("@/views/error/404.vue"),
|
||||
meta: { hidden: true, title: "404" },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -85,22 +80,15 @@ export const constantRoutes: RouteRecordRaw[] = [
|
||||
*/
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: constantRoutes.concat(staticRoutes), // 静态路由
|
||||
routes: constantRoutes,
|
||||
// 刷新时,滚动条位置还原
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
});
|
||||
|
||||
// 全局注册 router
|
||||
// 为了捕获并处理全局错误,在注册路由时添加错误处理
|
||||
// 初始化路由
|
||||
export function initRouter(app: App<Element>): void {
|
||||
configureNProgress(); // 顶部进度条
|
||||
setupBeforeEachGuard(router); // 路由前置守卫
|
||||
setupAfterEachGuard(router); // 路由后置守卫
|
||||
export function initRouter(app: App<Element>) {
|
||||
app.use(router);
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
||||
// 主页路径,默认使用菜单第一个有效路径,配置后使用此路径
|
||||
export const HOME_PAGE_PATH = '';
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export const articleRoutes: AppRouteRecord = {
|
||||
path: '/article',
|
||||
name: 'Article',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.article.title',
|
||||
icon: 'ri:book-2-line',
|
||||
roles: ['R_SUPER', 'R_ADMIN'],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'article-list',
|
||||
name: 'ArticleList',
|
||||
component: '/article/list',
|
||||
meta: {
|
||||
title: 'menus.article.articleList',
|
||||
icon: 'ri:article-line',
|
||||
keepAlive: true,
|
||||
authList: [
|
||||
{ title: '新增', authMark: 'add' },
|
||||
{ title: '编辑', authMark: 'edit' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'detail/:id',
|
||||
name: 'ArticleDetail',
|
||||
component: '/article/detail',
|
||||
meta: {
|
||||
title: 'menus.article.articleDetail',
|
||||
isHide: true,
|
||||
keepAlive: true,
|
||||
activePath: '/article/article-list',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'comment',
|
||||
name: 'ArticleComment',
|
||||
component: '/article/comment',
|
||||
meta: {
|
||||
title: 'menus.article.comment',
|
||||
icon: 'ri:mail-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'publish',
|
||||
name: 'ArticlePublish',
|
||||
component: '/article/publish',
|
||||
meta: {
|
||||
title: 'menus.article.articlePublish',
|
||||
icon: 'ri:telegram-2-line',
|
||||
keepAlive: true,
|
||||
authList: [{ title: '发布', authMark: 'add' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export const dashboardRoutes: AppRouteRecord = {
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.dashboard.title',
|
||||
icon: 'ri:pie-chart-line',
|
||||
roles: ['R_SUPER', 'R_ADMIN'],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'console',
|
||||
name: 'Console',
|
||||
component: '/dashboard/console',
|
||||
meta: {
|
||||
title: 'menus.dashboard.console',
|
||||
icon: 'ri:home-smile-2-line',
|
||||
keepAlive: false,
|
||||
fixedTab: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'analysis',
|
||||
name: 'Analysis',
|
||||
component: '/dashboard/analysis',
|
||||
meta: {
|
||||
title: 'menus.dashboard.analysis',
|
||||
icon: 'ri:align-item-bottom-line',
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'ecommerce',
|
||||
name: 'Ecommerce',
|
||||
component: '/dashboard/ecommerce',
|
||||
meta: {
|
||||
title: 'menus.dashboard.ecommerce',
|
||||
icon: 'ri:bar-chart-box-line',
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,134 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export const examplesRoutes: AppRouteRecord = {
|
||||
path: '/examples',
|
||||
name: 'Examples',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.examples.title',
|
||||
icon: 'ri:sparkling-line',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'permission',
|
||||
name: 'Permission',
|
||||
component: '',
|
||||
meta: {
|
||||
title: 'menus.examples.permission.title',
|
||||
icon: 'ri:fingerprint-line',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'switch-role',
|
||||
name: 'PermissionSwitchRole',
|
||||
component: '/examples/permission/switch-role',
|
||||
meta: {
|
||||
title: 'menus.examples.permission.switchRole',
|
||||
icon: 'ri:contacts-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'button-auth',
|
||||
name: 'PermissionButtonAuth',
|
||||
component: '/examples/permission/button-auth',
|
||||
meta: {
|
||||
title: 'menus.examples.permission.buttonAuth',
|
||||
icon: 'ri:mouse-line',
|
||||
keepAlive: true,
|
||||
authList: [
|
||||
{ title: '新增', authMark: 'add' },
|
||||
{ title: '编辑', authMark: 'edit' },
|
||||
{ title: '删除', authMark: 'delete' },
|
||||
{ title: '导出', authMark: 'export' },
|
||||
{ title: '查看', authMark: 'view' },
|
||||
{ title: '发布', authMark: 'publish' },
|
||||
{ title: '配置', authMark: 'config' },
|
||||
{ title: '管理', authMark: 'manage' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'page-visibility',
|
||||
name: 'PermissionPageVisibility',
|
||||
component: '/examples/permission/page-visibility',
|
||||
meta: {
|
||||
title: 'menus.examples.permission.pageVisibility',
|
||||
icon: 'ri:user-3-line',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'tabs',
|
||||
name: 'Tabs',
|
||||
component: '/examples/tabs',
|
||||
meta: {
|
||||
title: 'menus.examples.tabs',
|
||||
icon: 'ri:price-tag-line',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tables/basic',
|
||||
name: 'TablesBasic',
|
||||
component: '/examples/tables/basic',
|
||||
meta: {
|
||||
title: 'menus.examples.tablesBasic',
|
||||
icon: 'ri:layout-grid-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tables',
|
||||
name: 'Tables',
|
||||
component: '/examples/tables',
|
||||
meta: {
|
||||
title: 'menus.examples.tables',
|
||||
icon: 'ri:table-3',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'forms',
|
||||
name: 'Forms',
|
||||
component: '/examples/forms',
|
||||
meta: {
|
||||
title: 'menus.examples.forms',
|
||||
icon: 'ri:table-view',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'form/search-bar',
|
||||
name: 'SearchBar',
|
||||
component: '/examples/forms/search-bar',
|
||||
meta: {
|
||||
title: 'menus.examples.searchBar',
|
||||
icon: 'ri:table-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tables/tree',
|
||||
name: 'TablesTree',
|
||||
component: '/examples/tables/tree',
|
||||
meta: {
|
||||
title: 'menus.examples.tablesTree',
|
||||
icon: 'ri:layout-2-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'socket-chat',
|
||||
name: 'SocketChat',
|
||||
component: '/examples/socket-chat',
|
||||
meta: {
|
||||
title: 'menus.examples.socketChat',
|
||||
icon: 'ri:shake-hands-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export const exceptionRoutes: AppRouteRecord = {
|
||||
path: '/exception',
|
||||
name: 'Exception',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.exception.title',
|
||||
icon: 'ri:error-warning-line',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '403',
|
||||
name: 'Exception403',
|
||||
component: '/exception/403',
|
||||
meta: {
|
||||
title: 'menus.exception.forbidden',
|
||||
keepAlive: true,
|
||||
isHideTab: true,
|
||||
isFullPage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '404',
|
||||
name: 'Exception404',
|
||||
component: '/exception/404',
|
||||
meta: {
|
||||
title: 'menus.exception.notFound',
|
||||
keepAlive: true,
|
||||
isHideTab: true,
|
||||
isFullPage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '500',
|
||||
name: 'Exception500',
|
||||
component: '/exception/500',
|
||||
meta: {
|
||||
title: 'menus.exception.serverError',
|
||||
keepAlive: true,
|
||||
isHideTab: true,
|
||||
isFullPage: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
import { WEB_LINKS } from '@/utils/constants';
|
||||
|
||||
export const helpRoutes: AppRouteRecord[] = [
|
||||
{
|
||||
name: 'Document',
|
||||
path: '',
|
||||
component: '',
|
||||
meta: {
|
||||
title: 'menus.help.document',
|
||||
icon: 'ri:bill-line',
|
||||
link: WEB_LINKS.DOCS,
|
||||
isIframe: false,
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'LiteVersion',
|
||||
path: '',
|
||||
component: '',
|
||||
meta: {
|
||||
title: 'menus.help.liteVersion',
|
||||
icon: 'ri:bus-2-line',
|
||||
link: WEB_LINKS.LiteVersion,
|
||||
isIframe: false,
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'OldVersion',
|
||||
path: '',
|
||||
component: '',
|
||||
meta: {
|
||||
title: 'menus.help.oldVersion',
|
||||
icon: 'ri:subway-line',
|
||||
link: WEB_LINKS.OldVersion,
|
||||
isIframe: false,
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ChangeLog',
|
||||
path: '/change/log',
|
||||
component: '/change/log',
|
||||
meta: {
|
||||
title: 'menus.plan.log',
|
||||
showTextBadge: `v${__APP_VERSION__}`,
|
||||
icon: 'ri:gamepad-line',
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,27 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
import { dashboardRoutes } from './dashboard';
|
||||
import { templateRoutes } from './template';
|
||||
import { widgetsRoutes } from './widgets';
|
||||
import { examplesRoutes } from './examples';
|
||||
import { systemRoutes } from './system';
|
||||
import { articleRoutes } from './article';
|
||||
import { resultRoutes } from './result';
|
||||
import { exceptionRoutes } from './exception';
|
||||
import { safeguardRoutes } from './safeguard';
|
||||
import { helpRoutes } from './help';
|
||||
|
||||
/**
|
||||
* 导出所有模块化路由
|
||||
*/
|
||||
export const routeModules: AppRouteRecord[] = [
|
||||
dashboardRoutes,
|
||||
templateRoutes,
|
||||
widgetsRoutes,
|
||||
examplesRoutes,
|
||||
systemRoutes,
|
||||
articleRoutes,
|
||||
resultRoutes,
|
||||
exceptionRoutes,
|
||||
safeguardRoutes,
|
||||
...helpRoutes,
|
||||
];
|
||||
@@ -1,33 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export const resultRoutes: AppRouteRecord = {
|
||||
path: '/result',
|
||||
name: 'Result',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.result.title',
|
||||
icon: 'ri:checkbox-circle-line',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'success',
|
||||
name: 'ResultSuccess',
|
||||
component: '/result/success',
|
||||
meta: {
|
||||
title: 'menus.result.success',
|
||||
icon: 'ri:checkbox-circle-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'fail',
|
||||
name: 'ResultFail',
|
||||
component: '/result/fail',
|
||||
meta: {
|
||||
title: 'menus.result.fail',
|
||||
icon: 'ri:close-circle-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export const safeguardRoutes: AppRouteRecord = {
|
||||
path: '/safeguard',
|
||||
name: 'Safeguard',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.safeguard.title',
|
||||
icon: 'ri:shield-check-line',
|
||||
keepAlive: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'server',
|
||||
name: 'SafeguardServer',
|
||||
component: '/safeguard/server',
|
||||
meta: {
|
||||
title: 'menus.safeguard.server',
|
||||
icon: 'ri:hard-drive-3-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,149 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export const systemRoutes: AppRouteRecord = {
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.system.title',
|
||||
icon: 'ri:user-3-line',
|
||||
roles: ['R_SUPER', 'R_ADMIN'],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'User',
|
||||
component: '/system/user',
|
||||
meta: {
|
||||
title: 'menus.system.user',
|
||||
icon: 'ri:user-line',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
name: 'Role',
|
||||
component: '/system/role',
|
||||
meta: {
|
||||
title: 'menus.system.role',
|
||||
icon: 'ri:user-settings-line',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'user-center',
|
||||
name: 'UserCenter',
|
||||
component: '/system/user-center',
|
||||
meta: {
|
||||
title: 'menus.system.userCenter',
|
||||
icon: 'ri:user-line',
|
||||
isHide: true,
|
||||
keepAlive: true,
|
||||
isHideTab: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'menu',
|
||||
name: 'Menus',
|
||||
component: '/system/menu',
|
||||
meta: {
|
||||
title: 'menus.system.menu',
|
||||
icon: 'ri:menu-line',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER'],
|
||||
authList: [
|
||||
{ title: '新增', authMark: 'add' },
|
||||
{ title: '编辑', authMark: 'edit' },
|
||||
{ title: '删除', authMark: 'delete' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'nested',
|
||||
name: 'Nested',
|
||||
component: '',
|
||||
meta: {
|
||||
title: 'menus.system.nested',
|
||||
icon: 'ri:menu-unfold-3-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'menu1',
|
||||
name: 'NestedMenu1',
|
||||
component: '/system/nested/menu1',
|
||||
meta: {
|
||||
title: 'menus.system.menu1',
|
||||
icon: 'ri:align-justify',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'menu2',
|
||||
name: 'NestedMenu2',
|
||||
component: '',
|
||||
meta: {
|
||||
title: 'menus.system.menu2',
|
||||
icon: 'ri:align-justify',
|
||||
keepAlive: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'menu2-1',
|
||||
name: 'NestedMenu2-1',
|
||||
component: '/system/nested/menu2',
|
||||
meta: {
|
||||
title: 'menus.system.menu21',
|
||||
icon: 'ri:align-justify',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'menu3',
|
||||
name: 'NestedMenu3',
|
||||
component: '',
|
||||
meta: {
|
||||
title: 'menus.system.menu3',
|
||||
icon: 'ri:align-justify',
|
||||
keepAlive: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'menu3-1',
|
||||
name: 'NestedMenu3-1',
|
||||
component: '/system/nested/menu3',
|
||||
meta: {
|
||||
title: 'menus.system.menu31',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'menu3-2',
|
||||
name: 'NestedMenu3-2',
|
||||
component: '',
|
||||
meta: {
|
||||
title: 'menus.system.menu32',
|
||||
keepAlive: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'menu3-2-1',
|
||||
name: 'NestedMenu3-2-1',
|
||||
component: '/system/nested/menu3/menu3-2',
|
||||
meta: {
|
||||
title: 'menus.system.menu321',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export const templateRoutes: AppRouteRecord = {
|
||||
path: '/template',
|
||||
name: 'Template',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.template.title',
|
||||
icon: 'ri:apps-2-line',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'cards',
|
||||
name: 'Cards',
|
||||
component: '/template/cards',
|
||||
meta: {
|
||||
title: 'menus.template.cards',
|
||||
icon: 'ri:wallet-line',
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'banners',
|
||||
name: 'Banners',
|
||||
component: '/template/banners',
|
||||
meta: {
|
||||
title: 'menus.template.banners',
|
||||
icon: 'ri:rectangle-line',
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'charts',
|
||||
name: 'Charts',
|
||||
component: '/template/charts',
|
||||
meta: {
|
||||
title: 'menus.template.charts',
|
||||
icon: 'ri:bar-chart-box-line',
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
name: 'Map',
|
||||
component: '/template/map',
|
||||
meta: {
|
||||
title: 'menus.template.map',
|
||||
icon: 'ri:map-pin-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'chat',
|
||||
name: 'Chat',
|
||||
component: '/template/chat',
|
||||
meta: {
|
||||
title: 'menus.template.chat',
|
||||
icon: 'ri:message-3-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'calendar',
|
||||
name: 'Calendar',
|
||||
component: '/template/calendar',
|
||||
meta: {
|
||||
title: 'menus.template.calendar',
|
||||
icon: 'ri:calendar-2-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pricing',
|
||||
name: 'Pricing',
|
||||
component: '/template/pricing',
|
||||
meta: {
|
||||
title: 'menus.template.pricing',
|
||||
icon: 'ri:money-cny-box-line',
|
||||
keepAlive: true,
|
||||
isFullPage: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,146 +0,0 @@
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
|
||||
export const widgetsRoutes: AppRouteRecord = {
|
||||
path: '/widgets',
|
||||
name: 'Widgets',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.widgets.title',
|
||||
icon: 'ri:apps-2-add-line',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'icon',
|
||||
name: 'Icon',
|
||||
component: '/widgets/icon',
|
||||
meta: {
|
||||
title: 'menus.widgets.icon',
|
||||
icon: 'ri:palette-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'image-crop',
|
||||
name: 'ImageCrop',
|
||||
component: '/widgets/image-crop',
|
||||
meta: {
|
||||
title: 'menus.widgets.imageCrop',
|
||||
icon: 'ri:screenshot-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'excel',
|
||||
name: 'Excel',
|
||||
component: '/widgets/excel',
|
||||
meta: {
|
||||
title: 'menus.widgets.excel',
|
||||
icon: 'ri:download-2-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'video',
|
||||
name: 'Video',
|
||||
component: '/widgets/video',
|
||||
meta: {
|
||||
title: 'menus.widgets.video',
|
||||
icon: 'ri:vidicon-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'count-to',
|
||||
name: 'CountTo',
|
||||
component: '/widgets/count-to',
|
||||
meta: {
|
||||
title: 'menus.widgets.countTo',
|
||||
icon: 'ri:anthropic-line',
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'wang-editor',
|
||||
name: 'WangEditor',
|
||||
component: '/widgets/wang-editor',
|
||||
meta: {
|
||||
title: 'menus.widgets.wangEditor',
|
||||
icon: 'ri:t-box-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'watermark',
|
||||
name: 'Watermark',
|
||||
component: '/widgets/watermark',
|
||||
meta: {
|
||||
title: 'menus.widgets.watermark',
|
||||
icon: 'ri:water-flash-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'context-menu',
|
||||
name: 'ContextMenu',
|
||||
component: '/widgets/context-menu',
|
||||
meta: {
|
||||
title: 'menus.widgets.contextMenu',
|
||||
icon: 'ri:menu-2-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'qrcode',
|
||||
name: 'Qrcode',
|
||||
component: '/widgets/qrcode',
|
||||
meta: {
|
||||
title: 'menus.widgets.qrcode',
|
||||
icon: 'ri:qr-code-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'drag',
|
||||
name: 'Drag',
|
||||
component: '/widgets/drag',
|
||||
meta: {
|
||||
title: 'menus.widgets.drag',
|
||||
icon: 'ri:drag-move-fill',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'text-scroll',
|
||||
name: 'TextScroll',
|
||||
component: '/widgets/text-scroll',
|
||||
meta: {
|
||||
title: 'menus.widgets.textScroll',
|
||||
icon: 'ri:input-method-line',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'fireworks',
|
||||
name: 'Fireworks',
|
||||
component: '/widgets/fireworks',
|
||||
meta: {
|
||||
title: 'menus.widgets.fireworks',
|
||||
icon: 'ri:magic-line',
|
||||
keepAlive: true,
|
||||
showTextBadge: 'Hot',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/outside/iframe/elementui',
|
||||
name: 'ElementUI',
|
||||
component: '',
|
||||
meta: {
|
||||
title: 'menus.widgets.elementUI',
|
||||
icon: 'ri:apps-2-line',
|
||||
keepAlive: false,
|
||||
link: 'https://element-plus.org/zh-CN/component/overview.html',
|
||||
isIframe: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
// 权限文档:https://www.artd.pro/docs/zh/guide/in-depth/permission.html
|
||||
import { AppRouteRecord } from '@/types/router';
|
||||
import { routeModules } from '../modules';
|
||||
|
||||
/**
|
||||
* 动态路由(需要权限才能访问的路由)
|
||||
* 用于渲染菜单以及根据菜单权限动态加载路由,如果没有权限无法访问
|
||||
*/
|
||||
export const asyncRoutes: AppRouteRecord[] = routeModules;
|
||||
@@ -1,72 +0,0 @@
|
||||
import { AppRouteRecordRaw } from '@/utils/router';
|
||||
|
||||
/**
|
||||
* 静态路由配置(不需要权限就能访问的路由)
|
||||
*
|
||||
* 属性说明:
|
||||
* isHideTab: true 表示不在标签页中显示
|
||||
*
|
||||
* 注意事项:
|
||||
* 1、path、name 不要和动态路由冲突,否则会导致路由冲突无法访问
|
||||
* 2、静态路由不管是否登录都可以访问
|
||||
*/
|
||||
export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
// 不需要登录就能访问的路由示例
|
||||
// {
|
||||
// path: '/welcome',
|
||||
// name: 'WelcomeStatic',
|
||||
// component: () => import('@views/dashboard/console/index.vue'),
|
||||
// meta: { title: 'menus.dashboard.title' }
|
||||
// },
|
||||
{
|
||||
path: '/auth/login',
|
||||
name: 'Login',
|
||||
component: () => import('@views/module_system/auth/login/index.vue'),
|
||||
meta: { title: 'menus.login.title', isHideTab: true },
|
||||
},
|
||||
{
|
||||
path: '/auth/register',
|
||||
name: 'Register',
|
||||
component: () => import('@views/module_system/auth/register/index.vue'),
|
||||
meta: { title: 'menus.register.title', isHideTab: true },
|
||||
},
|
||||
{
|
||||
path: '/auth/forget-password',
|
||||
name: 'ForgetPassword',
|
||||
component: () => import('@views/module_system/auth/forget-password/index.vue'),
|
||||
meta: { title: 'menus.forgetPassword.title', isHideTab: true },
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Exception403',
|
||||
component: () => import('@views/exception/403/index.vue'),
|
||||
meta: { title: '403', isHideTab: true },
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'Exception404',
|
||||
component: () => import('@views/exception/404/index.vue'),
|
||||
meta: { title: '404', isHideTab: true },
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: 'Exception500',
|
||||
component: () => import('@views/exception/500/index.vue'),
|
||||
meta: { title: '500', isHideTab: true },
|
||||
},
|
||||
{
|
||||
path: '/outside',
|
||||
component: () => import('@views/index/index.vue'),
|
||||
name: 'Outside',
|
||||
meta: { title: 'menus.outside.title' },
|
||||
children: [
|
||||
// iframe 内嵌页面
|
||||
{
|
||||
path: '/outside/iframe/:path',
|
||||
name: 'Iframe',
|
||||
component: () => import('@/views/outside/Iframe.vue'),
|
||||
meta: { title: 'iframe' },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* 公共路由别名
|
||||
* 存放系统级公共路由路径,如布局容器、登录页等
|
||||
*/
|
||||
export enum RoutesAlias {
|
||||
Layout = '/index/index', // 布局容器
|
||||
Login = '/auth/login', // 登录页
|
||||
}
|
||||
Reference in New Issue
Block a user