Merge remote-tracking branch 'origin/main'

This commit is contained in:
3 2025-06-13 12:56:33 +08:00
commit 5db8ec9b4a
38 changed files with 765 additions and 121 deletions

46
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Deploy React App
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: 'src/frontend/pnpm-lock.yaml'
- name: Install dependencies
working-directory: src/frontend
run: pnpm install
- name: Build
working-directory: src/frontend
run: pnpm run build
- name: Copy 404.html
working-directory: src/frontend
run: cp build/index.html build/404.html
- name: Deploy to GitHub Pages
working-directory: src/frontend
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "github-actions"
git config --global user.email "github-actions@users.noreply.github.com"
npx gh-pages -d build -u "github-actions <github-actions@users.noreply.github.com>" --repo "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git"
# 大抵是没有问题了

View File

@ -2,7 +2,7 @@
<module type="PYTHON_MODULE" version="4"> <module type="PYTHON_MODULE" version="4">
<component name="FacetManager"> <component name="FacetManager">
<facet type="Python" name="Python facet"> <facet type="Python" name="Python facet">
<configuration sdkName="Python 3.12" /> <configuration sdkName="Python 3.10" />
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
@ -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.12 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

@ -1,10 +1,27 @@
# Network Admin Web UI # Network Admin Web UI
基于 React 和 Chakra UI ### 基于 `React``Chakra UI`
- 前端框架React #### 架构
- UI 组件库Chakra UI - 前端框架:`React`
- 网络图表Recharts - UI 组件库:`Chakra UI`
- 状态管理React hooks - 网络图表:`Recharts`
- 数据通信Axios - 状态管理:`React hooks`
- 页面路由React Router - 数据通信:`Axios`
- 页面路由:`React Router`
#### 部署方法
- `clone`项目
- 导航到`/src/frontend/`目录
- 使用`pnpm`管理软件包:`npm install pnpm -g`
- 安装依赖:`pnpm install`
- 执行`pnpm build`进行`react`服务端构建
- 运行`pnpm start`启动服务端
- 服务运行在本地`3000`端口
#### 功能(后端)
- []监控网络流量
- []扫描环境中的交换机
- []自然语言解析命令
- []下发配置到交换机
- []流量预测

View File

@ -2,6 +2,7 @@
"name": "network-admin-frontend", "name": "network-admin-frontend",
"version": "0.1.0", "version": "0.1.0",
"private": false, "private": false,
"homepage": "https://JerryPlsuy.github.io/AI-powered-switches",
"dependencies": { "dependencies": {
"@chakra-ui/react": "^3.19.1", "@chakra-ui/react": "^3.19.1",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
@ -28,6 +29,8 @@
"build": "react-app-rewired build", "build": "react-app-rewired build",
"test": "react-app-rewired test", "test": "react-app-rewired test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"predeploy": "pnpm run build",
"deploy": "gh-pages -d build",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"" "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\""
}, },
"eslintConfig": { "eslintConfig": {
@ -54,6 +57,7 @@
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"gh-pages": "^6.3.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react-app-rewired": "^2.2.1" "react-app-rewired": "^2.2.1"
} }

View File

@ -81,6 +81,9 @@ importers:
eslint-plugin-react-hooks: eslint-plugin-react-hooks:
specifier: ^4.3.0 specifier: ^4.3.0
version: 4.6.2(eslint@8.57.0) version: 4.6.2(eslint@8.57.0)
gh-pages:
specifier: ^6.3.0
version: 6.3.0
prettier: prettier:
specifier: ^3.5.3 specifier: ^3.5.3
version: 3.5.3 version: 3.5.3
@ -2277,6 +2280,10 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
commander@13.1.0:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
commander@2.20.3: commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@ -2757,6 +2764,9 @@ packages:
electron-to-chromium@1.5.159: electron-to-chromium@1.5.159:
resolution: {integrity: sha512-CEvHptWAMV5p6GJ0Lq8aheyvVbfzVrv5mmidu1D3pidoVNkB3tTBsTMVtPJ+rzRK5oV229mCLz9Zj/hNvU8GBA==} resolution: {integrity: sha512-CEvHptWAMV5p6GJ0Lq8aheyvVbfzVrv5mmidu1D3pidoVNkB3tTBsTMVtPJ+rzRK5oV229mCLz9Zj/hNvU8GBA==}
email-addresses@5.0.0:
resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==}
emittery@0.10.2: emittery@0.10.2:
resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -3108,6 +3118,14 @@ packages:
filelist@1.0.4: filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
filename-reserved-regex@2.0.0:
resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==}
engines: {node: '>=4'}
filenamify@4.3.0:
resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==}
engines: {node: '>=8'}
filesize@8.0.7: filesize@8.0.7:
resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
@ -3214,6 +3232,10 @@ packages:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
fs-extra@11.3.0:
resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
engines: {node: '>=14.14'}
fs-extra@9.1.0: fs-extra@9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3270,6 +3292,11 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
gh-pages@6.3.0:
resolution: {integrity: sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==}
engines: {node: '>=10'}
hasBin: true
glob-parent@5.1.2: glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -5547,6 +5574,10 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
strip-outer@1.0.1:
resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
engines: {node: '>=0.10.0'}
style-loader@3.3.4: style-loader@3.3.4:
resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
@ -5705,6 +5736,10 @@ packages:
resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==}
engines: {node: '>=8'} engines: {node: '>=8'}
trim-repeated@1.0.0:
resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==}
engines: {node: '>=0.10.0'}
tryer@1.0.1: tryer@1.0.1:
resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==}
@ -9156,6 +9191,8 @@ snapshots:
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
commander@13.1.0: {}
commander@2.20.3: {} commander@2.20.3: {}
commander@4.1.1: {} commander@4.1.1: {}
@ -9609,6 +9646,8 @@ snapshots:
electron-to-chromium@1.5.159: {} electron-to-chromium@1.5.159: {}
email-addresses@5.0.0: {}
emittery@0.10.2: {} emittery@0.10.2: {}
emittery@0.8.1: {} emittery@0.8.1: {}
@ -10135,6 +10174,14 @@ snapshots:
dependencies: dependencies:
minimatch: 5.1.6 minimatch: 5.1.6
filename-reserved-regex@2.0.0: {}
filenamify@4.3.0:
dependencies:
filename-reserved-regex: 2.0.0
strip-outer: 1.0.1
trim-repeated: 1.0.0
filesize@8.0.7: {} filesize@8.0.7: {}
fill-range@7.1.1: fill-range@7.1.1:
@ -10250,6 +10297,12 @@ snapshots:
jsonfile: 6.1.0 jsonfile: 6.1.0
universalify: 2.0.1 universalify: 2.0.1
fs-extra@11.3.0:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.1
fs-extra@9.1.0: fs-extra@9.1.0:
dependencies: dependencies:
at-least-node: 1.0.0 at-least-node: 1.0.0
@ -10311,6 +10364,16 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
gh-pages@6.3.0:
dependencies:
async: 3.2.6
commander: 13.1.0
email-addresses: 5.0.0
filenamify: 4.3.0
find-cache-dir: 3.3.2
fs-extra: 11.3.0
globby: 11.1.0
glob-parent@5.1.2: glob-parent@5.1.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@ -12976,6 +13039,10 @@ snapshots:
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
strip-outer@1.0.1:
dependencies:
escape-string-regexp: 1.0.5
style-loader@3.3.4(webpack@5.99.9): style-loader@3.3.4(webpack@5.99.9):
dependencies: dependencies:
webpack: 5.99.9 webpack: 5.99.9
@ -13169,6 +13236,10 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
trim-repeated@1.0.0:
dependencies:
escape-string-regexp: 1.0.5
tryer@1.0.1: {} tryer@1.0.1: {}
ts-interface-checker@0.1.13: {} ts-interface-checker@0.1.13: {}

View File

@ -1,13 +1,17 @@
import { Route, Routes } from 'react-router-dom'; import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Welcome from '@/pages/Welcome'; import AppShell from '@/components/system/layout/AppShell';
import Dashboard from '@/pages/Dashboard'; import buildRoutes from '@/constants/routes/routes';
const App = () => { const App = () => {
const isProd = process.env.NODE_ENV === 'production';
return ( return (
<Routes> <BrowserRouter basename={isProd ? '/AI-powered-switches' : '/'}>
<Route path={'/'} element={<Welcome />} /> <Routes>
<Route path={'/dashboard'} element={<Dashboard />} /> <Route path="/" element={<AppShell />}>
</Routes> {buildRoutes()}
</Route>
</Routes>
</BrowserRouter>
); );
}; };

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

View File

@ -0,0 +1,20 @@
import manageIcon from '@/resources/welcome/image/setting.svg';
import MotionCard from '@/components/ui/MotionCard';
import { useNavigate } from 'react-router-dom';
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
/**
* 进入管理后台按钮组件
* @returns {JSX.Element}
* @constructor
*/
const DashboardCard = () => {
const navigate = useNavigate();
return (
<FadeInWrapper delay={0.4} yOffset={-5}>
<MotionCard icon={manageIcon} text={'管理后台'} onClick={() => navigate('/dashboard')} />
</FadeInWrapper>
);
};
export default DashboardCard;

View File

@ -0,0 +1,23 @@
import githubIcon from '@/resources/welcome/image/github.svg';
import MotionCard from '@/components/ui/MotionCard';
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
/**
* GitHub按钮组件
* @returns {JSX.Element}
* @constructor
*/
const GithubCard = () => {
return (
<FadeInWrapper delay={0.1} yOffset={-10}>
<MotionCard
icon={githubIcon}
text={'Github'}
hasBlurBackground={true}
onClick={() => window.open('https://github.com/Jerryplusy/AI-powered-switches', '_blank')}
/>
</FadeInWrapper>
);
};
export default GithubCard;

View File

@ -0,0 +1,34 @@
import { Box, Heading, Text, VStack } from '@chakra-ui/react';
import DashboardCard from '@/components/pages/welcome/DashboardCard';
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
/**
* 欢迎文字
* @returns {JSX.Element}
* @constructor
*/
const WelcomeContent = () => {
return (
<VStack spacing={10} py={200} align={'center'} px={4}>
<FadeInWrapper delay={0.2} yOffset={-5}>
<Box textAlign={'center'}>
<Heading size="6xl" fontWeight={'black'} color={'teal.300'}>
智能网络交换机
<br />
管理系统
</Heading>
</Box>
</FadeInWrapper>
<FadeInWrapper delay={0.3} yOffset={-5}>
<Box textAlign={'center'}>
<Text mt={6} fontSize={'2xl'} color={'gray.300'}>
助力大型网络交换机配置及网络流量管理方便的管控网络让网络配置不再困难
</Text>
</Box>
</FadeInWrapper>
<DashboardCard />
</VStack>
);
};
export default WelcomeContent;

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

@ -0,0 +1,28 @@
import { Outlet, useLocation } from 'react-router-dom';
import { Box } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion';
import PageTransition from './PageTransition';
import GithubTransitionCard from '@/components/system/layout/github/GithubTransitionCard';
/**
* 应用加壳
* @returns {JSX.Element}
* @constructor
*/
const AppShell = () => {
const location = useLocation();
return (
<Box position="relative" height="100vh" overflow="hidden">
<GithubTransitionCard />
<Box overflowY="auto" height="100%">
<AnimatePresence mode="wait" initial={false}>
<PageTransition key={location.pathname}>
<Outlet />
</PageTransition>
</AnimatePresence>
</Box>
</Box>
);
};
export default AppShell;

View File

@ -0,0 +1,43 @@
import { AnimatePresence, motion } from 'framer-motion';
/**
* 组件进入 / 离开时的淡入淡出动画
* @param children 子组件
* @param delay 延迟
* @param yOffset y轴偏移量
* @param duration 动画时间
* @param className 类名
* @param props
* @returns {JSX.Element}
* @constructor
*/
const FadeInWrapper = ({
children,
delay = 0,
yOffset = 10,
duration = 0.6,
className = '',
...props
}) => {
return (
<AnimatePresence mode="wait">
<motion.div
key="fade-wrapper"
initial={{ opacity: 0, y: yOffset }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -yOffset }}
transition={{
delay,
duration,
ease: [0.16, 0.77, 0.47, 0.97],
}}
className={className}
{...props}
>
{children}
</motion.div>
</AnimatePresence>
);
};
export default FadeInWrapper;

View File

@ -0,0 +1,16 @@
import { motion } from 'framer-motion';
/**
* 页面动效
* @param children
* @returns {JSX.Element}
* @constructor
*/
const PageTransition = ({ children }) => <motion.div>{children}</motion.div>;
export default PageTransition;
/**
* initial={{ opacity: 0, y: 0 }}
* animate={{ opacity: 1, y: 0 }}
* transition={{ duration: 0.2 }}
*/

View File

@ -0,0 +1,24 @@
import FadeInWrapper from './FadeInWrapper';
/**
* 递归为组件及子组件添加载入动效
* @param children 子组件
* @param baseDelay 延迟
* @param increment 增值
* @param className 类名
* @returns {JSX.Element}
* @constructor
*/
const StaggeredFadeIn = ({ children, baseDelay = 0.2, increment = 0.1, className = '' }) => {
return (
<>
{React.Children.map(children, (child, index) => (
<FadeInWrapper key={index} delay={baseDelay + index * increment} className={className}>
{child}
</FadeInWrapper>
))}
</>
);
};
export default StaggeredFadeIn;

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

@ -1,5 +1,12 @@
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
/**
* 卡片组件
* @param children
* @param props
* @returns {JSX.Element}
* @constructor
*/
const Card = ({ children, ...props }) => ( const Card = ({ children, ...props }) => (
<Box <Box
bg={'rgba(255,255,255,0.1)'} bg={'rgba(255,255,255,0.1)'}

View File

@ -0,0 +1,80 @@
import { Box, Text, Image } from '@chakra-ui/react';
import { motion } from 'framer-motion';
const MotionBox = motion(Box);
/**
* 卡片组件
* @param icon 可选图标
* @param text 文字
* @param onClick 点击执行函数
* @param hasBlurBackground 是否模糊背景
* @param noHover 是否禁用 hover 动画
* @param children 子组件
* @param props
* @returns {JSX.Element}
* @constructor
*/
const MotionCard = ({
icon,
text,
onClick,
hasBlurBackground = false,
disableHover = false,
children,
...props
}) => (
<MotionBox
position={'relative'}
display={'flex'}
alignItems={'center'}
bg={'whiteAlpha.200'}
border={'1px solid'}
borderColor={'gray.600'}
px={4}
py={2}
borderRadius={'md'}
cursor={onClick ? 'pointer' : 'default'}
onClick={onClick}
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}
>
{hasBlurBackground && (
<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>
);
export default MotionCard;

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

@ -0,0 +1,17 @@
import { Route } from 'react-router-dom';
import Welcome from '@/pages/Welcome';
import Dashboard from '@/pages/Dashboard';
/**
* 路由
* @type {[{path: string, element: JSX.Element},{path: string, element: JSX.Element}]}
*/
const routeList = [
{ path: '/', element: <Welcome /> },
{ path: '/dashboard', element: <Dashboard /> },
];
const buildRoutes = () =>
routeList.map(({ path, element }) => <Route key={path} path={path} element={element} />);
export default buildRoutes;

View File

@ -1,14 +1,11 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from '@/App'; import App from '@/App';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from '@/components/ui/provider'; import { Provider } from '@/components/ui/provider';
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<Provider> <Provider>
<BrowserRouter> <App />
<App />
</BrowserRouter>
</Provider> </Provider>
); );

View File

@ -1,15 +1,22 @@
import React from 'react'; import React from 'react';
import { Box, Text } from '@chakra-ui/react'; import { Box, Text } from '@chakra-ui/react';
import Header from '../components/Header'; import DocumentTitle from '@/components/system/pages/DocumentTitle';
import PageContainer from '@/components/system/PageContainer';
import DashboardBackground from '@/components/system/pages/DashboardBackground';
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
const Dashboard = () => { const Dashboard = () => {
return ( return (
<> <DocumentTitle title={'控制台'}>
<Header /> <DashboardBackground />
<Box p={6}> <PageContainer>
<Text fontSize={'xl'}>控制台奇怪的功能+1</Text> <FadeInWrapper delay={0.1} yOffset={-5}>
</Box> <Box p={6}>
</> <Text fontSize={'xl'}>控制台奇怪的功能+1</Text>
</Box>
</FadeInWrapper>
</PageContainer>
</DocumentTitle>
); );
}; };

View File

@ -1,19 +1,22 @@
import React from 'react'; import { Box } from '@chakra-ui/react';
import { Box, Button, Heading, VStack } from '@chakra-ui/react'; import BackgroundBlur from '@/components/pages/welcome/BackgroundBlur';
import { useNavigate } from 'react-router-dom'; import WelcomeContent from '@/components/pages/welcome/WelcomeContent';
/**
* 欢迎页
* @returns {JSX.Element}
* @constructor
*/
const Welcome = () => { const Welcome = () => {
const navigate = useNavigate();
return ( return (
<Box textAlign={'center'} py={10} px={6}> <Box position={'relative'} height={'100vh'} overflow={'hidden'}>
<VStack spacing={4}> <BackgroundBlur />
<Heading as={'h1'} size={'x1'}> <Box position={'absolute'} top={4} right={4} zIndex={10}>
欢迎使用交换机管理后台 {/*<GithubCard />*/}
</Heading> </Box>
<Button colorScheme={'teal'} onClick={() => navigate('/dashboard')}> <Box overflowY={'auto'} height={'100%'} zIndex={1} position={'relative'}>
进入控制台 <WelcomeContent />
</Button> </Box>
</VStack>
</Box> </Box>
); );
}; };

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -0,0 +1,4 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.83329 16.14C4.97579 17.1042 2.59496 16.14 1.16663 13.1667" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.83329 17.3333V14.6317C7.83329 14.1333 7.98663 13.7 8.23329 13.3083C8.40329 13.04 8.28663 12.6583 7.98079 12.575C5.44496 11.8767 3.66663 10.7558 3.66663 7.0375C3.66663 6.07083 3.98329 5.1625 4.53996 4.37083C4.67829 4.17417 4.74829 4.07583 4.76496 3.9875C4.78163 3.8975 4.75246 3.78167 4.69413 3.54833C4.45829 2.60167 4.47413 1.59583 4.82746 0.69C4.82746 0.69 5.55829 0.450833 7.22246 1.49C7.60246 1.7275 7.79246 1.84667 7.95996 1.87333C8.12746 1.9 8.35079 1.84417 8.79746 1.7325C9.4361 1.57416 10.092 1.49606 10.75 1.5C11.4079 1.49719 12.0637 1.57556 12.7025 1.73333C13.1491 1.845 13.3733 1.9 13.5408 1.87417C13.7075 1.8475 13.8975 1.72833 14.2775 1.49083C15.9416 0.451667 16.6725 0.690833 16.6725 0.690833C17.0258 1.59667 17.0416 2.6025 16.8058 3.54917C16.7475 3.7825 16.7191 3.89917 16.7358 3.9875C16.7525 4.07583 16.8216 4.175 16.96 4.37167C17.5166 5.16333 17.8333 6.07167 17.8333 7.03833C17.8333 10.7567 16.055 11.8775 13.5191 12.5742C13.2133 12.6583 13.0966 13.04 13.2666 13.3075C13.5133 13.6992 13.6666 14.1325 13.6666 14.6317V17.3333" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="26" viewBox="0 0 25 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4166 13C9.27081 13 8.28991 12.592 7.47394 11.7761C6.65797 10.9601 6.24998 9.97919 6.24998 8.83335C6.24998 7.68752 6.65797 6.70662 7.47394 5.89065C8.28991 5.07467 9.27081 4.66669 10.4166 4.66669C11.5625 4.66669 12.5434 5.07467 13.3594 5.89065C14.1753 6.70662 14.5833 7.68752 14.5833 8.83335C14.5833 9.97919 14.1753 10.9601 13.3594 11.7761C12.5434 12.592 11.5625 13 10.4166 13ZM2.08331 21.3334V18.4167C2.08331 17.8438 2.23088 17.3056 2.52602 16.8021C2.82116 16.2986 3.22915 15.9167 3.74998 15.6563C4.6354 15.2049 5.63366 14.8229 6.74477 14.5104C7.85588 14.1979 9.07984 14.0417 10.4166 14.0417H10.7812C10.8854 14.0417 10.9896 14.059 11.0937 14.0938C10.9548 14.4063 10.8378 14.732 10.7427 15.0709C10.6475 15.4097 10.5736 15.7611 10.5208 16.125H10.4166C9.18401 16.125 8.07741 16.2813 7.09685 16.5938C6.1163 16.9063 5.31317 17.2188 4.68748 17.5313C4.53123 17.6181 4.40519 17.7396 4.30935 17.8959C4.21352 18.0521 4.16595 18.2257 4.16665 18.4167V19.25H10.7291C10.8333 19.6146 10.9722 19.975 11.1458 20.3313C11.3194 20.6875 11.5104 21.0215 11.7187 21.3334H2.08331ZM16.6666 22.375L16.3541 20.8125C16.1458 20.7257 15.9507 20.6347 15.7687 20.5396C15.5868 20.4445 15.4 20.3271 15.2083 20.1875L13.6979 20.6563L12.6562 18.8854L13.8541 17.8438C13.8194 17.6007 13.8021 17.375 13.8021 17.1667C13.8021 16.9584 13.8194 16.7327 13.8541 16.4896L12.6562 15.4479L13.6979 13.6771L15.2083 14.1459C15.3993 14.007 15.5861 13.89 15.7687 13.7948C15.9514 13.6997 16.1465 13.6084 16.3541 13.5209L16.6666 11.9584H18.75L19.0625 13.5209C19.2708 13.6077 19.4663 13.7031 19.6489 13.8073C19.8316 13.9115 20.018 14.0417 20.2083 14.1979L21.7187 13.6771L22.7604 15.5L21.5625 16.5417C21.5972 16.75 21.6146 16.967 21.6146 17.1927C21.6146 17.4184 21.5972 17.6354 21.5625 17.8438L22.7604 18.8854L21.7187 20.6563L20.2083 20.1875C20.0173 20.3264 19.8309 20.4438 19.6489 20.5396C19.467 20.6354 19.2715 20.7264 19.0625 20.8125L18.75 22.375H16.6666ZM17.7083 19.25C18.2812 19.25 18.7719 19.0462 19.1802 18.6386C19.5885 18.2309 19.7923 17.7403 19.7916 17.1667C19.791 16.5931 19.5871 16.1028 19.1802 15.6959C18.7732 15.2889 18.2826 15.0847 17.7083 15.0834C17.134 15.082 16.6437 15.2861 16.2375 15.6959C15.8312 16.1056 15.6271 16.5959 15.625 17.1667C15.6229 17.7375 15.8271 18.2281 16.2375 18.6386C16.6479 19.049 17.1382 19.2528 17.7083 19.25ZM10.4166 10.9167C10.9896 10.9167 11.4802 10.7129 11.8885 10.3052C12.2969 9.89759 12.5007 9.40696 12.5 8.83335C12.4993 8.25974 12.2955 7.76946 11.8885 7.36252C11.4816 6.95558 10.991 6.75141 10.4166 6.75002C9.84234 6.74863 9.35206 6.9528 8.94581 7.36252C8.53956 7.77224 8.3354 8.26252 8.33331 8.83335C8.33123 9.40419 8.5354 9.89481 8.94581 10.3052C9.35623 10.7156 9.84651 10.9195 10.4166 10.9167Z" fill="#2EE2E5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB