diff --git a/mock/model/route.ts b/mock/model/route.ts index bd911cf26..533025f6b 100644 --- a/mock/model/route.ts +++ b/mock/model/route.ts @@ -302,6 +302,53 @@ export const routeModel: Record = { order: 5 } }, + { + name: 'function', + path: '/function', + component: 'basic', + children: [ + { + name: 'function_tab', + path: '/function/tab', + component: 'self', + meta: { + title: 'Tab', + requiresAuth: true, + icon: 'ic:round-tab' + } + }, + { + name: 'function_tab-detail', + path: '/function/tab-detail', + component: 'self', + meta: { + title: 'Tab Detail', + requiresAuth: true, + hide: true, + activeMenu: 'function_tab', + icon: 'ic:round-tab' + } + }, + { + name: 'function_tab-multi-detail', + path: '/function/tab-multi-detail', + component: 'self', + meta: { + title: 'Tab Multi Detail', + requiresAuth: true, + hide: true, + multiTab: true, + activeMenu: 'function_tab', + icon: 'ic:round-tab' + } + } + ], + meta: { + title: '功能', + icon: 'ri:function-line', + order: 6 + } + }, { name: 'exception', path: '/exception', @@ -341,7 +388,7 @@ export const routeModel: Record = { meta: { title: '异常页', icon: 'ant-design:exception-outlined', - order: 6 + order: 7 } }, { @@ -395,7 +442,7 @@ export const routeModel: Record = { meta: { title: '多级菜单', icon: 'carbon:menu', - order: 7 + order: 8 } }, { @@ -407,7 +454,7 @@ export const routeModel: Record = { requiresAuth: true, singleLayout: 'basic', icon: 'fluent:book-information-24-regular', - order: 8 + order: 9 } } ], @@ -704,6 +751,53 @@ export const routeModel: Record = { order: 5 } }, + { + name: 'function', + path: '/function', + component: 'basic', + children: [ + { + name: 'function_tab', + path: '/function/tab', + component: 'self', + meta: { + title: 'Tab', + requiresAuth: true, + icon: 'ic:round-tab' + } + }, + { + name: 'function_tab-detail', + path: '/function/tab-detail', + component: 'self', + meta: { + title: 'Tab Detail', + requiresAuth: true, + hide: true, + activeMenu: 'function_tab', + icon: 'ic:round-tab' + } + }, + { + name: 'function_tab-multi-detail', + path: '/function/tab-multi-detail', + component: 'self', + meta: { + title: 'Tab Multi Detail', + requiresAuth: true, + hide: true, + multiTab: true, + activeMenu: 'function_tab', + icon: 'ic:round-tab' + } + } + ], + meta: { + title: '功能', + icon: 'ri:function-line', + order: 6 + } + }, { name: 'exception', path: '/exception', @@ -743,7 +837,7 @@ export const routeModel: Record = { meta: { title: '异常页', icon: 'ant-design:exception-outlined', - order: 6 + order: 7 } }, { @@ -797,7 +891,7 @@ export const routeModel: Record = { meta: { title: '多级菜单', icon: 'carbon:menu', - order: 7 + order: 8 } }, { @@ -809,7 +903,7 @@ export const routeModel: Record = { requiresAuth: true, singleLayout: 'basic', icon: 'fluent:book-information-24-regular', - order: 8 + order: 9 } } ], diff --git a/src/enum/common.ts b/src/enum/common.ts index 1ecb86664..49b3f7326 100644 --- a/src/enum/common.ts +++ b/src/enum/common.ts @@ -16,7 +16,7 @@ export enum EnumStorageKey { /** 用户信息 */ 'user-info' = '__USER_INFO__', /** 多页签路由信息 */ - 'tab-routes' = '__TAB_ROUTES__' + 'multi-tab-routes' = '__MULTI_TAB_ROUTES__' } /** 数据类型 */ diff --git a/src/layouts/common/GlobalContent/index.vue b/src/layouts/common/GlobalContent/index.vue index d98bdce30..e7b990161 100644 --- a/src/layouts/common/GlobalContent/index.vue +++ b/src/layouts/common/GlobalContent/index.vue @@ -12,7 +12,7 @@ @after-enter="handleAfterEnter" > - + diff --git a/src/layouts/common/GlobalTab/components/TabDetail/components/ContextMenu.vue b/src/layouts/common/GlobalTab/components/TabDetail/components/ContextMenu.vue index 45169555d..fed6ddc6a 100644 --- a/src/layouts/common/GlobalTab/components/TabDetail/components/ContextMenu.vue +++ b/src/layouts/common/GlobalTab/components/TabDetail/components/ContextMenu.vue @@ -69,7 +69,7 @@ const options = computed(() => [ { label: '关闭', key: 'close-current', - disabled: props.currentPath === tab.homeTab.path, + disabled: props.currentPath === tab.homeTab.fullPath, icon: iconifyRender('ant-design:close-outlined') }, { diff --git a/src/layouts/common/GlobalTab/components/TabDetail/index.vue b/src/layouts/common/GlobalTab/components/TabDetail/index.vue index c00316671..670924397 100644 --- a/src/layouts/common/GlobalTab/components/TabDetail/index.vue +++ b/src/layouts/common/GlobalTab/components/TabDetail/index.vue @@ -3,15 +3,15 @@ {{ item.meta.title }} @@ -77,11 +77,11 @@ function setDropdown(x: number, y: number, currentPath: string) { } /** 点击右键菜单 */ -async function handleContextMenu(e: MouseEvent, path: string) { +async function handleContextMenu(e: MouseEvent, fullPath: string) { e.preventDefault(); const { clientX, clientY } = e; hideDropdown(); - setDropdown(clientX, clientY, path); + setDropdown(clientX, clientY, fullPath); await nextTick(); showDropdown(); } diff --git a/src/layouts/common/GlobalTab/index.vue b/src/layouts/common/GlobalTab/index.vue index c615bbb3d..8dbf74492 100644 --- a/src/layouts/common/GlobalTab/index.vue +++ b/src/layouts/common/GlobalTab/index.vue @@ -45,10 +45,10 @@ function init() { } watch( - () => route.path, + () => route.fullPath, () => { tab.addTab(route); - tab.setActiveTab(route.path); + tab.setActiveTab(route.fullPath); } ); diff --git a/src/router/guard/dynamic.ts b/src/router/guard/dynamic.ts index cecdbd391..a71ebdca3 100644 --- a/src/router/guard/dynamic.ts +++ b/src/router/guard/dynamic.ts @@ -31,15 +31,10 @@ export async function createDynamicRouteGuard( if (to.name === routeName('not-found-page')) { // 动态路由没有加载导致被not-found-page路由捕获,等待权限路由加载好了,回到之前的路由 - // 若路由是从根路由重定向过来的,重新回到根路由 const ROOT_ROUTE_NAME: AuthRoute.RouteKey = 'root'; - if (to.redirectedFrom?.name === ROOT_ROUTE_NAME) { - next({ path: '/', replace: true, query: to.query }); - return false; - } - - next({ path: to.fullPath, replace: true, query: to.query }); + const path = to.redirectedFrom?.name === ROOT_ROUTE_NAME ? '/' : to.fullPath; + next({ path, replace: true, query: to.query, hash: to.hash }); return false; } } diff --git a/src/router/helpers/scroll.ts b/src/router/helpers/scroll.ts index 1cf759c2a..f691ce373 100644 --- a/src/router/helpers/scroll.ts +++ b/src/router/helpers/scroll.ts @@ -6,10 +6,13 @@ export const scrollBehavior: RouterScrollBehavior = (to, from) => { const tab = useTabStore(); if (to.hash) { - resolve({ - el: to.hash, - behavior: 'smooth' - }); + const el = document.querySelector(to.hash); + if (el) { + resolve({ + el, + behavior: 'smooth' + }); + } } const { left, top } = tab.getTabScrollPosition(to.path); diff --git a/src/router/modules/about.ts b/src/router/modules/about.ts index 219cd1331..687ad2db1 100644 --- a/src/router/modules/about.ts +++ b/src/router/modules/about.ts @@ -8,7 +8,7 @@ const about: AuthRoute.Route = { singleLayout: 'basic', permissions: ['super', 'admin', 'user'], icon: 'fluent:book-information-24-regular', - order: 8 + order: 9 } }; diff --git a/src/router/modules/exception.ts b/src/router/modules/exception.ts index f7cd56225..b9709937f 100644 --- a/src/router/modules/exception.ts +++ b/src/router/modules/exception.ts @@ -37,7 +37,7 @@ const exception: AuthRoute.Route = { meta: { title: '异常页', icon: 'ant-design:exception-outlined', - order: 6 + order: 7 } }; diff --git a/src/router/modules/function.ts b/src/router/modules/function.ts new file mode 100644 index 000000000..a5bc2c13e --- /dev/null +++ b/src/router/modules/function.ts @@ -0,0 +1,49 @@ +const functionRoute: AuthRoute.Route = { + name: 'function', + path: '/function', + component: 'basic', + children: [ + { + name: 'function_tab', + path: '/function/tab', + component: 'self', + meta: { + title: 'Tab', + requiresAuth: true, + icon: 'ic:round-tab' + } + }, + { + name: 'function_tab-detail', + path: '/function/tab-detail', + component: 'self', + meta: { + title: 'Tab Detail', + requiresAuth: true, + hide: true, + activeMenu: 'function_tab', + icon: 'ic:round-tab' + } + }, + { + name: 'function_tab-multi-detail', + path: '/function/tab-multi-detail', + component: 'self', + meta: { + title: 'Tab Multi Detail', + requiresAuth: true, + hide: true, + multiTab: true, + activeMenu: 'function_tab', + icon: 'ic:round-tab' + } + } + ], + meta: { + title: '功能', + icon: 'ri:function-line', + order: 6 + } +}; + +export default functionRoute; diff --git a/src/router/modules/multi-menu.ts b/src/router/modules/multi-menu.ts index 2a6af800f..d06fec139 100644 --- a/src/router/modules/multi-menu.ts +++ b/src/router/modules/multi-menu.ts @@ -49,7 +49,7 @@ const multiMenu: AuthRoute.Route = { meta: { title: '多级菜单', icon: 'carbon:menu', - order: 6 + order: 8 } }; diff --git a/src/store/modules/tab/helpers.ts b/src/store/modules/tab/helpers.ts index 5d2f8a2f9..1eccf9a92 100644 --- a/src/store/modules/tab/helpers.ts +++ b/src/store/modules/tab/helpers.ts @@ -1,13 +1,15 @@ import type { RouteRecordNormalized, RouteLocationNormalizedLoaded } from 'vue-router'; /** - * 根据vue路由获取tab路由 + * 根据vue路由获取tab路由 * @param route */ export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocationNormalizedLoaded) { + const fullPath = hasFullPath(route) ? route.fullPath : route.path; + const tabRoute: GlobalTabRoute = { name: route.name, - path: route.path, + fullPath, meta: route.meta, scrollPosition: { left: 0, @@ -20,17 +22,36 @@ export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocati /** * 获取该页签在多页签数据中的索引 * @param tabs - 多页签数据 - * @param path - 该页签的路径 + * @param fullPath - 该页签的路径 */ -export function getIndexInTabRoutes(tabs: GlobalTabRoute[], path: string) { - return tabs.findIndex(tab => tab.path === path); +export function getIndexInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) { + return tabs.findIndex(tab => tab.fullPath === fullPath); } /** * 判断该页签是否在多页签数据中 * @param tabs - 多页签数据 - * @param path - 该页签的路径 + * @param fullPath - 该页签的路径 + */ +export function isInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) { + return getIndexInTabRoutes(tabs, fullPath) > -1; +} + +/** + * 根据路由名称获取该页签在多页签数据中的索引 + * @param tabs - 多页签数据 + * @param routeName - 路由名称 + */ +export function getIndexInTabRoutesByRouteName(tabs: GlobalTabRoute[], routeName: string) { + return tabs.findIndex(tab => tab.name === routeName); +} + +/** + * 判断路由是否有fullPath属性 + * @param route 路由 */ -export function isInTabRoutes(tabs: GlobalTabRoute[], path: string) { - return getIndexInTabRoutes(tabs, path) > -1; +function hasFullPath( + route: RouteRecordNormalized | RouteLocationNormalizedLoaded +): route is RouteLocationNormalizedLoaded { + return Boolean((route as RouteLocationNormalizedLoaded).fullPath); } diff --git a/src/store/modules/tab/index.ts b/src/store/modules/tab/index.ts index 327c63154..f590f3de4 100644 --- a/src/store/modules/tab/index.ts +++ b/src/store/modules/tab/index.ts @@ -3,14 +3,14 @@ import { defineStore } from 'pinia'; import { useRouterPush } from '@/composables'; import { getTabRoutes, clearTabRoutes } from '@/utils'; import { useThemeStore } from '../theme'; -import { getTabRouteByVueRoute, isInTabRoutes, getIndexInTabRoutes } from './helpers'; +import { getTabRouteByVueRoute, isInTabRoutes, getIndexInTabRoutes, getIndexInTabRoutesByRouteName } from './helpers'; interface TabState { /** 多页签数据 */ tabs: GlobalTabRoute[]; /** 多页签首页 */ homeTab: GlobalTabRoute; - /** 当前激活状态的页签(路由path) */ + /** 当前激活状态的页签(路由fullPath) */ activeTab: string; } @@ -19,7 +19,7 @@ export const useTabStore = defineStore('tab-store', { tabs: [], homeTab: { name: 'root', - path: '/', + fullPath: '/', meta: { title: 'Root' }, @@ -34,7 +34,7 @@ export const useTabStore = defineStore('tab-store', { /** 当前激活状态的页签索引 */ activeTabIndex(state) { const { tabs, activeTab } = state; - return tabs.findIndex(tab => tab.path === activeTab); + return tabs.findIndex(tab => tab.fullPath === activeTab); } }, actions: { @@ -45,10 +45,10 @@ export const useTabStore = defineStore('tab-store', { }, /** * 设置当前路由对应的页签为激活状态 - * @param path - 路由path + * @param fullPath - 路由fullPath */ - setActiveTab(path: string) { - this.activeTab = path; + setActiveTab(fullPath: string) { + this.activeTab = fullPath; }, /** * 初始化首页页签路由 @@ -68,23 +68,39 @@ export const useTabStore = defineStore('tab-store', { * @param route - 路由 */ addTab(route: RouteLocationNormalizedLoaded) { - if (!isInTabRoutes(this.tabs, route.path)) { - const tab = getTabRouteByVueRoute(route); + const tab = getTabRouteByVueRoute(route); + + if (isInTabRoutes(this.tabs, tab.fullPath)) { + return; + } + + const index = getIndexInTabRoutesByRouteName(this.tabs, route.name as string); + + if (index === -1) { this.tabs.push(tab); + return; } + + const { multiTab = false } = route.meta; + if (!multiTab) { + this.tabs.splice(index, 1, tab); + return; + } + + this.tabs.push(tab); }, /** * 删除多页签 - * @param path - 路由path + * @param fullPath - 路由fullPath */ - removeTab(path: string) { + removeTab(fullPath: string) { const { routerPush } = useRouterPush(false); - const isActive = this.activeTab === path; - const updateTabs = this.tabs.filter(tab => tab.path !== path); + const isActive = this.activeTab === fullPath; + const updateTabs = this.tabs.filter(tab => tab.fullPath !== fullPath); this.tabs = updateTabs; if (isActive && updateTabs.length) { - const activePath = updateTabs[updateTabs.length - 1].path; + const activePath = updateTabs[updateTabs.length - 1].fullPath; this.setActiveTab(activePath); routerPush(activePath); } @@ -96,73 +112,73 @@ export const useTabStore = defineStore('tab-store', { clearTab(excludes: string[] = []) { const { routerPush } = useRouterPush(false); - const homePath = this.homeTab.path; + const homePath = this.homeTab.fullPath; const remain = [homePath, ...excludes]; const hasActive = remain.includes(this.activeTab); - const updateTabs = this.tabs.filter(tab => remain.includes(tab.path)); + const updateTabs = this.tabs.filter(tab => remain.includes(tab.fullPath)); this.tabs = updateTabs; if (!hasActive && updateTabs.length) { - const activePath = updateTabs[updateTabs.length - 1].path; + const activePath = updateTabs[updateTabs.length - 1].fullPath; this.setActiveTab(activePath); routerPush(activePath); } }, /** * 清除左边多页签 - * @param path - 路由path + * @param fullPath - 路由fullPath */ - clearLeftTab(path: string) { - const index = getIndexInTabRoutes(this.tabs, path); + clearLeftTab(fullPath: string) { + const index = getIndexInTabRoutes(this.tabs, fullPath); if (index > -1) { - const excludes = this.tabs.slice(index).map(item => item.path); + const excludes = this.tabs.slice(index).map(item => item.fullPath); this.clearTab(excludes); } }, /** * 清除右边多页签 - * @param path - 路由path + * @param fullPath - 路由fullPath */ - clearRightTab(path: string) { - const index = getIndexInTabRoutes(this.tabs, path); + clearRightTab(fullPath: string) { + const index = getIndexInTabRoutes(this.tabs, fullPath); if (index > -1) { - const excludes = this.tabs.slice(0, index + 1).map(item => item.path); + const excludes = this.tabs.slice(0, index + 1).map(item => item.fullPath); this.clearTab(excludes); } }, /** * 点击单个tab - * @param path - 路由path + * @param fullPath - 路由fullPath */ - handleClickTab(path: string) { + handleClickTab(fullPath: string) { const { routerPush } = useRouterPush(false); - const isActive = this.activeTab === path; + const isActive = this.activeTab === fullPath; if (!isActive) { - this.setActiveTab(path); - routerPush(path); + this.setActiveTab(fullPath); + routerPush(fullPath); } }, /** * 记录tab滚动位置 - * @param path - 路由path + * @param fullPath - 路由fullPath * @param position - tab当前页的滚动位置 */ - recordTabScrollPosition(path: string, position: { left: number; top: number }) { - const index = getIndexInTabRoutes(this.tabs, path); + recordTabScrollPosition(fullPath: string, position: { left: number; top: number }) { + const index = getIndexInTabRoutes(this.tabs, fullPath); if (index > -1) { this.tabs[index].scrollPosition = position; } }, /** * 获取tab滚动位置 - * @param path - 路由path + * @param fullPath - 路由fullPath */ - getTabScrollPosition(path: string) { + getTabScrollPosition(fullPath: string) { const position = { left: 0, top: 0 }; - const index = getIndexInTabRoutes(this.tabs, path); + const index = getIndexInTabRoutes(this.tabs, fullPath); if (index > -1) { Object.assign(position, this.tabs[index].scrollPosition); } @@ -174,20 +190,27 @@ export const useTabStore = defineStore('tab-store', { const tabs: GlobalTabRoute[] = theme.tab.isCache ? getTabRoutes() : []; - const hasHome = isInTabRoutes(tabs, this.homeTab.path); + const hasHome = getIndexInTabRoutesByRouteName(tabs, this.homeTab.name as string) > -1; if (!hasHome && this.homeTab.name !== 'root') { tabs.unshift(this.homeTab); } - const isHome = currentRoute.path === this.homeTab.path; - const hasCurrent = isInTabRoutes(tabs, currentRoute.path); - if (!isHome && !hasCurrent) { + const isHome = currentRoute.fullPath === this.homeTab.fullPath; + const index = getIndexInTabRoutesByRouteName(tabs, currentRoute.name as string); + if (!isHome) { const currentTab = getTabRouteByVueRoute(currentRoute); - tabs.push(currentTab); + if (!currentRoute.meta.multiTab) { + tabs.splice(index, 1, currentTab); + } else { + const hasCurrent = isInTabRoutes(tabs, currentRoute.fullPath); + if (!hasCurrent) { + tabs.push(currentTab); + } + } } this.tabs = tabs; - this.setActiveTab(currentRoute.path); + this.setActiveTab(currentRoute.fullPath); } } }); diff --git a/src/typings/route.d.ts b/src/typings/route.d.ts index 5f4e3b561..b9518181f 100644 --- a/src/typings/route.d.ts +++ b/src/typings/route.d.ts @@ -44,6 +44,10 @@ declare namespace AuthRoute { | 'auth-demo' | 'auth-demo_permission' | 'auth-demo_super' + | 'function' + | 'function_tab' + | 'function_tab-detail' + | 'function_tab-multi-detail' | 'exception' | 'exception_403' | 'exception_404' @@ -94,12 +98,14 @@ declare namespace AuthRoute { hide?: boolean; /** 外链链接 */ href?: string; + /** 是否支持多个tab页签(默认一个,即相同name的路由会被替换) */ + multiTab?: boolean; /** 路由顺序,可用于菜单的排序 */ order?: number; - /** 表示是否是多级路由的中间级路由(用于转换路由数据时筛选多级路由的标识,定义路由时不用填写) */ - multi?: boolean; /** 当前路由需要选中的菜单项(用于跳转至不在左侧菜单显示的路由且需要高亮某个菜单的情况) */ activeMenu?: RouteKey; + /** 表示是否是多级路由的中间级路由(用于转换路由数据时筛选多级路由的标识,定义路由时不用填写) */ + multi?: boolean; }; /** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */ diff --git a/src/typings/system.d.ts b/src/typings/system.d.ts index 4a9677eca..508a3160c 100644 --- a/src/typings/system.d.ts +++ b/src/typings/system.d.ts @@ -276,7 +276,8 @@ type GlobalBreadcrumb = import('naive-ui').DropdownOption & { }; /** 多页签Tab的路由 */ -interface GlobalTabRoute extends Pick { +interface GlobalTabRoute + extends Pick { /** 滚动的位置 */ scrollPosition: { left: number; diff --git a/src/utils/router/tab.ts b/src/utils/router/tab.ts index c6537a310..986cccc59 100644 --- a/src/utils/router/tab.ts +++ b/src/utils/router/tab.ts @@ -3,13 +3,13 @@ import { setLocal, getLocal } from '../storage'; /** 缓存多页签数据 */ export function setTabRoutes(data: GlobalTabRoute[]) { - setLocal(EnumStorageKey['tab-routes'], data); + setLocal(EnumStorageKey['multi-tab-routes'], data); } /** 获取缓存的多页签数据 */ export function getTabRoutes() { const routes: GlobalTabRoute[] = []; - const data = getLocal(EnumStorageKey['tab-routes']); + const data = getLocal(EnumStorageKey['multi-tab-routes']); if (data) { const defaultTabRoutes = data.map(item => ({ ...item, diff --git a/src/views/function/tab-detail/index.vue b/src/views/function/tab-detail/index.vue new file mode 100644 index 000000000..0cf99cdc1 --- /dev/null +++ b/src/views/function/tab-detail/index.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/views/function/tab-multi-detail/index.vue b/src/views/function/tab-multi-detail/index.vue new file mode 100644 index 000000000..c1b82ec1a --- /dev/null +++ b/src/views/function/tab-multi-detail/index.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/views/function/tab/index.vue b/src/views/function/tab/index.vue new file mode 100644 index 000000000..cafdeb656 --- /dev/null +++ b/src/views/function/tab/index.vue @@ -0,0 +1,28 @@ + + + + +