From 67ef656068a2a5ca9a98335cbf540b584d510131 Mon Sep 17 00:00:00 2001 From: wangxiang <1827945911@qq.com> Date: Sat, 16 Apr 2022 04:08:46 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=9F=20=E7=94=A8=E6=88=B7=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=8C=81=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kicc-ui/src/components/Menu/src/BasicMenu.vue | 4 +- kicc-ui/src/router/constant.ts | 8 +- kicc-ui/src/router/guard/index.ts | 41 ++++------- kicc-ui/src/router/guard/paramMenuGuard.ts | 31 -------- kicc-ui/src/router/guard/permissionGuard.ts | 59 +++++---------- kicc-ui/src/router/guard/stateGuard.ts | 23 +++--- kicc-ui/src/router/helper/menuHelper.ts | 73 +++++-------------- kicc-ui/src/router/helper/routeHelper.ts | 65 ++++++----------- kicc-ui/src/router/types.ts | 3 - kicc-ui/src/store/modules/permission.ts | 2 +- kicc-ui/types/vue-router.d.ts | 2 - 11 files changed, 90 insertions(+), 221 deletions(-) delete mode 100644 kicc-ui/src/router/guard/paramMenuGuard.ts diff --git a/kicc-ui/src/components/Menu/src/BasicMenu.vue b/kicc-ui/src/components/Menu/src/BasicMenu.vue index 8e344cc3..97ee87ed 100644 --- a/kicc-ui/src/components/Menu/src/BasicMenu.vue +++ b/kicc-ui/src/components/Menu/src/BasicMenu.vue @@ -138,13 +138,13 @@ return; } const path = (route || unref(currentRoute)).path; - setOpenKeys(path); + await setOpenKeys(path); if (unref(currentActiveMenu)) return; if (props.isHorizontal && unref(getSplit)) { const parentPath = await getCurrentParentPath(path); menuState.selectedKeys = [parentPath]; } else { - const parentPaths = await getAllParentPath(props.items, path); + const parentPaths = getAllParentPath(props.items, path); menuState.selectedKeys = parentPaths; } } diff --git a/kicc-ui/src/router/constant.ts b/kicc-ui/src/router/constant.ts index 91485d70..162ed148 100644 --- a/kicc-ui/src/router/constant.ts +++ b/kicc-ui/src/router/constant.ts @@ -16,9 +16,5 @@ export const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exceptio /** 默认布局 */ export const LAYOUT = () => import('/@/layouts/default/index.vue'); -/** 父布局 */ -export const getParentLayout = (_name?: string) => { - return () => new Promise((resolve) => { - resolve({ name: PARENT_LAYOUT_NAME }); - }); -}; +/** 定义一个空组件什么都没有只有一个组件名称充当父布局,不让没有组件的时候报找不到组件的错误 */ +export const getParentLayout = (_name?: string) => () => new Promise((resolve) => resolve({ name: PARENT_LAYOUT_NAME })); diff --git a/kicc-ui/src/router/guard/index.ts b/kicc-ui/src/router/guard/index.ts index 2caf42cd..561e272b 100644 --- a/kicc-ui/src/router/guard/index.ts +++ b/kicc-ui/src/router/guard/index.ts @@ -18,9 +18,8 @@ import { createPermissionGuard } from './permissionGuard'; import { createStateGuard } from './stateGuard'; import nProgress from 'nprogress'; import projectSetting from '/@/settings/projectSetting'; -import { createParamMenuGuard } from './paramMenuGuard'; -// 不要改变创建的顺序 +/** 不要改变创建的顺序 */ export function setupRouterGuard(router: Router) { createPageGuard(router); createPageLoadingGuard(router); @@ -29,16 +28,14 @@ export function setupRouterGuard(router: Router) { createMessageGuard(router); createProgressGuard(router); createPermissionGuard(router); - // 必须在 createPermissionGuard 之后(菜单已构建) - createParamMenuGuard(router); createStateGuard(router); } -/** 处理页面状态的钩子 */ +/** 处理页面缓存 */ function createPageGuard(router: Router) { const loadedPageMap = new Map(); router.beforeEach(async (to) => { - // 页面已经加载完毕,再次打开会更快,不需要再做加载等处理 + // 页面已经加载完毕,再次打开会更快,不需要再做加载等处理 to.meta.loaded = !!loadedPageMap.get(to.path); // 通知路由更改 setRouteChange(to); @@ -49,28 +46,23 @@ function createPageGuard(router: Router) { }); } -/** 用于处理页面加载状态 */ +/** 处理页面正在加载中 */ function createPageLoadingGuard(router: Router) { const userStore = useUserStoreWithOut(); const appStore = useAppStoreWithOut(); const { getOpenPageLoading } = useTransitionSetting(); router.beforeEach(async (to) => { - if (!userStore.getAccessToken) { - return true; - } - if (to.meta.loaded) { - return true; - } + if (!userStore.getAccessToken) return true; + if (to.meta.loaded) return true; if (unref(getOpenPageLoading)) { - appStore.setPageLoadingAction(true); + await appStore.setPageLoadingAction(true); return true; } return true; }); router.afterEach(async () => { if (unref(getOpenPageLoading)) { - // TODO 寻找更好的方法 - // 定时器模拟加载时间,防止闪烁过快, + // 定时器模拟加载时间,防止闪烁过快 TODO 寻找更好的方法 setTimeout(() => { appStore.setPageLoading(false); }, 220); @@ -79,7 +71,7 @@ function createPageLoadingGuard(router: Router) { }); } -/** 路由切换时关闭当前页面完成请求的接口 */ +/** 处理路由切换时关闭当前页面正在发生请求的axios */ function createHttpGuard(router: Router) { const { removeAllHttpPending } = projectSetting; let axiosCanceler: Nullable; @@ -93,11 +85,9 @@ function createHttpGuard(router: Router) { }); } -/** 路由开关回到顶部 */ +/** 处理路由切换时自动滚动到顶部 */ function createScrollGuard(router: Router) { - const isHash = (href: string) => { - return /^#/.test(href); - }; + const isHash = (href: string) => /^#/.test(href); const body = document.body; router.afterEach(async (to) => { // 滚动顶部 @@ -106,7 +96,7 @@ function createScrollGuard(router: Router) { }); } -/** 用于在路由切换时关闭消息实例 */ +/** 处理路由切换时关闭当前页面正在提示的消息实例 */ export function createMessageGuard(router: Router) { const { closeMessageOnSwitch } = projectSetting; router.beforeEach(async () => { @@ -116,18 +106,17 @@ export function createMessageGuard(router: Router) { notification.destroy(); } } catch (error) { - warn('message guard error:' + error); + warn('消息关闭错误:' + error); } return true; }); } +/** 处理发生请求时显示请求进度条 */ export function createProgressGuard(router: Router) { const { getOpenNProgress } = useTransitionSetting(); router.beforeEach(async (to) => { - if (to.meta.loaded) { - return true; - } + if (to.meta.loaded) return true; unref(getOpenNProgress) && nProgress.start(); return true; }); diff --git a/kicc-ui/src/router/guard/paramMenuGuard.ts b/kicc-ui/src/router/guard/paramMenuGuard.ts deleted file mode 100644 index 322eb7d6..00000000 --- a/kicc-ui/src/router/guard/paramMenuGuard.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @program: kicc-ui - * @description: 菜单路由导航守卫 - * 菜单路由参数保留一份,防止恶意修改参数 - * @author: entfrm开发团队-王翔 - * @create: 2022/4/9 - */ - -import type { Router } from 'vue-router'; -import { configureDynamicParamsMenu } from '../helper/menuHelper'; -import { Menu } from '../types'; -import { usePermissionStoreWithOut } from '/@/store/modules/permission'; - -export function createParamMenuGuard(router: Router) { - const permissionStore = usePermissionStoreWithOut(); - router.beforeEach(async (to, _, next) => { - // 过滤无名路由 - if (!to.name) { - next(); - return; - } - // 检测是否菜单已建立 - if (!permissionStore.getIsDynamicAddedRoute) { - next(); - return; - } - const menus: Menu[] = permissionStore.getMenuList; - menus.forEach((item) => configureDynamicParamsMenu(item, to.params)); - next(); - }); -} diff --git a/kicc-ui/src/router/guard/permissionGuard.ts b/kicc-ui/src/router/guard/permissionGuard.ts index 0f432ee6..721953b1 100644 --- a/kicc-ui/src/router/guard/permissionGuard.ts +++ b/kicc-ui/src/router/guard/permissionGuard.ts @@ -20,57 +20,36 @@ export function createPermissionGuard(router: Router) { const userStore = useUserStoreWithOut(); const permissionStore = usePermissionStoreWithOut(); router.beforeEach(async (to, from, next) => { - // 首次登录进来进行跳转页面 - if (from.path === ROOT_HOME && to.path === PageEnum.BASE_HOME && userStore.getUserInfo.homePath !== PageEnum.BASE_HOME) { - next(userStore.getUserInfo.homePath); - return; - } - // 白名单可直接进入 - if (whitePathList.includes(to.path as PageEnum)) { - next(); - return; - } + // 用户个性化设置根路由调整地址(首页地址),如果能进去说明以及获取了token不需要进行权限校验 + if (from.path === ROOT_HOME && to.path === PageEnum.BASE_HOME && userStore.getUserInfo.homePath !== PageEnum.BASE_HOME) + return next(userStore.getUserInfo.homePath!); + // 放过白名单路由 + if (whitePathList.includes(to.path as PageEnum)) + return next(); + // 校验token权限信息 const token = userStore.getAccessToken; - // 令牌不存在 if (!token) { - // 您可以在未经许可的情况下访问,您需要将路由meta.ignoreAuth设置为true - if (to.meta.ignoreAuth) { - next(); - return; - } + // 是否忽略权限验证,需要将路由ignoreAuth设置true + if (to.meta.ignoreAuth) return next(); // 重定向登录页面 - const redirectData: { path: string; replace: boolean; query?: Recordable } = { - path: LOGIN_PATH, - replace: true, - }; + const redirectLogin: { path: string; replace: boolean; query?: Recordable; } = { path: LOGIN_PATH, replace: true }; + // 补充拦截路由信息 if (to.path) { - redirectData.query = { - ...redirectData.query, + redirectLogin.query = { + ...redirectLogin.query, redirect: to.path, }; } - next(redirectData); - return; - } - // 处理完登录后未找到页面直接跳转到404页面 - if (from.path === LOGIN_PATH && to.name === PAGE_NOT_FOUND_NAME && to.fullPath !== (userStore.getUserInfo.homePath || PageEnum.BASE_HOME)) { - next(userStore.getUserInfo.homePath || PageEnum.BASE_HOME); - console.log({ from, to }); - return; - } - if (permissionStore.getIsDynamicAddedRoute) { - next(); - return; + return next(redirectLogin); } + // 检测是否构建完路由与菜单 + if (permissionStore.getIsDynamicAddedRoute) return next(); + // 构建路由与菜单 const routes = await permissionStore.buildRoutesAction(); - routes.forEach((route) => { - router.addRoute(route as unknown as RouteRecordRaw); - }); + routes.forEach((route) => router.addRoute(route as unknown as RouteRecordRaw)); permissionStore.setDynamicAddedRoute(true); - + // 动态添加路由后,此处应当重定向到fullPath,否则会加载404页面内容,添加query以免丢失 if (to.name === PAGE_NOT_FOUND_NAME) { - // 动态添加路由后,此处应当重定向到fullPath,否则会加载404页面内容 - // fix: 添加query以免丢失 next({ path: to.fullPath, replace: true, query: to.query }); } else { const redirectPath = (from.query.redirect || to.path) as string; diff --git a/kicc-ui/src/router/guard/stateGuard.ts b/kicc-ui/src/router/guard/stateGuard.ts index 5eeb04e8..5c6cbda4 100644 --- a/kicc-ui/src/router/guard/stateGuard.ts +++ b/kicc-ui/src/router/guard/stateGuard.ts @@ -1,7 +1,7 @@ /** * @program: kicc-ui - * @description: 状态保护导航守卫 - * 只要进入登录页面的路由,就会清除认证信息 + * @description: 清除数据储存处导航守卫 + * 只要进入登录页面的路由,就会清除认证信息 * @author: entfrm开发团队-王翔 * @create: 2022/4/9 */ @@ -16,17 +16,18 @@ import { removeTabChangeListener } from '/@/logics/mitt/routeChange'; export function createStateGuard(router: Router) { router.afterEach((to) => { - const tabStore = useMultipleTabStore(); - const userStore = useUserStore(); - const appStore = useAppStore(); - const permissionStore = usePermissionStore(); // 只需进入登录页面,清除认证信息即可 if (to.path === PageEnum.BASE_LOGIN) { - appStore.resetAllState(); - permissionStore.resetState(); - tabStore.resetState(); - userStore.resetState(); - removeTabChangeListener(); + const tabStore = useMultipleTabStore(); + const userStore = useUserStore(); + const appStore = useAppStore(); + const permissionStore = usePermissionStore(); + appStore.resetAllState().then(()=>{ + permissionStore.resetState(); + tabStore.resetState(); + userStore.resetState(); + removeTabChangeListener(); + }); } }); } diff --git a/kicc-ui/src/router/helper/menuHelper.ts b/kicc-ui/src/router/helper/menuHelper.ts index bed0987c..78ee4f11 100644 --- a/kicc-ui/src/router/helper/menuHelper.ts +++ b/kicc-ui/src/router/helper/menuHelper.ts @@ -11,50 +11,32 @@ import type { Menu, AppRouteRecordRaw } from '/@/router/types'; import { findPath, treeMap } from '/@/utils/helper/treeHelper'; import { cloneDeep } from 'lodash-es'; import { isUrl } from '/@/utils/is'; -import { RouteParams } from 'vue-router'; -import { toRaw } from 'vue'; import {usePermissionStore} from "/@/store/modules/permission"; +/** 获取当前菜单的所有父菜单路径 */ export function getAllParentPath(treeData: T[], path: string) { const menuList = findPath(treeData, (n) => n.path === path) as Menu[]; return (menuList || []).map((item) => item.path); } -function joinParentPath(menus: Menu[], parentPath = '') { +/** 构建嵌套路由,具体详情参考:https://next.router.vuejs.org/guide/essentials/nested-routes.html */ +function joinNestedRoute(menus: Menu[], parentPath = '') { for (let index = 0; index < menus.length; index++) { const menu = menus[index]; - // https://next.router.vuejs.org/guide/essentials/nested-routes.html - // 请注意,以 开头的嵌套路径将被视为根路径 - // 这允许您利用组件嵌套,而无需使用嵌套 URL - if (!(menu.path.startsWith('/') || isUrl(menu.path))) { - // 路径不以 开头,也不是 url,加入父路径 - menu.path = `${parentPath}/${menu.path}`; - } - if (menu?.children?.length) { - joinParentPath(menu.children, menu.meta?.hidePathForChildren ? parentPath : menu.path); - } + // 请注意,以/开头的嵌套路径将被视为根路径与外嵌网页url不允许加入嵌套路由 + if (!(menu.path.startsWith('/') || isUrl(menu.path))) menu.path = `${parentPath}/${menu.path}`; + // 递归处理子路由的拼接 + if (menu?.children?.length) joinNestedRoute(menu.children, menu.meta?.hidePathForChildren ? parentPath : menu.path); } } -export function transformRouteToMenu(routeModList: AppRouteModule[], routerMapping = false) { - const cloneRouteModList = cloneDeep(routeModList); - const routeList: AppRouteRecordRaw[] = []; - - cloneRouteModList.forEach((item) => { - if (routerMapping && item.meta.hideChildrenInMenu && typeof item.redirect === 'string') { - item.path = item.redirect; - } - if (item.meta?.single) { - const realItem = item?.children?.[0]; - realItem && routeList.push(realItem); - } else { - routeList.push(item); - } - }); +/** 将路由对象变成菜单对象 */ +export function transformRouteToMenu(routeModList: AppRouteModule[]) { + const routeList: AppRouteRecordRaw[] = cloneDeep(routeModList); + // 提取树指定的树形结构 const list = treeMap(routeList, { conversion: (node: AppRouteRecordRaw) => { const { meta: { title, hideMenu = false } = {} } = node; - return { ...(node.meta || {}), meta: node.meta, @@ -63,32 +45,10 @@ export function transformRouteToMenu(routeModList: AppRouteModule[], routerMappi path: node.path, ...(node.redirect ? { redirect: node.redirect } : {}), }; - }, - }); - joinParentPath(list); - return cloneDeep(list); -} - -/** 带有给定参数的配置菜单 */ -const menuParamRegex = /(?::)([\s\S]+?)((?=\/)|$)/g; -export function configureDynamicParamsMenu(menu: Menu, params: RouteParams) { - const { path, paramPath } = toRaw(menu); - let realPath = paramPath ? paramPath : path; - const matchArr = realPath.match(menuParamRegex); - - matchArr?.forEach((it) => { - const realIt = it.substr(1); - if (params[realIt]) { - realPath = realPath.replace(`:${realIt}`, params[realIt] as string); } }); - // 保存原始参数路径 - if (!paramPath && matchArr && matchArr.length > 0) { - menu.paramPath = path; - } - menu.path = realPath; - // children - menu.children?.forEach((item) => configureDynamicParamsMenu(item, params)); + joinNestedRoute(list); + return list; } /** 异步获取后台菜单数据 */ @@ -97,20 +57,21 @@ export const getMenus = async (): Promise => { return permissionStore.getMenuList.filter((item) => !item.hideMenu); }; +/** 获取当前菜单父级菜单路径 */ export async function getCurrentParentPath(currentPath: string) { const menus = await getMenus(); - const allParentPath = await getAllParentPath(menus, currentPath); + const allParentPath = getAllParentPath(menus, currentPath); return allParentPath?.[0]; } -/** 获取一级菜单,删除子项 */ +/** 获取一级菜单,删除子项 */ export async function getShallowMenus(): Promise { const menus = await getMenus(); const shallowMenuList = menus.map((item) => ({ ...item, children: undefined })); return shallowMenuList; } -/** 获取菜单的孩子 */ +/** 获取一级菜单的子集菜单 */ export async function getChildrenMenus(parentPath: string) { const menus = await getMenus(); const parent = menus.find((item) => item.path === parentPath); diff --git a/kicc-ui/src/router/helper/routeHelper.ts b/kicc-ui/src/router/helper/routeHelper.ts index 4de5570b..cbf16313 100644 --- a/kicc-ui/src/router/helper/routeHelper.ts +++ b/kicc-ui/src/router/helper/routeHelper.ts @@ -6,42 +6,22 @@ * @create: 2022/4/9 */ -import type { AppRouteModule, AppRouteRecordRaw } from '/@/router/types'; +import type { AppRouteModule } from '/@/router/types'; import { getParentLayout, LAYOUT } from '/@/router/constant'; -import { cloneDeep } from 'lodash-es'; import { warn } from '/@/utils/log'; const IFRAME = () => import('/@/views/sys/iframe/FrameBlank.vue'); -const LayoutMap = new Map Promise>(); +/** 布局组件Map */ +const LayoutMap = new Map Promise>(); LayoutMap.set('LAYOUT', LAYOUT); LayoutMap.set('IFRAME', IFRAME); +/** 性能优化,避免多次加载views下的组件 */ let dynamicViewsModules: Record Promise>; -function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) { - dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../views/**/*.{vue,tsx}'); - if (!routes) return; - routes.forEach((item) => { - if (!item.component && item.meta?.frameSrc) { - item.component = 'IFRAME'; - } - const { component, name } = item; - const { children } = item; - if (component) { - const layoutFound = LayoutMap.get(component as string); - if (layoutFound) { - item.component = layoutFound; - } else { - item.component = dynamicImport(dynamicViewsModules, component as string); - } - } else if (name) { - item.component = getParentLayout(); - } - children && asyncImportRoute(children); - }); -} - -function dynamicImport(dynamicViewsModules: Record Promise>, component: string) { +/** 查找导入的views目录下的组件 */ +function findImportComponent(dynamicViewsModules: Record Promise>, component: string) { const keys = Object.keys(dynamicViewsModules); + // 检测同一个目录下是否存在多个名字相同的组件 const matchKeys = keys.filter((key) => { let k = key.replace('../../views', ''); const lastIndex = k.lastIndexOf('.'); @@ -53,30 +33,29 @@ function dynamicImport(dynamicViewsModules: Record Promise 1) { - warn('Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure'); + warn('请不要创建".vue"和".TSX"具有相同文件的文件名称在同一层次结构目录中在视图文件夹下,这将导致动态引入失败!'); return; } } /** 将菜单对象变成路由对象 */ -export function transformObjToRoute(routeList: AppRouteModule[]): T[] { - routeList.forEach((route) => { - const component = route.component as string; +export function transformObjToRoute(routeList: AppRouteModule[]) { + dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../views/**/*.{vue,tsx}'); + routeList.forEach((item) => { + if (!item.component && item.meta?.frameSrc) { + item.component = 'IFRAME'; + } + const { component, name, children } = item; if (component) { - if (component.toUpperCase() === 'LAYOUT') { - route.component = LayoutMap.get(component.toUpperCase()); + const layoutFound = LayoutMap.get(component as string); + if (layoutFound) { + item.component = layoutFound; } else { - route.children = [cloneDeep(route)]; - route.component = LAYOUT; - route.name = `${route.name}Parent`; - route.path = ''; - const meta = route.meta || {}; - meta.single = true; - meta.affix = false; - route.meta = meta; + item.component = findImportComponent(dynamicViewsModules, component as string); } + } else if (name) { + item.component = getParentLayout(); } - route.children && asyncImportRoute(route.children); + children && transformObjToRoute(children); }); - return routeList as unknown as T[]; } diff --git a/kicc-ui/src/router/types.ts b/kicc-ui/src/router/types.ts index df4bd228..8d048934 100644 --- a/kicc-ui/src/router/types.ts +++ b/kicc-ui/src/router/types.ts @@ -30,9 +30,6 @@ export interface Menu { path: string; - // 路径包含参数,自动分配 - paramPath?: string; - disabled?: boolean; children?: Menu[]; diff --git a/kicc-ui/src/store/modules/permission.ts b/kicc-ui/src/store/modules/permission.ts index e285f67e..de925128 100644 --- a/kicc-ui/src/store/modules/permission.ts +++ b/kicc-ui/src/store/modules/permission.ts @@ -112,7 +112,7 @@ export const usePermissionStore = defineStore({ return (await getMenuList().then(data=> { let routeList = data; // 将菜单对象变成路由对象 - routeList = transformObjToRoute(routeList); + transformObjToRoute(routeList); const menuList = transformRouteToMenu(routeList); this.setMenuList(menuList); // 过滤忽略路由配置项,只构建菜单不构建路由 diff --git a/kicc-ui/types/vue-router.d.ts b/kicc-ui/types/vue-router.d.ts index e7ce7073..b8623ff7 100644 --- a/kicc-ui/types/vue-router.d.ts +++ b/kicc-ui/types/vue-router.d.ts @@ -29,8 +29,6 @@ declare module 'vue-router' { hideChildrenInMenu?: boolean; // 承载参数 carryParam?: boolean; - // 在内部用于标记单级菜单 - single?: boolean; // 当前活动菜单 currentActiveMenu?: string; // 从不显示在选项卡中