mirror of
https://github.com/Jerryplusy/AI-powered-switches.git
synced 2025-07-04 05:09:19 +00:00
Compare commits
2 Commits
3070c676f8
...
223d43f0d9
Author | SHA1 | Date | |
---|---|---|---|
223d43f0d9 | |||
093c7423e5 |
@ -7,6 +7,7 @@ import githubIcon from '@/resources/icon/pages/weclome/github.svg';
|
|||||||
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
||||||
import MotionCard from '@/components/ui/MotionCard';
|
import MotionCard from '@/components/ui/MotionCard';
|
||||||
import ConfigTool from '@/libs/config/ConfigTool';
|
import ConfigTool from '@/libs/config/ConfigTool';
|
||||||
|
import Notification from '@/libs/system/Notification';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: '面板', path: '/dashboard' },
|
{ label: '面板', path: '/dashboard' },
|
||||||
@ -46,6 +47,10 @@ const GithubTransitionCard = () => {
|
|||||||
setTestMode(checked);
|
setTestMode(checked);
|
||||||
const config = ConfigTool.load();
|
const config = ConfigTool.load();
|
||||||
ConfigTool.save({ ...config, testMode: checked });
|
ConfigTool.save({ ...config, testMode: checked });
|
||||||
|
let mode = checked ? '调试模式' : '常规模式';
|
||||||
|
Notification.success({
|
||||||
|
title: `成功切换至${mode}!`,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const MotionBox = motion(Box);
|
const MotionBox = motion(Box);
|
||||||
@ -109,9 +114,9 @@ const GithubTransitionCard = () => {
|
|||||||
{item.label}
|
{item.label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
<HStack spacing={4} ml={'auto'}>
|
<HStack spacing={4} ml={'right'}>
|
||||||
<Text fontSize={'sm'} color={'white'}>
|
<Text fontSize={'sm'} color={'white'}>
|
||||||
{'测试模式'}
|
{'调试模式'}
|
||||||
</Text>
|
</Text>
|
||||||
<Switch.Root
|
<Switch.Root
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
AiFillWarning,
|
AiFillWarning,
|
||||||
AiFillCheckCircle,
|
AiFillCheckCircle,
|
||||||
AiFillExclamationCircle,
|
AiFillExclamationCircle,
|
||||||
|
AiOutlineLoading,
|
||||||
} from 'react-icons/ai';
|
} from 'react-icons/ai';
|
||||||
|
|
||||||
const surfaceStyle = {
|
const surfaceStyle = {
|
||||||
@ -16,7 +17,13 @@ const surfaceStyle = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用通知组件
|
* 通用通知组件
|
||||||
* @type {{info({title: *, description: *, button: *}): void, success({title: *, description: *, button: *}): void, warn({title: *, description: *, button: *}): void, error({title: *, description: *, button: *}): void}}
|
* @type {{
|
||||||
|
* info({title: *, description: *, button: *}): void,
|
||||||
|
* success({title: *, description: *, button: *}): void,
|
||||||
|
* warn({title: *, description: *, button: *}): void,
|
||||||
|
* error({title: *, description: *, button: *}): void,
|
||||||
|
* promise({promise: *, loading: *, success: *, error: *}): void
|
||||||
|
* }}
|
||||||
*/
|
*/
|
||||||
const Notification = {
|
const Notification = {
|
||||||
/**
|
/**
|
||||||
@ -57,6 +64,38 @@ const Notification = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步操作通知
|
||||||
|
* @param {Object} params 参数对象
|
||||||
|
* @param {Promise} params.promise 异步操作Promise
|
||||||
|
* @param {Object} params.loading 加载中状态配置
|
||||||
|
* @param {Object} params.success 成功状态配置
|
||||||
|
* @param {Object} params.error 错误状态配置
|
||||||
|
* @returns {{id: string | undefined, unwrap: () => Promise<unknown>}}
|
||||||
|
*/
|
||||||
|
async promise({ promise, loading, success, error }) {
|
||||||
|
return toaster.promise(promise, {
|
||||||
|
loading: {
|
||||||
|
title: loading.title,
|
||||||
|
description: loading.description,
|
||||||
|
icon: <AiOutlineLoading className={'animate-spin'} size={24} />,
|
||||||
|
style: surfaceStyle,
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
title: success.title,
|
||||||
|
description: success.description,
|
||||||
|
icon: <AiFillCheckCircle size={24} />,
|
||||||
|
style: surfaceStyle,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: error.title,
|
||||||
|
description: error.description,
|
||||||
|
icon: <AiFillWarning size={24} />,
|
||||||
|
style: surfaceStyle,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 警告
|
* 警告
|
||||||
* @param title 标题
|
* @param title 标题
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Box, VStack, Heading, Textarea, Button, Select, Text } from '@chakra-ui/react';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
createListCollection,
|
||||||
|
Field,
|
||||||
|
Heading,
|
||||||
|
HStack,
|
||||||
|
Portal,
|
||||||
|
Select,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
VStack,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import DocumentTitle from '@/components/system/pages/DocumentTitle';
|
import DocumentTitle from '@/components/system/pages/DocumentTitle';
|
||||||
import PageContainer from '@/components/system/PageContainer';
|
import PageContainer from '@/components/system/PageContainer';
|
||||||
import DashboardBackground from '@/components/system/pages/DashboardBackground';
|
import DashboardBackground from '@/components/system/pages/DashboardBackground';
|
||||||
@ -7,44 +19,184 @@ import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
|||||||
import ConfigTool from '@/libs/config/ConfigTool';
|
import ConfigTool from '@/libs/config/ConfigTool';
|
||||||
import { api } from '@/services/api/api';
|
import { api } from '@/services/api/api';
|
||||||
import Notification from '@/libs/system/Notification';
|
import Notification from '@/libs/system/Notification';
|
||||||
|
import Common from '@/libs/common';
|
||||||
|
|
||||||
|
const testMode = ConfigTool.load().testMode;
|
||||||
|
|
||||||
const ConfigPage = () => {
|
const ConfigPage = () => {
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [selectedDevice, setSelectedDevice] = useState('');
|
const [selectedDevice, setSelectedDevice] = useState('');
|
||||||
const [inputText, setInputText] = useState('');
|
const [inputText, setInputText] = useState('');
|
||||||
const [parsedConfig, setParsedConfig] = useState('');
|
const [parsedConfig, setParsedConfig] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [editableConfig, setEditableConfig] = useState('');
|
||||||
|
const [applying, setApplying] = useState(false);
|
||||||
|
const [hasParsed, setHasParsed] = useState(false);
|
||||||
|
|
||||||
|
const deviceCollection = createListCollection({
|
||||||
|
items: devices.map((device) => ({
|
||||||
|
label: `${device.name} (${device.ip})`,
|
||||||
|
value: device.ip,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const config = ConfigTool.load();
|
const config = ConfigTool.load();
|
||||||
setDevices(config.devices || []);
|
setDevices(config.devices || []);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const generateRealisticConfig = (command, devices = []) => {
|
||||||
|
const timestamp = new Date().toLocaleString();
|
||||||
|
let config = `! 配置生成于 ${timestamp}\n`;
|
||||||
|
const cmd = command.toLowerCase();
|
||||||
|
// VLAN 配置
|
||||||
|
if (cmd.includes('vlan')) {
|
||||||
|
const vlanIdMatch = command.match(/vlan\s*(\d+)/i);
|
||||||
|
const vlanId = vlanIdMatch?.[1] || '10';
|
||||||
|
const isMgmt = cmd.includes('管理') || cmd.includes('mgmt');
|
||||||
|
config +=
|
||||||
|
`vlan ${vlanId}\n` +
|
||||||
|
` name ${isMgmt ? 'MGMT' : 'USER'}_VLAN\n` +
|
||||||
|
` exit\n` +
|
||||||
|
`interface Vlan${vlanId}\n` +
|
||||||
|
` description ${isMgmt ? 'Management VLAN' : 'User VLAN'}\n` +
|
||||||
|
` ip address 192.168.${vlanId}.1 255.255.255.0\n` +
|
||||||
|
` exit\n`;
|
||||||
|
}
|
||||||
|
// SSH 配置
|
||||||
|
if (cmd.includes('ssh') || cmd.includes('安全') || cmd.includes('登录')) {
|
||||||
|
const password = Math.random().toString(36).slice(2, 10);
|
||||||
|
config +=
|
||||||
|
`ip ssh server\n` +
|
||||||
|
`ip ssh version 2\n` +
|
||||||
|
`username admin privilege 15 secret 0 ${password}\n` +
|
||||||
|
`line vty 0 4\n` +
|
||||||
|
` transport input ssh\n` +
|
||||||
|
` login local\n` +
|
||||||
|
` exit\n`;
|
||||||
|
}
|
||||||
|
// 端口配置
|
||||||
|
if (cmd.includes('端口') || cmd.includes('接口') || cmd.includes('port')) {
|
||||||
|
const portMatch = command.match(/端口\s*(\d+)/i) || command.match(/port\s*(\d+)/i);
|
||||||
|
const port = portMatch?.[1] || '1';
|
||||||
|
const isTrunk = cmd.includes('trunk');
|
||||||
|
const isAccess = cmd.includes('access') || !isTrunk;
|
||||||
|
const desc = cmd.includes('上联') || cmd.includes('uplink') ? 'Uplink_Port' : 'Access_Port';
|
||||||
|
const vlanId = '10';
|
||||||
|
config +=
|
||||||
|
`interface GigabitEthernet0/${port}\n` +
|
||||||
|
` description ${desc}\n` +
|
||||||
|
` switchport mode ${isTrunk ? 'trunk' : 'access'}\n` +
|
||||||
|
` ${isTrunk ? 'switchport trunk allowed vlan all' : `switchport access vlan ${vlanId}`}\n` +
|
||||||
|
` no shutdown\n` +
|
||||||
|
` exit\n`;
|
||||||
|
}
|
||||||
|
// ACL 配置
|
||||||
|
if (cmd.includes('acl') || cmd.includes('访问控制') || cmd.includes('防火墙')) {
|
||||||
|
let targetIP = '192.168.10.10';
|
||||||
|
if (devices.length > 0) {
|
||||||
|
const randomDevice = devices[Math.floor(Math.random() * devices.length)];
|
||||||
|
targetIP = randomDevice.ip;
|
||||||
|
}
|
||||||
|
config +=
|
||||||
|
`ip access-list extended PROTECT_SERVERS\n` +
|
||||||
|
` permit tcp any host ${targetIP} eq 22\n` +
|
||||||
|
` permit tcp any host ${targetIP} eq 80\n` +
|
||||||
|
` deny ip any any\n` +
|
||||||
|
` exit\n` +
|
||||||
|
`interface Vlan10\n` +
|
||||||
|
` ip access-group PROTECT_SERVERS in\n` +
|
||||||
|
` exit\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.trim() === `! 配置生成于 ${timestamp}`) {
|
||||||
|
config += '! 当前命令未识别到任何可配置项目\n';
|
||||||
|
}
|
||||||
|
return { config };
|
||||||
|
};
|
||||||
|
|
||||||
const handleParse = async () => {
|
const handleParse = async () => {
|
||||||
if (!selectedDevice || !inputText) {
|
if (!selectedDevice || !inputText.trim()) {
|
||||||
Notification.error({ title: '请选择设备并输入配置指令' });
|
Notification.error({
|
||||||
|
title: '操作失败',
|
||||||
|
description: '请选择设备并输入配置指令',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const res = await api.parseCommand(inputText);
|
const performParse = async () => {
|
||||||
setParsedConfig(res.config);
|
if (testMode) {
|
||||||
} catch (e) {
|
await Common.sleep(800 + Math.random() * 700);
|
||||||
Notification.error({ title: '配置解析失败' });
|
return generateRealisticConfig(inputText);
|
||||||
|
}
|
||||||
|
return await api.parseCommand(inputText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultWrapper = await Notification.promise({
|
||||||
|
promise: performParse(),
|
||||||
|
loading: {
|
||||||
|
title: '正在解析配置',
|
||||||
|
description: '正在分析您的指令...',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
title: '解析完成',
|
||||||
|
description: '已生成交换机配置',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: '解析失败',
|
||||||
|
description: '请检查指令格式或网络连接',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await resultWrapper.unwrap();
|
||||||
|
|
||||||
|
if (result?.config) {
|
||||||
|
setParsedConfig(result.config);
|
||||||
|
setEditableConfig(result.config);
|
||||||
|
setHasParsed(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('配置解析异常:', error);
|
||||||
|
Notification.error({
|
||||||
|
title: '配置解析异常',
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApply = async () => {
|
const handleApply = async () => {
|
||||||
if (!parsedConfig) return;
|
if (!editableConfig.trim()) {
|
||||||
setLoading(true);
|
Notification.warn({
|
||||||
try {
|
title: '配置为空',
|
||||||
await api.applyConfig(selectedDevice, parsedConfig);
|
description: '请先解析或编辑有效配置',
|
||||||
Notification.success({ title: '配置已成功应用!' });
|
});
|
||||||
} catch (e) {
|
return;
|
||||||
Notification.error({ title: '配置应用失败' });
|
}
|
||||||
|
|
||||||
|
setApplying(true);
|
||||||
|
try {
|
||||||
|
const applyOperation = testMode
|
||||||
|
? Common.sleep(1000).then(() => ({ success: true }))
|
||||||
|
: await api.applyConfig(selectedDevice, editableConfig);
|
||||||
|
|
||||||
|
await Notification.promise({
|
||||||
|
promise: applyOperation,
|
||||||
|
loading: {
|
||||||
|
title: '配置应用中',
|
||||||
|
description: '正在推送配置到设备...',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
title: '应用成功',
|
||||||
|
description: '配置已成功生效',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: '应用失败',
|
||||||
|
description: '请检查设备连接或配置内容',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setApplying(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -52,57 +204,118 @@ const ConfigPage = () => {
|
|||||||
<DashboardBackground />
|
<DashboardBackground />
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<FadeInWrapper delay={0.3} yOffset={-5}>
|
<FadeInWrapper delay={0.3} yOffset={-5}>
|
||||||
<VStack spacing={8} align={'stretch'}>
|
<VStack spacing={6} align={'stretch'}>
|
||||||
<Heading fontSize={'2xl'} color={'teal.300'}>
|
<Heading fontSize={'xl'} color={'teal.300'}>
|
||||||
{'交换机配置中心'}
|
交换机配置中心
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Select
|
<Field.Root>
|
||||||
placeholder={'请选择交换机设备'}
|
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
|
||||||
value={selectedDevice}
|
选择交换机设备
|
||||||
onChange={(e) => setSelectedDevice(e.target.value)}
|
</Field.Label>
|
||||||
bg={'whiteAlpha.200'}
|
<Select.Root
|
||||||
>
|
collection={deviceCollection}
|
||||||
{devices.map((device) => (
|
value={selectedDevice ? [selectedDevice] : []}
|
||||||
<option key={device.ip} value={device.ip}>
|
onValueChange={({ value }) => setSelectedDevice(value[0] ?? '')}
|
||||||
{device.name} ({device.ip})
|
placeholder={'请选择交换机设备'}
|
||||||
</option>
|
size={'sm'}
|
||||||
))}
|
colorPalette={'teal'}
|
||||||
</Select>
|
>
|
||||||
|
<Select.HiddenSelect />
|
||||||
|
<Select.Control>
|
||||||
|
<Select.Trigger>
|
||||||
|
<Select.ValueText />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.IndicatorGroup>
|
||||||
|
<Select.Indicator />
|
||||||
|
<Select.ClearTrigger />
|
||||||
|
</Select.IndicatorGroup>
|
||||||
|
</Select.Control>
|
||||||
|
<Portal>
|
||||||
|
<Select.Positioner>
|
||||||
|
<Select.Content>
|
||||||
|
{deviceCollection.items.map((item) => (
|
||||||
|
<Select.Item key={item.value} item={item}>
|
||||||
|
{item.label}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</Select.Root>
|
||||||
|
</Field.Root>
|
||||||
|
|
||||||
<Textarea
|
<Field.Root>
|
||||||
rows={6}
|
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
|
||||||
placeholder={'输入自然语言配置指令...'}
|
配置指令输入
|
||||||
value={inputText}
|
</Field.Label>
|
||||||
onChange={(e) => setInputText(e.target.value)}
|
<Textarea
|
||||||
bg={'whiteAlpha.200'}
|
rows={4}
|
||||||
/>
|
placeholder={'例子:创建VLAN 10并配置IP 192.168.10.1/24并在端口1启用SSH访问"'}
|
||||||
|
value={inputText}
|
||||||
|
onChange={(e) => setInputText(e.target.value)}
|
||||||
|
bg={'whiteAlpha.200'}
|
||||||
|
size={'sm'}
|
||||||
|
/>
|
||||||
|
</Field.Root>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
colorPalette={'teal'}
|
colorScheme={'teal'}
|
||||||
variant={'solid'}
|
variant={'solid'}
|
||||||
isLoading={loading}
|
size={'sm'}
|
||||||
onClick={handleParse}
|
onClick={handleParse}
|
||||||
|
isDisabled={!selectedDevice || !inputText.trim()}
|
||||||
>
|
>
|
||||||
{'解析配置'}
|
解析配置
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{parsedConfig && (
|
{hasParsed && (
|
||||||
<Box
|
<FadeInWrapper>
|
||||||
p={4}
|
<Box
|
||||||
bg={'whiteAlpha.100'}
|
p={4}
|
||||||
borderRadius={'xl'}
|
bg={'whiteAlpha.100'}
|
||||||
border={'1px solid'}
|
borderRadius={'xl'}
|
||||||
borderColor={'whiteAlpha.300'}
|
border={'1px solid'}
|
||||||
>
|
borderColor={'whiteAlpha.300'}
|
||||||
<Text fontWeight={'bold'} mb={2}>
|
>
|
||||||
{'已生成配置:'}
|
<Text fontWeight={'bold'} mb={2} fontSize="sm">
|
||||||
</Text>
|
生成配置:
|
||||||
<Textarea value={parsedConfig} rows={8} readOnly />
|
</Text>
|
||||||
<Button mt={4} colorPalette={'teal'} onClick={handleApply}>
|
<Textarea
|
||||||
{'应用到交换机'}
|
value={editableConfig}
|
||||||
</Button>
|
rows={12}
|
||||||
</Box>
|
onChange={(e) => setEditableConfig(e.target.value)}
|
||||||
|
fontFamily={'monospace'}
|
||||||
|
size={'sm'}
|
||||||
|
bg={'blackAlpha.200'}
|
||||||
|
/>
|
||||||
|
<HStack mt={4} spacing={3} justify={'flex-end'}>
|
||||||
|
<Button
|
||||||
|
variant={'outline'}
|
||||||
|
colorScheme={'gray'}
|
||||||
|
size={'sm'}
|
||||||
|
onClick={() => {
|
||||||
|
setEditableConfig(parsedConfig);
|
||||||
|
Notification.success({
|
||||||
|
title: '成功重置配置!',
|
||||||
|
description: '现在您可以重新审查生成的配置',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重置为原始配置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme={'teal'}
|
||||||
|
size={'sm'}
|
||||||
|
onClick={handleApply}
|
||||||
|
isLoading={applying}
|
||||||
|
isDisabled={!editableConfig.trim()}
|
||||||
|
>
|
||||||
|
应用到交换机
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</FadeInWrapper>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
</FadeInWrapper>
|
</FadeInWrapper>
|
||||||
|
@ -31,18 +31,21 @@ const Dashboard = () => {
|
|||||||
if (res) {
|
if (res) {
|
||||||
setNetworkStatus('ok');
|
setNetworkStatus('ok');
|
||||||
console.log(JSON.stringify(res));
|
console.log(JSON.stringify(res));
|
||||||
Notification.info({ title: '成功连接至后端服务!' });
|
Notification.info({ title: '成功连接至后端服务!', description: res.message });
|
||||||
} else {
|
} else {
|
||||||
setNetworkStatus('fail');
|
setNetworkStatus('fail');
|
||||||
Notification.error({ title: '后端服务响应异常!' });
|
Notification.error({ title: '后端服务响应异常!', description: JSON.stringify(res) });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setNetworkStatus('fail');
|
setNetworkStatus('fail');
|
||||||
Notification.error({ title: '无法连接到后端服务!' });
|
Notification.error({ title: '无法连接到后端服务!', description: err.message });
|
||||||
}
|
}
|
||||||
}, [Notification]);
|
}, [Notification]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
Notification.info({
|
||||||
|
title: '正在尝试连接后端服务',
|
||||||
|
});
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
checkBackend();
|
checkBackend();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
@ -17,6 +17,8 @@ import DashboardBackground from '@/components/system/pages/DashboardBackground';
|
|||||||
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
||||||
import ConfigTool from '@/libs/config/ConfigTool';
|
import ConfigTool from '@/libs/config/ConfigTool';
|
||||||
import Common from '@/libs/common';
|
import Common from '@/libs/common';
|
||||||
|
import switchIcon from '@/resources/icon/pages/devices/switch.png';
|
||||||
|
import Notification from '@/libs/system/Notification';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 交换机管理
|
* 交换机管理
|
||||||
@ -41,9 +43,12 @@ const DevicesPage = () => {
|
|||||||
const handleSave = (idx) => {
|
const handleSave = (idx) => {
|
||||||
const updated = [...devices];
|
const updated = [...devices];
|
||||||
updated[idx].name = editingName;
|
updated[idx].name = editingName;
|
||||||
|
Common.sleep(200);
|
||||||
setDevices(updated);
|
setDevices(updated);
|
||||||
ConfigTool.save({ ...ConfigTool.load(), devices: updated });
|
ConfigTool.save({ ...ConfigTool.load(), devices: updated });
|
||||||
Common.sleep(500);
|
Notification.success({
|
||||||
|
title: '设备重命名成功!',
|
||||||
|
});
|
||||||
setEditingIndex(null);
|
setEditingIndex(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,7 +74,7 @@ const DevicesPage = () => {
|
|||||||
transition={'all 0.3s ease'}
|
transition={'all 0.3s ease'}
|
||||||
_hover={{ transform: 'translateY(-8px)', bg: 'whiteAlpha.200' }}
|
_hover={{ transform: 'translateY(-8px)', bg: 'whiteAlpha.200' }}
|
||||||
>
|
>
|
||||||
<Image src={'/assets/switch.png'} alt={'Switch'} borderRadius={'md'} mb={3} />
|
<Image src={switchIcon} alt={'Switch'} borderRadius={'md'} mb={3} />
|
||||||
|
|
||||||
<Collapsible.Root open={editingIndex === idx}>
|
<Collapsible.Root open={editingIndex === idx}>
|
||||||
<Collapsible.Trigger asChild>
|
<Collapsible.Trigger asChild>
|
||||||
|
@ -36,9 +36,9 @@ const ScanPage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLocalInfo = async () => {
|
const fetchLocalInfo = async () => {
|
||||||
setLocalIp('172.17.99.208');
|
setLocalIp('192.168.1.100');
|
||||||
if (!subnet) {
|
if (!subnet) {
|
||||||
const ipParts = '172.17.99.208'.split('.');
|
const ipParts = '192.168.1.0'.split('.');
|
||||||
setSubnet(`${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.0/24`);
|
setSubnet(`${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.0/24`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -65,9 +65,15 @@ const ScanPage = () => {
|
|||||||
if (testMode) {
|
if (testMode) {
|
||||||
await Common.sleep(2000);
|
await Common.sleep(2000);
|
||||||
scanDevices = getTestDevices();
|
scanDevices = getTestDevices();
|
||||||
|
Notification.success({
|
||||||
|
title: '扫描子网设备成功!',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const res = await api.scan(subnet);
|
const res = await api.scan(subnet);
|
||||||
scanDevices = res.devices || [];
|
scanDevices = res.devices || [];
|
||||||
|
Notification.success({
|
||||||
|
title: '扫描子网设备成功!',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
scanDevices = scanDevices.map((d, idx) => ({
|
scanDevices = scanDevices.map((d, idx) => ({
|
||||||
@ -97,8 +103,11 @@ const ScanPage = () => {
|
|||||||
try {
|
try {
|
||||||
let scanDevices;
|
let scanDevices;
|
||||||
if (testMode) {
|
if (testMode) {
|
||||||
await Common.sleep(2000);
|
await Common.sleep(500);
|
||||||
scanDevices = getTestDevices();
|
scanDevices = getTestDevices();
|
||||||
|
Notification.success({
|
||||||
|
title: '获取上一次扫描记录成功!',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const res = await api.listDevices();
|
const res = await api.listDevices();
|
||||||
scanDevices = res.devices || [];
|
scanDevices = res.devices || [];
|
||||||
|
BIN
src/frontend/src/resources/icon/pages/devices/switch.png
Normal file
BIN
src/frontend/src/resources/icon/pages/devices/switch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
1
src/frontend/src/resources/icon/pages/devices/switch.svg
Normal file
1
src/frontend/src/resources/icon/pages/devices/switch.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24"><path fill="#41d7e1" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8M6.5 9L10 5.5L13.5 9H11v4H9V9zm11 6L14 18.5L10.5 15H13v-4h2v4z"/></svg>
|
After Width: | Height: | Size: 298 B |
Loading…
x
Reference in New Issue
Block a user