diff --git a/src/interface/common/route.ts b/src/interface/common/route.ts index cbba94c6..015e45f3 100644 --- a/src/interface/common/route.ts +++ b/src/interface/common/route.ts @@ -1,26 +1,7 @@ import type { RouteRecordRaw } from 'vue-router'; -/** 路由描述 */ -export interface CustomRouteMeta { - /** 路由名称 */ - title?: string; - /** 缓存页面 */ - keepAlive?: boolean; - /** 页面100%视高 */ - fullPage?: boolean; - /** 不作为菜单 */ - isNotMenu?: boolean; - /** 菜单和面包屑对应的图标 */ - icon?: string; - /** 导入的路由模块排序,可用于菜单的排序 */ - order?: number; -} - -/** 路由配置 */ -export type CustomRoute = RouteRecordRaw & { meta: CustomRouteMeta }; - /** 导入的路由模块 */ -export type ImportedRouteModules = Record; +export type ImportedRouteModules = Record; /** 路由声明的key */ export type RouteKey = diff --git a/src/layouts/RouterViewLayout/index.vue b/src/layouts/RouterViewLayout/index.vue deleted file mode 100644 index e905dd5b..00000000 --- a/src/layouts/RouterViewLayout/index.vue +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/src/layouts/common/GlobalHeader/components/GlobalBreadcrumb.vue b/src/layouts/common/GlobalHeader/components/GlobalBreadcrumb.vue index 133d16e7..f5a84ebb 100644 --- a/src/layouts/common/GlobalHeader/components/GlobalBreadcrumb.vue +++ b/src/layouts/common/GlobalHeader/components/GlobalBreadcrumb.vue @@ -61,7 +61,7 @@ function generateBreadcrumb() { function recursionBreadcrumb(routeMatched: RouteLocationMatched[]) { const list: Breadcrumb[] = []; routeMatched.forEach(item => { - if (!item.meta?.isNotMenu) { + if (!item.meta?.notAsMenu) { const routeName = item.name as RouteKey; const breadcrumItem: Breadcrumb = { key: routeName, diff --git a/src/layouts/common/MixSider/index.vue b/src/layouts/common/MixSider/index.vue index c1ac51b1..ce04f6cb 100644 --- a/src/layouts/common/MixSider/index.vue +++ b/src/layouts/common/MixSider/index.vue @@ -69,7 +69,7 @@ const activeParentRouteName = ref(getActiveRouteName()); function getActiveRouteName() { let name = ''; - const menuMatched = route.matched.filter(item => !item.meta.isNotMenu); + const menuMatched = route.matched.filter(item => !item.meta?.notAsMenu); if (menuMatched.length) { name = menuMatched[0].name as string; } diff --git a/src/layouts/common/VerticalMixSider/index.vue b/src/layouts/common/VerticalMixSider/index.vue index c1ac51b1..ce04f6cb 100644 --- a/src/layouts/common/VerticalMixSider/index.vue +++ b/src/layouts/common/VerticalMixSider/index.vue @@ -69,7 +69,7 @@ const activeParentRouteName = ref(getActiveRouteName()); function getActiveRouteName() { let name = ''; - const menuMatched = route.matched.filter(item => !item.meta.isNotMenu); + const menuMatched = route.matched.filter(item => !item.meta?.notAsMenu); if (menuMatched.length) { name = menuMatched[0].name as string; } diff --git a/src/layouts/index.ts b/src/layouts/index.ts index 42c5dca9..90c3e369 100644 --- a/src/layouts/index.ts +++ b/src/layouts/index.ts @@ -1,5 +1,4 @@ import BasicLayout from './BasicLayout/index.vue'; import BlankLayout from './BlankLayout/index.vue'; -import RouterViewLayout from './RouterViewLayout/index.vue'; -export { BasicLayout, BlankLayout, RouterViewLayout }; +export { BasicLayout, BlankLayout }; diff --git a/src/router/modules/about.ts b/src/router/modules/about.ts index ceb48ad0..05224220 100644 --- a/src/router/modules/about.ts +++ b/src/router/modules/about.ts @@ -1,4 +1,4 @@ -import type { CustomRoute } from '@/interface'; +import type { RouteRecordRaw } from 'vue-router'; import { setSingleRoute } from '@/utils'; import { BasicLayout } from '@/layouts'; import About from '@/views/about/index.vue'; @@ -6,19 +6,21 @@ import { getRouteConst, routeName } from '../constant'; const { name, path, title } = getRouteConst('about'); -const ABOUT: CustomRoute = setSingleRoute({ +const ABOUT: RouteRecordRaw = setSingleRoute({ route: { name, path, component: About, meta: { - requiresAuth: true, title, + requiresAuth: true, + keepAlive: true, icon: 'fluent:book-information-24-regular' } }, container: BasicLayout, - meta: { + containerMeta: { + title, order: 7 }, notFoundName: routeName('not-found') diff --git a/src/router/modules/component.ts b/src/router/modules/component.ts index cda3eef5..59cd4250 100644 --- a/src/router/modules/component.ts +++ b/src/router/modules/component.ts @@ -1,5 +1,5 @@ -import type { CustomRoute } from '@/interface'; -import { BasicLayout, RouterViewLayout } from '@/layouts'; +import type { RouteRecordRaw } from 'vue-router'; +import { BasicLayout } from '@/layouts'; import ComponentMap from '@/views/component/map/index.vue'; import ComponentVideo from '@/views/component/video/index.vue'; import EditorQuill from '@/views/component/editor/quill/index.vue'; @@ -7,7 +7,7 @@ import EditorMarkdown from '@/views/component/editor/markdown/index.vue'; import ComponentSwiper from '@/views/component/swiper/index.vue'; import { routeName, routePath, routeTitle } from '../constant'; -const COMPONENT: CustomRoute = { +const COMPONENT: RouteRecordRaw = { name: routeName('component'), path: routePath('component'), component: BasicLayout, @@ -42,12 +42,9 @@ const COMPONENT: CustomRoute = { { name: routeName('component_editor'), path: routePath('component_editor'), - component: RouterViewLayout, redirect: { name: routeName('component_editor_quill') }, meta: { - requiresAuth: true, - title: routeTitle('component_editor'), - fullPage: true + title: routeTitle('component_editor') }, children: [ { diff --git a/src/router/modules/dashboard.ts b/src/router/modules/dashboard.ts index 4f537643..c04593f4 100644 --- a/src/router/modules/dashboard.ts +++ b/src/router/modules/dashboard.ts @@ -1,10 +1,10 @@ -import type { CustomRoute } from '@/interface'; +import type { RouteRecordRaw } from 'vue-router'; import { BasicLayout } from '@/layouts'; import DashboardAnalysis from '@/views/dashboard/analysis/index.vue'; import DashboardWorkbench from '@/views/dashboard/workbench/index.vue'; import { routeName, routePath, routeTitle } from '../constant'; -const DASHBOARD: CustomRoute = { +const DASHBOARD: RouteRecordRaw = { name: routeName('dashboard'), path: routePath('dashboard'), component: BasicLayout, diff --git a/src/router/modules/document.ts b/src/router/modules/document.ts index 4c652eb1..3439acce 100644 --- a/src/router/modules/document.ts +++ b/src/router/modules/document.ts @@ -1,11 +1,11 @@ -import type { CustomRoute } from '@/interface'; +import type { RouteRecordRaw } from 'vue-router'; import { BasicLayout } from '@/layouts'; import DocumentVue from '@/views/document/vue/index.vue'; import DocumentVite from '@/views/document/vite/index.vue'; import DocumentNaive from '@/views/document/naive/index.vue'; import { routeName, routePath, routeTitle } from '../constant'; -const DOCUMENT: CustomRoute = { +const DOCUMENT: RouteRecordRaw = { name: routeName('document'), path: routePath('document'), component: BasicLayout, diff --git a/src/router/modules/exception.ts b/src/router/modules/exception.ts index 2574e228..9ce3c9cb 100644 --- a/src/router/modules/exception.ts +++ b/src/router/modules/exception.ts @@ -1,11 +1,11 @@ -import type { CustomRoute } from '@/interface'; +import type { RouteRecordRaw } from 'vue-router'; import { BasicLayout } from '@/layouts'; import Exception403 from '@/views/system/exception/403.vue'; import Exception404 from '@/views/system/exception/404.vue'; import Exception500 from '@/views/system/exception/500.vue'; import { routeName, routePath, routeTitle } from '../constant'; -const EXCEPTION: CustomRoute = { +const EXCEPTION: RouteRecordRaw = { name: routeName('exception'), path: routePath('exception'), component: BasicLayout, diff --git a/src/router/modules/feat.ts b/src/router/modules/feat.ts index 3b5fc836..0ce2d4bd 100644 --- a/src/router/modules/feat.ts +++ b/src/router/modules/feat.ts @@ -1,11 +1,11 @@ -import type { CustomRoute } from '@/interface'; +import type { RouteRecordRaw } from 'vue-router'; import { BasicLayout } from '@/layouts'; import FeatCopy from '@/views/feat/copy/index.vue'; import FeatIcon from '@/views/feat/icon/index.vue'; import FeatPrint from '@/views/feat/print/index.vue'; import { routeName, routePath, routeTitle } from '../constant'; -const FEAT: CustomRoute = { +const FEAT: RouteRecordRaw = { name: routeName('feat'), path: routePath('feat'), component: BasicLayout, diff --git a/src/router/modules/multi-menu.ts b/src/router/modules/multi-menu.ts index 86f6dfb6..679d7c33 100644 --- a/src/router/modules/multi-menu.ts +++ b/src/router/modules/multi-menu.ts @@ -1,9 +1,9 @@ -import type { CustomRoute } from '@/interface'; -import { BasicLayout, RouterViewLayout } from '@/layouts'; +import type { RouteRecordRaw } from 'vue-router'; +import { BasicLayout } from '@/layouts'; import MultiMenuFirstSecond from '@/views/multi-menu/first/second/index.vue'; import { routeName, routePath, routeTitle } from '../constant'; -const MULTI_MENU: CustomRoute = { +const MULTI_MENU: RouteRecordRaw = { name: routeName('multi-menu'), path: routePath('multi-menu'), component: BasicLayout, @@ -17,12 +17,9 @@ const MULTI_MENU: CustomRoute = { { name: routeName('multi-menu_first'), path: routePath('multi-menu_first'), - component: RouterViewLayout, redirect: { name: routeName('multi-menu_first_second') }, meta: { - keepAlive: true, - requiresAuth: true, - title: routeTitle('multi-menu_first_second') + title: routeTitle('multi-menu_first') }, children: [ { @@ -30,9 +27,9 @@ const MULTI_MENU: CustomRoute = { path: routePath('multi-menu_first_second'), component: MultiMenuFirstSecond, meta: { - keepAlive: true, - requiresAuth: true, title: routeTitle('multi-menu_first_second'), + requiresAuth: true, + keepAlive: true, fullPage: true } } diff --git a/src/router/modules/website.ts b/src/router/modules/website.ts index 9ae9ad13..67a66211 100644 --- a/src/router/modules/website.ts +++ b/src/router/modules/website.ts @@ -1,4 +1,4 @@ -import type { CustomRoute } from '@/interface'; +import type { RouteRecordRaw } from 'vue-router'; import { setSingleRoute } from '@/utils'; import { BlankLayout } from '@/layouts'; import Website from '@/views/website/index.vue'; @@ -6,7 +6,7 @@ import { getRouteConst, routeName } from '../constant'; const { name, path, title } = getRouteConst('website'); -const WEBSITE: CustomRoute = setSingleRoute({ +const WEBSITE: RouteRecordRaw = setSingleRoute({ route: { name, path, @@ -14,11 +14,12 @@ const WEBSITE: CustomRoute = setSingleRoute({ meta: { title, icon: 'codicon:remote-explorer', - isNotMenu: true + notAsMenu: true } }, container: BlankLayout, - meta: { + containerMeta: { + title, order: 8 }, notFoundName: routeName('not-found') diff --git a/src/router/routes/constant-routes.ts b/src/router/routes/constant-routes.ts index 6df201e9..0a3bf98c 100644 --- a/src/router/routes/constant-routes.ts +++ b/src/router/routes/constant-routes.ts @@ -19,12 +19,13 @@ const constantRoutes: RouteRecordRaw[] = [ redirect: { name: ROUTE_HOME_NAME } }, { - // 名称、路由随意,不在路由声明里面,只是为各个页面充当传递BlankLayout的桥梁,因此访问该路由时重定向到404 + // 名称、路径随意,不在路由声明里面,只是为各个子路由充当应用BlankLayout布局的桥梁,因此访问该路由时重定向到404 name: 'constant-single_', path: '/constant-single_', component: BlankLayout, redirect: { name: routeName('not-found') }, meta: { + title: 'constant-single_', keepAlive: true }, children: [ diff --git a/src/router/routes/index.ts b/src/router/routes/index.ts index c9bf8f63..89423316 100644 --- a/src/router/routes/index.ts +++ b/src/router/routes/index.ts @@ -1,8 +1,11 @@ import type { RouteRecordRaw } from 'vue-router'; +import { transformMultiDegreeRoutes } from '@/utils'; import customRoutes from '../modules'; import constantRoutes from './constant-routes'; +const transformRoutes = transformMultiDegreeRoutes(customRoutes); + /** 所有路由 */ -export const routes: RouteRecordRaw[] = [...customRoutes, ...constantRoutes]; +export const routes: RouteRecordRaw[] = [...transformRoutes, ...constantRoutes]; export { ROUTE_HOME } from './route-home'; diff --git a/src/typings/router.d.ts b/src/typings/router.d.ts new file mode 100644 index 00000000..a01f4389 --- /dev/null +++ b/src/typings/router.d.ts @@ -0,0 +1,22 @@ +import 'vue-router'; + +declare module 'vue-router' { + interface RouteMeta { + /** 路由名称(作为菜单时为菜单的名称) */ + title: string; + /** 需要登录权限 */ + requiresAuth?: boolean; + /** 缓存页面 */ + keepAlive?: boolean; + /** 页面占满剩余高度(去除头部、tab和底部后的高度) */ + fullPage?: boolean; + /** 不作为菜单 */ + notAsMenu?: boolean; + /** 菜单和面包屑对应的图标 */ + icon?: string; + /** 导入的路由模块排序,可用于菜单的排序 */ + order?: number; + /** y方向滚动的距离(被缓存的页面保留滚动行为) */ + scrollY?: number; + } +} diff --git a/src/utils/router/cache.ts b/src/utils/router/cache.ts index e384018d..b98db4be 100644 --- a/src/utils/router/cache.ts +++ b/src/utils/router/cache.ts @@ -1,6 +1,13 @@ import type { Component } from 'vue'; import type { RouteRecordRaw } from 'vue-router'; +/** 给需要缓存的页面组件设置名称 */ +function setComponentName(component?: Component, name?: string) { + if (component && name) { + Object.assign(component, { name }); + } +} + function getCacheName(route: RouteRecordRaw, isCache: boolean) { const cacheNames: string[] = []; const hasChild = hasChildren(route); @@ -26,13 +33,6 @@ function hasChildren(route: RouteRecordRaw) { return Boolean(route.children && route.children.length); } -/** 给需要缓存的页面组件设置名称 */ -export function setComponentName(component?: Component, name?: string) { - if (component && name) { - Object.assign(component, { name }); - } -} - /** 获取被缓存的路由 */ export function getCacheRoutes(routes: RouteRecordRaw[]) { const cacheNames: string[] = []; diff --git a/src/utils/router/helpers.ts b/src/utils/router/helpers.ts index aa0338dc..7e73a8f5 100644 --- a/src/utils/router/helpers.ts +++ b/src/utils/router/helpers.ts @@ -1,13 +1,45 @@ import type { Component } from 'vue'; -import type { Router } from 'vue-router'; -import type { CustomRoute, ImportedRouteModules, CustomRouteMeta } from '@/interface'; +import type { Router, RouteRecordRaw, RouteMeta } from 'vue-router'; +import type { ImportedRouteModules } from '@/interface'; + +interface SingleRouteConfig { + /** 路由 */ + route: RouteRecordRaw; + /** 路由容器 */ + container: Component; + /** 路由容器的描述 */ + containerMeta: RouteMeta; + /** 404路由的名称 */ + notFoundName: string; +} + +/** 设置单个路由 */ +export function setSingleRoute(config: SingleRouteConfig) { + const { route, container, containerMeta, notFoundName } = config; + const routeItem: RouteRecordRaw = { + name: `${route.name as string}_`, + path: `${route.path}_`, + component: container, + redirect: { name: notFoundName }, + meta: { + notAsMenu: true, + ...containerMeta, + title: `${containerMeta.title}-container` + }, + children: [route] + }; + + return routeItem; +} /** 处理导入的路由模块 */ export function transformRouteModules(routeModules: ImportedRouteModules) { const modules = Object.keys(routeModules).map(key => { return routeModules[key].default; }); - const constantRoutes: CustomRoute[] = modules.sort((next, pre) => Number(next.meta.order) - Number(pre.meta.order)); + const constantRoutes: RouteRecordRaw[] = modules.sort( + (next, pre) => Number(next.meta?.order) - Number(pre.meta?.order) + ); return constantRoutes; } @@ -16,12 +48,12 @@ export function transformRouteModules(routeModules: ImportedRouteModules) { * @param routes - 路由 * @param routeHomeName - 路由首页名称 */ -export function getRouteHome(routes: CustomRoute[], routeHomeName: string) { - let routeHome: CustomRoute; - function hasChildren(route: CustomRoute) { +export function getRouteHome(routes: RouteRecordRaw[], routeHomeName: string) { + let routeHome: RouteRecordRaw; + function hasChildren(route: RouteRecordRaw) { return Boolean(route.children && route.children.length); } - function getRouteHomeByRoute(route: CustomRoute) { + function getRouteHomeByRoute(route: RouteRecordRaw) { if (routeHome) return; const hasChild = hasChildren(route); if (!hasChild) { @@ -29,12 +61,12 @@ export function getRouteHome(routes: CustomRoute[], routeHomeName: string) { routeHome = route; } } else { - getRouteHomeByRoutes(route.children as CustomRoute[]); + getRouteHomeByRoutes(route.children as RouteRecordRaw[]); } } - function getRouteHomeByRoutes(_routes: CustomRoute[]) { + function getRouteHomeByRoutes(_routes: RouteRecordRaw[]) { _routes.some(item => { - getRouteHomeByRoute(item as CustomRoute); + getRouteHomeByRoute(item as RouteRecordRaw); return routeHome !== undefined; }); } @@ -42,37 +74,36 @@ export function getRouteHome(routes: CustomRoute[], routeHomeName: string) { return routeHome!; } +/** + * 将多层级路由转换成二级路由 + * @param routes - 路由 + */ +export function transformMultiDegreeRoutes(routes: RouteRecordRaw[]) { + function hasComponent(route: RouteRecordRaw) { + return Boolean(route.component); + } + function hasChildren(route: RouteRecordRaw) { + return Boolean(route.children && route.children.length); + } + + function upDimension(route: RouteRecordRaw): RouteRecordRaw[] { + if (hasChildren(route)) { + const updateRoute = { ...route }; + if (!hasComponent(route)) { + return updateRoute.children!; + } + updateRoute.children = updateRoute.children?.map(item => upDimension(item)).flat(); + return [updateRoute]; + } + return [route]; + } + + return routes.map(item => upDimension(item)).flat(); +} + /** 获取登录后的重定向地址 */ export function getLoginRedirectUrl(router: Router) { const path = router.currentRoute.value.fullPath as string; const redirectUrl = path === '/' ? undefined : path; return redirectUrl; } - -interface SingleRouteConfig { - /** 路由 */ - route: CustomRoute; - /** 路由容器 */ - container: Component; - /** 路由容器的描述 */ - meta: CustomRouteMeta; - /** 404路由的名称 */ - notFoundName: string; -} -/** * 设置单个路由 */ -export function setSingleRoute(config: SingleRouteConfig) { - const { route, container, meta, notFoundName } = config; - const routeItem: CustomRoute = { - name: `${route.name as string}_`, - path: `${route.path}_`, - component: container, - redirect: { name: notFoundName }, - meta: { - ...meta, - isNotMenu: true - }, - children: [route] - }; - - return routeItem; -} diff --git a/src/utils/router/menus.ts b/src/utils/router/menus.ts index cedd3b0d..1cd260ba 100644 --- a/src/utils/router/menus.ts +++ b/src/utils/router/menus.ts @@ -1,9 +1,10 @@ -import type { CustomRoute, GlobalMenuOption } from '@/interface'; +import type { RouteRecordRaw } from 'vue-router'; +import type { GlobalMenuOption } from '@/interface'; import { iconifyRender } from '../common'; /** 判断路由是否作为菜单 */ -function asMenu(route: CustomRoute) { - return !route.meta?.isNotMenu; +function asMenu(route: RouteRecordRaw) { + return !route.meta?.notAsMenu; } /** 给菜单添加可选属性 */ @@ -19,14 +20,14 @@ function addPartialProps(menuItem: GlobalMenuOption, icon?: string, children?: G } /** 将路由转换成菜单 */ -export function transformRouteToMenu(routes: CustomRoute[]) { +export function transformRouteToMenu(routes: RouteRecordRaw[]) { const globalMenu: GlobalMenuOption[] = []; routes.forEach(route => { const { name, path, meta } = route; const routeName = name as string; let menuChildren: GlobalMenuOption[] | undefined; if (route.children) { - menuChildren = transformRouteToMenu(route.children as CustomRoute[]); + menuChildren = transformRouteToMenu(route.children as RouteRecordRaw[]); } const menuItem: GlobalMenuOption = addPartialProps( {