多标签页很多公司的后台管理系统都会有这个需求,之前用vue一般架子也是带的,现在公司用了antd pro ,看了下官方不支持,确实会影响性能,但是架不住需求。
补充: antdpro 已经内置了多页签,可以自己配置看看。这篇文章留着学习。
背景
先看下远古截图:
https://github.com/ant-design/ant-design-pro/issues/220
17年提出的需求现在还没有实现,看样官方也是铁了心了。在看看提供的其他解决方案:
- https://github.com/LANIF-UI/dva-boot-admin
- https://github.com/kuhami/react-ant
- https://github.com/zkboys/react-admin
- https://github.com/kuhami/react-ant-pro
- https://github.com/MudOnTire/antd-pro-page-tabs
- https://github.com/zpr1g/ant-design-pro-plus
- https://github.com/lengjh/antd-design-pro-tabs
- https://github.com/alitajs/alita/tree/master/packages/tabs-layout
另外我还找到了一个插件也可以实现:
https://github.com/fangzhengjin/umi-plugin-panel-tabs
这些不是因为这版本不对,就是细节不满意决定自己实现一个版本。
自己实现的好处:
- UI自定义
- 功能添加方便,知道核心原理,修改Bug也方便。
核心原理
先看最后实现的版本:
核心问题:
- 菜单标签路由地址联动
- 标签卡内容需要缓存,切换不丢失
- 后台返回路由也应该支持
功能实现
核心实现思路:
- 通过地址栏变化匹配路由变化标签栏
- 标签卡选用Tabs组件+ Route 标签加key缓存
- dva来实现数据管理,也可以选用别的,能全局操作即可。
具体逻辑就是,写一个TabsView 组件,在Layout chlidren的时候嵌套上Tabs 多页签卡这一层。
Layout 文件夹Index.ts 文件:
<TabsView
activeKey={getActiveKey(props.tagsModel)}
tags={props.tagsModel}
route={props.route}
dispatch={props.dispatch}
/>
1、 数据实现组织
利用dva 来实现tags 数组的增删改查,具体代码如下:
/*
* @Author: ZY
* @Date: 2021-10-25 13:42:43
* @LastEditors: ZY
* @LastEditTime: 2022-05-01 10:18:48
* @Description: dva tags
* tabs 整理设计思路:
* 需求:两种逻辑,一种是菜单功能,功能、路由、tag是一对一的关系,另一种单据类可以开多个
* 设计:
* 利用tabs 进行页面布局,来实现缓存的目的。key是path和query的合集,这样能满足需求
* 利用dva组织数据
* 动态加载组件,组件利用key关联
*/
import type { Reducer } from 'umi';
import _remove from 'lodash/remove';
import _cloneDeep from 'lodash/cloneDeep';
import _findIndex from 'lodash/findIndex';
export interface Tag {
key: string;
title: string;
path?: string;
active: boolean;
query?: any;
}
export type TagsStateType = Tag[];
export interface TagsModelType {
namespace: 'tagsModel';
state: TagsStateType;
reducers: {
addTag: Reducer<TagsStateType>;
updateActive: Reducer<TagsStateType>;
removeTag: Reducer<TagsStateType>;
removeAllTags: Reducer<TagsStateType>;
};
}
/**
* @description: 初始化tab
* @param {*}
* @return {*}
*/
const homeTag: Tag = {
key: `/dashboard`,
title: '首页',
active: true,
path: '/dashboard',
};
/**
* @description:
* @param {*}
* @return {*}
*/
const addNewTag = (tags: Tag[], newTag: Tag) => {
if (_findIndex(tags, (t) => t.key === newTag.key) === -1) {
// tags数组里面有没有新增的tag
const cTags = tags.map((t) => {
const ct = _cloneDeep(t);
ct.active = false;
return ct;
});
return [...cTags, newTag];
}
// 新增tag 在数组中,选中即可。
const cTags = tags.map((t) => {
const ct = _cloneDeep(t);
ct.active = ct.key === newTag.key;
return ct;
});
return cTags;
};
const TagsModel: TagsModelType = {
namespace: 'tagsModel',
state: [homeTag] as TagsStateType,
reducers: {
addTag: (state, action) => {
if (state) {
return addNewTag(state, action.payload);
}
return [];
},
updateActive: (state, action) => {
if (!state) {
return [];
}
const cTags = state.map((t) => {
const ct = _cloneDeep(t);
ct.active = ct.key === action.payload;
return ct;
});
return [...cTags];
},
removeTag: (state, action) => {
if (!state) {
return [];
}
const ct = _cloneDeep(state);
if (ct.filter((t) => t.active)[0].key === action.payload) {
_remove(ct, (tag: Tag) => tag.key === action.payload);
// 如果关闭的是当前选中的标签,默认选中最后一个的策略
ct[ct.length - 1].active = true;
} else {
_remove(ct, (tag: Tag) => tag.key === action.payload);
}
return [...ct];
},
removeAllTags: () => {
return [];
},
},
};
export default TagsModel;
2、 tabsView 组件核心代码
这里会遍历tags 数组,然后创建tab, 每一个tab 都用Route 标签缓存,通过路径匹配的组件。
<Tabs
activeKey={activeKey}
type="editable-card"
hideAdd={true}
onEdit={tabOnEdit}
onChange={tabOnChange}
onTabClick={onTabClick}
>
{tags.length &&
tags.map((tag) => {
return (
<TabPane tab={tag.title} key={tag.key} closable={tag.key !== '/dashboard'}>
<Route key={tag.key} component={getPathComponent(tag.path!)} exact />
</TabPane>
);
})}
</Tabs>
通过路径找到组件
const getPathComponent = (path: string) => {
const r = route.routes?.filter((t) => {
// 如果是动态路由,匹配非动态部分
if (t.path?.includes(':')) {
return path.includes(t.path.split(':')[0]);
}
return t.path === path;
})[0];
return (r as any).component;
};
这样核心内容基本讲完,边缘代码就不多赘述了,代码再项目里还没来得及抽取,如有需要以后找时间发出。
应同学们要求匆忙抽取了这部分代码demo ,地址如下:
https://github.com/RainManGO/antd-pro-v5-tabs