diff --git a/src/assets/css/components/common/sidebar.scss b/src/assets/css/components/common/sidebar.scss new file mode 100644 index 0000000..b94d972 --- /dev/null +++ b/src/assets/css/components/common/sidebar.scss @@ -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; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/assets/css/pages/tools-framework.scss b/src/assets/css/pages/tools-framework.scss index f9e08cf..9906a03 100644 --- a/src/assets/css/pages/tools-framework.scss +++ b/src/assets/css/pages/tools-framework.scss @@ -6,295 +6,7 @@ body { } .left-panel { - display: flex; - flex-direction: column; - width: clamp(180px, 20vw, 240px); 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 { diff --git a/src/components/common/sidebar/SidebarFooter.tsx b/src/components/common/sidebar/SidebarFooter.tsx new file mode 100644 index 0000000..55e1d24 --- /dev/null +++ b/src/components/common/sidebar/SidebarFooter.tsx @@ -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 ( +
+ + + + + + +
+ ) +} + +export default SidebarFooter diff --git a/src/components/common/sidebar/SidebarItem.tsx b/src/components/common/sidebar/SidebarItem.tsx new file mode 100644 index 0000000..4fff29d --- /dev/null +++ b/src/components/common/sidebar/SidebarItem.tsx @@ -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 = (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 ( +
  • +
    + + isPending ? 'pending' : isActive ? 'active' : '' + } + > +
    + {props.icon ? ( + + ) : undefined} +
    + {props.text} +
    +
    + {props.children ? ( + + {props.children} + + ) : undefined} +
  • + ) +} + +export default SidebarItem diff --git a/src/components/common/sidebar/SidebarItemList.tsx b/src/components/common/sidebar/SidebarItemList.tsx new file mode 100644 index 0000000..42337e1 --- /dev/null +++ b/src/components/common/sidebar/SidebarItemList.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +const SidebarItemList: React.FC = (props) => { + return +} + +export default SidebarItemList diff --git a/src/components/common/sidebar/SidebarScroll.tsx b/src/components/common/sidebar/SidebarScroll.tsx new file mode 100644 index 0000000..8690664 --- /dev/null +++ b/src/components/common/sidebar/SidebarScroll.tsx @@ -0,0 +1,34 @@ +import React, { useImperativeHandle } from 'react' +import HideScrollbar, { HideScrollbarElement } from '@/components/common/HideScrollbar' + +export interface SidebarScrollElement { + refreshLayout(): void +} + +const SidebarScroll = forwardRef((props, ref) => { + useImperativeHandle(ref, () => { + return { + refreshLayout() { + hideScrollbarRef.current?.refreshLayout() + } + } + }) + + const hideScrollbarRef = useRef(null) + + return ( +
    + + {props.children} + +
    + ) +}) + +export default SidebarScroll diff --git a/src/components/common/sidebar/SidebarSeparate.tsx b/src/components/common/sidebar/SidebarSeparate.tsx new file mode 100644 index 0000000..3887824 --- /dev/null +++ b/src/components/common/sidebar/SidebarSeparate.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +const SidebarSeparate: React.FC< + React.DetailedHTMLProps, HTMLDivElement> +> = (props) => { + const { className, ..._props } = props + + return
    +} + +export default SidebarSeparate diff --git a/src/components/common/sidebar/SidebarSubmenu.tsx b/src/components/common/sidebar/SidebarSubmenu.tsx new file mode 100644 index 0000000..ed2039b --- /dev/null +++ b/src/components/common/sidebar/SidebarSubmenu.tsx @@ -0,0 +1,22 @@ +import React from 'react' + +interface SidebarSubmenuProps extends React.PropsWithChildren { + submenuTop: number + submenuLeft: number +} + +const SidebarSubmenu: React.FC = (props) => { + return ( +
      +
      {props.children}
      +
    + ) +} + +export default SidebarSubmenu diff --git a/src/components/common/sidebar/index.tsx b/src/components/common/sidebar/index.tsx new file mode 100644 index 0000000..41f1006 --- /dev/null +++ b/src/components/common/sidebar/index.tsx @@ -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 = (props) => { + const [hideSidebar, setHideSidebar] = useState(getLocalStorage('hideSidebar') === 'false') + + const switchSidebar = () => { + setHideSidebar(!hideSidebar) + setLocalStorage('hideSidebar', hideSidebar ? 'true' : 'false') + props.onSidebarSwitch && props.onSidebarSwitch(hideSidebar) + } + + return ( + <> +
    +
    + + + + {props.title} +
    + +
    {props.children}
    + + +
    + + ) +} + +export default Sidebar diff --git a/src/pages/ToolsFramework.tsx b/src/pages/ToolsFramework.tsx index 27d51cc..8930fd5 100644 --- a/src/pages/ToolsFramework.tsx +++ b/src/pages/ToolsFramework.tsx @@ -1,251 +1,69 @@ 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 HideScrollbar, { HideScrollbarElement } from '@/components/common/HideScrollbar' -import { getLocalStorage, getRedirectUrl, setLocalStorage } from '@/utils/common' -import { getLoginStatus, logout } from '@/utils/auth' -import { NavLink, Outlet } from 'react-router-dom' +import '@/assets/css/pages/tools-framework.scss' +import FitFullScreen from '@/components/common/FitFullScreen' +import SidebarScroll, { SidebarScrollElement } from '@/components/common/sidebar/SidebarScroll' +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 matches = useMatches() - const lastMatch = matches.reduce((_, second) => second) - const location = useLocation() - const navigate = useNavigate() - const hideScrollbarRef = useRef(null) - const [submenuTop, setSubmenuTop] = useState(0) - const [submenuLeft, setSubmenuLeft] = useState(0) - const [hideSidebar, setHideSidebar] = useState(getLocalStorage('hideSidebar') === 'false') - const [exiting, setExiting] = useState(false) + const sidebarScrollRef = useRef(null) - const switchSidebar = () => { - setHideSidebar(!hideSidebar) - setLocalStorage('hideSidebar', hideSidebar ? 'true' : 'false') + const handleOnSidebarSwitch = () => { setTimeout(() => { - hideScrollbarRef.current?.refreshLayout() + sidebarScrollRef.current?.refreshLayout() }, 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 ( <> -
    -
    - - - - 氮工具 -
    -
    -
    -
      -
    • -
      - - isPending ? 'pending' : isActive ? 'active' : '' - } - > -
      - {tools[0].icon ? ( - - ) : undefined} -
      - {tools[0].name} -
      -
      -
    • -
    • -
      - - isPending ? ' pending' : isActive ? ' active' : '' - } - > -
      - {tools[1].icon ? ( - - ) : undefined} -
      - {tools[1].name} -
      -
      -
    • -
    • -
      -
    • -
    -
    - -
      - {tools.map((tool) => { - return tool.menu && - tool.id !== 'tools' && - tool.id !== 'tools-all' ? ( -
    • -
      - - isPending - ? 'pending' - : isActive - ? 'active' - : '' - } - > -
      - {tool.icon ? ( - - ) : undefined} -
      - {tool.name} -
      -
      - {tool.children ? ( -
        -
        - {tool.children.map((subTool) => { - return subTool.menu ? ( -
      • - - isPending - ? 'pending' - : isActive - ? 'active' - : '' - } - > - - {subTool.name} - - -
      • - ) : undefined - })} -
        -
      - ) : undefined} -
    • - ) : undefined - })} -
    -
    -
    -
    -
    -
    - - - - - - -
    +
    + + + + + + + + + {tools.map((tool) => { + return tool.menu && + tool.id !== 'tools' && + tool.id !== 'tools-all' ? ( + + {tool.children + ? tool.children.map((subTool) => { + return ( + + ) + }) + : undefined} + + ) : undefined + })} + + +