后台管理系统中,多页签功能是一个常见的功能,本文将介绍如何实现一个简单的多页签功能,包括增加、删除、切换。
实现的总体逻辑,我先用一个思维导图来表示:
1. 创建多页签组件
一般后台页面多数有个 layout 的通用布局,所以我们需要在 layout 中引入多页签组件,这样就可以使用多页签功能了。
以react为例,我们可以在layout中引入PageTabs组件即可
// layout/index.jsx
<Content>
{/* 这个是新加的多页签组件,将其逻辑放在PageTabs里 */}
<PageTabs />
{/* 这个是路由对应展示的组件内容 */}
<Outlet />
</Content>
// components/PageTabs/index.jsx
import React, { useEffect, useState } from 'react';
import { Tabs } from 'antd';
type ITabItem = {
label: string;
key: string;
closable: boolean;
}
const PageTabs: React.FC = () => {
const [activeKey, setActiveKey] = useState<string>();
const [tabItems, setTabItems] = useState<ITabItem[]>([
// mock下tabs
{
label: '首页',
key: 'home',
closable: false,
},
{
label: '关于',
key: 'about',
closable: true,
},
]);
return (
<Tabs
{/* 可编辑 */}
type="editable-card"
activeKey={activeKey}
{/* 隐藏加号 */}
hideAdd
{/* items就是tab的配置 */}
items={tabItems}
/>
);
}
PageTabs.displayName = "PageTabs"
export default PageTabs
2. 实现增加、删除、切换功能
Tabs最核心的两个数据是tabItems和activeKey,tabItems是一个数组,里面存放了每个页签的配置信息,activeKey是当前激活的页签的key,交互事件主要处理这两个数据,下面我们来实现增加、删除、切换三个简单的交互功能。
每个 tab 的配置信息包括label、key、closable。
这里tab的key是路由的pathname,activeKey是当前激活的tab的key,当其发生变化的时候,就会切换到对应的tab,且触发navigate跳转到对应的路由。
2.1 增加功能
路由发生变化的时候,执行该逻辑:
- 设置
activeKey为当前路由的pathname - 当前路由的
pathname如果在items里,则无需处理items - 如果不在
items里,根据pathname去routes配置里找到 label,然后将其 push 到 items,注意当 pathname 是首页的时候,禁止删除
// components/PageTabs/index.jsx
const navigate = useNavigate();
// 当前路由
const { pathname: curPathname } = useLocation();
const [activeKey, setActiveKey] = useState<string>();
const [tabItems, setTabItems] = useState<ITabItem[]>([]);
// 获取路由配置
const { menuItems } = useRouteLoaderData('layout') as any;
const addTabItem = (curPathname: string) => {
// 根据pathname查找对应的路由配置
const route = searchRoute(curPathname, menuItems);
// 如果没有找到,直接返回
if (!route) return;
// 如果找到了,先设置激活的tab
setActiveKey(curPathname);
// 如果已经存在,则不添加
if (tabItems.some((item) => item.key === curPathname)) return;
// 如果不存在,就添加
setTabItems([
...tabItems,
{
label: route.label,
key: curPathname,
// 首页不可关闭
closable: curPathname !== '/dashboard',
},
]);
};
// 每次路由变化,都会执行addTabItem
useEffect(() => {
addTabItem(curPathname);
}, [curPathname]);
2.2 删除功能
点击 tab 的关闭按钮,执行该逻辑:
items里过滤掉要删除的item- 如果删除的
item的key就是activeKey,那么需要重新设置activeKey,跳到下一个页签,没有下一个就跳到上一个页签,如果都没有,就跳到首页 - 反之,不做处理
// components/PageTabs/index.jsx
const removeTabItem = (tabItemKey: string) => {
// 直接将对应的tab从items中过滤掉
const newPanes = tabItems.filter((item) => item.key !== tabItemKey);
setTabItems(newPanes);
// newPanes为空的话,直接跳到首页
if (newPanes.length === 0) {
return navigate('/dashboard');
}
// 然后设置激活的tab
// 如果删除的不是当前激活的tab,那么不需要切换
const isNotActive = activeKey !== tabItemKey;
if (isNotActive) return;
// 如果删除的是当前激活的tab,那么有下一个tab,就切换到下一个tab,否则切换到上一个tab
const index = tabItems.findIndex((item) => item.key === tabItemKey);
const newActiveKey = newPanes[index]?.key || newPanes[index - 1].key;
setActiveKey(newActiveKey);
};
2.3 切换功能
点击 tab,执行该逻辑: 设置activeKey为当前点击的tab的key
const changeTabItem = (tabItemKey: string) => {
setActiveKey(tabItemKey);
};
2.4 完整代码
// components/PageTabs/index.jsx
import React, { useEffect, useState } from 'react';
import { Tabs } from 'antd';
import { useRouteLoaderData, useNavigate } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { searchRoute } from '@/router/searchRoute';
// tabs的逻辑:https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/multi_tab.png
type ITabItem = {
label: string;
key: string;
closable: boolean;
}
const HOME_PATH = '/dashboard'
const PageTabs: React.FC = (props) => {
const navigate = useNavigate();
// 当前路由
const { pathname: curPathname } = useLocation();
const [activeKey, setActiveKey] = useState<string>();
const [tabItems, setTabItems] = useState<ITabItem[]>([]);
// 获取路由配置
const { menuItems } = useRouteLoaderData('layout') as any;
const addTabItem = (curPathname: string) => {
// 根据pathname查找对应的路由配置
const route = searchRoute(curPathname, menuItems)
// 如果没有找到,直接返回
if (!route) return
// 如果找到了,先设置激活的tab
setActiveKey(curPathname);
// 如果已经存在,则不添加
if (tabItems.some(item => item.key === curPathname)) return
// 如果不存在,就添加
setTabItems([
...tabItems,
{
label: route.label,
key: curPathname,
// 首页不可关闭
closable: curPathname !== HOME_PATH,
}]);
}
// 每次路由变化,都会执行addTabItem
useEffect(() => {
addTabItem(curPathname);
}, [curPathname])
// 每次activeKey变化,都会执行navigate
useEffect(() => {
activeKey && navigate(activeKey);
}, [activeKey])
const changeTabItem = (tabItemKey: string) => {
setActiveKey(tabItemKey);
};
const removeTabItem = (tabItemKey: string) => {
// 直接将对应的tab从items中过滤掉
const newPanes = tabItems.filter(item => item.key !== tabItemKey);
setTabItems(newPanes);
// newPanes为空的话,直接跳到首页
if (newPanes.length === 0) {
return navigate(HOME_PATH);
}
// 然后设置激活的tab
// 如果删除的不是当前激活的tab,那么不需要切换
const isNotActive = activeKey !== tabItemKey;
if (isNotActive) return
// 如果删除的是当前激活的tab,那么有下一个tab,就切换到下一个tab,否则切换到上一个tab
const index = tabItems.findIndex(item => item.key === tabItemKey);
const newActiveKey = newPanes[index]?.key || newPanes[index - 1].key;
setActiveKey(newActiveKey);
};
const onEdit = (
tabItemKey: React.MouseEvent | React.KeyboardEvent | string,
action: 'add' | 'remove',
) => {
if (action === 'add') return
removeTabItem(tabItemKey as string);
};
return (
<Tabs
type="editable-card"
onChange={changeTabItem}
activeKey={activeKey}
onEdit={onEdit}
hideAdd
items={tabItems}
{...props}
/>
);
}
PageTabs.displayName = "PageTabs"
export default PageTabs
// router/searchRoute.ts
type MenuItem = {
key: string;
path: string;
icon: JSX.Element | null;
label: string;
children?: MenuItem[];
}
export function searchRoute(pathname: string, menuItems: MenuItem[]): MenuItem | null {
for(const item of menuItems) {
if(item.path === pathname) {
return item;
}
if(item.children) {
const route = searchRoute(pathname, item.children);
if(route) {
return route;
}
}
}
return null;
}
3. 总结
多页签功能是一个常见的功能,本文介绍了如何实现一个简单的多页签功能,包括增加、删除、切换。核心逻辑是tabItems和activeKey,通过这两个数据来控制多页签的增删改。
tab的label和key是和路由配置是一一对应的,通过路由配置来生成tab的label和key,这样就可以实现多页签和路由的一一对应。
路由变化的时候,会执行addTabItem,引起tab变化。而activeKey变化的时候,会执行navigate,引起路由变化。