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即可
},