Ant Design Pro多tabs 布局,可关闭左侧 - 可关闭右侧 - 可关闭其它 - 可刷新页面并且提供刷新钩子 - 一个标签去除删除按钮

1,672 阅读4分钟

前景

  • 我从事物流行业的开发,之前做一个SaaS系统。
  • 整个框架都是自己搭建。页面布局+权限系统+表格表单封装(和ProComponents差不多,早知道有ProComponents就不自己封装了)+Tabs布局 + 导入导出 + 自动化打包部署 + 基于antd封装业务组件库部署在阿里云,等功能。
  • 由于项目越来越大基础业务组件基于antd封装过度,antd升级之后改动很大而无法再次扩展。
  • 所以寻找新的方向。

image.png

寻找出口

  • ANT DESIGN PRO的功能和我们的业务十分吻合,加上ProComponents和我之前封装的组件库也十分吻合。
  • 所以就打算先做一个demo把项目基本功能架构实现。
  • 发现ANT DESIGN PRO不支持多tabs布局。然后就在掘金搜索解决方案,无意之间找到了阿里大佬聪小陈juejin.cn/post/710949… 写的博客。但是该版本没有可关闭左侧 - 可关闭右侧 - 可关闭其它 - 可刷新页面并且提供刷新钩子。所以在大佬的基础上扩展了该功能。

实现流程

  1. 在config.ts中配置
{
      model: {},
      keepalive: [/./],
      tabsLayout: {
        hasCustomTabs: true,
      }
  }
  1. 在app.js暴露自定义tabs getCustomTabs
export interface TabsViewPropsData {
  activeKey: string;
  dropByCacheKey: (path: string) => void;
  isKeep: boolean;
  keepElements: {
    current: object;
  };
  local: object;
  navigate: (to: string, options?: any) => void;
}

export const getCustomTabs = () => (data: TabsViewPropsData) => {
  return <TabsView data={data} />;
};
  1. 自定义TabsView组件
import { Dropdown, Menu, message, Tabs } from 'antd';
import type { MenuInfo } from 'rc-menu/lib/interface';
import type { FC } from 'react';
import { useEffect } from 'react';
import { useModel } from 'umi';
import routes from '../../../config/routes';
import './index.less';

const { TabPane } = Tabs;

export interface TabsViewPropsData {
  activeKey: string;
  dropByCacheKey: (path: string) => void;
  isKeep: boolean;
  keepElements: {
    current: object;
  };
  local: object;
  navigate: (to: string, options?: any) => void;
}

export interface TabsViewProps {
  data: TabsViewPropsData;
}

const TabsView: FC<TabsViewProps> = ({ data }) => {
  const { updateTabsParams } = useModel('tabs');
  useEffect(() => {
    updateTabsParams(data);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data]);

  const onEdit = (activeKey: any) => {
    const buffer = Object.keys(data.keepElements.current);
    if (buffer.length <= 1) {
      message.destroy();
      message.warning('至少留一个标签页');
      return;
    }
    if (activeKey === data.activeKey) {
      const index = buffer.findIndex((it) => it === activeKey);
      if (index === 0) {
        data.navigate(buffer[1]);
        data.dropByCacheKey(activeKey);
        return;
      }
      data.navigate(buffer[index - 1]);
      data.dropByCacheKey(activeKey);
    } else {
      data.dropByCacheKey(activeKey);
    }
  };

  const getRoute = (routesArr: any[], path: any) => {
    const buffer: any[] = [];
    const eachItem = (list: { routes: any }[], path2: any) => {
      list.forEach((it: { routes: any }) => {
        if (it.routes) {
          eachItem(it.routes, path2);
        } else {
          buffer.push(it);
        }
      });
    };
    eachItem(routesArr, path);
    const item = buffer.find((it) => it.path === path);
    return item;
  };

  const getOperation = (arr: any[], path: any) => {
    let buffer: any = [];
    const index = arr.findIndex((it: any) => it === path);
    if (index === 0 && arr.length === 1) {
      buffer = [
        {
          key: 1,
          label: <a>刷新</a>,
        },
      ];
    } else if (index === 0 && arr.length > 1) {
      buffer = [
        {
          key: 1,
          label: <a>刷新</a>,
        },
        {
          key: 2,
          label: <a>关闭右边侧标签</a>,
        },
        {
          key: 4,
          label: <a>关闭全部标签</a>,
        },
      ];
    } else if (index === arr.length - 1) {
      buffer = [
        {
          key: 1,
          label: <a>刷新</a>,
        },
        {
          key: 3,
          label: <a>关闭左边侧标签</a>,
        },
        {
          key: 4,
          label: <a>关闭全部标签</a>,
        },
      ];
    } else {
      buffer = [
        {
          key: 1,
          label: <a>刷新</a>,
        },
        {
          key: 2,
          label: <a>关闭右侧标签</a>,
        },
        {
          key: 3,
          label: <a>关闭左边侧标签</a>,
        },
        {
          key: 4,
          label: <a>关闭全部标签</a>,
        },
      ];
    }
    return buffer;
  };

  const operationAction = (event: MenuInfo, it: string) => {
    const buffer = Object.keys(data.keepElements.current);
    if (event.key === '1') {
      data.navigate(it);
      return;
    }

    if (event.key === '2') {
      const currentIndex = buffer.findIndex((it2) => it2 === it);
      const bufferArr = buffer.filter((_, index) => {
        return index > currentIndex;
      });
      bufferArr.forEach((item: any) => {
        data.dropByCacheKey(item);
      });
    }

    if (event.key === '3') {
      const currentIndex = buffer.findIndex((it2) => it2 === it);
      const bufferArr = buffer.filter((_, index) => {
        return index < currentIndex;
      });
      bufferArr.forEach((item: any) => {
        data.dropByCacheKey(item);
      });
    }

    if (event.key === '4') {
      const bufferArr = buffer.filter((item) => {
        return item != it;
      });
      bufferArr.forEach((item: any) => {
        data.dropByCacheKey(item);
      });
    }
  };

  return (
    <div className="card-container">
      <Tabs
        hideAdd
        type="editable-card"
        activeKey={data.activeKey}
        onChange={(activeKey) => {
          data.navigate(activeKey);
        }}
        onEdit={(activeKey) => onEdit(activeKey)}
      >
        {Object.keys(data.keepElements.current).map((it: any) => {
          return (
            it && (
              <TabPane
                closable={Object.keys(data.keepElements.current).length > 1}
                tab={
                  <div className={'layout-tabs-title'}>
                    <Dropdown
                      overlay={
                        <Menu
                          style={{ width: 150 }}
                          items={getOperation(Object.keys(data.keepElements.current), it)}
                          onClick={(event) => operationAction(event, it)}
                        />
                      }
                      trigger={['contextMenu']}
                    >
                      <div>
                        <span style={{ marginLeft: '2px' }}>{getRoute(routes, it)?.name}</span>
                      </div>
                    </Dropdown>
                  </div>
                }
                key={it}
              />
            )
          );
        })}
      </Tabs>
    </div>
  );
};

export default TabsView;

.card-container {
  padding: 20px 20px 0 20px;
}
.card-container p {
  margin: 0;
}
.card-container > .ant-tabs-card .ant-tabs-content {
  height: 120px;
  margin-top: -16px;
}
.card-container > .ant-tabs-card .ant-tabs-content > .ant-tabs-tabpane {
  padding: 16px;
  background: #fff;
}
.card-container > .ant-tabs-card > .ant-tabs-nav::before {
  display: none;
}
.card-container > .ant-tabs-card .ant-tabs-tab,
[data-theme='compact'] .card-container > .ant-tabs-card .ant-tabs-tab {
  background: transparent;
  border-color: transparent;
}
.card-container > .ant-tabs-card .ant-tabs-tab-active,
[data-theme='compact'] .card-container > .ant-tabs-card .ant-tabs-tab-active {
  background: #fff;
  border-color: #fff;
}
#components-tabs-demo-card-top .code-box-demo {
  padding: 24px;
  overflow: hidden;
  background: #f5f5f5;
}
[data-theme='compact'] .card-container > .ant-tabs-card .ant-tabs-content {
  height: 120px;
  margin-top: -8px;
}
[data-theme='dark'] .card-container > .ant-tabs-card .ant-tabs-tab {
  background: transparent;
  border-color: transparent;
}
[data-theme='dark'] #components-tabs-demo-card-top .code-box-demo {
  background: #000;
}
[data-theme='dark'] .card-container > .ant-tabs-card .ant-tabs-content > .ant-tabs-tabpane {
  background: #141414;
}
[data-theme='dark'] .card-container > .ant-tabs-card .ant-tabs-tab-active {
  background: #141414;
  border-color: #141414;
}
.card-container {
  .ant-tabs-nav {
    margin-bottom: 0 !important;
  }
  .ant-tabs-content-holder {
    display: none;
  }
}

  1. 开启 model插件,在models新建tabs.tsx 实现对页面栈的管理。
import type { TabsViewPropsData } from '@/components/TabsView';
import { useState } from 'react';
interface RouteItem {
  url: string;
  number: number;
}
interface TabsParamsVo {
  routes: RouteItem[];
  activeKey: string;
  tabs: TabsViewPropsData | undefined;
}

export default () => {
  const [tabsParams, setTabsParams] = useState<TabsParamsVo>({
    routes: [],
    activeKey: '/welcome',
    tabs: undefined,
  });

  const updateTabsParams = (data: TabsViewPropsData) => {
    const { routes } = tabsParams;
    const currentRoutes = Object.keys(data.keepElements.current);
    const currentActiveKey = data.activeKey;
    const finalRoutes: RouteItem[] = [];
    currentRoutes.forEach((element) => {
      const item = routes.find((it) => it.url === element);
      if (item) {
        finalRoutes.push(item);
      } else {
        finalRoutes.push({
          url: element,
          number: new Date().getTime(),
        });
      }
    });
    if (routes.length === currentRoutes.length || tabsParams.activeKey !== data.activeKey) {
      finalRoutes.forEach((it) => {
        if (it.url === currentActiveKey) {
          it.number = new Date().getTime();
        }
      });
    }

    setTabsParams({
      routes: finalRoutes,
      activeKey: currentActiveKey,
      tabs: data,
    });
  };

  const getNumber = (path: string) => {
    const { routes } = tabsParams;
    const item = routes.find((it) => it.url === path);
    return item?.number;
  };

  return { tabsParams, updateTabsParams, getNumber };
};

  1. 在页面使用刷新钩子。
// /waybill/list当前页面地址
// 具体思路是每添加一个页面就在页面栈存储,并且记录添加时的时间
//  当刷新触发当前页面的时间
// 页面发现当前页面的时间发生变化就触发刷新钩子
export default function Waybill() {
    const { getNumber } = useModel('tabs');
    const number = getNumber('/waybill/list');
    useEffect(() => {
        //刷新钩子回调
    }, [number]);
}
  1. 最终演示

666.gif