添加导航栏,管理后台背景等

This commit is contained in:
Jerry 2025-06-11 18:20:00 +08:00
parent 7031f7b251
commit 5917e26247
23 changed files with 333 additions and 167 deletions

View File

@ -11,6 +11,5 @@
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Python 3.10 interpreter library" level="application" />
</component> </component>
</module> </module>

View File

@ -11,7 +11,6 @@
- Framer-motion - Framer-motion
- chakra-ui - chakra-ui
- HTML5 - HTML5
### 项目分工 ### 项目分工
- **后端api人工智能算法** : `3`(主要) & `log_out` & `Jerry`(maybe) 使用python - **后端api人工智能算法** : `3`(主要) & `log_out` & `Jerry`(maybe) 使用python
- **前端管理后台设计**`Jerry`使用react - **前端管理后台设计**`Jerry`使用react
@ -19,7 +18,9 @@
### 各部分说明 ### 各部分说明
[网页管理前端](https://github.com/Jerryplusy/AI-powered-switches/blob/main/src/frontend/README.md) [网页管理前端如下](https://github.com/Jerryplusy/AI-powered-switches/blob/main/src/frontend/README.md)
![img](./src/frontend/src/resources/img.png)
[逻辑处理后端](https://github.com/Jerryplusy/AI-powered-switches/blob/main/src/backend/README.md) [逻辑处理后端](https://github.com/Jerryplusy/AI-powered-switches/blob/main/src/backend/README.md)

View File

@ -8,4 +8,4 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View File

@ -1,13 +1,16 @@
import { Box, Image } from '@chakra-ui/react'; import { Box, Image } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import image from '@/resources/welcome/image/background.png'; import image from '@/resources/welcome/image/background.png';
const MotionBox = motion(Box);
/** /**
* 带高斯模糊的背景 * 带高斯模糊的背景
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
const BackgroundBlur = () => ( const BackgroundBlur = () => (
<Box <MotionBox
position={'absolute'} position={'absolute'}
top={0} top={0}
left={0} left={0}
@ -15,9 +18,12 @@ const BackgroundBlur = () => (
height={'100%'} height={'100%'}
filter={'blur(6px)'} filter={'blur(6px)'}
zIndex={0} zIndex={0}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, ease: 'easeInOut' }}
> >
<Image src={image} objectFit={'cover'} width={'100%'} height={'100%'} /> <Image src={image} objectFit={'cover'} width={'100%'} height={'100%'} />
</Box> </MotionBox>
); );
export default BackgroundBlur; export default BackgroundBlur;

View File

@ -12,7 +12,7 @@ const DashboardCard = () => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<FadeInWrapper delay={0.4} yOffset={-5}> <FadeInWrapper delay={0.4} yOffset={-5}>
<MotionCard icon={manageIcon} text="管理后台" onClick={() => navigate('/dashboard')} /> <MotionCard icon={manageIcon} text={'管理后台'} onClick={() => navigate('/dashboard')} />
</FadeInWrapper> </FadeInWrapper>
); );
}; };

View File

@ -13,6 +13,7 @@ const GithubCard = () => {
<MotionCard <MotionCard
icon={githubIcon} icon={githubIcon}
text={'Github'} text={'Github'}
hasBlurBackground={true}
onClick={() => window.open('https://github.com/Jerryplusy/AI-powered-switches', '_blank')} onClick={() => window.open('https://github.com/Jerryplusy/AI-powered-switches', '_blank')}
/> />
</FadeInWrapper> </FadeInWrapper>

View File

@ -9,10 +9,10 @@ import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
*/ */
const WelcomeContent = () => { const WelcomeContent = () => {
return ( return (
<VStack spacing={10} py={200} align="center" px={4}> <VStack spacing={10} py={200} align={'center'} px={4}>
<FadeInWrapper delay={0.2} yOffset={-5}> <FadeInWrapper delay={0.2} yOffset={-5}>
<Box textAlign="center"> <Box textAlign={'center'}>
<Heading size="6xl" fontWeight="black" color="teal.300"> <Heading size="6xl" fontWeight={'black'} color={'teal.300'}>
智能网络交换机 智能网络交换机
<br /> <br />
管理系统 管理系统
@ -20,8 +20,8 @@ const WelcomeContent = () => {
</Box> </Box>
</FadeInWrapper> </FadeInWrapper>
<FadeInWrapper delay={0.3} yOffset={-5}> <FadeInWrapper delay={0.3} yOffset={-5}>
<Box textAlign="center"> <Box textAlign={'center'}>
<Text mt={6} fontSize="2xl" color="gray.300"> <Text mt={6} fontSize={'2xl'} color={'gray.300'}>
助力大型网络交换机配置及网络流量管理方便的管控网络让网络配置不再困难 助力大型网络交换机配置及网络流量管理方便的管控网络让网络配置不再困难
</Text> </Text>
</Box> </Box>

View File

@ -1,21 +0,0 @@
import React from 'react';
import { Box, Flex, Heading, Spacer, Button, Card } from '@chakra-ui/react';
import { useNavigate } from 'react-router-dom';
import NavButton from '@/components/ui/NavButton';
const Header = () => {
const navigate = useNavigate();
return (
<Box bg={'teal.500'} px={4} py={2} color={'white'}>
<Flex align={'center'}>
<Heading size={'md'}>网络管理后台</Heading>
<Spacer />
<NavButton varint={'ghost'} color={''} onClick={() => navigate('/')}>
返回欢迎页
</NavButton>
</Flex>
</Box>
);
};
export default Header;

View File

@ -0,0 +1,17 @@
import { Box } from '@chakra-ui/react';
/**
* 解决导航栏占位问题
* @param children
* @returns {JSX.Element}
* @constructor
*/
const PageContainer = ({ children }) => {
return (
<Box pt={'60px'} px={6}>
{children}
</Box>
);
};
export default PageContainer;

View File

@ -2,6 +2,7 @@ import { Outlet, useLocation } from 'react-router-dom';
import PageTransition from './PageTransition'; import PageTransition from './PageTransition';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import GithubTransitionCard from '@/components/system/layout/github/GithubTransitionCard';
/** /**
* 应用加壳 * 应用加壳
@ -13,8 +14,9 @@ const AppShell = () => {
return ( return (
<Box position={'relative'} height={'100vh'} overflow={'hidden'}> <Box position={'relative'} height={'100vh'} overflow={'hidden'}>
<GithubTransitionCard />
<Box overflowY={'auto'} height={'100%'}> <Box overflowY={'auto'} height={'100%'}>
<AnimatePresence mode={'wait'}> <AnimatePresence mode={'sync'}>
<PageTransition key={location.pathname}> <PageTransition key={location.pathname}>
<Outlet /> <Outlet />
</PageTransition> </PageTransition>

View File

@ -1,58 +0,0 @@
import { useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Box, Text, Image } from '@chakra-ui/react';
import githubIcon from '@/resources/welcome/image/github.svg';
/**
* GitHub图标变换=>导航栏
* @returns {JSX.Element}
* @constructor
*/
const GithubNavTransition = () => {
const { pathname } = useLocation();
const isDashboard = pathname.startsWith('/dashboard');
return (
<AnimatePresence>
<motion.div
key={isDashboard ? 'nav' : 'icon'}
initial={{ opacity: 0, scale: 0.9 }}
animate={{
opacity: 1,
scale: 1,
right: 16,
top: 16,
width: isDashboard ? 160 : 50,
}}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.4 }}
style={{
position: 'absolute',
zIndex: 20,
}}
>
<Box
display={'flex'}
alignItems={'center'}
bg={'whiteAlpha.200'}
border={'1px solid'}
borderColor={'gray.600'}
px={isDashboard ? 4 : 2}
py={2}
borderRadius={'md'}
cursor={'pointer'}
onClick={() => window.open('https://github.com/Jerryplusy/AI-powered-switches', '_blank')}
>
<Image src={githubIcon} boxSize={5} mr={2} />
{isDashboard && (
<Text color={'white'} fontSize={'sm'}>
GitHub 项目主页
</Text>
)}
</Box>
</motion.div>
</AnimatePresence>
);
};
export default GithubNavTransition;

View File

@ -0,0 +1,103 @@
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { Button, HStack } from '@chakra-ui/react';
import web from '@/resources/icon/web.svg';
import githubIcon from '@/resources/welcome/image/github.svg';
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
import MotionCard from '@/components/ui/MotionCard';
const navItems = [
{ label: '面板', path: '/dashboard' },
{ label: '网络', path: '/dashboard/network' },
{ label: '交换机', path: '/dashboard/switch' },
];
/**
* 导航栏&github按钮组件
* @returns {JSX.Element}
* @constructor
*/
const GithubTransitionCard = () => {
const { pathname } = useLocation();
const navigate = useNavigate();
const isDashboard = pathname.startsWith('/dashboard');
const [showNavButtons, setShowNavButtons] = useState(false);
useEffect(() => {
setShowNavButtons(false);
const timer = setTimeout(() => {
if (isDashboard) setShowNavButtons(true);
}, 400);
return () => clearTimeout(timer);
}, [isDashboard]);
return (
<AnimatePresence mode={'wait'}>
<motion.div
key={isDashboard ? 'dashboard' : 'welcome'}
initial={{ opacity: 0, height: 'auto', width: isDashboard ? 200 : 'auto' }}
animate={{
opacity: 1,
height: isDashboard ? 64 : 'auto',
width: isDashboard ? '100%' : 'fit-content',
}}
exit={{ opacity: 0 }}
transition={{ duration: 0.4, ease: 'easeInOut' }}
style={{
position: 'fixed',
top: 10,
left: isDashboard ? 0 : 'auto',
right: isDashboard ? 0 : 16,
zIndex: 999,
padding: isDashboard ? '0 16px' : 0,
}}
>
<FadeInWrapper delay={0.1} yOffset={-10}>
<MotionCard
icon={isDashboard ? web : githubIcon}
text={isDashboard ? '控制台导航栏' : 'Github'}
hasBlurBackground
onClick={() => {
if (!isDashboard) {
window.open('https://github.com/Jerryplusy/AI-powered-switches', '_blank');
}
}}
justifyContent={isDashboard ? 'flex-start' : 'center'}
alignItems={'center'}
flexDirection={'row'}
w={'100%'}
px={isDashboard ? 4 : 3}
py={isDashboard ? 3 : 2}
noHover={isDashboard}
>
{isDashboard && showNavButtons && (
<HStack spacing={4} ml={'auto'}>
{navItems.map((item) => (
<Button
key={item.path}
size={'sm'}
variant={'ghost'}
color={'white'}
_hover={{
color: 'teal.300',
background: 'transparent',
}}
onClick={(e) => {
e.stopPropagation();
navigate(item.path);
}}
>
{item.label}
</Button>
))}
</HStack>
)}
</MotionCard>
</FadeInWrapper>
</motion.div>
</AnimatePresence>
);
};
export default GithubTransitionCard;

View File

@ -0,0 +1,61 @@
import React, { useState, useEffect } from 'react';
import { Box } from '@chakra-ui/react';
import { motion } from 'framer-motion';
const MotionBox = motion(Box);
/**
* 控制台背景
* @returns {JSX.Element}
* @constructor
*/
const DashboardBackground = () => {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setMousePos({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
const spotlight = {
background: `radial-gradient(
circle at ${mousePos.x}px ${mousePos.y}px,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 120px,
transparent 240px
)`,
};
return (
<MotionBox
position={{ base: 'fixed' }}
top={0}
left={0}
w={{ base: '100vw' }}
h={{ base: '100vh' }}
zIndex={-1}
background={{
base: 'linear-gradient(135deg, #18274C 0%, #21397F 50%, #1D3062 100%)',
}}
_after={{
content: { base: '""' },
position: { base: 'absolute' },
top: 0,
left: 0,
w: { base: '100%' },
h: { base: '100%' },
pointerEvents: { base: 'none' },
...spotlight,
transition: { base: 'background 0.2s ease' },
}}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.2, ease: 'easeInOut' }}
/>
);
};
export default DashboardBackground;

View File

@ -0,0 +1,10 @@
import { useEffect } from 'react';
const DocumentTitle = ({ title, children }) => {
useEffect(() => {
document.title = title || '网络管理后台';
}, [title]);
return children;
};
export default DocumentTitle;

View File

@ -4,19 +4,28 @@ import { motion } from 'framer-motion';
const MotionBox = motion(Box); const MotionBox = motion(Box);
/** /**
* 带有动作效果的卡片 * 卡片组件
* @param icon 可选图标 * @param icon 可选图标
* @param text 文字 * @param text 文字
* @param onClick 点击执行操作 * @param onClick 点击执行函数
* @param hasBlurBackground 是否模糊背景
* @param noHover 是否禁用 hover 动画
* @param children 子组件
* @param props
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
const MotionCard = ({ icon, text, onClick }) => ( const MotionCard = ({
icon,
text,
onClick,
hasBlurBackground = false,
disableHover = false,
children,
...props
}) => (
<MotionBox <MotionBox
whileHover={{ position={'relative'}
y: -3,
boxShadow: 'inset 0 0 0 1000px rgba(255, 255, 255, 0.3)',
}}
display={'flex'} display={'flex'}
alignItems={'center'} alignItems={'center'}
bg={'whiteAlpha.200'} bg={'whiteAlpha.200'}
@ -25,12 +34,46 @@ const MotionCard = ({ icon, text, onClick }) => (
px={4} px={4}
py={2} py={2}
borderRadius={'md'} borderRadius={'md'}
cursor={'pointer'} cursor={onClick ? 'pointer' : 'default'}
onClick={onClick} onClick={onClick}
transition={{ duration: 0.1 }} transition={'all 0.2s ease'}
overflow={'hidden'}
_hover={
disableHover
? {}
: {
_before: {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: 'whiteAlpha.100',
zIndex: 1,
},
}
}
{...props}
> >
{icon && <Image src={icon} boxSize={5} mr={2} />} {hasBlurBackground && (
<Text color={'white'}>{text}</Text> <Box
position={'absolute'}
top={0}
left={0}
right={0}
bottom={0}
backdropFilter={'blur(4px)'}
zIndex={-1}
/>
)}
{icon && <Image src={icon} boxSize={5} mr={2} zIndex={2} />}
{text && (
<Text color={'white'} zIndex={2}>
{text}
</Text>
)}
<Box zIndex={2}>{children}</Box>
</MotionBox> </MotionBox>
); );

View File

@ -1,90 +1,86 @@
'use client' 'use client';
import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react' import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react';
import { ThemeProvider, useTheme } from 'next-themes' import { ThemeProvider, useTheme } from 'next-themes';
import * as React from 'react' import * as React from 'react';
import { LuMoon, LuSun } from 'react-icons/lu' import { LuMoon, LuSun } from 'react-icons/lu';
export function ColorModeProvider(props) { export function ColorModeProvider(props) {
return ( return <ThemeProvider attribute="class" disableTransitionOnChange {...props} />;
<ThemeProvider attribute='class' disableTransitionOnChange {...props} />
)
} }
export function useColorMode() { export function useColorMode() {
const { resolvedTheme, setTheme, forcedTheme } = useTheme() const { resolvedTheme, setTheme, forcedTheme } = useTheme();
const colorMode = forcedTheme || resolvedTheme const colorMode = forcedTheme || resolvedTheme;
const toggleColorMode = () => { const toggleColorMode = () => {
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark') setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
} };
return { return {
colorMode: colorMode, colorMode: colorMode,
setColorMode: setTheme, setColorMode: setTheme,
toggleColorMode, toggleColorMode,
} };
} }
export function useColorModeValue(light, dark) { export function useColorModeValue(light, dark) {
const { colorMode } = useColorMode() const { colorMode } = useColorMode();
return colorMode === 'dark' ? dark : light return colorMode === 'dark' ? dark : light;
} }
export function ColorModeIcon() { export function ColorModeIcon() {
const { colorMode } = useColorMode() const { colorMode } = useColorMode();
return colorMode === 'dark' ? <LuMoon /> : <LuSun /> return colorMode === 'dark' ? <LuMoon /> : <LuSun />;
} }
export const ColorModeButton = React.forwardRef( export const ColorModeButton = React.forwardRef(function ColorModeButton(props, ref) {
function ColorModeButton(props, ref) { const { toggleColorMode } = useColorMode();
const { toggleColorMode } = useColorMode() return (
return ( <ClientOnly fallback={<Skeleton boxSize="8" />}>
<ClientOnly fallback={<Skeleton boxSize='8' />}> <IconButton
<IconButton onClick={toggleColorMode}
onClick={toggleColorMode} variant="ghost"
variant='ghost' aria-label="Toggle color mode"
aria-label='Toggle color mode' size="sm"
size='sm' ref={ref}
ref={ref} {...props}
{...props} css={{
css={{ _icon: {
_icon: { width: '5',
width: '5', height: '5',
height: '5', },
}, }}
}} >
> <ColorModeIcon />
<ColorModeIcon /> </IconButton>
</IconButton> </ClientOnly>
</ClientOnly> );
) });
},
)
export const LightMode = React.forwardRef(function LightMode(props, ref) { export const LightMode = React.forwardRef(function LightMode(props, ref) {
return ( return (
<Span <Span
color='fg' color="fg"
display='contents' display="contents"
className='chakra-theme light' className="chakra-theme light"
colorPalette='gray' colorPalette="gray"
colorScheme='light' colorScheme="light"
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
}) });
export const DarkMode = React.forwardRef(function DarkMode(props, ref) { export const DarkMode = React.forwardRef(function DarkMode(props, ref) {
return ( return (
<Span <Span
color='fg' color="fg"
display='contents' display="contents"
className='chakra-theme dark' className="chakra-theme dark"
colorPalette='gray' colorPalette="gray"
colorScheme='dark' colorScheme="dark"
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
}) });

View File

@ -1,13 +1,19 @@
import React from 'react'; import React from 'react';
import { Box, Text } from '@chakra-ui/react'; import { Box, Text } from '@chakra-ui/react';
import DocumentTitle from '@/components/system/pages/DocumentTitle';
import PageContainer from '@/components/system/PageContainer';
import DashboardBackground from '@/components/system/pages/DashboardBackground';
const Dashboard = () => { const Dashboard = () => {
return ( return (
<> <DocumentTitle title={'控制台'}>
<Box p={6}> <DashboardBackground />
<Text fontSize={'xl'}>控制台奇怪的功能+1</Text> <PageContainer>
</Box> <Box p={6}>
</> <Text fontSize={'xl'}>控制台奇怪的功能+1</Text>
</Box>
</PageContainer>
</DocumentTitle>
); );
}; };

View File

@ -1,7 +1,6 @@
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import BackgroundBlur from '@/components/pages/welcome/BackgroundBlur'; import BackgroundBlur from '@/components/pages/welcome/BackgroundBlur';
import WelcomeContent from '@/components/pages/welcome/WelcomeContent'; import WelcomeContent from '@/components/pages/welcome/WelcomeContent';
import GithubCard from '@/components/pages/welcome/GithubCard';
/** /**
* 欢迎页 * 欢迎页
@ -13,7 +12,7 @@ const Welcome = () => {
<Box position={'relative'} height={'100vh'} overflow={'hidden'}> <Box position={'relative'} height={'100vh'} overflow={'hidden'}>
<BackgroundBlur /> <BackgroundBlur />
<Box position={'absolute'} top={4} right={4} zIndex={10}> <Box position={'absolute'} top={4} right={4} zIndex={10}>
<GithubCard /> {/*<GithubCard />*/}
</Box> </Box>
<Box overflowY={'auto'} height={'100%'} zIndex={1} position={'relative'}> <Box overflowY={'auto'} height={'100%'} zIndex={1} position={'relative'}>
<WelcomeContent /> <WelcomeContent />

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24"><g fill="none" stroke="#2ee2e5" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="#2ee2e5"><circle cx="12" cy="12" r="10"/><ellipse cx="12" cy="12" rx="4" ry="10"/><path d="M2 12h20"/></g></svg>

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 KiB