React
效果如下:
参考: https://github.com/yunsii/ant-design-pro-plus
安装包
yarn add use-switch-tabs
创建组件SwitchTabs
src/component/SwitchTabs/index.tsx
import React, { useEffect, useRef, useMemo } from 'react';
import { Tabs, Dropdown, Menu } from 'antd';
import { history, useLocation, useIntl } from 'umi';
import type { TabsProps } from 'antd/lib/tabs';
import type { MenuProps } from 'antd/lib/menu';
import type * as H from 'history-with-query';
import { useMemoizedFn } from 'ahooks';
import type { UseSwitchTabsOptions, ActionType } from 'use-switch-tabs';
import useSwitchTabs from 'use-switch-tabs';
import classNames from 'classnames';
import _get from 'lodash/get';
import styles from './index.less';
enum CloseTabKey {
Current = 'current',
Others = 'others',
ToRight = 'toRight',
}
export interface RouteTab {
/** tab's title */
tab: React.ReactNode;
key: string;
content: JSX.Element;
closable?: boolean;
/** used to extends tab's properties */
location: Omit<H.Location, 'key'>;
}
export interface SwitchTabsProps
extends Omit<UseSwitchTabsOptions, 'location' | 'history'>,
Omit<TabsProps, 'hideAdd' | 'activeKey' | 'onEdit' | 'onChange' | 'children'> {
fixed?: boolean;
footerRender?: (() => React.ReactNode) | false;
}
export default function SwitchTabs(props: SwitchTabsProps): JSX.Element {
const { mode, fixed, originalRoutes, setTabName, persistent, children, footerRender, ...rest } =
props;
const { formatMessage } = useIntl();
const location = useLocation() as any;
const actionRef = useRef<ActionType>();
const { tabs, activeKey, handleSwitch, handleRemove, handleRemoveOthers, handleRemoveRightTabs } =
useSwitchTabs({
children,
setTabName,
originalRoutes,
mode,
persistent,
location,
history,
actionRef,
});
const remove = useMemoizedFn((key: string) => {
handleRemove(key);
});
const handleTabEdit = useMemoizedFn((targetKey: string, action: 'add' | 'remove') => {
if (action === 'remove') {
remove(targetKey);
}
});
const handleTabsMenuClick = useMemoizedFn((tabKey: string): MenuProps['onClick'] => (event) => {
const { key, domEvent } = event;
domEvent.stopPropagation();
if (key === CloseTabKey.Current) {
handleRemove(tabKey);
} else if (key === CloseTabKey.Others) {
handleRemoveOthers(tabKey);
} else if (key === CloseTabKey.ToRight) {
handleRemoveRightTabs(tabKey);
}
});
const setMenu = useMemoizedFn((key: string, index: number) => (
<Menu onClick={handleTabsMenuClick(key)}>
<Menu.Item disabled={tabs.length === 1} key={CloseTabKey.Current}>
{formatMessage({ id: 'component.switchTabs.closeCurrent' })}
</Menu.Item>
<Menu.Item disabled={tabs.length === 1} key={CloseTabKey.Others}>
{formatMessage({ id: 'component.switchTabs.closeOthers' })}
</Menu.Item>
<Menu.Item disabled={tabs.length === index + 1} key={CloseTabKey.ToRight}>
{formatMessage({ id: 'component.switchTabs.closeToRight' })}
</Menu.Item>
</Menu>
));
const setTab = useMemoizedFn((tab: React.ReactNode, key: string, index: number) => (
<span onContextMenu={(event) => event.preventDefault()}>
<Dropdown overlay={setMenu(key, index)} trigger={['contextMenu']}>
<span className={styles.tabTitle}>{tab}</span>
</Dropdown>
</span>
));
useEffect(() => {
window.tabsAction = actionRef.current!;
}, []);
const footer = useMemo(() => {
if (typeof footerRender === 'function') {
return footerRender();
}
return footerRender;
}, [footerRender]);
return (
<Tabs
tabPosition="top"
type="editable-card"
tabBarStyle={{ margin: 0 }}
tabBarGutter={0}
animated
className={classNames('switch-tabs', { 'switch-tabs-fixed': fixed })}
{...rest}
hideAdd
activeKey={activeKey}
onEdit={handleTabEdit as TabsProps['onEdit']}
onChange={handleSwitch}
>
{tabs.map((item, index) => (
<Tabs.TabPane
tab={setTab(item.title, item.key, index)}
key={item.key}
closable={item.closable}
forceRender={_get(persistent, 'force', false)}
>
<main className={styles.content}>{item.content}</main>
{footer}
</Tabs.TabPane>
))}
</Tabs>
);
}
src/component/SwitchTabs/index.less
.tabTitle {
position: relative;
font-weight: initial;
&::before {
position: absolute;
top: -10px;
left: -16px;
width: calc(100% + 24px);
height: calc(100% + 20px);
content: ' ';
}
&::after {
position: absolute;
top: -10px;
right: -40px;
width: 12px;
height: calc(100% + 20px);
content: ' ';
}
}
.content {
flex: auto;
}
:global {
.custom-by-switch-tabs {
.ant-layout {
.ant-layout {
// 当值 Tabs force render 时向右切换出现闪烁的纵向滚动条
overflow: hidden;
}
}
}
.switch-tabs-fixed {
> .ant-tabs-nav {
// height: 41px;
// border-bottom: 1px solid #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
}
> .ant-tabs-content-holder {
// 当值 Tabs force render 时向右切换出现闪烁的纵向滚动条
overflow: hidden;
.ant-tabs-content {
//add by goby
> div.ant-tabs-tabpane-hidden {
display: none;
}
.ant-tabs-tabpane {
display: flex;
flex-direction: column;
}
.ant-tabs-tabpane-active {
height: calc(100vh - 88px);
overflow: auto;
}
}
}
}
.switch-tabs {
.ant-tabs-tab-remove {
width: 1.6em;
height: 1.6em;
padding: 0;
border-radius: 0.8em;
&:hover {
background: #d9d9d9;
}
}
}
}
创建SwitchTabsLayout
src/component/SwitchTabsLayout/index.tsx
/**
* Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
* You can view component api by:
* https://github.com/ant-design/ant-design-pro-layout
*/
import type { MenuDataItem } from '@ant-design/pro-layout';
import React from 'react';
import type * as H from 'history-with-query';
import { useIntl, useLocation } from 'umi';
import _isArray from 'lodash/isArray';
import memoizedOne from 'memoize-one';
import deepEqual from 'fast-deep-equal';
import type { Route } from '@ant-design/pro-layout/lib/typings';
import { PageLoading } from '@ant-design/pro-layout';
import type { Mode, RouteConfig } from 'use-switch-tabs';
import { isSwitchTab } from 'use-switch-tabs';
import type { SwitchTabsProps } from '@/components/SwitchTabs';
import SwitchTabs from '@/components/SwitchTabs';
export interface MakeUpRoute extends Route, Pick<RouteConfig, 'follow' | 'redirect'> {}
/** 根据路由定义中的 name 本地化标题 */
function localeRoutes(
routes: MakeUpRoute[],
formatMessage: any,
parent: MakeUpRoute | null = null,
): MenuDataItem[] {
const result: MenuDataItem[] = [];
routes.forEach((item) => {
const { routes: itemRoutes, ...rest } = item;
if (item.layout === false || item.path?.startsWith('/_demos')) {
return;
}
// 为标签页展示的页面注入 redirect 路由
if (item.redirect && item.path !== '/') {
result.push(item);
return;
}
if (!item.name) {
return;
}
// 初始化 locale 字段
let newItem: MenuDataItem = {
...rest,
routes: [],
locale: item.name,
};
const localeId = parent ? `${parent.locale}.${newItem.locale}` : `menu.${newItem.locale}`;
newItem = {
...rest,
locale: localeId,
name: formatMessage({ id: localeId }),
};
if (_isArray(itemRoutes) && itemRoutes.length) {
newItem = {
...newItem,
children: localeRoutes(itemRoutes, formatMessage, newItem),
};
}
result.push(newItem);
});
return result;
}
const memoizedOneLocaleRoutes = memoizedOne(localeRoutes, deepEqual);
export interface RouteTabsLayoutProps
extends Pick<SwitchTabsProps, 'persistent' | 'fixed' | 'setTabName' | 'footerRender'> {
mode?: Mode | false;
loading?: boolean;
routes?: MakeUpRoute[];
children: React.ReactElement;
}
export default function SwitchTabsLayout(props: RouteTabsLayoutProps): JSX.Element {
const { mode, loading, routes, children, ...rest } = props;
const { formatMessage } = useIntl();
const location = useLocation() as H.Location;
const originalTabsRoutes = memoizedOneLocaleRoutes(routes!, formatMessage);
if (mode && isSwitchTab(location as any, originalTabsRoutes)) {
if (loading) {
return <PageLoading />;
}
if (routes) {
return (
<SwitchTabs
mode={mode}
{...rest}
originalRoutes={originalTabsRoutes}
// animated={false}
>
{children}
</SwitchTabs>
);
}
}
return children;
}
修改src/app.tsx
import
import SwitchTabsLayout from '@/components/SwitchTabsLayout';
调整layout
export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
// +++
const { switchTabs, ...restSettings } = initialState?.settings || {};
return {
// rightContentRender: () => <RightContent />,
rightContentRender: () => (
<RightContent switchTabsReloadable={switchTabs?.mode && switchTabs.reloadable} />
),
...
// +++
className: switchTabs?.mode && 'custom-by-switch-tabs',
...
childrenRender: (children, props) => {
console.log('childrenRender props', props);
const { route } = props;
return (
<SwitchTabsLayout
mode={switchTabs?.mode}
persistent={switchTabs?.persistent}
fixed={switchTabs?.fixed}
routes={route!.routes}
footerRender={() => <Footer />}
>
{children}
</SwitchTabsLayout>
);
},
...initialState?.settings,
};
};
修改 src/global.less
增加
.custom-by-switch-tabs {
.ant-pro-basicLayout-content {
margin: unset;
& .ant-pro-page-container {
margin: unset;
}
}
}
可选
右上角增加刷新按钮
修改 src/components/RightContent/index.tsx
import { Space, Dropdown, Tooltip } from 'antd';
import { QuestionCircleOutlined, DownOutlined, ReloadOutlined } from '@ant-design/icons';
import React from 'react';
import { useModel, SelectLang, useLocation, history,useIntl } from 'umi';
import Avatar from './AvatarDropdown';
import HeaderSearch from '../HeaderSearch';
import styles from './index.less';
import NoticeIconView from '../NoticeIcon';
export type SiderTheme = 'light' | 'dark';
export interface GlobalHeaderRightProps {
switchTabsReloadable?: boolean;
}
const GlobalHeaderRight: React.FC<GlobalHeaderRightProps> = ({ switchTabsReloadable }) => {
const uri: any = useLocation();
const { query } = uri;
const { initialState } = useModel('@@initialState');
const { formatMessage } = useIntl();
...
return (
<Space className={className}>
...
{switchTabsReloadable ? (
<Tooltip title={formatMessage({ id: 'component.globalHeader.reload' })}>
<a
style={{
color: 'inherit',
}}
className={styles.action}
onClick={() => window.tabsAction.reloadTab()}
>
<ReloadOutlined />
</a>
</Tooltip>
) : null}
{initialState?.currentUser && !isHideHeader && <span
className={styles.action}
>
</span>}
<Avatar menu={false} />
{/* <SelectLang className={styles.action} /> */}
</Space>
);
};
export default GlobalHeaderRight;