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

寻找出口
- ANT DESIGN PRO的功能和我们的业务十分吻合,加上ProComponents和我之前封装的组件库也十分吻合。
- 所以就打算先做一个demo把项目基本功能架构实现。
- 发现ANT DESIGN PRO不支持多tabs布局。然后就在掘金搜索解决方案,无意之间找到了阿里大佬聪小陈juejin.cn/post/710949… 写的博客。但是该版本没有可关闭左侧 - 可关闭右侧 - 可关闭其它 - 可刷新页面并且提供刷新钩子。所以在大佬的基础上扩展了该功能。
实现流程
- 在config.ts中配置
{
model: {},
keepalive: [/./],
tabsLayout: {
hasCustomTabs: true,
}
}
- 在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} />;
};
- 自定义
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;
}
}
- 开启 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 }
}
- 在页面使用刷新钩子。
export default function Waybill() {
const { getNumber } = useModel('tabs');
const number = getNumber('/waybill/list');
useEffect(() => {
}, [number]);
}
- 最终演示
