2024最新 基于ant design pro 6实现多tab页(路由keepalive) 切换tab可刷新页面(可配置非缓存路由) 右键菜单 删除其他/删除左侧/右侧/刷新/刷新当前/拖拽

1、前言

其实ant pro已经内置了多tabs页面,无需手写,插件为 alitajs,点我打开官网

2、使用

// config/config.ts
在配置项中加上这几行即可
{
  keepalive: [/./],
  tabsLayout: {
    hasDropdown: true,
  },
}

以上重启服务即可,基本都是官网实例。

3、自定义tabs

由于需要动态配置部分网页 是否刷新,或需要增加额外功能,那么只能使用自定义tab了

3.1、修改layouts


/src/layouts/index.tsx

/src/layouts/index.tsx
import { useOutlet } from '@umijs/max'
import styles from './index.less'
const Index = () => {
    const outlet = useOutlet()
    return (<div className={styles.layout}>
        {outlet}
    </div>);
}

export default Index;

/src/layouts/index.less

@height: 56px;
.layout {
  position: relative;
  padding-top: @height;
  height: calc(100vh - @height);
  overflow: auto;
}

3.2、新建customTabs文件夹(核心)

3.2.1、安装拖拽库

首先安装拖拽插件,我参考的事antd的tab拖拽,当然,可以用别的拖拽库

pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

3.2.2、核心代码 /src/layouts/customTabs/index.tsx

/src/layouts/customTabs/index.tsx

// /src/layouts/customTabs/index.tsx

import styles from './index.less'
import { Dropdown, Tabs, message } from "antd";
import type { MenuInfo } from 'rc-menu/lib/interface';
import { useMemo, useState, ReactNode, useEffect, HTMLAttributes, CSSProperties, cloneElement, ReactElement } from "react";
import type { Items, KeepElementsType, MenuClickStrateg, layoutTabs } from './typings.d'
import { OperationType } from './typings.d'
import { useAppData } from '@umijs/max'
import { getNotKeepAlive, menuItemRender, sortCurrentByItemsKeys } from './utils';
import type { DragEndEvent } from '@dnd-kit/core';
import { DndContext, PointerSensor, useSensor } from '@dnd-kit/core';
import {
    arrayMove,
    horizontalListSortingStrategy,
    SortableContext,
    useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

interface DraggableTabPaneProps extends HTMLAttributes<HTMLDivElement> {
    'data-node-key': string;
}

const DraggableTabNode = ({ ...props }: DraggableTabPaneProps) => {
    const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
        id: props['data-node-key'],
    });

    const style: CSSProperties = {
        ...props.style,
        transform: CSS.Transform.toString(transform && { ...transform, scaleX: 1 }),
        transition,
        cursor: 'move',
    };

    return cloneElement(props.children as ReactElement, {
        ref: setNodeRef,
        style,
        ...attributes,
        ...listeners,
    });
};

const Tab = (props: layoutTabs) => {

    const {
        keepElements,
        navigate,
        dropByCacheKey,
        activeKey,
        refreshTab,
        dropLeftTabs,
        dropRightTabs,
        dropOtherTabs
    } = props

    const [items, setItems] = useState<Items[]>([])

    const sensor = useSensor(PointerSensor, { activationConstraint: { distance: 10 } });

    const onDragEnd = ({ active, over }: DragEndEvent) => {
        if (active.id !== over?.id) {
            const activeIndex = items.findIndex((i) => i.key === active.id);
            const overIndex = items.findIndex((i) => i.key === over?.id);
            const moveList: Items[] = arrayMove(items, activeIndex, overIndex)
            setItems(moveList)
            sortCurrentByItemsKeys(keepElements, moveList)
        }
    };

    const [menuItemList, setMenuItemList] = useState<any[]>([]);
    const [currentPath, setCurrentPath] = useState<string>('');
    const routerList: KeepElementsType[] = Object.values(keepElements?.current || {})
    const pathList: string[] = Object.keys(keepElements?.current || {})
    const length: number = routerList?.length ?? 0//获取长度
    const { routes } = useAppData() as any;

    const onEdit = (targetKey: string) => {
        if (pathList.length === 1) {
            message.info('至少要保留一个窗口')
            return
        }
        dropByCacheKey(targetKey)
        if (targetKey === activeKey) {
            // 删除当前选中的tab时:
            // 1.如果当前tab是第一个时自动选中后一个
            // 2.不是第一个时自动选中前一个
            const i = pathList.indexOf(targetKey)
            navigate(pathList[i === 0 ? i + 1 : i - 1])
        }
    }

    // 右键下拉处理
    const menuClickStrateg: MenuClickStrateg = {
        [OperationType.REFRESH]: () => refreshTab(currentPath),//刷新
        [OperationType.CLOSECRUTTNET]: () => onEdit(currentPath),//关闭当前
        [OperationType.CLOSEOTHER]: () => dropOtherTabs(currentPath),//关闭其他
        [OperationType.CLOSELEFT]: () => dropLeftTabs(currentPath),//关闭左侧
        [OperationType.CLOSERIGHT]: () => dropRightTabs(currentPath),//关闭右侧
    }

    const renderLabel = (icon: ReactNode, name: string, pathname: string) => <Dropdown
        menu={{
            items: menuItemList, onClick: ({ key, domEvent }: MenuInfo) => {
                domEvent.stopPropagation();
                if (key !== OperationType.CLOSECRUTTNET) {
                    navigate(currentPath)
                }
                menuClickStrateg?.[key]?.()
            }
        }}
        onOpenChange={(open: boolean) => {
            if (!open) return
            setCurrentPath(pathname)
            // 当前下标
            const currentIndex: number = keepElements.current?.[pathname]?.index
            // 左菜单显示标识
            const leftShow: boolean = length > 1 && !!currentIndex
            // 右菜单显示标识
            const rightShow: boolean = length > 1 && currentIndex !== (routerList.length - 1)
            setMenuItemList(menuItemRender(length, leftShow, rightShow))
        }}
        trigger={['contextMenu']}
    >
        <div>{icon}{name}</div>
    </Dropdown >

    useEffect(() => {
        setItems(routerList.map(({ icon, name, location: { pathname } }: KeepElementsType) => ({
            key: pathname,
            label: renderLabel(icon, name, pathname),
            closable: length > 1,
        })))
    }, [props, menuItemList])

    // 获取当前keepAlive = false 的路由路径
    const notKeepAliveList: string[] = useMemo(() => getNotKeepAlive(routes), [])

    return (
        <Tabs
            style={{ zIndex: 99 }}
            items={items}
            hideAdd
            renderTabBar={(tabBarProps, DefaultTabBar) => (
                <DndContext sensors={[sensor]} onDragEnd={onDragEnd}>
                    <SortableContext items={items.map((i) => i.key)} strategy={horizontalListSortingStrategy}>
                        <DefaultTabBar {...tabBarProps}>
                            {(node) => (
                                <DraggableTabNode {...node.props} key={node.key}>
                                    {node}
                                </DraggableTabNode>
                            )}
                        </DefaultTabBar>
                    </SortableContext>
                </DndContext>
            )}
            onChange={(key: string) => {
                // 路由keepAlive = false 时刷新组件
                if (notKeepAliveList.includes(key)) {
                    //如果每个组件都需要刷新,这个判断去掉
                    refreshTab(key)
                }
                navigate(key);
            }}
            activeKey={activeKey}
            type="editable-card"
            onEdit={(targetKey: string | any) => onEdit(targetKey)}
        />
    )
}

const customTabs = () => ((props: layoutTabs) => {
    const { isKeep } = props
    return (
        <div className={styles.tabsLayout} hidden={!isKeep}>
            <Tab {...props} />
        </div>
    )
})

export default customTabs

3.2.3、样式 /src/layouts/customTabs/index.less

注意需要关闭动画,否则拖拽明显会卡顿

.tabsLayout {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: 999;
  :global {
    .ant-tabs-nav {
      background-color: #fafafa;
    }
    :where(.css-dev-only-do-not-override-1ansfxo).ant-tabs-card > .ant-tabs-nav .ant-tabs-tab,
    :where(.css-dev-only-do-not-override-1ansfxo).ant-tabs-card
      > div
      > .ant-tabs-nav
      .ant-tabs-tab {
      transition: none; //必须设置,否则拖拽会卡
    }
  }
}

3.2.4、类型定义 /src/layouts/customTabs/typings.d.ts

import type { ItemType } from 'rc-menu/lib/interface';
import { MutableRefObject, ReactNode } from 'react';

type Location = {
  pathname: string;
  search: string;
  hash: string;
  state: any;
  key: string;
};

type KeepElementsType = {
  pathname: string;
  children: ReactNode;
  closable: boolean;
  icon?: ReactNode;
  index: number;
  location: Location;
  name: string;
};

export type KeepElements = MutableRefObject<Record<string, KeepElementsType>>;

// 定义props的具体类型
type layoutTabs = {
  isKeep: boolean;
  activeKey: string;
  tabNameMap: Record<string, number>;
  local: Record<string, string>;
  keepElements: KeepElements;
  navigate: (path: string) => void;
  dropByCacheKey: (path: string) => void;
  refreshTab: (path: string) => void;
  dropLeftTabs: (path: string) => void;
  dropRightTabs: (path: string) => void;
  dropOtherTabs: (path: string) => void;
  refreshTab: (path: string) => void;
  updateTab: (path: string, config: TabConfig) => void;
};

// 定义操作类型
export enum OperationType {
  REFRESH = 'refresh',
  CLOSECRUTTNET = 'close-current',
  CLOSEOTHER = 'close-other',
  CLOSELEFT = 'close-left',
  CLOSERIGHT = 'close-right',
}

export type MenuItemType = (ItemType & { key: OperationType }) | null;

export type MenuClickStrateg = {
  [key: string]: () => void;
};

export type Routes = {
  [key: string]: {
    path: string;
    keepAlive?: boolean;
  };
};
export type Items = {
  key: string;
  closable: boolean;
  label: ReactNode;
};

3.2.5、工具类 /src/layouts/customTabs/utils/index.ts

import { Routes } from '../typings';
import type { Items, KeepElements, MenuItemType } from '../typings.d';
import { OperationType } from '../typings.d'

import {
  ArrowLeftOutlined,
  ArrowRightOutlined,
  CloseCircleOutlined,
  CloseOutlined,
  RedoOutlined,
} from '@ant-design/icons';

// 获取路由keepAlive为false的路径的函数
export const getNotKeepAlive = (routes: Routes): string[] => {
  const paths: string[] = [];
  Object.values(routes).forEach((route) => {
    if (route?.keepAlive === false) {
      paths.push(route.path);
    }
  });
  return paths;
};

// 右键菜单
export const menuItemRender: MenuItemType[] | any = (
  length: number,
  leftShow: boolean = true,
  rightShow: boolean = true,
) => [
    {
      label: '刷新',
      key: OperationType.REFRESH,
      icon: <RedoOutlined />,
    },
    length > 1 && {
      label: '关闭当前',
      key: OperationType.CLOSECRUTTNET,
      icon: <CloseOutlined />,
    },
    length > 1 && {
      label: '关闭其他',
      key: OperationType.CLOSEOTHER,
      icon: <CloseCircleOutlined />,
    },
    leftShow && {
      label: '关闭左侧',
      key: OperationType.CLOSELEFT,
      icon: <ArrowLeftOutlined />,
    },
    rightShow && {
      label: '关闭右侧',
      key: OperationType.CLOSERIGHT,
      icon: <ArrowRightOutlined />,
    },
  ]

// 排序对比重置当前tab列表
export const sortCurrentByItemsKeys = (keepElements: KeepElements, items: Items[]) => {
  const sortedElements: any = {};
  items.forEach((item: Items, index: number) => {
    const key = item.key;
    sortedElements[key] = keepElements.current[key];
    sortedElements[key].index = index; // 更新index值
  });
  keepElements.current = sortedElements
}

3.2.6、配置路由 /config/routes.ts

      {
        path: '/functionality/rightMouseMenu',
        name: '鼠标右键菜单',
        icon: 'smile',
        component: './Functionality/RightMouseMenu',
        keepAlive: false,//不需要缓存则设置为false即可
      },

4、效果图

完整代码请看我的仓库地址 ,点我打开

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值