效果图:
目前react\vue\angular都是单页面的实现路由跳转方式
但因为平台是后台管理系统,业务方想实现浏览器标签页切换的方式进行配置,以前多页面是采用iframe实现,页面性能大大降低,多页面本来就是很鸡肋的方式,奈何人在屋檐下,不得不低头
首先实现要实现一个组件来管理路由吧,可以这么理解,如图的路径
1、TagView的index.jsx,index.less
import React, { useState, useEffect, useRef } from 'react';
import { RouteContext } from '@ant-design/pro-layout';
import { history } from 'umi';
import Tags from './Tags';
import styles from './index.less';
/**
* @component TagView 标签页组件
*/
const TagView = ({ children, home }) => {
const [tagList, setTagList] = useState([]);
const routeContextRef = useRef();
useEffect(() => {
if (routeContextRef?.current) {
handleOnChange(routeContextRef.current);
}
}, [routeContextRef?.current]);
// 初始化 visitedViews,设置project为首页
const initTags = (routeContext) => {
if (tagList.length === 0 && routeContext.menuData) {
const firstTag = routeContext.menuData.filter((el) => el.path === home)[0];
const title = firstTag.name;
const path = firstTag.path;
history.push({ pathname: firstTag.path, query: firstTag.query });
setTagList([
{
title,
path,
children,
refresh: 0,
active: true,
},
]);
}
};
// 监听路由改变
const handleOnChange = (routeContext) => {
const { currentMenu } = routeContext;
// tags初始化
if (tagList.length === 0) {
return initTags(routeContext);
}
// 判断是否已打开过该页面
let hasOpen = false;
const tagsCopy = tagList.map((item) => {
if (currentMenu?.path === item.path) {
hasOpen = true;
// 刷新浏览器时,重新覆盖当前 path 的 children
return { ...item, active: true, children };
} else {
return { ...item, active: false };
}
});
// 没有该tag时追加一个,并打开这个tag页面
if (!hasOpen) {
const title = routeContext.title || '';
const path = currentMenu?.path;
tagsCopy.push({
title,
path,
children,
refresh: 0,
active: true,
});
}
return setTagList(tagsCopy);
};
// 关闭标签
const handleCloseTag = (tag) => {
const tagsCopy = tagList.map((el, i) => ({ ...el }));
// 判断关闭标签是否处于打开状态
tagList.forEach((el, i) => {
if (el.path === tag.path && tag.active) {
const next = tagList[i - 1];
next.active = true;
history.push({ pathname: next?.path, query: next?.query });
}
});
setTagList(tagsCopy.filter((el) => el.path !== tag?.path));
};
// 关闭所有标签
const handleCloseAll = () => {
const tagsCopy = tagList.filter((el) => el.path === home);
history.push(home);
setTagList(tagsCopy);
};
// 关闭其他标签
const handleCloseOther = (tag) => {
const tagsCopy = tagList.filter(
(el) => el.path === home || el.path === tag.path,
);
history.push({ pathname: tag?.path, query: tag?.query });
setTagList(tagsCopy);
};
// 刷新选择的标签
const handleRefreshTag = (tag) => {
const tagsCopy = tagList.map((item) => {
if (item.path === tag.path) {
history.push({ pathname: tag?.path, query: tag?.query });
return { ...item, refresh: item.refresh + 1, active: true };
}
return { ...item, active: false };
});
setTagList(tagsCopy);
};
return (
<>
<RouteContext.Consumer>
{(value) => {
// console.log(value);
routeContextRef.current = value;
return null;
}}
</RouteContext.Consumer>
<div className={styles.tag_view}>
<div className={styles.tags_container}>
<Tags
tagList={tagList}
closeTag={handleCloseTag}
closeAllTag={handleCloseAll}
closeOtherTag={handleCloseOther}
refreshTag={handleRefreshTag}
/>
</div>
</div>
{tagList.map((item) => {
return (
<div key={item.path} style={{ display: item.active ? 'block' : 'none' }}>
<div key={item.refresh}>{item.children}</div>
</div>
);
})}
</>
);
};
export default TagView;
.tag_view {
.tags_container {
position: relative;
top: -24px;
margin-left: -24px;
z-index: 99;
width: calc(100% + 48px);
height: 36px;
}
}
2、TagView-Tags的index.jsx,index.less
import React, { useState, useRef, useEffect } from 'react';
import { history } from 'umi';
import { CloseOutlined } from '@ant-design/icons';
import styles from './index.less';
const Tags = ({ tagList, closeTag, closeAllTag, closeOtherTag, refreshTag }) => {
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);
const [menuVisible, setMenuVisible] = useState(false);
const [currentTag, setCurrentTag] = useState();
const tagListRef = useRef();
const contextMenuRef = useRef();
useEffect(() => {
return () => {
document.body.removeEventListener('click', handleClickOutside);
};
}, []);
// 由于react的state不能及时穿透到 document.body.addEventListener去,需要在每次值发送改变时进行解绑和再次监听
useEffect(() => {
document.body.removeEventListener('click', handleClickOutside);
document.body.addEventListener('click', handleClickOutside);
}, [menuVisible]);
const handleClickOutside = (event) => {
const isOutside = !(contextMenuRef.current && contextMenuRef.current.contains(event.target));
if (isOutside && menuVisible) {
setMenuVisible(false);
}
};
const openContextMenu = (
event,
tag,
) => {
event.preventDefault();
const menuMinWidth = 105;
const clickX = event.clientX;
const clickY = event.clientY; //事件发生时鼠标的Y坐标
const clientWidth = tagListRef.current?.clientWidth || 0; // container width
const maxLeft = clientWidth - menuMinWidth; // left boundary
setCurrentTag(tag);
setMenuVisible(true);
setTop(clickY);
// 当鼠标点击位置大于左侧边界时,说明鼠标点击的位置偏右,将菜单放在左边
// 反之,当鼠标点击的位置偏左,将菜单放在右边
const Left = clickX > maxLeft ? clickX - menuMinWidth + 15 : clickX;
setLeft(Left);
};
return (
<div className={styles.tags_wrapper} ref={tagListRef}>
<div >
{tagList.map((item, i) => (
<div
key={item.path}
className={item.active ? `${styles.item} ${styles.active}` : styles.item}
onClick={() => history.push({ pathname: item.path, query: item.query })}
onContextMenu={(e) => openContextMenu(e, item)}
>
<span>{item.title}</span>
{i !== 0 && (
<CloseOutlined
className={styles.icon_close}
onClick={(e) => {
e.stopPropagation();
closeTag && closeTag(item);
}}
/>
)}
</div>
))}
</div>
{menuVisible ? (
<ul
className={styles.contextmenu}
style={{ left: `${left}px`, top: `${top}px` }}
ref={contextMenuRef}
>
<li
onClick={() => {
setMenuVisible(false);
currentTag && refreshTag && refreshTag(currentTag);
}}
>
刷新
</li>
<li
onClick={() => {
setMenuVisible(false);
currentTag && closeOtherTag && closeOtherTag(currentTag);
}}
>
关闭其他
</li>
<li
onClick={() => {
setMenuVisible(false);
closeAllTag && closeAllTag();
}}
>
关闭所有
</li>
</ul>
) : null}
</div>
);
};
export default Tags;
@primary: #1890ff;
.tags_wrapper {
position: relative;
width: 100%;
height: 34px;
line-height: 34px;
background: #fff;
.item {
position: relative;
display: inline-block;
height: 28px;
margin-top: 2px;
margin-left: 5px;
padding: 0 8px;
color: #495060;
font-size: 13px;
line-height: 26px;
background: #fff;
border: 1px solid #d8dce5;
cursor: pointer;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
color: #fff;
background-color: @primary;
border-color: @primary;
&::before {
position: relative;
display: inline-block;
width: 8px;
height: 8px;
margin-right: 2px;
background: #fff;
border-radius: 50%;
content: '';
}
}
}
.icon_close {
position: relative;
top: -1px;
margin-left: 6px;
font-size: 10px;
&:hover {
color: red;
}
}
.contextmenu {
position: fixed;
z-index: 3000;
margin: 0;
padding: 5px 0;
color: #333;
font-weight: 400;
font-size: 12px;
list-style-type: none;
background: #fff;
border-radius: 4px;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
margin: 0;
padding: 2px 16px;
line-height: 24px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
3、在主页面app.jsx实现引入
export const layout = ({ initialState }) => {
return {
rightContentRender: () => <RightContent />,
//tad---start
childrenRender: (children) => {
return (
<>
{initialState?.currentUser && location.pathname !== loginPath ? (
<TagView children={children} home="/welcome" />
) : (
children
)}
</>
);
},
//tag----end
disableContentMargin: false,
footerRender: () => <Footer />,
onPageChange: () => {
const { location } = history; // 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== loginPath) {
history.push(loginPath);
}
},
menuHeaderRender: undefined,
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
...initialState?.settings,
};
};