Compare commits

...

2 Commits

Author SHA1 Message Date
921b17e3f5 需修改 2025-06-20 14:02:07 +08:00
ae8786f297 添加api调用模块 2025-06-20 13:34:15 +08:00
8 changed files with 254 additions and 23 deletions

View File

@ -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={() => {

View File

@ -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' },
]; ];
/** /**

View File

@ -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 = () =>

View File

@ -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}

View File

@ -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>

View 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;

View File

@ -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 }),
}),
};