@@ -73,6 +73,7 @@ | |||
"omit.js": "^2.0.2", | |||
"qrcode.react": "^1.0.1", | |||
"react": "^17.0.0", | |||
"react-custom-scrollbars": "^4.2.1", | |||
"react-dev-inspector": "^1.1.1", | |||
"react-dom": "^17.0.0", | |||
"react-helmet-async": "^1.0.4", | |||
@@ -4,6 +4,7 @@ import { notification } from 'antd'; | |||
import { history, Link, RequestConfig } from 'umi'; | |||
import RightContent from '@/components/RightContent'; | |||
import Footer from '@/components/Footer'; | |||
import TagView from '@/components/TagView'; | |||
import * as Icon from '@ant-design/icons'; | |||
import api from '@/services/api'; | |||
const isDev = process.env.NODE_ENV === 'development'; | |||
@@ -978,6 +979,15 @@ export const layout = ({ initialState }) => { | |||
} | |||
}, | |||
menuDataRender: () => loopMenuItem(initialState?.menuData), | |||
childrenRender: (children) => { | |||
// children.pathname = history.location.pathname; | |||
console.log("11111",children); | |||
return ( | |||
<TagView children={children} home="/quickStart" current={history.location.pathname}> | |||
{/* <PageContainer>{children}</PageContainer> */} | |||
</TagView> | |||
); | |||
}, | |||
// 自定义 403 页面 | |||
// unAccessible: <div>unAccessible</div>, | |||
...initialState?.settings, | |||
@@ -0,0 +1,128 @@ | |||
import React, { useState, useRef, useEffect } from 'react'; | |||
import { Scrollbars } from 'react-custom-scrollbars'; | |||
import { CloseOutlined } from '@ant-design/icons'; | |||
import { history } from 'umi'; | |||
// import type { TagsItemType } from '../index'; | |||
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(); | |||
const [currentPath, setCurrentPath] = useState(); | |||
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}> | |||
<Scrollbars autoHide autoHideTimeout={1000} autoHideDuration={200}> | |||
{tagList.map((item, i) => { | |||
if (item.path == history.location.pathname) { | |||
item.active = true; | |||
} else { | |||
item.active = false; | |||
} | |||
return ( | |||
<div | |||
key={item.path} | |||
className={item.active ? `${styles.item} ${styles.active}` : styles.item} | |||
onClick={(e) => { | |||
e.stopPropagation(); | |||
setCurrentPath(item.path); | |||
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> | |||
); | |||
})} | |||
</Scrollbars> | |||
{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; |
@@ -0,0 +1,88 @@ | |||
@primary: #FA541C; | |||
span{ | |||
-moz-user-select: none; | |||
user-select: none | |||
} | |||
.tags_wrapper { | |||
position: relative; | |||
width: 100%; | |||
height: 34px; | |||
line-height: 34px; | |||
background: #fff; | |||
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.12), 0 0 1px 0 rgba(0, 0, 0, 0.01); | |||
.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; | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,169 @@ | |||
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'; | |||
// export type TagsItemType = { | |||
// title?: string; | |||
// path?: string; | |||
// active: boolean; | |||
// query?: any; | |||
// children: any; | |||
// refresh: number; | |||
// }; | |||
// interface IProps { | |||
// home: string; | |||
// current: string; | |||
// } | |||
/** | |||
* @component TagView 标签页组件 | |||
*/ | |||
const TagView = ({ children, home, current }) => { | |||
const [tagList, setTagList] = useState([]); | |||
const routeContextRef = useRef(); | |||
useEffect(() => { | |||
if (routeContextRef?.current) { | |||
handleOnChange(routeContextRef.current); | |||
} | |||
}, [current]); | |||
// 初始化 visitedViews,设置project为首页 | |||
const initTags = (routeContext) => { | |||
if (tagList.length === 0 && routeContext.menuData) { | |||
console.log('routeContext.menuData',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 }); | |||
console.log(children); | |||
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; |
@@ -0,0 +1,11 @@ | |||
.tag_view { | |||
.tags_container { | |||
position: relative; | |||
top: -22px; | |||
right: 24px; | |||
z-index: 199; | |||
width: calc(100% + 48px); | |||
height: 36px; | |||
border: 0px dashed coral; | |||
} | |||
} |