mirror of
https://github.com/Jerryplusy/AI-powered-switches.git
synced 2025-07-04 13:19:20 +00:00
Compare commits
2 Commits
a528009674
...
921b17e3f5
Author | SHA1 | Date | |
---|---|---|---|
921b17e3f5 | |||
ae8786f297 |
@ -28,7 +28,7 @@ const FeatureCard = ({ title, description, buttonText, to, disabled }) => (
|
|||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
colorScheme={'blue'}
|
colorPalette={'blue'}
|
||||||
variant={'solid'}
|
variant={'solid'}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -9,8 +9,10 @@ import MotionCard from '@/components/ui/MotionCard';
|
|||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: '面板', path: '/dashboard' },
|
{ label: '面板', path: '/dashboard' },
|
||||||
{ label: '网络', path: '/dashboard/network' },
|
{ label: '网络扫描', path: '/dashboard/scan' },
|
||||||
{ label: '交换机', path: '/dashboard/switch' },
|
{ label: '交换机设备', path: '/dashboard/devices' },
|
||||||
|
{ label: '交换机配置', path: '/dashboard/config' },
|
||||||
|
{ label: '流量监控', path: '/dashboard/watch' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
import Welcome from '@/pages/Welcome';
|
import Welcome from '@/pages/Welcome';
|
||||||
import Dashboard from '@/pages/Dashboard';
|
import Dashboard from '@/pages/Dashboard';
|
||||||
|
import ScanPage from '@/pages/ScanPage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由
|
* 路由
|
||||||
@ -9,6 +10,7 @@ import Dashboard from '@/pages/Dashboard';
|
|||||||
const routeList = [
|
const routeList = [
|
||||||
{ path: '/', element: <Welcome /> },
|
{ path: '/', element: <Welcome /> },
|
||||||
{ path: '/dashboard', element: <Dashboard /> },
|
{ path: '/dashboard', element: <Dashboard /> },
|
||||||
|
{ path: '/dashboard/scan', element: <ScanPage /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const buildRoutes = () =>
|
const buildRoutes = () =>
|
||||||
|
@ -48,6 +48,7 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
|
|
||||||
const MotionBox = motion(Box);
|
const MotionBox = motion(Box);
|
||||||
|
|
||||||
|
// TODO 弹窗颜色问题及重新渲染问题
|
||||||
return (
|
return (
|
||||||
<NotificationContext.Provider value={notify}>
|
<NotificationContext.Provider value={notify}>
|
||||||
{children}
|
{children}
|
||||||
@ -95,7 +96,7 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
<Text mt={2}>{item.description}</Text>
|
<Text mt={2}>{item.description}</Text>
|
||||||
{item.button && (
|
{item.button && (
|
||||||
<Button
|
<Button
|
||||||
colorScheme={'whiteAlpha'}
|
colorPalette={'whiteAlpha'}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
mt={3}
|
mt={3}
|
||||||
onClick={item.button.onClick}
|
onClick={item.button.onClick}
|
||||||
|
@ -8,6 +8,11 @@ import FeatureCard from '@/components/pages/dashboard/FeatureCard';
|
|||||||
import StatCard from '@/components/pages/dashboard/StatCard';
|
import StatCard from '@/components/pages/dashboard/StatCard';
|
||||||
import { useNotification } from '@/libs/system/Notification';
|
import { useNotification } from '@/libs/system/Notification';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制台
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
totalDevices: 0,
|
totalDevices: 0,
|
||||||
@ -15,7 +20,7 @@ const Dashboard = () => {
|
|||||||
lastScan: '',
|
lastScan: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [networkStatus, setNetworkStatus] = useState('idle'); // idle | loading | ok | fail
|
const [networkStatus, setNetworkStatus] = useState('loading'); // loading | ok | fail
|
||||||
const notify = useNotification();
|
const notify = useNotification();
|
||||||
|
|
||||||
const checkBackend = useCallback(async () => {
|
const checkBackend = useCallback(async () => {
|
||||||
@ -39,7 +44,6 @@ const Dashboard = () => {
|
|||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
checkBackend();
|
checkBackend();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [checkBackend]);
|
}, [checkBackend]);
|
||||||
|
|
||||||
@ -92,35 +96,42 @@ const Dashboard = () => {
|
|||||||
{'网络健康状态'}
|
{'网络健康状态'}
|
||||||
</Text>
|
</Text>
|
||||||
<HStack spacing={4}>
|
<HStack spacing={4}>
|
||||||
{networkStatus === 'idle' && (
|
|
||||||
<Badge colorScheme={'gray'} variant={'solid'} p={2} borderRadius={'lg'}>
|
|
||||||
{'等待检测'}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{networkStatus === 'loading' && (
|
{networkStatus === 'loading' && (
|
||||||
<Badge colorScheme={'gray'} variant={'solid'} p={2} borderRadius={'lg'}>
|
<Badge
|
||||||
|
colorScheme={'gray'}
|
||||||
|
variant={'surface'}
|
||||||
|
p={2}
|
||||||
|
borderRadius={'lg'}
|
||||||
|
colorPalette={'blue'}
|
||||||
|
>
|
||||||
<Spinner size="sm" mr={2} /> {'检测网络中...'}
|
<Spinner size="sm" mr={2} /> {'检测网络中...'}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{networkStatus === 'ok' && (
|
{networkStatus === 'ok' && (
|
||||||
<Badge colorScheme={'green'} variant={'solid'} p={2} borderRadius={'lg'}>
|
<Badge
|
||||||
|
colorScheme={'green'}
|
||||||
|
variant={'surface'}
|
||||||
|
p={2}
|
||||||
|
borderRadius={'lg'}
|
||||||
|
colorPalette={'green'}
|
||||||
|
>
|
||||||
{'网络连接正常'}
|
{'网络连接正常'}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{networkStatus === 'fail' && (
|
{networkStatus === 'fail' && (
|
||||||
<Badge colorScheme={'red'} variant={'solid'} p={2} borderRadius={'lg'}>
|
<Badge colorPalette={'red'} variant={'surface'} p={2} borderRadius={'lg'}>
|
||||||
{'无法连接后端'}
|
{'无法连接后端'}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<Badge colorScheme={'blue'} variant={'solid'} p={2} borderRadius={'lg'}>
|
<Badge colorPalette={'blue'} variant={'surface'} p={2} borderRadius={'lg'}>
|
||||||
{'交换机正常运行'}
|
{'交换机正常运行'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorScheme={'yellow'} variant={'solid'} p={2} borderRadius={'lg'}>
|
<Badge colorPalette={'yellow'} variant={'surface'} p={2} borderRadius={'lg'}>
|
||||||
{'流量监控未启动'}
|
{'流量监控已启动'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<Button mt={4} onClick={checkBackend} colorScheme={'teal'}>
|
<Button mt={4} onClick={checkBackend} colorPalette={'teal'} variant={'outline'}>
|
||||||
{'重新检测'}
|
{'重新检测'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
@ -132,25 +143,25 @@ const Dashboard = () => {
|
|||||||
title={'网络扫描'}
|
title={'网络扫描'}
|
||||||
description={'快速扫描指定子网,发现可用设备,展示设备 IP/MAC 和开放端口信息'}
|
description={'快速扫描指定子网,发现可用设备,展示设备 IP/MAC 和开放端口信息'}
|
||||||
buttonText={'立即扫描'}
|
buttonText={'立即扫描'}
|
||||||
to={'/scan'}
|
to={'/dashboard/scan'}
|
||||||
/>
|
/>
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
title={'设备管理'}
|
title={'设备管理'}
|
||||||
description={'查看已记录的交换机设备信息,未来支持编辑、备注、批量管理'}
|
description={'查看已记录的交换机设备信息,未来支持编辑、备注、批量管理'}
|
||||||
buttonText={'管理设备'}
|
buttonText={'管理设备'}
|
||||||
to={'/devices'}
|
to={'/dashboard/devices'}
|
||||||
/>
|
/>
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
title={'命令配置'}
|
title={'命令配置'}
|
||||||
description={'输入自然语言命令,自动生成设备配置,支持一键应用到交换机'}
|
description={'输入自然语言命令,自动生成设备配置,支持一键应用到交换机'}
|
||||||
buttonText={'前往配置'}
|
buttonText={'前往配置'}
|
||||||
to={'/config'}
|
to={'/dashboard/config'}
|
||||||
/>
|
/>
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
title={'流量监控'}
|
title={'流量监控'}
|
||||||
description={'未来将支持实时监控每台设备上下行带宽,帮助掌握网络流量变化'}
|
description={'未来将支持实时监控每台设备上下行带宽,帮助掌握网络流量变化'}
|
||||||
buttonText={'敬请期待'}
|
buttonText={'前往查看'}
|
||||||
disabled
|
to={'dashboard/watch'}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</FadeInWrapper>
|
</FadeInWrapper>
|
||||||
|
164
src/frontend/src/pages/ScanPage.jsx
Normal file
164
src/frontend/src/pages/ScanPage.jsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Spinner,
|
||||||
|
Badge,
|
||||||
|
HStack,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Table } from '@chakra-ui/react';
|
||||||
|
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';
|
||||||
|
import { api } from '@/services/api';
|
||||||
|
import { useNotification } from '@/libs/system/Notification';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网络扫描页面
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
const ScanPage = () => {
|
||||||
|
const [subnet, setSubnet] = useState('');
|
||||||
|
const [devices, setDevices] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [localIp, setLocalIp] = useState('');
|
||||||
|
const notify = useNotification();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchLocalInfo = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.test();
|
||||||
|
if (res.message) {
|
||||||
|
setLocalIp(res.local_ip || '172.17.99.208');
|
||||||
|
if (!subnet) {
|
||||||
|
const ipParts = (res.local_ip || '172.17.99.208').split('.');
|
||||||
|
setSubnet(`${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.0/24`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setLocalIp('未知');
|
||||||
|
notify.error({ title: '获取后端信息失败' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchLocalInfo();
|
||||||
|
}, [subnet, notify]);
|
||||||
|
|
||||||
|
const handleScan = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await api.scan(subnet);
|
||||||
|
setDevices(res.devices || []);
|
||||||
|
} catch (err) {
|
||||||
|
notify.error({ title: '扫描网络失败' });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLastScan = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await api.listDevices();
|
||||||
|
setDevices(res.devices || []);
|
||||||
|
} catch (err) {
|
||||||
|
notify.error({ title: '获取上次扫描记录失败' });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentTitle title={'网络扫描'}>
|
||||||
|
<DashboardBackground />
|
||||||
|
<PageContainer>
|
||||||
|
<FadeInWrapper delay={0.3} yOffset={-5}>
|
||||||
|
<VStack spacing={8} align={'stretch'}>
|
||||||
|
<Box
|
||||||
|
p={6}
|
||||||
|
bg={'whiteAlpha.100'}
|
||||||
|
borderRadius={'xl'}
|
||||||
|
border={'1px solid'}
|
||||||
|
borderColor={'whiteAlpha.300'}
|
||||||
|
mx={4}
|
||||||
|
transition={'all 0.2s'}
|
||||||
|
_hover={{ transform: 'translateY(-4px)' }}
|
||||||
|
>
|
||||||
|
<Heading fontSize={'2xl'} mb={4} color={'teal.300'}>
|
||||||
|
{'网络扫描'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<HStack mb={4}>
|
||||||
|
<Text fontWeight={'medium'}>{'后端服务器IP: '}</Text>
|
||||||
|
<Badge colorPalette={'blue'}>{localIp}</Badge>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack mb={4} spacing={4}>
|
||||||
|
<Input
|
||||||
|
placeholder={'输入子网 (如 192.168.1.0/24)'}
|
||||||
|
value={subnet}
|
||||||
|
onChange={(e) => setSubnet(e.target.value)}
|
||||||
|
width={'300px'}
|
||||||
|
bg={'whiteAlpha.200'}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleScan}
|
||||||
|
isLoading={loading}
|
||||||
|
colorPalette={'teal'}
|
||||||
|
variant={'solid'}
|
||||||
|
>
|
||||||
|
{'开始扫描'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleLastScan}
|
||||||
|
isLoading={loading}
|
||||||
|
colorPalette={'blue'}
|
||||||
|
variant={'outline'}
|
||||||
|
>
|
||||||
|
{'显示上次结果'}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<HStack>
|
||||||
|
<Spinner />
|
||||||
|
<Text>{'正在加载,请稍候...'}</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && devices.length > 0 && (
|
||||||
|
<Table.Root variant={'outline'} striped={'true'} size={'md'} mt={4}>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeader>{'IP 地址'}</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>{'MAC 地址'}</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>{'开放端口'}</Table.ColumnHeader>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{devices.map((d) => (
|
||||||
|
<Table.Row key={d.ip}>
|
||||||
|
<Table.Cell>{d.ip}</Table.Cell>
|
||||||
|
<Table.Cell>{d.mac}</Table.Cell>
|
||||||
|
<Table.Cell>{d.ports.join(', ')}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && devices.length === 0 && (
|
||||||
|
<Text color={'gray.400'}>{'暂无扫描结果,请执行扫描。'}</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
</FadeInWrapper>
|
||||||
|
</PageContainer>
|
||||||
|
</DocumentTitle>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScanPage;
|
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* 获取config
|
||||||
|
* @returns {any|null}
|
||||||
|
*/
|
||||||
|
export const getConfig = () => {
|
||||||
|
const cfg = localStorage.getItem('connection-config');
|
||||||
|
return cfg ? JSON.parse(cfg) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取后端url
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const getBaseUrl = () => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
return cfg?.backendUrl || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetchUrl
|
||||||
|
* @param path 路径
|
||||||
|
* @param options 选项
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
const fetchWithBase = async (path, options = {}) => {
|
||||||
|
const base = getBaseUrl();
|
||||||
|
const res = await fetch(`${base}${path}`, options);
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* api模块
|
||||||
|
* @type {{test: (function(): Promise<*>), scan: (function(*): Promise<*>), listDevices: (function(): Promise<*>), parseCommand: (function(*): Promise<*>), applyConfig: (function(*, *): Promise<*>)}}
|
||||||
|
*/
|
||||||
|
export const api = {
|
||||||
|
test: () => fetchWithBase('/api/test'),
|
||||||
|
scan: (subnet) => fetchWithBase(`/api/scan_network?subnet=${encodeURIComponent(subnet)}`),
|
||||||
|
listDevices: () => fetchWithBase('/api/list_devices'),
|
||||||
|
parseCommand: (text) =>
|
||||||
|
fetchWithBase('/api/parse_command', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ command: text }),
|
||||||
|
}),
|
||||||
|
applyConfig: (switch_ip, config) =>
|
||||||
|
fetchWithBase('/api/apply_config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ switch_ip, config }),
|
||||||
|
}),
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user