多页签功能的简单实现,包括增加、删除、切换

1,015 阅读5分钟

后台管理系统中,多页签功能是一个常见的功能,本文将介绍如何实现一个简单的多页签功能,包括增加、删除、切换。

multi_tab.gif

实现的总体逻辑,我先用一个思维导图来表示: multi_tab.png

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最核心的两个数据是tabItemsactiveKeytabItems是一个数组,里面存放了每个页签的配置信息,activeKey是当前激活的页签的key,交互事件主要处理这两个数据,下面我们来实现增加、删除、切换三个简单的交互功能。
每个 tab 的配置信息包括labelkeyclosable

这里tab的key是路由的pathnameactiveKey是当前激活的tab的key,当其发生变化的时候,就会切换到对应的tab,且触发navigate跳转到对应的路由。

2.1 增加功能

路由发生变化的时候,执行该逻辑:

  1. 设置activeKey为当前路由的pathname
  2. 当前路由的pathname如果在items里,则无需处理items
  3. 如果不在items里,根据pathnameroutes配置里找到 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 的关闭按钮,执行该逻辑:

  1. items里过滤掉要删除的item
  2. 如果删除的itemkey就是activeKey,那么需要重新设置activeKey,跳到下一个页签,没有下一个就跳到上一个页签,如果都没有,就跳到首页
  3. 反之,不做处理
// 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为当前点击的tabkey

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. 总结

多页签功能是一个常见的功能,本文介绍了如何实现一个简单的多页签功能,包括增加、删除、切换。核心逻辑是tabItemsactiveKey,通过这两个数据来控制多页签的增删改。
tab的label和key是和路由配置是一一对应的,通过路由配置来生成tab的label和key,这样就可以实现多页签和路由的一一对应。
路由变化的时候,会执行addTabItem,引起tab变化。而activeKey变化的时候,会执行navigate,引起路由变化。