基于AntDesignPro实现多Tab页
实现步骤
基本布局
umi会默认读取/src/layouts/index.tsx
作为全局layout,因此我们新建一个/src/layouts/index.tsx
文件
const KeepAliveLayout = () => {
return (
<div>KeepAliveLayout</div>
)
}
export default KeepAliveLayout;
效果:无论怎么切换路由都会显示这个文本
引入Tab组件
import { Tabs } from 'antd';
const KeepAliveLayout = () => {
return (
<Tabs
items={[{
key: 'tab1',
label: 'tab1',
children: (
<div>tab1</div>
)
}, {
key: 'tab2',
label: 'tab2',
children: (
<div>tab2</div>
)
}]}
/>
)
}
export default KeepAliveLayout;
效果:
封装Hooks
封装一个hooks,获取当前匹配到的路由信息,以及组件实例,
-
通过umi内置的
useSelectedRoutes
这个api,获取所有匹配到的路由。 -
通过
useOutlet
获取匹配到的路由组件实例 -
通过
useLocation
获取当前url,
layouts/useMatchRoute.tsx
:
// /src/layouts/useMatchRoute.tsx代码
import {history, IRoute, useAppData, useLocation, useOutlet, useSelectedRoutes} from '@umijs/max';
import {useEffect, useState} from 'react';
type CustomIRoute = IRoute & {
name: string;
}
interface MatchRouteType {
title: string;
pathname: string; // /user/1
children: any;
routePath: string; // /user/:id
icon?: any;
}
export function useMatchRoute() {
// 获取匹配到的路由
const selectedRoutes = useSelectedRoutes();
// 获取路由组件实例
const children = useOutlet();
// 获取所有路由
const {routes} = useAppData();
// 获取当前url
const {pathname} = useLocation();
const [matchRoute, setMatchRoute] = useState<MatchRouteType | undefined>();
// 处理菜单名称
const getMenuTitle = (lastRoute: any) => {
return lastRoute.route.name;
}
// 监听pathname变了,说明路由有变化,重新匹配,返回新路由信息
useEffect(() => {
// 获取当前匹配的路由
const lastRoute = selectedRoutes.at(-1);
if (!lastRoute?.route?.path) return;
const routeDetail = routes[(lastRoute.route as any).id];
// 如果匹配的路由需要重定向,这里直接重定向
if (routeDetail?.redirect) {
history.replace(routeDetail?.redirect);
return;
}
// 获取菜单名称
const title = getMenuTitle(lastRoute);
setMatchRoute({
title,
pathname,
children,
routePath: lastRoute.route.path,
icon: (lastRoute.route as any).icon, // icon是拓展出来的字段
});
}, [pathname])
return matchRoute;
}
layouts/index.tsx
:
// /src/layouts/index.tsx
import { Tabs } from 'antd';
import { useMatchRoute } from './useMatchRoute';
const KeepAliveLayout = () => {
const matchRoute = useMatchRoute();
return (
<Tabs
items={[{
key: matchRoute?.pathname || '',
label: matchRoute?.title,
children: matchRoute?.children,
}]}
/>
)
}
export default KeepAliveLayout;
效果
保存路由数组
接下来,把路由信息保存到数据中:
useKeepAliveTabs.tsx
// /src/layouts/useKeepAliveTabs.tsx
import { useEffect, useState } from 'react';
import { useMatchRoute } from './useMatchRoute';
export interface KeepAliveTab {
title: string;
routePath: string;
key: string; // 这个key,后面刷新有用到它
pathname: string;
icon?: any;
children: any;
}
function getKey() {
return new Date().getTime().toString();
}
export function useKeepAliveTabs() {
const [keepAliveTabs, setKeepAliveTabs] = useState<KeepAliveTab[]>([]);
const [activeTabRoutePath, setActiveTabRoutePath] = useState<string>('');
const matchRoute = useMatchRoute();
useEffect(() => {
if (!matchRoute) return;
const existKeepAliveTab = keepAliveTabs.find(o => o.routePath === matchRoute?.routePath);
// 如果不存在则需要插入
if (!existKeepAliveTab) {
setKeepAliveTabs(prev => [...prev, {
title: matchRoute.title,
key: getKey(),
routePath: matchRoute.routePath,
pathname: matchRoute.pathname,
children: matchRoute.children,
icon: matchRoute.icon,
}]);
}
setActiveTabRoutePath(matchRoute.routePath);
}, [matchRoute])
return {
keepAliveTabs,
activeTabRoutePath,
}
}
index.tsx
:
// /src/layouts/index.tsx
import { Tabs } from 'antd';
import { useCallback, useMemo } from 'react';
import { history } from '@umijs/max';
import { useKeepAliveTabs } from './useKeepAliveTabs';
const KeepAliveLayout = () => {
const { keepAliveTabs, activeTabRoutePath } = useKeepAliveTabs();
const tabItems = useMemo(() => {
return keepAliveTabs.map(tab => {
return {
key: tab.routePath,
label: (
<span>
{tab.icon}
{tab.title}
</span>
),
children: (
<div
key={tab.key}
style={{ height: 'calc(100vh - 112px)', overflow: 'auto' }}
>
{tab.children}
</div>
),
closable: false,
}
})
}, [keepAliveTabs]);
const onTabsChange = useCallback((tabRoutePath: string) => {
history.push(tabRoutePath);
}, [])
return (
<Tabs
type="editable-card"
items={tabItems}
activeKey={activeTabRoutePath}
onChange={onTabsChange}
className='keep-alive-tabs'
hideAdd
/>
)
}
export default KeepAliveLayout;
修改/src/global.less
样式,添加:
.keep-alive-tabs {
.ant-tabs-nav {
margin: 0;
}
}
:where(.css-dev-only-do-not-override-1e5rcno).ant-pro .ant-pro-layout .ant-pro-layout-content {
padding: 0;
}
效果,此时已实现切换效果
实现刷新关闭
在src/layouts/useKeepAliveTabs.tsx
文件添加代码
新增的内容是
closeTab
,refreshTab
,closeOtherTab
,
// /src/layouts/useKeepAliveTabs.tsx
import {useCallback, useEffect, useState} from 'react';
import {useMatchRoute} from './useMatchRoute';
import {history} from "@umijs/max";
export interface KeepAliveTab {
title: string;
routePath: string;
key: string; // 这个key,后面刷新有用到它
pathname: string;
icon?: any;
children: any;
}
function getKey() {
return new Date().getTime().toString();
}
export function useKeepAliveTabs() {
const [keepAliveTabs, setKeepAliveTabs] = useState<KeepAliveTab[]>([]);
const [activeTabRoutePath, setActiveTabRoutePath] = useState<string>('');
const matchRoute = useMatchRoute();
useEffect(() => {
if (!matchRoute) return;
const existKeepAliveTab = keepAliveTabs.find(o => o.routePath === matchRoute?.routePath);
// 如果不存在则需要插入
if (!existKeepAliveTab) {
setKeepAliveTabs(prev => [...prev, {
title: matchRoute.title,
key: getKey(),
routePath: matchRoute.routePath,
pathname: matchRoute.pathname,
children: matchRoute.children,
icon: matchRoute.icon,
}]);
}
setActiveTabRoutePath(matchRoute.routePath);
}, [matchRoute])
// 关闭tab
const closeTab = useCallback(
(routePath: string = activeTabRoutePath) => {
const index = keepAliveTabs.findIndex(o => o.routePath === routePath);
if (keepAliveTabs[index].routePath === activeTabRoutePath) {
if (index > 0) {
history.push(keepAliveTabs[index - 1].routePath);
} else {
history.push(keepAliveTabs[index + 1].routePath);
}
}
keepAliveTabs.splice(index, 1);
setKeepAliveTabs([...keepAliveTabs]);
},
[activeTabRoutePath],
);
// 关闭其他
const closeOtherTab = useCallback((routePath: string = activeTabRoutePath) => {
setKeepAliveTabs(prev => prev.filter(o => o.routePath === routePath));
}, [activeTabRoutePath]);
// 刷新tab
const refreshTab = useCallback((routePath: string = activeTabRoutePath) => {
setKeepAliveTabs(prev => {
const index = prev.findIndex(tab => tab.routePath === routePath);
if (index >= 0) {
// 这个是react的特性,key变了,组件会卸载重新渲染
prev[index].key = getKey();
}
return [...prev];
});
}, [activeTabRoutePath]);
return {
keepAliveTabs,
activeTabRoutePath,
closeTab,
refreshTab,
closeOtherTab,
}
}