Add Sidebar component
This commit is contained in:
301
src/assets/css/components/common/sidebar.scss
Normal file
301
src/assets/css/components/common/sidebar.scss
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
@use "@/assets/css/constants" as constants;
|
||||||
|
@use "@/assets/css/mixins" as mixins;
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
transition: all .3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: constants.$main-color;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.icon-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: constants.$SIZE_ICON_SM;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
span {
|
||||||
|
transform: rotateZ(180deg);
|
||||||
|
transition: all .3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: constants.$background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 2em;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
transform: translateX(0.1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.scroll {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
> li {
|
||||||
|
&.item {
|
||||||
|
position: relative;
|
||||||
|
margin: 4px 14px;
|
||||||
|
font-size: 1.4em;
|
||||||
|
|
||||||
|
>.menu-bt {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
.icon-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: constants.$SIZE_ICON_SM;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: constants.$origin-color;
|
||||||
|
background-color: constants.$main-color !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
padding-left: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 10px 10px;
|
||||||
|
background-color: constants.$origin-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
border-radius: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 16px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: constants.$origin-color;
|
||||||
|
background-color: constants.$main-color !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover a {
|
||||||
|
background-color: constants.$background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
>.menu-bt {
|
||||||
|
a {
|
||||||
|
background-color: constants.$background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu {
|
||||||
|
display: block;
|
||||||
|
animation: 0.3s ease;
|
||||||
|
@include mixins.unique-keyframes {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separate {
|
||||||
|
height: 0;
|
||||||
|
margin: 10px 5px;
|
||||||
|
border: {
|
||||||
|
width: 1px;
|
||||||
|
color: constants.$font-secondary-color;
|
||||||
|
style: solid;
|
||||||
|
};
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 8px 14px;
|
||||||
|
color: constants.$main-color;
|
||||||
|
|
||||||
|
.icon-user {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: constants.$SIZE_ICON_XS;
|
||||||
|
border: 2px constants.$font-secondary-color solid;
|
||||||
|
color: constants.$font-secondary-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 1.4em;
|
||||||
|
color: constants.$font-main-color;
|
||||||
|
user-select: text;
|
||||||
|
|
||||||
|
a{
|
||||||
|
color: constants.$main-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-exit {
|
||||||
|
font-size: constants.$SIZE_ICON_XS;
|
||||||
|
color: constants.$error-color;
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: constants.$background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hide {
|
||||||
|
width: 68px !important;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
.icon-box {
|
||||||
|
span {
|
||||||
|
transform: rotateZ(360deg);
|
||||||
|
transition: all .3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-bt {
|
||||||
|
.text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu {
|
||||||
|
.menu-bt {
|
||||||
|
.text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
.text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-exit {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
padding-left: 6px;
|
||||||
|
left: 100%;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: constants.$origin-color;
|
||||||
|
|
||||||
|
.icon-exit {
|
||||||
|
padding: 4px 8px;
|
||||||
|
&:hover {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: constants.$background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hide {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .submenu-exit {
|
||||||
|
display: block;
|
||||||
|
animation: 0.3s ease;
|
||||||
|
@include mixins.unique-keyframes {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,295 +6,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.left-panel {
|
.left-panel {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: clamp(180px, 20vw, 240px);
|
|
||||||
background-color: constants.$origin-color;
|
background-color: constants.$origin-color;
|
||||||
user-select: none;
|
|
||||||
transition: all .3s;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 10px 14px;
|
|
||||||
color: constants.$main-color;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.icon-box {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
font-size: constants.$SIZE_ICON_SM;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
span {
|
|
||||||
transform: rotateZ(180deg);
|
|
||||||
transition: all .3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: constants.$background-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 2em;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: 0.6em;
|
|
||||||
transform: translateX(0.3em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
min-height: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.toolsMenu {
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
> li {
|
|
||||||
&.item {
|
|
||||||
position: relative;
|
|
||||||
margin: 4px 14px;
|
|
||||||
font-size: 1.4em;
|
|
||||||
|
|
||||||
.menu-bt {
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 40px;
|
|
||||||
|
|
||||||
.icon-box {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 10px;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
font-size: constants.$SIZE_ICON_SM;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
flex: 1;
|
|
||||||
padding-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: constants.$origin-color;
|
|
||||||
background-color: constants.$main-color !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submenu {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
padding-left: 20px;
|
|
||||||
z-index: 10000;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
padding: 10px 10px;
|
|
||||||
background-color: constants.$origin-color;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
.item {
|
|
||||||
border-radius: 8px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
padding: 8px 16px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: constants.$origin-color;
|
|
||||||
background-color: constants.$main-color !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover a {
|
|
||||||
background-color: constants.$background-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.menu-bt {
|
|
||||||
a {
|
|
||||||
background-color: constants.$background-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submenu {
|
|
||||||
display: block;
|
|
||||||
animation: 0.3s ease;
|
|
||||||
@include mixins.unique-keyframes {
|
|
||||||
0% {
|
|
||||||
transform: translateX(-10px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.separate {
|
|
||||||
height: 0;
|
|
||||||
margin: 10px 5px;
|
|
||||||
border: {
|
|
||||||
width: 1px;
|
|
||||||
color: constants.$font-secondary-color;
|
|
||||||
style: solid;
|
|
||||||
};
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 8px 14px;
|
|
||||||
color: constants.$main-color;
|
|
||||||
|
|
||||||
.icon-user {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
font-size: constants.$SIZE_ICON_XS;
|
|
||||||
border: 2px constants.$font-secondary-color solid;
|
|
||||||
color: constants.$font-secondary-color;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
flex: 1;
|
|
||||||
padding-left: 10px;
|
|
||||||
font-size: 1.4em;
|
|
||||||
color: constants.$font-main-color;
|
|
||||||
user-select: text;
|
|
||||||
|
|
||||||
a{
|
|
||||||
color: constants.$main-color;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-exit {
|
|
||||||
font-size: constants.$SIZE_ICON_XS;
|
|
||||||
color: constants.$error-color;
|
|
||||||
padding: 6px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: constants.$background-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hide {
|
|
||||||
width: 68px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
.icon-box {
|
|
||||||
span {
|
|
||||||
transform: rotateZ(360deg);
|
|
||||||
transition: all .3s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-bt {
|
|
||||||
.text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
position: relative;
|
|
||||||
.text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submenu-exit {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
padding-left: 6px;
|
|
||||||
left: 100%;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: constants.$origin-color;
|
|
||||||
|
|
||||||
.icon-exit {
|
|
||||||
padding: 4px 8px;
|
|
||||||
&:hover {
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: constants.$background-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hide {
|
|
||||||
display: none!important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .submenu-exit {
|
|
||||||
display: block;
|
|
||||||
animation: 0.3s ease;
|
|
||||||
@include mixins.unique-keyframes {
|
|
||||||
0% {
|
|
||||||
transform: translateX(-10px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-panel {
|
.right-panel {
|
||||||
|
|||||||
64
src/components/common/sidebar/SidebarFooter.tsx
Normal file
64
src/components/common/sidebar/SidebarFooter.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Icon from '@ant-design/icons'
|
||||||
|
import { getLoginStatus, logout } from '@/utils/auth'
|
||||||
|
import { getRedirectUrl } from '@/utils/common'
|
||||||
|
|
||||||
|
const SidebarFooter: React.FC = () => {
|
||||||
|
const matches = useMatches()
|
||||||
|
const lastMatch = matches.reduce((_, second) => second)
|
||||||
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [exiting, setExiting] = useState(false)
|
||||||
|
const handleClickAvatar = () => {
|
||||||
|
if (getLoginStatus()) {
|
||||||
|
navigate('/user')
|
||||||
|
} else {
|
||||||
|
navigate(getRedirectUrl('/login', `${lastMatch.pathname}${location.search}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
if (exiting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setExiting(true)
|
||||||
|
void logout().finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 1500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'footer'}>
|
||||||
|
<span className={'icon-user'} onClick={handleClickAvatar}>
|
||||||
|
<Icon component={IconFatwebUser} />
|
||||||
|
</span>
|
||||||
|
<span hidden={getLoginStatus()} className={'text'}>
|
||||||
|
未
|
||||||
|
<NavLink to={getRedirectUrl('/login', `${lastMatch.pathname}${location.search}`)}>
|
||||||
|
登录
|
||||||
|
</NavLink>
|
||||||
|
</span>
|
||||||
|
<span hidden={!getLoginStatus()} className={'text'}>
|
||||||
|
已登录
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
hidden={!getLoginStatus()}
|
||||||
|
className={`submenu-exit${!getLoginStatus() ? ' hide' : ''}`}
|
||||||
|
>
|
||||||
|
<div className={'content'}>
|
||||||
|
<span hidden={!getLoginStatus()} className={'icon-exit'} onClick={handleLogout}>
|
||||||
|
<Icon
|
||||||
|
component={exiting ? IconFatwebLoading : IconFatwebExit}
|
||||||
|
spin={exiting}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarFooter
|
||||||
60
src/components/common/sidebar/SidebarItem.tsx
Normal file
60
src/components/common/sidebar/SidebarItem.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Icon from '@ant-design/icons'
|
||||||
|
import SidebarSubmenu from '@/components/common/sidebar/SidebarSubmenu'
|
||||||
|
|
||||||
|
type ItemProps = {
|
||||||
|
icon?: IconComponent
|
||||||
|
text?: string
|
||||||
|
path: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarItem: React.FC<ItemProps> = (props) => {
|
||||||
|
const [submenuTop, setSubmenuTop] = useState(0)
|
||||||
|
const [submenuLeft, setSubmenuLeft] = useState(0)
|
||||||
|
|
||||||
|
const showSubmenu = (e: React.MouseEvent) => {
|
||||||
|
const parentElement = e.currentTarget.parentElement
|
||||||
|
if (parentElement && parentElement.childElementCount === 2) {
|
||||||
|
const parentClientRect = parentElement.getBoundingClientRect()
|
||||||
|
if (parentClientRect.top <= screen.height / 2) {
|
||||||
|
setSubmenuTop(parentClientRect.top)
|
||||||
|
} else {
|
||||||
|
setSubmenuTop(
|
||||||
|
parentClientRect.top -
|
||||||
|
(parentElement.lastElementChild?.clientHeight ?? 0) +
|
||||||
|
e.currentTarget.clientHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setSubmenuLeft(parentClientRect.right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={'item'}>
|
||||||
|
<div className={'menu-bt'} onMouseEnter={showSubmenu}>
|
||||||
|
<NavLink
|
||||||
|
to={props.path}
|
||||||
|
end
|
||||||
|
className={({ isActive, isPending }) =>
|
||||||
|
isPending ? 'pending' : isActive ? 'active' : ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={'icon-box'}>
|
||||||
|
{props.icon ? (
|
||||||
|
<Icon className={'icon'} component={props.icon} />
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
<span className={'text'}>{props.text}</span>
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
{props.children ? (
|
||||||
|
<SidebarSubmenu submenuTop={submenuTop} submenuLeft={submenuLeft}>
|
||||||
|
{props.children}
|
||||||
|
</SidebarSubmenu>
|
||||||
|
) : undefined}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarItem
|
||||||
7
src/components/common/sidebar/SidebarItemList.tsx
Normal file
7
src/components/common/sidebar/SidebarItemList.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const SidebarItemList: React.FC<React.PropsWithChildren> = (props) => {
|
||||||
|
return <ul>{props.children}</ul>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarItemList
|
||||||
34
src/components/common/sidebar/SidebarScroll.tsx
Normal file
34
src/components/common/sidebar/SidebarScroll.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { useImperativeHandle } from 'react'
|
||||||
|
import HideScrollbar, { HideScrollbarElement } from '@/components/common/HideScrollbar'
|
||||||
|
|
||||||
|
export interface SidebarScrollElement {
|
||||||
|
refreshLayout(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarScroll = forwardRef<SidebarScrollElement, React.PropsWithChildren>((props, ref) => {
|
||||||
|
useImperativeHandle<SidebarScrollElement, SidebarScrollElement>(ref, () => {
|
||||||
|
return {
|
||||||
|
refreshLayout() {
|
||||||
|
hideScrollbarRef.current?.refreshLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hideScrollbarRef = useRef<HideScrollbarElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'scroll'}>
|
||||||
|
<HideScrollbar
|
||||||
|
isShowVerticalScrollbar={true}
|
||||||
|
scrollbarWidth={2}
|
||||||
|
animationTransitionTime={300}
|
||||||
|
autoHideWaitingTime={800}
|
||||||
|
ref={hideScrollbarRef}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</HideScrollbar>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default SidebarScroll
|
||||||
11
src/components/common/sidebar/SidebarSeparate.tsx
Normal file
11
src/components/common/sidebar/SidebarSeparate.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const SidebarSeparate: React.FC<
|
||||||
|
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
|
||||||
|
> = (props) => {
|
||||||
|
const { className, ..._props } = props
|
||||||
|
|
||||||
|
return <div className={`separate ${className ? ` ${className}` : ''}`} {..._props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarSeparate
|
||||||
22
src/components/common/sidebar/SidebarSubmenu.tsx
Normal file
22
src/components/common/sidebar/SidebarSubmenu.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface SidebarSubmenuProps extends React.PropsWithChildren {
|
||||||
|
submenuTop: number
|
||||||
|
submenuLeft: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarSubmenu: React.FC<SidebarSubmenuProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
className={'submenu'}
|
||||||
|
style={{
|
||||||
|
top: props.submenuTop,
|
||||||
|
left: props.submenuLeft
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={'content'}>{props.children}</div>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarSubmenu
|
||||||
44
src/components/common/sidebar/index.tsx
Normal file
44
src/components/common/sidebar/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Icon from '@ant-design/icons'
|
||||||
|
import '@/assets/css/components/common/sidebar.scss'
|
||||||
|
import SidebarSeparate from '@/components/common/sidebar/SidebarSeparate'
|
||||||
|
import SidebarFooter from '@/components/common/sidebar/SidebarFooter'
|
||||||
|
import { getLocalStorage, setLocalStorage } from '@/utils/common'
|
||||||
|
|
||||||
|
interface SidebarProps extends React.PropsWithChildren {
|
||||||
|
title: string
|
||||||
|
width?: string
|
||||||
|
onSidebarSwitch?: (hidden: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||||
|
const [hideSidebar, setHideSidebar] = useState(getLocalStorage('hideSidebar') === 'false')
|
||||||
|
|
||||||
|
const switchSidebar = () => {
|
||||||
|
setHideSidebar(!hideSidebar)
|
||||||
|
setLocalStorage('hideSidebar', hideSidebar ? 'true' : 'false')
|
||||||
|
props.onSidebarSwitch && props.onSidebarSwitch(hideSidebar)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`sidebar${hideSidebar ? ' hide' : ''}`}
|
||||||
|
style={{ width: props.width ?? 'clamp(180px, 20vw, 240px)' }}
|
||||||
|
>
|
||||||
|
<div className={'title'}>
|
||||||
|
<span className={'icon-box'} onClick={switchSidebar}>
|
||||||
|
<Icon component={IconFatwebExpand} />
|
||||||
|
</span>
|
||||||
|
<span className={'text'}>{props.title}</span>
|
||||||
|
</div>
|
||||||
|
<SidebarSeparate style={{ marginTop: 0 }} />
|
||||||
|
<div className={'content'}>{props.children}</div>
|
||||||
|
<SidebarSeparate style={{ marginTop: 0, marginBottom: 0 }} />
|
||||||
|
<SidebarFooter />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidebar
|
||||||
@@ -1,251 +1,69 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import FitFullScreen from '@/components/common/FitFullScreen'
|
|
||||||
import '@/assets/css/pages/tools-framework.scss'
|
|
||||||
import Icon from '@ant-design/icons'
|
|
||||||
import { tools } from '@/router/tools'
|
import { tools } from '@/router/tools'
|
||||||
import HideScrollbar, { HideScrollbarElement } from '@/components/common/HideScrollbar'
|
import '@/assets/css/pages/tools-framework.scss'
|
||||||
import { getLocalStorage, getRedirectUrl, setLocalStorage } from '@/utils/common'
|
import FitFullScreen from '@/components/common/FitFullScreen'
|
||||||
import { getLoginStatus, logout } from '@/utils/auth'
|
import SidebarScroll, { SidebarScrollElement } from '@/components/common/sidebar/SidebarScroll'
|
||||||
import { NavLink, Outlet } from 'react-router-dom'
|
import Sidebar from '@/components/common/sidebar'
|
||||||
|
import SidebarItemList from '@/components/common/sidebar/SidebarItemList'
|
||||||
|
import SidebarItem from '@/components/common/sidebar/SidebarItem'
|
||||||
|
import SidebarSeparate from '@/components/common/sidebar/SidebarSeparate'
|
||||||
|
|
||||||
const ToolsFramework: React.FC = () => {
|
const ToolsFramework: React.FC = () => {
|
||||||
const matches = useMatches()
|
const sidebarScrollRef = useRef<SidebarScrollElement>(null)
|
||||||
const lastMatch = matches.reduce((_, second) => second)
|
|
||||||
const location = useLocation()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const hideScrollbarRef = useRef<HideScrollbarElement>(null)
|
|
||||||
const [submenuTop, setSubmenuTop] = useState(0)
|
|
||||||
const [submenuLeft, setSubmenuLeft] = useState(0)
|
|
||||||
const [hideSidebar, setHideSidebar] = useState(getLocalStorage('hideSidebar') === 'false')
|
|
||||||
const [exiting, setExiting] = useState(false)
|
|
||||||
|
|
||||||
const switchSidebar = () => {
|
const handleOnSidebarSwitch = () => {
|
||||||
setHideSidebar(!hideSidebar)
|
|
||||||
setLocalStorage('hideSidebar', hideSidebar ? 'true' : 'false')
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hideScrollbarRef.current?.refreshLayout()
|
sidebarScrollRef.current?.refreshLayout()
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showSubmenu = (e: React.MouseEvent) => {
|
|
||||||
const parentElement = e.currentTarget.parentElement
|
|
||||||
if (parentElement && parentElement.childElementCount === 2) {
|
|
||||||
const parentClientRect = parentElement.getBoundingClientRect()
|
|
||||||
if (parentClientRect.top <= screen.height / 2) {
|
|
||||||
setSubmenuTop(parentClientRect.top)
|
|
||||||
} else {
|
|
||||||
setSubmenuTop(
|
|
||||||
parentClientRect.top -
|
|
||||||
(parentElement.lastElementChild?.clientHeight ?? 0) +
|
|
||||||
e.currentTarget.clientHeight
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setSubmenuLeft(parentClientRect.right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClickAvatar = () => {
|
|
||||||
if (getLoginStatus()) {
|
|
||||||
navigate(`${lastMatch.pathname}${location.search}`)
|
|
||||||
} else {
|
|
||||||
navigate(getRedirectUrl('/login', `${lastMatch.pathname}${location.search}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
if (exiting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setExiting(true)
|
|
||||||
void logout().finally(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload()
|
|
||||||
}, 1500)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FitFullScreen className={'flex-horizontal'}>
|
<FitFullScreen className={'flex-horizontal'}>
|
||||||
<div className={`left-panel${hideSidebar ? ' hide' : ''}`}>
|
<div className={'left-panel'}>
|
||||||
<div className={'title'}>
|
<Sidebar title={'氮工具'} onSidebarSwitch={handleOnSidebarSwitch}>
|
||||||
<span className={'icon-box'} onClick={switchSidebar}>
|
<SidebarItemList>
|
||||||
<Icon component={IconFatwebExpand} />
|
<SidebarItem
|
||||||
</span>
|
path={''}
|
||||||
<span className={'text'}>氮工具</span>
|
icon={tools[0].icon}
|
||||||
</div>
|
text={tools[0].name}
|
||||||
<div style={{ marginTop: '0' }} className={'separate'} />
|
></SidebarItem>
|
||||||
<div className={'content'}>
|
<SidebarItem
|
||||||
<ul>
|
path={'all'}
|
||||||
<li className={'item'}>
|
icon={tools[1].icon}
|
||||||
<div className={'menu-bt'}>
|
text={tools[1].name}
|
||||||
<NavLink
|
></SidebarItem>
|
||||||
to={''}
|
</SidebarItemList>
|
||||||
end
|
<SidebarSeparate style={{ marginBottom: 0 }} />
|
||||||
className={({ isActive, isPending }) =>
|
<SidebarScroll ref={sidebarScrollRef}>
|
||||||
isPending ? 'pending' : isActive ? 'active' : ''
|
<SidebarItemList>
|
||||||
}
|
{tools.map((tool) => {
|
||||||
>
|
return tool.menu &&
|
||||||
<div className={'icon-box'}>
|
tool.id !== 'tools' &&
|
||||||
{tools[0].icon ? (
|
tool.id !== 'tools-all' ? (
|
||||||
<Icon
|
<SidebarItem
|
||||||
className={'icon'}
|
path={tool.path}
|
||||||
component={tools[0].icon}
|
icon={tool.icon}
|
||||||
/>
|
text={tool.name}
|
||||||
) : undefined}
|
key={tool.id}
|
||||||
</div>
|
>
|
||||||
<span className={'text'}>{tools[0].name}</span>
|
{tool.children
|
||||||
</NavLink>
|
? tool.children.map((subTool) => {
|
||||||
</div>
|
return (
|
||||||
</li>
|
<SidebarItem
|
||||||
<li className={'item'}>
|
path={`${tool.path}/${subTool.path}`}
|
||||||
<div className={'menu-bt'}>
|
text={subTool.name}
|
||||||
<NavLink
|
key={tool.id}
|
||||||
to={'all'}
|
/>
|
||||||
className={({ isActive, isPending }) =>
|
)
|
||||||
isPending ? ' pending' : isActive ? ' active' : ''
|
})
|
||||||
}
|
: undefined}
|
||||||
>
|
</SidebarItem>
|
||||||
<div className={'icon-box'}>
|
) : undefined
|
||||||
{tools[1].icon ? (
|
})}
|
||||||
<Icon
|
</SidebarItemList>
|
||||||
className={'icon'}
|
</SidebarScroll>
|
||||||
component={tools[1].icon}
|
</Sidebar>
|
||||||
/>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
|
||||||
<span className={'text'}>{tools[1].name}</span>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div className={'separate'} style={{ marginBottom: 0 }} />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div className={'toolsMenu'}>
|
|
||||||
<HideScrollbar
|
|
||||||
isShowVerticalScrollbar={true}
|
|
||||||
scrollbarWidth={2}
|
|
||||||
animationTransitionTime={300}
|
|
||||||
autoHideWaitingTime={800}
|
|
||||||
ref={hideScrollbarRef}
|
|
||||||
>
|
|
||||||
<ul>
|
|
||||||
{tools.map((tool) => {
|
|
||||||
return tool.menu &&
|
|
||||||
tool.id !== 'tools' &&
|
|
||||||
tool.id !== 'tools-all' ? (
|
|
||||||
<li className={'item'} key={tool.id}>
|
|
||||||
<div
|
|
||||||
className={'menu-bt'}
|
|
||||||
onMouseEnter={showSubmenu}
|
|
||||||
>
|
|
||||||
<NavLink
|
|
||||||
to={tool.path}
|
|
||||||
className={({ isActive, isPending }) =>
|
|
||||||
isPending
|
|
||||||
? 'pending'
|
|
||||||
: isActive
|
|
||||||
? 'active'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={'icon-box'}>
|
|
||||||
{tool.icon ? (
|
|
||||||
<Icon
|
|
||||||
className={'icon'}
|
|
||||||
component={tool.icon}
|
|
||||||
/>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
|
||||||
<span className={'text'}>{tool.name}</span>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
{tool.children ? (
|
|
||||||
<ul
|
|
||||||
className={'submenu'}
|
|
||||||
style={{
|
|
||||||
top: submenuTop,
|
|
||||||
left: submenuLeft
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={'content'}>
|
|
||||||
{tool.children.map((subTool) => {
|
|
||||||
return subTool.menu ? (
|
|
||||||
<li
|
|
||||||
className={'item'}
|
|
||||||
key={subTool.id}
|
|
||||||
>
|
|
||||||
<NavLink
|
|
||||||
to={`${tool.path}/${subTool.path}`}
|
|
||||||
className={({
|
|
||||||
isActive,
|
|
||||||
isPending
|
|
||||||
}) =>
|
|
||||||
isPending
|
|
||||||
? 'pending'
|
|
||||||
: isActive
|
|
||||||
? 'active'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={'text'}
|
|
||||||
>
|
|
||||||
{subTool.name}
|
|
||||||
</span>
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
) : undefined
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
) : undefined}
|
|
||||||
</li>
|
|
||||||
) : undefined
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</HideScrollbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={'separate'} style={{ marginTop: 0, marginBottom: 0 }} />
|
|
||||||
<div className={'footer'}>
|
|
||||||
<span className={'icon-user'} onClick={handleClickAvatar}>
|
|
||||||
<Icon component={IconFatwebUser} />
|
|
||||||
</span>
|
|
||||||
<span hidden={getLoginStatus()} className={'text'}>
|
|
||||||
未
|
|
||||||
<NavLink
|
|
||||||
to={getRedirectUrl(
|
|
||||||
'/login',
|
|
||||||
`${lastMatch.pathname}${location.search}`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
登录
|
|
||||||
</NavLink>
|
|
||||||
</span>
|
|
||||||
<span hidden={!getLoginStatus()} className={'text'}>
|
|
||||||
已登录
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
hidden={!getLoginStatus()}
|
|
||||||
className={`submenu-exit${!getLoginStatus() ? ' hide' : ''}`}
|
|
||||||
>
|
|
||||||
<div className={'content'}>
|
|
||||||
<span
|
|
||||||
hidden={!getLoginStatus()}
|
|
||||||
className={'icon-exit'}
|
|
||||||
onClick={handleLogout}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
component={exiting ? IconFatwebLoading : IconFatwebExit}
|
|
||||||
spin={exiting}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={'right-panel'}>
|
<div className={'right-panel'}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
Reference in New Issue
Block a user