diff --git a/package.json b/package.json index b6fbda4..7c7f48b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app.jsx b/src/app.jsx index ec7736d..e623305 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -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 ( + + {/* {children} */} + + ); + }, // 自定义 403 页面 // unAccessible:
unAccessible
, ...initialState?.settings, diff --git a/src/components/TagView/Tags/index.jsx b/src/components/TagView/Tags/index.jsx new file mode 100644 index 0000000..0d8e504 --- /dev/null +++ b/src/components/TagView/Tags/index.jsx @@ -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 ( +
+ + {tagList.map((item, i) => { + if (item.path == history.location.pathname) { + item.active = true; + } else { + item.active = false; + } + return ( +
{ + e.stopPropagation(); + setCurrentPath(item.path); + history.push({ pathname: item.path, query: item.query }); + }} + onContextMenu={(e) => openContextMenu(e, item)} + > + {item.title} + {i !== 0 && ( + { + e.stopPropagation(); + closeTag && closeTag(item); + }} + /> + )} +
+ ); + })} +
+ {menuVisible ? ( + + ) : null} +
+ ); +}; + +export default Tags; diff --git a/src/components/TagView/Tags/index.less b/src/components/TagView/Tags/index.less new file mode 100644 index 0000000..ff81c16 --- /dev/null +++ b/src/components/TagView/Tags/index.less @@ -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; + } + } + } +} diff --git a/src/components/TagView/index.jsx b/src/components/TagView/index.jsx new file mode 100644 index 0000000..e0821c8 --- /dev/null +++ b/src/components/TagView/index.jsx @@ -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 ( + <> + + {(value) => { + // console.log(value); + routeContextRef.current = value; + return null; + }} + +
+
+ +
+
+ + {tagList.map((item) => { + return ( +
+
+ {item.children} +
+
+ ); + })} + + ); +}; + +export default TagView; diff --git a/src/components/TagView/index.less b/src/components/TagView/index.less new file mode 100644 index 0000000..bf1ec75 --- /dev/null +++ b/src/components/TagView/index.less @@ -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; + } +}